# 子应用注册

记录子应用基本信息

// 批量,可以按全局 filter 进行过滤、去重,常用
const modules = JModule.registerModules([
    {
        key: 'app1',
        url: 'http://localhost:8080/index.js',
        resourceType: 'auto', // 可选
        resourceLoadStrategy: ResourceLoadStrategy.Fetch, // 可选,默认为 Element
    },
    { key: 'app2', url: '...' }
]);
// 单用,不受 filter 配置的影响,也不会去重;
const module = new JModule({ key: 'app1', url: 'http://localhost:8080/index.js' });

# 入口资源(url参数)类型

url 所指向的资源可以是一个 js 文件、json 文件等, 具体来说默认支持以下几种类型:

  1. js

    资源指向一个 javascript 文件, 该文件加载后会执行 Resource.setResourceData({ js: [], css: [] }, resourceUrl)

    通过 @jmodule/plugin-webpack 自动生成的 index.js 文件即为此类. 通过 script 元素进行加载.

  2. json

    资源指向一个 javascript 文件, 该文件包含了 { js: [], css: [] }资源清单信息, 即 js 类型资源的第一个参数. 通过 fetch 方式进行加载.

  3. html

    资源指向一个 html 文件, JModule将自动从其中解析出 script/link/style 元素, 并生成子应用的资源清单. 通过 fetch 方式加载.

    v4.7.0 及以上版本默认支持该类型, 4.7.0以下(不含)版本以下需要通过@jmodule/helper 提供支持, 具体参考@jmodule/helper

    如果刚升级到4.7.0以上版本, 且使用了 Resource.defineType('html', xxx), 请将其删除

  4. auto

    该类型将统一通过fetch方式加载入口资源, 并通过响应头的 Content-Type 自动判定资源类型. 判定完继续通过以上类型处理.

TIP

resourceType 的判定:

  1. 注册JModule实例时指定的 resourceType 参数优先级最高.
  2. 如果 url path 以 '.js'/'.json' 结尾, 则自动判定为 js/json
  3. 此外默认为 auto.

更多类型可以通过 Resource.defineType 进行扩展. 具体参考 @jmodule/client 的接口文档

# 加载策略

可以指定加载子应用清单中的资源时所使用的方式, 有两种:

  1. ResourceLoadStrategy.Element (默认)

    通过 script/link 元素进行资源加载, 该方式不支持对加载的资源进行细粒度的控制, 但对于跨域资源更为友好, 对代码侵入性也更小.

  2. ResourceLoadStrategy.Fetch

    通过 fetch 方式进行加载, 该方式可以通过 Resouece 的 hook 对加载的资源进行细粒度的控制

# 子应用加载

module.load();

# 子应用挂载

// 仅示例,请与子应用约定格式与使用方式
JModule.addHook('afterInit', (module, pkg) => {
    if (module.type === 'app') {
        // 仅示例,请与子应用约定格式与使用方式
        Object.assign(module.metadata, pkg);
        router.addRoute(createModuleRouter(module));
    }
    if (module.type === 'module') {
        // 仅示例,请与子应用约定格式与使用方式
        const { route, store: storeConfig } = pkg;
        store.registerModule(module.key, storeConfig);
        router.addRoute(createModuleRouter(module, route));
    }
    return [module, pkg];
});

# 子应用激活

JModule 不内置激活规则,可以自行定义,比如定义路由规则: /:moduleKey/:subpath, 对于约定的 bootstrap/mount 的执行,也由宿主应用自行控制。

# 组件共享

JModule.export({
    $node_modules: Vue,
});
JModule.export({
    $node_modules: Vue,
}, { scope: 'someScope' });

使用参考 使用宿主应用导出的组件

# 获取组件

const modules = JModule.getRegisteredModules();

// 同步
const module = JModule.getModule('moduleKey');

// 异步
const module = await JModule.getModuleAsync('moduleKey', 10000);

# 高级功能

# 资源预加载

为优化子应用加载速度,通常会需要对暂时没有使用的子应用进行资源预加载。但需要注意,预加载并非无副作用,可以参考以下说明。

// 预加载
module.load(
    'preload', // 'init'|'preload'|'load',多次执行 load 不会导致重复加载
    {
        elementModifier: (element: HTMLElement) => void, // 对于加载资源的节点进行修改
        autoApplyStyle: true, // 自动应用样式,仅对 load 有效
    },
);

WARNING

出于性能优化的考虑,对预加载的调用,建议在主要页面加载完成之后执行,尤其对于大型应用。

  1. 对于 index.js/index.json 这种入口资源的请求,在加载完之后是立即解析执行的,面对大量入口文件解析执行时,容易导致较明显的主线程占用;
  2. 网络请求本身并不是一个足够轻量的操作,会占用系统资源,对于大量请求会产生较大的资源开销。

TIP

使用时请考虑以下建议:

  1. 在主要内容加载完后执行预加载操作;
  2. 减少没有意义的预加载,比如,仅对当前页面存在的子应用入口的应用进行预加载;

# 监听子应用状态

window.addEventListener('module.afterRegister', resolverListener);
window.addEventListener(`module.${module.key}.statusChange`, eventData);
window.addEventListener(`module.${module.key}.${module.status}`, eventData);

