AngularJS插件架构:Blob动态导入ES模块(含es-module-shims)问题
解决方案
一、适配es-module-shims的Blob URL导入方案
Angular原生联邦依赖的es-module-shims对模块导入有特定处理逻辑,直接用原生import()加载Blob URL会触发无效URL错误,改用它提供的importShim()方法即可解决:
- 确保Blob的MIME类型正确
转换插件源码Blob时,指定模块类型的MIME格式:
// 假设pluginBlob是从OpenAPI服务获取的插件源码Blob const moduleBlob = new Blob([await blobToText(pluginBlob)], { type: 'text/javascript;module' });
配套的Blob转文本工具函数:
async function blobToText(blob: Blob): Promise<string> { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsText(blob); }); }
- 用importShim加载Blob URL
创建Blob URL后,通过es-module-shims的importShim方法导入模块并调用导出函数:
const blobUrl = URL.createObjectURL(moduleBlob); try { const module = await importShim(blobUrl); // 调用插件导出的initialize函数 await module.initialize(); } finally { // 用完后释放Blob URL,避免内存泄漏 URL.revokeObjectURL(blobUrl); }
二、结合IndexedDB实现插件缓存
为了避免每次启动重新下载插件,将下载后的插件源码存储到IndexedDB,下次启动直接从本地读取:
- 封装IndexedDB操作工具
class PluginCache { private db: IDBDatabase | null = null; async init(): Promise<void> { return new Promise((resolve, reject) => { const request = indexedDB.open('PluginCache', 1); request.onupgradeneeded = (e) => { const db = (e.target as IDBOpenDBRequest).result; db.createObjectStore('plugins', { keyPath: 'pluginId' }); }; request.onsuccess = (e) => { this.db = (e.target as IDBOpenDBRequest).result; resolve(); }; request.onerror = () => reject(request.error); }); } async savePlugin(pluginId: string, source: string): Promise<void> { if (!this.db) await this.init(); const transaction = this.db.transaction('plugins', 'readwrite'); const store = transaction.objectStore('plugins'); store.put({ pluginId, source }); return new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async getPlugin(pluginId: string): Promise<string | null> { if (!this.db) await this.init(); const transaction = this.db.transaction('plugins', 'readonly'); const store = transaction.objectStore('plugins'); const request = store.get(pluginId); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result?.source || null); request.onerror = () => reject(request.error); }); } }
- 加载插件时优先读取缓存
const pluginCache = new PluginCache(); const pluginId = 'your-target-plugin-id'; // 先尝试从缓存读取 let pluginSource = await pluginCache.getPlugin(pluginId); if (!pluginSource) { // 缓存不存在,从后端下载 const blob = await yourOpenApiPluginService.getPlugin(pluginId).toPromise(); pluginSource = await blobToText(blob); // 存入缓存 await pluginCache.savePlugin(pluginId, pluginSource); } // 转为Blob并加载 const moduleBlob = new Blob([pluginSource], { type: 'text/javascript;module' }); const blobUrl = URL.createObjectURL(moduleBlob); const module = await importShim(blobUrl); await module.initialize(); URL.revokeObjectURL(blobUrl);
三、改进script标签方案(避免全局作用域)
如果上述方案仍有问题,可以通过创建模块脚本并利用模块导出绑定获取插件内容:
async function loadPluginFromSource(source: string) { // 创建临时模块包装插件源码,将导出挂载到可访问对象 const wrappedSource = ` const pluginModule = {}; (async () => { const moduleExports = await import(\`data:text/javascript;module,\${encodeURIComponent(\`${source}\`)}\`); Object.assign(pluginModule, moduleExports); })(); export default pluginModule; `; const blob = new Blob([wrappedSource], { type: 'text/javascript;module' }); const blobUrl = URL.createObjectURL(blob); const wrapperModule = await importShim(blobUrl); // 等待插件模块加载完成 await new Promise(resolve => setTimeout(resolve, 100)); const pluginModule = wrapperModule.default; URL.revokeObjectURL(blobUrl); return pluginModule; } // 使用示例 const pluginSource = await blobToText(pluginBlob); const module = await loadPluginFromSource(pluginSource); await module.initialize();
内容的提问来源于stack exchange,提问作者Nicolas Stadler
相关产品推荐
相关产品推荐

