labrador 命令
labrador init 初始化项目命令
注意此命令会初始化当前的目录为项目目录。
labrador build 构建当前项目
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 压缩代码
-f, --force 强制构建,不使用缓存
labrador watch 监测文件变化
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模式
labrador 库
labrador 库对全局的 wx 变量进行了封装,将大部分 wx 对象中的方法进行了promise支持, 除了以 on* 开头或以 *sync结尾的方法。在如下代码中使用 labrador 库。
import wx from 'labrador';console.log(wx.version);wx.app;
// 和全局的 getapp() 函数效果一样,代码风格不建议粗暴地访问全局对象和方法wx.component;
// labrador 自定义组件基类wx.list;
// labrador 自定义组件列表类wx.proptypes;
// labrador 数据类型校验器集合wx.login;
// 封装后的微信登录接口wx.getstorage;
// 封装后的读取缓存接口
我们建议不要再使用 wx.getstoragesync() 等同步阻塞方法,而在 async 函数中使用 await wx.getstorage() 异步非阻塞方法提高性能,除非遇到特殊情况。
app.js
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.proptypes;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。
布局 src/compontents/title/title.xml
<view class="text-view">
<text class="title-text" catchtap="handletap" style="color:{{color}};">{{text}}</text>
</view>
xml布局文件和微信wxml文件语法完全一致,只是扩充了两个自定义标签 <component/> 和 <list/>,下文中详细叙述。
样式 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
};
children = {
list: new list(),
motto: new title({ text: '@mottotitle', hello: '@mottotitle' }),
counter: new counter({ count: '@count', onchange: '#handlecountchange' })
}; handlecountchange(count) { this.setdata({ count });
} //事件处理函数
handleviewtap() { wx.navigateto({
url: '../logs/logs'
});
} 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');
}
}
页面代码的格式和自定义组件的格式一模一样,我们的思想是 页面也是组件。
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。如果属性值以 # 开头,则将父组件的属性(非data的属性)直接绑定到子组件 props,如上边代码中的 #handlecountchange,会将父组件的 handlecountchange 方法绑定到子组件的 props.onchange 属性,这样子组件中可以通过调用 this.props.onchange(newvalue) 来通知父组件数据变化。
页面也是组件,所有的组件都拥有一样的生命周期函数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 命令生成的示例项目。
自定义组件列表
labrador 0.5版本后支持循环调用自定义组件生成一个列表。
逻辑 src/components/list/list.js
import wx from 'labrador';import title from '../title/title';import item from '../item/item';import { sleep } from '../../utils/util';export default class list extends wx.component {
data = {
items: [
{ title: 'labrador' },
{ title: 'alaska' }
]
};
children = {
title: new title({ text: 'the list title' }),
listitems: new wx.list(item, 'items', {
item: '>>',
title: '>title',
isnew: '>isnew',
onchange: '#handlechange'
})
}; async onload() { await sleep(1000); this.setdata({
items: [{ title: 'collie', isnew: true }].concat(this.data.items)
});
} handlechange(component, title) { let item = this.data.items[component.key]; item.title = title; this.setdata('items', this.data.items);
}
}
在上边代码中的 children.listitems 子组件定义时,并没有直接实例化子组件类,而是实例化了一个 labrador.list 类,这个类是labrador中专门用来管理组件列表。labrador.list 实例化时,接受三个参数:
第一个参数是列表中的自定义组件类,请将原始类传入即可,不用实例化。
第二个参数是父组件上 data 属性指向,指向的属性必须是一个数组,例如上述代码中,第二个参数为 items ,则当前父组件的 data.items 属性是一个数组,这个数组又多少个元素,那么子组件列表中就自动产生多少个子组件。子组件的数量跟随data.items 数组动态变化,labrador会自动实例化或销毁相应的子组件。销毁子组件时,子组件的 onunload() 方法将会被调用。
第三个参数是子组件 props 数据绑定设置,如果属性值以 > 开头,则将 data.items 中对应元素的属性绑定到子组件的props。如果属性值以 # 开头,则将父组件的方法绑定到子组件的 props 中。注意,因为子组件是一个列表,所以为了区别,父组件对应的方法被调用时,第一个参数为子组件的实例,第二个及其之后的参数才是子组件中传回的参数。如果属性值是 >> 则将整个列表项数据绑定到对应的 props 上。
模板 src/components/list/list.xml
<view class="list">
<component key="title" name="title"/>
<list key="listitems" name="item"/>
</view>
在xml模板中,调用 <list/> 标签即可自动渲染子组件列表。和 <component/> 标签类似,<list/> 同样也有两个属性,key 和 name。labrador编译后,会自动将 <list/> 标签编译成 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函数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.jsexport 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
}
}, "classnames": { "for-test":true
}
}
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 参数即可对项目中的所有文件进行压缩处理。
classnames 属性指定了不压缩的wxss类名,在压缩模式下,默认会将所有wxss类名压缩为非常短的字符串,并抛弃所有wxml页面中未曾使用的样式类,如果指定了该配置项,则指定的类不会被压缩和抛弃。这个配置在动态类名的情况下非常实用,比如xml中class=text-{{color}},在编译less时,无法确定less中的.text-red类是否被用到,所以需要配置此项强制保留text-red类。
changelog
2016-10-09
labrador 0.3.0
重构自定义组件支持绑定子组件数据和事件
2016-10-12
labrador 0.4.0
增加自定义组件props机制
自动化测试
uglifyjs压缩集成
npm包映射
增加.labrador项目配置文件
2016-10-13
labrador 0.4.2
修复组件setdata方法优化性能产生的数据不同步问题
在debug模式下输出调试信息
2016-10-16
labrador 0.5.0
新增组件列表
重构xml模板编译器
编译时绑定事件改为事件发生时自动分派
以上就是微信小程序组件化开发labrador框架的介绍的详细内容。