最近在接触小程序的项目,并且使用的是taro作为小程序的框架作为基建,于是趁着无聊,就来学习一下小程序原理的解析。
在理解小程序框架之前,我们先需要知道以下几点
- 小程序是什么
- 小程序和网页的区别
- 小程序的代码构成
- 小程序如何部署上线
- ……
小程序是什么
小程序是一种全新的连接用户与服务的方式,可以在微信内被便捷的获取和传播,同时具有出色的使用体验。 ——— 摘自小程序官网的介绍
小程序在大家生活中其实已经随处可见了,比如奶茶店点的点单小程序、粤省事小程序等。小程序的使用十分便捷,直接在微信便可启动,无需在手机上下载app。
小程序和网页的区别
首先说说网页:
- 网页是通过输入一个网址,打开的一个页面;需要借助浏览器才能打开使用;
- 网页开发者可以使用浏览器暴漏的各种
DOM api
来对dom节点进行操作。 - 网页的开发中渲染线程和js线程是互斥的,也就是说我们在网页的渲染过程中js能够阻塞页面的响应。
再说说小程序:
- 小程序的开发和网页很相似,但是小程序的开发使用需要借助微信开发者工具;
- 小程序没有完整的浏览器,所以缺少相关
DOM api
和BOM api
- 小程序的运行环境分为逻辑层和渲染层,其中页面的使用webview展示在渲染层(wxml和wxss工作在此层),js脚本工作在逻辑层,采用jsCore运行js脚本。
- 小程序不需要面对各式各样的浏览器,在它的世界里,只需要面对两大操作系统:ios、Android的微信客户端。
小程序的代码构成
-
wxml
:和网页的HTML类似,用来使用微信提供的标签编写出页面的结构; -
wxss
: 具有css的大部分的特性,并且新增了尺寸单位rpx
,开发者可以无需换算不同像素比设备宽高,小程序底层已经替你做了这件事。 -
js
:用来进行逻辑交互。 -
json
: json文件在小程序中提供配置,- 项目的跟目录中有一个
app.json
用来配置小程序的全局配置。 project.config.json
工具配置,比如配置上传是否压缩、小程序的appid、是否校验合法域名等- 每个页面下还有一个属于页面的配置文件
page.json
。可以配置该页面的顶部颜色风格或者独立属性,可以单独配置插件等。
- 项目的跟目录中有一个
小程序如何集成上线
网页的上线流程需要将打包后的产物通过nginx上传到服务器,并且配置域名进行项目的部署。
小程序发布流程:开发 ——— 构建 ——— 上传 ——— 提交审核 ——— 发布
- 开发者通过结合开发者工具进行功能开发
- 通过本地build后的产物文件,依赖微信开发者工具上传到微信公众平台的小程序后台
- 将上传的版本进行提交审核
- 微信官方审核通过后,小程序管理人员将小程序进行发布。
以上流程是一个普通小程序经历的上线流程。上面所说的是借助手动的去进行构建、上传、提审、发布。后续会讲到自动化的过程,大大提效~
了解了上面的一些基础问题,那么接下来可以开始进入小程序的一个原理解析了~
在这里我想带着几个问题来看小程序的原理
- 为什么小程序的逻辑层单独存在于一个线程中
- 为什么小程序无法通过js操作DOM
- 小程序的wxml是如何被识别成页面结构的
- 微信小程序的rpx如何进行的变换以及实现
- 小程序怎么识别wxss
双线程架构
- 渲染层:小程序有多个页面,所以webview可以有多个
- 逻辑层:用jscore进行处理js脚本。
两个线程之间无论是通信、数据传递还是网络请求都是通过native统一进行转发
webView渲染层
从微信官网中可以下载一个demo,或者尝试自己写一个demo(此文中用的官网例子,咱们关注看内部如何处理)
可以看到在page/index
页面下的wxml元素的展示,确实是我们写的View
等标签,并且关注到咱们页面的顶部是用一个page
标签包裹着。
page/index
页面的wxml标签是被转换成什么呢?
-
在微信开发者工具打开开发者工具调试模式,会新打开一个新的开发调试窗口,这个时候可以看到wxml标签在编译后展示成什么
-
打开之后,我们选中我们的页面,在
devTools
中可以看到,该页面是用一个webview
标签包裹着,那内部其实是一个iframe内嵌。在这里我们关注到这个页面的行联样式中设置了position: absolute
,在这里带着疑问🤔️为啥会设置为绝对定位呢,我们接着往下走 -
点击头像,跳转到
page/log/log
页面,这个时候观察发现,多了一个新的webview
标签,并且这个时候,第一个webview的包裹div元素上的样式增加了z-index:1
,新的webview
的包裹div的样式上z-index:2
,其实这个就是小程序的页面栈,通过z-index
增加层级来对前面的页面进行覆盖,这也是为什么小程序的页面最多打开10个页面,因为打开的页面越多,小程序的页面层级以及逻辑复杂度提升,影响小程序的性能和用户体验。 -
那么一个page元素包裹着的就标志为这是一个页面webview,每个page都是不同的webview去进行渲染的,可以提供很好的用户交互体验。也避免了单个的webview的任务复杂度。
-
那么 iframe内部又是什么呢?在devtools中输入如下代码即可打开对应的webview下的渲染层
document.querySelectorAll('webView')[0].showDevTools(true,null) // 代表第一个页面的渲染层
可以看到
View
标签被解析成wx-view
以及image
标签解析成wx-image
。这些标签是小程序自定义组件类型。通过小程序内置基础库进行处理。并且可以看到小程序处理标签会在最终解析的元素上有一个
exparser
字段。Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。
exparser组件模型其实也是参考了web component中的shadow DOM并且进行了一些修改。事件系统、插槽、属性传递等都基本一致。
逻辑层APP Service
在控制台输入document
可以看到js逻辑层的代码
由上面的内容所说,逻辑层和渲染层的通信是通过native
作为中间件进行转发。想要看到其中如何通信的,那么就需要进入到微信的基础库
来进行查看。
微信基础库
通过在微信开发者工具console窗口中直接输入openVendor()
会打开本地小程序的weappVendor
目录,可以从目录中看到一下几个类型的文件:
wcc
负责将wxml
编译成js文件;将动态数据识别后,再转换为htmlwcsc
负责将wxss
编译成js文件,wcsc 将 css 编译成对应的 js,因为要将 rpx 识别,然后再转换为 css.wxvpkg
结尾的文件,表示不同微信版本的基础库;
wcc
wcc是负责把wxml编译成js文件。可以在本地将weappVendor
目录下的wcc和wcsc复制到项目文件下。
-
创建两个基础文件,wxml和wxss,然后执行以下命令可以证实两个文件的作用。
./wcc -b index.wxml >> wxml.js
并且可以在开发者工具控制台中,查看到逻辑层的处理逻辑里面,有个
generataFunc
函数,调用了$gwx
函数,这个函数的作用是将组件标签转换成js,生成虚拟dom.- 利用
$gwx
创建虚拟dom的节点树 - 创建自定义事件
window.CustomEvent
(generateFuncReady) - 派发这个事件
dispatchEvent
。 __global.timing.addPoint('PAGEFRAME_GENERATE_FUNC_READY', Date.now())
监听这个事件,js逻辑视图层已经准备好。
- 利用
Wcsc
用来处理wxss文件,将css编译成js文件,并且转换rpx尺寸。
利用如下命令可以将wxss文件转换成js文件
./wcsc -js ./pages/index/index.wxss >> wxss_output.js
-
rpx单位的换算,转换成px;
rpx(responsive pixel) : 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
设备 rpx换算px (屏幕宽度/750) px换算rpx (750/屏幕宽度) iPhone5 1rpx = 0.42px 1px = 2.34rpx iPhone6 1rpx = 0.5px 1px = 2rpx iPhone6 Plus 1rpx = 0.552px 1px = 1.81rpx -
通过
setCssToHead
方法,将转换好的css添加到head中。
.wxvpkg
代表着不同版本的基础库。可以通过工具解压后可以得到小程序基础库的具体实现逻辑的文件。
解压之后可以看到基础的有两个主要的文件:
WAWebview
:小程序视图层基础库,提供视图层基础能力WAService
:小程序逻辑层基础库,提供逻辑层基础能力
WAWebview源码
// WAWebview.js
var __wxLibrary = {
fileName: 'WAWebview.js',
envType: 'WebView',
contextType: 'others',
execStart: Date.now()
};
var __WAWebviewStartTime__ = Date.now();
var __libVersionInfo__ = {
"updateTime": "2020.11.25 23:32:34",
"version": "2.13.2",
"features": {
"pruneWxConfigByPage": true,
"injectGameContextPlugin": true,
"lazyCodeLoading2": true,
"injectAppSeparatedPlugin": true,
"nativeTrans": true
}
};
/**
* core-js 模块
*/
!function(n, o, Ye) {
...
}, function(e, t, i) {
var n = i(3),
o = "__core-js_shared__",
r = n[o] || (n[o] = {});
e.exports = function(e) {
return r[e] || (r[e] = {})
}
...
}(1, 1);
var __wxTest__ = !1,
var __wxConfig;
var wxRunOnDebug = function(e) {
e()
};
/**
* 基础模块
*/
var Foundation = function(i) {
...
}]).default;
var nativeTrans = function(e) {
...
}(this);
/**
* 消息通信模块
*/
var WeixinJSBridge = function(e) {
...
}(this);
/**
* 监听 nativeTrans 相关事件
*/
function() {
...
}();
/**
* 解析配置
*/
function(r) {
...
__wxConfig = _(__wxConfig), __wxConfig = v(__wxConfig), Foundation.onConfigReady(function() {
m()
}), n ? __wxConfig.__readyHandler = A : d ? Foundation.onBridgeReady(function() {
WeixinJSBridge.on("onWxConfigReady", A)
}) : Foundation.onLibraryReady(A)
}(this);
/**
* 异常捕获(error、onunhandledrejection)
*/
function(e) {
function t(e) {
Foundation.emit("unhandledRejection", e) || console.error("Uncaught (in promise)", e.reason)
}
"object" == typeof e && "function" == typeof e.addEventListener ? (e.addEventListener("unhandledrejection", function(e) {
t({
reason: e.reason,
promise: e.promise
}), e.preventDefault()
}), e.addEventListener("error", function(e) {
var t;
t = e.error, Foundation.emit("error", t) || console.error("Uncaught", t), e.preventDefault()
})) : void 0 === e.onunhandledrejection && Object.defineProperty(e, "onunhandledrejection", {
value: function(e) {
t({
reason: (e = e || {}).reason,
promise: e.promise
})
}
})
}(this);
/**
* 原生缓冲区
*/
var NativeBuffer = function(e) {
...
}(this);
var WeixinNativeBuffer = NativeBuffer;
var NativeBuffer = null;
/**
* 日志模块:wxConsole、wxPerfConsole、wxNativeConsole、__webviewConsole__
*/
var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxNativeConsole = function(i) {
...
}([function(e, t, i) {
...
}]).default;
var __webviewConsole__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 上报模块
*/
var Reporter = function(i) {
...
}([function(e, L, O) {
...
}]).default;
var Perf = function(i) {
...
}([function(e, t, i) {
...
}]).default;
/**
* 视图层 API
*/
var __webViewSDK__ = function(i) {
...
}([function(e, L, O) {
...
}]).default;
var wx = __webViewSDK__.wx;
/**
* 组件系统
*/
var exparser = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 框架粘合层
*
* 使用 exparser.registerBehavior 和 exparser.registerElement 方法注册内置组件
* 转发 window、wx 对象上到事件转发到 exparser
*/
!function(i) {
...
}([function(e, t) {
...
}, function(e, t) {}, , function(e, t) {}]);
/**
* Virtual DOM
*/
var __virtualDOMDataThread__ = !1,
var __virtualDOM__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* __webviewEngine__
*/
var __webviewEngine__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 注入默认样式到页面
*/
!function() {
...
function e() {
var e = i('...');
__wxConfig.isReady ? void 0 !== __wxConfig.theme && i(t, e.nextElementSibling) : __wxConfig.onReady(function() {
void 0 !== __wxConfig.theme && i(t, e.nextElementSibling)
})
}
window.document && "complete" === window.document.readyState ? e() : window.onload = e
}();
var __WAWebviewEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;
由上面源码可以看出,WAwebview主要实现了以下几个部分
Foundation
: 基础模块WeixinJSBridge
: 消息通信模块exparser
: 组件系统模块__virtualDOM__
: Virtual DOM 模块__webViewSDK__
: WebView SDK 模块Reporter
: 日志上报模块(异常和性能统计数据)
WAService 源码
var __wxLibrary = {
fileName: 'WAService.js',
envType: 'Service',
contextType: 'App:Uncertain',
execStart: Date.now()
};
var __WAServiceStartTime__ = Date.now();
(function(global) {
var __exportGlobal__ = {};
var __libVersionInfo__ = {
"updateTime": "2020.11.25 23:32:34",
"version": "2.13.2",
"features": {
"pruneWxConfigByPage": true,
"injectGameContextPlugin": true,
"lazyCodeLoading2": true,
"injectAppSeparatedPlugin": true,
"nativeTrans": true
}
};
var __Function__ = global.Function;
var Function = __Function__;
/**
* core-js 模块
*/
!function(r, o, Ke) {
}(1, 1);
var __wxTest__ = !1;
wxRunOnDebug = function(A) {
A()
};
var __wxConfig;
/**
* 基础模块
*/
var Foundation = function(n) {
...
}([function(e, t, n) {
...
}]).default;
var nativeTrans = function(e) {
...
}(this);
/**
* 消息通信模块
*/
var WeixinJSBridge = function(e) {
...
}(this);
/**
* 监听 nativeTrans 相关事件
*/
function() {
...
}();
/**
* 解析配置
*/
function(i) {
...
}(this);
/**
* 异常捕获(error、onunhandledrejection)
*/
!function(A) {
...
}(this);
/**
* 原生缓冲区
*/
var NativeBuffer = function(e) {
...
}(this);
WeixinNativeBuffer = NativeBuffer;
NativeBuffer = null;
var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxNativeConsole = function(n) {
...
}([function(e, t, n) {
...
}]).default;
/**
* Worker 模块
*/
var WeixinWorker = function(A) {
...
}(this);
/**
* JSContext
*/
var JSContext = function(n) {
...
}([
...
}]).default;
var __appServiceConsole__ = function(n) {
...
}([function(e, N, R) {
...
}]).default;
var Protect = function(n) {
...
}([function(e, t, n) {
...
}]);
var Reporter = function(n) {
...
}([function(e, N, R) {
...
}]).default;
var __subContextEngine__ = function(n) {
...
}([function(e, t, n) {
...
}]);
var __waServiceInit__ = function() {
...
}
function __doWAServiceInit__() {
var e;
"undefined" != typeof wx && wx.version && (e = wx.version), __waServiceInit__(), e && "undefined" != typeof __exportGlobal__ && __exportGlobal__.wx && (__exportGlobal__.wx.version = e)
}
__subContextEngine__.isIsolateContext();
__subContextEngine__.isIsolateContext() || __doWAServiceInit__();
__subContextEngine__.initAppRelatedContexts(__exportGlobal__);
})(this);
var __WAServiceEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;
以上源码结构也可以看到分为以下模块:
Foundation
: 基础模块WeixinJSBridge
: 消息通信模块WeixinNativeBuffer
: 原生Buffer
WeixinWorker
:Worker
线程JSContext
: JS Engine ContextProtect
: JS 保护的对象__subContextEngine__
: 提供App
、Page
、Component
、Behavior
、getApp
、getCurrentPages
等方法
Foundation模块
渲染层和逻辑层都拥有这个Foundation
模块,这个模块被称为基础模块。那我们来看看这个模块具体做了什么:
在控制台输入Foundation
代码回车,即可看到该模块输出的一些环境变量。
- 环境变量env
- 挂在window的全局变量
- 发布订阅的EventEmitter
- 配置/基础库/通信桥Ready事件等
Exparser模块
小程序的渲染层文章前面已经说过是由webview包裹渲染的。小程序里面无法使用web组件和动态执行js。exparser是微信小程序的组件组织框架,内置在小程序基础框架中,为小程序的各种组件提供基础的支持。Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现。
Exparser 的主要特点包括以下几点:
- 基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程。
- 可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、createSelectorQuery 调用和自定义组件特性等。
Virtual DOM 模块
生成虚拟dom的模块,但是这里其他文章中提到生成的不是原生DOM,而是模拟了各种DOM接口的wx-element对象。
之前我们说过,微信小程序内部执行文件是通过wcc
和wcsc
分别编译wxml和wxss文件为对应的js文件。那么通过demo看出来,一个wxml文件中的组件节点被编译成如下内容:
wx-page
、wx-text
、wx-view
代表的是小程序自定义的组件。在这里渲染层的处理中调用$gwx
将wxml处理完虚拟dom之后,怎么讲数据data进行绑定呢。在其中可以看到,在编译的js文件中有这样一段代码:
if (path && e_[path]) {
window.__wxml_comp_version__ = 0.02
return function (env, dd, global) {
$gwxc = 0;
var root = {
"tag": "wx-page"
};
root.children = []
var main = e_[path].f
if (typeof global === "undefined") global = {};
global.f = $gdc(f_[path], "", 1);
if (typeof (window.__webview_engine_version__) != 'undefined' && window.__webview_engine_version__ + 1e-6 >= 0.02 + 1e-6 && window.__mergeData__) {
env = window.__mergeData__(env, dd); // 合并data数据
}
try {
main(env, {}, root, global);
_tsd(root)
if (typeof (window.__webview_engine_version__) == 'undefined' || window.__webview_engine_version__ + 1e-6 < 0.01 + 1e-6) {
return _ev(root);
}
} catch (err) {
console.log(err)
}
return root;
}
}
那么在经历wxml转换成js文件过程以下:
- wcc编译wxml得到一个js文件
- js文件接受一个wxml文件,得到一个虚拟的dom
- 然后通过exparser将虚拟的dom解析成真实的dom。并且合并绑定的data数据。渲染到页面上。
wcsc将wxss编译成js文件过程:
- wcsc将wxss编译成js文件
- 进行rpx的转换
- 创建style标签,将css插入到head
- 生成webview在渲染层。
WeixinJSBridge模块
属于通信模块了,是渲染层和逻辑层之间的通信桥梁。可以看到提供了以下的方法来进行消息通信机制。
方法名 | 作用 |
---|---|
invoke | JS 调用 Native API |
invokeCallbackHandler | Native 传递 invoke 方法回调结果 |
on | JS 监听 Native 消息 |
publish | 视图层发布消息 |
subscribe | 订阅逻辑层的消息 |
subscribeHandler | 视图层和逻辑层消息订阅转发 |
setCustomPublishHandler | 自定义消息转发 |
回到最初提出的问题:
-
为什么小程序的逻辑层单独存在于一个线程中 小程序双线程模式,逻辑层和视图层通过
WeixinBridge
进行通信,这样的互不影响,也不会出现js阻塞页面。 -
为什么小程序无法通过js操作DOM 小程序因为是双线程,逻辑层和视图层是不影响的,视图层无法访问到视图层的dom;
如果开发者可以直接通过
JS
操作界面的DOM
树,那么一些敏感数据就毫无安全性可言,故微信提供了一个沙箱的环境来运行开发者的JS
代码,这个环境不能有任何的浏览器相关的接口,只能通过JS
解释执行环境,类似于HTML5
的ServiceWorker
启动另一个线程来执行JS
-
小程序的wxml是如何被识别成页面结构的
- wcc编译wxml得到一个js文件
- js文件接受一个wxml文件,得到一个虚拟的dom
- 然后通过exparser将虚拟的dom解析成真实的dom。并且合并绑定的data数据。渲染到页面上。
-
微信小程序的rpx如何进行的变换以及实现
小程序在最开始的时候会先获取设备的宽度和dpr。
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素
由此可以看出,小程序在实现rpx转换时,不论是什么屏幕的手机,都是将屏幕宽度固定设为750rpx,然后根据实际屏幕的设备像素比
dpr
(dpr = 设备像素 / css像素)来进行转换的。具体对应关系如下:1rpx = (number/ 750) * 设备宽度 px
在视图层,wxss样式文件经rpx初始转换后并将样式注入到页面过程中,会向
window.__rpxRecalculatingFuncs__
数组中收集窗口变化时的回调;先看wcsc
可执行程序输出的处理rpx转换相关的setCssToHead函数实现,其最终返回rewritor函数。换后的样式嵌入到document.head
中后,依然保存有创建的style元素的句柄,在页面窗口变更时执行对应的回调来修正rpx转换后的px值。webview基础库中内部注册窗口变更事件回调来通知样式文件重新进行rpx转换。
-
小程序怎么识别wxss
wcsc将wxss编译成js文件过程:
- wcsc将wxss编译成js文件
- 进行rpx的转换
- 创建style标签,将css插入到head
- 生成webview在渲染层。
小程序的自动化发布
小程序除了可以通过执行minprogram-ci
命令来进行小程序代码的上传。可以通过微信第三方平台支撑的api接口来搭建一个小程序发布平台实现构建、上传、提交代码、审核代码、发布等各种流程。此处不再细说,可以去微信文档进行详细查看~
总结
大概的跟着教程以及其他可参考资料进行了一遍梳理,希望自己在写小程序代码中能够清楚小程序的内部处理原理。如果有啥不妥,欢迎指正和交流~
【参考文献】
这是一个从 https://juejin.cn/post/7368784048574693410 下的原始话题分离的讨论话题