【微信小程序项目实践总结】30分钟从陌生到熟悉


前言

我们之前对小程序做了基本学习:

阅读本文之前,如果大家想对小程序有更深入的了解,或者一些细节的了解可以先阅读上述文章,本文后面点需要对着代码调试阅读

对应的github地址是:https://github.com/yexiaochai/wxdemo

首先我们来一言以蔽之,什么是微信小程序?PS:这个问题问得好像有些扯:)

小程序是一个不需要下载安装就可使用的应用,它实现了应用触手可及的梦想,用户扫一扫或者搜一下即可打开应用。也体现了用完即走的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。从字面上看小程序具有类似Web应用的热部署能力,在功能上又接近于原生APP。

所以说,其实微信小程序是一套超级Hybrid的解决方案,现在看来,小程序应该是应用场景最广,也最为复杂的解决方案了

很多公司都会有自己的Hybrid平台,我这里了解到比较不错的是携程的Hybrid平台、阿里的Weex、百度的糯米,但是从应用场景来说都没有微信来得丰富,这里根本的区别是:

微信小程序是给各个公司开发者接入的,其他公司平台多是给自己业务团队使用,这一根本区别,就造就了我们看到的很多小程序不一样的特性:

① 小程序定义了自己的标签语言WXML

② 小程序定义了自己的样式语言WXSS

③ 小程序提供了一套前端框架包括对应Native API

④ 禁用浏览器Dom API(这个区别,会影响我们的代码方式)

只要了解到这些区别就会知道为什么小程序会这么设计:

因为小程序是给各个公司的开发做的,其他公司的Hybrid方案是给公司业务团队用的,一般拥有Hybrid平台的公司实力都不错
但是开发小程序的公司实力良莠不齐,所以小程序要做绝对的限制,最大程度的保证框架层(小程序团队)对程序的控制
因为毕竟程序运行在微信这种体量的APP中

之前我也有一个疑惑为什么微信小程序会设计自己的标签语言,也在知乎看到各种各样的回答,但是如果出于设计层面以及应用层面考虑的话:这样会有更好的控制,而且我后面发现微信小程序事实上依旧使用的是webview做渲染(这个与我之前认为微信是NativeUI是向左的),但是如果我们使用的微信限制下面的标签,这个是有限的标签,后期想要换成NativeUI会变得更加轻易:

【微信小程序项目实践总结】30分钟从陌生到熟悉

另一方面,经过之前的学习,我这边明确可以得出一个感受:

小程序的页面核心是标签,标签是不可控制的(我暂时没用到js操作元素的方法),只能按照微信给的玩法玩,标签控制显示是我们的view

② 标签的展示只与data有关联,和js是隔离的,没有办法在标签中调用js的方法

③ 而我们的js的唯一工作便是根据业务改变data,重新引发页面渲染,以后别想操作DOM,别想操作Window对象了,改变开发方式,改变开发方式,改变开发方式!

1 this.setData({'wxml': `
2   <my-component>
3   <view>动态插入的节点</view> 
4   </my-component>
5 `});

【微信小程序项目实践总结】30分钟从陌生到熟悉 

然后可以看到这个是一个MVC模型

【微信小程序项目实践总结】30分钟从陌生到熟悉

每个页面的目录是这个样子的:

 1 project
 2 ├── pages
 3 |   ├── index
 4 |   |   ├── index.json  index 页面配置
 5 |   |   ├── index.js    index 页面逻辑
 6 |   |   ├── index.wxml  index 页面结构
 7 |   |   └── index.wxss  index 页面样式表
 8 |   └── log
 9 |       ├── log.json    log 页面配置
10 |       ├── log.wxml    log 页面逻辑
11 |       ├── log.js      log 页面结构
12 |       └── log.wxss    log 页面样式表
13 ├── app.js              小程序逻辑
14 ├── app.json            小程序公共设置
15 └── app.wxss            小程序公共样式表

每个组件的目录也大概是这个样子的,大同小异,但是入口是Page层。

小程序打包后的结构(这里就真的不懂了,引用:小程序底层框架实现原理解析):

【微信小程序项目实践总结】30分钟从陌生到熟悉

所有的小程序基本都最后都被打成上面的结构

1、WAService.js  框架JS库,提供逻辑层基础的API能力

2、WAWebview.js 框架JS库,提供视图层基础的API能力

3、WAConsole.js 框架JS库,控制台

4、app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型

5、app-service.js 我们自己的JS代码,全部打包到这个文件

6、page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里

7、pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域

从设计的角度上说,小程序采用的组件化开发的方案,除了页面级别的标签,后面全部是组件,而组件中的标签view、data、js的关系应该是与page是一致的,这个也是我们平时建议的开发方式,将一根页面拆分成一个个小的业务组件或者UI组件:

【微信小程序项目实践总结】30分钟从陌生到熟悉

从我写业务代码过程中,觉得整体来说还是比较顺畅的,小程序是有自己一套完整的前端框架的,并且释放给业务代码的主要就是page,而page只能使用标签和组件,所以说框架的对业务的控制力度很好。

最后我们从工程角度来看微信小程序的架构就更加完美了,小程序从三个方面考虑了业务者的感受:

① 开发工具+调试工具

② 开发基本模型(开发基本标准WXML、WXSS、JS、JSON)

③ 完善的构建(对业务方透明)

④ 自动化上传离线包(对业务费透明离线包逻辑)

⑤ 监控统计逻辑

所以,微信小程序从架构上和使用场景来说是很令人惊艳的,至少惊艳了我……所以我们接下来在开发层面对他进行更加深入的剖析,我们这边最近一直在做基础服务,这一切都是为了完善技术体系,这里对于前端来说便是我们需要做一个Hybrid体系,如果做App,React Native也是不错的选择,但是一定要有完善的分层:

① 底层框架解决开发效率,将复杂的部分做成一个黑匣子,给页面开发展示的只是固定的三板斧,固定的模式下开发即可

② 工程部门为业务开发者封装最小化开发环境,最优为浏览器,确实不行便为其提供一个类似浏览器的调试环境

如此一来,业务便能快速迭代,因为业务开发者写的代码大同小异,所以底层框架配合工程团队(一般是同一个团队),便可以在底层做掉很多效率性能问题。

稍微大点的公司,稍微宽裕的团队,还会同步做很多后续的性能监控、错误日志工作,如此形成一套文档->开发->调试->构建->发布->监控、分析 为一套完善的技术体系

如果形成了这么一套体系,那么后续就算是内部框架更改、技术革新,也是在这个体系上改造,这块微信小程序是做的非常好的。但很可惜,很多其他公司团队只会在这个路径上做一部分,后面由于种种原因不在深入,有可能是感觉没价值,而最恐怖的行为是,自己的体系没形成就贸然的换基础框架,戒之慎之啊!好了闲话少说,我们继续接下来的学习。

我对小程序的理解有限,因为没有源码只能靠惊艳猜测,如果文中有误,请各位多多提点

文章更多面对初中级选手,如果对各位有用,麻烦点赞哟

微信小程序的执行流程

微信小程序为了对业务方有更强的控制,App层做的工作很有限,我后面写demo的时候根本没有用到app.js,所以我这里认为app.js只是完成了一个路由以及初始化相关的工作,这个是我们看得到的,我们看不到的是底层框架会根据app.json的配置将所有页面js都准备好。

我这里要表达的是,我们这里配置了我们所有的路由:

"pages":[
  "pages/index/index",
  "pages/list/list",
  "pages/logs/logs"
],

微信小程序一旦载入,会开3个webview,装载3个页面的逻辑,完成基本的实例化工作,只显示首页!这个是小程序为了优化页面打开速度所做的工作,也势必会浪费一些资源,所以到底是全部打开或者预加载几个,详细底层Native会根据实际情况动态变化,我们也可以看到,从业务层面来说,要了解小程序的执行流程,其实只要能了解Page的流程就好了,关于Page生命周期,除了释放出来的API:onLoad -> onShow -> onReady -> onHide等,官方还出了一张图进行说明:

【微信小程序项目实践总结】30分钟从陌生到熟悉

Native层在载入小程序时候,起了两个线程一个的view Thread一个是AppService Thread,我这边理解下来应该就是程序逻辑执行与页面渲染分离,小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

因为之前我认为页面是使用NativeUI做渲染跟Webview没撒关系,便觉得这个图有问题,但是后面实际代码看到了熟悉的shadow-dom以及Android可以看到哪部分是Web的,其实小程序主体还是使用的浏览器渲染的方式,还是webview装载HTML和CSS的逻辑,最后我发现这张图是没有问题的,有问题的是我的理解,哈哈,这里我们重新解析这张图:

WXML先会被编译成JS文件,引入数据后在WebView中渲染,这里可以认为微信载入小程序时同时初始化了两个线程,分别执行彼此逻辑:

① WXML&CSS编译形成的JS View实例化结束,准备结束时向业务线程发送通知

② 业务线程中的JS Page部分同步完成实例化结束,这个时候接收到View线程部分的等待数据通知,将初始化data数据发送给View

③ View线程接到数据,开始渲染页面,渲染结束执行通知Page触发onReady事件

这里翻开源码,可以看到,应该是全局控制器完成的Page实例化,完成后便会执行onLoad事件,但是在执行前会往页面发通知:

1 __appServiceSDK__.invokeWebviewMethod({
2     name: "appDataChange",
3     args: o({}, e, {
4         complete: n
5     }),
6     webviewIds: [t]
7 })

【微信小程序项目实践总结】30分钟从陌生到熟悉

【微信小程序项目实践总结】30分钟从陌生到熟悉

真实的逻辑是这样的,全局控制器会完成页面实例化,这个是根据app.json中来的,全部完成实例化存储起来然后选择第一个page实例执行一些逻辑,然后通知view线程,即将执行onLoad事件,因为view线程和业务线程是两个线程,所以不会造成阻塞,view线程根据初始数据完成渲染,而业务线程继续后续逻辑,执行onLoad,如果onLoad中有setData,那么会进入队列继续通知view线程更新。

所以我个人感觉微信官网那张图不太清晰,我这里重新画了一个图:

【微信小程序项目实践总结】30分钟从陌生到熟悉

引用一张其他地方的图

【微信小程序项目实践总结】30分钟从陌生到熟悉

模拟实现

都这个时候了,不来个简单的小程序框架实现好像有点不对,我们做小程序实现的主要原因是想做到一端代码三端运行:web、小程序、Hybrid甚至Servce端

我们这里没有可能实现太复杂的功能,这里想的是就实现一个基本的页面展示带一个最基本的标签即可,只做Page一块的简单实现,让大家能了解到小程序可能的实现,以及如何将小程序直接转为H5的可能走法

【微信小程序项目实践总结】30分钟从陌生到熟悉

1 <view>
2   <!-- 以下是对一个自定义组件的引用 -->
3   <my-component inner-text="组件数据"></my-component>
4   <view>{{pageData}}</view>
5 </view>
1 Page({
2   data: {
3     pageData: '页面数据'
4   },
5   onLoad: function () {
6     console.log('onLoad')
7   },
8 })
1 <!-- 这是自定义组件的内部WXML结构 -->
2 <view class="inner">
3   {{innerText}}
4 </view>
5 <slot></slot>
 1 Component({
 2   properties: {
 3     // 这里定义了innerText属性,属性值可以在组件使用时指定
 4     innerText: {
 5       type: String,
 6       value: 'default value',
 7     }
 8   },
 9   data: {
10     // 这里是一些组件内部数据
11     someData: {}
12   },
13   methods: {
14     // 这里是一个自定义方法
15     customMethod: function () { }
16   }
17 })

我们直接将小程序这些代码拷贝一份到我们的目录:

【微信小程序项目实践总结】30分钟从陌生到熟悉

我们需要做的就是让这段代码运行起来,而这里的目录是我们最终看见的目录,真实运行的时候可能不是这个样,运行之前项目会通过我们的工程构建,变成可以直接运行的代码,而我这里思考的可以运行的代码事实上是一个模块,所以我们这里从最终结果反推、分拆到开发结构目录,我们首先将所有代码放到index.html,可能是这样的:

