微信小程序開發(fā)如何使用npm包--labrador使用

相信做過微信小程序的都知道,官方給出的微信web開發(fā)工具上根本就無法加載node_modules包,即使可以加載,node_modules動輒幾十M的大小,小程序的代碼限制在1M以內(nèi),微信小程序的三個(gè)不足:

1無法調(diào)用npm包

2無法使用babel轉(zhuǎn)嗎

3無法重用組件(像react那樣重用組件功能)

接下來給大家介紹一個(gè)相對完整的微信開發(fā)解決方案:

Labrador:(目前最新版本為:0.6.12)

github地址:https://github.com/maichong/labrador

特點(diǎn):

1,使用Labrador框架可以使微信開發(fā)者工具支持加載海量NPM包

2,支持ES6/7標(biāo)準(zhǔn)代碼,使用async/await能夠有效避免回調(diào)地獄

3,組件重用,對微信小程序框架進(jìn)行了二次封裝,實(shí)現(xiàn)了組件重用和嵌套

4,自動化測試,非常容易編寫單元測試腳本,不經(jīng)任何額外配置即可自動化測試

5,使用Editor Config及ESLint標(biāo)準(zhǔn)化代碼風(fēng)格,方便團(tuán)隊(duì)協(xié)作

當(dāng)然了也有缺點(diǎn),你看完會發(fā)現(xiàn)缺點(diǎn)

首先系統(tǒng)全局安裝nodejs和Labrador命令行工具。

npminstall -g labrador-cli

查看當(dāng)前l(fā)abrador版本

labrador -V

新建一個(gè)目錄,初始化項(xiàng)目

labrador create mylabrador# 初始化labrador項(xiàng)目 mylabrador是你的項(xiàng)目名字

用Egret Wing3(這個(gè)IDE更適合開發(fā)微信小程序),打開labradordemo這個(gè)項(xiàng)目,

開啟代碼自動轉(zhuǎn)換功能

labrador watch

然后用微信開發(fā)著工具打開labradordemo項(xiàng)目下面的dist文件

這個(gè)里面不需要做任何的編碼工作,在下面的src目錄作修改,會自動同步到微信開發(fā)者工具上面

在src/pages/index新增一個(gè)index.json文件,主要內(nèi)容為設(shè)置頁面的title

{

"navigationBarTitleText": "主頁",

"enablePullDownRefresh": false

}

然后保存,會同步到微信開發(fā)者工具

labrador庫對全局的wx變量進(jìn)行了封裝,將所有wx對象中的異步方法進(jìn)行了Promise支持, 除了同步的方法,這些方法往往以on*、create*、stop*、pause*、close*開頭或以*Sync結(jié)尾。在如下代碼中使用labrador庫。

importwx, { Component, PropTypes }from'labrador';

wx.wx;// 原始的全局 wx 對象

wx.app;// 和全局的 getApp() 函數(shù)效果一樣,代碼風(fēng)格不建議粗暴地訪問全局對象和方法

wx.currentPages// 對全局函數(shù) getCurrentPages() 優(yōu)雅的封裝

Component;// Labrador 自定義組件基類

PropTypes;// Labrador 數(shù)據(jù)類型校驗(yàn)器集合

wx.login;// 封裝后的微信登錄接口

wx.getStorage;// 封裝后的讀取緩存接口

//... 更多請參見 https://mp.weixin.qq.com/debug/wxadoc/dev/api/

我們建議不要再使用wx.getStorageSync()等同步阻塞方法,而在async函數(shù)中使用await wx.getStorage()異步非阻塞方法提高性能,除非遇到特殊情況。

app.js文件

import request from 'al-request';

import { setStore } from 'labrador-redux';

import { sleep } from './utils/utils';

import store from './redux';

if (__DEV__) {

console.log('當(dāng)前為開發(fā)環(huán)境');

}

// 向labrador-redux注冊store

setStore(store);

export default class {

async onLaunch() {

try {

await sleep(100);

await request('api/start');

} catch (error) {

console.error(error);

}

this.timer();

}

async timer() {

while (true) {

console.log('hello');

await sleep(10000);

}

}

}

