打造微信小程序组件化开发框架

EmeIxb 8年前
   <h2><strong>正文</strong></h2>    <p>作为第一批小程序内测用户,我很有幸见证了小程序的成长,小程序上手十分简单,容易理解。但同时,因为运行环境的原因导致小程序无法使用市面上的流行框架。小程序本身提供一此特性如:模块化,模板,数据绑定等,能极大的方便了使用惯MVVM框架的用户。</p>    <p>在几个月的开发历程里,我一直希望能有一套方案更大可能的让小程序开发更贴近于当下开发习惯,因此才会有 wepy 。通过 wepy 开发的代码经过编译后,能生成一份完美运行在小程序端的代码,而且 wepy 的目的就是让小程序开发更贴近于传统H5框架开发,让小程序能像开发H5一样支持引入NPM包,支持组件化开发以及支持JS新特性等等。</p>    <h2><strong>小程序框架wepy文档</strong></h2>    <h2><strong>成品DEMO展示</strong></h2>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/a758c50caaa0b2d680a39b6973b4bd3f.jpg"></p>    <ul>     <li> <p>一个是使用 wepy new demo 命令生成的标准demo</p> </li>     <li> <p>一个是基于wepy开发的手机充值的完整demo。</p> </li>     <li> <p>一个是基于wepy开发的开源的仿微信的聊天界面</p> </li>    </ul>    <p>以上三个demo均在安卓机和IOS机上运行过通。</p>    <p>附上DEMO下载地址</p>    <p>https://github.com/wepyjs/wepy-wechat-demo</p>    <h2><strong>快速入门</strong></h2>    <h3><strong>代码规范:</strong></h3>    <ol>     <li> <p>变量与方法使用尽量使用驼峰式命名,避免使用 $ 开头。</p> <p>以 $ 开头的方法或者属性为框架内建方法或者属性,可以被使用,使用前请参考文中提供的API文档。</p> </li>     <li> <p>入口,页面,组件的命名后缀为 .wpy 。外链的文件可以是其它后缀。</p> <p>请参考文中的wpy文件说明</p> </li>     <li> <p>使用ES6语法开发。</p> <p>框架在ES6下开发,因此也需要使用ES6开发小程序,ES6中有大量的语法糖可以让我们的代码更加简洁高效。</p> </li>     <li> <p>使用Promise</p> <p>框架默认对小程序提供的API全都进行了 Promise 处理,甚至可以直接使用 async/await 等新特性进行开发。</p> </li>    </ol>    <h3><strong>项目创建与使用</strong></h3>    <p><strong>安装wepy</strong></p>    <p>以下安装都通过 npm 安装</p>    <ol>     <li> <p>安装 wepy 命令行工具</p> <pre>  npm install wepy-cli -g</pre> </li>     <li> <p>在开发目录生成开发DEMO</p> <pre>  wepy new myproject</pre> </li>     <li> <p>开发实时编译</p> <pre>  wepy build --watch</pre> </li>    </ol>    <p><strong>项目目录结构</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/0f8df1bcd66819ba42a171f302145a63.png"></p>    <p><strong>开发使用说明</strong></p>    <ol>     <li> <p>使用 微信开发者工具 新建项目,本地开发选择 dist 目录。</p> </li>     <li> <p>微信开发者工具 → 项目 → 关闭ES6转ES5。</p> </li>     <li> <p>本地项目根目录运行 wepy build --watch ,开启实时编译。</p> </li>    </ol>    <h2><strong>主要解决问题:</strong></h2>    <h3><strong>1. 开发模式转换</strong></h3>    <p>在原有的小程序的开发模式下进行再次封装,更贴近于现有MVVM框架开发模式。框架在开发过程中参考了一些现在框架的一些特性,并且融入其中,以下是使用wepy前后的代码对比图。</p>    <p>官方DEMO代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/48a78495a608279e314203dd231a4447.png"></p>    <p>基于wepy的实现:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/ce740fe8ccc5bf347e520e385bc9504a.png"></p>    <h3><strong>2. 支持组件化开发。</strong></h3>    <p>参见文中章节:组件</p>    <p>示例代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d8112f1558cf35cbe1a1245739d7afdc.jpg"></p>    <h3><strong>3. 支持加载外部NPM包。</strong></h3>    <p>在编译过程当中,会递归遍历代码中的 require 然后将对应依赖文件从node_modules当中拷贝出来,并且修改 require 为相对路径,从而实现对外部NPM包的支持。如下图:</p>    <p><img src="https://simg.open-open.com/show/ab833f734ebde88e7c863502f0ce33ab.jpg"></p>    <h3><strong>4. 单文件模式,使得目录结构更加清晰。</strong></h3>    <p>官方目录结构要求app必须有三个文件 app.json , app.js , app.wxss ,页面有4个文件 index.json , index.js , index.wxml , index.wxss 。而且文件必须同名。</p>    <p>所以使用wepy开发前后开发目录对比如下:</p>    <p>官方DEMO:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/f65f624a3c4ca37a675f8b2efd7990a3.png"></p>    <p>使用wepy框架后目录结构:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e0277fed8ca314438a25b4dae0cf355d.png"></p>    <h3><strong>5. 默认使用babel编译,支持ES6/7的一些新特性。</strong></h3>    <p>用户可以通过修改 .wepyrc 配置文件,配置自己熟悉的babel环境进行开发。默认开启使用了一些新的特性如 promise , async/await 等等。</p>    <p>示例代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/942a98c89c8ec0205ee2e53f9dc24064.png"></p>    <h3><strong>6. 针对原生API进行优化。</strong></h3>    <p>对现在API进行promise处理,同时修复一些现有API的缺陷,比如:wx.request并发问题等。</p>    <p>原有代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/bcddb48e33064a203dff556f08393164.png"></p>    <p>基于wepy实现代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9338e918972b5ad8da0e884652d7ecfc.png"></p>    <p>在同时并发10个request请求测试时:</p>    <p>不使用wepy:</p>    <p><img src="https://simg.open-open.com/show/a634e0753ac79637e055614b4b7bbb22.jpg"> <img src="https://simg.open-open.com/show/cee170d02577a2322e26be63bf34d999.jpg"></p>    <p>使用wepy后:</p>    <p><img src="https://simg.open-open.com/show/f219f3262c33b726df3f1b1cbcff56f7.jpg"></p>    <h2><strong>进阶说明</strong></h2>    <h3><strong>.wepyrc 配置文件说明</strong></h3>    <p>执行 wepy new demo 后,会生成类似配置文件。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e3fcc8de3fceb36117d8ad9b60952715.png"></p>    <p>wpyExt:缺省值为’.wpy’,IDE默认情况下不会对此文件类型高亮,此时可以修改所有文件为 .vue 后缀(因为与vue高亮规则一样),然后将此选项修改为 .vue ,就能解决部分IDE代码高亮问题。</p>    <p>sass:sass编译配置,参见( https://github.com/sass/node-sass )</p>    <p>less:less编译配置,参见( http://lesscss.org/#using-less-usage-in-code )</p>    <p><strong>babel:</strong></p>    <p>babel编译配置,参见(</p>    <p>http://babeljs.io/docs/usage/options/</p>    <p>)</p>    <h3><strong>wpy文件说明</strong></h3>    <p>wpy 文件的编译过程过下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/03810f0a0e97e1bc03e06771fa25b921.jpg"></p>    <p>一个 .wpy 文件分为三个部分:</p>    <ol>     <li> <p>样式 <style></style> 对应原有 wxss 。</p> </li>     <li> <p>模板 <template></template> 对应原有 wxml 。</p> </li>     <li> <p>代码 <script></script> 对应原有 js 。</p> </li>    </ol>    <p>其中入口文件 app.wpy 不需要 template ,所以编译时会被忽略。这三个标签都支持 type 和 src 属性, type 决定了其代码编译过程, src 决定是否外联代码,存在 src 属性且有效时,忽略内联代码,示例如下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c5d1d70746195e97ad42837fb1de6e89.png"></p>    <p>标签对应 type</p>    <p>值如下表所示:</p>    <h3 style="text-align: center;"><img src="https://simg.open-open.com/show/e7f4ad2647d9871fb1963f0ec5b7e4d0.png"></h3>    <h3><strong>Script说明</strong></h3>    <p><strong>程序入口app.wpy</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/7cec80d74148c4f63617535dd16e3798.png"></p>    <p>入口 app.wpy 继承自 wepy.app ,包含一个 config 属性和其全局属性、方法、事件。其中 config 属性对应原有的 app.json ,编译时会根据 config 生成 app.json 文件,如果需要修改 config 中的内容,请使用系统提供API。</p>    <p><strong>页面index.wpy</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/1c40839ac7ade1c9286c08678e1e9391.jpg"></p>    <p>页面入口继承自 wepy.page ,主要属性说明如下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6aea1ef1219ec377e06163de42b5034d.png"></p>    <p><strong>组件com.wpy</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/23e1be7d947fbf2280fc7b832274f695.png"></p>    <p>页面入口继承自 wepy.component ,属性与页面属性一样,除了不需要 config 以及页面特有的一些小程序事件等等。</p>    <h3><strong>组件</strong></h3>    <p>在小程序中,可以利用 JS模块化 和wxml模板 ,对业务模块进行划分,实现如下效果:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/bd5e6765becc55c25444702c40acfb12.png"></p>    <p>但实际上不同的模块代码与事件交互都是在同一个页面空间处理的,比如说 moduleA 和 moduleB 中同时存在一个 add 响应事件时,就需要在 html 和 js 中分别定义为 moduleA_add , moduleB_add 。业务模块复杂之后就不利于开发和维护。</p>    <p>在wepy中,利用组件化的特性可以解决此类问题,如下图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4539b725cddfc6cfca82a710258a4c69.png"></p>    <p>ComA 和 ComB 中间的数据与事件相互隔离,可以分别拥有自己的 add 事件。</p>    <p><strong>组件引用</strong></p>    <p>当页面或者组件需要引入子组件时,需要在页面或者 script 中的 components 给组件分配唯一id,并且在 template 中添加 <component> 标签,如 index.wpy 。</p>    <p>页面和组件都可以引入子组件,引入若干组件后,如下图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/2388aac3821da32e6ae9ea7841dbfada.png"></p>    <p>Index页面引入A,B,C三个组件,同时组件A和B又有自己的子组件D,E,F,G,H。</p>    <p><strong>组件通信与交互</strong></p>    <p>wepy.component 基类提供三个方法 $broadcast , $emit , $invoke ,因此任一页面或任一组件都可以调用上述三种方法实现通信与交互,如:</p>    <pre>  $this.$emit('some-event', 1, 2, 3, 4);</pre>    <p>组件的事件监听需要写在 events 属性下,如:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4be1f39494fdd465fee5c1c09f7944d2.png"></p>    <ol>     <li style="text-align: center;"> <p style="text-align: left;">$broadcast</p> <p>$broadcast 事件是由父组件发起,所有子组件都会收到此广播事件,除非事件被手动取消。事件广播的顺序为广度优先搜索顺序,如上图,如果 Page_Index 发起一个 $broadcast 事件,那么接收到事件的先后顺序为:A, B, C, D, E, F, G, H。如下图:</p> <img src="https://simg.open-open.com/show/b041a79ba3c34fbfce84ba3e4c662b5f.jpg"></li>     <li style="text-align: center;"> <p style="text-align: left;">$emit</p> <p>$emit 与 $broadcast 正好相反,事件发起组件的父组件会依次接收到 $emit 事件,如上图,如果E发起一个 $emit 事件,那么接收到事件的先后顺序为:A, Page_Index。如下图:</p> <img src="https://simg.open-open.com/show/eaf9b23ff02015ab1e69aa9ec91426bf.jpg"></li>     <li> <p>$invoke</p> <p>$invoke 是一个组件对另一个组件的直接调用,通过传入的组件路径找到相应组件,然后再调用其方法。</p> <p>如果想在 Page_Index 中调用组件A的某个方法:</p> <pre>  this.$invoke('ComA', 'someMethod', 'someArgs');</pre> <p>如果想在组件A中调用组件G的某个方法:</p> <pre>  this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');</pre> </li>    </ol>    <h3>数据绑定</h3>    <p>小程序数据绑定方式</p>    <p>小程序通过 Page 提供的 setData 方法去绑定数据,如:</p>    <pre>  this.setData({title: 'this is title'});</pre>    <p>因为小程序架构本身原因,页面渲染层和JS逻辑层分开的,setData操作实际就是JS逻辑层与页面渲染层之间的通信,那么如果在同一次运行周期内多次执行 setData 操作时,那么通信的次数是一次还是多次呢?在经过与开发小程序的同事求证后得知,确实会通信多次。</p>    <p>wepy数据绑定方式</p>    <p>wepy使用脏数据检查对setData进行封装,在函数运行周期结束时执行脏数据检查,一来可以不用关心页面多次setData是否会有性能上的问题,二来可以更加简洁去修改数据实现绑定,不用重复去写setData方法。代码如下:</p>    <pre>  this.title = 'this is title';</pre>    <p>但需注意,在函数运行周期之外的函数里去修改数据需要手动调用 $apply 方法。如:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/5f1e59ae210d440212c409ab4b7ef48f.png"></p>    <p><strong>wepy脏数据检查流程</strong></p>    <p>在执行脏数据检查是,会通过 this.$$phase 标识当前检查状态,并且会保证在并发的流程当中,只会有一个脏数据检查流程在运行,以下是执行脏数据检查的流程图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/f50aa4262115c9a544df2c2ada49ff26.jpg"></p>    <h3><strong>其它优化细节</strong></h3>    <p>1. wx.request 接收参数修改</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/00ef78f5ed5c6e91a43f24b9c1ad4fc7.png"></p>    <p>2. 优化事件参数传递</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e50a6426f6fe179065b6586356fbd9cd.jpg"></p>    <p>3. 改变数据绑定方式</p>    <p>保留setData方法,但不建议使用setData执行绑定,修复传入 undefined 的bug,并且修改入参支持:</p>    <p>this.setData(target, value)<br> this.setData(object)</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/7c0109228d8283093d3ff5cf2f9fc9e2.png"></p>    <p>4. 组件代替模板和模块</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/840b79c5e39066409c774ca6f7f9ea0e.jpg"></p>    <h2><strong>API</strong></h2>    <h3><strong>wepy.event</strong></h3>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/930a0249f19069b9706bf2c261648c6b.png"></p>    <p><strong>wepy.component</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c1a05ee1176cd17fc647edf025bffe5e.jpg"></p>    <h3><strong>wepy.page</strong></h3>    <h3 style="text-align: center;"><img src="https://simg.open-open.com/show/2b798b875b74d62c93be1b66d1b98028.png"> wepy.app</h3>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e07c82dd777d245c8375c6f3ce96015c.png"></p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/2nQzsuqq7Avgs8wsRizUhw</p>    <p> </p>