【微信小程序项目实践总结】30分钟从陌生到熟悉【微信小程序项目实践总结】30分钟从陌生到熟悉

  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4   <meta charset="UTF-8">
  5   <title>Title</title>
  6 </head>
  7 <body>
  8 
  9 <script type="text/javascript" src="libs/zepto.js" ></script>
 10 <script type="text/javascript">
 11 
 12   class View {
 13     constructor(opts) {
 14       this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';
 15 
 16       //由控制器page传入的初始数据或者setData产生的数据
 17       this.data = {
 18         pageShow: 'pageshow',
 19         pageData: 'pageData',
 20         pageShow1: 'pageShow1'
 21       };
 22 
 23       this.labelMap = {
 24         'view': 'div',
 25         '#text': 'span'
 26       };
 27 
 28       this.nodes = {};
 29       this.nodeInfo = {};
 30     }
 31 
 32     /*
 33       传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变
 34       并且将其中包含{{}}标志的节点信息记录下来
 35     */
 36     _handlerNode (node) {
 37 
 38       let reg = //{/{([/s/S]+?)/}/}/;
 39       let result, name, value, n, map = {};
 40       let attrs , i, len, attr;
 41 
 42       name = node.nodeName;
 43       attrs = node.attributes;
 44       value = node.nodeValue;
 45       n = document.createElement(this.labelMap[name.toLowerCase()] || name);
 46 
 47       //说明是文本,需要记录下来了
 48       if(node.nodeType === 3) {
 49         n.innerText =  this.data[value] || '';
 50 
 51         result =  reg.exec(value);
 52         if(result) {
 53           n.innerText =  this.data[result[1]] || '';
 54 
 55           if(!map[result[1]]) map[result[1]] = [];
 56           map[result[1]].push({
 57             type: 'text',
 58             node: n
 59           });
 60         }
 61       }
 62 
 63       if(attrs) {
 64         //这里暂时只处理属性和值两种情况,多了就复杂10倍了
 65         for (i = 0, len = attrs.length; i < len; i++) {
 66           attr = attrs[i];
 67           result = reg.exec(attr.value);
 68 
 69           n.setAttribute(attr.name, attr.value);
 70           //如果有node需要处理则需要存下来标志
 71           if (result) {
 72             n.setAttribute(attr.name, this.data[result[1]] || '');
 73 
 74             //存储所有会用到的节点,以便后面动态更新
 75             if (!map[result[1]]) map[result[1]] = [];
 76             map[result[1]].push({
 77               type: 'attr',
 78               name: attr.name,
 79               node: n
 80             });
 81 
 82           }
 83         }
 84       }
 85 
 86       return {
 87         node: n,
 88         map: map
 89       }
 90 
 91     }
 92 
 93     //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止
 94     _runAllNode(node, map, root) {
 95 
 96       let nodeInfo = this._handlerNode(node);
 97       let _map = nodeInfo.map;
 98       let n = nodeInfo.node;
 99       let k, i, len, children = node.childNodes;
100 
101       //先将该根节点插入到上一个节点中
102       root.appendChild(n);
103 
104       //处理map数据,这里的map是根对象,最初的map
105       for(k in _map) {
106         if(map[k]) {
107           map[k].push(_map[k]);
108         } else {
109           map[k] = _map[k];
110         }
111       }
112 
113       for(i = 0, len = children.length; i < len; i++) {
114         this._runAllNode(children[i], map, n);
115       }
116 
117     }
118 
119     //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录
120     splitTemplate () {
121       let nodes = $(this.template);
122       let map = {}, root = document.createElement('div');
123       let i, len;
124 
125       for(i = 0, len = nodes.length; i < len; i++) {
126         this._runAllNode(nodes[i], map, root);
127       }
128 
129       window.map = map;
130       return root
131     }
132 
133       //拆分目标形成node,这个方法过长,真实项目需要拆分
134     splitTemplate1 () {
135       let template = this.template;
136       let node = $(this.template)[0];
137       let map = {}, n, name, root = document.createElement('div');
138       let isEnd = false, index = 0, result;
139 
140       let attrs, i, len, attr;
141       let reg = //{/{([/s/S]+?)/}/}/;
142 
143       window.map = map;
144 
145       //开始遍历节点,处理
146       while (!isEnd) {
147         name = node.localName;
148         attrs = node.attributes;
149         value = node.nodeValue;
150         n = document.createElement(this.labelMap[name] || name);
151 
152         //说明是文本,需要记录下来了
153         if(node.nodeType === 3) {
154           n.innerText =  this.data[value] || '';
155 
156           result =  reg.exec(value);
157           if(result) {
158             n.innerText =  this.data[value] || '';
159 
160             if(!map[value]) map[value] = [];
161             map[value].push({
162               type: 'text',
163               node: n
164             });
165           }
166         }
167 
168         //这里暂时只处理属性和值两种情况,多了就复杂10倍了
169         for(i = 0, len = attrs.length; i < len; i++) {
170           attr = attrs[i];
171           result =  reg.exec(attr.value);
172 
173           n.setAttribute(attr.name, attr.value);
174           //如果有node需要处理则需要存下来标志
175           if(result) {
176             n.setAttribute(attr.name, this.data[result[1]] || '');
177 
178             //存储所有会用到的节点,以便后面动态更新
179             if(!map[result[1]]) map[result[1]] = [];
180             map[result[1]].push({
181               type: 'attr',
182               name: attr.name,
183               node: n
184             });
185 
186           }
187         }
188 
189 debugger
190 
191         if(index === 0) root.appendChild(n);
192         isEnd = true;
193         index++;
194 
195       }
196 
197       return root;
198 
199 
200       console.log(node)
201     }
202 
203   }
204 
205   let view = new View();
206 
207   document.body.appendChild(window.node)
208 
209 </script>
210 </body>
211 </html>

