微信小程序:超级 Hybrid 解决方案,应用场景广泛且复杂

2024-07-19
来源:网络整理

首先我们简单解释一下什么是微信小程序。PS:这个问题可能有点牵强:)

小程序是无需下载安装即可使用的应用程序,实现了应用触手可及的梦想,用户只需扫一扫或搜索即可打开应用,也体现了即用即走的理念,用户无需担心是否安装了太多应用,应用将无处不在、随时可用,却无需安装或卸载。从字面意义上讲,小程序拥有类似Web应用的热部署能力,在功能上也接近原生APP。

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

很多公司都有自己的平台,我知道比较好的有携程的平台,阿里的Weex,百度的糯米,但是从应用场景上来说,都没有微信那么丰富,这里面的根本区别在于:

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

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

因为小程序是针对各个公司开发的,其他公司的解决方案是针对公司业务团队的,所以拥有平台的公司一般实力较强。但开发小程序的公司实力参差不齐,所以必须对小程序进行绝对限制,最大程度保证框架层(小程序团队)对程序的控制权。毕竟程序是运行在微信这么大的一个APP里的。

我之前也有一个疑问,微信小程序为什么要设计自己的标签语言,在知乎上也看到了各种答案。但如果从设计和应用层面考虑:这样会有更好的控制,而且我后来发现,微信小程序其实还是在使用渲染(这和我之前认为微信向左的思路相反),但如果我们用微信来限制以下标签,这样就是限制标签了,以后改起来也比较容易:

另一方面,经过以前的研究,我可以清楚地得出一个结论:

这个。({'wxml':`

动态插入节点

`});

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

每个页面的目录如下所示:

──

| │ ──

| | │──.页面配置

| | ═──.页面逻辑

| | │──.页面结构

| | └──.页面样式表

| └──日志

| ═──log.page配置

| ═──log. 页面逻辑

| ═──log.页面结构

| └──log.page 样式表

═──app.js 小程序逻辑

═──app.json 小程序公共设置

└──app.wxss 小程序公共样式表

各个组件的目录大致相同,但是入口都是Page层。

打包之后的小程序结构(这里实在看不懂,参考:小程序底层框架实现原理分析):

所有的小程序,最后基本上都是按照上面的结构来写的。

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

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

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

4. app-.js 小程序的完整配置,包含app.json中的所有配置,以及默认的配置类型

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

6、page-.html是小程序视图的模板文件,所有的页面都使用这个文件加载渲染,所有的WXML文件都分解成JS文件打包在这里。

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

从设计角度来说,小程序采用的组件化开发方案是,除了页面级标签之外,下面都是组件,并且组件内的 view、data、js 标签之间的关系要和页面一致。这也是我们通常推荐的开发方式,把一个页面拆分成一个个小的业务组件或者 UI 组件:

所有的小程序,最后基本上都是按照上面的结构来写的。

从设计角度来说,小程序采用的组件化开发方案是,除了页面级标签之外,下面都是组件,并且组件内的 view、data、js 标签之间的关系要和页面一致。这也是我们通常推荐的开发方式,把一个页面拆分成一个个小的业务组件或者 UI 组件:

从编写业务代码的过程来看,感觉整体还是比较顺利的。小程序有自己完整的前端框架,发布到业务代码的主要是页面,页面中只能使用标签和组件,所以框架对业务的把控还是不错的。

最后从工程角度来说,微信小程序的架构更加完善,小程序从三个方面考虑到B端用户的体验:

所以微信小程序在架构和使用场景上都是很惊艳的,至少是让我很惊艳的……所以我们再从开发层面来更深入的分析一下。我们最近都在做基础服务,都是在完善技术体系。对于前端来说,我们要搭建一个体系。如果搭建一个App,也是不错的选择,但是要有一个完整的分层:

这样业务就可以快速迭代,因为业务开发人员写的代码都差不多,底层框架可以和工程团队(通常是同一个团队)配合解决底层很多效率、性能的问题。

稍微大一点的公司,稍微富裕一点的团队,也会同步做大量后续的性能监控、错误日志的工作,从而形成一套文档->开发->调试->构建->发布->监控分析的完整技术体系

如果这样的体系形成了,那么后面即使内部框架改变或者技术创新,也都是基于这个体系。微信小程序在这方面做的非常好。可惜很多其他公司团队只会做这条路的一部分,然后因为各种原因就不再深入了,可能是觉得没价值吧。最可怕的行为就是在还没有形成自己的体系之前就贸然的去改变基础框架。一定要谨慎!好了,废话不多说,继续学习。

