Skip to content

Commit

Permalink
feat: support defer scripts and keep the executing order to consist w…
Browse files Browse the repository at this point in the history
…ith browser (#2811)
  • Loading branch information
kuitos authored Nov 15, 2023
1 parent 3f7e7f0 commit 98b071b
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 46 deletions.
7 changes: 7 additions & 0 deletions .changeset/hungry-needles-doubt.md
Original file line number Diff line number Diff line change
@@ -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
59 changes: 46 additions & 13 deletions packages/loader/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,6 +27,16 @@ export type LoaderOpts = {
nodeTransformer?: NodeTransformer;
} & Omit<BaseTranspilerOpts, 'moduleResolver'> & { 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
Expand All @@ -34,9 +49,6 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: L
if (res.body) {
let foundEntryScript = false;
const entryScriptLoadedDeferred = new Deferred<T | void>();
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) {
Expand All @@ -47,6 +59,8 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: L
entryScriptLoadedDeferred.resolve({} as T);
}
};
const deferScripts: HTMLScriptElement[] = [];
const deferScriptDeferredWeakMap = new WeakMap<HTMLScriptElement, Deferred<void>>();

let readableStream = res.body.pipeThrough(new TextDecoderStream());

Expand All @@ -69,22 +83,41 @@ export async function loadEntry<T>(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`,
Expand Down
51 changes: 21 additions & 30 deletions packages/sandbox/src/patchers/dynamicAppend/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -184,41 +184,32 @@ export function getOverwrittenAppendChildOrInsertBefore(

const externalSyncMode = scriptElement.hasAttribute('src') && !scriptElement.hasAttribute('async');

let prevScriptTranspiledDeferred: Deferred<void> | undefined;
let scriptTranspiledDeferred: Deferred<void> | 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<void>();
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;
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/assets-transpilers/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './module-resolver';
export * from './common';
export * from './reporter';
export * from './fetch-utils/wrapFetchWithCache';
export * from './script-queue';
30 changes: 30 additions & 0 deletions packages/shared/src/script-queue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Deferred, waitUntilSettled } from '../utils';

export function prepareScriptForQueue(
scriptQueue: HTMLScriptElement[],
scriptDeferredWeakMap: WeakMap<HTMLScriptElement, Deferred<void>>,
): { scriptDeferred: Deferred<void>; prevScriptDeferred?: Deferred<void>; 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<void>();

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);
});
}
},
};
}
2 changes: 1 addition & 1 deletion packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 98b071b

Please sign in to comment.