模拟核心代码

这段代码,非常简单:

① 设置了一段模板,甚至,我们这里根本不关系其格式化状态,直接写成一行方便处理

this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';

② 然后我们将这段模板转为node节点(这里可以不用zepto,但是模拟实现怎么简单怎么来吧),然后遍历处理所有节点,我们就可以处理我们的数据了,最终形成了这个html:

1 <div><div><span>ffsd</span></div><div class="ddd" is-show="pageshow"><span>pageshow</span><div class="c1"><span>pageData</span></div></div></div>

③ 与此同时,我们存储了一个对象,这个对象包含所有与之相关的节点:

【微信小程序项目实践总结】30分钟从陌生到熟悉

这个对象是所有setData会影响到node的一个映射表,后面调用setData的时候,便可以直接操作对应的数据了,这里我们分拆我们代码,形成了几个关键部分,首先是View类,这个对应我们的模板,是核心类:

【微信小程序项目实践总结】30分钟从陌生到熟悉【微信小程序项目实践总结】30分钟从陌生到熟悉

  1 //View为模块的实现,主要用于解析目标生产node
  2 class View {
  3   constructor(template) {
  4     this.template = template;
  5 
  6     //由控制器page传入的初始数据或者setData产生的数据
  7     this.data = {};
  8 
  9     this.labelMap = {
 10       'view': 'div',
 11       '#text': 'span'
 12     };
 13 
 14     this.nodes = {};
 15     this.root = {};
 16   }
 17 
 18   setInitData(data) {
 19     this.data = data;
 20   }
 21 
 22   //数据便会引起的重新渲染
 23   reRender(data, allData) {
 24     this.data = allData;
 25     let k, v, i, len, j, len2, v2;
 26 
 27     //开始重新渲染逻辑,寻找所有保存了的node
 28     for(k in data) {
 29       if(!this.nodes[k]) continue;
 30       for(i = 0, len = this.nodes[k].length; i < len; i++) {
 31         for(j = 0; j < this.nodes[k][i].length; j++) {
 32           v = this.nodes[k][i][j];
 33           if(v.type === 'text') {
 34             v.node.innerText = data[k];
 35           } else if(v.type === 'attr') {
 36             v.node.setAttribute(v.name, data[k]);
 37           }
 38         }
 39       }
 40     }
 41   }
 42   /*
 43     传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变
 44     并且将其中包含{{}}标志的节点信息记录下来
 45   */
 46   _handlerNode (node) {
 47 
 48     let reg = //{/{([/s/S]+?)/}/}/;
 49     let result, name, value, n, map = {};
 50     let attrs , i, len, attr;
 51 
 52     name = node.nodeName;
 53     attrs = node.attributes;
 54     value = node.nodeValue;
 55     n = document.createElement(this.labelMap[name.toLowerCase()] || name);
 56 
 57     //说明是文本,需要记录下来了
 58     if(node.nodeType === 3) {
 59       n.innerText =  this.data[value] || '';
 60 
 61       result =  reg.exec(value);
 62       if(result) {
 63         n.innerText =  this.data[result[1]] || '';
 64 
 65         if(!map[result[1]]) map[result[1]] = [];
 66         map[result[1]].push({
 67           type: 'text',
 68           node: n
 69         });
 70       }
 71     }
 72 
 73     if(attrs) {
 74       //这里暂时只处理属性和值两种情况,多了就复杂10倍了
 75       for (i = 0, len = attrs.length; i < len; i++) {
 76         attr = attrs[i];
 77         result = reg.exec(attr.value);
 78 
 79         n.setAttribute(attr.name, attr.value);
 80         //如果有node需要处理则需要存下来标志
 81         if (result) {
 82           n.setAttribute(attr.name, this.data[result[1]] || '');
 83 
 84           //存储所有会用到的节点,以便后面动态更新
 85           if (!map[result[1]]) map[result[1]] = [];
 86           map[result[1]].push({
 87             type: 'attr',
 88             name: attr.name,
 89             node: n
 90           });
 91 
 92         }
 93       }
 94     }
 95 
 96     return {
 97       node: n,
 98       map: map
 99     }
100 
101   }
102 
103   //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止
104   _runAllNode(node, map, root) {
105 
106     let nodeInfo = this._handlerNode(node);
107     let _map = nodeInfo.map;
108     let n = nodeInfo.node;
109     let k, i, len, children = node.childNodes;
110 
111     //先将该根节点插入到上一个节点中
112     root.appendChild(n);
113 
114     //处理map数据,这里的map是根对象,最初的map
115     for(k in _map) {
116       if(!map[k]) map[k] = [];
117       map[k].push(_map[k]);
118     }
119 
120     for(i = 0, len = children.length; i < len; i++) {
121       this._runAllNode(children[i], map, n);
122     }
123 
124   }
125 
126   //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录
127   splitTemplate () {
128     let nodes = $(this.template);
129     let map = {}, root = document.createElement('div');
130     let i, len;
131 
132     for(i = 0, len = nodes.length; i < len; i++) {
133       this._runAllNode(nodes[i], map, root);
134     }
135 
136     this.nodes = map;
137     this.root = root;
138   }
139 
140   render() {
141     let i, len;
142     this.splitTemplate();
143     for(i = 0, len = this.root.childNodes.length; i< len; i++)
144       document.body.appendChild(this.root.childNodes[0]);
145   }
146 
147 }

