微信小程序开发如何使用npm包--labrador使用

相信做过微信小程序的都知道,官方给出的微信web开发工具上根本就无法加载node_modules包,即使可以加载,node_modules动辄几十M的大小,小程序的代码限制在1M以内,微信小程序的三个不足:

1无法调用npm包

2无法使用babel转吗

3无法重用组件(像react那样重用组件功能)

接下来给大家介绍一个相对完整的微信开发解决方案:

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

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

特点:

1,使用Labrador框架可以使微信开发者工具支持加载海量NPM包

2,支持ES6/7标准代码,使用async/await能够有效避免回调地狱

3,组件重用,对微信小程序框架进行了二次封装,实现了组件重用和嵌套

4,自动化测试,非常容易编写单元测试脚本,不经任何额外配置即可自动化测试

5,使用Editor Config及ESLint标准化代码风格,方便团队协作

当然了也有缺点,你看完会发现缺点

首先系统全局安装nodejs和Labrador命令行工具。

npminstall -g labrador-cli

查看当前labrador版本

labrador -V

新建一个目录,初始化项目

labrador create mylabrador# 初始化labrador项目 mylabrador是你的项目名字

用Egret Wing3(这个IDE更适合开发微信小程序),打开labradordemo这个项目,

开启代码自动转换功能

labrador watch

然后用微信开发着工具打开labradordemo项目下面的dist文件

这个里面不需要做任何的编码工作,在下面的src目录作修改,会自动同步到微信开发者工具上面

在src/pages/index新增一个index.json文件,主要内容为设置页面的title

{

"navigationBarTitleText": "主页",

"enablePullDownRefresh": false

}

然后保存,会同步到微信开发者工具

labrador库对全局的wx变量进行了封装,将所有wx对象中的异步方法进行了Promise支持, 除了同步的方法,这些方法往往以on*、create*、stop*、pause*、close*开头或以*Sync结尾。在如下代码中使用labrador库。

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

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

wx.app;// 和全局的 getApp() 函数效果一样,代码风格不建议粗暴地访问全局对象和方法

wx.currentPages// 对全局函数 getCurrentPages() 优雅的封装

Component;// Labrador 自定义组件基类

PropTypes;// Labrador 数据类型校验器集合

wx.login;// 封装后的微信登录接口

wx.getStorage;// 封装后的读取缓存接口

//... 更多请参见 https://mp.weixin.qq.com/debug/wxadoc/dev/api/

我们建议不要再使用wx.getStorageSync()等同步阻塞方法,而在async函数中使用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('当前为开发环境');

}

// 向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标准语法。代码不必声明use strict,因为在编译时,所有代码都会强制使用严格模式。

代码中并未调用全局的App()方法,而是使用export语法默认导出了一个类,在编译后,Labrador会自动增加App()方法调用,所有请勿手动调用App()方法。这样做是因为代码风格不建议粗暴地访问全局对象和方法。

Labrador的自定义组件,是基于微信小程序框架的组件之上,进一步自定义组合,拥有逻辑处理、布局和样式。

项目中通用自定义组件存放在src/compontents目录,一个组件一般由三个文件组成,*.js、*.xml和*.less分别对应微信小程序框架的js、wxml和wxss文件。在Labardor项目源码中,我们特意采用了xml和less后缀以示区别。如果组件包含单元测试,那么在组件目录下会存在一个*.test.js的测试脚本文件。

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

自定义组件示例

下面是一个简单的自定义组件代码实例:

逻辑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很相似,最大的区别是在js逻辑代码中,没有调用全局的Page()函数声明页面,而是用export语法导出了一个默认的类,这个类必须继承于Component组件基类。

相对于微信框架中的page,Labrador自定义组件扩展了propTypes、defaultProps、onUpdate()、setState()、children()等方法和属性,children()方法返回当前组件中的子组件集合,此选项将在下文中叙述。

Labrador的目标是构建一个可以重用、嵌套的自定义组件方案,在现实情况中,当多个组件互相嵌套组合,就一定会遇到父子组件件的数据和消息传递。因为所有的组件都实现了setState方法,所以我们可以使用this._children.foobar.setState(data)或this.parent.setState(data)这样的代码调用来解决父子组件间的数据传递问题,但是,如果项目中出现大量这样的代码,那么数据流将变得非常混乱。

