微信小程序組件化開發(fā)框架文檔
2017年5月22日,微信小程序推出之時(shí),限制非常多,現(xiàn)在越來越開放。微信小程序開發(fā)文檔也受到越來越多人的關(guān)注,下面從多個(gè)方面來談?wù)勎⑿判〕绦蜷_發(fā)文檔的一些內(nèi)容。
小程序框架wepy文檔
成品DEMO展示
一個(gè)是使用 wepy new demo
命令生成的標(biāo)準(zhǔn)demo
一個(gè)是基于wepy開發(fā)的手機(jī)充值的完整demo
一個(gè)是基于wepy開發(fā)的開源的仿微信的聊天界面 (源代碼下載)
以上三個(gè)demo均在安卓機(jī)和IOS機(jī)上運(yùn)行過通。
快速入門
代碼規(guī)范:
1 . 變量與方法使用盡量使用駝峰式命名,避免使用 $
開頭。
以 $
開頭的方法或者屬性為框架內(nèi)建方法或者屬性,可以被使用,使用前請(qǐng)參考 API文檔 。
2 .入口,頁(yè)面,組件的命名后綴為 .wpy
。外鏈的文件可以是其它后綴。
請(qǐng)參考 wpy文件說明
3 .使用ES6語法開發(fā)。
框架在ES6下開發(fā),因此也需要使用ES6開發(fā)小程序,ES6中有大量的語法糖可以讓我們的代碼更加簡(jiǎn)潔高效。
4 .使用Promise
框架默認(rèn)對(duì)小程序提供的API全都進(jìn)行了 Promis小程序第三方平臺(tái)e 處理,甚至可以直接使用async/await等新特性進(jìn)行開發(fā)。
項(xiàng)目創(chuàng)建與使用
安裝wepy
以下安裝都通過npm安裝
注:在騰訊開發(fā)網(wǎng),因?yàn)閚pm需要使用代理才可以,沒使用npm代理的同學(xué)建議使用騰訊內(nèi)部工具tnmp代替npm安裝,tnpm的安裝與使用請(qǐng)參考 alsotan g的文章。
1 .安裝 wepy 命令行工具
npm install wepy-cli -g
2 .在開發(fā)目錄生成開發(fā)DEMO
wepy new myproject
3 .開發(fā)實(shí)時(shí)編譯
wepy build --watch
項(xiàng)目目錄結(jié)構(gòu)
dist node_modules src components com_a.wpy com_b.wpy pages index.wpy page2.wpy app.wpy package.json
開發(fā)使用說明
1 .使用微信開發(fā)者工具新建項(xiàng)目,本地開發(fā)選擇dist目錄。
2 .微信開發(fā)者工具 —> 項(xiàng)目 —> 關(guān)閉ES6轉(zhuǎn)ES5。
3 .本地項(xiàng)目根目錄運(yùn)行wepy build --watch,開啟實(shí)時(shí)編譯。
主要解決問題:
1. 開發(fā)模式轉(zhuǎn)換
在原有的小程序的開發(fā)模式下進(jìn)行再次封裝,更貼近于現(xiàn)有MVVM框架開發(fā)模式??蚣茉陂_發(fā)過程中參考了一些現(xiàn)在框架的一些特性,并且融入其中,以下是使用wep小程序制作y前后的代碼對(duì)比圖。
官方DEMO代碼:
//index.js //獲取應(yīng)用實(shí)例 var app = getApp() Page({ data: { motto: 'Hello World', userInfo: {} }, //事件處理函數(shù) bindViewTap: function() { console.log('button clicked') }, onLoad: function () { console.log('onLoad') } })
基于wepy的實(shí)現(xiàn):
import wepy from 'wepy'; export default class Index extends wepy.page { data = { motto: 'Hello World', userInfo: {} }; methods = { bindViewTap () { console.log('button clicked'); } }; onLoad() { console.log('onLoad'); }; }
2. 支持組件化開發(fā)
參見章節(jié): 組件
示例代碼:
// index.wpy <template> <view> <component id="pannel" path="pannel"></component> <component id="counter1" path="counter"></component> <component id="counter2" path="counter"></component> <component id="list" path="list"></component> </view> </template> <script> import wepy from 'wepy'; import List from '../components/list'; import Panel from '../components/panel'; import Counter from '../components/counter'; export default class Index extends wepy.page { config = { "navigationBarTitleText": "test" }; components = { panel: Panel, counter1: Counter, counter2: Counter, list: List }; } </script>
3. 支持加載外部NPM包。
在編譯過程當(dāng)中,會(huì)遞歸遍歷代碼中的require然后將對(duì)應(yīng)依賴文件從node_modules當(dāng)中拷貝出來,并且修改require為相對(duì)路徑,從而實(shí)現(xiàn)對(duì)外部NPM包的支持。如下圖:
4. 單文件模式,使得目錄結(jié)構(gòu)更加清晰。
官方目錄結(jié)構(gòu) 要求app必須有三個(gè)文件app.json,app.js,app.wxss,頁(yè)面有4個(gè)文件 index.json,index.js,index.wxml,index.wxss。而且文件必須同名。
所以使用wepy開發(fā)前后開發(fā)目錄對(duì)比如下:
官方DEMO:
project pages index index.json index.js index.wxml index.wxss log log.json log.wxml log.js log.wxss app.js app.json app.wxss
使用wepy框架后目錄結(jié)構(gòu):
project src pages index.wpy log.wpy app.wpy
默認(rèn)使用babel編譯,支持ES6/7的一些新特性。
用戶可以通過修改wepy.config.js配置文件,配置自己熟悉的babel環(huán)境進(jìn)行開發(fā)。默認(rèn)開啟使用了一些新的特性如promise,async/await等等。
示例代碼:
import wepy from 'wepy'; export default class Index extends wepy.page { getData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve({data: 123}); }, 3000); }); }; async onLoad() { let data = await this.getData(); console.log(data.data); }; }
5. 針對(duì)原生API進(jìn)行優(yōu)化
對(duì)現(xiàn)在API進(jìn)行promise處理,同時(shí)修復(fù)一些現(xiàn)有API的缺陷,比如:wx.request并發(fā)問題等。
原有代碼:
onLoad = function () { var self = this; wx.login({ success: function (data) { wx.getUserInfo({ success: function (userinfo) { self.setData({userInfo: userinfo}); } }); } }); }
基于wepy實(shí)現(xiàn)代碼:
async onLoad() { await wx.login(); this.userInfo = await wx.getUserInfo(); }
在同時(shí)并發(fā)10個(gè)request請(qǐng)求測(cè)試時(shí):
不使用wepy:
使用wepy后:
進(jìn)階說明
wepy.config.js 配置文件說明
執(zhí)行wepy new demo后,會(huì)生成類似配置文件。
let prod = process.env.NODE_ENV === 'production'; module.exports = { "wpyExt": ".wpy", "babel": { "presets": [ "es2015", "stage-1" ], "plugins": [ "transform-export-extensions", "syntax-export-extensions", "transform-runtime" ] } }; if (prod) { // 壓縮sass module.exports['sass'] = {"outputStyle": "compressed"}; // 壓縮less module.exports['less'] = {"compress": true}; // 壓縮js module.exports.plugins = { 'UglifyJsPlugin': { filter: /\.js$/, config: { compress: { warning: false } } }, 'TestPlugin': { filter: /\.wxml$/, config: { } } }; }
wpyExt:缺省值為’.wpy’,IDE默認(rèn)情況下不會(huì)對(duì)此文件類型高亮,此時(shí)可以修改所有文件為.vue后綴(因?yàn)榕cvue高亮規(guī)則一樣),然后將此選項(xiàng)修改為.vue,就能解決部分IDE代碼高亮問題。
sa小程序設(shè)計(jì)ss:sass編譯配置,參見 這里
less:less編譯配置,參見 這里
babel:babel編譯配置,參見 這里
plugins:plugins為1.1.6版本之后功能,目前支持js壓縮與圖片壓縮,持續(xù)開發(fā)……
wpy文件說明
wpy文件的編譯過程過下:
一個(gè) .wpy
文件分為三個(gè)部分:
樣式 <style></style>
對(duì)應(yīng)原有wxss
模板 <template></template>
對(duì)應(yīng)原有wxml
代碼 <script></script>
對(duì)應(yīng)原有js
其中入口文件app.wpy不需要template,所以編譯時(shí)會(huì)被忽略。這三個(gè)標(biāo)簽都支持type和src屬性,type決定了其代碼編譯過程,src決定是否外聯(lián)代碼,存在src屬性且有效時(shí),忽略內(nèi)聯(lián)代碼,示例如下:
<style type="less" src="page1.less"></style> <template type="wxml" src="page1.wxml"></template> <script> // some code </script>
標(biāo)簽對(duì)應(yīng) type 值如下表所示:
|標(biāo)簽|type默認(rèn)值|type支持值| |—|—|—| |style|css|css,less,sass(待完成)| |template|wxml|wxml,xml,html(待完成)| |script|js|js,TypeScript(待完成)|
script說明
程序入口app.wpy
<style type="less"> /** less **/ </style> <script> import wepy from 'wepy'; export default class extends wepy.app { config = { "pages":[ "pages/index/index" ], "window":{ "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" } }; onLaunch() { console.log(this); } } </script>
入口app.wpy繼承自wepy.app,包含一個(gè)config屬性和其全局屬性、方法、事件。其中config屬性對(duì)應(yīng)原有的app.json,編譯時(shí)會(huì)根據(jù)config生成app.json文件,如果需要修改config中的內(nèi)容,請(qǐng)使用系統(tǒng)提供API。
頁(yè)面index.wpy
<style type="less"> /** less **/ </style> <template type="wxml"> <view> </view> <component id="counter1" path="counter"></component> </template> <script> import wepy form 'wepy'; import Counter from '../components/counter'; export default class Index extends wepy.page { config = {}; components = {counter1: Counter}; data = {}; methods = {}; events = {}; onLoad() {}; // Other properties } </script>
頁(yè)面入口繼承自wepy.page,主要屬性說明如下:
組件com.wpy
<style type="less"> /** less **/ </style> <template type="wxml"> <view> </view> </template> <script> import wepy form 'wepy'; export default class Com extends wepy.component { components = {}; data = {}; methods = {}; events = {}; // Other properties } </script>
頁(yè)面入口繼承自wepy.component,屬性與頁(yè)面屬性一樣,除了不需要config以及頁(yè)面特有的一些小程序事件等等。
組件
在小程序中,可以利用 JS模塊化 和 wxml模板 ,對(duì)業(yè)務(wù)模塊進(jìn)行劃分,實(shí)現(xiàn)如下效果:
但實(shí)際上不同的模塊代碼與事件交互都是在同一個(gè)頁(yè)面空間處理的,比如說 moduleA 和 moduleB 中同時(shí)存在一個(gè) add 響應(yīng)事件時(shí),就需要在 html 和 js 中分別定義為 moduleA_add,moduleB_add。業(yè)務(wù)模塊復(fù)雜之后就不利于開發(fā)和維護(hù)。
在wepy中,利用組件化的特性可以解決此類問題,如下圖:
ComA 和 ComB中間的數(shù)據(jù)與事件相互隔離,可以分別擁有自己的add事件。
組件引用
當(dāng)頁(yè)面或者組件需要引入子組件時(shí),需要在頁(yè)面或者script中的components給組件分配唯一id,并且在template中添加 <component>
標(biāo)簽,如index.wpy
Index頁(yè)面引入A,B,C三個(gè)組件,同時(shí)組件A和B又有自己的子組件D,E,F(xiàn),G,H。
組件通信與交互
wepy.component基類提供三個(gè)方法$broadcast,$emit,$invoke,因此任一頁(yè)面或任一組件都可以調(diào)用上述三種方法實(shí)現(xiàn)通信與交互,如:
$this.$emit('some-event', 1, 2, 3, 4);
組件的事件監(jiān)聽需要寫在events屬性下,如:
import wepy form 'wepy'; export default class Com extends wepy.component { components = {}; data = {}; methods = {}; events = { 'some-event': ($event, ...args) { console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`); } }; // Other properties }
1 . $broadcast
$broadcast事件是由父組件發(fā)起,所有子組件都會(huì)收到此廣播事件,除非事件被手動(dòng)取消。事件廣播的順序?yàn)閺V度優(yōu)先搜索順序,如果Page_Index發(fā)起一個(gè)$broadcast事件,那么接收到事件的先后順序?yàn)椋篈, B, C, D, E, F, G, H。如下圖:
2 .$emit
$emit與$broadcast正好相反,事件發(fā)起組件的父組件會(huì)依次接收到$emit事件,如上圖,如果E發(fā)起一個(gè)$emit事件,那么接收到事件的先后順序?yàn)椋篈, Page_Index。如下圖:
3 .$invoke
$i微信小程序 apinvoke是一個(gè)組件對(duì)另一個(gè)組件的直接調(diào)用,通過傳入的組件路徑找到相應(yīng)組件,然后再調(diào)用其方法。
如果想在Page_Index中調(diào)用組件A的某個(gè)方法:
this.$invoke('ComA', 'someMethod', 'someArgs');
如果想在組件A中調(diào)用組件G的某個(gè)方法:
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');
混合
混合可以將組之間的可復(fù)用部分抽離,從而在組件中使用混合時(shí),可以將混合的數(shù)據(jù),事件以及方法注入到組件之中?;旌戏址譃閮煞N:
-
默認(rèn)式混合
-
兼容式混合
默認(rèn)式混合
對(duì)于組件data數(shù)據(jù),components組件,events事件以及其它自定義方法采用 默認(rèn)式混合 ,即如果組件未聲明該數(shù)據(jù),組件,事件,自定義方法等,那么將混合對(duì)象中的選項(xiàng)將注入組件這中。對(duì)于組件已聲明的選項(xiàng)將不受影響。
// mixins/test.js import wepy from 'wepy'; export default class TestMixin extends wepy.page { data = { foo: 'foo defined by page', bar: 'bar defined by testMix' }; methods: { tap () { console.log('mix tap'); } } } // pages/index.wpy import wepy from 'wepy'; import TestMixin from './mixins/test'; export default class Index extends wepy.mixin { data = { foo: 'foo defined by index' }; mixins = [TestMixin ]; onShow() { console.log(this.foo); // foo defined by index. console.log(this.bar); // foo defined by testMix. } }
兼容式混合
對(duì)于組件methods響應(yīng)事件,以及小程序頁(yè)面事件將采用兼容式混合,即先響應(yīng)組件本身響應(yīng)事件,然后再響應(yīng)混合對(duì)象中響應(yīng)事件。
// mixins/test.js import wepy from 'wepy'; export default class TestMixin extends wepy.page { methods = { tap () { console.log('mix tap'); } }; onShow() { console.log('mix onshow'); } } // pages/index.wpy import wepy from 'wepy'; import TestMixin from './mixins/test'; export default class Index extends wepy.mixin { mixins = [TestMixin]; methods = { tap () { console.log('index tap'); } }; onShow() { console.log('index onshow'); } } // index onshow // mix onshow // ----- when tap // index tap // mix tap
數(shù)據(jù)綁定
小程序數(shù)據(jù)綁定方式
小程序通過Page提供的setData方法去綁定數(shù)據(jù),如:
this.setData({title: 'this is title'});
因?yàn)樾〕绦蚣軜?gòu)本身原因,頁(yè)面渲染層和JS邏輯層分開的,setData操作實(shí)際就是JS邏輯層與頁(yè)面渲染層之間的通信,那么如果在同一次運(yùn)行周期內(nèi)多次執(zhí)行setData操作時(shí),那么通信的次數(shù)是一次還是多次呢?經(jīng)過跟小程序團(tuán)隊(duì)確認(rèn)后得知多次setData會(huì)執(zhí)行多次通信。
wepy數(shù)據(jù)綁定方式
wepy使用臟數(shù)據(jù)檢查對(duì)setData進(jìn)行封裝,在函數(shù)運(yùn)行周期結(jié)束時(shí)執(zhí)行臟數(shù)據(jù)檢查,一來可以不用關(guān)心頁(yè)面多次setData是否會(huì)有性能上的問題,二來可以更加簡(jiǎn)潔去修改數(shù)據(jù)實(shí)現(xiàn)綁定,不用重復(fù)去寫setData方法。代碼如下:
this.title = 'this is title';
但需注意,在函數(shù)運(yùn)行周期之外的函數(shù)里去修改數(shù)據(jù)需要手動(dòng)調(diào)用$apply方法。如:
setTimeout(() => { this.title = 'this is title'; this.$apply(); }, 3000);
wepy臟數(shù)據(jù)檢查流程
在執(zhí)行臟數(shù)據(jù)檢查是,會(huì)通過this.$$phase標(biāo)識(shí)當(dāng)前檢查狀態(tài),并且會(huì)保證在并發(fā)的流程當(dāng)中,只會(huì)有一個(gè)臟數(shù)據(jù)檢查流程在運(yùn)行,以下是執(zhí)行臟數(shù)據(jù)檢查的流程圖:
其它優(yōu)化細(xì)節(jié)
1. wx.request 接收參數(shù)修改
點(diǎn)這里查看 官方文檔
// 官方 wx.request({ url: 'xxx', success: function (data) { console.log(data); } }); // wepy 使用方式 // request 接口從只接收Object變?yōu)榭山邮誗tring wx.request('xxxx').then((d) => console.log(d));
2. 優(yōu)化事件參數(shù)傳遞
點(diǎn)這里查看 官方文檔
// 官方 <view id="tapTest" data-hi="WeChat" bindtap="tapName"> Click me! </view> Page({ tapName: function(event) { console.log(event.currentTarget.hi)// output: WeChat } }); // wepy 建議傳參方式 <view id="tapTest" data-wepy-params="1-wepy-something" bindtap="tapName"> Click me! </view> events: { tapName (event, id, title, other) { console.log(id, title, other)// output: 1, wepy, something } }
3. 改變數(shù)據(jù)綁定方式
保留setData方法,但不建議使用setData執(zhí)行綁定,修復(fù)傳入undefined的bug,并且修改入?yún)⒅С郑?/p>
-
this.setData(target, value)
-
this.setData(object)
點(diǎn)這里查看 官方文檔
// 官方 <view> {{ message }} </view> onLoad: function () { this.setData({message: 'hello world'}); } // wepy <view> {{ message }} </view> onLoad () { this.message = 'hello world'; }
4. 組件代替模板和模塊
點(diǎn)這里查看 官方文檔
// 官方 <!-- item.wxml --> <template name="item"> <text>{{text}}</text> </template> <!-- index.wxml --> <import src="item.wxml"/> <template is="item" data="{{text: 'forbar'}}"/> <!-- index.js --> var item = require('item.js') // wepy <!-- /components/item.wpy --> <text>{{text}}</text> <!-- index.wpy --> <template> <component id="item"></component> </template> <script> import wepy from 'wepy'; import Item from '../componen制作小程序ts/item'; export default class Index extends wepy.page { components = { Item } } </script>
API
wepy.event
wepy.component
wepy.page
wepy.app
CHANGELOG
1.3.1 (2016-12-16)
-
新增對(duì)第三方Compiler的支持
-
新增pug編譯器
-
重新整理代碼結(jié)構(gòu),使用lerna維護(hù)不同的NPM包
-
重新處理Plugins,同樣交由第三方包處理
-
添加了編譯時(shí)檢測(cè)依賴的Compiler或者Plugins是否缺失的邏輯,如果缺失會(huì)自行安裝
如何制作小程序
-
添加了cli工具版本檢測(cè)的功能
1.1.9 (2016-12-14)
-
新增了wepy upgrade命令升級(jí)wepyjs版本
-
新增對(duì)第三方組件的支持
-
新增第三方組件[wepy-com-toast]
-
模板中添加toast組件測(cè)試
1.1.8 (2016-12-08)
-
修復(fù)了script使用src外鏈報(bào)錯(cuò)的BUG
-
修復(fù)了LESS編譯會(huì)調(diào)用到SASS的BUG
-
優(yōu)化了事件傳參數(shù),支持直接傳參
-
加入了Travis-CI以及Coveralls
-
修復(fù)其它細(xì)節(jié)BUG問題
1.1.7 (2016-12-06)
-
script/template/style的屬性同時(shí)支持type和lang
-
添加mixins支持
1.1.6 (2016-12-02)
-
修復(fù)了組件ID大寫導(dǎo)致無法識(shí)別的問題
-
添加了對(duì)小程序頁(yè)面所有響應(yīng)事件的支持
-
修改wepy.config.js支持plugins
-
添加UglifyJsPlugin,在編譯時(shí)對(duì)生成的所有JS文件進(jìn)行壓縮
-
添加ImageMinPlugin(不推薦使用,處理大圖片時(shí)還有問題)
-
添加wepy build --no-cache參數(shù),編譯時(shí)會(huì)重新編譯所有依賴文件
-
wepy new demo時(shí),由在當(dāng)前目錄下生成項(xiàng)目改為創(chuàng)建demo目錄,然后再生成項(xiàng)目
-
更新生成demo支持最新功能
1.1.4 (2016-12-01)
-
添加了小程序其它頁(yè)面事件的支持
-
修改默認(rèn)配置文件.wepyrc為wepy.config.js,方便以后功能擴(kuò)展。(兼容老配置文件
)
1.1.3 (2016-11-28)
-
修復(fù)SASS編譯異常導(dǎo)致watch結(jié)束的BUG
-
修復(fù)子組件修改時(shí)不會(huì)觸發(fā)父組件更新的BUG
-
修復(fù)$invoke('../')的BUG
-
修復(fù)頁(yè)面onLoad事件中傳參的BUG
1.1.1 (2016-11-23)
-
添加了對(duì)sass/scss的編譯支持
-
.wepyrc中加入對(duì)less/sass的配置支持
-
.wepyrc中添加wpyExt選項(xiàng)
-
更新生成模板
hishop微信小程序可以實(shí)現(xiàn)一鍵開通微信小程序,結(jié)合移動(dòng)云商城,可以實(shí)現(xiàn)七大端口的線上和線下結(jié)合模式。
第二部分:如何開通一個(gè)小商店