Skip to content

Commit

Permalink
🐛 compatible with webpack chunk cache logic (#2730)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos authored Oct 19, 2023
1 parent 1d14f74 commit 76b6bff
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 30 deletions.
8 changes: 8 additions & 0 deletions .changeset/tough-phones-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@qiankunjs/loader": patch
"qiankun": patch
"@qiankunjs/sandbox": patch
"@qiankunjs/shared": patch
---

🐛 compatible with webpack chunk cache logic
39 changes: 31 additions & 8 deletions packages/loader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { qiankunHeadTagName } from '@qiankunjs/sandbox';
import type { BaseTranspilerOpts } from '@qiankunjs/shared';
import { Deferred, moduleResolver as defaultModuleResolver, QiankunError, transpileAssets } from '@qiankunjs/shared';
import {
Deferred,
isValidJavaScriptType,
moduleResolver as defaultModuleResolver,
QiankunError,
transpileAssets,
} from '@qiankunjs/shared';
import { TagTransformStream } from './TagTransformStream';
import { isUrlHasOwnProtocol } from './utils';
import WritableDOMStream from './writable-dom';
Expand All @@ -26,7 +32,11 @@ export type ImportOpts = {
* @param container
* @param opts
*/
export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: ImportOpts): Promise<T | void> {
export async function loadEntry<T>(
entry: Entry,
container: HTMLElement,
opts: ImportOpts,
): Promise<[Promise<void>, Promise<T | void>]> {
const {
fetch,
nodeTransformer = transpileAssets,
Expand All @@ -39,8 +49,9 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
const res = isUrlHasOwnProtocol(entry) ? await fetch(entry) : new Response(entry);
if (res.body) {
let noExternalScript = true;
const firstScriptStartLoadDeferred = new Deferred<void>();
const entryScriptLoadedDeferred = new Deferred<T | void>();
const entryDocumentLoadedDeferred = new Deferred<void>();
const entryHTMLLoadedDeferred = new Deferred<void>();

void res.body
.pipeThrough(new TextDecoderStream())
Expand All @@ -66,6 +77,15 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
});

const script = transformedNode as unknown as HTMLScriptElement;

if (
firstScriptStartLoadDeferred.status === 'pending' &&
script.tagName === 'SCRIPT' &&
isValidJavaScriptType(script.type)
) {
firstScriptStartLoadDeferred.resolve();
}

/*
* 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.
Expand All @@ -74,10 +94,13 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
if (script.tagName === 'SCRIPT' && (script.src || script.dataset.src)) {
noExternalScript = false;

/**
* Script with entry attribute or the last script is the entry script
*/
const isEntryScript = async () => {
if (script.hasAttribute('entry')) return true;

await entryDocumentLoadedDeferred.promise;
await entryHTMLLoadedDeferred.promise;

const scripts = container.querySelectorAll('script[src]');
const lastScript = scripts[scripts.length - 1];
Expand All @@ -88,7 +111,7 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
'load',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async () => {
if (await isEntryScript()) {
if (entryScriptLoadedDeferred.status === 'pending' && (await isEntryScript())) {
// the latest set prop is the entry script exposed global variable
if (sandbox?.latestSetProp) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
Expand All @@ -105,7 +128,7 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
'error',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async (evt) => {
if (await isEntryScript()) {
if (entryScriptLoadedDeferred.status === 'pending' && (await isEntryScript())) {
entryScriptLoadedDeferred.reject(
new QiankunError(`entry ${entry} loading failed as entry script trigger error -> ${evt.message}`),
);
Expand All @@ -119,14 +142,14 @@ export async function loadEntry<T>(entry: Entry, container: HTMLElement, opts: I
}),
)
.then(() => {
entryDocumentLoadedDeferred.resolve();
entryHTMLLoadedDeferred.resolve();

if (noExternalScript) {
entryScriptLoadedDeferred.resolve();
}
});

return entryScriptLoadedDeferred.promise;
return [firstScriptStartLoadDeferred.promise, entryScriptLoadedDeferred.promise];
}

throw new QiankunError(`entry ${entry} response body is empty!`);
Expand Down
15 changes: 9 additions & 6 deletions packages/qiankun/src/core/loadApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export default async function loadApp<T extends ObjectType>(
unmountSandbox = () => sandboxContainer.unmount();
}

const containerOpts: ImportOpts = { fetch, sandbox: sandboxInstance };

const [firstScriptStartLoadPromise, entryScriptLoadedPromise] = await loadEntry<MicroAppLifeCycles>(
entry,
microAppContainer,
containerOpts,
);
const assetPublicPath = calcPublicPath(entry);
const {
beforeUnmount = [],
Expand All @@ -66,15 +73,11 @@ export default async function loadApp<T extends ObjectType>(
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) =>
concat((v1 ?? []) as LifeCycleFn<T>, (v2 ?? []) as LifeCycleFn<T>),
);

await firstScriptStartLoadPromise;
await execHooksChain(toArray(beforeLoad), app, global);

const containerOpts: ImportOpts = { fetch, sandbox: sandboxInstance };

const lifecycles = await loadEntry<MicroAppLifeCycles>(entry, microAppContainer, containerOpts);

const lifecycles = await entryScriptLoadedPromise;
if (!lifecycles) throw new QiankunError(`${appName} entry ${entry} load failed as it not export lifecycles`);

const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
lifecycles,
appName,
Expand Down
4 changes: 2 additions & 2 deletions packages/sandbox/src/core/sandbox/StandardSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { without } from 'lodash';
import { hasOwnProperty } from '@qiankunjs/shared';
import { without } from 'lodash';
import { Compartment } from '../compartment';
import type { Endowments } from '../membrane';
import { Membrane } from '../membrane';
import { globals as constantGlobals } from './globals';
import type { Sandbox } from './types';
import { SandboxType } from './types';
import { globals as constantGlobals } from './globals';

const whitelistBOMAPIs = ['requestAnimationFrame', 'cancelAnimationFrame'];

Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/assets-transpilers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ export function transpileAssets<T extends Node>(node: T, baseURI: string, opts:
}

export type * from './types';

export { isValidJavaScriptType } from './utils';
18 changes: 6 additions & 12 deletions packages/shared/src/assets-transpilers/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,7 @@ import type { MatchResult } from '../module-resolver';
import { getEntireUrl } from '../utils';
import type { AssetsTranspilerOpts } from './types';
import { Mode } from './types';
import { createReusingObjectUrl } from './utils';

const isValidJavaScriptType = (type?: string): boolean => {
const handleTypes = [
'text/javascript',
'module',
'application/javascript',
'text/ecmascript',
'application/ecmascript',
];
return !type || handleTypes.indexOf(type) !== -1;
};
import { createReusingObjectUrl, isValidJavaScriptType } from './utils';

const getCredentials = (crossOrigin: string | null): RequestInit['credentials'] | undefined => {
switch (crossOrigin) {
Expand Down Expand Up @@ -92,6 +81,11 @@ export default function transpileScript(
const srcAttribute = script.getAttribute('src');
const { sandbox, fetch } = opts;

// To prevent webpack from skipping reload logic and causing the js not to re-execute when a micro app is loaded multiple times, the data-webpack attribute of the script must be removed.
// see https://github.com/webpack/webpack/blob/1f13ff9fe587e094df59d660b4611b1bd19aed4c/lib/runtime/LoadScriptRuntimeModule.js#L131-L136
// FIXME We should determine whether the current micro application is being loaded for the second time. If not, this removal should not be performed.
script.removeAttribute('data-webpack');

const { mode, result } = preTranspile(
{
src: srcAttribute || undefined,
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/assets-transpilers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ export const createReusingObjectUrl = memoize(
},
(src, url, type) => `${src}#${url}#${type}`,
);

export const isValidJavaScriptType = (type?: string): boolean => {
const handleTypes = [
'text/javascript',
'module',
'application/javascript',
'text/ecmascript',
'application/ecmascript',
];
return !type || handleTypes.indexOf(type) !== -1;
};
12 changes: 10 additions & 2 deletions packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ export const hasOwnProperty = (caller: unknown, p: PropertyKey) => Object.protot
export class Deferred<T> {
promise: Promise<T>;

status: 'pending' | 'fulfilled' | 'rejected' = 'pending';

resolve!: (value: T | PromiseLike<T>) => void;

reject!: (reason?: unknown) => void;

constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
this.resolve = (value: T | PromiseLike<T>) => {
this.status = 'fulfilled';
resolve(value);
};
this.reject = (reason?: unknown) => {
this.status = 'rejected';
reject(reason);
};
});
}
}
Expand Down

0 comments on commit 76b6bff

Please sign in to comment.