oak-cli/plugins/LoadFromCdn.js

132 lines
4.0 KiB
JavaScript

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.*?src="(.*?)".*?><\/script>/,
`<script src="loadFromCdn.js"></script>
<script>
loadExternals().then(() => {
const script = document.createElement('script');
script.src = "$1";
document.body.appendChild(script);
});
</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;