本节概述
经过前面章节的学习,我们已经完成了小程序的编译以及组件包的构建工作,这节我们将在前面的基础上,将编译后的产物,结合组件库完整渲染出小程序页面。
HTML根环境
首先我们需要在小程序webview根页面完成基本的初始化工作:
- 引入相关的依赖包
- rpx单位适配
依赖包引入
这里我们需要导入编译好的组件包代码和Vue框架代码; 目前我们把编译好的组件包也放在 public
目录下方便加载;
html
<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
ts
// 适配 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渲染主要有三个关注点:
- 在组件库实现的时候,组件事件的处理需要提供一个
bridge id
进行逻辑通信 - 按照编译时生成的scopeId设置Vue的scopeId,以保证渲染的节点能够正确匹配CSS样式
- 在特定生命周期触发小程序声明周期事件
ts
// ui/src/runtimeManager/index.ts
class 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
实例对象上的 navigateTo
,navigationBack
,showToast
三个API,别的API大家感兴趣的话可以自行尝试实现,原理都是很类似的。
navigation 相关的API主要通过消息通知给native层,有native层进行小程序页面的管理创建。 showToast API则通过消息通知给Native再转发给ui层进行页面组件的弹出.(这里我们已经在组件实现的时候,给组件包添加了对应的showToast方法并挂载在ui线程的全局对象上)
ts
/**
* 全局 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)
ts
// 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;
// 创建新的bridge
const 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,以及其webview
const preBridge = this.bridgeList[this.bridgeList.length - 1];
const preWebview = preBridge.webview!;
this.bridgeList.push(bridge);
this.bridges[bridge.id] = bridge;
// 触发bridge的初始化逻辑,此时不需要在初始化 jscore
bridge.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通信消息:
ts
// 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消息:
ts
// ui/src/messageManager/index.ts
// 调用组件包注册在全局对象上的相关API
showToast(msg) {
window.wxComponentsApi.showToast(msg);
}
目前为止,我们基础的小程序渲染就实现完成了,文章中只摘取出了主要部分逻辑进行介绍实现,本小节完整代码已上传至Github: mini-wx-app