Labrador 是一個專爲微信小程序開發的組件化開發框架。css
使用Labrador框架可使微信開發者工具支持加載海量NPM包node
支持ES6/7標準代碼,使用async/await可以有效避免回調地獄git
組件重用,對微信小程序框架進行了二次封裝,實現了組件重用和嵌套github
自動化測試,很是容易編寫單元測試腳本,不經任何額外配置便可自動化測試web
使用Editor Config及ESLint標準化代碼風格,方便團隊協做npm
首先您的系統中安裝Node.js和npm v3 下載Node.js,而後運行下面的命令將全局安裝Labrador命令行工具。json
npm install -g labrador-cli
mkdir demo # 新建目錄 cd demo # 跳轉目錄 npm init # 初始化npm包 labrador init # 初始化labrador項目
demo # 項目根目錄 ├── .labrador # Labrador項目配置文件 ├── .babelrc # babel配置文件 ├── .editorconfig # Editor Config ├── .eslintignore # ESLint 忽略配置 ├── .eslintrc # ESLint 語法檢查配置 ├── package.json ├── dist/ # 目標目錄 ├── node_modules/ └── src/ # 源碼目錄 ├── app.js ├── app.json ├── app.less ├── components/ # 通用組件目錄 ├── pages/ # 頁面目錄 └── utils/
注意 dist目錄中的全部文件是由labrador命令編譯生成,請勿直接修改小程序
項目初始化後使用WebStorm或Sublime等你習慣的IDE打開項目根目錄。而後打開 微信web開發者工具 新建項目,本地開發目錄選擇 dist
目標目錄。segmentfault
在WebStorm或Sublime等IDE中編輯 src
目錄下的源碼,而後在項目根目錄中運行labrador build
命令構建項目,而後在 微信web開發者工具 的調試界面中點擊左側菜單的 重啓 按鈕便可查看效果。微信小程序
咱們在開發中, 微信web開發者工具 僅僅用來作調試和預覽,不要在 微信web開發者工具 的編輯界面修改代碼。
微信web開發者工具 會偶爾出錯,表現爲點擊 重啓 按鈕沒有反應,調試控制檯輸出大量的沒法require文件的錯誤,編輯 界面中代碼文件不顯示。這是由於
labrador build
命令會更新整個dist
目錄,而 微信web開發者工具 在監測代碼改變時會出現異常,遇到這種狀況只須要關掉 微信web開發者工具 再啓動便可。
咱們還可使用 labrador watch
命令來監控 src
目錄下的代碼,當發生改變後自動構建,不用每一次編輯代碼後手動運行 labrador build
。
因此最佳的姿式是:
在項目中運行 labrador watch
在WebStorm中編碼,保存
切換到 微信web開發者工具 中調試、預覽
再回到WebStorm中編碼
...
注意此命令會初始化當前的目錄爲項目目錄。
Usage: labrador build [options] Options: -h, --help output usage information -V, --version output the version number -c, --catch 在載入時自動catch全部JS腳本的錯誤 -t, --test 運行測試腳本 -d, --debug DEBUG模式 -m, --minify uglify壓縮代碼
Usage: labrador watch [options] Options: -h, --help output usage information -V, --version output the version number -c, --catch 在載入時自動catch全部JS腳本的錯誤 -t, --test 運行測試腳本 -d, --debug DEBUG模式 -m, --minify uglify壓縮代碼
labrador
庫對全局的 wx
變量進行了封裝,將大部分 wx
對象中的方法進行了Promise支持, 除了以 on*
開頭或以 *Sync
結尾的方法。在以下代碼中使用 labrador
庫。
import wx from 'labrador'; console.log(wx.version); wx.app; // 和全局的 getApp() 函數效果同樣,代碼風格不建議粗暴地訪問全局對象和方法 wx.Component; // Labrador 自定義組件基類 wx.Types; // Labrador 數據類型校驗器集合 wx.login; // 封裝後的微信登陸接口 wx.getStorage; // 封裝後的讀取緩存接口 //... 更多請參見 https://mp.weixin.qq.com/debug/wxadoc/dev/api/
咱們建議不要再使用 wx.getStorageSync()
等同步阻塞方法,而在 async
函數中使用 await wx.getStorage()
異步非阻塞方法提升性能,除非遇到特殊狀況。
src/app.js
示例代碼以下:
import wx from 'labrador'; import {sleep} from './utils/util'; export default class { globalData = { userInfo: null }; async onLaunch() { //調用API從本地緩存中獲取數據 let res = await wx.getStorage({ key: 'logs' }); let logs = res.data || []; logs.unshift(Date.now()); await wx.setStorage({ key: 'logs', data: logs }); this.timer(); } async timer() { while (true) { console.log('hello'); await sleep(10000); } } async getUserInfo() { if (this.globalData.userInfo) { return this.globalData.userInfo; } await wx.login(); let res = await wx.getUserInfo(); this.globalData.userInfo = res.userInfo; return res.userInfo; } }
代碼中所有使用ES6/7標準語法。代碼沒必要聲明 use strict
,由於在編譯時,全部代碼都會強制使用嚴格模式。
代碼中並未調用全局的 App()
方法,而是使用 export
語法默認導出了一個類,在編譯後,Labrador會自動增長 App()
方法調用,全部請勿手動調用 App()
方法。這樣作是由於代碼風格不建議粗暴地訪問全局對象和方法。
Labrador的自定義組件,是基於微信小程序框架的組件之上,進一步自定義組合,擁有邏輯處理和樣式。這樣作的目的請參見 微信小程序開發三宗罪和解決方案
項目中通用自定義組件存放在 src/compontents
目錄,一個組件通常由三個文件組成,*.js
、 *.xml
和 *.less
分別對應微信小程序框架的 js
、 wxml
和 wxss
文件。在Labardor項目源碼中,咱們特地採用了 xml
和 less
後綴以示區別。若是組件包含單元測試,那麼在組件目錄下會存在一個 *.test.js
的測試腳本文件。
下面是一個簡單的自定義組件代碼實例:
src/compontents/title/title.js
import wx from 'labrador'; import randomColor from '../../utils/random-color'; const { string } = wx.Types; export default class Title extends wx.Component { propTypes = { text: string }; props = { text: '' }; data = { text: '', color: randomColor() }; onUpdate(props) { this.setData('text', props.text); } handleTap() { this.setData({ color: randomColor() }); } }
自定義組件的邏輯代碼和微信框架中的page很類似,最大的區別是在js邏輯代碼中,沒有調用全局的 Page()
函數聲明頁面,而是用 export
語法導出了一個默認的類,這個類必須繼承於 labrador.Component
組件基類。
相對於微信框架中的page,Labrador自定義組件擴展了 propTypes
、 props
、 children
選項及 onUpdate
生命週期函數。children
選項表明當前組件中的子組件集合,此選項將在下文中敘述。
Labrador的目標是構建一個能夠重用、嵌套的自定義組件方案,在現實狀況中,當多個組件互相嵌套組合,就必定會遇到父子組件件的數據和消息傳遞。由於全部的組件都實現了 setData
方法,因此咱們可使用 this.children.foobar.setData(data)
或 this.parent.setData(data)
這樣的代碼調用來解決父子組件間的數據傳遞問題,可是,若是項目中出現大量這樣的代碼,那麼數據流將變得很是混亂。
咱們借鑑了 React.js 的思想,爲組件增長了 props 機制。子組件經過 this.props
獲得父組件給本身傳達的參數數據。父組件怎樣將數據傳遞給子組件,咱們下文中敘述。
onUpdate
生命週期函數是當組件的 props
發生變化後被調用,相似React.js中的 componentWillReceiveProps
因此咱們能夠在此函數體內監測 props
的變化。
組件定義時的 propTypes
選項是對當前組件的props參數數據類型的定義。 props
選項表明的是當前組件默認的各項參數值。propTypes
、 props
選項均可以省略,可是強烈建議定義 propTypes
,由於這樣可使得代碼更清晰易懂,另外還能夠經過Labrador自動檢測props值類型,以減小BUG。爲優化性能,只有在DEBUG模式下才會自動檢測props值類型。
編譯時加上 -d
參數時便可進入DEBUG模式,在代碼中任何地方均可以使用魔術變量 __DEBUG__
來判斷是不是DEBUG模式。
另外,Labrador自定義組件的 setData
方法,支持兩種傳參方式,第一種像微信框架同樣接受一個 object
類型的對象參數,第二種方式接受做爲KV對的兩個參數,setData
方法將自動將其轉爲 object
。
注意 組件中事件響應方法必須以
handle
開頭!例如上文中的handleTap
,不然子組件將沒法與模板綁定。這樣作也是爲了代碼風格統一,方便團隊協做。建議事件響應方法命名採用handle + 組件名 + 事件名
例如:handleUsernameChange
handleLoginButtonTap
,這樣咱們很容易區分是模板上哪個組件發生了什麼事件,若是省略中間的名詞,如handleTap
,則表明當前整個自定義組件發生了tap
事件。
src/compontents/title/title.xml
<view class="text-view"> <text class="title-text" catchtap="handleTap" style="color:{{color}};">{{text}}</text> </view>
XML佈局文件和微信WXML文件語法徹底一致,只是擴充了一個自定義標籤 <component/>
,下文中詳細敘述。
src/compontents/title/title.less
.title-text { font-weight: bold; font-size: 2em; }
雖然咱們採用了LESS文件,可是因爲微信小程序框架的限制,不能使用LESS的層級選擇及嵌套語法。可是咱們可使用LESS的變量、mixin、函數等功能方便開發。
咱們要求全部的頁面必須存放在 pages
目錄中,每一個頁面的子目錄中的文件格式和自定義組件一致,只是能夠多出一個 *.json
配置文件。
下面是默認首頁的示例代碼:
src/pages/index/index.js
import wx from 'labrador'; import List from '../../components/list/list'; import Title from '../../components/title/title'; import Counter from '../../components/counter/counter'; export default class Index extends wx.Component { data = { userInfo: {}, mottoTitle: 'Hello World', count: 0 }; get children() { return { list: new List(), motto: new Title({ text: '@mottoTitle' }), counter: new Counter({ count: '@count', onChange: this.handleCountChange }) }; } async onLoad() { try { //調用應用實例的方法獲取全局數據 let userInfo = await wx.app.getUserInfo(); //更新數據 this.setData({ userInfo }); this.update(); } catch (error) { console.error(error.stack); } } onReady() { this.setData('mottoTitle', 'Labrador'); } handleCountChange(count) { this.setData({ count }); } //事件處理函數 handleViewTap() { wx.navigateTo({ url: '../logs/logs' }); } }
頁面代碼的格式和自定義組件的格式如出一轍,咱們的思想是 頁面也是組件。
js邏輯代碼中一樣使用 export default
語句導出了一個默認類,也不能手動調用 Page()
方法,由於在編譯後,pages
目錄下的全部js文件所有會自動調用 Page()
方法聲明頁面。
咱們看到組件類中,有一個對象屬性 children
,這個屬性定義了該組件依賴、包含的其餘自定義組件,在上面的代碼中頁面包含了三個自定義組件 list
、 title
和 counter
,這個三個自定義組件的 key
分別爲 list
、 motto
和 counter
。
自定義組件類在實例化時接受一個類型爲 object
的參數,這個參數就是父組件要傳給子組件的props數據。通常狀況下,父組件傳遞給子組件的props屬性在其生命週期中是不變的,這是由於JS的語法和小程序框架的限制,沒有React.js的JSX靈活。可是咱們能夠傳遞一個以 @
開頭的屬性值,這樣咱們就能夠把子組建的 props
屬性值綁定到父組件的 data
上來,當父組件的 data
發生變化後,Labrador將自動更新子組件的 props
。例如上邊代碼中,將子組件 motto
的 text
屬性綁定到了 @mottoTitle
。那麼在 onReady
方法中,將父組件的 mottoTitle
設置爲 Labrador
,那麼子組件 motto
的 text
屬性就會自動變爲 Labrador
。
頁面也是組件,全部的組件都擁有同樣的生命週期函數onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setData函數。
componets
和 pages
兩個目錄的區別在於,componets
中存放的組件可以被智能加載,pages
目錄中的組件在編譯時自動加上 Page()
調用,因此,pages
目錄中的組件不能被其餘組件調用,不然將出現屢次調用Page()
的錯誤。若是某個組件須要重用,請存放在 componets
目錄或打包成NPM包。
src/pages/index/index.xml
<view class="container"> <view class="userinfo" catchtap="handleViewTap"> <image class="userinfo-avatar" src="{{ userInfo.avatarUrl }}" background-size="cover"/> <text class="userinfo-nickname">{{ userInfo.nickName }}</text> </view> <view class="usermotto"> <component key="motto" name="title"/> </view> <component key="list"/> <component key="counter"/> </view>
XML佈局代碼中,使用了Labrador提供的 <component/>
標籤,此標籤的做用是導入一個自定義子組件的佈局文件,標籤有兩個屬性,分別爲 key
(必選)和 name
(可選,默認爲key的值)。key
與js邏輯代碼中的組件 key
對應,name
是組件的目錄名。key
用來綁定組件JS邏輯對象的 children
中對應的數據, name
用於在src/componets
和 node_modules
目錄中尋找子組件模板。
src/pages/index/index.less
@import 'list'; @import 'title'; @import 'counter'; .motto-title-text { font-size: 3em; padding-bottom: 1rem; } /* ... */
LESS樣式文件中,咱們使用了 @import
語句加載全部子組件樣式,這裏的 @import 'list'
語句按照LESS的語法,會首先尋找當前目錄 src/pages/index/
中的 list.less
文件,若是找不到就會按照Labrador的規則智能地嘗試尋找 src/componets
和 node_modules
目錄中的組件樣式。
接下來,咱們定義了 .motto-title-text
樣式,這樣作是由於 motto
key 表明的title組件的模板中(src/compontents/title/title.xml
)有一個view 屬於 title-text
類,編譯時,Labrador將自動爲其增長一個前綴 motto-
,因此編譯後這個view所屬的類爲 title-text motto-title-text
(能夠查看 dist/pages/index/index.xml
)。那麼咱們就能夠在父組件的樣式代碼中使用 .motto-title-text
來從新定義子組件的樣式。
Labrador支持多層組件嵌套,在上述的實例中,index
包含子組件 list
和 title
,list
包含子組件 title
,因此在最終顯示時,index
頁面上回顯示兩個 title
組件。
詳細代碼請參閱 labrador init
命令生成的示例項目。
咱們規定項目中全部後綴爲 *.test.js
的文件爲測試腳本文件。每個測試腳本文件對應一個待測試的JS模塊文件。例如 src/utils/util.js
和 src/utils/utils.test.js
。這樣,項目中全部模塊和其測試文件就所有存放在一塊兒,方便查找和模塊劃分。這樣規劃主要是受到了GO語言的啓發,也符合微信小程序一向的目錄結構風格。
在編譯時,加上 -t
參數便可自動調用測試腳本完成項目測試,若是不加 -t
參數,則全部測試腳本不會被編譯到 dist
目錄,因此沒必要擔憂項目會肥胖。
測試腳本中使用 export
語句導出多個名稱以 test*
開頭的函數,這些函數在運行後會被逐個調用完成測試。若是test測試函數在運行時拋出異常,則視爲測試失敗,例如代碼:
// src/util.js // 普通項目模塊文件中的代碼片斷,導出了一個通用的add函數 export function add(a, b) { return a + b; }
// src/util.test.js // 測試腳本文件代碼片斷 import assert from 'assert'; //測試 util.add() 函數 export function testAdd(exports) { assert(exports.add(1, 1) === 2); }
代碼中 testAdd
即爲一個test測試函數,專門用來測試 add()
函數,在test函數執行時,會將目標模塊做爲參數傳進來,即會將 util.js
中的 exports
傳進來。
自定義組件的測試腳本中能夠導出兩類測試函數。第三類和普通測試腳本同樣,也爲 test*
函數,可是參數不是 exports
而是運行中的、實例化後的組件對象。那麼咱們就能夠在test函數中調用組件的方法或則訪問組件的props
和 data
屬性,來測試行爲。另外,普通模塊測試腳本是啓動後就開始逐個運行 test*
函數,而組件測試腳本是當組件 onReady
之後纔會開始測試。
自定義組件的第二類測試函數是以 on*
開頭,和組件的生命週期函數名稱如出一轍,這一類測試函數不是等到組件 onReady
之後開始運行,而是當組件生命週期函數運行時被觸發。函數接收兩個參數,第一個爲組件的對象引用,第二個爲run
函數。好比某個組件有一個 onLoad
測試函數,那麼當組件將要運行 onLoad
生命週期函數時,先觸發 onLoad
測試函數,在測試函數內部調用 run()
函數,繼續執行組件的生命週期函數,run()
函數返回的數據就是生命週期函數返回的數據,若是返回的是Promise,則表明生命週期函數是一個異步函數,測試函數也能夠寫爲async
異步函數,等待生命週期函數結束。這樣咱們就能夠獲取run()
先後兩個狀態數據,最後對比,來測試生命週期函數的運行是否正確。
第三類測試函數與生命週期測試函數相似,是以 handle*
開頭,用以測試事件處理函數是否正確,是在對應事件發生時運行測試。例如:
// src/components/counter/counter.test.js export function handleTap(c, run) { let num = c.data.num; run(); let step = c.data.num - num; if (step !== 1) { throw new Error('計數器點擊一次應該自增1,可是自增了' + step); } }
生命週期測試函數和事件測試函數只會執行一次,自動化測試的結果將會輸出到Console控制檯。
labrador init
命令在初始化項目時,會在項目根目錄中建立一個 .labrador
項目配置文件,若是你的項目是使用 labrador-cli 0.3 版本建立的,能夠手動增長此文件。
配置文件爲JSON格式,默認配置爲:
{ "npmMap":{ }, "uglify":{ "mangle": [], "compress": { "warnings": false } } }
npmMap
屬性爲NPM包映射設置,例如 {"underscore":"lodash"}
配置,若是你的源碼中有require('underscore')
那麼編譯後將成爲 require('lodash')
。這樣作是爲了解決小程序的環境限制致使一些NPM包沒法使用的問題。好比咱們的代碼必須依賴於包A,A又依賴於B,若是B和小程序不兼容,將致使A也沒法使用。在這總狀況下,咱們能夠Fork一份B,起名爲C,將C中與小程序不兼容的代碼調整下,最後在項目配置文件中將B映射爲C,那麼在編譯後就會繞過B而加載C,從而解決這個問題。
uglify
屬性爲 UglifyJs2 的壓縮配置,在編譯時附加 -m
參數便可對項目中的全部文件進行壓縮處理。
labrador 0.3.0
重構自定義組件支持綁定子組件數據和事件
labrador 0.4.0
增長自定義組件props機制
自動化測試
UglifyJS壓縮集成
NPM包映射
增長.labrador項目配置文件
本項目依據MIT開源協議發佈,容許任何組織和我的無償使用。