核心模板处理类View

这个类主要完成的工作是:

① 接受传入的template字符串(直接由index.wxml读出)

② 解析template模板,生成字符串和兼职与node映射表,方便后期setData导致的改变

③ 渲染和再次渲染工作

然后就是我们的Page类的实现了,这里反而比较简单(当然这里的实现是不完善的):

 1 //这个为js罗杰部分实现,后续会释放工厂方法
 2 class PageClass {
 3   //构造函数,传入对象
 4   constructor(opts) {
 5 
 6     //必须拥有的参数
 7     this.data = {};
 8     Object.assign(this, opts);
 9   }
10 
11   //核心方法,每个Page对象需要一个模板实例
12   setView(view) {
13     this.view = view;
14   }
15 
16   //核心方法,设置数据后会引发页面刷新
17   setData(data) {
18     Object.assign(this.data, data);
19 
20     //只影响改变的数据
21     this.view.reRender(data, this.data)
22   }
23 
24   render() {
25     this.view.setInitData(this.data);
26     this.view.render();
27 
28     if(this.onLoad) this.onLoad();
29   }
30 
31 }

现在轮着我们实际调用方,Page方法出场了:

function Page (data) {
  let page = new PageClass(data);
  return page;
}

基本上什么都没有干的感觉,调用层代码这样写:

 1 function main() {
 2   let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');
 3   let page = Page({
 4     data: {
 5       pageShow: 'pageshow',
 6       pageData: 'pageData',
 7       pageShow1: 'pageShow1'
 8     },
 9     onLoad: function () {
10       this.setData({
11         pageShow: '我是pageShow啊'
12       });
13     }
14   });
15 
16   page.setView(view);
17   page.render();
18 }
19 
20 main();

于是,我们可以看到页面的变化,由开始的初始化页面到执行onLoad时候的变化:

【微信小程序项目实践总结】30分钟从陌生到熟悉

【微信小程序项目实践总结】30分钟从陌生到熟悉

这里是最终完整的代码:

【微信小程序项目实践总结】30分钟从陌生到熟悉【微信小程序项目实践总结】30分钟从陌生到熟悉

  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4   <meta charset="UTF-8">
  5   <title>Title</title>
  6 </head>
  7 <body>
  8 
  9 <script type="text/javascript" src="libs/zepto.js" ></script>
 10 <script type="text/javascript">
 11 
 12 //这个为js罗杰部分实现,后续会释放工厂方法
 13 class PageClass {
 14   //构造函数,传入对象
 15   constructor(opts) {
 16 
 17     //必须拥有的参数
 18     this.data = {};
 19     Object.assign(this, opts);
 20   }
 21 
 22   //核心方法,每个Page对象需要一个模板实例
 23   setView(view) {
 24     this.view = view;
 25   }
 26 
 27   //核心方法,设置数据后会引发页面刷新
 28   setData(data) {
 29     Object.assign(this.data, data);
 30 
 31     //只影响改变的数据
 32     this.view.reRender(data, this.data)
 33   }
 34 
 35   render() {
 36     this.view.setInitData(this.data);
 37     this.view.render();
 38 
 39     if(this.onLoad) this.onLoad();
 40   }
 41 
 42 }
 43 
 44 //View为模块的实现,主要用于解析目标生产node
 45 class View {
 46   constructor(template) {
 47     this.template = template;
 48 
 49     //由控制器page传入的初始数据或者setData产生的数据
 50     this.data = {};
 51 
 52     this.labelMap = {
 53       'view': 'div',
 54       '#text': 'span'
 55     };
 56 
 57     this.nodes = {};
 58     this.root = {};
 59   }
 60 
 61   setInitData(data) {
 62     this.data = data;
 63   }
 64 
 65   //数据便会引起的重新渲染
 66   reRender(data, allData) {
 67     this.data = allData;
 68     let k, v, i, len, j, len2, v2;
 69 
 70     //开始重新渲染逻辑,寻找所有保存了的node
 71     for(k in data) {
 72       if(!this.nodes[k]) continue;
 73       for(i = 0, len = this.nodes[k].length; i < len; i++) {
 74         for(j = 0; j < this.nodes[k][i].length; j++) {
 75           v = this.nodes[k][i][j];
 76           if(v.type === 'text') {
 77             v.node.innerText = data[k];
 78           } else if(v.type === 'attr') {
 79             v.node.setAttribute(v.name, data[k]);
 80           }
 81         }
 82       }
 83     }
 84   }
 85   /*
 86     传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变
 87     并且将其中包含{{}}标志的节点信息记录下来
 88   */
 89   _handlerNode (node) {
 90 
 91     let reg = //{/{([/s/S]+?)/}/}/;
 92     let result, name, value, n, map = {};
 93     let attrs , i, len, attr;
 94 
 95     name = node.nodeName;
 96     attrs = node.attributes;
 97     value = node.nodeValue;
 98     n = document.createElement(this.labelMap[name.toLowerCase()] || name);
 99 
100     //说明是文本,需要记录下来了
101     if(node.nodeType === 3) {
102       n.innerText =  this.data[value] || '';
103 
104       result =  reg.exec(value);
105       if(result) {
106         n.innerText =  this.data[result[1]] || '';
107 
108         if(!map[result[1]]) map[result[1]] = [];
109         map[result[1]].push({
110           type: 'text',
111           node: n
112         });
113       }
114     }
115 
116     if(attrs) {
117       //这里暂时只处理属性和值两种情况,多了就复杂10倍了
118       for (i = 0, len = attrs.length; i < len; i++) {
119         attr = attrs[i];
120         result = reg.exec(attr.value);
121 
122         n.setAttribute(attr.name, attr.value);
123         //如果有node需要处理则需要存下来标志
124         if (result) {
125           n.setAttribute(attr.name, this.data[result[1]] || '');
126 
127           //存储所有会用到的节点,以便后面动态更新
128           if (!map[result[1]]) map[result[1]] = [];
129           map[result[1]].push({
130             type: 'attr',
131             name: attr.name,
132             node: n
133           });
134 
135         }
136       }
137     }
138 
139     return {
140       node: n,
141       map: map
142     }
143 
144   }
145 
146   //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止
147   _runAllNode(node, map, root) {
148 
149     let nodeInfo = this._handlerNode(node);
150     let _map = nodeInfo.map;
151     let n = nodeInfo.node;
152     let k, i, len, children = node.childNodes;
153 
154     //先将该根节点插入到上一个节点中
155     root.appendChild(n);
156 
157     //处理map数据,这里的map是根对象,最初的map
158     for(k in _map) {
159       if(!map[k]) map[k] = [];
160       map[k].push(_map[k]);
161     }
162 
163     for(i = 0, len = children.length; i < len; i++) {
164       this._runAllNode(children[i], map, n);
165     }
166 
167   }
168 
169   //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录
170   splitTemplate () {
171     let nodes = $(this.template);
172     let map = {}, root = document.createElement('div');
173     let i, len;
174 
175     for(i = 0, len = nodes.length; i < len; i++) {
176       this._runAllNode(nodes[i], map, root);
177     }
178 
179     this.nodes = map;
180     this.root = root;
181   }
182 
183   render() {
184     let i, len;
185     this.splitTemplate();
186     for(i = 0, len = this.root.childNodes.length; i< len; i++)
187       document.body.appendChild(this.root.childNodes[0]);
188   }
189 
190 }
191 
192 function Page (data) {
193   let page = new PageClass(data);
194   return page;
195 }
196 
197 function main() {
198   let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');
199   let page = Page({
200     data: {
201       pageShow: 'pageshow',
202       pageData: 'pageData',
203       pageShow1: 'pageShow1'
204     },
205     onLoad: function () {
206       this.setData({
207         pageShow: '我是pageShow啊'
208       });
209     }
210   });
211 
212   page.setView(view);
213   page.render();
214 }
215 
216 main();
217 
218 </script>
219 </body>
220 </html>

