In March 2020, after supporting the compile-time scheme, the Rax applet released a version that supports the run-time scheme. Up to now, Rax is still the only small program development framework in the industry that supports both compile-time and run-time solutions. This article will introduce to you the principles of the Rax applet runtime solution and our thinking.
Before introducing the run-time solution, let’s review what the compile-time solution is. As the name suggests, the compile-time solution focuses on compilation, and the representative framework is Taro v2.x. It converts JSX into the template language of the mini program (i.e. WXML/AXML, etc.) through static compilation, and then supplements it with lightweight runtime JS code to smooth the difference between the life cycle of the mini program and the React life cycle, making it Users can develop small programs with the familiar React DSL. The compile-time scheme principle of Rax is similar to that of Taro v2.x. For implementation details, you can refer to the previous articles Analysis of the Principle of the Rax to Mini Program Link (1) and Analysis of the Principle of the Rax Mini Program Compilation Scheme. Different from the compile-time solution, the run-time solution focuses on implementing rendering capabilities at runtime and does not rely on static compilation. Therefore, there are almost no syntax restrictions, which is its biggest feature. Let's take a look at the principles of runtime solution implementation.
The underlying implementation of the mini program is actually based on Web technology, but when reflected at the developer level, it is quite different from the Web. In the mini program, the logic layer and the view layer are isolated. The logic layer passes data to the view layer to trigger rendering through the unique setData
method, and the view layer triggers the logic layer code through events. Its architecture is as shown below shown. Compared with web development, developers can use JS to call the DOM/BOM API provided by the browser to manipulate and render content as they wish. The architecture of mini programs is more closed and safer, but it also means that web code cannot run directly on mini programs.
For modern front-end frameworks (React/Vue), the bottom layer basically creates views by calling the DOM API. The view layer template of the mini program needs to be written by the developer in advance, which means that the method of dynamically creating DOM is not allowed in the mini program. However, the "self-reference" feature of the mini program's custom component opens up a breakthrough for dynamically creating DOM. The so-called self-reference means that the custom component supports using itself as a child node, which means that through recursive reference, we can construct any level and number of DOM trees.
For example, assume that the WXML template of a small program custom component element is as follows:
<view> <block> <element></element> </block></view><text> {{r.content}}</text>复制代码
Notice that element refers to itself recursively in the template and terminates the recursion through conditional judgment . Then, when the logic layer passes the following data through setData
:
{ "nodeId": "1", "tagName": "view", "children": [ { "nodeId": "2", "tagName": "text", “content”: “我是?" }, { "nodeId": "3", “tagName": "text", "content": "rax" } ] }复制代码
The final view becomes:
<view> <text>我是</text> <text>rax</text></view>复制代码
In this way, We cleverly implemented the ability to dynamically render views based on the incoming setData
data when the WXML template is fixed. And this is the basis for the birth of runtime solutions.
Rax’s runtime solution is derived from kbone, an isomorphic solution for mini programs and web end officially launched by WeChat. The design principles of kbone can be referred to its official website introduction. A simple summary is to simulate the DOM/BOM API at the logical layer, convert these methods of creating views into maintaining a VDOM tree, and then convert it into the corresponding setData
data, and finally render the actual view recursively through the preset template. The basic principle of the process from DOM API to maintaining VDOM tree is not complicated. createElement/appendChild/insertBefore/removeChild and so on correspond to the basic data structure operations.
Students who are familiar with Rax should know that in order to support cross-terminal, Rax has a driver design. In fact, we can write another driver for the small program and implement its interface API based on the above principles. But our final choice was to complete the entire rendering mechanism through a lower-level simulated BOM/DOM API. The considerations for doing this are, first, based on kbone development, which is the fastest solution. The driver on the mini program side only needs to reuse the driver-dom on the web side. After all, the underlying document
andwindow
The variables have been simulated; secondly, it is because we want to provide developers with a development experience that is closer to the web. This solution means that in addition to using JSX, developers can also directly use the BOM/DOM API to create views, which will be more flexible. We look at all the mini program runtime frameworks on the market. Remax directly interfaces with mini programs from the VDOM layer through react-reconciler (similar to the Rax mini program driver design mentioned above), while kbone and Taro 3.0 both choose to use simulation. Web environment to implement rendering. This is also related to the design intention of the framework developer, and opinions vary. The basic schematic diagram of the Rax applet runtime solution is as follows:
In the Rax applet runtime, the library that simulates the DOM/BOM API is Decryption and thinking about Rax applet runtime solution. The APIs it supports are as follows:
In addition to processing rendering data, another important thing is the event system. It implements a complete event dispatch mechanism through the EventTarget
base class. Logical layer DOM nodes all inherit from EventTarget
and collect their own binding events through the unique nodeId
. Each built-in component on the view layer template will be bound to nodeId
and listen to all triggerable events. For example, a simple view tag will bind bindtap/bindtouchstart/bindtouchend and other events. When the event is triggered, the target node id is obtained through event.currentTarget.dataset.nodeId
, and then the corresponding function bound by the user on the node is triggered.
The main project process of Rax mini program runtime follows the design of Rax Web. The JS Bundle packaged by Webpack on the web side can be reused in the mini program runtime. We inject the window and document variables simulated by Decryption and thinking about Rax applet runtime solution into the bundle through the plug-in, generate a fixed mini program project skeleton, and load the JS Bundle in app.js. The overall project structure is shown in the figure below:
The above architecture is the result of gradual evolution. Initially, we used webpack's multi-entry mode to package the runtime applet code, that is, each page would be packaged independently as an entry. This makes the applet behave more like an MPA. The problem this brings is that the code that is commonly dependent between pages is not executed in the same memory, which does not match the performance of the native applet. This difference led to our final decision to change the project packaging model. The current version of the Rax runtime applet is more in line with the form of SPA, and all business codes are packaged into a JS file.
We have modified the link of the rax-app package of the Rax project entrance when the mini program is running. During initialization, it will return the render
function of each page according to the route. The render
function creates the root node (document.createElement
), mounts the corresponding Rax component to it, and appends the root node to the body node (document.body. appendChild
). During the onLoad life cycle of each page of the mini program, an independent document
will be created and set as a global variable, and then its corresponding render
function will be called to render each page independently.
从上面的小程序运行时原理来看,其性能相比原生是存在一定差距的,这主要由以下几个方面造成:第一:逻辑层运行完整的 Rax + 通过模拟 DOM/BOM API 处理 VDOM 并生成 setData 数据,需要消耗更多的计算时间;第二,相比原生小程序需要传递更多 setData 数据,如果容器层数据序列化能力较弱,会大大增加数据传输耗时;第三,视图层通过自定义组件递归动态生成视图,而我们知道递归动作本身就是一个性能损耗点。此外,由于无法预先知晓用户需要绑定的属性和事件,自定义组件模板中只能将所有属性和事件预先绑好,这导致小程序运行过程中会触发很多无用的事件,进一步加重负担。经过我们的 benchmark 计算,在支付宝小程序平台上,运行时小程序框架(包括 Rax/Taro/Remax 等)与原生小程序存在约 40% 的性能差距。
Rax 小程序运行时发布后,经测试其性能相比其他运行时框架存在着较为明显的差距,于是我们启动了性能调优的专项计划。通过以下方面的重构,成功将 Rax 小程序运行时小程序的性能拉升至业界领先水平,与 Taro/Remax 基本处于同一水平线。
更新数据精确化。在旧版本中,setData 的数据是全量更新的,虽然有 dom 子树分割批量更新的设计,但是数据传输仍然存在大量冗余。重构版本中,Rax 增加了节点渲染判断,未挂载节点无须触发更新;将所有更新收拢至顶层 root 节点统一批量处理, 并且通过精确计算数据更新的 path,实现局部更新。比如某次更新节点的 class 属性时,setData 的数据可能是:
{ "root.children.[0].children.[1].class": "active"}复制代码
内置小程序组件无需维护其属性列表,而是根据用户传参直接赋值。旧版本中,我们维护了所有内置组件的属性,在获取属性值的时候均需要调用 domNode.getAttribute,具有一定性能开销。重构版本 Rax 直接根据用户传参给属性赋值,并将默认值设置的操作移至视图层 WXS/SJS 中处理。
更新 Decryption and thinking about Rax applet runtime solution 中的数据结构。经过梳理,Rax 移除了冗余的 tree 数据,重写了 getaElementById 等 API;重构了 attribute、classList 等类;使用了更符合场景需要的 Map/Set 等数据结构,提升了整体的数据处理性能。
渲染模板优化。在支付宝小程序中,Rax 使用 template 进行递归调用;在微信中,Rax 使用 template 调用 element 再调用 template 的形式以避免微信端递归调用 template 的层数限制。在模板中,我们尽量使用 template is 语法进行判断,减少 a:if/wx:if 条件判断,提升模板递归时的性能。
无论是出于旧有业务的迁移,或者是出于性能考虑,Rax 小程序运行时中都存在着混合使用的需求。目前,Rax 已经打通与小程序内置组件、小程序自定义组件、小程序页面、小程序插件混合使用的能力。这其中,使用小程序自定义组件是最为复杂的。
在 Rax 中使用小程序自定义组件,其引入路径需要与 usingComponents
保持一致(例如 import CustomComp from '../components/CustomComp/index'
)。 在编译阶段,Rax 工程使用 Babel 插件进行代码扫描,检测到 JSX 中使用的某个组件是小程序自定义组件(根据其引入路径是否存在同名 axml 文件)时,会将其使用到的属性和事件进行缓存,然后通过 webpack 插件动态生成至递归模板中。在运行时中的创建节点阶段,通过查询缓存判断节点是否为自定义组件。若是自定义组件,则其渲染数据中会插入缓存中的属性,并且绑定事件至该自定义组件实例。
通过 Rax 小程序编译时方案产出的组件,从使用形态上来说,可以直接视为小程序自定义组件。而 Rax 工程加强了运行时与编译时的联系,当在 Rax 小程序运行时中使用编译时组件 npm 包时,用户无需引入组件的具体路径,只需像使用普通组件时一样引入,Rax 工程将自动根据该组件 package.json
中是否存在 miniappConfig
字段来判断其是否为一个 Rax 多端组件,然后直接使用其编译时的组件实现。
Rax is the only small program development solution in the industry that supports both compile-time and run-time engines. Its ability to mix dual engines can perfectly achieve a balance between performance and development efficiency. In the future, Rax will implement a more flexible dual-engine mixed use method, such as supporting the designation of a component to be compiled with the compile-time engine in a single project, providing higher flexibility for the business.
The above is the principle analysis of the Rax applet runtime solution. The run-time solution solves the syntax limitations inherent in the compile-time solution, but there are also obvious performance constraints. It can be said that at the current node of 2020, there is still no so-called silver bullet for mini program development. Perhaps the fusion of the Rax mini program dual engines will be an optimal solution within a relatively wide range. No one can tell how far mini programs that go against the standards can go. Developers will still have to face various problems for quite some time in the future. From the perspective of a small program development framework, I only hope that all developers can choose the most suitable framework for themselves and complete the development of small programs quickly and efficiently.
Related free learning recommendations: WeChat Mini Program Development Tutorial
The above is the detailed content of Decryption and thinking about Rax applet runtime solution. For more information, please follow other related articles on the PHP Chinese website!