ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue渲染器:打通开发编译渲染的最后一步

2026/8/13 19:25:17 拓冰建站 浏览量
Vue渲染器:打通开发编译渲染的最后一步

本节概述

经过前面章节的学习,我们已经完成了小程序的编译以及组件包的构建工作,这节我们将在前面的基础上,将编译后的产物,结合组件库完整渲染出小程序页面。

HTML根环境

首先我们需要在小程序webview根页面完成基本的初始化工作:

  • 引入相关的依赖包
  • rpx单位适配

依赖包引入

这里我们需要导入编译好的组件包代码和Vue框架代码; 目前我们把编译好的组件包也放在 public 目录下方便加载;

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script src="/components.js"></script>

rpx适配

小程序中特有的css单位 rpx,其中 750rpx 尺寸表示屏幕宽度,在前面的编译过程中,我们将这个特殊的单位转化为了 rem 单位进行适配。

rem 单位我们知道它是相对于根root节点的字号大小进行计算的,如 root 设置fontSize=16px,2rem 就表示 2 * root fontSize = 32px; 那么这里我们只需要让根fontSize 按照屏幕的尺寸分割成750份,每份的大小就是 1rem = 1rpx

// 适配 rpx 单位: rem
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const rootFontSize = viewportWidth / 750;
const rootTag = document.querySelector('html');
rootTag.style.fontSize = rootFontSize + 'px';

UI渲染引擎切换Vue渲染

在之前实现 UI 渲染引擎的时候,我们直接使用了一个render函数,传入data数据,返回HTML的形式进行页面渲染,现在小程序经过我们的编译,已经变成了Vue render函数。现在我们需要将渲染引擎中相关逻辑调整成Vue mount;

切换为Vue渲染主要有三个关注点:

  1. 在组件库实现的时候,组件事件的处理需要提供一个 bridge id 进行逻辑通信
  2. 按照编译时生成的scopeId设置Vue的scopeId,以保证渲染的节点能够正确匹配CSS样式
  3. 在特定生命周期触发小程序声明周期事件
// ui/src/runtimeManager/index.tsclass RuntimeManager {startRender() {const { pagePath, bridgeId } = opts;// 构造根Vue组件实例const vueOptions = this.makeVueOptions({ path: pagePath, bridgeId });this.pageId = bridgeId;// 使用vue挂载const root = document.querySelector('#root') as HTMLElement;this.page = new (window as any).Vue(vueOptions).$mount();root.appendChild(this.page.$el);}makeVueOptions(opts) {const { path, bridgeId } = opts;const pageModule = loader.getModuleByPath(path);const self = this;const { scopedId } = pageModule.moduleInfo;console.log('scopedId: ', scopedId);return {// 设置scopeId_scopeId: scopedId,data() {return {...pageModule.data};},beforeCreate() {// 注入bridge 信息(this as any)._bridgeInfo = {id: bridgeId,};},created() {self.uiInstance[bridgeId] = this;message.send({type: 'moduleCreated',body: {path,id: bridgeId,}});},mounted() {message.send({type: 'moduleMounted',body: {id: bridgeId}});},render: pageModule.moduleInfo.render,}}
}

逻辑层添加 WX 全局实例

开发过小程序的都知道,在小程序开发过程中有一个非常常用且重要的全局对象 wx,里面提供了如页面跳转,showToast 等常用的API,现在我们就在逻辑线程来添加一些这个对象.

这里我们只实现 wx 实例对象上的 navigateTonavigationBackshowToast 三个API,别的API大家感兴趣的话可以自行尝试实现,原理都是很类似的。

navigation 相关的API主要通过消息通知给native层,有native层进行小程序页面的管理创建。
showToast API则通过消息通知给Native再转发给ui层进行页面组件的弹出.(这里我们已经在组件实现的时候,给组件包添加了对应的showToast方法并挂载在ui线程的全局对象上)

/*** 全局 wx 实例*/
import message from '@/message';
import callback from '@/callback';
import navigation from '@/navigation';class Weixin {navigateTo(opts) {const { url, success } = opts;const successId = callback.saveCallback(success);message.send({type: 'triggerWXApi',body: {apiName: 'navigateTo',params: {url,success: successId}}});}navigateBack(opts) {message.send({type: 'triggerWXApi',body: {apiName: 'navigateBack',params: {},},});}showToast(opts) {const currentPageInfo = navigation.getCurrentPageInfo();message.send({type: 'showToast',body: {bridgeId: currentPageInfo.bridgeId,params: opts,}})}
}export default new Weixin();