代碼中全部使用ES6/7標(biāo)準(zhǔn)語法。代碼不必聲明use strict,因?yàn)樵诰幾g時(shí),所有代碼都會強(qiáng)制使用嚴(yán)格模式。

代碼中并未調(diào)用全局的App()方法,而是使用export語法默認(rèn)導(dǎo)出了一個(gè)類,在編譯后,Labrador會自動增加App()方法調(diào)用,所有請勿手動調(diào)用App()方法。這樣做是因?yàn)榇a風(fēng)格不建議粗暴地訪問全局對象和方法。

Labrador的自定義組件,是基于微信小程序框架的組件之上,進(jìn)一步自定義組合,擁有邏輯處理、布局和樣式。

項(xiàng)目中通用自定義組件存放在src/compontents目錄,一個(gè)組件一般由三個(gè)文件組成,*.js、*.xml和*.less分別對應(yīng)微信小程序框架的js、wxml和wxss文件。在Labardor項(xiàng)目源碼中,我們特意采用了xml和less后綴以示區(qū)別。如果組件包含單元測試,那么在組件目錄下會存在一個(gè)*.test.js的測試腳本文件。

0.6 版本后,支持*.sass和*.scss格式樣式文件。

自定義組件示例

下面是一個(gè)簡單的自定義組件代碼實(shí)例:

邏輯src/compontents/todo/todo.js

import { Component, PropTypes } from 'labrador-immutable';

const { string, bool, func } = PropTypes;

class Todo extends Component {

static propTypes = {

id: string,

title: string,

createdAt: string,

finished: bool,

finishedAt: string,

onRemove: func,

onRestore: func,

onFinish: func

};

constructor(props) {

super(props);

this.state = {

icon: props.finished ? 'success_circle' : 'circle',

className: props.finished ? 'todo-finished' : ''

};

}

onUpdate(props) {

this.setState({

icon: props.finished ? 'success_circle' : 'circle',

className: props.finished ? 'todo-finished' : ''

});

}

handleRemove() {

this.props.onRemove(this.props.id);

}

handleFinish() {

if (this.props.finished) {

this.props.onRestore(this.props.id);

} else {

this.props.onFinish(this.props.id);

}

}

}

export default Todo;

自定義組件的邏輯代碼和微信框架中的page很相似,最大的區(qū)別是在js邏輯代碼中,沒有調(diào)用全局的Page()函數(shù)聲明頁面,而是用export語法導(dǎo)出了一個(gè)默認(rèn)的類,這個(gè)類必須繼承于Component組件基類。

相對于微信框架中的page,Labrador自定義組件擴(kuò)展了propTypes、defaultProps、onUpdate()、setState()、children()等方法和屬性,children()方法返回當(dāng)前組件中的子組件集合,此選項(xiàng)將在下文中敘述。

Labrador的目標(biāo)是構(gòu)建一個(gè)可以重用、嵌套的自定義組件方案,在現(xiàn)實(shí)情況中,當(dāng)多個(gè)組件互相嵌套組合,就一定會遇到父子組件件的數(shù)據(jù)和消息傳遞。因?yàn)樗械慕M件都實(shí)現(xiàn)了setState方法,所以我們可以使用this._children.foobar.setState(data)或this.parent.setState(data)這樣的代碼調(diào)用來解決父子組件間的數(shù)據(jù)傳遞問題,但是,如果項(xiàng)目中出現(xiàn)大量這樣的代碼,那么數(shù)據(jù)流將變得非?;靵y。

我們借鑒了 React.js 的思想,為組件增加了 props 機(jī)制。子組件通過this.props得到父組件給自己傳達(dá)的參數(shù)數(shù)據(jù)。父組件怎樣將數(shù)據(jù)傳遞給子組件,我們下文中敘述。

onUpdate生命周期函數(shù)是當(dāng)組件的props發(fā)生變化后被調(diào)用,類似React.js中的componentWillReceiveProps所以我們可以在此函數(shù)體內(nèi)監(jiān)測props的變化。

