WePY | 小程式元件化開發框架
在說說WePY框架之前,先小結一下自己對前端工作的基本認識。對普通的前端開發者來講,做到心中有數,方能得心應手。在工作中遇到問題時,能快速地定位到是環境層面的問題?還是開發層面的問題?還是效能優化層面的問題?這很重要,因為快速地定位問題是找到問題根源的前提,進而做到寵辱不驚。我通常把日常工作拆解為如下三大模組的任務:
任務1:基於(nodejs/npm/webpack/eslint/editorconfig/git)等工具搭建工程開發環境,以保證我們可以在開發環境中使用ES6、TypeScript、Less等更加便捷的進階技術,可以使用npm對依賴庫進行管理,可以使用eslint進行程式碼規範等。所謂“工欲善其事,必先利其器”,小到程式碼編輯器的選擇、外掛的選擇、程式碼高亮等;大到構建出合理的工程結構目錄以便於程式碼管理,並實現開發環境下熱更新、代理服務,前端自動化等。
任務2:藉助於前端框架(Vue/React/Angular/Wepy)等實現MVVM元件化開發,靈活使用這些框架所提供的特色功能,如元件的設計與複用、元件例項的生命週期與鉤子函式、元件之間的資料傳遞與事件通訊、Slot內容分發插槽,Mixin混合、資料動態繫結、事件繫結、列表迴圈渲染、表單輸入與取值、條件渲染、動態樣式等。在這個任務下,要深刻理解當前所選用框架的核心特點並靈活使用。特別是元件的設計與封裝,需要花工夫精心設計,這不僅需要開發者對框架特色深入理解,還需要開發者對產品、UI和資料介面的綜合考慮;可擴充套件性、靈活性,是元件設計的核心。
任務3:最終使用webpack打包工具進行打包,程式碼優化,效能優化、部署上線等。但凡涉及到這方面的話題,就比較複雜,大多數情況下我們使用框架配套的命令列工具可以構建出標準模板的專案目錄以及打包配置,比如Vue的 vue-cli,Wepy的 wepy-cli等。打包優化常常使用的就是Webpack這個神奇而牛逼的工具了。
前端工作亂如麻,希望一點小結能幫助自己再次梳理工作中的主要內容,對知識進行對比性學習,不盲目崇拜任何一項技術,不跟風;而應該結合專案的實際需求靈活地選擇框架和技術。大家都說前端框架大同小異,我也希望有時間和機會能深入地研究其中之一的原始碼,通過原始碼分析幫助自己更加深入地理解前端開發工作。好了,就扯淡這麼多了,接下來就分享一下“關於WePY框架入門使用”所需要掌握的一些基本知識點。
1、安裝Wepy腳手架工具
在 nodejs / npm 環境下安裝 wepy-cli :
npm install wepy-cli -g
使用 wepy-cli 建立專案標準模板:
wepy init standard project_name cd project_name npm install npm run dev// 執行開發環境
專案打包:
npm run build
2、認識 Wepy專案的目錄結構

wepy專案的目錄結構
其中,/src/components/ 目錄用於放置我們自定義的元件,/src/pages/ 目錄用於放置程式中的頁面。app.wpy 是程式的入口檔案,放置著各種配置項。WePY框架對原生小程式開發進行了二次封裝,將更加貼近於MVVM架構模式。
注意(重要):應在WePY專案的根目錄新增 project.config.json檔案,它的作用如下圖示:

3、程式碼編輯器和除錯環境
建議使用 atom 編輯器進行開發,配置程式碼高亮的方法如下:

atom
除錯環境直接使用“微信開發者工具”,除錯前得先申請一個小程式賬號,需要 appid才能開啟WePY小程式專案。
4、WePY程式碼規範
開頭的。
2)程式入口檔案、元件檔案、頁面檔案,都以 .wpy 為字尾。其它外鏈檔案不受限制。
3)要使用 ES6 語法進行開發,整個WePY框架就是在 ES6 下開發的。
4)WePY 支援 Promise,可以直接在程式中使用 async / await 等新特性。
5)小程式原生開發中的事件繫結,如 bindtap="click",在 WePY框架下要替換成 @tap="click";原生的 catchtap="click",在WePY框架下要替換成 @tap.stop="click"。更多關於事件繫結的變化,請參見WePY官網之“元件自定義事件”。
6)小程式原生開發中的事件傳參 bindtap="click" data-item={{item}} ,在WePY框架下優化成了 @tap="click({{item}})",更加簡潔了。
7)自定義元件的命名,應該避免與微信原生元件名稱相同。
5、元件化開發模式
WePY支援元件化開發模式,檔案模板如下:
// demo.wpy <template> <view>HTML部分</view> </template> <script> import wepy from 'wepy'; export default class Demo extends wepy.page { } </script> <style lang="less"> </style>
從上述檔案模板看,其程式碼組織與 Vue 元件化開發模式幾乎一模一樣。把小程式原生開發中的 demo.wxml、demo.wxss、demo.json、demo.js 四個檔案合併成了一個 demo.wpy 檔案。

.wpy檔案的編譯過程
三種標籤分別支援哪些語法?如下圖示:

