const fs = require('fs'); const path = require('path'); class CdnLoaderPlugin { constructor(options) { this.options = options; } apply(compiler) { compiler.hooks.emit.tapAsync('CdnLoaderPlugin', (compilation, callback) => { // 生成 loadFromCdn.js 内容 const cdnLoaderContent = this.generateCdnLoaderContent(); // 将 loadFromCdn.js 添加到输出文件中 compilation.assets['loadFromCdn.js'] = { source: () => cdnLoaderContent, size: () => cdnLoaderContent.length }; // 修改 HTML 文件 Object.keys(compilation.assets).forEach(filename => { if (filename.endsWith('.html')) { const asset = compilation.assets[filename]; let content = asset.source(); // 替换入口脚本的加载方式 content = content.replace( /<\/script>/, ` ` ); compilation.assets[filename] = { source: () => content, size: () => content.length }; } }); callback(); }); } generateCdnLoaderContent() { const { from, module, timeout, retry } = this.options; const cdnUrls = Object.entries(module).reduce((acc, [name, config]) => { acc[name] = config.direct? [config.js] : Array.isArray(from) ? from.map(url => `${url}/${name}/${config.version}/${config.js}`) : [`${from}/${name}/${config.version}/${config.js}`]; return acc; }, {}); return ` const GLOBAL_CONFIG = { TIMEOUT: ${timeout}, MAX_RETRIES: ${retry}, onLoadError: null, }; const cdnUrls = ${JSON.stringify(cdnUrls, null, 2)}; function loadScriptWithTimeout(url, timeout) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; const timeoutId = setTimeout(() => { reject(new Error(\`Loading \${url} timed out\`)); }, timeout); script.onload = () => { clearTimeout(timeoutId); resolve(script); }; script.onerror = () => { clearTimeout(timeoutId); reject(new Error(\`Failed to load \${url}\`)); }; document.head.appendChild(script); }); } async function loadLibraryWithRetry(library, retryCount = 0) { const controllers = cdnUrls[library].map(() => new AbortController()); try { const loadPromises = cdnUrls[library].map((url, index) => { return loadScriptWithTimeout(url, GLOBAL_CONFIG.TIMEOUT) .then(script => { controllers.forEach((controller, i) => { if (i !== index) controller.abort(); }); return script; }); }); return await Promise.any(loadPromises); } catch (error) { if (retryCount < GLOBAL_CONFIG.MAX_RETRIES) { console.warn(\`Retry loading \${library}, attempt \${retryCount + 1}\`); return loadLibraryWithRetry(library, retryCount + 1); } else { if (GLOBAL_CONFIG.onLoadError) { GLOBAL_CONFIG.onLoadError(library, error); } throw error; } } } function loadExternals() { const libraries = Object.keys(cdnUrls); return Promise.all(libraries.map(library => loadLibraryWithRetry(library))); } // 默认的错误处理函数 GLOBAL_CONFIG.onLoadError = (library, error) => { console.error(\`Failed to load \${library} after \${GLOBAL_CONFIG.MAX_RETRIES} retries:\`, error); }; `; } } module.exports = CdnLoaderPlugin;