我们借鉴了 React.js 的思想,为组件增加了 props 机制。子组件通过this.props得到父组件给自己传达的参数数据。父组件怎样将数据传递给子组件,我们下文中叙述。

onUpdate生命周期函数是当组件的props发生变化后被调用,类似React.js中的componentWillReceiveProps所以我们可以在此函数体内监测props的变化。

组件定义时的propTypes静态属性是对当前组件的props参数数据类型的定义。defaultProps选项代表的是当前组件默认的各项参数值。propTypes、defaultProps选项都可以省略,但是强烈建议定义propTypes,因为这样可以使得代码更清晰易懂,另外还可以通过Labrador自动检测props值类型,以减少BUG。为优化性能,只有在开发环境下才会自动检测props值类型。

编译时默认是开发环境,当编译时候采用-m参数才会是生产模式,在代码中任何地方都可以使用魔术变量__DEV__来判断是否是开发环境。

组件向模板传值需要调用setState方法,换言之,组件模板能够读取到当前组件的所有内部状态数据。

0.6版本后,Component基类中撤销了setData方法,新增了setState方法,这样做并不是仅仅为了像React.js,而是在老版本中,我们将所有组件树的内部状态数据和props全存放在page.data中,在组件更新时产生了大量的setData递归调用,为了优化性能,必须将组件树的状态和page.data进行了分离。

布局src/compontents/todo/todo.xml

{{props.title}}

删除

XML布局文件和微信WXML文件语法完全一致,只是扩充了两个自定义标签和,下文中详细叙述。

使用{{}}绑定变量时,以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、函数等功能方便开发。

页面

我们要求所有的页面必须存放在pages目录中,每个页面的子目录中的文件格式和自定义组件一致,只是可以多出一个*.json配置文件。

页面示例

下面是默认首页的示例代码:

逻辑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: '请输入任务' });

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语句导出了一个默认类,也不能手动调用Page()方法,因为在编译后,pages目录下的所有js文件全部会自动调用Page()方法声明页面。

我们看到组件类中,有一个对象方法children(),这个方法返回了该组件依赖、包含的其他自定义组件,在上面的代码中页面包含了三个自定义组件list、title和counter,这个三个自定义组件的key分别为list、motto和counter。

children()返回的每个组件的定义都包含两个属性,component属性定义了组件类,props属性定义了父组件向子组件传入的props属性对象。

页面也是组件,所有的组件都拥有一样的生命周期函数onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setState函数。

componets和pages两个目录的区别在于,componets中存放的组件能够被智能加载、重用,pages目录中的组件在编译时自动加上Page()调用,所以,pages目录中的组件不能被其他组件调用,否则将出现多次调用Page()的错误。如果某个组件需要重用,请存放在componets目录或打包成NPM包。

注意虽然页面也是组件,虽然页面的代码格式和组件一模一样,但是运行时,getCurrentPages()得到的页面对象page并非pages目录中声明的页面对象,page.root才是pages目录中声明的页面对象,才是组件树的最顶端。这里我们用了组合模式而非继承模式。

注意所有组件的生命周期函数支持async,但默认是普通函数,如果函数体内没有异步操作,我们建议采用普通函数,因为async函数会有一定的性能开销,并且无法保证执行顺序。当声明周期函数内需要异步操作,并且【不关心】各个生命周期函数的执行顺序时,可以采用async函数。

布局src/pages/index/index.xml

已完成

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

查看全部已完成

总数 {{props.todos.length}} 已完成

{{state.finished}}

当前没有任务

请在下方输入框中填入新任务然后点击新增

bindinput="handleInput"/>

新增

Powered by Labrador

AL UI

XML布局代码中,使用了Labrador提供的标签,此标签的作用是导入一个自定义子组件的布局文件,标签有两个属性,分别为key(必选)和name(可选,默认为key的值)。key与js逻辑代码中的组件key对应,name是组件的目录名。key用来绑定组件JS逻辑对象的children中对应的数据,name用于在src/componets和node_modules目录中寻找子组件模板。

样式src/pages/index/index.less

