UNPKG

@jmodule/client

Version:
571 lines (538 loc) 19.9 kB
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;