組件定義時(shí)的propTypes靜態(tài)屬性是對當(dāng)前組件的props參數(shù)數(shù)據(jù)類型的定義。defaultProps選項(xiàng)代表的是當(dāng)前組件默認(rèn)的各項(xiàng)參數(shù)值。propTypes、defaultProps選項(xiàng)都可以省略,但是強(qiáng)烈建議定義propTypes,因?yàn)檫@樣可以使得代碼更清晰易懂,另外還可以通過Labrador自動檢測props值類型,以減少BUG。為優(yōu)化性能,只有在開發(fā)環(huán)境下才會自動檢測props值類型。

編譯時(shí)默認(rèn)是開發(fā)環(huán)境,當(dāng)編譯時(shí)候采用-m參數(shù)才會是生產(chǎn)模式,在代碼中任何地方都可以使用魔術(shù)變量__DEV__來判斷是否是開發(fā)環(huán)境。

組件向模板傳值需要調(diào)用setState方法,換言之,組件模板能夠讀取到當(dāng)前組件的所有內(nèi)部狀態(tài)數(shù)據(jù)。

0.6版本后,Component基類中撤銷了setData方法,新增了setState方法,這樣做并不是僅僅為了像React.js,而是在老版本中,我們將所有組件樹的內(nèi)部狀態(tài)數(shù)據(jù)和props全存放在page.data中,在組件更新時(shí)產(chǎn)生了大量的setData遞歸調(diào)用,為了優(yōu)化性能,必須將組件樹的狀態(tài)和page.data進(jìn)行了分離。

布局src/compontents/todo/todo.xml

{{props.title}}

刪除

XML布局文件和微信WXML文件語法完全一致,只是擴(kuò)充了兩個(gè)自定義標(biāo)簽和,下文中詳細(xì)敘述。

使用{{}}綁定變量時(shí),以props.*或state.*開頭,即XML模板文件能夠訪問組件對象的props和state。

樣式src/compontents/todo/todo.less

@import 'al-ui';

.todo {

background: #fff;

font-size: @font-size-medium;

}

.todo-icon {

margin-right: 10px;

}

.todo-finished {

background: @color-page;

}

.todo-finished-title {

.gray;

text-decoration: line-through;

}

雖然我們采用了LESS文件,但是由于微信小程序框架的限制,不能使用LESS的層級選擇及嵌套語法。但是我們可以使用LESS的變量、mixin、函數(shù)等功能方便開發(fā)。

頁面

我們要求所有的頁面必須存放在pages目錄中,每個(gè)頁面的子目錄中的文件格式和自定義組件一致,只是可以多出一個(gè)*.json配置文件。

頁面示例

下面是默認(rèn)首頁的示例代碼:

邏輯src/pages/index/index.js

import wx, { Component, PropTypes } from 'labrador-immutable';

import { bindActionCreators } from 'redux';

import { connect } from 'labrador-redux';

import Todo from '../../components/todo/todo';

import * as todoActions from '../../redux/todos';

import { sleep } from '../../utils/utils';

const { array, func } = PropTypes;

class Index extends Component {

static propTypes = {

todos: array,

removeTodo: func,

restoreTodo: func,

createTodo: func,

finishTodo: func

};

state = {

titleInput: '',

finished: 0

};

children() {

let todos = this.props.todos || [];

let unfinished = [];

let finished = [];

if (todos.length) {

unfinished = todos.filter((todo) => !todo.finished);

finished = todos.asMutable()

.filter((todo) => todo.finished)

.sort((a, b) => (a.finishedAt < b.finishedAt ? 1 : -1))

.slice(0, 3);

}

return {

list: unfinished.map((todo) => ({

component: Todo,

key: todo.id,

props: {

...todo,

onRemove: this.handleRemove,

onRestore: this.handleRestore,

onFinish: this.handleFinish

}

})),

finished: finished.map((todo) => ({

component: Todo,

key: todo.id,

props: {

...todo,

onRemove: this.handleRemove,

onRestore: this.handleRestore,

onFinish: this.handleFinish

}

}))

};

}

onUpdate(props) {

let nextState = {

finished: 0

};

props.todos.forEach((todo) => {

if (todo.finished) {

nextState.finished += 1;

}

});

this.setState(nextState);

}

async onPullDownRefresh() {

await sleep(1000);

wx.showToast({ title: '刷新成功' });

wx.stopPullDownRefresh();

}

handleCreate() {

let title = this.state.titleInput;

if (!title) {

wx.showToast({ title: '請輸入任務(wù)' });

return;

}

this.props.createTodo({ title });

this.setState({ titleInput: '' });

}

handleInput(e) {

this.setState({ titleInput: e.detail.value });

}

handleRemove = (id) => {

this.props.removeTodo(id);

};

handleFinish = (id) => {

this.props.finishTodo(id);

};

handleRestore = (id) => {

this.props.restoreTodo(id);

};

handleShowFinished() {

wx.navigateTo({ url: 'finished' });

}

handleShowUI() {

wx.navigateTo({ url: '/pages/ui/index' });

}

}

