import type { Compiler } from '@rspack/core' import { rspack } from '@rspack/core' export interface ChunkLoadRetryPluginOptions { /** 每个 chunk 的最大重试次数,不包含首次请求 */ maxRetries?: number /** 每次重试之间的等待时间(毫秒) */ retryDelay?: number /** 重试时追加时间戳参数,避免命中失败缓存 */ cacheBust?: boolean } const PLUGIN_NAME = 'ChunkLoadRetryPlugin' class ChunkLoadRetryRuntimeModule extends rspack.RuntimeModule { constructor(private readonly options: Required) { super('webpack/runtime/edu-scripts_chunk_load_retry', rspack.RuntimeModule.STAGE_TRIGGER) } override generate() { const { maxRetries, retryDelay, cacheBust } = this.options return ` var __eduMaxRetries__ = ${JSON.stringify(maxRetries)}; var __eduRetryDelay__ = ${JSON.stringify(retryDelay)}; var __eduCacheBust__ = ${JSON.stringify(cacheBust)}; var __eduLoadScript__ = __webpack_require__.l; if (typeof __eduLoadScript__ === 'function' && !__eduLoadScript__.__eduChunkRetryPatched__) { __webpack_require__.l = function(url, done, key, chunkId) { var retries = 0; var attempt = function(requestUrl) { __eduLoadScript__(requestUrl, function(event) { if (!event || event.type === 'load') { done(event); return; } if (retries >= __eduMaxRetries__) { done(event); return; } retries += 1; var nextUrl = url; if (__eduCacheBust__) { nextUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'retry=' + retries + '&t=' + Date.now(); } if (__eduRetryDelay__ > 0) { setTimeout(function() { attempt(nextUrl); }, __eduRetryDelay__); } else { attempt(nextUrl); } }, key ? key + '-retry-' + retries : key, chunkId); }; attempt(url); }; __webpack_require__.l.__eduChunkRetryPatched__ = true; } ` } } export default class ChunkLoadRetryPlugin { private readonly options: Required constructor(options: ChunkLoadRetryPluginOptions = {}) { this.options = { maxRetries: Math.max(0, options.maxRetries ?? 3), retryDelay: Math.max(0, options.retryDelay ?? 100), cacheBust: options.cacheBust ?? true, } } apply(compiler: Compiler) { compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { compilation.hooks.runtimeRequirementInTree .for(rspack.RuntimeGlobals.loadScript) .tap(PLUGIN_NAME, (chunk) => { compilation.addRuntimeModule(chunk, new ChunkLoadRetryRuntimeModule(this.options)) }) }) } }