微信小程序执行流程

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

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

“”[

“//”,

“/列表/列表”,

“/日志/日志”

],

微信小程序加载一次,会打开3个页面,加载3个页面的逻辑,完成基本的实例化工作,最后只显示首页!这是小程序为了优化页面打开速度所做的,势必会浪费一些资源。所以是全部打开还是预加载几个,细节会根据实际情况动态变化。我们也可以看出,从业务角度来说,要理解小程序的执行流程,其实理解Page的流程就可以了。关于Page的生命周期,除了已经放出的API:->->->等,官方还放出了一张图来解释:

在小程序加载的时候,视图层会启动两个线程,一个用于视图,一个用于页面。我的理解就是将程序逻辑执行和页面渲染分离,小程序的视图层目前作为渲染载体,而逻辑层则是独立的运行环境。从架构上来说,和 都是独立的模块,并不具备直接数据共享的通道。目前视图层与逻辑层之间的数据传输其实是通过双方提供的数据来实现的。即需要将用户传输的数据转化为字符串形式进行传输,并将转化后的数据内容拼接成JS脚本,然后通过执行JS脚本传输到双方独立的环境中。 的执行会受到很多因素的影响,数据到达视图层并不是实时的。

因为之前以为页面渲染出来跟这个没关系,所以就以为这个图有问题,但是后来看到实际代码中熟悉的DOM,就能看出哪些部分是Web。其实小程序主体还是采用浏览器渲染的方式,还是加载HTML和CSS的逻辑。最后发现这个图没什么问题,问题出在我的理解上。哈哈,这里我们重新分析一下这个图:

WXML会先编译成JS文件,传入数据后再在JS文件中渲染。这里可以认为微信在加载小程序时同时初始化了两个线程,分别执行各自的逻辑:

查看这里的源代码,可以看到Page的实例化应该由全局控制器完成,完成后会执行事件,但是在执行之前会给页面发送一个通知:

.({

姓名: ””,

参数:o({},e,{

:否

}),

:[t]

})

真正的逻辑是这样的:全局控制器会完成页面的实例化,而页面的实例化是基于app.json的,等到所有实例化完成并存储之后,选择第一个页面实例执行一些逻辑,然后通知视图线程即将执行事件。因为视图线程和业务线程是两个线程,所以不会出现阻塞的情况,视图线程根据初始数据完成渲染,业务线程继续后续的逻辑和执行,如果有的话就会进入队列,继续通知视图线程更新。

所以个人觉得微信官网上的图不太清楚,所以这里重新画了一下:

这是从其他地方拍摄的另一张照片:

模拟实施

到了这一步,不拿出一个简单的小程序框架实现,似乎有点不对,我们做小程序实现最主要的原因就是在一端跑三个终端:web、小程序、甚至终端。

我们这里没有可能实现太复杂的功能,我们这里想要的只是用基本的标签实现一个基本的页面展示,我们只做了Page块的简单实现,让大家了解小程序可能的实现方式,以及直接将小程序转为H5的可能方式。

{{}}

页({

数据: {

:'页面数据'

},

:(){

。日志('')

},

})

{{}}

