小程序发展史
小程序并非凭空而生,在其未出现前,webview作为web的主要入口。初期开发者通过调用微信原生的一些api进行一些交互,如图片预览等(称为JS-SDK),源于开发者依赖原生功能完成一些难以实现的东西。
JS-SDK是对之前weixinJSBrige的一个包装,以及能力的释放
虽然js-sdk虽然解决了移动网页能力不足的问题,但其并没有解决使用移动网页遇到的不良体验问题,比如,受限于设备性能和网络速度出现的白屏问题,于是出现了增强版本的JS-SDK,其中有一个重要功能,称之为“微信web资源离线存储”。但是在复杂的页面场景下,依然会出现白屏问题,原因表现为页面切换的生硬感以及点击的迟滞感。此时,微信小程序应运而生,一个可解决js-sdk无法解决的问题以及给开发者及用户一个很好体验感的系统。
其特点为:
1、快速的加载 2、更强大的能力 3、原生的体验 4、易用且安全的微信数据开放 5、高效和简单的开发
小程序和PC开发的区别
小程序和普通网页开发有很大的相似性,小程序主要的开发语言是js,但是二者是有区别的
- 普通web开发依赖于浏览器,可以使用浏览器中的DOM、BOM的api。小程序的逻辑层和渲染层是分开的,逻辑层运行在JSCore中,并没有完整的浏览器对象,所以缺少相关的dom bom的api
- 普通网页开发的逻辑层和渲染层是互斥的,可以说明的是长时间的脚本运行会导致页面无响应;而小程序的逻辑层和渲染层是分开的,分别运行在不同的线程中,
- 网页开发时可以依赖各种浏览器,搭配一些辅助工具和编辑器即可。而小程序开发则需要申请小程序账号并安装开发者工具、配置项目等过程来完成。
-
小程序的执行环境
小程序的特点
- 对于普通用户,可以通过二维码扫描、搜索或者朋友分享可以直接打开,加上优秀的体验,小程序使得服务提供者的触达能力变得更强。
- 对于开发者,小程序具有快速加载和渲染的能力,加之配套的与能力、运维能力和数据汇总能力,使得开发者不需要处理琐碎的工作,精力放于具体的业务逻辑的开发上。
- 小程序的模式使得微信可以开放更多的数据,开发者可以获得用户的一些基本信息,甚至可以获取微信群的基本信息,使得小程序的开放能力变得更强。
小程序目录结构
小程序架构
一、技术选型
一般来说,渲染界面的技术有三种
1. 用纯客户端原生技术来渲染
2. 用纯web技术渲染
3. 用客户端原生技术和web技术结合的混合技术(简称Hybrid技术)来渲染
小程序技术选型分析过程如下:
- 开发门槛:web门槛低,Native也有像RN这样的框架支持
- 体验:Native比Web要好太多,Hybird在一定程度上比Web接近原生体验
- 版本更新:Web支持在线更新,Native则需要打包到微信一起审核发布
- 管控安全:web可跳转或是改变页面内容,存在一些不可控因素和安全风险
由于小程序的宿主环境是微信,如果用纯客户端的原生技术来编写小程序,那么小程序代码更新每次都需要与微信的代码一起发版,此种方法不行。
因此需要向web一样可以把随时可更新的资源甩在云端,通过下载到本地,动态执行和即可渲染出界面。但如果用纯web来渲染小程序,在一些复杂的交互上会有些性能问题,因为在web中UI和js脚本是互斥的,在一个线程中执行,这就容易导致一些逻辑任务抢占UI资源。
因此,最终采用二者结合起来的Hybrid技术来渲染小程序,用近似web的开发方式来开发,可以实现在线更新代码,同时引入客户端原生参与组件(原生组件)也有以下好处:
- 扩展web能力,例如像输入框组件
input
textarea
有更好的控制键盘的能力 - 体验更好,同时也减轻webView的渲染工作。例如地图组件
map
这类较复杂的组件,其渲染工作不占用webview线程,而交给更高效的客户端原生处理。 - 绕过setData、数据通信和重渲染流程,使渲染性能更好
- 用客户端原生渲染内置一些复杂组件、可以提供更好的性能
二、双线程模型
小程序的渲染层和逻辑层分别由2个线程管理:视图层的界面使用了webview进行渲染,逻辑层采用JsCore线程运行js脚本。视图层和逻辑层通过系统层WeiXinJsBridge进行通信,逻辑层把数据变化通知到视图层,触发视图层的页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
如此设计原因:
前面提到管控和安全,为了解决这些问题,我们需要阻止开发者使用一些例如浏览器的window对象跳转页面、操作dom、动态执行脚本的开放性接口。
我们可以使用客户端系统的JS引擎,IOSxiade Javascriptcore框架,安卓下腾讯x5内核提供的jscore环境。这个沙箱环境只提供纯js的解释执行环境,没有任何浏览器相关接口
这就是小程序双线程模型的由来:
1、逻辑层:创建一个单独的线程去执行js,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等
2、视图层:界面渲染相关的任务全都在webview线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个webview线程
3、jsBridge:起到架起上层开发与native(系统层)的桥梁,使得小程序可以通过API使用原生功能,且部分组件为原生组件实现,从而有良好的体验
三、双线程通信
读到此处不知道大家有没有疑问,视图层与逻辑层分别为不同的线程且单独运行,那么在webview里,开发者就没有办法直接操作dom
那么如何实现动态更改界面呢?
如上【交互图】所示,逻辑层和视图层通信会由native做中转,逻辑层发送网络请求也经由native转发。也就是说,可以把dom的更新通过简单的数据通信来实现
采用Virtual DOM(虚拟dom):即用js对象模拟DOM树 --> 比较两棵虚拟dom树的差异 --> 把差异应用到真正的dom树上
当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而evaluateJavascript
的执行会受很多方面的影响,数据到达视图层并不是实时的。所以我们的setData函数将数据从逻辑层发送到视图层,是异步的。
有了线程之间的通信,再来看看小程序的渲染机制
双线程渲染机制
双线程的渲染,其实是结合了一系列机制(模版绑定、虚拟 DOM、线程通信),最后整合的一个执行步骤。
1、通过模版数据绑定和虚拟 DOM 机制,小程序提供了带有数据绑定语法的 DSL (领域专用语言)给到开发者,用来在渲染层描述界面的结构。
<view>{{ message }}</view>
<view wx:if="{{ true }}">hello</view>
<checkbox checked="{{ false }}"></checkbox>
注:wx:if中不支持复杂函数运算。运用过程中注意
2、小程序再逻辑层提供了设置页面数据的api(即,setData)
this.setData({
key: value
})
// setData函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data的值(同步)。
3、逻辑层需要更改界面时,只要把修改后的 data 通过 setData 传到渲染层。
传输的数据,会转换为字符串形式传递,故应尽量避免传递大量数据。
4、渲染层会根据前面提到的渲染机制重新生成 VD(虚拟 DOM)树,并更新到对应的 DOM 树上,引起界面变化。
因为此涉关setData 我们详细拓展一下
模板数据绑定
过程:
1、解析语法生成AST
2、根据AST结果生成DOM
3、将数据绑定更新至模板
浏览器会把 HTML 解析成一棵树,最后渲染出来。整个界面是对应着一棵 DOM 树。
其实浏览器页面的 DOM 结构树,也是 AST 的一种,把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成 HTML DOM。
而最容易引发性能问题的,主要是第三点。而关于数据更新的解决方案,React 首先提出了虚拟 DOM 的设计,而现在也基本被大部分框架吸收,小程序也不例外。
虚拟dom机制
说到数据更新的 Diff,更多的则是Diff + 更新模板这样一个过程。
虚拟 DOM 解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。
计算过程如下:
用JS对象模拟DOM树。
一个真正的DOM元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化 DOM 对象。我们用一个 JavaScript 对象结构表示 DOM 树的结构。比较两棵虚拟DOM树的差异。
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录,最后得到一组差异记录。把差异应用到真正的DOM树上。
对差异记录要应用到真正的 DOM 树上,例如节点的替换、移动、删除,文本内容的改变等。
小程序里,由于无法直接操作 DOM,主要也是通过数据传递的方式来进行相关的模版更新。模版绑定的机制、数据更新的机制,都可以参照上面的说明。
四、小程序的基础库
小程序的基础库可以被注入到视图层和逻辑层运行,主要用于一下几个方面:
- 视图层:提供各类组件来组建界面的元素
- 逻辑层:提供各类api处理各种逻辑
- 在处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑
由于小程序的渲染层和逻辑层是两个线程管理,两个线程都各自注入了基础库。渲染层WebView层注入的称为WebView基础库,逻辑层注入的称为AppService基础库。
小程序的基础库不会打包在某个小程序的代码里,他会被提前内置在微信客户端。以此来:1、降低业务小程序代码包大小 2、可以单独修复基础库中的Bug,无需修改业务小程序中的代码包
基础库的版本号
小程序基础库版本号使用 semver 规范,格式为 Major.Minor.Patch,其中Major、Minor、Patch均为整数
小程序中可使用wx.getSystemInfo()
或者wx.getSystemInfoSync()
方法获取小程序版本号
拓展: 比较小程序版本号方法
// 不使用字符串或者parseInt等比较,版本越高,越会引发一些逻辑错误
function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
var len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (var i = 0; i < len; i++) {
var num1 = parseInt(v1[i])
var num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
compareVersion('1.11.0', '1.9.9') // => 1 // 1表示 1.11.0比1.9.9要新
compareVersion('1.11.0', '1.11.0') // => 0 // 0表示1.11.0和1.11.0是同一个版本
compareVersion('1.11.0', '1.99.0') // => -1 // -1表示1.11.0比 1.99.0要老
五、Exparser框架
由来:
小程序的视图是在webView里面渲染的,那搭建视图的方式需要用到html语言。如果微信团队直接提供html能力,那解决管控和解决安全性的双线程模型就成为摆设,开发者可以使用A标签实现各种跳转。 i标签的不足之处:1、标签众多,增加理解成本 2、接口底层,不利于快速开发 3、能力有限,会限制小程序的表现形式。
因此Exparser框架应运而生,其内置了一套组件,涵盖小程序的基础功能,便于开发者快速搭建出任何界面,同时也提供了自定义组件的能力,开发者可以自行扩展更多的组件,以实现代码的复用。
是什么:
Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。
特点:
- 基于Shadow
DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。 - 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。
内置组件
基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。
六、运行机制
小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
- 小程序没有重启的概念
- 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
-
当短时间内(5s)连续收到两次以上系统的内存警告,会进行小程序的销毁
七、更新机制
小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager
api进行处理。
结构
一个完整的小程序主要由以下几部分组成:
一个入口文件:app.js
一个全局样式:app.wxss
一个全局配置:app.json
页面:pages下,每个页面再按文件夹划分,每个页面4个文件
视图:wxml,wxss
逻辑:js,json(页面配置,不是必须)
pages里面还可以再根据模块划分子目录,孙子目录,只需要在app.json里注册时填写路径就行
打包如何实现
这就涉及到这个编辑器的实现原理和方式了,它本身也是基于WEB技术体系实现的,nwjs+react,nwjs是什么:简单是说就是node+webkit,node提供给我们本地api能力,而webkit提供给我们web能力,两者结合就能让我们使用JS+HTML实现本地应用程序。
既然有nodejs,那上面的打包选项里的功能就好实现了。
ES6转ES5:引入babel-core的node包
CSS补全:引入postcss和autoprefixer的node包(postcss和autoprefixer的原理看这里)
代码压缩:引入uglifyjs的node包
注:在android上使用的x5内核,对ES6的支持不好,要兼容的话,要么使用ES5的语法或者引入babel-polyfill兼容库。
小程序技术实现
小程序的UI视图和逻辑处理是用多个webview实现的,逻辑处理的JS代码全部加载到一个Webview里面,称之为AppService,整个小程序只有一个,并且整个生命周期常驻内存,而所有的视图(wxml和wxss)都是单独的Webview来承载,称之为AppView。所以一个小程序打开至少就会有2个webview进程,正式因为每个视图都是一个独立的webview进程,考虑到性能消耗,小程序不允许打开超过5个层级的页面,当然同是也是为了体验更好。
-
AppService
可以理解AppService即一个简单的页面,主要功能是负责逻辑处理部分的执行,底层提供一个WAService.js的文件来提供各种api接口,主要是以下几个部分:
消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)1、日志组件Reporter封装
2、wx对象下面的api方法
3、全局的App,Page,getApp,getCurrentPages等全局方法
4、还有就是对AMD模块规范的实现然后整个页面就是加载一堆JS文件,包括小程序配置config,上面的WAService.js(调试模式下有asdebug.js),剩下就是我们自己写的全部的js文件,一次性都加载。
-
在开发环境下
1、页面模板:app.nw/app/dist/weapp/tpl/appserviceTpl.js
2、配置信息,是直接写入一个js变量,__wxConfig。
3,其他配置
-
线上环境
而在上线后是应用部分会打包为2个文件,名称app-config.json和app-service.js,然后微信会打开webview去加载。线上部分应该是微信自身提供了相应的模板文件,在压缩包里没有找到。
1、WAService.js(底层支持)
2、app-config.json(应用配置)
3、app-service.js(应用逻辑)然后运行在JavaScriptCore引擎里面。
-
AppView
这里可以理解为h5的页面,提供UI渲染,底层提供一个WAWebview.js来提供底层的功能,具体如下:
1、消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
2、日志组件Reporter封装
3、wx对象下的api,这里的api跟WAService里的还不太一样,有几个跟那边功能差不多,但是大部分都是处理UI显示相关的方法
4、小程序组件实现和注册
5、VirtualDOM,Diff和Render UI实现
6、页面事件触发在此基础上,AppView有一个html模板文件,通过这个模板文件加载具体的页面,这个模板主要就一个方法,$gwx,主要是返回指定page的VirtualDOM,而在打包的时候,会事先把所有页面的WXML转换为ViirtualDOM放到模板文件里,而微信自己写了2个工具wcc(把WXML转换为VirtualDOM)和wcsc(把WXSS转换为一个JS字符串的形式通过style标签append到header里)。
-
Service和View通信
使用消息publish和subscribe机制实现两个Webview之间的通信,实现方式就是统一封装一个WeixinJSBridge对象,而不同的环境封装的接口不一样,具体实现的技术如下:windows环境
通过window.postMessage实现(使用chrome扩展的接口注入一个contentScript.js,它封装了postMessage方法,实现webview之间的通信,并且也它通过chrome.runtime.connect方式,也提供了直接操作chrome native原生方法的接口)
发送消息:window.postMessage(data, ‘*’);,// data里指定 webviewID
接收消息:window.addEventListener(‘message’, messageHandler); // 消息处理并分发,同样支持调用nwjs的原生能力。
在contentScript里面看到一句话,证实了appservice也是通过一个webview实现的,实现原理上跟view一样,只是处理的业务逻辑不一样。
'webframe' === b ? postMessageToWebPage(a) : 'appservice' === b && postMessageToWebPage(a)
IOS
通过 WKWebview的window.webkit.messageHandlers.NAME.postMessage实现微信navite代码里实现了两个handler消息处理器:
invokeHandler: 调用原生能力
publishHandler: 消息分发
** Android**
通过WeixinJSCore.invokeHanlder实现,这个WeixinJSCore是微信提供给JS调用的接口(native实现)
invokeHandler: 调用原生能力
publishHandler: 消息分发 -
微信组件
在WAWebview.js里有个对象叫exparser,它完整的实现小程序里的组件,看具体的实现方式,思路上跟w3c的web components规范神似,但是具体实现上是不一样的,我们使用的所有组件,都会被提前注册好,在Webview里渲染的时候进行替换组装。
exparser有个核心方法:
regiisterBehavior: 注册组件的一些基础行为,供组件继承
registerElement:注册组件,跟我们交互接口主要是属性和事件
组件触发事件(带上webviewID),调用WeixinJSBridge的接口,publish到native,然后native再分发到AppService层指定webviewID的Page注册事件处理方法。
-
底层总结
小程序底层还是基于Webview来实现的,并没有发明创造新技术,整个框架体系,比较清晰和简单,基于Web规范,保证现有技能价值的最大化,只需了解框架规范即可使用已有Web技术进行开发。易于理解和开发。MSSM:对逻辑和UI进行了完全隔离,这个跟当前流行的react,agular,vue有本质的区别,小程序逻辑和UI完全运行在2个独立的Webview里面,而后面这几个框架还是运行在一个webview里面的,如果你想,还是可以直接操作dom对象,进行ui渲染的。
组件机制:引入组件化机制,但是不完全基于组件开发,跟vue一样大部分UI还是模板化渲染,引入组件机制能更好的规范开发模式,也更方便升级和维护。
多种节制:不能同时打开超过5个窗口,打包文件不能大于1M,dom对象不能大于16000个等,这些都是为了保证更好的体验。
底层相关块知识 引入CSDN博主「xiangzhihong8」的原创文章
原文链接:https://blog.csdn.net/xiangzhihong8/article/details/66521459
微信小程序安全的环境
由于小程序的双线程模式,逻辑层没有办法操作dom元素,但是开发者却可以访问全局的内容。逻辑层建立了个沙箱环境。小程序根据AMD规范构造了模块化形式,通过define和require函数实现定义和引入模块。
首先,小程序将每一个js文件都当作一个独立的模块,并且通过define函数将业务代码通过函数包了一层,业务代码可以访问到的全局变量,都是define函数的参数,因此开发者所有的行为都被限制在了沙箱环境当中。
定义好了模块之后,用到这些模块的时候就会通过require函数来进行引入。require函数的第一个参数是模块的名称,首先模块名称是不能重复,从而保证每个模块都是唯一的,其次,每个模块在define时都有一个status属性,用来判断模块是否被require过,保证业务逻辑只会执行一次。最后就是在加载模块的时候,小程序只会传递小程序允许开发者访问的变量给define函数,例如,有些人在开发微信小程序的时候,发现全局上可以访问到__wxConfig的变量,但是这个变量在微信开发者文档上又没有提及,有些人可能会认为这是微信的漏洞,其实这个变量上的属性也是微信经过筛选后才暴露出来的。
WX处理
本来,小程序将用户的代码通过AMD的形式封装在一个沙箱环境中,已经可以做到安全性的访问了,不过微信小程序又给开发者提供了一种WXS的语法。为什么要提供WXS呢,微信的官方文档上面解释了说提供wxs的原因是由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异,而且从微信文档这处也可以看出来,WXS是一种可以运行在视图层的js,从而减少小程序双线程的通信次数。
wxs是运行在视图层的js 微信对wxs内层做了处理。
var navigateTo = function (url) {
window.location.href = url
}
var getRandomColor = function () {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
module.exports = {
navigateTo: navigateTo,
getRandomColor: getRandomColor
}
function np_0() {
var nv_module = { nv_exports: {} };
var nv_navigateTo = function (nv_url) {
nv_window.nv_location.nv_href = nv_url;
};
var nv_getRandomColor = function () {
return "#" + Math.nv_floor(Math.nv_random() * 16777215).nv_toString(16);
};
nv_module.nv_exports = {
nv_navigateTo: nv_navigateTo,
nv_getRandomColor: nv_getRandomColor,
};
return nv_module.nv_exports;
}
第一块代码中的navigateTo方法试图去通过window上的location对象去进行跳转,第二块代码是微信编译后的代码,我们可以看到,微信为每一个变量都添加了一个nv_的前缀,这样navigateTo方法就访问不到真正的window属性了,看到这里大家都明白了微信是通过改写所有用户访问属性的前缀,来实现用户逻辑的隔离,从而保证了视图层的安全性
在wxs中所有类型的constructor的返回值都被改写成了字符串类型