export default connect(

({ todos }) => ({ todos }),

(dispatch) => bindActionCreators({

createTodo: todoActions.create,

removeTodo: todoActions.remove,

finishTodo: todoActions.finish,

restoreTodo: todoActions.restore,

}, dispatch)

)(Index);

頁面代碼的格式和自定義組件的格式一模一樣,我們的思想是頁面也是組件。

js邏輯代碼中同樣使用export default語句導(dǎo)出了一個(gè)默認(rèn)類,也不能手動調(diào)用Page()方法,因?yàn)樵诰幾g后,pages目錄下的所有js文件全部會自動調(diào)用Page()方法聲明頁面。

我們看到組件類中,有一個(gè)對象方法children(),這個(gè)方法返回了該組件依賴、包含的其他自定義組件,在上面的代碼中頁面包含了三個(gè)自定義組件list、title和counter,這個(gè)三個(gè)自定義組件的key分別為list、motto和counter。

children()返回的每個(gè)組件的定義都包含兩個(gè)屬性,component屬性定義了組件類,props屬性定義了父組件向子組件傳入的props屬性對象。

頁面也是組件,所有的組件都擁有一樣的生命周期函數(shù)onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setState函數(shù)。

componets和pages兩個(gè)目錄的區(qū)別在于,componets中存放的組件能夠被智能加載、重用,pages目錄中的組件在編譯時(shí)自動加上Page()調(diào)用,所以,pages目錄中的組件不能被其他組件調(diào)用,否則將出現(xiàn)多次調(diào)用Page()的錯誤。如果某個(gè)組件需要重用,請存放在componets目錄或打包成NPM包。

注意雖然頁面也是組件,雖然頁面的代碼格式和組件一模一樣,但是運(yùn)行時(shí),getCurrentPages()得到的頁面對象page并非pages目錄中聲明的頁面對象,page.root才是pages目錄中聲明的頁面對象,才是組件樹的最頂端。這里我們用了組合模式而非繼承模式。

注意所有組件的生命周期函數(shù)支持async,但默認(rèn)是普通函數(shù),如果函數(shù)體內(nèi)沒有異步操作,我們建議采用普通函數(shù),因?yàn)閍sync函數(shù)會有一定的性能開銷,并且無法保證執(zhí)行順序。當(dāng)聲明周期函數(shù)內(nèi)需要異步操作,并且【不關(guān)心】各個(gè)生命周期函數(shù)的執(zhí)行順序時(shí),可以采用async函數(shù)。

布局src/pages/index/index.xml

已完成

3}}" class="padding-h-xxlarge padding-top-large">

查看全部已完成

總數(shù) {{props.todos.length}} 已完成

{{state.finished}}

當(dāng)前沒有任務(wù)

請?jiān)谙路捷斎肟蛑刑钊胄氯蝿?wù)然后點(diǎn)擊新增

bindinput="handleInput"/>

新增

Powered by Labrador

AL UI

XML布局代碼中,使用了Labrador提供的標(biāo)簽,此標(biāo)簽的作用是導(dǎo)入一個(gè)自定義子組件的布局文件,標(biāo)簽有兩個(gè)屬性,分別為key(必選)和name(可選,默認(rèn)為key的值)。key與js邏輯代碼中的組件key對應(yīng),name是組件的目錄名。key用來綁定組件JS邏輯對象的children中對應(yīng)的數(shù)據(jù),name用于在src/componets和node_modules目錄中尋找子組件模板。

