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

本节概述

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

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渲染主要有三个关注点:

  1. 在组件库实现的时候,组件事件的处理需要提供一个 bridge id 进行逻辑通信
  2. 按照编译时生成的scopeId设置Vue的scopeId,以保证渲染的节点能够正确匹配CSS样式
  3. 在特定生命周期触发小程序声明周期事件
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 实例对象上的 navigateTonavigationBackshowToast 三个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

相关推荐
gzzeason9 分钟前
在HTML中CSS三种使用方式
前端·css·html
hnlucky22 分钟前
《Nginx + 双Tomcat实战:域名解析、静态服务与反向代理、负载均衡全指南》
java·linux·服务器·前端·nginx·tomcat·web
huihuihuanhuan.xin24 分钟前
前端八股-promise
前端·javascript
星语卿1 小时前
浏览器重绘与重排
前端·浏览器
小庞在加油1 小时前
Apollo源码架构解析---附C++代码设计示例
开发语言·c++·架构·自动驾驶·apollo
小小小小宇1 小时前
前端实现合并两个已排序链表
前端
森焱森1 小时前
60 美元玩转 Li-Fi —— 开源 OpenVLC 平台入门(附 BeagleBone Black 驱动简单解析)
c语言·单片机·算法·架构·开源
yngsqq1 小时前
netdxf—— CAD c#二次开发之(netDxf 处理 DXF 文件)
java·前端·c#
mrsk1 小时前
🧙‍♂️ CSS中的结界术:BFC如何拯救你的布局混乱?
前端·css·面试
jonssonyan1 小时前
我自建服务器部署了 Next.js 全栈项目
前端