微信小程序自发布到如今已经有半年多的时间了,凭借微信平台的强大影响力,越来越多企业加入小程序开发。 小程序于M页比相比,有以下优势:
1、小程序拥有更多的能力,包括定位、录音、文件、媒体、各种硬件能力等,想象空间更大
2、运行在微信内部,体验更接近APP
3、在过度竞争的互联网行业中,获取一个有效APP用户的成本已经非常高了,小程序相比APP更加轻量、即用即走, 更容易获取用户
开发对比
从开发角度来讲,小程序官方封装了很多常用组件给开发带来很多便利性,但同时也带来很多不便:
1、小程序重新定义了DOM结构,没有window、document、div、span等,小程序只有view、text、image等 封装好的组件,页面布局只能通过这些基础组件来实现,对开发人员来讲需要一定的习惯转换成本
2、小程序不推荐直接操作DOM(仅仅从2017年7月开始才可以获取DOM和部分属性),如果不熟悉MVVM模式的开发者, 需要很高的学习成本
3、小程序没有cookie,只能通过storage来模拟各项cookie操作(包括http中的setCookie也需要自行处理)
wepy
笔者团队最近开发了多个微信小程序,为了弥补小程序各项不足和延续开发者VUE的开发习惯,团队在开发初期 就选用了wepy框架,该框架是腾讯内部基于小程序的开发框架,设计思路基本参考VUE,开发模式和编码风 格上80%以上接近VUE,开发者可以以很小的成本从VUE开发切换成小程序开发,相比于小程序,主要优点如下:
1、开发模式容易转换 wepy在原有的小程序的开发模式下进行再次封装,更贴近于现有MVVM框架开发模式。框架在开发过程中参考了 一些现在框架的一些特性,并且融入其中,以下是使用wepy前后的代码对比图。
官方DEMO代码:
1 /index.js
2
3 //获取应用实例
4
5 var app =getApp()6
7 Page({8
9 data: {10
11 motto: 'Hello World',12
13 userInfo: {}14
15 },16
17 //事件处理函数
18
19 bindViewTap: function() {20
21 console.log('button clicked')22
23 },24
25 onLoad: function() {26
27 console.log('onLoad')28
29 }30
31 })
基于wepy的实现:
1 import wepy from 'wepy';2
3
4
5 export defaultclass Index extends wepy.page {6
7
8
9 data ={10
11 motto: 'Hello World',12
13 userInfo: {}14
15 };16
17 methods ={18
19 bindViewTap () {20
21 console.log('button clicked');22
23 }24
25 };26
27 onLoad() {28
29 console.log('onLoad');30
31 };32
33 }
2.真正的组件化开发 小程序虽然有标签可以实现组件复用,但仅限于模板片段层面的复用,业务代码与交互事件 仍需在页面处理。无法实现组件化的松耦合与复用的效果。
wepy组件示例
1 //index.wpy
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import wepy from 'wepy';26
27 import List from '../components/list';28
29 import Panel from '../components/panel';30
31 import Counter from '../components/counter';32
33
34
35 export defaultclass Index extends wepy.page {36
37
38
39 config ={40
41 "navigationBarTitleText": "test"
42
43 };44
45 components ={46
47 panel: Panel,48
49 counter1: Counter,50
51 counter2: Counter,52
53 list: List54
55 };56
57 data ={58
59 myNum: 50,60
61 syncNum: 100,62
63 items: [1, 2, 3, 4]64
65 }66
67 }68
69
3.支持加载外部NPM包 小程序较大的缺陷是不支持NPM包,导致无法直接使用大量优秀的开源内容,wepy在编译过程当中,会递归 遍历代码中的require然后将对应依赖文件从node_modules当中拷贝出来,并且修改require为相对路径, 从而实现对外部NPM包的支持。如下图:
4.单文件模式,使得目录结构更加清晰 小程序官方目录结构要求app必须有三个文件app.json,app.js,app.wxss,页面有4个文件 index.json,index.js,index.wxml,index.wxss。而且文 件必须同名。 所以使用wepy开发前后开发目录对比如下:
官方DEMO:
1 project2
3 ├── pages4
5 |├── index6
7 | |├── index.json index 页面配置8
9 | |├── index.js index 页面逻辑10
11 | |├── index.wxml index 页面结构12
13 | |└── index.wxss index 页面样式表14
15 |└── log16
17 |├── log.json log 页面配置18
19 |├── log.wxml log 页面逻辑20
21 |├── log.js log 页面结构22
23 |└── log.wxss log 页面样式表24
25 ├── app.js 小程序逻辑26
27 ├── app.json 小程序公共设置28
29 └── app.wxss 小程序公共样式表
使用wepy框架后目录结构:
1 project2
3 └── src4
5 ├── pages6
7 |├── index.wpy index 页面配置、结构、样式、逻辑8
9 |└── log.wpy log 页面配置、结构、样式、逻辑10
11 └──app.wpy 小程序配置项(全局样式配置、声明钩子等)
5.默认使用babel编译,支持ES6/7的一些新特性。
6.wepy支持使用less
默认开启使用了一些新的特性如promise,async/await等等
如何开发
快速起步
安装
1 npm install wepy-cli -g
脚手架
1 wepy new myproject
切换至项目目录
1 cd myproject
实时编译
1 wepy build --watch
目录结构
1 ├── dist 微信开发者工具指定的目录2
3 ├── node_modules4
5 ├── src 代码编写的目录6
7 |├── components 组件文件夹(非完整页面)8
9 | |├── com_a.wpy 可复用组件 a10
11 | |└── com_b.wpy 可复用组件 b12
13 |├── pages 页面文件夹(完整页面)14
15 | |├── index.wpy 页面 index16
17 | |└── page.wpy 页面 page18
19 |└── app.wpy 小程序配置项(全局样式配置、声明钩子等)20
21 └── package.json package 配置
wepy和VUE在编码风格上面非常相似,VUE开发者基本可以无缝切换,因此这里仅介绍两者的主要区别:
1.二者均支持props、data、computed、components、methods、watch(wepy中是watcher), 但wepy中的methods仅可用于页面事件绑定,其他自定义方法都要放在外层,而VUE中所有方法均放在 methods下
2.wepy中props传递需要加上.sync修饰符(类似VUE1.x)才能实现props动态更新,并且父组件再 变更传递给子组件props后要执行this.$apply()方法才能更新
3.wepy支持数据双向绑定,子组件在定义props时加上twoway:true属性值即可实现子组件修改父组 件数据
4.VUE2.x推荐使用eventBus方式进行组件通信,而在wepy中是通过$broadcast,$emit,$invoke 三种方法实现通信
1 · 首先事件监听需要写在events属性下:2
3 ``` bash4
5 import wepy from 'wepy';6
7 export defaultclass Com extends wepy.component {8
9 components ={};10
11 data ={};12
13 methods ={};14
15 events ={16
17 'some-event': (p1, p2, p3, $event) =>{18
19 console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`);20
21 }22
23 };24
25 //Other properties
26
27 }28
29 ```30
31 · $broadcast:父组件触发所有子组件事件32
33
34
35 · $emit:子组件触发父组件事件36
37
38
39 · $invoke:子组件触发子组件事件
5.VUE的生命周期包括created、mounted等,wepy仅支持小程序的生命周期:onLoad、onReady等
6.wepy不支持过滤器、keep-alive、ref、transition、全局插件、路由管理、服务端渲染等VUE特性技术
wepy原理研究
虽然wepy提升了小程序开发体验,但毕竟最终要运行在小程序环境中,归根结底wepy还是需要编译成小程序 需要的格式,因此wepy的核心在于代码解析与编译。
wepy项目文件主要有两个: wepy-cli:用于把.wpy文件提取分析并编译成小程序所要求的wxml、wxss、js、json格式 wepy:编译后js文件中的js框架
wepy编译过程
拆解过程核心代码
1 //wepy自定义属性替换成小程序标准属性过程
2
3 return content.replace(//ig, (tag, tagName) =>{4
5 tagName =tagName.toLowerCase();6
7 return tag.replace(/\s+:([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { //replace :param.sync => v-bind:param.sync
8
9 if (type === '.once' || type === '.sync') {10
11 }12
13 else
14
15 type = '.once';16
17 return ` v-bind:${name}${type}=`;18
19 }).replace(/\s+\@([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { //replace @change => v-on:change
20
21 const prefix = type !== '.user' ? (type === '.stop' ? 'catch' : 'bind') : 'v-on:';22
23 return ` ${prefix}${name}=`;24
25 });26
27 });28
29
30
31 ...32
33 //按xml格式解析wepy文件
34
35 xml = this.createParser().parseFromString(content);36
37 const moduleId =util.genId(filepath);38
39 //提取后的格式
40
41 let rst ={42
43 moduleId: moduleId,44
45 style: [],46
47 template: {48
49 code: '',50
51 src: '',52
53 type: ''
54
55 },56
57 script: {58
59 code: '',60
61 src: '',62
63 type: ''
64
65 }66
67 };68
69 //循环拆解提取过程
70
71 [].slice.call(xml.childNodes || []).forEach((child) =>{72
73 const nodeName =child.nodeName;74
75 if (nodeName === 'style' || nodeName === 'template' || nodeName === 'script') {76
77 let rstTypeObj;78
79
80
81 if (nodeName === 'style') {82
83 rstTypeObj = {code: ''};84
85 rst[nodeName].push(rstTypeObj);86
87 } else{88
89 rstTypeObj =rst[nodeName];90
91 }92
93
94
95 rstTypeObj.src = child.getAttribute('src');96
97 rstTypeObj.type = child.getAttribute('lang') || child.getAttribute('type');98
99 if (nodeName === 'style') {100
101 //针对于 style 增加是否包含 scoped 属性
102
103 rstTypeObj.scoped = child.getAttribute('scoped') ? true : false;104
105 }106
107
108
109 if(rstTypeObj.src) {110
111 rstTypeObj.src =path.resolve(opath.dir, rstTypeObj.src);112
113 }114
115
116
117 if (rstTypeObj.src &&util.isFile(rstTypeObj.src)) {118
119 const fileCode = util.readFile(rstTypeObj.src, 'utf-8');120
121 if (fileCode === null) {122
123 throw '打开文件失败: ' +rstTypeObj.src;124
125 } else{126
127 rstTypeObj.code +=fileCode;128
129 }130
131 } else{132
133 [].slice.call(child.childNodes || []).forEach((c) =>{134
135 rstTypeObj.code +=util.decode(c.toString());136
137 });138
139 }140
141
142
143 if (!rstTypeObj.src)144
145 rstTypeObj.src = path.join(opath.dir, opath.name +opath.ext);146
147 }148
149 });150
151 ...152
153 //拆解提取wxml过程
154
155 (() =>{156
157 if (rst.template.type !== 'wxml' && rst.template.type !== 'xml') {158
159 let compiler =loader.loadCompiler(rst.template.type);160
161 if (compiler &&compiler.sync) {162
163 if (rst.template.type === 'pug') { //fix indent for pug, https://github.com/wepyjs/wepy/issues/211
164
165 let indent =util.getIndent(rst.template.code);166
167 if(indent.firstLineIndent) {168
169 rst.template.code = util.fixIndent(rst.template.code, indent.firstLineIndent * -1, indent.char);170
171 }172
173 }174
175 //调用wxml解析模块
176
177 let compilerConfig =config.compilers[rst.template.type];178
179
180
181 //xmldom replaceNode have some issues when parsing pug minify html, so if it's not set, then default to un-minify html.
182
183 if (compilerConfig.pretty ===undefined) {184
185 compilerConfig.pretty = true;186
187 }188
189 rst.template.code = compiler.sync(rst.template.code, config.compilers[rst.template.type] ||{});190
191 rst.template.type = 'wxml';192
193 }194
195 }196
197 if(rst.template.code)198
199 rst.template.node = this.createParser().parseFromString(util.attrReplace(rst.template.code));200
201 })();202
203
204
205 //提取import资源文件过程
206
207 (() =>{208
209 let coms ={};210
211 rst.script.code.replace(/import\s*([\w\-\_]*)\s*from\s*['"]([\w\-\_\.\/]*)['"]/ig, (match, com, path) =>{212
213 coms[com] =path;214
215 });216
217
218
219 let match = rst.script.code.match(/[\s\r\n]components\s*=[\s\r\n]*/);220
221 match = match ? match[0] : undefined;222
223 let components = match ? this.grabConfigFromScript(rst.script.code, rst.script.code.indexOf(match) + match.length) : false;224
225 let vars = Object.keys(coms).map((com, i) => `var ${com} = "${coms[com]}";`).join('\r\n');226
227 try{228
229 if(components) {230
231 rst.template.components = newFunction(`${vars}\r\nreturn ${components}`)();232
233 } else{234
235 rst.template.components ={};236
237 }238
239 } catch(e) {240
241 util.output('错误', path.join(opath.dir, opath.base));242
243 util.error(`解析components出错,报错信息:${e}\r\n${vars}\r\nreturn ${components}`);244
245 }246
247 })();248
249 ...
wepy中有专门的script、style、template、config解析模块 以template模块举例:
1 //compile-template.js
2
3 ...4
5 //将拆解处理好的wxml结构写入文件
6
7 getTemplate (content) {8
9 content = `${content}`;
10
11 let doc = newDOMImplementation().createDocument();12
13 let node = newDOMParser().parseFromString(content);14
15 let template = [].slice.call(node.childNodes || []).filter((n) => n.nodeName === 'template');16
17
18
19 [].slice.call(template[0].childNodes || []).forEach((n) =>{20
21 doc.appendChild(n);22
23 });24
25 ...26
27 returndoc;28
29 },30
31 //处理成微信小程序所需的wxml格式
32
33 compileXML (node, template, prefix, childNodes, comAppendAttribute = {}, propsMapping ={}) {34
35 //处理slot
36
37 this.updateSlot(node, childNodes);38
39 //处理数据绑定bind方法
40
41 this.updateBind(node, prefix, {}, propsMapping);42
43 //处理className
44
45 if (node &&node.documentElement) {46
47 Object.keys(comAppendAttribute).forEach((key) =>{48
49 if (key === 'class') {50
51 let classNames = node.documentElement.getAttribute('class').split(' ').concat(comAppendAttribute[key].split(' ')).join(' ');52
53 node.documentElement.setAttribute('class', classNames);54
55 } else{56
57 node.documentElement.setAttribute(key, comAppendAttribute[key]);58
59 }60
61 });62
63 }64
65 //处理repeat标签
66
67 let repeats = util.elemToArray(node.getElementsByTagName('repeat'));68
69 ...70
71
72
73 //处理组件
74
75 let componentElements = util.elemToArray(node.getElementsByTagName('component'));76
77 ...78
79 returnnode;80
81 },82
83
84
85 //template文件编译模块
86
87 compile (wpy){88
89 ...90
91 //将编译好的内容写入到文件
92
93 let plg = newloader.PluginHelper(config.plugins, {94
95 type: 'wxml',96
97 code: util.decode(node.toString()),98
99 file: target,100
101 output (p) {102
103 util.output(p.action, p.file);104
105 },106
107 done (rst) {108
109 //写入操作
110
111 util.output('写入', rst.file);112
113 rst.code =self.replaceBooleanAttr(rst.code);114
115 util.writeFile(target, rst.code);116
117 }118
119 });120
121 }
编译前后文件对比
wepy编译前的文件:
1
2
3
4
5
6
7
8
9
10
11
wepy编译后的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13 {{item.title}}
14
15
16
17
18
19 0}}"class="item-nowPrice">¥{{item.price}}
20
21 0}}"class="item-oriPrice">¥{{item.originalPrice}}
22
23
24
25 {{item.cityName}}{{item.cityName&&item.businessName?' | ':''}}{{item.businessName}}
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
可以看到wepy将页面中所有引入的组件都直接写入页面当中,并且按照微信小程序的格式来输出 当然也从一个侧面看出,使用wepy框架后,代码风格要比原生的更加简洁优雅
以上是wepy实现原理的简要分析,有兴趣的朋友可以去阅读源码(https://github.com/wepyjs/wepy)。 综合来讲,wepy的核心在于编译环节,能够将优雅简洁的类似VUE风格的代码,编译成微信小程序所需要的繁杂代码。
wepy作为一款优秀的微信小程序框架,可以帮我们大幅提高开发效率,在为数不多的小程序框架中一枝独秀,希望有更多的团队选择wepy。
PS:wepy也在实现小程序和VUE代码同构,但目前还处在开发阶段,如果未来能实现一次开发,同时产出小程序和M页,将是一件非常爽的事情。