樣式src/pages/index/index.less

@import 'al-ui';

@import 'todo';

.todo-list {

}

LESS樣式文件中,我們使用了@import語句加載所有子組件樣式,這里的@import 'list'語句按照LESS的語法,會首先尋找當(dāng)前目錄src/pages/index/中的list.less文件,如果找不到就會按照Labrador的規(guī)則智能地嘗試尋找src/componets和node_modules目錄中的組件樣式。

接下來,我們定義了.motto-title-text樣式,這樣做是因?yàn)閙ottokey 代表的title組件的模板中(src/compontents/title/title.xml)有一個(gè)view 屬于title-text類,編譯時(shí),Labrador將自動為其增加一個(gè)前綴motto-,所以編譯后這個(gè)view所屬的類為title-text motto-title-text(可以查看dist/pages/index/index.xml)。那么我們就可以在父組件的樣式代碼中使用.motto-title-text來重新定義子組件的樣式。

Labrador支持多層組件嵌套,在上述的實(shí)例中,index包含子組件list和title,list包含子組件title,所以在最終顯示時(shí),index頁面上回顯示兩個(gè)title組件。

自定義組件列表

邏輯src/components/list/list.js

importwx, { Component }from'labrador';

importTitlefrom'../title/title';

importItemfrom'../item/item';

import{ sleep }from'../../utils/util';

exportdefaultclassListextendsComponent{

constructor(props){

super(props);

this.state={

items:[

{ id:1, title:'Labrador'},

{ id:2, title:'Alaska'}

]

};

}

children(){

return{

title:{

component:Title,

props:{ text:'The List Title'}

},

listItems:this.state.items.map((item)=>{

return{

component:Item,

key:item.id,

props:{

item:item,

title:item.title,

isNew:item.isNew,

onChange:(title)=>{this.handleChange(item, title) }

}

};

})

};

}

asynconLoad() {

awaitsleep(1000);

this.setState({

items:[{ id:3, title:'Collie', isNew:true}].concat(this.data.items)

});

}

handleChange(item, title) {

letitems=this.state.items.map((i)=>{

if(item.id==i.id){

returnObject.assign({},i,{ title });

}

returni;

});

this.setState({ items });

}

}

在上邊代碼中的children()返回的listItems子組件定義時(shí),是一個(gè)組件數(shù)組。數(shù)組的每一項(xiàng)都是一個(gè)子組件的定義,并且需要指定每一項(xiàng)的key屬性,key屬性將用于模板渲染性能優(yōu)化,建議將唯一且不易變化的值設(shè)置為子組件的key,比如上邊例子中的id。

模板src/components/list/list.xml

在XML模板中,調(diào)用標(biāo)簽即可自動渲染子組件列表。和標(biāo)簽類似,同樣也有兩個(gè)屬性,key和name。Labrador編譯后,會自動將標(biāo)簽編譯成wx:for循環(huán)。

自動化測試

我們規(guī)定項(xiàng)目中所有后綴為*.test.js的文件為測試腳本文件。每一個(gè)測試腳本文件對應(yīng)一個(gè)待測試的JS模塊文件。例如src/utils/util.js和src/utils/utils.test.js。這樣,項(xiàng)目中所有模塊和其測試文件就全部存放在一起,方便查找和模塊劃分。這樣規(guī)劃主要是受到了GO語言的啟發(fā),也符合微信小程序一貫的目錄結(jié)構(gòu)風(fēng)格。

在編譯時(shí),加上-t參數(shù)即可自動調(diào)用測試腳本完成項(xiàng)目測試,如果不加-t參數(shù),則所有測試腳本不會被編譯到dist目錄,所以不必?fù)?dān)心項(xiàng)目會肥胖。

普通JS模塊測試

測試腳本中使用export語句導(dǎo)出多個(gè)名稱以test*開頭的函數(shù),這些函數(shù)在運(yùn)行后會被逐個(gè)調(diào)用完成測試。如果test測試函數(shù)在運(yùn)行時(shí)拋出異常,則視為測試失敗,例如代碼:

