├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── component.js ├── create-page.js ├── flow ├── deep-equal.js └── labrador.js ├── global.js ├── index.js ├── package.json ├── prop-types.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = false 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "node": true 5 | }, 6 | "globals": { 7 | "wx": true, 8 | "getApp": true, 9 | "__DEV__": true 10 | }, 11 | "parser": "babel-eslint", 12 | "plugins": ["flowtype", "flowtype-errors"], 13 | "rules": { 14 | "flowtype/define-flow-type": 1, 15 | "flowtype/use-flow-type": 1, 16 | "flowtype-errors/show-errors": 2, 17 | "prefer-const": 0, 18 | "prefer-template": 0, 19 | "no-param-reassign": 0, 20 | "comma-dangle": [1, "never"], 21 | "spaced-comment": [0, "always"], 22 | "func-names": 0, 23 | "no-underscore-dangle": 0, 24 | "import/prefer-default-export": 1, 25 | "class-methods-use-this": 1, 26 | "no-prototype-builtins": 0, 27 | "import/no-extraneous-dependencies": 1, 28 | "max-len": [2, {"code": 120, "ignoreComments": true}], 29 | "radix": 0, 30 | "strict": 0, 31 | "object-shorthand": 0, 32 | "prefer-arrow-callback": 0, 33 | "prefer-rest-params": 0, 34 | "prefer-spread": 0, 35 | "vars-on-top": 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maichong/labrador/ed416658f1ab5395e81a847b6255d47857a39410/.npmignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Maichong Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | #[Labrador](https://github.com/maichong/labrador) 3 | 4 | 微信小程序组件化开发框架 5 | 6 | DEMO:https://github.com/maichong/labrador-demo 7 | 8 | > QQ交流群 282140496 9 | 10 | ## 特性 11 | * 使用Labrador框架可以使微信开发者工具支持加载海量NPM包 12 | * 支持ES6/7标准代码,使用async/await能够有效避免回调地狱 13 | * 组件重用,对微信小程序框架进行了二次封装,实现了组件重用和嵌套 14 | * 可集成Redux,使用Redux数据流控制,让项目逻辑清晰可维护 15 | * 自动持久化数据,支持redux-persist自动将运行数据保存 16 | * 自动化测试,非常容易编写单元测试脚本,不经任何额外配置即可自动化测试 17 | * Flow.js强类型检查,编写更加安全稳定的代码 18 | * 使用Editor Config及ESLint标准化代码风格,方便团队协作 19 | * 强力压缩代码,尽可能减小程序体积,让你在1M的限制内做更多的事 20 | 21 | ## 安装 22 | 23 | 首先您的系统中安装Node.js和npm v3 [下载Node.js](https://nodejs.org/en/),然后运行下面的命令将全局安装Labrador命令行工具。 24 | 25 | ``` 26 | npm install -g labrador-cli 27 | ``` 28 | 29 | ## 初始化项目 30 | 31 | ``` 32 | labrador create demo # 新建项目 33 | cd demo # 跳转到项目目录 34 | ``` 35 | 36 | ## 项目目录结构 37 | 38 | ```sh 39 | demo # 项目根目录 40 | ├── .labrador # Labrador项目配置文件 41 | ├── .babelrc # babel配置文件 42 | ├── .editorconfig # Editor Config 43 | ├── .eslintignore # ESLint 忽略配置 44 | ├── .eslintrc # ESLint 语法检查配置 45 | ├── .build/ # Labrador编译临时目录 46 | ├── package.json 47 | ├── dist/ # 目标目录 48 | ├── node_modules/ 49 | └── src/ # 源码目录 50 | ├── app.js 51 | ├── app.json 52 | ├── app.less 53 | ├── components/ # 通用组件目录 54 | ├── pages/ # 页面目录 55 | └── utils/ 56 | 57 | ``` 58 | 59 | > **注意** dist目录中的所有文件是由labrador命令编译生成,请勿直接修改 60 | 61 | ## 配置开发工具 62 | 63 | 项目初始化后使用WebStorm或Sublime等你习惯的IDE打开项目根目录。然后打开 *微信web开发者工具* 新建项目,本地开发目录选择 `dist` 目标目录。 64 | 65 | ## 开发流程 66 | 67 | 在WebStorm或Sublime等IDE中编辑 `src` 目录下的源码,然后在项目根目录中运行`labrador build` 命令构建项目,然后在 *微信web开发者工具* 的调试界面中点击左侧菜单的 *重启* 按钮即可查看效果。 68 | 69 | 我们在开发中, *微信web开发者工具* 仅仅用来做调试和预览,不要在 *微信web开发者工具* 的编辑界面修改代码。 70 | 71 | > *微信web开发者工具* 会偶尔出错,表现为点击 *重启* 按钮没有反应,调试控制台输出大量的无法require文件的错误,*编辑* 界面中代码文件不显示。这是因为 `labrador build` 命令会更新整个 `dist` 目录,而 *微信web开发者工具* 在监测代码改变时会出现异常,遇到这种情况只需要关掉 *微信web开发者工具* 再启动即可。 72 | 73 | 我们还可以使用 `labrador watch` 命令来监控 `src` 目录下的代码,当发生改变后自动构建,不用每一次编辑代码后手动运行 `labrador build` 。 74 | 75 | 所以最佳的姿势是: 76 | 77 | 1. 在项目中运行 `labrador watch` 78 | 2. 在WebStorm中编码,保存 79 | 3. 切换到 *微信web开发者工具* 中调试、预览 80 | 4. 再回到WebStorm中编码 81 | 5. ... 82 | 83 | ## labrador 命令 84 | 85 | #### `labrador create ` 创建项目 86 | 87 | 注意此命令会初始化当前的目录为项目目录。 88 | 89 | #### `labrador build [options]` 构建当前项目 90 | 91 | ``` 92 | Options: 93 | 94 | -h, --help output usage information 95 | -c, --catch 在载入时自动catch所有JS脚本的错误 96 | -t, --test 运行测试脚本 97 | -m, --minify 压缩代码 98 | -f, --force 强制构建,不使用缓存 99 | --work-dir [dir] 工作目录,默认为当前目录 100 | --config [file] 配置文件,默认为.labrador 101 | --src-dir [dir] 源码目录,默认为工作目录下的src文件夹 102 | --dist-dir [dir] 目标目录,默认为工作目录下的dist文件夹 103 | --modules-dir [dir] NPM模块目录,默认为工作目录下的node_modules文件夹 104 | --temp-dir [dir] 临时目录,默认为工作目录下的.build文件夹 105 | --ignore-minify-js minify模式下,不压缩JS代码 106 | --ignore-minify-page minify模式下,不强力压缩WXSS和WXML代码 107 | ``` 108 | 109 | #### `labrador watch [options]` 编译当前项目并检测文件改动 110 | 111 | ``` 112 | Options: 113 | 114 | -h, --help output usage information 115 | -c, --catch 在载入时自动catch所有JS脚本的错误 116 | -t, --test 运行测试脚本 117 | --work-dir [dir] 工作目录,默认为当前目录 118 | --config [file] 配置文件,默认为.labrador 119 | --src-dir [dir] 源码目录,默认为工作目录下的src文件夹 120 | --dist-dir [dir] 目标目录,默认为工作目录下的dist文件夹 121 | --modules-dir [dir] NPM模块目录,默认为工作目录下的node_modules文件夹 122 | --temp-dir [dir] 临时目录,默认为工作目录下的.build文件夹 123 | ``` 124 | 125 | #### `labrador generate [options] ` 创建新组件、页面、Redux、Saga等等 126 | 127 | ``` 128 | Options: 129 | 130 | -h, --help output usage information 131 | --work-dir [dir] 工作目录,默认为当前目录 132 | --config [file] 配置文件,默认为.labrador 133 | --src-dir [dir] 源码目录,默认为工作目录下的src文件夹 134 | --scss 使用scss,默认为less 135 | 136 | Names: 137 | 138 | component 创建新组件 139 | page 创建新页面 140 | redux 创建Redux文件 141 | saga 创建Saga文件 142 | ``` 143 | 144 | >例如 145 | > * labrador generate page home/home 146 | > * labrador generate component home 147 | > * labrador generate redux home 148 | > * labrador generate saga home 149 | 150 | ## labrador 库 151 | 152 | `labrador` 库对全局的 `wx` 变量进行了封装,将所有 `wx` 对象中的异步方法进行了Promise支持, 除了同步的方法,这些方法往往以 `on*`、`create*`、`stop*`、`pause*`、`close*` 开头或以 `*Sync` 结尾。在如下代码中使用 `labrador` 库。 153 | 154 | ```js 155 | import wx, { Component, PropTypes } from 'labrador'; 156 | 157 | wx.wx; // 原始的全局 wx 对象 158 | wx.app; // 和全局的 getApp() 函数效果一样,代码风格不建议粗暴地访问全局对象和方法 159 | wx.currentPages // 对全局函数 getCurrentPages() 优雅的封装 160 | Component; // Labrador 自定义组件基类 161 | PropTypes; // Labrador 数据类型校验器集合 162 | 163 | wx.login; // 封装后的微信登录接口 164 | wx.getStorage; // 封装后的读取缓存接口 165 | //... 更多请参见 https://mp.weixin.qq.com/debug/wxadoc/dev/api/ 166 | ``` 167 | 168 | 我们建议不要再使用 `wx.getStorageSync()` 等同步阻塞方法,而在 `async` 函数中使用 `await wx.getStorage()` 异步非阻塞方法提高性能,除非遇到特殊情况。 169 | 170 | ## app.js 171 | 172 | `src/app.js` 示例代码如下: 173 | 174 | ```js 175 | import wx from 'labrador'; 176 | import {sleep} from './utils/util'; 177 | 178 | export default class { 179 | globalData = { 180 | userInfo: null 181 | }; 182 | 183 | async onLaunch() { 184 | //调用API从本地缓存中获取数据 185 | let res = await wx.getStorage({ key: 'logs' }); 186 | let logs = res.data || []; 187 | logs.unshift(Date.now()); 188 | await wx.setStorage({ key: 'logs', data: logs }); 189 | this.timer(); 190 | } 191 | 192 | async timer() { 193 | while (true) { 194 | console.log('hello'); 195 | await sleep(10000); 196 | } 197 | } 198 | 199 | async getUserInfo() { 200 | if (this.globalData.userInfo) { 201 | return this.globalData.userInfo; 202 | } 203 | await wx.login(); 204 | let res = await wx.getUserInfo(); 205 | this.globalData.userInfo = res.userInfo; 206 | return res.userInfo; 207 | } 208 | } 209 | ``` 210 | 211 | 代码中全部使用ES6/7标准语法。代码不必声明 `use strict` ,因为在编译时,所有代码都会强制使用严格模式。 212 | 213 | 代码中并未调用全局的 `App()` 方法,而是使用 `export` 语法默认导出了一个类,在编译后,Labrador会自动增加 `App()` 方法调用,所有请勿手动调用 `App()` 方法。这样做是因为代码风格不建议粗暴地访问全局对象和方法。 214 | 215 | ## 自定义组件 216 | 217 | Labrador的自定义组件,是基于微信小程序框架的组件之上,进一步自定义组合,拥有逻辑处理、布局和样式。 218 | 219 | 项目中通用自定义组件存放在 `src/compontents` 目录,一个组件一般由三个文件组成,`*.js` 、 `*.xml` 和 `*.less` 分别对应微信小程序框架的 `js` 、 `wxml` 和 `wxss` 文件。在Labardor项目源码中,我们特意采用了 `xml` 和 `less` 后缀以示区别。如果组件包含单元测试,那么在组件目录下会存在一个 `*.test.js` 的测试脚本文件。 220 | 221 | > 0.6 版本后,支持 `*.sass` 和 `*.scss` 格式样式文件。 222 | 223 | #### 自定义组件示例 224 | 225 | 下面是一个简单的自定义组件代码实例: 226 | 227 | ##### 逻辑 `src/compontents/title/title.js` 228 | 229 | ```js 230 | import wx, { Component } from 'labrador'; 231 | import randomColor from '../../utils/random-color'; 232 | 233 | const { string } = wx.PropTypes; 234 | 235 | export default class Title extends Component { 236 | 237 | static propTypes = { 238 | text: string 239 | }; 240 | 241 | static defaultProps = { 242 | text: '' 243 | }; 244 | 245 | state = { 246 | color: randomColor() 247 | }; 248 | 249 | onUpdate(props) { 250 | this.setState({ 251 | color: randomColor() 252 | }); 253 | } 254 | 255 | handleTap() { 256 | this.setState({ 257 | color: randomColor() 258 | }); 259 | } 260 | } 261 | 262 | ``` 263 | 264 | 自定义组件的逻辑代码和微信框架中的page很相似,最大的区别是在js逻辑代码中,没有调用全局的 `Page()` 函数声明页面,而是用 `export` 语法导出了一个默认的类,这个类必须继承于 `Component` 组件基类。 265 | 266 | 相对于微信框架中的page,Labrador自定义组件扩展了 `propTypes` 、 `defaultProps` 、 `onUpdate()`、`setState()` 、 `children()` 等方法和属性,`children()`方法返回当前组件中的子组件集合,此选项将在下文中叙述。 267 | 268 | Labrador的目标是构建一个可以重用、嵌套的自定义组件方案,在现实情况中,当多个组件互相嵌套组合,就一定会遇到父子组件件的数据和消息传递。因为所有的组件都实现了 `setState` 方法,所以我们可以使用 `this._children.foobar.setState(data)` 或 `this.parent.setState(data)` 这样的代码调用来解决父子组件间的数据传递问题,但是,如果项目中出现大量这样的代码,那么数据流将变得非常混乱。 269 | 270 | 我们借鉴了 React.js 的思想,为组件增加了 props 机制。子组件通过 `this.props` 得到父组件给自己传达的参数数据。父组件怎样将数据传递给子组件,我们下文中叙述。 271 | 272 | `onUpdate` 生命周期函数是当组件的 `props` 发生变化后被调用,类似React.js中的 `componentWillReceiveProps` 所以我们可以在此函数体内监测 `props` 的变化。 273 | 274 | 组件定义时的 `propTypes` 静态属性是对当前组件的props参数数据类型的定义。 `defaultProps` 选项代表的是当前组件默认的各项参数值。`propTypes` 、 `defaultProps` 选项都可以省略,但是强烈建议定义 `propTypes`,因为这样可以使得代码更清晰易懂,另外还可以通过Labrador自动检测props值类型,以减少BUG。为优化性能,只有在开发环境下才会自动检测props值类型。 275 | 276 | 编译时默认是开发环境,当编译时候采用 `-m` 参数才会是生产模式,在代码中任何地方都可以使用魔术变量 `__DEV__` 来判断是否是开发环境。 277 | 278 | 组件向模板传值需要调用 `setState` 方法,换言之,组件模板能够读取到当前组件的所有内部状态数据。 279 | 280 | > 0.6版本后,`Component` 基类中撤销了 `setData` 方法,新增了 `setState` 方法,这样做并不是仅仅为了像React.js,而是在老版本中,我们将所有组件树的内部状态数据和props全存放在`page.data`中,在组件更新时产生了大量的 `setData` 递归调用,为了优化性能,必须将组件树的状态和 `page.data` 进行了分离。 281 | 282 | ##### 布局 `src/compontents/title/title.xml` 283 | 284 | ```xml 285 | 286 | {{props.text}} 287 | 288 | ``` 289 | 290 | XML布局文件和微信WXML文件语法完全一致,只是扩充了两个自定义标签 `` 和 ``,下文中详细叙述。 291 | 292 | 使用 `{{}}` 绑定变量时,以 `props.*` 或 `state.*` 开头,即XML模板文件能够访问组件对象的 `props` 和 `state`。 293 | 294 | ##### 样式 `src/compontents/title/title.less` 295 | 296 | ```css 297 | .title-text { 298 | font-weight: bold; 299 | font-size: 2em; 300 | } 301 | ``` 302 | 303 | 虽然我们采用了LESS文件,但是由于微信小程序框架的限制,**不能**使用LESS的层级选择及嵌套语法。但是我们可以使用LESS的变量、mixin、函数等功能方便开发。 304 | 305 | ## 页面 306 | 307 | 我们要求所有的页面必须存放在 `pages` 目录中,每个页面的子目录中的文件格式和自定义组件一致,只是可以多出一个 `*.json` 配置文件。 308 | 309 | #### 页面示例 310 | 311 | 下面是默认首页的示例代码: 312 | 313 | ##### 逻辑 `src/pages/index/index.js` 314 | 315 | ```js 316 | import wx, { Component } from 'labrador'; 317 | import List from '../../components/list/list'; 318 | import Title from '../../components/title/title'; 319 | import Counter from '../../components/counter/counter'; 320 | 321 | export default class Index extends wx.Component { 322 | state = { 323 | userInfo: {}, 324 | mottoTitle: 'Hello World', 325 | count: 0 326 | }; 327 | 328 | children() { 329 | return { 330 | list: { 331 | component: List 332 | }, 333 | motto: { 334 | component: Title, 335 | props: { 336 | text: this.state.mottoTitle 337 | } 338 | }, 339 | counter: { 340 | component: Counter, 341 | props: { 342 | count: this.state.count, 343 | onChange: this.handleCountChange 344 | } 345 | } 346 | }; 347 | } 348 | 349 | handleCountChange(count) { 350 | this.setState({ count }); 351 | } 352 | 353 | //事件处理函数 354 | handleViewTap() { 355 | wx.navigateTo({ 356 | url: '../logs/logs' 357 | }); 358 | } 359 | 360 | async onLoad() { 361 | try { 362 | //调用应用实例的方法获取全局数据 363 | let userInfo = await wx.app.getUserInfo(); 364 | //更新数据 365 | this.setState({ userInfo }); 366 | this.update(); 367 | } catch (error) { 368 | console.error(error.stack); 369 | } 370 | } 371 | 372 | onReady() { 373 | this.setState('mottoTitle', 'Labrador'); 374 | } 375 | } 376 | 377 | ``` 378 | 379 | 页面代码的格式和自定义组件的格式一模一样,我们的思想是 **页面也是组件**。 380 | 381 | js逻辑代码中同样使用 `export default` 语句导出了一个默认类,也不能手动调用 `Page()` 方法,因为在编译后,`pages` 目录下的所有js文件全部会自动调用 `Page()` 方法声明页面。 382 | 383 | 我们看到组件类中,有一个对象方法 `children()` ,这个方法返回了该组件依赖、包含的其他自定义组件,在上面的代码中页面包含了三个自定义组件 `list` 、 `title` 和 `counter` ,这个三个自定义组件的 `key` 分别为 `list` 、 `motto` 和 `counter`。 384 | 385 | `children()` 返回的每个组件的定义都包含两个属性,`component` 属性定义了组件类,`props` 属性定义了父组件向子组件传入的 `props` 属性对象。 386 | 387 | 页面也是组件,所有的组件都拥有一样的生命周期函数onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setState函数。 388 | 389 | `componets` 和 `pages` 两个目录的区别在于,`componets` 中存放的组件能够被智能加载、重用,`pages` 目录中的组件在编译时自动加上 `Page()` 调用,所以,`pages` 目录中的组件不能被其他组件调用,否则将出现多次调用`Page()`的错误。如果某个组件需要重用,请存放在 `componets` 目录或打包成NPM包。 390 | 391 | **注意** 虽然页面也是组件,虽然页面的代码格式和组件一模一样,但是运行时,`getCurrentPages()` 得到的页面对象 `page` 并非pages目录中声明的页面对象,`page.root` 才是pages目录中声明的页面对象,才是组件树的最顶端。这里我们用了 *组合* 模式而非继承模式。 392 | 393 | **注意** 所有组件的生命周期函数支持 `async` ,但默认是普通函数,如果函数体内没有异步操作,我们建议采用普通函数,因为 `async` 函数会有一定的性能开销,并且无法保证执行顺序。当声明周期函数内需要异步操作,并且【不关心】各个生命周期函数的执行顺序时,可以采用 `async` 函数。 394 | 395 | ##### 布局 `src/pages/index/index.xml` 396 | 397 | ```xml 398 | 399 | 400 | 401 | {{ state.userInfo.nickName }} 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | ``` 410 | 411 | XML布局代码中,使用了Labrador提供的 `` 标签,此标签的作用是导入一个自定义子组件的布局文件,标签有两个属性,分别为 `key` (必选)和 `name` (可选,默认为key的值)。`key` 与js逻辑代码中的组件 `key` 对应,`name` 是组件的目录名。`key` 用来绑定组件JS逻辑对象的 `children` 中对应的数据, `name` 用于在`src/componets` 和 `node_modules` 目录中寻找子组件模板。 412 | 413 | ##### 样式 `src/pages/index/index.less` 414 | 415 | ```css 416 | @import 'list'; 417 | @import 'title'; 418 | @import 'counter'; 419 | 420 | .motto-title-text { 421 | font-size: 3em; 422 | padding-bottom: 1rem; 423 | } 424 | 425 | /* ... */ 426 | ``` 427 | 428 | LESS样式文件中,我们使用了 `@import` 语句加载所有子组件样式,这里的 `@import 'list'` 语句按照LESS的语法,会首先寻找当前目录 `src/pages/index/` 中的 `list.less` 文件,如果找不到就会按照Labrador的规则智能地尝试寻找 `src/componets` 和 `node_modules` 目录中的组件样式。 429 | 430 | 接下来,我们定义了 `.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` 来重新定义子组件的样式。 431 | 432 | Labrador支持多层组件嵌套,在上述的实例中,`index` 包含子组件 `list` 和 `title`,`list` 包含子组件 `title`,所以在最终显示时,`index` 页面上回显示两个 `title` 组件。 433 | 434 | ## 自定义组件列表 435 | 436 | ##### 逻辑 `src/components/list/list.js` 437 | 438 | ```js 439 | import wx, { Component } from 'labrador'; 440 | import Title from '../title/title'; 441 | import Item from '../item/item'; 442 | import { sleep } from '../../utils/util'; 443 | 444 | export default class List extends Component { 445 | 446 | constructor(props){ 447 | super(props); 448 | this.state = { 449 | items: [ 450 | { id:1, title: 'Labrador' }, 451 | { id:2, title: 'Alaska' } 452 | ] 453 | }; 454 | } 455 | 456 | children (){ 457 | return { 458 | title:{ 459 | component: Title, 460 | props: { text: 'The List Title' } 461 | }, 462 | listItems: this.state.items.map((item) => { 463 | return { 464 | component: Item, 465 | key: item.id, 466 | props: { 467 | item: item, 468 | title: item.title, 469 | isNew: item.isNew, 470 | onChange: (title) => { this.handleChange(item, title) } 471 | } 472 | }; 473 | }) 474 | }; 475 | } 476 | 477 | async onLoad() { 478 | await sleep(1000); 479 | this.setState({ 480 | items: [{ id:3, title: 'Collie', isNew: true }].concat(this.data.items) 481 | }); 482 | } 483 | 484 | handleChange(item, title) { 485 | let items = this.state.items.map((i) => { 486 | if(item.id == i.id){ 487 | return Object.assign({},i,{ title }); 488 | } 489 | return i; 490 | }); 491 | this.setState({ items }); 492 | } 493 | } 494 | ``` 495 | 496 | 在上边代码中的 `children()` 返回的 `listItems` 子组件定义时,是一个组件数组。数组的每一项都是一个子组件的定义,并且需要指定每一项的 `key` 属性,`key` 属性将用于模板渲染性能优化,建议将唯一且不易变化的值设置为子组件的 `key`,比如上边例子中的 `id`。 497 | 498 | ##### 模板 `src/components/list/list.xml` 499 | 500 | ```xml 501 | 502 | 503 | 504 | 505 | ``` 506 | 507 | 在XML模板中,调用 `` 标签即可自动渲染子组件列表。和 `` 标签类似,`` 同样也有两个属性,`key` 和 `name`。Labrador编译后,会自动将 `` 标签编译成 `wx:for` 循环。 508 | 509 | ## 自动化测试 510 | 511 | 我们规定项目中所有后缀为 `*.test.js` 的文件为测试脚本文件。每一个测试脚本文件对应一个待测试的JS模块文件。例如 `src/utils/util.js` 和 `src/utils/utils.test.js` 。这样,项目中所有模块和其测试文件就全部存放在一起,方便查找和模块划分。这样规划主要是受到了GO语言的启发,也符合微信小程序一贯的目录结构风格。 512 | 513 | 在编译时,加上 `-t` 参数即可自动调用测试脚本完成项目测试,如果不加 `-t` 参数,则所有测试脚本不会被编译到 `dist` 目录,所以不必担心项目会肥胖。 514 | 515 | #### 普通JS模块测试 516 | 517 | 测试脚本中使用 `export` 语句导出多个名称以 `test*` 开头的函数,这些函数在运行后会被逐个调用完成测试。如果test测试函数在运行时抛出异常,则视为测试失败,例如代码: 518 | 519 | ```js 520 | // src/util.js 521 | // 普通项目模块文件中的代码片段,导出了一个通用的add函数 522 | export function add(a, b) { 523 | return a + b; 524 | } 525 | ``` 526 | 527 | ```js 528 | // src/util.test.js 529 | // 测试脚本文件代码片段 530 | 531 | import assert from 'assert'; 532 | 533 | //测试 util.add() 函数 534 | export function testAdd(exports) { 535 | assert(exports.add(1, 1) === 2); 536 | } 537 | ``` 538 | 539 | 代码中 `testAdd` 即为一个test测试函数,专门用来测试 `add()` 函数,在test函数执行时,会将目标模块作为参数传进来,即会将 `util.js` 中的 `exports` 传进来。 540 | 541 | #### 自定义组件测试 542 | 543 | 自定义组件的测试脚本中可以导出两类测试函数。第三类和普通测试脚本一样,也为 `test*` 函数,但是参数不是 `exports` 而是运行中的、实例化后的组件对象。那么我们就可以在test函数中调用组件的方法或则访问组件的`props` 和 `state` 属性,来测试行为。另外,普通模块测试脚本是启动后就开始逐个运行 `test*` 函数,而组件测试脚本是当组件 `onReady` 以后才会开始测试。 544 | 545 | 自定义组件的第二类测试函数是以 `on*` 开头,和组件的生命周期函数名称一模一样,这一类测试函数不是等到组件 `onReady` 以后开始运行,而是当组件生命周期函数运行时被触发。函数接收两个参数,第一个为组件的对象引用,第二个为`run` 函数。比如某个组件有一个 `onLoad` 测试函数,那么当组件将要运行 `onLoad` 生命周期函数时,先触发 `onLoad` 测试函数,在测试函数内部调用 `run()` 函数,继续执行组件的生命周期函数,`run()` 函数返回的数据就是生命周期函数返回的数据,如果返回的是Promise,则代表生命周期函数是一个异步函数,测试函数也可以写为`async` 异步函数,等待生命周期函数结束。这样我们就可以获取`run()`前后两个状态数据,最后对比,来测试生命周期函数的运行是否正确。 546 | 547 | 第三类测试函数与生命周期测试函数类似,是以 `handle*` 开头,用以测试事件处理函数是否正确,是在对应事件发生时运行测试。例如: 548 | 549 | ```js 550 | // src/components/counter/counter.test.js 551 | 552 | export function handleTap(c, run) { 553 | let num = c.data.num; 554 | run(); 555 | let step = c.data.num - num; 556 | if (step !== 1) { 557 | throw new Error('计数器点击一次应该自增1,但是自增了' + step); 558 | } 559 | } 560 | ``` 561 | 562 | 生命周期测试函数和事件测试函数只会执行一次,自动化测试的结果将会输出到Console控制台。 563 | 564 | 565 | ## 项目配置文件 566 | 567 | `labrador create` 命令在初始化项目时,会在项目根目录中创建一个 `.labrador` 项目配置文件,如果你的项目是使用 labrador-cli 0.3 版本创建的,可以手动增加此文件。 568 | 569 | 配置文件为[JSON5](https://github.com/json5/json5)格式,默认配置为: 570 | 571 | ```json 572 | { 573 | "define":{ 574 | "API_ROOT":"http://localhost:5000/" 575 | }, 576 | "npmMap":{ 577 | "lodash-es":"lodash" 578 | }, 579 | "uglify":{ 580 | "mangle": [], 581 | "compress": { 582 | "warnings": false 583 | } 584 | }, 585 | "classNames": { 586 | "text-red":true 587 | }, 588 | "env":{ 589 | "development": {}, 590 | "production": { 591 | "define":{ 592 | "API_ROOT":"https://your.online.domain/" 593 | } 594 | } 595 | } 596 | } 597 | ``` 598 | 599 | 600 | 属性 | 说明 601 | --------- | -------- 602 | srcDir | 指定源码目录,默认为工作目录下的`src` 603 | distDir | 指定目标目录,默认为工作目录下的`dist` 604 | tempDir | 临时目录,默认为工作目录下的`.build` 605 | define | 属性为静态常量列表,编译时Labrador会自动替换JS代码中的对应变量 606 | npmMap | NPM包映射设置,例如 `{"underscore":"lodash"}` 配置,如果你的源码中有`require('underscore')` 那么编译后将成为 `require('lodash')`。这样做是为了解决小程序的环境限制导致一些NPM包无法使用的问题。比如我们的代码必须依赖于包A,A又依赖于B,如果B和小程序不兼容,将导致A也无法使用。在这总情况下,我们可以Fork一份B,起名为C,将C中与小程序不兼容的代码调整下,最后在项目配置文件中将B映射为C,那么在编译后就会绕过B而加载C,从而解决这个问题。同样也可以解决一些包重复加载的问题减小程序体积,比如 `{ "lodash-es":"lodash"} `,`{ "lodash.isstring":"lodash/isString" }`。 607 | uglify | UglifyJs2 压缩配置,在编译时附加 `-m` 参数即可对项目中的所有文件进行压缩处理。 608 | classNames| 指定不压缩的WXSS类名,在压缩模式下,默认会将所有WXSS类名压缩为非常短的字符串,并抛弃所有WXML页面中未曾使用的样式类,如果指定了该配置项,则指定的类不会被压缩和抛弃。这个配置在动态类名的情况下非常实用,比如XML中`class="text-{{color}}"`,在编译LESS时,无法确定LESS中的`.text-red`类是否被用到,所以需要配置此项强制保留`text-red`类。 609 | env | 不同环境下的特殊设置,上边的例子中,如果是生产环境(编译时开启了`-m`)则将静态变量 `API_ROOT` 设置为 `"https://your.online.domain/"`。 610 | 611 | ## immutable 612 | 613 | 我们强烈建议组件的 `state` 和 `props` 都定义为不可变,这样可以清晰地管理数据流,避免一些低级错误,也利于Labrador优化性能。 614 | 615 | 在你的代码中可以调用 `seamless-immutable` 库: 616 | 617 | ```js 618 | import wx, { Component } from 'labrador'; 619 | import immutable from 'seamless-immutable'; 620 | class Index extends Component{ 621 | onLoad(){ 622 | this.setState({ 623 | foo : immutable({title: 'bar'}) 624 | }); 625 | } 626 | } 627 | ``` 628 | 629 | 这样的代码可以将 `state` 的属性 `foo` 定义为不可变,但是整个 `state` 对象还是可变的。 630 | 631 | 我们提供了 `labrador-immutable` 库,基于这个库的 `Component` 基类的组件,`state` 和 `props` 都将会是不可变的。 632 | 633 | ```js 634 | import wx, { Component } from 'labrador-immutable'; 635 | class Index extends Component{ 636 | onLoad(){ 637 | this.setState({ 638 | foo : {title: 'bar'} 639 | }); 640 | 641 | this.state.foo = 'test'; //ERROR 不能修改不可变的state 642 | } 643 | } 644 | ``` 645 | 646 | `labrador-immutable` 库提供的所有接口和 `labrador` 一模一样,所以你只需要将你的代码中的 647 | 648 | ```js 649 | import wx, { Component } from 'labrador'; 650 | ``` 651 | 替换为 652 | 653 | ```js 654 | import wx, { Component } from 'labrador-immutable'; 655 | ``` 656 | 657 | 如果你不习惯immutable,那么仍然可以继续使用 `labrador` 库。 658 | 659 | ## template/import 标签 660 | 661 | 首先声明,不建议在模板中使用template和import标签,因为Labrador框架所做出的所有努力正是为了解决微信小程序框架无法组件化的问题,微信官方提供的template/import机制不能满足开发需求,所以Labrador才实现了自定义组件机制。而Labrador的自定义组件机制和template/import机制在实现逻辑上存在差异,无法无缝结合。 662 | 663 | 但是,毕竟Labrador兼容底层所有小程序标签,虽然template/import机制谈不上是组件化方案,但毕竟轻量化实现了模板抽象,所以在简单的场景下可以使用template/import标签,但是需要注意以下几点: 664 | 665 | 1. template 标签在编译时无法确定最终挂载点,即无法确定是哪一个对象引用了template,因为template是公用的。 666 | 2. template 标签子节点上绑定的事件只能分发到Page对象上,不能分发到子组件对象上,因为 #1。 667 | 3. 子组件中使用template标签时,只能用来显示数据,不能使用事件绑定,因为 #2。 668 | 4. 子组件中import公共template文件时,`src` 的相对路径并非由当前子组件位置决定,而由子组件被引用时Page的位置决定。所以容易照成`src`路径混乱,尤其是子组件被多个Page引用时。 669 | 5. 子组件中不建议使用template,因为 #2 #3 #4。 670 | 6. 公用的template文件必须存放在templates或pages目录中,因为其他目录中的XML文件编译时会被抛弃。 671 | 7. 如果template不写在独立的文件中,而是直接写在pages目录中的页面模板里,即不需要import情况下,不会有以上问题。 672 | 673 | ## 0.6版本升级指南 674 | 675 | 我们在0.6版本将Labrador代码进行了彻底的重构,同时API进行了一些调整,基于0.5版本的项目需要升级代码后才能正常运行。 676 | 677 | 升级代码前请首先将全局的 labrador-cli 和项目中所有 labrador 库升级到最新版本。 678 | 679 | #### 1. `__DEV__` 680 | 681 | 将所有 `__DEBUG__` 更新为 `__DEV__`。 682 | Labrador `build` 和 `watch` 命令取消 `-d` 参数,默认情况就视作开发环境。 683 | 684 | #### 2. `labrador` API调整 685 | 686 | ```js 687 | import wx from 'labrador'; 688 | class Index extends wx.Component{} 689 | ``` 690 | 更新为 691 | 692 | ```js 693 | import wx, { Component } from 'labrador'; 694 | class Index extends Component{} 695 | ``` 696 | 697 | #### 3. propTypes 698 | 699 | 0.6版本后 `propTypes` 必须是组件类的静态属性:`static propTypes={}`。 700 | 701 | #### 4. defaultProps 702 | 703 | 请将组件类中的 `props={};` 更新为 `static defaultProps={}`; 704 | 705 | #### 5. setState 706 | 707 | 请将所有的 `setData` 更新为 `setState`,并且不支持 `setState(key,value)`,必须使用 `setState({key:value})`。 708 | 709 | 将组件内所有的 `this.data.*` 更新为 `this.state.*`。 710 | 711 | #### 6. 子组件及组件列表 712 | 713 | ```js 714 | class Index extends Component{ 715 | children = { 716 | title: new Title({ text: 'The List Title' }), 717 | listItems: new wx.List(Item, 'items', { 718 | title: '>title', 719 | onChange: '#handleChange' 720 | }) 721 | }; 722 | } 723 | ``` 724 | 更新为 725 | 726 | ```js 727 | class Index extends Component{ 728 | children() { 729 | const items = this.state.items || []; 730 | return { 731 | title:{ 732 | component: Title, 733 | props: { text: 'The List Title' } 734 | }, 735 | listItems: items.map((item) => { 736 | return { 737 | component: Item, 738 | key: item.id, 739 | props:{ 740 | title: item.title, 741 | onChange: (data) => {this.handleChange(item, data)} 742 | } 743 | }; 744 | }) 745 | }; 746 | } 747 | } 748 | ``` 749 | 750 | #### 7. Flow.js 751 | 752 | 新版本采用了Flow.js重写,所以需要为Babel和ESlint配置Flow.js插件: 753 | 754 | ``` 755 | npm install --save \ 756 | babel-plugin-syntax-flow \ 757 | babel-plugin-transform-flow-strip-types \ 758 | eslint-plugin-flowtype \ 759 | eslint-plugin-flowtype-errors \ 760 | flow-bin 761 | ``` 762 | 763 | Babel配置文件示例: https://github.com/maichong/labrador-demo/blob/master/.babelrc 764 | 765 | ESlint配置文件示例:https://github.com/maichong/labrador-demo/blob/master/.eslintrc 766 | 767 | Flow.js配置文件示例 https://github.com/maichong/labrador-demo/blob/master/.flowconfig 768 | 769 | #### 8. `getCurrentPages()` 770 | 通过 `getCurrentPages()` 得到的`page`对象不再是组件树的最顶级组件对象,`page.root` 才是。 771 | 772 | #### 9. immutable 773 | 774 | ```js 775 | import wx, { Component } from 'labrador'; 776 | ``` 777 | 替换为 778 | 779 | ```js 780 | import wx, { Component } from 'labrador-immutable'; 781 | ``` 782 | 不强制要求,只是建议。 783 | 784 | #### 10. 更新模板变量绑定 785 | 模板中所有变量绑定需要增加指定 `state.` 或 `props.` 。 786 | 787 | ```xml 788 | 789 | {{title}} 790 |