View Code

我们简单的模拟便先到此结束,这里结束的比较仓促有一些原因:

① 这段代码可以是最终打包构建形成的代码,但是我这里的完成度只有百分之一,后续需要大量的构建相关介入

② 这篇文章目的还是接受开发基础,而本章模拟实现太过复杂,如果篇幅大了会主旨不清

③ 这个是最重要的点,我一时也写不出来啊!!!,所以各位等下个长篇,小程序前端框架模拟实现吧

④ 如果继续实现,这里马上要遇到组件处理、事件模型、分文件构建等高端知识,时间会拉得很长

所以我们继续下章吧……

小程序中的Page的封装

小程序的Page类是这样写的:

1 Page({
2   data: {
3     pageData: '页面数据'
4   },
5   onLoad: function () {
6     console.log('onLoad')
7   },
8 })

传入的是一个对象,显然,我们为了更好的拆分页面逻辑,前面我们介绍了小程序是采用组件化开发的方式,这里的说法可以更进一步,小程序是采用标签化的方式开发,而标签对应的控制器js只会改变数据影响标签显示,所以某种程度小程序开发的特点是:先标签后js,我们构建一个页面,首先就应该思考这个页面有哪些标签,哪些标签是公共的标签,然后设计好标签再做实现。

比如我们一个页面中有比较复杂的日历相关模块,事实上这个日历模块也就是在操作日历标签的数据以及设置点击回调,那么我们就需要将页面分开

【微信小程序项目实践总结】30分钟从陌生到熟悉

比如这里的业务日历模块仅仅是index的一部分(其他页面也可能用得到),所以我们实现了一个页面共用的记录,便与我们更好的分拆页面:

【微信小程序项目实践总结】30分钟从陌生到熟悉【微信小程序项目实践总结】30分钟从陌生到熟悉

  1 class Page {
  2   constructor(opts) {
  3     //用于基础page存储各种默认ui属性
  4     this.isLoadingShow = 'none';
  5     this.isToastShow = 'none';
  6     this.isMessageShow = 'none';
  7 
  8     this.toastMessage = 'toast提示';
  9 
 10     this.alertTitle = '';
 11     this.alertMessage = 'alertMessage';
 12     this.alertBtn = [];
 13 
 14     //通用方法列表配置,暂时约定用于点击
 15     this.methodSet = [
 16       'onToastHide',
 17       'showToast',
 18       'hideToast',
 19       'showLoading',
 20       'hideLoading',
 21       'onAlertBtnTap',
 22       'showMessage',
 23       'hideMessage'
 24     ];
 25 
 26     //当前page对象
 27     this.page = null;
 28   }
 29   //产出页面组件需要的参数
 30   getPageData() {
 31     return {
 32       isMessageShow: this.isMessageShow,
 33       alertTitle: this.alertTitle,
 34       alertMessage: this.alertMessage,
 35       alertBtn: this.alertBtn,
 36 
 37       isLoadingShow: this.isLoadingShow,
 38       isToastShow: this.isToastShow,
 39       toastMessage: this.toastMessage
 40 
 41     }
 42   }
 43 
 44   //pageData为页面级别数据,mod为模块数据,要求一定不能重复
 45   initPage(pageData, mod) {
 46     //debugger;
 47