• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

微信小程序技术原理分析

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

微信小程序技术原理分析

来源 https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html

 

前言

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

一方面微信、支付宝等各家小程序平台遍地开花,另一方面移动开发插件化技术逐渐没落,移动应用构建的方式在悄悄的发生变化。对于企业应用形态而言,也在逐步发生变化,超级 APP(移动门户)+ 轻应用是一种新的流行趋势。微信、支付宝是互联网生态下的“移动门户”,手机银行是金融典型的 ToC “移动门户”。

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

双线程模型

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

小程序的双层架构思想可以追溯到 PWA,但又有所扬弃。

 PWA小程序框架
逻辑层 以 Service Worker 为载体。开发者需编写业务逻辑、管理资源缓存。 以 JSCore 或 V8 引擎为载体。开发者只需编写业务逻辑。
渲染层 基于 Web 网页的单页或多标签页方案。 基于多个 WebView 组成的页面栈。

小程序框架与 PWA 相比,小程序的开发者可以更聚焦于业务逻辑,而无需关注静态资源的缓存。小程序包的缓存和更新机制交由小程序框架自动完成,开发者可以在适当时机通过 API 影响这一过程。小程序的渲染层由多个 WebView 组成的页面栈构成,这与 PWA 相比有着更接近移动端原生应用的用户体验。同时,小程序的开发者也能更从容地处理多页面间跳转时页面状态的变化。

类似于微信 JSSDK 这样的 Hybrid 技术,微信小程序的界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的 WebView 去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个 WebView 的任务过于繁重。此外,界面渲染这一块我们定义了一套内置组件以统一体验,并且提供一些基础和通用的能力,进一步降低开发者的学习门槛。值得一提的是,内置组件有一部分较复杂组件是用客户端原生实现的同层渲染,以提供更好的性能。

为什么要这么设计呢?

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

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

运行环境逻辑层渲染层
Android V8 Chromium 定制内核
iOS JavaScriptCore WKWebView
小程序开发者工具 NWJS Chrome WebView

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

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

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

开发工具

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

调试逻辑层

在微信开发者工具中,Workbench 的 DevTools 调试器默认与模拟器逻辑层连接,所以 DevTools 中的 Console 面板进行输入 JS 脚本,JS 脚本实际的执行环境是逻辑层的 JS Context,对于逻辑层的调试,可以直接在调试器中进行。编译运行你的小程序项目,然后打开控制台,输入 document 并回车,就可以看到小程序逻辑层 WebView,如下图:

调试视图层

模拟器视图层 WebView 的就相对逻辑层麻烦一些,需要在 IDE 的 DevTools 下中注入 JS 打开视图层 WebView 的 DevTools。

IDE DevTools 面板的 Console Panel 输入:

// 查找 WebView 元素
$$(\'webview\')
// 打开 视图层 WebView DevTools
$$(\'webview\')[0].showDevTools(true)

然后就可以在视图层 WebView 的 DevTools 中进行调试了。

逆向技巧

获取基础库

我们如何能够拿到视图层和逻辑层 WebView 加载的文件呢?

  • 基于 Sources 面板的 Save AS 功能获取代码

  • 基于开发者工具内置命令 openVendor() 找到 .wxvpkg 包获取代码

在开发者工具中使用 help() 方法,可以查看一些指令和方法。

openVendor 命令可以打开微信开发者工具在小程序框架所在目录。我们可以在微信小程序 IDE 控制台输入 openVendor 命令,可以打开微信小程序开发工具的资源目录:

我们可以看到有 wccwcsc,小程序各版本的基础库包 .wxvpkg.wxvpkg 文件可以使用 wechat-app-unpack 解开,解开后里面就是 WAService.js 和 WAWebView.js 等代码。

  • 利用 apktool 反编译微信客户端

我们可以找到 wxa_library 文件夹,这个和上面微信开发工具中 .wxvpkg 包解开的结构很类似,这就是小程序的基础库。

反编译代码

python2 unwxapkg.py [filename]

解开后的目录结构:

客户端中的 .wxvpkg 包比 IDE WeappVendor 文件夹下的包多一些文件。

find . -type f -name \'*.js\' -exec js-beautify -r -s 2 -p -f \'{}\' \;
  • 利用 jsnice 美化代码

关于代码的逆向还原细节本文暂不做详细介绍,后面专门写文章展开讲解。

基础库

整体架构

小程序的基础库是 JavaScript 编写的,基础库提供组件和 API,处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑,可以被注入到渲染层和逻辑层运行。在渲染层可以用各类组件组建界面的元素,在逻辑层可以用各类 API 来处理各种逻辑。PageView 可以是 WebView、React-Native-Like、Flutter 来渲染,详细架构设计可以参考:基于小程序技术栈的微信客户端跨平台实践

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

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

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

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);

!function() {
  function e(e, t, i) {
    return e !== nativeTrans.EVT_WV_CREATED && (nativeTrans.publish(e, t, i), true);
  }
  if (nativeTrans) {
    if (nativeTrans.isService) {
      nativeTrans.onMessage(function(e, t, i) {
        e !== nativeTrans.EVT_NTRANS_READY && WeixinJSBridge.subscribeHandler(e, t, i, {
          nativeTime: Date.now()
        });
      });
      WeixinJSBridge.subscribe(nativeTrans.EVT_WV_CREATED, function(e) {
        nativeTrans.registerTarget(e.id + "", e.id);
      });
    } else {
      nativeTrans.onMessage(function(e, t, i) {
        e !== nativeTrans.EVT_NTRANS_READY ? WeixinJSBridge.subscribeHandler(e, t, void 0, {
          nativeTime: Date.now()
        }) : nativeTrans.registerTarget(nativeTrans.SERVICE_TAG, i);
      });
      nativeTrans.onceServiceConfirmed(function() {
        WeixinJSBridge.setCustomPublishHandler(null);
        WeixinJSBridge.publish(nativeTrans.EVT_WV_CREATED, {
          id: nativeTrans.id
        });
        WeixinJSBridge.setCustomPublishHandler(e);
      });
    }
    WeixinJSBridge.setCustomPublishHandler(e);
  }
}();

/**
 * 解析配置文件
 */
!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: 日志上报模块(异常和性能统计数据)

WAWebview 基础模块

基础模块提供环境变量 env、发布订阅 EventEmitter、配置/基础库/通信桥 Ready 事件。

WAWebview 组件系统模块

小程序的视图是在 WebView 里渲染的,为解决管控与安全,小程序里面不能使用 Web 组件和动态执行 JavaScript。Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。Exparser 的组件模型与 WebComponents 标准中的 ShadowDOM 高度相似。Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现。

Exparser 的主要特点包括以下几点:

  • 基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程。
  • 可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  • 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、createSelectorQuery 调用和自定义组件特性等。

WAWebview Virtual DOM 模块

Virtual DOM 模块提供了如下几个方法:

早期接口与 virtual-dom 一致,这里特别的地方在于它所 diff 和生成的并不是原生 DOM,而是各种模拟了 DOM 接口的 wx- element 对象。

WAWebview WebView SDK 模块

消息通信模块

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

方法名作用
invoke JS 调用 Native API
invokeCallbackHandler Native 传递 invoke 方法回调结果
on JS 监听 Native 消息
publish 视图层发布消息
subscribe 订阅逻辑层的消息
subscribeHandler 视图层和逻辑层消息订阅转发
setCustomPublishHandler 自定义消息转发

WAService 源码结构

WAService 基本组成:

  • WeixinJSBridge:消息通信,处理 AppService 与 Native 消息通信
  • WeixinNativeBuffer: 原生 Buffer
  • WeixinWorker: Worker 线程
  • JSContext: JS 引擎
  • Protect: JS 保护的对象
  • Reporter: 日志组件
  • appServiceEngine: 提供 AppPageComponentBehaviorgetAppgetCurrentPages 等方法
  • AMD 接口:为 global 对象添加 AMD 接口 requiredefine

编译原理

微信开发者工具和微信客户端都无法直接运行小程序的源码,因此我们需要对小程序的源码进行编译。代码编译过程包括本地预处理、本地编译和服务器编译。为了快速预览,微信开发者工具模拟器运行的代码只经过本地预处理、本地编译,没有服务器编译过程,而微信客户端运行的代码是额外经过服务器编译的。

微信官方提供了 wcc 和 wcsc 两个编译工具,wcc 编译器可以将 wxml 文件编译成 JS 文件,wcsc 编译器可以将 wxss 文件编译成 JS 文件。

编译 WXML

我们这里一步步去研究微信官方编译器,先研究看看 wcc 做了什么事情。

例如编译 wxml 为 JS:

index.wxml:

<view>
  <text class="window">{{ text }}</text>
</view>

借助 miniprogram-compiler 转化:

const fs = require("fs");
const miniprogramCompiler = require("miniprogram-compiler");

const path = require("path");
let compileResult = miniprogramCompiler.wxmlToJs(path.join(__dirname));
fs.writeFileSync("index.wxml.js", compileResult);

编译之后的代码为:

window.__wcc_version__ = \'v0.5vv_20181221_syb_scopedata\';
window.__wcc_version_info__ = {
  customComponents: true,
  fixZeroRpx: true,
  propValueDeepCopy: false
};
var $gwxc;
var $gaic = {};
$gwx = function(path, global) {
  ...
}
return $gwx;

                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap