diff --git a/docs/api/preloadApp.md b/docs/api/preloadApp.md index fc2c99f4..6f190acd 100644 --- a/docs/api/preloadApp.md +++ b/docs/api/preloadApp.md @@ -51,6 +51,10 @@ type preOptions { deactivated?: lifecycle; /** 子应用资源加载失败后调用 */ loadError?: loadErrorHandler + /** 超时取消开关 */ + cancelRequest?: boolean; + /** 超时等待时间 */ + timeout?: number; }; ``` diff --git a/docs/api/setupApp.md b/docs/api/setupApp.md index 1cadc610..3e6dc79e 100644 --- a/docs/api/setupApp.md +++ b/docs/api/setupApp.md @@ -48,6 +48,10 @@ type baseOptions = { activated?: lifecycle; deactivated?: lifecycle; loadError?: loadErrorHandler; + /** 超时取消开关 */ + cancelRequest?: boolean; + /** 超时等待时间 */ + timeout?: number; }; type preOptions = baseOptions & { diff --git a/docs/api/startApp.md b/docs/api/startApp.md index 91d1b723..ba7644ec 100644 --- a/docs/api/startApp.md +++ b/docs/api/startApp.md @@ -58,7 +58,11 @@ type startOption { activated?: lifecycle; deactivated?: lifecycle; /** 子应用资源加载失败后调用 */ - loadError?: loadErrorHandler + loadError?: loadErrorHandler; + /** 超时取消开关 */ + cancelRequest?: boolean; + /** 超时等待时间 */ + timeout?: number; }; ``` diff --git a/packages/wujie-core/src/effect.ts b/packages/wujie-core/src/effect.ts index 2444df52..17889234 100644 --- a/packages/wujie-core/src/effect.ts +++ b/packages/wujie-core/src/effect.ts @@ -179,7 +179,18 @@ function rewriteAppendOrInsertChild(opts: { const { rawDOMAppendOrInsertBefore, wujieId } = opts; const sandbox = getWujieById(wujieId); - const { styleSheetElements, replace, fetch, plugins, iframe, lifecycles, proxyLocation, fiber } = sandbox; + const { + styleSheetElements, + replace, + fetch, + plugins, + iframe, + lifecycles, + proxyLocation, + fiber, + cancelRequest, + timeout, + } = sandbox; if (!isHijackingTag(element.tagName) || !wujieId) { const res = rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; @@ -288,29 +299,32 @@ function rewriteAppendOrInsertChild(opts: { ignore: isMatchUrl(src, getEffectLoaders("jsIgnores", plugins)), attrs: parseTagAttributes(element.outerHTML), } as ScriptObject; - getExternalScripts([scriptOptions], fetch, lifecycles.loadError, fiber).forEach((scriptResult) => { - dynamicScriptExecStack = dynamicScriptExecStack.then(() => - scriptResult.contentPromise.then( - (content) => { - if (sandbox.execQueue === null) return warn(WUJIE_TIPS_REPEAT_RENDER); - const execQueueLength = sandbox.execQueue?.length; - sandbox.execQueue.push(() => - fiber - ? sandbox.requestIdleCallback(() => { - execScript({ ...scriptResult, content }); - }) - : execScript({ ...scriptResult, content }) - ); - // 同步脚本如果都执行完了,需要手动触发执行 - if (!execQueueLength) sandbox.execQueue.shift()(); - }, - () => { - manualInvokeElementEvent(element, "error"); - element = null; - } - ) - ); - }); + getExternalScripts([scriptOptions], fetch, lifecycles.loadError, fiber, cancelRequest, timeout).forEach( + (scriptResult) => { + dynamicScriptExecStack = dynamicScriptExecStack.then(() => + // fetch请求超时会影响后续其它任务通过then链式调用的方式推入execQueue当中 + scriptResult.contentPromise.then( + (content) => { + if (sandbox.execQueue === null) return warn(WUJIE_TIPS_REPEAT_RENDER); + const execQueueLength = sandbox.execQueue?.length; + sandbox.execQueue.push(() => + fiber + ? sandbox.requestIdleCallback(() => { + execScript({ ...scriptResult, content }); + }) + : execScript({ ...scriptResult, content }) + ); + // 同步脚本如果都执行完了,需要手动触发执行 + if (!execQueueLength) sandbox.execQueue.shift()(); + }, + () => { + manualInvokeElementEvent(element, "error"); + element = null; + } + ) + ); + } + ); } else { const execQueueLength = sandbox.execQueue?.length; sandbox.execQueue.push(() => diff --git a/packages/wujie-core/src/entry.ts b/packages/wujie-core/src/entry.ts index 2a3614f2..efab3681 100644 --- a/packages/wujie-core/src/entry.ts +++ b/packages/wujie-core/src/entry.ts @@ -5,7 +5,15 @@ import processTpl, { ScriptBaseObject, StyleObject, } from "./template"; -import { defaultGetPublicPath, getInlineCode, requestIdleCallback, error, compose, getCurUrl } from "./utils"; +import { + defaultGetPublicPath, + getInlineCode, + requestIdleCallback, + error, + compose, + getCurUrl, + fetchWithTimeOut, +} from "./utils"; import { WUJIE_TIPS_NO_FETCH, WUJIE_TIPS_SCRIPT_ERROR_REQUESTED, @@ -34,6 +42,8 @@ type ImportEntryOpts = { fiber?: boolean; plugins?: Array; loadError?: loadErrorHandler; + cancelRequest?: boolean; + timeout?: number; }; const styleCache = {}; @@ -105,10 +115,12 @@ const fetchAssets = ( cache: Object, fetch: (input: RequestInfo, init?: RequestInit) => Promise, cssFlag?: boolean, - loadError?: loadErrorHandler + loadError?: loadErrorHandler, + cancelRequest?: boolean, + timeout?: number ) => cache[src] || - (cache[src] = fetch(src) + (cache[src] = fetchWithTimeOut(src, fetch, cancelRequest, timeout) .then((response) => { // usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event // https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603 @@ -143,7 +155,9 @@ const fetchAssets = ( export function getExternalStyleSheets( styles: StyleObject[], fetch: (input: RequestInfo, init?: RequestInit) => Promise = defaultFetch, - loadError: loadErrorHandler + loadError: loadErrorHandler, + cancelRequest?: boolean, + timeout?: number ): StyleResultList { return styles.map(({ src, content, ignore }) => { // 内联 @@ -157,7 +171,9 @@ export function getExternalStyleSheets( return { src, ignore, - contentPromise: ignore ? Promise.resolve("") : fetchAssets(src, styleCache, fetch, true, loadError), + contentPromise: ignore + ? Promise.resolve("") + : fetchAssets(src, styleCache, fetch, true, loadError, cancelRequest, timeout), }; } }); @@ -168,7 +184,9 @@ export function getExternalScripts( scripts: ScriptObject[], fetch: (input: RequestInfo, init?: RequestInit) => Promise = defaultFetch, loadError: loadErrorHandler, - fiber: boolean + fiber: boolean, + cancelRequest?: boolean, + timeout?: number ): ScriptResultList { // module should be requested in iframe return scripts.map((script) => { @@ -178,8 +196,10 @@ export function getExternalScripts( if ((async || defer) && src && !module) { contentPromise = new Promise((resolve, reject) => fiber - ? requestIdleCallback(() => fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject)) - : fetchAssets(src, scriptCache, fetch, false, loadError).then(resolve, reject) + ? requestIdleCallback(() => + fetchAssets(src, scriptCache, fetch, false, loadError, cancelRequest, timeout).then(resolve, reject) + ) + : fetchAssets(src, scriptCache, fetch, false, loadError, cancelRequest, timeout).then(resolve, reject) ); // module || ignore } else if ((module && src) || ignore) { @@ -189,7 +209,7 @@ export function getExternalScripts( contentPromise = Promise.resolve(script.content); // outline } else { - contentPromise = fetchAssets(src, scriptCache, fetch, false, loadError); + contentPromise = fetchAssets(src, scriptCache, fetch, false, loadError, cancelRequest, timeout); } // refer https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer if (module && !async) script.defer = true; @@ -205,7 +225,7 @@ export default function importHTML(params: { const { url, opts, html } = params; const fetch = opts.fetch ?? defaultFetch; const fiber = opts.fiber ?? true; - const { plugins, loadError } = opts; + const { plugins, loadError, cancelRequest, timeout } = opts; const htmlLoader = plugins ? compose(plugins.map((plugin) => plugin.htmlLoader)) : defaultGetTemplate; const jsExcludes = getEffectLoaders("jsExcludes", plugins); const cssExcludes = getEffectLoaders("cssExcludes", plugins); @@ -243,7 +263,9 @@ export default function importHTML(params: { .map((script) => ({ ...script, ignore: script.src && isMatchUrl(script.src, jsIgnores) })), fetch, loadError, - fiber + fiber, + cancelRequest, + timeout ), getExternalStyleSheets: () => getExternalStyleSheets( @@ -251,7 +273,9 @@ export default function importHTML(params: { .filter((style) => !style.src || !isMatchUrl(style.src, cssExcludes)) .map((style) => ({ ...style, ignore: style.src && isMatchUrl(style.src, cssIgnores) })), fetch, - loadError + loadError, + cancelRequest, + timeout ), }; }); diff --git a/packages/wujie-core/src/index.ts b/packages/wujie-core/src/index.ts index 0bf908ee..3c36fbf5 100644 --- a/packages/wujie-core/src/index.ts +++ b/packages/wujie-core/src/index.ts @@ -118,6 +118,10 @@ type baseOptions = { iframeAddEventListeners?: Array; /** 子应用iframe on事件 */ iframeOnEvents?: Array; + /** 是否取消请求 */ + cancelRequest?: boolean; + /** 请求超时时间 */ + timeout?: number; /** 子应用生命周期 */ beforeLoad?: lifecycle; beforeMount?: lifecycle; @@ -212,6 +216,8 @@ export async function startApp(startOptions: startOptions): Promise; iframeOnEvents?: Array; + cancelRequest?: boolean; + timeout?: number; }) { // 传递inject给嵌套子应用 if (window.__POWERED_BY_WUJIE__) this.inject = window.__WUJIE.inject; @@ -515,7 +525,8 @@ export default class Wujie { this.plugins = getPlugins(plugins); this.iframeAddEventListeners = options.iframeAddEventListeners; this.iframeOnEvents = options.iframeOnEvents; - + this.cancelRequest = options.cancelRequest; + this.timeout = options.timeout; // 创建目标地址的解析 const { urlElement, appHostPath, appRoutePath } = appRouteParse(url); const { mainHostPath } = this.inject; diff --git a/packages/wujie-core/src/shadow.ts b/packages/wujie-core/src/shadow.ts index 130b0654..e62894ec 100644 --- a/packages/wujie-core/src/shadow.ts +++ b/packages/wujie-core/src/shadow.ts @@ -108,7 +108,7 @@ export function initRenderIframeAndContainer( */ async function processCssLoaderForTemplate(sandbox: Wujie, html: HTMLHtmlElement): Promise { const document = sandbox.iframe.contentDocument; - const { plugins, replace, proxyLocation } = sandbox; + const { plugins, replace, proxyLocation, cancelRequest, timeout } = sandbox; const cssLoader = getCssLoader({ plugins, replace }); const cssBeforeLoaders = getPresetLoaders("cssBeforeLoaders", plugins); const cssAfterLoaders = getPresetLoaders("cssAfterLoaders", plugins); @@ -116,7 +116,7 @@ async function processCssLoaderForTemplate(sandbox: Wujie, html: HTMLHtmlElement return await Promise.all([ Promise.all( - getExternalStyleSheets(cssBeforeLoaders, sandbox.fetch, sandbox.lifecycles.loadError).map( + getExternalStyleSheets(cssBeforeLoaders, sandbox.fetch, sandbox.lifecycles.loadError, cancelRequest, timeout).map( ({ src, contentPromise }) => contentPromise.then((content) => ({ src, content })) ) ).then((contentList) => { @@ -131,7 +131,7 @@ async function processCssLoaderForTemplate(sandbox: Wujie, html: HTMLHtmlElement }); }), Promise.all( - getExternalStyleSheets(cssAfterLoaders, sandbox.fetch, sandbox.lifecycles.loadError).map( + getExternalStyleSheets(cssAfterLoaders, sandbox.fetch, sandbox.lifecycles.loadError, cancelRequest, timeout).map( ({ src, contentPromise }) => contentPromise.then((content) => ({ src, content })) ) ).then((contentList) => { diff --git a/packages/wujie-core/src/utils.ts b/packages/wujie-core/src/utils.ts index d29672d5..c4736498 100644 --- a/packages/wujie-core/src/utils.ts +++ b/packages/wujie-core/src/utils.ts @@ -355,6 +355,8 @@ export function mergeOptions(options: cacheOptions, cacheOptions: cacheOptions) deactivated: options.deactivated || cacheOptions?.deactivated, loadError: options.loadError || cacheOptions?.loadError, }, + cancelRequest: options.cancelRequest || cacheOptions?.cancelRequest, + timeout: options.timeout || cacheOptions?.timeout, }; } @@ -376,3 +378,20 @@ export function stopMainAppRun() { warn(WUJIE_TIPS_STOP_APP_DETAIL); throw new Error(WUJIE_TIPS_STOP_APP); } + +export function fetchWithTimeOut( + url: string, + fetch: (input: RequestInfo, init?: RequestInit) => Promise, + cancelRequest?: boolean, + timeout: number = 5000 +) { + if (cancelRequest) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + return fetch(url, { + signal: controller.signal, + }).finally(() => clearTimeout(id)); + } else { + return fetch(url); + } +} diff --git a/packages/wujie-react/index.js b/packages/wujie-react/index.js index e8a9c4d3..afa31afb 100644 --- a/packages/wujie-react/index.js +++ b/packages/wujie-react/index.js @@ -92,4 +92,6 @@ const propTypes = { style: PropTypes.object, iframeAddEventListeners: PropTypes.arrayOf(PropTypes.string), iframeOnEvents: PropTypes.arrayOf(PropTypes.string), + cancelRequest: PropTypes.bool, + timeout: PropTypes.number, }; diff --git a/packages/wujie-vue2/index.js b/packages/wujie-vue2/index.js index 623661ed..b19d02b6 100644 --- a/packages/wujie-vue2/index.js +++ b/packages/wujie-vue2/index.js @@ -30,6 +30,8 @@ const wujieVueOptions = { style: { type: Object, default: undefined }, iframeAddEventListeners: { type: Array, default: null }, iframeOnEvents: { type: Array, default: null }, + cancelRequest: { type: Boolean, default: undefined }, + timeout: { type: Number, default: undefined }, }, data() { return { @@ -89,6 +91,8 @@ const wujieVueOptions = { loadError: this.loadError, iframeAddEventListeners: this.iframeAddEventListeners, iframeOnEvents: this.iframeOnEvents, + cancelRequest: this.cancelRequest, + timeout: this.timeout, }); } catch (error) { console.log(error); diff --git a/packages/wujie-vue3/index.js b/packages/wujie-vue3/index.js index 5cd8c4dd..af2218c6 100644 --- a/packages/wujie-vue3/index.js +++ b/packages/wujie-vue3/index.js @@ -30,6 +30,8 @@ const wujieVueOptions = { style: { type: Object, default: undefined }, iframeAddEventListeners: { type: Array, default: null }, iframeOnEvents: { type: Array, default: null }, + cancelRequest: { type: Boolean, default: undefined }, + timeout: { type: Number, default: undefined }, }, data() { return { @@ -88,6 +90,8 @@ const wujieVueOptions = { loadError: this.loadError, iframeAddEventListeners: this.iframeAddEventListeners, iframeOnEvents: this.iframeOnEvents, + cancelRequest: this.cancelRequest, + timeout: this.timeout, }); } catch (error) { console.log(error);