# Hook

Hook 是Jmodule 提供功能扩展的核心,你可以基于它来拦截、修改子应用从定义到加载完成过程中的各种行为来实现某些定制功能,比如自定义子应用类型、子应用运行沙箱等。

JModule、Resource、JModuleManager 均继承自 ModuleHook。宿主应用可以通过这些Hook来对子应用进行细粒度的控制。

Hook 具有以下特点:

  1. 区分异步、同步 Hook, 参考以下文档说明
  2. 链式调用,先定义先执行,每个 hook 的返回值为下一个 hook 的执行参数,因此返回值必须为数组。

# JModule#afterInit

  • 类型:异步
  • 作用时期:子应用的 JModule.define 的执行过程
  • 参数:
    • module: JModule
    • options: ModuleMetadata, JModule.define 的参数
  • 场景:通常用于扩展子应用类型、定制子应用初始化协议。

eg:

// module 为子应用实例
// pkg 为子应用声明的 JModule.define 的参数
JModule.addHook('afterInit', (module: JModule, options) => {
    return [module, options];
});

JModule.define 执行过程中还有其它 hook,均为异步hook,且拥有相同参数:

  1. JModule#beforeDefine

    在正常读取到 module 实例后,实际执行 JModule.define 的逻辑之前执行

  2. JModule#afterInit

    在 JModule.define 执行过程中,init 函数执行完成后执行

  3. JModule#afterImports

    在 JModule.define 执行过程中,imports 字段解析完成后执行

  4. JModule#afterExports

    在 JModule.define 执行过程中,exports 字段解析完成后执行

  5. JModule#afterDefine

    在 JModule.define 执行后执行,JModule 中的 defineType 就是基于这个 hook 实现的,defineType 中的函数也将在该过程中被执行。

# JModule#beforeFilterModules

  • 类型:异步
  • 作用时期:宿主应用的 JModule.registerModules 的执行过程
  • 参数:
    • moduleOptions: ModuleOptions[]
  • 场景:对注册的子应用进行过滤、拦截

# JModule#beforeRegisterModules

  • 类型:异步
  • 作用时期:宿主应用的 JModule.registerModules 的执行过程,在 beforeFilterModules Hook 之后。
  • 参数:
    • moduleOptions: ModuleOptions[]
  • 场景:对过滤后的子应用参数进行进一步调整,比如对于需要 JModule 子类进行初始化时,可以自行初始化实例以改变默认的 new JModule() 操作。

# JModule#afterRegisterModules

  • 类型:异步
  • 作用时期:宿主应用的 JModule.registerModules 的执行过程,在 beforeRegisterModules Hook 之后。
  • 参数:
    • modules: JModule[]
  • 场景:获取本次注册的 modules 实例。

# Resource#resource:getFetchOptions

  • 类型:异步
  • 作用时期:子应用静态资源的加载过程
  • 条件:子应用资源加载策略为 ResourceLoadStrategy.Fetch 时生效
  • 参数:
    • options: { currentUrl, type, resource, fetchOptions: {} }
  • 场景:修改 fetch 请求参数

# Resource#resource:transformFetchResult

  • 类型:异步
  • 作用时期:子应用静态资源的加载过程,资源获取完成后、解析前执行
  • 条件:子应用资源加载策略为 ResourceLoadStrategy.Fetch 时生效
  • 参数:
    • options: { currentUrl, type, resource, buffer: Uint8Array }
  • 场景:对自应用的资源进行封装,包括 js,css。比如用于 js 沙箱的实现、postcss 处理 css 资源来实现样式隔离。

WARNING

该 hook 拥有修改子应用代码的能力,请充分测试、谨慎使用。

# Resource#resource:insertPrivateVariable

  • 类型:异步
  • 作用时期:子应用静态资源的加载过程,资源获取完成后、解析前执行
  • 条件:子应用资源加载策略为 ResourceLoadStrategy.Fetch 时生效
  • 参数:
    • options: { currentUrl, type, resource, code: '' }
  • 场景:对 js 代码注入/改写全局的变量,比如用于解决自应用路由隔离的问题。

eg:

// module 为子应用实例
// pkg 为子应用声明的 JModule.define 的参数
JModule.addHook('resource:insertPrivateVariable', (options) => {
    const code = 'const history = createCustomHistory(context);';
    return [{ ...options, code  }];
});

# 关于沙箱/隔离的实现

JModule 本身没有提供 js沙箱、css 隔离的具体实现,但开发者可以通过Hook: Resource#resource:transformFetchResult 来对自应用代码进行封装以到隔离目的。

样式隔离:JModule 自带通过移除、加载样式实现隔离的接口,在大部分情况也能运行良好。缺点在于不能进行更细节的样式隔离处理, 如果加载策略为默认的 ResourceLoadStrategy.Element 方式则不能对动态 style 标签的样式进行管理。

js 隔离:对于子应用大部分问题,通过Hook: Resource#resource:insertPrivateVariable 实现部分沙箱,应该都能得到妥善解决,需要思考是否有必要进行全局的运行沙箱处理。