@import 'al-ui';

@import 'todo';

.todo-list {

}

LESS样式文件中,我们使用了@import语句加载所有子组件样式,这里的@import 'list'语句按照LESS的语法,会首先寻找当前目录src/pages/index/中的list.less文件,如果找不到就会按照Labrador的规则智能地尝试寻找src/componets和node_modules目录中的组件样式。

接下来,我们定义了.motto-title-text样式,这样做是因为mottokey 代表的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组件。

自定义组件列表

逻辑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子组件定义时,是一个组件数组。数组的每一项都是一个子组件的定义,并且需要指定每一项的key属性,key属性将用于模板渲染性能优化,建议将唯一且不易变化的值设置为子组件的key,比如上边例子中的id。

模板src/components/list/list.xml

在XML模板中,调用标签即可自动渲染子组件列表。和标签类似,同样也有两个属性,key和name。Labrador编译后,会自动将标签编译成wx:for循环。

自动化测试

我们规定项目中所有后缀为*.test.js的文件为测试脚本文件。每一个测试脚本文件对应一个待测试的JS模块文件。例如src/utils/util.js和src/utils/utils.test.js。这样,项目中所有模块和其测试文件就全部存放在一起,方便查找和模块划分。这样规划主要是受到了GO语言的启发,也符合微信小程序一贯的目录结构风格。

在编译时,加上-t参数即可自动调用测试脚本完成项目测试,如果不加-t参数,则所有测试脚本不会被编译到dist目录,所以不必担心项目会肥胖。

普通JS模块测试

测试脚本中使用export语句导出多个名称以test*开头的函数,这些函数在运行后会被逐个调用完成测试。如果test测试函数在运行时抛出异常,则视为测试失败,例如代码:

// src/util.js

// 普通项目模块文件中的代码片段,导出了一个通用的add函数

exportfunctionadd(a, b) {

returna+b;

}

// src/util.test.js

// 测试脚本文件代码片段

importassertfrom'assert';

//测试 util.add() 函数

exportfunctiontestAdd(exports) {

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

}

代码中testAdd即为一个test测试函数,专门用来测试add()函数,在test函数执行时,会将目标模块作为参数传进来,即会将util.js中的exports传进来。

自定义组件测试

自定义组件的测试脚本中可以导出两类测试函数。第三类和普通测试脚本一样,也为test*函数,但是参数不是exports而是运行中的、实例化后的组件对象。那么我们就可以在test函数中调用组件的方法或则访问组件的props和state属性,来测试行为。另外,普通模块测试脚本是启动后就开始逐个运行test*函数,而组件测试脚本是当组件onReady以后才会开始测试。

自定义组件的第二类测试函数是以on*开头,和组件的生命周期函数名称一模一样,这一类测试函数不是等到组件onReady以后开始运行,而是当组件生命周期函数运行时被触发。函数接收两个参数,第一个为组件的对象引用,第二个为run函数。比如某个组件有一个onLoad测试函数,那么当组件将要运行onLoad生命周期函数时,先触发onLoad测试函数,在测试函数内部调用run()函数,继续执行组件的生命周期函数,run()函数返回的数据就是生命周期函数返回的数据,如果返回的是Promise,则代表生命周期函数是一个异步函数,测试函数也可以写为async异步函数,等待生命周期函数结束。这样我们就可以获取run()前后两个状态数据,最后对比,来测试生命周期函数的运行是否正确。

第三类测试函数与生命周期测试函数类似,是以handle*开头,用以测试事件处理函数是否正确,是在对应事件发生时运行测试。例如:

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

exportfunctionhandleTap(c, run) {

letnum=c.data.num;

run();

letstep=c.data.num-num;

if(step!==1) {

thrownewError('计数器点击一次应该自增1,但是自增了'+step);

}

}

生命周期测试函数和事件测试函数只会执行一次,自动化测试的结果将会输出到Console控制台。

项目配置文件

labrador create命令在初始化项目时,会在项目根目录中创建一个.labrador项目配置文件,如果你的项目是使用 labrador-cli 0.3 版本创建的,可以手动增加此文件。

配置文件为JSON5格式,默认配置为:

{

"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/"

}

}

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容