逻辑引擎:执行小程序JS代码

本节概述

上小节我们完成了基础的小程序双线程架构的搭建,我们通过 Worker 启动了一个逻辑线程用于执行小程序JS代码,使用 iFrame 创建了一个UI线程,用于渲染小程序的页面;

这节开始我们将分别针对双线程的具体逻辑进行实现,本节我们先从逻辑线程入手,看看JS代码是如何在 Worker 线程内运行的;

开始之前,我们先通过一个图来看看小程序启动过程,双线程之间的通信流程:

逻辑引擎:执行小程序JS代码_第1张图片

从图上可以看到,小程序的启动分别由 逻辑线程UI线程 创建对应的实例,然后通过 逻辑线程 获取小程序的 data 数据发送给 UI 线程进行页面渲染;

现在我们根据这个流程来实现 逻辑线程 侧基础的引擎管理能力,不过开始之前,我们先来构造一个小程序页面的逻辑线程侧代码,方便后续的验证:

/**
 * 我们开发小程序过程中,存在一个 app.js 和 每个页面的 page js 文件
 * app.js 整个小程序只有一个,作用于整个小程序应用
 * page js 每个小程序页面都存在一个,用于管理小程序页面的逻辑和数据
 * 
 * 小程序经过编译后会变成一个js文件,这里面每个js都会以modDefine 模块的形式进行注册管理
 * 模块注册的逻辑会由编译器进行完成,这块我们会在后续的章节中来讲述如何构建一个小程序编译器
 */
modDefine('pages/home/index', function() {
  Page({
    data: {
      text: '首页',
      number: 10
    },
    onReady() {
      console.log('页面Ready')
    },
    tapHandler() {
      this.setData({
        number: this.number + 10
      })
    }
  }, {
    path: 'pages/home/index'
  });
});
modDefine('app', function () {
  App({
    onLaunch() {
      console.log('app onLaunch')
    },
    globalData: 'this is global data'
  });
});

环境准备

在开始实现逻辑线程引擎代码之前,我们先来构建一下引擎逻辑的代码环境; 这里将通过 pnpmworkspace 模式进行多包的管理;

首先我们来创建一个 pnpm-workspace.yaml 文件:

packages:
  - packages/*

现在我们创建一个 packages 目录并在下面创建一个 logic 目录进行逻辑引擎代码的编写;

引擎逻辑的打包我们继续使用 vite:

import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  build: {
    outDir: path.resolve(__dirname, './build'),
    rollupOptions: {
      input: {
        logic: path.resolve(__dirname, './src/index.ts')
      },
      output: {
        entryFileNames: 'core.js'
      }
    }
  },
  resolve: {
    extensions: ['.js', '.ts'],
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
});

逻辑引擎全局环境

从上面构建的小程序编译后的逻辑JS代码可以分析得出,在逻辑环境执行时,全局环境只需要需要提供以下能力:

  • modDefine 相关的模块注册管理能力
  • Page, App 全局API
模块管理

小程序的每个逻辑js文件最终都被编译成 modDefine 的形式然后在引擎环境下注册成为一个个模块,模块内的依赖导入逻辑也可以依赖模块管理进行实现;

// globalApi/amd.ts
const defineCache: Record<string, any> = {};  // 缓存已定义的模块
const requireCache: Record<string, any> = {}; // 缓存已加载的模块
const loadingModules: Record<string, any> = {};

// 模块注册函数
export function modDefine(id: string, factory: any) {
  if (!defineCache[id]) {
    const modules = {
      id: id,
      dependencies: [],
      factory: factory
    };
    defineCache[id] = modules;
  }
}

// 模块加载函数
export function modRequire(id: string) {
  if (loadingModules[id]) {
    return {}; // 直接返回,尽管它可能还没有完全被加载
  }

  if (!requireCache[id]) {
    const mod = defineCache[id];
    if (!mod) throw new Error("No module defined with id " + id);

    const modules = {
      exports: {}
    };
    loadingModules[id] = true;
    const factoryArgs = [modRequire, modules.exports, modules];
    
    mod.factory.apply(null, factoryArgs);
    requireCache[id] = modules.exports;
    delete loadingModules[id];
  }

  return requireCache[id].exports;
}
App 和 Page 函数

App 和 Page 函数主要是创建小程序配置和页面配置逻辑,实际调用这两个函数并不会立即创建运行时实例,只是将配置的对象信息保存为 AppModulePageModule,然后在实际渲染页面的时候,再将对应页面的配置信息创建运行时实例

// loader/appModule.ts
import type { AppModuleInfo } from '@/types/common';

/**
 * App 模块: 小程序的 app.js 文件执行时,会创建一个 AppModule 实例。
 * 
 * App({
 *  onLaunch: (options) => {
 *    console.log('App onLaunch', options);
 *  },
 *  onShow: (options) => {
 *    console.log('App onShow', options);
 *  },
 * });
 */
