小程序框架原理之渲染流程及通信流程

6,491 阅读7分钟

前言

互联网生态演进:超级 APP + 小程序成为「轻应用时代」下的新生态。

小程序方式构建应用是大趋势,被越来越多的企业用户看到其中的优势,构建一个跨多端平台的小程序开发平台是一种思路,帮助企业用户构建一个具备小程序能力的“移动门户”也是一种思路。本文主要调研微信小程序运行时的基本原理,从而构建一个适合我们自己平台的小程序运行框架。

双线程

小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。一个小程序存在多个界面,所以渲染层存在多个 WebView。这两个线程间的通信经由小程序 Native 侧中转,逻辑层发送网络请求也经由 Native 侧转发,小程序的通信模型下图所示。

  • 视图层主要负责页面的渲染
  • 逻辑层负责js的执行。

他们之间通过event和data来通信。通信是有微信客户端(native)做的一层中转;然后也可以通过jsBridge来调用原生的api,比如什么相机、扫码等功能。这个视图层,最后我们打包出来的代码,就是html和css,在这里面运行,视图层目前使用 WebView 作为渲染载体

逻辑层是由独立的 JsCore 作为js的运行环境,所以他和浏览器不一样,只有一些js对应的方法,不能直接操作dom和获取dom,中间都需要通信这一层中转,在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。

当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。 即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

由于这之间他们是彼此独立的,是基于消息驱动来渲染的,所以不会阻塞页面;,我的渲染不会影响你的js逻辑,js的执行也不会柱塞渲染的过程;

比如你在发送一些请求的时候,这种一般是经由native转发;

为什么要这样子设计呢?

为了管控和安全,微信小程序阻止开发者使用一些浏览器提供的,诸如跳转页面、操作 DOM、动态执行脚本的开放性接口。将逻辑层与视图层进行分离,视图层和逻辑层之间只有数据的通信,可以防止开发者随意操作界面,更好的保证了用户数据安全。

微信小程序视图层是 WebView,逻辑层是 JS 引擎。三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

我们看一下单 WebView 实例与小程序双线程多实例下代码执行的差异点。

单 WebView 模式下,Page 视图与 App 逻辑共享同一个 JSContext,这样所有的页面可以共享全局的数据和方法,能够实现全局的状态管理。多 WebView 模式下,每一个 WebView 都有一个独立的 JSContext,虽然可以通过窗口通信实现数据传递,但是无法共享数据和方法,对于全局的状态管理也相对比较复杂,抽离一个通用的 WebView 或者 JS Engine 作为应用的 JSContext 就可以解决这些问题,但是同时引入了其他问题:视图和逻辑如何通信,在小程序里面数据更新后视图是异步更新的。

小程序双线程交互的生命周期图示:

开发者工具

微信开发者工具是基于 NW.js 构建,主要由工具栏、模拟器、编辑器、调试器四大部分组成。通过 微信开发者工具 => 调试 => 调试微信微信开发者工具 可以打开小程序 IDE DevTools 面板。通过 DevTools 审查我们可以发现模拟器是通过 WebView 展示页面。微信小程序是双线程的设计,所以存在视图层和逻辑层两个 WebView。

wxml真面目

我们都知道小程序提供了很多方便快捷的自定义组件(标签),但你知道小程序的这些组件编译过后会渲染成什么吗?先说答案,其实 wxml 经过编译后会渲染成 html 。很简单的一点,你发现在小程序内编写 html 标签,最终也可以运行。

光说可能体会不到,下面开始探寻小程序真实渲染的样子。先看下开发者工具内 wxml 的内容,待会和真实渲染的内容做对比。

打开调试工具

接下来一步步找到小程序 wxml 渲染完成的真实样子,工具菜单栏点击微信开发者工具,选择调试微信开发者工具。打开的控制台可以调试整个微信开发者工具,用调试箭头指向小程序内容区域,这时可以看到小程序视图层是被嵌套在 webview 的 iframe 内。

如果想要查看调试 webview,只需选中 webview 打开它的调试工具即可,在控制台输入以下代码:

document.getElementsByTagName('webview')[1].showDevTools(true,null)

可以看到又打开了一个调试窗口,这里面就是小程序视图层渲染的真实样子:

可以看到结构和 wxml 里的内容几乎一模一样,只是view 变成了 wx-view等。这些都是内部实现的一套对应小程序标签的 webComponent 组件,而 webComponent 实际渲染出来还是 html 标签。

转换过程

转换过程是微信开发者工具内部通过一个可执行编译工具实现对小程序文件转换。在微信开发者工具控制台输入 openVendor() 会打开一个文件夹,里面存放着微信的基础库及工具,在里面可以找到 wcc.exe、wcsc.exe 执行文件,分别对应 wxml 和 wxss 的文件转换。

该工具可以单独对小程序文件进行转换,使用方法 ./wcc -d wxml文件路径 >> 输出路径。例如,将工具复制到一个文件夹内,再将一个 wxml 放入该文件夹内,命令行输入 :

./wcc -d index.wxml >> index.js

可能有人很好奇为什么是生成 js 文件,而不是 html 文件。原因很简单,因为需要处理 wxml 的动态绑定数据。看看这个 js 文件生成的是什么:

因为这些都是混淆压缩过的代码,基本没有可读性。这里只需要注意一个函数就好,那就是 $gwx。这是个很关键的函数,它的作用是生成虚拟dom树,用于渲染真实节点。

接下来回到 webview 调试窗口,在 head 内找到这段插入的 script 标签代码:

有没有很熟悉,没错,就是和上面转换后的代码是同一个东西。也就是说,我们的 wxml 文件通过编译,最终在视图层中执行的就是这段 js 代码,实际需要向逻辑层获取数据才能渲染页面。控制台输入 gwx发现这个函数存在,那么这个函数如何生成虚拟dom呢?gwx 发现这个函数存在,那么这个函数如何生成虚拟dom呢?gwx 函数的第一个参数接收一个路径参数,这个路径就是 wxml 文件路径,此时在控制台输入:

generateFunc()

单纯调用 generateFunc 生成出来的虚拟dom是没有动态绑定数据的,如果想要动态的绑定数据,在调用 generateFunc 时传入一个数据对象。但是数据全在逻辑层里,这时就需要进行通信了。

wxss是如何工作的?

wxss 工作原理和 wxml 差不多,都是通过工具转换为 js。为什么又是转换成 js,因为有 rpx 单位,需要根据手机尺寸进行设置 px。

wcsc.exe 转换命令如下:

./wcsc -js index.wxss >> index.js

之后创建 style 标签,动态添加到视图层中

基础库架构

小程序的基础库主要分为:

  • WAWebview:小程序视图层基础库,提供视图层基础能力
  • WAService:小程序逻辑层基础库,提供逻辑层基础能力

微信小程序基础库更新过程可能会对基础库有些变更,下面就 v2.12.2 版本对基础库进行分析:

WAWebview 源码结构

借助于 VS Code 折叠功能,将基础库中 WAWebview 文件美化后,并且进行必要的模块结构拆分,可以看到代码主要结构如下:

var __wxLibrary = {
  fileName: 'WAWebview.js',
  envType: 'WebView',
  contextType: 'others',
  execStart: Date.now()
};
var __WAWebviewStartTime__ = Date.now();
var __libVersionInfo__ = {
  "updateTime": "2020.4.4 10:25:02",
  "version": "2.10.4"
};

/**
 * 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 __wxConfig;
var __wxTest__ = false;

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__ = false;
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 源码结构

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.18 10:25:02",
    "version": "2.10.4"
  };
  var __Function__ = global.Function;
  var Function = __Function__;

  /**
   * core-js 模块
   */
  !function(r, o, Ke) {
  }(1, 1);

  var __wxTest__ = false;
  var wxRunOnDebug = function(e) {
    e()
  };

  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(e) {
    ...
  }(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(e) {
    ...
  }(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;

WAService 基本组成:

  • Foundation: 基础模块
  • WeixinJSBridge: 消息通信模块
  • WeixinNativeBuffer: 原生 Buffer
  • WeixinWorker: Worker 线程
  • JSContext: JS Engine Context
  • Protect: JS 保护的对象
  • subContextEngine: 提供 App、Page、Component、Behavior、getApp、getCurrentPages 等方法

WeixinJSBridge 模块

WeixinJSBridge 提供了视图层 JS 与 Native、视图层与逻辑层之间消息通信的机制,提供了如下几个方法:

数据通信

首先要知道小程序时运行在基础库之上的,但它们都是压缩打包好的,后面找到反编译出来的基础库代码,其中最重要的就是 WAService.js 和 WAWebview.js,它们分别是视图层和逻辑层的核心实现。

它们之间需要一个桥梁来进行通信,那就是 JS Bridge。JS Bridge 提供调用原生功能的接口(摄像头,定位等),它的核心是构建原生和非原生间消息通信的通道,而且这个通信的通道是双向的。通过 JS Bridge 的发布订阅方法,视图层和逻辑层进行数据通信。

通信流程

  1. wxml 转换成对应的 js 文件,等待生成虚拟dom函数 $gwx 准备完成,使用 dispatchEvent自定义事件 通知 WAWebview。

  2. WAWebview 监听到 generateFuncReady 事件触发,使用 WeixinJSBridge.publish 向逻辑层通信。

  3. 逻辑层处理逻辑,也就是我们平常写的小程序 js 文件里的东西,然后通过 JS Bridge 通知并返回数据给视图层。

  4. 视图层接收到数据,将数据传入生成虚拟dom的函数内,渲染页面,当然小程序也有相应的diff算法。

// 例如在 wxml 中绑定一个动态数据 title,视图层接收到数据后,重新生成虚拟dom
generateFunc({
  title: '小程序接口'
})

  1. 初始化完成后,就会走对应的其他生命周期,或者用户触发事件,数据都会在逻辑层处理完成后通过 JS Bridge 通知到视图层,视图层再次调用生成虚拟dom的函数,更新页面。

总结

古人说 “纸上得来终觉浅,绝知此事要躬行”,只有真的的手写一些例子对这句话理解更深。本文是一个学习过程记录,还是要自己亲自动手实践才能理解更深。

参考引用