@jmodule/client
Version:
JModule运行时
571 lines (538 loc) • 19.9 kB
text/typescript
import { ModuleDebug } from './debug';
import { ResourceMetadata, Resource } from './resource';
import { DepResolver } from './depResolver';
import { ModuleHook } from './hook';
import { Matcher } from './utils/matcher';
import { ModuleOptions, ModuleMetadata, ModuleStatus } from './config';
import manager from './globalManager';
import { ElementModifier, LoadOptions } from './types';
import { eventToPromise } from './utils/eventToPromise';
/* 调试模式打印信息:路由变更信息,初始化模块实例、资源实例信息,模块状态变更信息 */
const getCurrentUrl = () => {
// 指向执行时脚本
return (document.currentScript as HTMLScriptElement)?.src || import.meta?.url || '';
}
// 指向解析时脚本
const currentScript = document.currentScript;
const moduleMap: { [key: string]: JModule } = {};
const moduleLog = {
[ModuleStatus.init]: '已创建模块实例',
[ModuleStatus.initializing]: '正在获取资源列表',
[ModuleStatus.initialized]: '资源初始化完成',
[ModuleStatus.initializeFailed]: '资源初始化失败',
[ModuleStatus.loading]: '正在加载模块资源',
[ModuleStatus.loaded]: '模块加载完成', // 代码加载并define解析完成,但 define 过程未执行
[ModuleStatus.loadFailure]: '模块加载或解析失败', // 加载失败或解析 define 失败
[ModuleStatus.defined]: '解析模块定义', // 初始化了 bootstrap 方法但未执行
[ModuleStatus.booting]: '正在挂载模块',
[ModuleStatus.done]: '模块已挂载', // bootstrap/define 过程已完成
[ModuleStatus.bootFailure]: '挂载模块过程异常', // bootstrap/define 过程中异常
};
const filteredModules: { [moduleKey: string]: string } = {};
const {
filter,
debug,
} = manager.getInitialConfig();
const filterModule = (conf: ModuleOptions) => {
const { key } = conf;
let errorMsg;
if (!key || moduleMap[key]) {
errorMsg = '重复注册或moduleKey未定义';
}
if (filter && typeof filter === 'function' && !filter(conf)) {
errorMsg = '主动过滤';
}
if (errorMsg) {
ModuleDebug.print({ key, message: errorMsg, instance: conf });
filteredModules[key] = errorMsg;
return false;
}
return true;
};
const urlOriginReg = /^((?:http|https):\/\/[^/]+)(\/.*)?$/g;
/**
* 从url中解析origin
* @param {String} url 需要解析的地址
* @return {String} url的origin 信息,包含protocol, domain 和 post
*/
function extractOrigin(url = '') {
return urlOriginReg.test(url)
? url.replace(urlOriginReg, (a, b) => b)
: '';
}
export type DeactivateHandler = () => void | Promise<void>;
export type ActivateHandler = (parentEl: Element) => void | Promise<void> | DeactivateHandler;
export type TypeHandler<T extends ModuleMetadata = ModuleMetadata> = (module: JModule, options: T) => ({
activate: ActivateHandler,
deactivate: DeactivateHandler,
})
/**
* JModule 实例
* @class
*/
export class JModule extends ModuleHook {
private static _debug?: boolean;
static id: number;
/**
* 模块类型
*/
type?: string;
/**
* 模块的key, 全局唯一
*/
key: string;
/**
* 模块别名
*/
name?: string;
/**
* 模块资源地址
*/
url: string;
/**
* 远程资源服务器
* @ignore
*/
server?: string;
/**
* 是否为远程模块, 根据url和当前origin计算
* @ignore
* @deprecated since version 1.1.0
* @type {Boolean}
*/
isRemoteModule?: boolean;
/**
* 远程模块所在域
* @ignore
* @deprecated since version 1.1.0
* @type {String}
*/
domain: string;
/**
* [不可配置] JModule.define 执行时自动生成的模块启动函数
* 全局仅执行一次, 内部依次处理: 执行init函数、加载 imports 声明的依赖模块、记录 exports 信息
*/
bootstrap?: { (): Promise<JModule> };
/**
* 模块加载后自动执行 bootstrap 函数, 默认为: true
*/
autoBootstrap?: boolean;
/** 约定的模块激活函数, 通常由 JModule.defineType 进行实现 */
activate?: ActivateHandler;
/** 约定的模块卸载函数, 通常由 JModule.defineType 进行实现 */
deactivate?: DeactivateHandler;
/** 模块对应的资源实例 */
resource: Resource;
/** 模块扩展信息 */
metadata:{[key: string]: any};
/**
* 模块内置的 hooks 信息, 仅支持 hooks.complete
* @example
* await module.hooks.complete
*/
hooks: {
complete: undefined|Promise<JModule>;
};
/**@ignore */
_status!: ModuleStatus;
/**
* @constructor
* @example
* new JModule({
* key: 'pipeline',
* url: 'http://localhost:8080/modules/pipeline/index.json',
* });
*/
constructor({
key, url, server, name, autoBootstrap = true,
resourceType, resource, type,
resourceLoadStrategy,
...others
}: ModuleOptions) {
const domain = server || extractOrigin(url);
const isRemoteModule = domain !== '/';
super();
/**
* 代码加载完成后执行
* @type {Promise<JModule>}
*/
this.hooks = { complete: undefined };
this.type = type;
this.key = key;
this.name = name;
this.url = url;
this.status = ModuleStatus.init;
this.server = domain;
this.isRemoteModule = isRemoteModule;
this.domain = domain;
this.resource = resource && resource instanceof Resource
? resource
: new Resource(url, {
type: resourceType,
strategy: resourceLoadStrategy,
});
this.autoBootstrap = autoBootstrap;
this.bootstrap = undefined;
this.metadata = others;
// 登记资源地址 与 moduleKey 之间的映射关系
manager.mapResourceUrlAndModuleKey(this.resource.url, this.key);
manager.jmodule(this.key, this);
}
/**
* 设置模块状态, 更新后会自动触发 `module.${this.key}.statusChange`事件
* @fires window#module.[moduleKey].statusChange
* @fires window#module.[moduleKey].[status]
*/
set status(status: ModuleStatus) {
if (status === ModuleStatus.loaded && this._status !== ModuleStatus.loading) {
return; // 异常状态事件
}
this._status = status;
if (status === ModuleStatus.loaded) {
setTimeout(() => {
if (this.status === ModuleStatus.loaded) {
ModuleDebug.print({
type: 'warning',
key: this.key,
message: 'JModule.define 可能无法正常执行, 请检查子应用资源响应是否正常、是否执行异常、是否能执行JModule.define',
instance: this,
});
}
}, 5000);
}
const eventData = { detail: this };
/* eslint-disable no-nested-ternary */
ModuleDebug.print({
type: status !== ModuleStatus.loadFailure
? status !== ModuleStatus.loading ? 'success' : 'log' : 'error',
key: this.key,
message: moduleLog[status],
instance: this,
});
window.dispatchEvent(new CustomEvent(`module.${this.key}.statusChange`, eventData));
window.dispatchEvent(new CustomEvent(`module.${this.key}.${status}`, eventData));
}
/**
* 获取模块状态
* @enum {ModuleStatus}
*/
get status() {
return this._status;
}
/**
* 设置debug模式,开启后将打印模块注册、加载、解析的全过程信息
* @example
* JModule.debug = true;
*/
static set debug(status: boolean) {
// eslint-disable-next-line no-underscore-dangle
JModule._debug = status;
if (status) {
ModuleDebug.enable();
} else {
ModuleDebug.disable();
}
}
static get debug(): boolean {
// eslint-disable-next-line no-underscore-dangle
return JModule._debug || false;
}
/**
* 定义子应用类型的处理逻辑
* @param {String} type 子应用类型
* @param {TypeHandler} typeHandler 类型处理函数
*/
static defineType(type: string, typeHandler: TypeHandler) {
JModule.addHook('afterDefine', (module: JModule, metadata: ModuleMetadata) => {
if (module.type === type && typeof typeHandler === 'function') {
const { activate, deactivate } = typeHandler(module, metadata) || {};
module.activate = activate;
module.deactivate = deactivate;
}
return [module, metadata];
});
}
/**
* 获取已注册的模块列表
* @readOnly
*/
static get registeredModules(): JModule[] {
return Object.values(moduleMap);
}
/**
* 根据 moduleKey 获取模块实例
* @static
* @param {String} key moduleKey
* @return {JModule|undefined}
*/
static getModule(key: string): JModule|undefined {
return manager.jmodule(key);
}
/**
* 根据 moduleKey 异步获取模块实例
* @static
* @param {String} key moduleKey
* @return {Promise<JModule>}
*/
static async getModuleAsync(key: string, timeout?: number): Promise<JModule> {
const module = manager.jmodule(key);
return module ? Promise.resolve(module) : new Promise((resolve, reject) => {
if (timeout) {
const timer = setTimeout(() => {
clearTimeout(timer);
if (!manager.jmodule(key)) {
reject(new Error(`Timeout:getModuleAsync('${key}', ${timeout})`));
}
}, timeout);
}
function resolverListener() {
if (manager.jmodule(key)) {
window.removeEventListener('module.afterRegister', resolverListener);
resolve(manager.jmodule(key) as JModule);
}
}
window.addEventListener('module.afterRegister', resolverListener);
});
}
/**
* 引用其它模块暴露的功能
*
* @param {String} namespace
* @example
* JModule.require('pipeline.models.PipelineApp')
* .then((PipelineApp) => {
* // do something
* });
* @return {Promise<var>}
*/
static async require(namespace: string): Promise<any> {
return manager.require(namespace);
}
/**
* 注册模块
* @fires window#module.afterRegister
* @example
* JModule.registerModules([{
* type: 'page',
* key: 'pipeline',
* name: 'pipeline',
* url: 'http://localhost:8080/modules/pipeline/index.json',
* }]);
* window.addEventListener('module.afterRegister', ({ detail:modules }) => {
* // do sth;
* })
*/
static async registerModules(moduleOptions: ModuleOptions[] = []): Promise<JModule[]> {
ModuleDebug.print({
type: 'log',
key: 'registerModules',
message: '请求注册模块',
instance: moduleOptions,
});
await ModuleHook.runHook('beforeFilterModules', moduleOptions);
const results = moduleOptions.filter(filterModule);
await ModuleHook.runHook('beforeRegisterModules', results);
const modules = (<ModuleOptions[]>results).map(
item => (item instanceof JModule ? item : new this(item)),
);
modules.forEach((item) => {
if (!item.key) {
return;
}
moduleMap[item.key] = item;
});
window.dispatchEvent(new CustomEvent('module.afterRegister', { detail: modules }));
await ModuleHook.runHook('afterRegisterModules', modules);
return modules;
}
/**
* 暴露平台功能给模块使用
* @param {object} obj 需要暴露的对象
* @example
* JModule.export({
* $platform: {
* utils, event, router,
* },
* $node_modules: {
* vue: Vue,
* },
* });
* import Vue from '$node_modules.vue';
* @return {JModule}
*/
static export(obj = {}, matcher = {}) {
new Matcher(matcher).cache(obj);
return JModule;
}
/**
* 定义模块
* @param {String} moduleKey 定义模块唯一标识
* @param {Object} metadata 定义模块
* @param {Function} [metadata.init<jModuleInstance>] 初始化函数,自动调用
* @param {Array<moduleKey>} [metadata.imports] 依赖的模块
* @param {Object} [metadata.exports] 对外暴露的功能
* @example
* JModule.define('pipeline', {
* init(module) {
* console.log(module);
* },
* imports: [],
* exports: {},
* });
*/
static define = manager.define;
/**
* 根据 resourceLoaderUrl 设置资源
* 当 resourceLoaderUrl 为空时, 该函数只能在 resource.url 文件对应的脚本中执行
*
* @param { ResourceMetadata } resourceMetadata 资源元数据
* @param { string|undefined } resourceLoaderUrl 加载资源的初始脚本地址, 默认为执行当前函数的脚本文件
* @returns { Resource }
*/
static applyResource(resourceMetadata: ResourceMetadata, resourceLoaderUrl?: string): Resource {
const loaderUrl: string | undefined = resourceLoaderUrl || getCurrentUrl();
if (!loaderUrl) {
throw new Error('JModule.applyResource 执行异常: 无法找到脚本加载源并关联到对应的资源实例');
}
// 任意 Resource 执行该函数均等价
return Resource.setResourceData(resourceMetadata, loaderUrl.replace(/(\?|&)__v__=\d+$/, ''));
}
/**
* @ignore
* @deprecated
*/
static getMeta() {
const url = getCurrentUrl();
if (!url) {
return {};
}
return {
url,
server: new URL(url).origin,
};
}
/**
* 引用平台暴露的对象
* 优先从初始化自身Module实例的 JModule.exports 对象中查找
* 如果查找失败, 最终将回退到 JModuleManager.import 进行查找
*
* @ignore
* @param {String} namespace
* @param {Object} config 通过编译工具注入的相关环境参数
* @return {var}
*/
static import<T>(namespace = '', config: Record<string, string|number> = {}, force = false): T|{
url?: string,
server?: string,
} { // 用于导入平台接口
if (namespace === '$module.meta') {
return this.getMeta();
}
// 向下兼容: 如果直接使用的全局JModule, 则应直接使用全局JModule上的导出.
if (!force && this === window.JModule) {
return this.import(namespace, config, true);
}
if (!force && this !== manager.defaultJModule && currentScript) {
const { dataset } = <HTMLScriptElement>currentScript;
const sourceUrl = dataset?.jmoduleFrom || '';
// 从 sourceUrl 找到对应的 module
const [targetModule] = manager.getModulesByResourceUrl(sourceUrl) || [];
if (targetModule && targetModule.constructor !== JModule) {
return (targetModule.constructor as typeof JModule).import(namespace, config, true);
}
// 回退到默认JModule执行
// esm 模块目前无法读取 scriptElement, 也会回退到这里
return this.import(namespace, config, true);
}
const matchedExports = new Matcher(config).getCache();
const res = namespace.split('.').reduce((res, key) => (res || {})[key], matchedExports);
if (!res && this !== manager.defaultJModule) {
// 如果无法正确找到共享数据, 回退到默认JModule执行
return manager.import(namespace, config);
}
if (res && res instanceof DepResolver) {
return res.resolve(config);
}
return res;
}
// 兼容以前的 cli 工具
/**@ignore */
static _import(namespace = '', config = {}) {
console.warn('JModule._import is deprecated');
return this.import(namespace, config);
}
private setCompleteHook() {
this.hooks.complete = Promise.race([
eventToPromise(`module.${this.key}.${ModuleStatus.done}`),
eventToPromise(`module.${this.key}.${ModuleStatus.loadFailure}`),
eventToPromise(`module.${this.key}.${ModuleStatus.bootFailure}`),
]).then(() => {
if (this._status !== ModuleStatus.done) {
throw new Error(moduleLog[this._status]);
}
return this;
});
}
/**
* 加载模块
* @async
* @method
* @param {'init'|'preload'|'load'} [targetStatus='load'] - 期望达到的目标状态,默认为 'load',向下兼容。
* @param {LoadOptions<HTMLScriptElement|HTMLLinkElement>} [options={ autoApplyStyle: true }] - 选项参数。
* @return {Promise<Resource|void>} - 返回一个承诺,该承诺在模块加载完成时解决。
*/
async load(
targetStatus: 'init'|'preload'|'load' = 'load',
options: LoadOptions<HTMLScriptElement|HTMLLinkElement> = { autoApplyStyle: true },
): Promise<Resource|void> {
const { resource } = this;
const {
loading,
defined,
booting,
done,
loaded,
initializeFailed,
loadFailure,
bootFailure,
} = ModuleStatus;
// 已经进入正式 load 状态且未失败, 则等待执行结果, 即已经 load 过了, 下次再执行
if ([loading, defined, booting, done, loaded].includes(this.status)) {
if (targetStatus === 'load' && options.autoApplyStyle) {
resource.applyStyle();
}
await (targetStatus !== 'load' ? Promise.resolve() : this.hooks.complete);
return resource;
}
// init: 从初始化到初始化有结果, 任何失败状态重新执行都强制 forceInit
await resource.init([loadFailure, bootFailure, initializeFailed].includes(this.status));
// preload: 只要 init 是成功的就可以
if (targetStatus === 'preload') {
if (window.requestIdleCallback) {
window.requestIdleCallback(() => resource.preload(options.elementModifier));
} else {
window.setTimeout(() => resource.preload(options.elementModifier), 500);
}
}
// load
if (targetStatus === 'load') {
this.status = ModuleStatus.loading;
this.setCompleteHook();
resource.applyScript(options.elementModifier as ElementModifier<HTMLScriptElement>);
if (options.autoApplyStyle) {
resource.applyStyle(options.elementModifier);
}
try {
await this.hooks.complete;
} catch(e) {
// js 执行失败时, 应用的样式也没有意义, 移除为后续加载做准备
resource.removeStyle();
resource.resetStyleStatus();
throw e;
}
}
return resource;
}
}
JModule.id = manager.registerJModule(JModule);
JModule.debug = debug;