// src/util.js

// 普通項(xiàng)目模塊文件中的代碼片段,導(dǎo)出了一個(gè)通用的add函數(shù)

exportfunctionadd(a, b) {

returna+b;

}

// src/util.test.js

// 測試腳本文件代碼片段

importassertfrom'assert';

//測試 util.add() 函數(shù)

exportfunctiontestAdd(exports) {

assert(exports.add(1,1)===2);

}

代碼中testAdd即為一個(gè)test測試函數(shù),專門用來測試add()函數(shù),在test函數(shù)執(zhí)行時(shí),會將目標(biāo)模塊作為參數(shù)傳進(jìn)來,即會將util.js中的exports傳進(jìn)來。

自定義組件測試

自定義組件的測試腳本中可以導(dǎo)出兩類測試函數(shù)。第三類和普通測試腳本一樣,也為test*函數(shù),但是參數(shù)不是exports而是運(yùn)行中的、實(shí)例化后的組件對象。那么我們就可以在test函數(shù)中調(diào)用組件的方法或則訪問組件的props和state屬性,來測試行為。另外,普通模塊測試腳本是啟動后就開始逐個(gè)運(yùn)行test*函數(shù),而組件測試腳本是當(dāng)組件onReady以后才會開始測試。

自定義組件的第二類測試函數(shù)是以on*開頭,和組件的生命周期函數(shù)名稱一模一樣,這一類測試函數(shù)不是等到組件onReady以后開始運(yùn)行,而是當(dāng)組件生命周期函數(shù)運(yùn)行時(shí)被觸發(fā)。函數(shù)接收兩個(gè)參數(shù),第一個(gè)為組件的對象引用,第二個(gè)為run函數(shù)。比如某個(gè)組件有一個(gè)onLoad測試函數(shù),那么當(dāng)組件將要運(yùn)行onLoad生命周期函數(shù)時(shí),先觸發(fā)onLoad測試函數(shù),在測試函數(shù)內(nèi)部調(diào)用run()函數(shù),繼續(xù)執(zhí)行組件的生命周期函數(shù),run()函數(shù)返回的數(shù)據(jù)就是生命周期函數(shù)返回的數(shù)據(jù),如果返回的是Promise,則代表生命周期函數(shù)是一個(gè)異步函數(shù),測試函數(shù)也可以寫為async異步函數(shù),等待生命周期函數(shù)結(jié)束。這樣我們就可以獲取run()前后兩個(gè)狀態(tài)數(shù)據(jù),最后對比,來測試生命周期函數(shù)的運(yùn)行是否正確。

第三類測試函數(shù)與生命周期測試函數(shù)類似,是以handle*開頭,用以測試事件處理函數(shù)是否正確,是在對應(yīng)事件發(fā)生時(shí)運(yùn)行測試。例如:

// src/components/counter/counter.test.js

exportfunctionhandleTap(c, run) {

letnum=c.data.num;

run();

letstep=c.data.num-num;

if(step!==1) {

thrownewError('計(jì)數(shù)器點(diǎn)擊一次應(yīng)該自增1,但是自增了'+step);

}

}

生命周期測試函數(shù)和事件測試函數(shù)只會執(zhí)行一次,自動化測試的結(jié)果將會輸出到Console控制臺。

項(xiàng)目配置文件

labrador create命令在初始化項(xiàng)目時(shí),會在項(xiàng)目根目錄中創(chuàng)建一個(gè).labrador項(xiàng)目配置文件,如果你的項(xiàng)目是使用 labrador-cli 0.3 版本創(chuàng)建的,可以手動增加此文件。

配置文件為JSON5格式,默認(rèn)配置為:

{

"define":{

"API_ROOT":"http://localhost:5000/"

},

"npmMap":{

"lodash-es":"lodash"

},

"uglify":{

"mangle": [],

"compress": {

"warnings":false

}

},

"classNames": {

"text-red":true

},

"env":{

"development": {},

"production": {

"define":{

"API_ROOT":"https://your.online.domain/"

}

}

}

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容