export class AppModule {
  type: string = 'app';
  /**
   * App 模块配置信息
   */
  moduleInfo: AppModuleInfo;

  constructor(moduleInfo: AppModuleInfo) {
    this.moduleInfo = moduleInfo;
  }
}
// loader/pageModule.ts
import type { PageModuleInfo, PageModuleCompileInfo } from '@/types/common';

/**
 * Page 页面模块, 小程序的每个页面逻辑代码都会创建一个对应的 PageModule 实例
 * 
 * Page({
 *  data: {
 *    text: 'test'
 *  },
 *  onReady() {
 *    console.log('Page onReady');
 *  },
 *  onShow() {
 *    console.log('Page onShow');
 *  },
 * }, {
 *  path: 'pages/home/index'  // 这个编译配置参数会在编译小程序阶段由编译器注入
 * })
 */
export class PageModule {
  type: string = 'page';
  /**
   * Page 模块配置信息
   */
  moduleInfo: PageModuleInfo;
  /**
   * page 模块编译信息
   */
  compileInfo: PageModuleCompileInfo;

  constructor(moduleInfo: PageModuleInfo, compileInfo: PageModuleCompileInfo) {
    this.moduleInfo = moduleInfo;
    this.compileInfo = compileInfo; // 这部分信息由编译器注册,如页面path等
  }

  getInitialData() {
    const moduleData = this.moduleInfo.data || {};
    
    return {
      ...moduleData,
    }
  }
}

有了这两个类定义后,我们现在来创建一个 Loader 类,用于管理小程序逻辑代码的加载执行,创建对应的Module等能力

// loader/index.ts
/**
 * 逻辑线程资源加载器,主要用于管理各个小程序模块的一些配置信息
 * 
 * staticModules: 存储小程序App逻辑代码和页面模块配置数据
 * 
 * - loadResources(opts): void 加载小程序资源
 * - createAppModule(moduleInfo: AppModuleInfo): void 创建小程序App模块
 * - createPageModule(moduleInfo: PageModuleInfo, compileInfo: PageModuleCompileInfo): void 创建小程序页面模块
 * - getInitialDataByPagePath(path: string): any 根据页面路径获取对应页面初始数据
 */
import { AppModule } from './appModule';
import { PageModule } from './pageModule';
import type { AppModuleInfo, PageModuleInfo, PageModuleCompileInfo } from '@/types/common';

interface LoaderResourceOpts {
  appId: string;
  pages: string[];
}

class Loader {
  /** 存储逻辑页面定义的App模块和页面模块信息 */
  staticModules: Record<string, AppModule | PageModule> = {};

  /**
   * 加载小程序逻辑 js 资源并执行
   */
  loadResources(opts: LoaderResourceOpts) {
    const { appId, pages } = opts;
    // 拼接模版资源loader路径
    const logicResourcePath = `${appId}/logic.js`;
    globalThis.importScripts(logicResourcePath);
    globalThis.modRequire('app'); // 加载小程序App模块
    pages.forEach(pathPath => {
      globalThis.modRequire(pathPath); // 加载小程序页面模块
    });
  }

  /**
   * 创建小程序 AppModule
   */
  createAppModule(moduleInfo: AppModuleInfo) {
    const appModule = new AppModule(moduleInfo);
    this.staticModules['app'] = appModule;
  }

  /**
   * 创建小程序 PageModule
   */ 
  createPageModule(moduleInfo: PageModuleInfo, compileInfo: PageModuleCompileInfo) {
    const pageModule = new PageModule(moduleInfo, compileInfo);
    const { path } = compileInfo;
    this.staticModules[path] = pageModule;
  }

  /**
   * 根据页面 path 获取当前页面的逻辑数据 data
   */
  getInitialDataByPagePath(path: string) {
    const pageModule = this.staticModules[path] as PageModule;
    return {
      [path]: pageModule.getInitialData(),
    };
  }

  getModuleByPath(path: string) {
    return this.staticModules[path];
  }
}

export default new Loader(); 

现在我们把准备好的这些内容注册到全局对象上,方便小程序逻辑代码加载执行时直接触发调用

// globalApi/index.ts
import { modDefine, modRequire } from './amd';
import loader from '@/loader';
import type { AppModuleInfo, PageModuleInfo, PageModuleCompileInfo } from '@/types/common';

