diff --git a/.changeset/hungry-needles-doubt.md b/.changeset/hungry-needles-doubt.md new file mode 100644 index 000000000..ef993743a --- /dev/null +++ b/.changeset/hungry-needles-doubt.md @@ -0,0 +1,7 @@ +--- +"@qiankunjs/loader": patch +"@qiankunjs/sandbox": patch +"@qiankunjs/shared": patch +--- + +feat: support defer scripts and keep the executing order to consist with browser diff --git a/packages/loader/src/index.ts b/packages/loader/src/index.ts index 0bfc0e51c..25cbab178 100644 --- a/packages/loader/src/index.ts +++ b/packages/loader/src/index.ts @@ -1,7 +1,12 @@ import type { Sandbox } from '@qiankunjs/sandbox'; import { qiankunHeadTagName } from '@qiankunjs/sandbox'; -import type { BaseTranspilerOpts, NodeTransformer } from '@qiankunjs/shared'; -import { Deferred, QiankunError } from '@qiankunjs/shared'; +import type { + AssetsTranspilerOpts, + BaseTranspilerOpts, + NodeTransformer, + ScriptTranspilerOpts, +} from '@qiankunjs/shared'; +import { Deferred, prepareScriptForQueue, QiankunError } from '@qiankunjs/shared'; import { createTagTransformStream } from './TagTransformStream'; import { isUrlHasOwnProtocol } from './utils'; import WritableDOMStream from './writable-dom'; @@ -22,6 +27,16 @@ export type LoaderOpts = { nodeTransformer?: NodeTransformer; } & Omit & { sandbox?: Sandbox }; +const isExternalScript = (script: HTMLScriptElement): boolean => { + return script.tagName === 'SCRIPT' && !!(script.src || script.dataset.src); +}; +const isEntryScript = (script: HTMLScriptElement): boolean => { + return isExternalScript(script) && script.hasAttribute('entry'); +}; +const isDeferScript = (script: HTMLScriptElement): boolean => { + return isExternalScript(script) && script.hasAttribute('defer'); +}; + /** * @param entry * @param container @@ -34,9 +49,6 @@ export async function loadEntry(entry: Entry, container: HTMLElement, opts: L if (res.body) { let foundEntryScript = false; const entryScriptLoadedDeferred = new Deferred(); - const isEntryScript = (script: HTMLScriptElement): boolean => { - return script.hasAttribute('entry'); - }; const onEntryLoaded = () => { // the latest set prop is the entry script exposed global variable if (sandbox?.latestSetProp) { @@ -47,6 +59,8 @@ export async function loadEntry(entry: Entry, container: HTMLElement, opts: L entryScriptLoadedDeferred.resolve({} as T); } }; + const deferScripts: HTMLScriptElement[] = []; + const deferScriptDeferredWeakMap = new WeakMap>(); let readableStream = res.body.pipeThrough(new TextDecoderStream()); @@ -69,22 +83,41 @@ export async function loadEntry(entry: Entry, container: HTMLElement, opts: L ) .pipeTo( new WritableDOMStream(container, null, (clone, node) => { - const transformedNode = nodeTransformer - ? nodeTransformer(clone, entry, { - fetch, - sandbox, - rawNode: node as unknown as Node, - }) - : clone; + let transformerOpts: AssetsTranspilerOpts = { + fetch, + sandbox, + rawNode: node as unknown as Node, + }; + + let queueScript: (script: HTMLScriptElement) => void; + const deferScriptMode = isDeferScript(node as unknown as HTMLScriptElement); + if (deferScriptMode) { + const { scriptDeferred, prevScriptDeferred, queue } = prepareScriptForQueue( + deferScripts, + deferScriptDeferredWeakMap, + ); + transformerOpts = { + ...transformerOpts, + prevScriptTranspiledDeferred: prevScriptDeferred, + scriptTranspiledDeferred: scriptDeferred, + } as ScriptTranspilerOpts; + queueScript = queue; + } + + const transformedNode = nodeTransformer ? nodeTransformer(clone, entry, transformerOpts) : clone; const script = transformedNode as unknown as HTMLScriptElement; + if (deferScriptMode) { + queueScript!(script); + } + /* * If the entry script is executed, we can complete the entry process in advance * otherwise we need to wait until the last script is executed. * Notice that we only support external script as entry script thus we could do resolve the promise after the script is loaded. */ - if (script.tagName === 'SCRIPT' && (script.src || script.dataset.src) && isEntryScript(script)) { + if (isEntryScript(script)) { if (foundEntryScript) { throw new QiankunError( `You should not set multiply entry script in one entry html, but ${entry} has at least 2 entry scripts`, diff --git a/packages/sandbox/src/patchers/dynamicAppend/common.ts b/packages/sandbox/src/patchers/dynamicAppend/common.ts index 155bd808d..503c754bc 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/common.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/common.ts @@ -3,8 +3,8 @@ * @author Kuitos * @since 2019-10-21 */ -import type { ScriptTranspilerOpts } from '@qiankunjs/shared'; -import { Deferred, waitUntilSettled } from '@qiankunjs/shared'; +import { prepareScriptForQueue } from '@qiankunjs/shared'; +import type { AssetsTranspilerOpts, Deferred, ScriptTranspilerOpts } from '@qiankunjs/shared'; import { qiankunHeadTagName } from '../../consts'; import type { SandboxConfig } from './types'; @@ -184,41 +184,32 @@ export function getOverwrittenAppendChildOrInsertBefore( const externalSyncMode = scriptElement.hasAttribute('src') && !scriptElement.hasAttribute('async'); - let prevScriptTranspiledDeferred: Deferred | undefined; - let scriptTranspiledDeferred: Deferred | undefined; + let transformerOpts: AssetsTranspilerOpts = { + fetch, + sandbox, + rawNode: scriptElement, + }; + let queueScript: (script: HTMLScriptElement) => void | undefined; if (externalSyncMode) { - const dynamicScriptsLength = dynamicExternalSyncScriptElements.length; - const prevSyncScriptElement = dynamicScriptsLength - ? dynamicExternalSyncScriptElements[dynamicScriptsLength - 1] - : undefined; - prevScriptTranspiledDeferred = prevSyncScriptElement - ? scriptFetchedDeferredWeakMap.get(prevSyncScriptElement) - : undefined; - scriptTranspiledDeferred = new Deferred(); + const { scriptDeferred, prevScriptDeferred, queue } = prepareScriptForQueue( + dynamicExternalSyncScriptElements, + scriptFetchedDeferredWeakMap, + ); + transformerOpts = { + ...transformerOpts, + scriptTranspiledDeferred: scriptDeferred, + prevScriptTranspiledDeferred: prevScriptDeferred, + } as ScriptTranspilerOpts; + queueScript = queue; } - const transpiledScriptElement = nodeTransformer(scriptElement, location.href, { - fetch, - sandbox, - rawNode: scriptElement, - prevScriptTranspiledDeferred, - scriptTranspiledDeferred, - } as ScriptTranspilerOpts); + const transpiledScriptElement = nodeTransformer(scriptElement, location.href, transformerOpts); const result = appendChild.call(this, transpiledScriptElement, refChild) as T; - // Previously it was an external synchronous script, and after the transpile, there was no src attribute, indicating that the script needs to wait for the src to be filled - if (externalSyncMode && !transpiledScriptElement.hasAttribute('src')) { - dynamicExternalSyncScriptElements.push(transpiledScriptElement); - scriptFetchedDeferredWeakMap.set(transpiledScriptElement, scriptTranspiledDeferred!); - - // clear the memory regardless the script loaded or failed - void waitUntilSettled(scriptTranspiledDeferred!.promise).then(() => { - const scriptIndex = dynamicExternalSyncScriptElements.indexOf(transpiledScriptElement); - dynamicExternalSyncScriptElements.splice(scriptIndex, 1); - scriptFetchedDeferredWeakMap.delete(transpiledScriptElement); - }); + if (externalSyncMode) { + queueScript!(transpiledScriptElement); } return result; diff --git a/packages/shared/src/assets-transpilers/script.ts b/packages/shared/src/assets-transpilers/script.ts index f2cf99f0f..12898b6a9 100644 --- a/packages/shared/src/assets-transpilers/script.ts +++ b/packages/shared/src/assets-transpilers/script.ts @@ -120,7 +120,7 @@ export default function transpileScript( const codeFactory = beforeExecutedListenerScript + sandbox!.makeEvaluateFactory(code, src); if (syncMode) { - // if it's a sync script and there is a previous sync script, we should wait it to finish fetching + // if it's a sync script and there is a previous sync script, we should wait it until loaded to consistent with the browser behavior if (prevScriptTranspiledDeferred && !prevScriptTranspiledDeferred.isSettled()) { await waitUntilSettled(prevScriptTranspiledDeferred.promise); } @@ -129,7 +129,7 @@ export default function transpileScript( script.fetchPriority = 'high'; } - // change the script src to a blob url to make it execute in the sandbox + // change the script src to the blob url to make it execute in the sandbox script.src = URL.createObjectURL(new Blob([codeFactory], { type: 'text/javascript' })); window.addEventListener(beforeScriptExecuteEvent, function listener(evt: CustomEventInit) { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f20ff52b9..76d7c5c72 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -8,3 +8,4 @@ export * from './module-resolver'; export * from './common'; export * from './reporter'; export * from './fetch-utils/wrapFetchWithCache'; +export * from './script-queue'; diff --git a/packages/shared/src/script-queue/index.ts b/packages/shared/src/script-queue/index.ts new file mode 100644 index 000000000..e8fbb3f69 --- /dev/null +++ b/packages/shared/src/script-queue/index.ts @@ -0,0 +1,30 @@ +import { Deferred, waitUntilSettled } from '../utils'; + +export function prepareScriptForQueue( + scriptQueue: HTMLScriptElement[], + scriptDeferredWeakMap: WeakMap>, +): { scriptDeferred: Deferred; prevScriptDeferred?: Deferred; queue: (script: HTMLScriptElement) => void } { + const queueLength = scriptQueue.length; + const prevScript = queueLength ? scriptQueue[scriptQueue.length - 1] : undefined; + const prevScriptDeferred = prevScript ? scriptDeferredWeakMap.get(prevScript) : undefined; + const scriptDeferred = new Deferred(); + + return { + scriptDeferred, + prevScriptDeferred, + queue: (script: HTMLScriptElement) => { + // the script have no src attribute, indicating that the script needs to wait for the src to be filled + if (!script.hasAttribute('src')) { + scriptQueue.push(script); + scriptDeferredWeakMap.set(script, scriptDeferred); + + // clear the memory regardless the script loaded or failed + void waitUntilSettled(scriptDeferred.promise).then(() => { + const scriptIndex = scriptQueue.indexOf(script); + scriptQueue.splice(scriptIndex, 1); + scriptDeferredWeakMap.delete(script); + }); + } + }, + }; +} diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 5f23e05dd..8dd73b0df 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -51,7 +51,7 @@ export function getEntireUrl(uri: string, baseURI: string): string { } /** - * Check if the running environment support qiankun3.0 + * Check if the running environment support qiankun 3.0 * */ export function isRuntimeCompatible(): boolean {