({

:{

// 此处定义属性,组件使用时可以指定属性值

:{

类型: ,

:' ',

},

数据: {

// 以下是一些组件内部数据

:{}

},

:{

// 这是一个自定义方法

: (){}

})

我们直接把小程序的代码复制到我们的目录中:

我们要做的就是让这个代码运行起来,这里的目录就是我们最终看到的目录,真正运行起来可能不是这样的,在运行之前会通过我们的项目进行构建,变成可以直接运行的代码。这里可以运行的代码其实是一个模块,所以我们把最后的结果反转过来,拆分到开发结构目录中,我们先把所有的代码都放到.html中,可能是这样的:

HTML>

这段代码非常简单:

① 建立一个模板,我们不用关心它的格式,直接写成一行就好,方便处理。

这个。= '{{}}{{}}1">{{}}';

②然后我们将这个模板转换成节点(这里不是必须的,但是模拟实现尽量简单),然后遍历处理所有节点,就可以处理我们的数据了,最后形成这个HTML:

③ 同时我们存储一个包含与其相关的所有节点的对象:

这个对象就是所有会受影响的节点的一个映射表,后面调用的时候直接操作对应的数据就可以了。这里我们把我们的代码拆分成几个关键的部分,第一个是View类,这个类对应我们的模板,是核心类:

//View是模块的实现,主要用于解析目标生产节点

{

(){

这个。=;

//初始数据或者控制器页面生成的数据

这个.数据 = {};

这个。= {

‘视图’:‘div’,

'#text':'跨度'

};

这个。= {};

这个.root = {};

(数据){

这个.数据=数据;

//该数据将导致重新渲染

(数据,){

这个.数据 = ;

让 k,v,i,len,j,len2,v2;

//启动重新渲染逻辑并找到所有已保存的节点

为了(){

如果(!这个。[k]);

对于(i = 0,len = this。[k].;i < len;i++){

对于(j = 0; j < this.[k][i].; j++){

v = this.[k][i][j];

如果(v.type ==='text'){

v.节点。 = 数据[k];

}(v.type ==='attr'){

v.节点。(v.名称,数据[k]);

/*

传入一个节点,解析出一个节点,并将节点中的数据改为初始数据

并记录包含{{}}标记的节点信息

*/

(节点){

让 reg = /\{\{([\s\S]+?)\}\}/;

让,名称,,n,地图={};

让,我,len,属性;

名称 = 节点。;

=节点。;

=节点。;

n = .(this.[名称.()] || 名称);

//描述是文字,需要记录

如果(节点。=== 3){

n. =this.数据[] ||'';

=reg.exec();

如果(){

n. =this.数据[[1]] || '';

如果(!地图[[1]])地图[[1]] = [];

地图[[1]].推({

类型:'文本',

节点:n

});

如果(){

//目前我们只处理属性和值。如果有更多的值,那么就会复杂 10 倍。

对于(i = 0,len = .; i < len; i ++){

属性 = [i];

= reg.exec(属性);

n.(属性名称,属性);

//如果有需要处理的节点,则需要保存该标志

如果(){

n.(属性名称,this.数据[[1]] || '');

//存储稍后用于动态更新的所有节点

如果(!地图[[1]])地图[[1]] = [];

地图[[1]].推({

类型:'attr',

名称:属性名称,

节点:n

});

{

节点: n,

地图:地图

//遍历某个节点的所有子节点,如果有子节点,则继续遍历,直到没有子节点为止。

(节点,地图,根){

让 = 这个。(节点);

让 _map = .map;

让 = .节点;

让 k,i,len, = 节点。;

//首先将根节点插入到前一个节点中

根(n);

//处理地图数据,其中map是根对象,初始地图

为了(){

如果 (!map[k])map[k] = [];

map[k].push(_map[k]);

对于(i = 0,len = .; i < len; i ++){

这个。([i],map,n);

//处理每一个节点,将其翻译成页面可以识别的节点,并记录需要操作的节点

(){

让 = $(这个。);

让 map = {},root = .('div');

leti,len;

对于(i = 0,len = .; i < len; i ++){

这个。([i],map,root);

这个。=地图;

这个.root = 根;

(){

leti,len;

这。();

对于(i = 0,len = this.root..;i<len;i++)

.body.(这个.root.[0]);

该类的主要任务是:

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

//这是js的实现 ,工厂方法稍后会放出

{

//构造函数,传入对象

(选择){

//必选参数

这个.数据 = {};

.(这个,选择);

//核心方法,每个Page对象都需要一个模板实例

(看法){

这个.视图 = 视图;

//核心方法,设置数据会引起页面刷新

(数据){

.(这个.数据,数据);

//只影响改变的数据

这个.视图.(数据,这个.数据)

(){

这个.视图。(这个.数据);

这个.视图.();

如果(这个。)这个。();

现在轮到我们实际调用 Page 方法了:

(数据){

让页面=(数据);

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

(){

让视图 = ('{{}}{{}}{{}}');

让页面 = 页面({

数据: {

:'',

:'',

:''

},

:(){

这。({

: '我是'

});

});

页面预览);

页。();

主要的();

所以,我们可以看到页面的变化,从初始化页面到执行时:

最终的完整代码如下:

HTML>

我们的简单模拟到此结束。它匆忙结束的原因如下:

【关于投稿】

如果你有原创好文章可以投稿,欢迎直接给公众号留言。

①消息格式:

【投稿】+《文章标题》+文章链接

②示例:

【投稿】《别再称自己为程序员,我十多年IT工作的总结》:

③最后请附上你的个人资料~

分享