针对navigation相关的API,我们在Native层小程序的miniApp实例进行统一处理,这里我们以调整小程序页面为例,页面返回逻辑类似,大家后面可前往本节代码仓库查看.

首先我们需要在miniApp实例中监听来自jscore线程内的消息,跳转页面的过程主要是需要新建一个bridge 和对应的webview实例,jscore 则无需创建和初始化,因为一个小程序的逻辑线程是复用的,已经在初始化打开小程序的时候初始化完了。

创建完对应的实例之后,需要将当前页面推出(实际就是隐藏起来),然后再将新的页面推入(设置下页面的层级zIndex)

// src/native/miniApp/index.ts
class MiniApp {constructor(opts) {this.jscore = new JSCore();// 注册jscore消息监听,处理小程序全局事件this.jscore.addEventListener('message', this.jscoreMessageHandler.bind(this));}jscoreMessageHandler(msg: IMessage) {const { type, body } = msg;if (type !== 'triggerWXApi') {return;}const { apiName, params } = body;this[apiName]?.(params);}// 通知回logic侧触发回调createCallback(callbackId: string) {const self = this;return function(...args: any) {self.jscore.postMessage({type: 'triggerCallback',body: {callbackId,args}})}}navigateTo(params: NavigateToParams) {// 获取跳转页面的参数const { url, success } = params;const { pagePath, query } = queryPath(url);// 将回调方法进行封装,在触发时通过消息通知给logic层的callback处理器const successCallback = success ? this.createCallback(success) : undefined;this.openPage({pagePath: pagePath.replace(/^\//g, ''),query,onSuccess: successCallback});}async openPage(opts: OpenPageParams) {if (!this.webviewAnimaEnd) {return;}this.webviewAnimaEnd = false;const { pagePath, query, onSuccess } = opts;// 创建新的bridgeconst pageConfig = this.appConfig!.modules[pagePath];const bridge = await this.createBridge({pagePath,query,scene: this.app.scene,jscore: this.jscore,isRoot: false,appId: this.app.appId,pages: this.appConfig!.app.pages,configInfo: mergePageConfig(this.appConfig!.app, pageConfig),});// 获取前一个bridge,以及其webviewconst preBridge = this.bridgeList[this.bridgeList.length - 1];const preWebview = preBridge.webview!;this.bridgeList.push(bridge);this.bridges[bridge.id] = bridge;// 触发bridge的初始化逻辑,此时不需要在初始化 jscorebridge.start(false);bridge.webview!.el.style.zIndex = `${this.bridgeList.length + 1}`;bridge.webview?.el.classList.add('wx-native-view--before-enter');await sleep(20);// 上一个页面推出preWebview.el.classList.remove('wx-native-view--instage');preWebview.el.classList.add('wx-native-view--linear-anima');preWebview.el.classList.add('wx-native-view--slide-out');preBridge.pageHide?.();// 新页面推入bridge.webview!.el.classList.add('wx-native-view--instage');bridge.webview!.el.classList.add('wx-native-view--enter-anima');await sleep(540);// 移除相关动画this.webviewAnimaEnd = true;preWebview.el.classList.remove('wx-native-view--linear-anima');bridge.webview!.el.classList.remove('wx-native-view--before-enter');bridge.webview!.el.classList.remove('wx-native-view--enter-anima');bridge.webview!.el.classList.add('wx-native-view--instage');onSuccess && onSuccess();}
}

showToast UI页面显示类API,需要通过Native将消息传递给UI层触发页面相关组件的渲染。通过bridge来传递showToast通信消息:

// src/native/bridge/index.ts
jscoreMessageHandler(message: IMessage) {switch (type) {// ...case 'showToast':this.showToast(body);break;}
}
showToast(payload) {const { params } = payload;this.webview?.postMessage({type: 'showToast',body: {...params,}})}

在ui线程处理showToast消息:

// ui/src/messageManager/index.ts
// 调用组件包注册在全局对象上的相关API
showToast(msg) {window.wxComponentsApi.showToast(msg);
}

目前为止,我们基础的小程序渲染就实现完成了,文章中只摘取出了主要部分逻辑进行介绍实现,本小节完整代码已上传至Github: mini-wx-app