class GlobalApi {
  init() {
    globalThis.App = (moduleInfo: AppModuleInfo) => {
      loader.createAppModule(moduleInfo);
    }

    globalThis.Page = (moduleInfo: PageModuleInfo, compileInfo: PageModuleCompileInfo) => {
      loader.createPageModule(moduleInfo, compileInfo);
    }

    globalThis.modDefine = modDefine;
    globalThis.modRequire = modRequire; 
  }
}

export default new GlobalApi(); 

逻辑线程线程监听native消息

我们先来创建一个 Message 类用来统一监听 native 层发送来的消息并进行统一的派发

/**
 * 逻辑线程引擎的通信模块
 * 
 * event: 事件对象
 * 
 * - init(): void; 初始化消息类
 * - receive(messageType, handler): void 接收原生层消息
 * - send(data): void 发送消息到原生层
 */
import mitt, { Emitter } from 'mitt';
import type { IMessage } from '@/types/common';

export class Messgae {
  event: Emitter<Record<string, any>>;
  constructor() {
    this.event = mitt<Record<string, any>>();
    this.init();
  }

  init() {
    globalThis.addEventListener('message', (e) => {
      /**
       * {
       *  type: string, 消息类型
       *  body: any, 消息体
       * }
       */
      const msg = e.data;
      const { type, body } = msg;
      this.event.emit(type, body);
    });
  }

  receive(type: string, callback: (data: any) => void) {
    this.event.on(type, callback);
  }

  send(message: IMessage) {
    globalThis.postMessage(message);
  }
}

export default new Messgae();

创建 MessageManager 管理类,分别对不同的消息类型进行处理,主要包括:

  • loadResource 加载小程序逻辑资源
  • createApp 创建App实例
  • appShow 触发App show LifeCycle
  • pageShow 触发 Page show LifeCycle
  • makePageInitialData 获取小程序初始化页面data
  • createInstance 创建小程序页面实例
  • triggerEvent UI页面触发逻辑
  • triggerCallback 回调函数触发
/**
 * MessageManager class
 * 小程序逻辑引擎监听原生层消息通知
 * 
 * - message: Message 通信对象
 * - init(): void 消息监听注册
 */
import message, { type Messgae } from '@/message';
import loader from '@/loader';
import runtimeManager from '@/runtimeManager';
import callback from '@/callback';

class messageManager {
  message: Messgae;

  constructor() {
    this.message = message;
  }

  init() {
    this.message.receive('loadResource', this.loadResource.bind(this));
    this.message.receive('createApp', this.createApp.bind(this));
    this.message.receive('appShow', this.appShow.bind(this));
    this.message.receive('pageShow', this.pageShow.bind(this));
    this.message.receive('makePageInitialData', this.makePageInitialData.bind(this));
    this.message.receive('createInstance', this.createPage.bind(this));
    
    this.message.receive('triggerEvent', this.triggerEvent.bind(this));
    this.message.receive('triggerCallback', this.triggerCallback.bind(this));
  }

  private loadResource(data) {
    // native 层通知加载小程序逻辑代码
    const { appId, bridgeId, pages } = data;
    loader.loadResources({
      appId,
      pages,
    });
    this.message.send({
      type: 'logicResourceLoaded',
      body: {
        bridgeId,  // 带上bridgeId
      }
    });
  }
  
  private createApp(data) {
    const { bridgeId, scene, pagePath, query } = data;
    // 创建小程序 App 运行实例
    runtimeManager.createApp({
      scene,
      pagePath,
      query,
    });
    // 发送消息给原生层,告知app创建完毕
    this.message.send({
      type: 'appIsCreated',
      body: {
        bridgeId
      }
    })
  }

  private appShow() {
    // 触发小程序app 实例 show 事件
    runtimeManager.appShow();
  }

  private pageShow(data) {
    const { bridgeId } = data;
    runtimeManager.pageShow({ id: bridgeId });
  }

  // 创建小程序页面实例
  private createPage(data) {
    runtimeManager.createPage(data);
  }

  // 获取小程序页面初始化data数据,用于初始渲染页面
  private makePageInitialData(data) {
    const { bridgeId, pagePath } = data;
    const initialData = loader.getInitialDataByPagePath(pagePath);
    this.message.send({
      type: 'initialDataReady',
      body: {
        bridgeId,
        initialData,
      }
    });
  }
  
  // 页面出发js逻辑事件
  private triggerEvent(data) {
    runtimeManager.triggerEvent(data);
  }