三種標籤所支援的語法
6、小程式入口檔案 app.wpy 與 小程式例項
<script> import wepy from 'wepy' import 'wepy-async-function' export default class extends wepy.app { config = { pages: [ 'pages/demo', ], window: { navigationStyle: 'custom' } } globalData = { userInfo: null, } constructor () { super() this.use('requestfix') console.log('app例項', this) } onLaunch() { // 測試非同步 this.testAsync() wepy.login({ success (res) { console.log('登入', res) } }) } // 獲取使用者資訊 getUserInfo(cb) { const that = this if (this.globalData.userInfo) { return this.globalData.userInfo } wepy.getUserInfo({ success (res) { that.globalData.userInfo = res.userInfo cb && cb(res.userInfo) } }) } // 測試非同步 async testAsync () { const data = await this.sleep(3) console.log('async', data) } sleep (s) { return new Promise((resolve, reject) => { setTimeout(() => { resolve('promise resolved') }, s * 1000) }) } } </script>
入口檔案app.wpy中所宣告的小程式例項繼承自wepy.app類,包含一個config屬性和其它全域性屬性、方法、事件。其中config屬性對應原生的app.json檔案,build編譯時會根據config屬性自動生成app.json檔案,如果需要修改config中的內容,請使用微信提供的相關API。
在頁面例項中,可以通過 this.$parent
來訪問 App 例項;在子元件中可以使用 this.$parent
訪問父元件例項。
7、頁面例項 與 元件例項
<template> </ template> <script> import wepy from 'wepy'; export default class MyPage extends wepy.page {// 定義頁面 export default class MyComponent extends wepy.component {// 定義元件 customData = {}// 自定義資料 customFunction () {}//自定義方法 onLoad () {}// 在Page和Component共用的生命週期函式 onShow () {}// 只在Page中存在的頁面生命週期函式 config = {};// 只在Page例項中存在的配置資料,對應於原生的page.json檔案 data = {};// 頁面所需資料均需在這裡宣告,可用於模板資料繫結 components = {};// 宣告頁面中所引用的元件,或宣告元件中所引用的子元件 mixins = [];// 宣告頁面所引用的Mixin例項 computed = {};// 宣告計算屬性(詳見後文介紹) watch = {};// 宣告資料watcher(詳見後文介紹) methods = {};// 宣告頁面wxml中標籤的事件處理函式。注意,此處只用於宣告頁面wxml中標籤的bind、catch事件,自定義方法需以自定義方法的方式宣告 events = {};// 宣告元件之間的事件處理函式 } <script /> <style lang="less"></ style>

屬性解讀
這裡需要特別強調一下:WePY中的methods屬性只能宣告頁面 template 標籤中的@ 事件所需要的繫結方法,不能宣告自定義方法,切記。
在 WePY 中,小程式被分為三個例項:小程式例項App、頁面例項Page、元件例項Component。其中Page例項繼承自Component。它們各自的宣告方式如下:
import wepy from 'wepy'; // 宣告一個App小程式例項 export default class MyAPP extends wepy.app { } // 宣告一個Page頁面例項 export default class IndexPage extends wepy.page { } // 宣告一個Component元件例項 export default class MyComponent extends wepy.component { }
8、computed 計算屬性
computed計算屬性,是一個有返回值的函式,可直接被當作繫結資料來使用。因此類似於data屬性,程式碼中可通過this.計算屬性名來引用,模板中也可通過{{ 計算屬性名 }}來繫結資料。當元件中有任何資料發生了改變,那麼所有計算屬性就都會被重新計算。示例程式碼如下:
<template> <view>{{aPlus}}</view> </template> <script> import wepy from 'wepy'; export default class Demo extends wepy.page { data = { a: 1 } // 計算屬性aPlus,在指令碼中可通過this.aPlus來引用,在模板中可通過{{ aPlus }}來插值 computed = { aPlus () { return this.a + 1 } } } </script>
9、watcher 監聽器
監聽器watcher能夠監聽到任何屬性的更新。監聽器在watch屬性中宣告,型別為函式,函式名與需要被監聽的data物件中的屬性同名,每當被監聽的屬性改變一次,監聽器函式就會被自動呼叫執行一次。監聽器適用於當屬性改變時需要進行某些額外處理的情景。示例程式碼如下:
<script> import wepy from 'wepy'; export default class Demo extends wepy.page { data = { num: 1 } // 監聽器函式名必須跟需要被監聽的data物件中的屬性num同名。 // 其引數中的newValue為屬性改變後的新值,oldValue為改變前的舊值。 watch = { num (newValue, oldValue) { console.log(`num value: ${oldValue} -> ${newValue}`) } } // 每當被監聽的屬性num改變一次,對應的同名監聽器函式num()就被自動呼叫執行一次 onLoad () { setInterval(() => { this.num++; this.$apply(); }, 1000) } } </script>
注意:監聽器函式名必須跟需要被監聽的data物件中的屬性num同名。
10、模組化與作用域
在WePY中實現了小程式的元件化、模組化開發,元件的所有業務與功能在元件本身內部實現,元件與元件之間是彼此隔離的。比如在兩個不同元件中定義了兩個同名的事件方法,基於模組化的設計,那麼這兩個同名的事件方法事實上是沒有任何關聯的,它們互不影響。WePY編譯元件的過程如下:

元件編譯過程
11、列表迴圈渲染
在WePY中,使用了 <repeat> 標籤替代了原生小程式中的 wx:for 迴圈渲染。示例程式碼如下:
<template> <!-- 注意,使用for屬性,而不是使用wx:for屬性 --> <repeat for="{{list}}" key="index" index="index" item="item"> <!-- 插入<script>指令碼部分所宣告的child元件,同時傳入item --> <child :item="item"></child> </repeat> </template>
12、在元件或頁面中引用其它元件
<template> <view> <!-- 在 --> <child1></child1> <child2></child2> <child3></child3> </view> </template> <script> import wepy from 'wepy'; //引入元件檔案 import Child from '../components/child'; export default class Index extends wepy.component { //宣告元件,分配元件id為child components = { //為三個相同元件的不同例項分配不同的元件ID,從而避免資料同步變化的問題 child1: Child, child2: Child, child3: Child }; } </script>
當元件或頁面中需要引入其它元件時,必須在.wpy檔案的<script>指令碼部分先import元件檔案,然後在components屬性中給元件宣告唯一的元件ID,接著在<template>模板部分中新增以components物件中所宣告的元件ID進行命名的自定義標籤以插入元件。
需要注意的是,WePY中的元件都是靜態元件,是以元件ID作為唯一標識的,每一個ID都對應一個元件例項,當頁面引入兩個相同ID的元件時,這兩個元件共用同一個例項與資料,當其中一個元件資料變化時,另外一個也會一起變化。為了避免這個問題,則需要分配多個元件ID和例項,如上述程式碼中同一個元件的三個ID —— child1、child2、child3。
頁面可以引入元件,而元件還可以引入子元件。一個頁面引入若干元件後,元件結構如下圖(元件樹):

元件樹
13、父子元件之間的 props 傳值
props傳值在WePY中屬於父子元件之間傳值的一種機制,包括靜態傳值與動態傳值。
1)靜態傳值
靜態傳值只能從父元件向子元件傳遞常量資料,且為String字串型別。在子元件使用props物件屬性來接收從父元件傳遞過來的值。示例程式碼如下:
在父元件中:
<template> <childname="geekxia"></child> </template> <script> import wepy from 'wepy'; import Child from '../components/child'; export default class Parent extends wepy.page { components = { child: Child } } </script>
在子元件中使用 props 物件屬性接收父元件的傳值:
<template> <view>{{name}}</view> </template> <script> import wepy from 'wepy'; export default class Child exntends wepy.component { props = { name: String,// 接收傳值 } } </script>
2)動態傳值
動態傳值是指父元件向子元件傳遞動態資料,父子元件資料完全獨立且互不干擾。所謂動態傳值,即傳給子元件的資料不是常量,而是變數,且在父子元件內部分別發生變化時,不會影響到另一方的變化。
當在父元件中使用 .sync修飾符的props傳值,在父元件中改變這個props傳值時,會同步改變子元件中這個對應的值。(即由父元件向子元件中流動)
當在子元件的 props物件屬性中使用 twoWay: true 修飾所要傳遞的值,在子元件中改變這個props傳值時,會同步改變父元件中這個對應的傳值。(即由子元件向父元件中流動)
當在父元件中使用 .sync修飾,並同時在子元件中使用 twoWay:true 修飾時,可以實現父子元件之間的資料雙向繫結,即這個要傳遞的值,在父子元件任意一方的變化都會同步更新到另一方。如下示例
父元件程式碼如下:
<template> <view> <child :name1.sync="name1" :name2="nam2" :name3.sync="name3"></child> </view> </template> <script> import wepy from 'wepy'; import Child from '../components/child'; export default class Parent extends wepy.page { components = { child: Child } data = { name1: '1', name2: '2', nmae3: '3' } } </script>
子元件程式碼如下:
<template> <view> <view>{{name1}}</view> <view>{{name2}}</view> <view>{{name3}}</view> </view> </template> <script> import wepy from 'wepy'; export default class Child exntends wepy.component { props = { // 父元件向子元件 單向傳值 name1: { type: String, default: "geekxia", twoWay: false,// 預設是 false }, // 子元件向父元件 單向傳值 name2: { type: String, default: "geekxia", twoWay: true }, // 資料雙向繫結 name3: { type: String, default: "geekxia", twoWay: true } } } </script>
如上示例,當在父元件中改變 name1 時,子元件中的name1將會同步改變;當在子元件中改變 name2時,父元件中的 name2 將會同步改變。當父子元件其中任意一方改變 name3時,另一方中的 name3 將會同步改變。
14、元件之間的事件通訊與互動
WePY 提供了 $broadcast、$emit、$invoke
三個方法用於元件之間的通訊和互動。注意:用於元件之間事件通訊與互動的事件處理函式要寫在元件和頁面的events物件屬性中去。
1) $broadcast
事件機制
$broadcast
事件是由父元件發起,所有子元件都會收到此廣播事件,除非事件被手動取消。事件廣播的順序為廣度優先搜尋順序,如上圖,如果頁面Page_Index發起一個 $broadcast
事件,那麼按先後順序依次接收到該事件的元件為:ComA、ComB、ComC、ComD、ComE、ComF、ComG、ComH。如下圖:

$broadcast事件機制
2) $emit
事件機制
$emit
與 $broadcast
正好相反,事件發起元件的所有祖先元件會依次接收到 $emit
事件。如果元件ComE發起一個 $emit
事件,那麼接收到事件的先後順序為:元件ComA、頁面Page_Index。如下圖:

$emit 事件機制
3) $invoke
直接呼叫另一個元件中 event 事件
$invoke
是一個頁面或元件對另一個元件中的方法的直接呼叫,通過傳入元件路徑找到相應的元件,然後再呼叫其方法。如,在元件ComA中直接呼叫元件ComG的某個方法:
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');
15、三種事件修飾符與自定義事件
1).default 繫結小程式冒泡型事件,即原生中的 bindXXX,.default 預設可以省略不寫。
2).stop 繫結小程式捕獲型事件,即原生中的 catchXXX。
3).user 繫結自定義事件,並且自定義事件只能在子元件中通過 $emit
來觸發。
使用以上三個修飾符所修飾的事件,都必須寫在 methods 物件屬性中。如果寫在了 event 物件屬性中,將不會被觸發。示例如下
父元件程式碼如下:
<template> <view> <!-- 繫結冒泡事件 --> <view @tap.default="bindEvent"></view> <!-- 繫結捕獲事件 --> <view @tap.stop="bindEvent"></view> <!-- 給子元件繫結 自定義事件 --> <child @customFn.user="customEvent"></child> </view> </template> <script> import wepy from 'wepy'; import Child from '../components/child'; export default class Parent extends wepy.page { components = { child: Child } methods = { bindEvent() { console.log('冒泡事件'); }, catchEvent() { console.log('捕獲事件'); }, customEvent(arg1, arg2) { console.log('自定義事件,且只能在子元件中使用 $emit 來觸發'); console.log(arg1, arg2); } } } </script>
子元件程式碼如下:
<template> <view> <view @tap="click"></view> </view> </template> <script> import wepy from 'wepy'; export default class Child exntends wepy.component { methods = { click() { console.log('這裡是子元件中的事件'); // 觸發父元件中的自定義事件 $emit("customFn", "arg1", "arg2"); } } } </script>
從上述程式碼可以看出,.default用於控制冒泡事件,.stop用於控制捕獲事件,.user用於定義自定義事件(自定義事件用於在子元件中觸發)。事件修飾符非常有用,要理解它們的基本使用。
16、slot 內容分發插槽
WePY中的slot插槽作為內容分發標籤的空間佔位標籤,便於在父元件中通過對相當於擴充套件板卡的內容分發標籤的“插拔”,更為靈活、方便地對子元件進行內容分發。示例如下
定義Modal彈框子元件,並使用 slot 插槽分發彈框的頭部、內容體和按鈕組:
<template> <!-- 彈框下面的遮罩層 --> <view class="layer"> <!-- 彈框 --> <view class="modal" style="height:{{height}}rpx;margin-top:{{-0.5*height}}rpx;"> <image class="modal-close" src="../assets/image/icon/close.png" @tap="closeModal"></image> <view class="modal-title"> <!-- 彈框的頭部插槽 --> <slot name="title"></slot> </view> <view class="modal-content"> <!-- 彈框的內容體插槽 --> <slot name="content"></slot> </view> <view class="modal-btns"> <!-- 彈框的按鈕組插槽 --> <slot name="btns"></slot> </view> </view> </view> </template> <script> import wepy from 'wepy'; export default class Modal extends wepy.component { props = { height: String } methods = { // 關閉Modal closeModal() { this.$emit('closeModal'); }, } } </script>
在父組中使用這個Modal 彈框子元件:
<template> <view> <modal hidden="{{modalHide}}" height="492" @closeModal.user="closeModal"> <view slot="title"> <text>更換手機號</text> </view> <view slot="content"> <view> <input class="full" type="text" placeholder="請輸入手機號" /> </view> <view> <input class="half" type="text" placeholder="簡訊驗證碼" /> <text>獲取驗證碼</text> </view> </view> <view slot="btns"> <text class="one-btn">確定</text> </view> </modal> </view> </template> <script> import wepy from 'wepy'; import Modal from '../components/modal'; export default class Parent extends wepy.page { components = { modal: Modal } data = { modalHide: true, } methods = { closeModal() { this.modalHide = true; } } } </script>
從上述程式碼可見,使用 slot 內容插槽可以靈活方便地封裝 UI 元件。如 Modal 彈框中的內容體可以因實際情景而不同,slot 就派上了大用場。
17、Mixin 混合
Mixin 混合可以把元件之間的可複用部分抽離出來,從而在元件中使用Mixin 混合時,可以將混合的資料、事件以及方法注入到元件之中。Mixin 混合又為兩種模式,分別是預設式混合和相容式混合。程式碼如下
定義一個 mixin ,程式碼如下:
import wepy from 'wepy' export default class testMixin extends wepy.mixin { data = { foo: 'foo from mixin', bar: 'bar from mixin' } methods = { click () { console.log('tap from mixin'); } } onShow() { console.log('mixin -> onShow') } }
在元件或頁面中使用 Mixin ,程式碼如下:
<template> <view @tap="click"></view> </template> <script> import wepy from 'wepy'; import testMixin from '../mixin/test.js'; export default class Demo extends wepy.page { mixins = [testMixin] data = { foo: 'foo from index' } methods = { click () { console.log('tap from index'); } } onShow () { console.log(this.foo);// foo from index console.log(this.bar);// bar from mixin } } </script>
對於元件的 data、components、events以及methods中的.user自定義事件,都將採用預設式的Mixin混合,即元件中優先使用自身定義的資料。如果元件中未定義這些資料時,將使用Mixin混合中的資料。
對於元件的 methods 中非自定義事件(即小程式頁面中的響應事件),則採用相容式的 Mixin混合,即先響應元件自身事件,再接著響應從Mixin中混合而來的事件。另需說明,當元件中引入了Mixin混合時,同樣是元件的 onShow / onLoad 函式先執行,Mixin 的 onShow / onLoad 後執行;這個順序與頁面事件的執行順序是一致的,即相容式Mixin混合。
18、資料繫結的變化
在原生小程式中,使用 this.setData() 來繫結資料,如下程式碼:
this.setData({title: 'this is title'});
WePY使用髒資料檢查對setData進行封裝,在函式執行週期結束時執行髒資料檢查,一來可以不用關心頁面多次setData是否會有效能上的問題,二來可以更加簡潔去修改資料實現繫結,不用重複去寫setData方法。資料繫結像如下做:
this.title = 'this is title';
需注意的是,在非同步函式中更新資料的時候,必須手動呼叫$apply方法,才會觸發髒資料檢查流程的執行。示例如下:
setTimeout(() => { this.title = 'this is title'; this.$apply(); }, 3000);
髒資料檢查的流程,如下示意圖:

髒檢查流程
19、事件傳參的變化
原生小程式中的事件傳參,通常做法如下:
<view data-id="{{index}}" data-title="wepy" data-other="otherparams" bindtap="click"></view> Page({ click: function (e) { console.log(e.currentTarget.dataset.id); console.log(e.currentTarget.dataset.title); console.log(e.currentTarget.dataset.other); } });
在WePY優化之後可以更加方便地為事件傳遞引數了,示例如下:
<template> <view @tap="click({{index}},{{item}},"1")"></view> </template> <script> import wepy from 'wepy'; export default class Demo extends wepy.page { methods = { click (index, item, arg, e) { console.log(index); console.log(item); console.log(arg); console.log(e); } } } </script>
20、Intercept 攔截器
在WePY中提供了全域性的 intercept 攔截器,以對原生API的請求進行攔截。具體做法是配置 API 的config、fail、success、complete回撥函式。示例程式碼如下:
import wepy from 'wepy'; export default class extends wepy.app { constructor () { // this is not allowed before super() super(); // 攔截 request 請求 this.intercept('request', { // 發出請求時的回撥函式 config (p) { // 對所有request請求中的OBJECT引數物件統一附加時間戳屬性 p.timestamp = +new Date(); console.log('config request: ', p); // 必須返回OBJECT引數物件,否則無法傳送請求到服務端 return p; }, // 請求成功後的回撥函式 success (p) { // 可以在這裡對收到的響應資料物件進行加工處理 console.log('request success: ', p); // 必須返回響應資料物件,否則後續無法對響應資料進行處理 return p; }, //請求失敗後的回撥函式 fail (p) { console.log('request fail: ', p); // 必須返回響應資料物件,否則後續無法對響應資料進行處理 return p; }, // 請求完成時的回撥函式(請求成功或失敗都會被執行) complete (p) { console.log('request complete: ', p); } }); } }
上述程式碼,我們對小程式原生 API —— request() 進行攔截處理。在 request 的前、中、後分別做了相應的邏輯處理,可以理解成把一個任務拆解成多個階段,並在不同的階段中去實現我們想做的特定任務。
至此關於 WePY的基礎使用,就整理到這裡。這些基本的知識點,在各大MVVM框架中或多或小都有使用到。學會使用一個框架,其它框架可謂大同小異。把每一個技術點產生的原因,為了解決什麼問題搞明白了,這將有助於我們舉一反三,靈活地使用各個技術點,在專案開發過程中做到寵辱不驚,以不變應千變。
END 2018年12月1日