  // native 层出发逻辑引擎回调
  // 这里主要是因为message消息通信没法传递函数,回调函数的触发通过一个id来触发
  private triggerCallback(data) {
    const { callbackId, args } = data;
    callback.triggerCallback(callbackId, args);
  }
}

export default new messageManager();

这里针对小程序 app实例的创建管理,page实例的创建管理我们时放到了 RuntimeManager 类进行,这个类将会根据小程序调用 AppPage Api注册的模块信息,来分别创建其运行时实例;

小程序运行时整个小程序只有一个 App 运行时实例和一个或多个 Page 运行时实例,这两个运行时实例分别会管理小程序的生命周期调用和逻辑函数的触发,以及 Page 实例上最重要的 setData 函数,现在我们分别来实现 App 的运行时类和 Page 运行时类:

App 运行时

// runtimeManager/app.ts
import { isFunction } from 'lodash';
import type { AppModuleInfo, AppOpenInfo } from "@/types/common";

/**
 * 小程序 App 运行时实例,一个小程序只会存在一个 App 实例
 */
const LifecycleMethods = ['onLaunch', 'onShow', 'onHide'];
export class App {
  /**
   * 应用注册信息
   */
  moduleInfo: AppModuleInfo;
  /**
   * 页面打开信息,如 scene 场景值,打开的页面路径和query参数等
   */
  openInfo: AppOpenInfo;
  constructor(moduleInfo: AppModuleInfo, openInfo: AppOpenInfo) {
    this.moduleInfo = moduleInfo;
    this.openInfo = openInfo;
    this.init();
  } 

  /**
   * 初始化App
   */
  init() {
    this.initLifecycle();
    // 触发launch周期和show周期函数
    (this as any).onLaunch?.(this.openInfo);
    (this as any).onShow?.(this.openInfo);
  }

  /**
   * 初始化app生命周期
   */
  initLifecycle() {
    LifecycleMethods.forEach(name => {
      const lifecycle = this.moduleInfo[name];
      if (!isFunction(lifecycle)) {
        return;
      }
      this[name] = lifecycle.bind(this);
    })
  }

  /**
   * 触发onShow生命周期
   */
  callShowLifecycle() {
    const { pagePath, query, scene } = this.openInfo;
    const options = {
      scene,
      query,
      path: pagePath,
    };
    (this as any).onShow?.(options); 
  }
}

Page 运行时

import { cloneDeep, isFunction, set } from 'lodash';
import { PageModule } from "@/loader/pageModule";
import message from '@/message';

/**
 * 小程序页面运行时实例,对应一个小程序页面,管理小程序页面的生命周期,setData 响应式数据更新等逻辑的处理
 */
const LifecycleMethods = ['onLoad', 'onShow', 'onReady', 'onHide', 'onUnload', 'onPageScroll'];
export class Page {
  /**
   * 页面模块
   */
  pageModule: PageModule;
  // 扩展参数
  extraOption: Record<string, any>;
  // 对应ui线程的页面id - webview id
  id: string;
  /**
   * 页面数据
   */
  data: Record<string, any>;

  constructor(pageModule: PageModule, extraOption: Record<string, any>) {
    this.pageModule = pageModule;
    this.extraOption = extraOption;
    this.id = extraOption.id;
    this.data = cloneDeep(pageModule.moduleInfo.data || {});
    this.initLifecycle();
    this.initMethods();
    // 触发onload生命周期
    (this as any).onLoad?.(this.extraOption.query || {});
    (this as any).onShow?.();
  }

  /**
   * 初始化page生命周期
   */
  initLifecycle() {
    LifecycleMethods.forEach(name => {
      const lifecycle = this.pageModule.moduleInfo[name];
      if (!isFunction(lifecycle)) {
        return; 
      }
      this[name] = lifecycle.bind(this);
    })
  }

  /**
   * 初始化用户的自定义函数
   */
  initMethods() {
    const moduleInfo = this.pageModule.moduleInfo;
    for (let attr in moduleInfo) {
      if (isFunction(moduleInfo[attr]) && !LifecycleMethods.includes(attr)) {
        this[attr] = moduleInfo[attr].bind(this);
      }
    }
  }

  /**
   * 页面响应式数据更新
   * 数据更新后。发送消息给 bridge 层通知Ui重新渲染
   */
  setData(data: Record<string, any>) {
    for (let key in data) {
      set(this.data, key, data[key]);
    }
    // 发送最新的数据到渲染线程
    message.send({
      type: 'updateModule',
      body: {
        id: this.id,
        data: this.data,
        bridgeId: this.id,
      }
    })
  }
}

有了运行时模块类之后,我们来创建 RuntimeManager 类:

/**
 * runtimeManager class
 * 运行管理器
 * 
 * - app: App 小程序实例
 * - pages: Pages页面实例
 * 
 * - createApp(): void 创建App实例
 * - appHide(): void 调用应用 onHide生命周期函数
 * - appShow(): void 调用应用 onShow生命周期函数
 */
import loader from '@/loader';
import { App } from './app';
import { Page } from './page';
import { PageModule } from '@/loader/pageModule';
import navigation from '@/navigation';

interface RuntimManagerCreateAppOpts {
  scene: number;
  pagePath: string;
  query?: Record<string, any>;
}
export interface CreatePageOpts {
  path: string;
  id: string;
  bridgeId: string;
  query?: Record<string, any>;
}
class RuntimManager {
  /**
   * App实例
   */
  app: App | null = null;
  /**
   * 对应渲染线程的页面实例
   */
  pages: Record<string, Page> = {}

  createApp(opts: RuntimManagerCreateAppOpts) {
    const { scene, pagePath, query } = opts;
    const appModuleInfo = loader.staticModules.app.moduleInfo;
    if (this.app) {
      return;
    }

    this.app = new App(appModuleInfo, {
      scene,
      pagePath,
      query,
    })
  }

  // 创建页面运行时实例
  createPage(opts: CreatePageOpts) {
    const { id, path, query, bridgeId } = opts;
    const pageModule = loader.getModuleByPath(path) as PageModule;
    // 这里维护一个页面导航的页面栈
    navigation.pushState({
      bridgeId,
      query,
      pagePath: path,
    });
    this.pages[id] = new Page(pageModule, opts);
  }

  appShow() {
    this.app?.callShowLifecycle();
  }

  pageShow(opts) {
    const { id } = opts;
    const page = this.pages[id];
    (page as any)?.onShow?.();
  }

  appHide() {
    this.app?.moduleInfo.onHide?.();
  }

  pageHide(opts) {
    const { id } = opts;
    const page = this.pages[id];
    (page as any)?.onHide?.();
  }

  pageReady(opts) {
    const { id } = opts;
    const page = this.pages[id];
    (page as any)?.onReady?.();
  }

  pageUnload(opts) {
    const { id } = opts;
    const page = this.pages[id];
    navigation.popState();
    (page as any)?.onUnload?.();
    // 销毁当前页面实例
    delete this.pages[id];
  }

  // 调用小程序自定义逻辑方法: 主要是页面交互触发逻辑事件
  triggerEvent(opts) {
    const { id, methodName, paramsList } = opts;
    const page = this.pages[id];
    (page as any)?.[methodName]?.(...paramsList);
  }
}

export default new RuntimManager();

MessageManager 管理类中,还有一个 triggerCallback 的消息类型,主要是当逻辑线程和 bridge 侧进行通信的时候,有时候需要带一些回调函数的时候用的,这是因为在线程间通信的时候,没法传递函数类型的参数,所以我们通过一个 Callback 管理类进行处理,将回调函数注册进去拿到一个回调函数的ID,然后发送给 bridge 侧,bridge 侧在需要触发回调的时候,在将回调函数的ID 和 触发的参数传递给 logic 层,由逻辑层来执行回调函数的触发工作;

这里我们来实现一下 Callback 管理类:

/**
 * 处理线程间函数无法传递的问题,把函数包装成一个对象,通过一个id标识,回调时通过id匹配执行
 */
import { uuid } from '@/utils';

type AnyFunc = (...args: any[]) => any;
class Callback {
  callback: Record<string, AnyFunc> = {};

  saveCallback(callback: AnyFunc) {
    const functionId = `function_id:${uuid()}`;
    this.callback[functionId] = callback;
    return functionId;
  }

  triggerCallback(callbackId, args) {
    const callbackFucntion = this.callback[callbackId];
    if (callbackFucntion) {
      callbackFucntion(...args);
      delete this.callback[callbackId];
    }
  }
}

export default new Callback();

统一注册全局API 和 初始化消息监听

准备好上述的全局API和消息监听管理器后,现在我们需要在逻辑引擎的入口处来初始化一下,这样在逻辑线程启动时,通过加载逻辑引擎就可以完成线程的初始化工作:

// index.ts
import messageManager from './messageManager';
import globalApi from './globalApi';

globalApi.init();
messageManager.init();

至此我们的逻辑线程引擎部分代码就基本实现完啦,下一节我们将会实现UI侧的引擎代码,会和逻辑引擎侧进行对应;

本小节代码已同步至github 仓库,可前往查看完整逻辑: mini-wx-app

你可能感兴趣的:(从零搭建小程序框架架构,小程序,前端,架构)