Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(loader): add lru cache for assets fetch by default #2794

Merged
merged 8 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/itchy-snakes-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"qiankun": patch
"@qiankunjs/sandbox": patch
---

feat(loader): add lru cache for assets fetch by default
43 changes: 40 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- 1.x

jobs:
check:
build-check-and-lint:
runs-on: ubuntu-latest

strategy:
Expand All @@ -29,6 +29,43 @@ jobs:
cache: "pnpm"

- run: corepack enable # https://nodejs.org/api/corepack.html

- run: pnpm install

- name: TS Build Check
run: pnpm run build

- name: Run eslint
run: pnpm run eslint

- name: Run prettier
run: pnpm run prettier:check

- name: Doc Build check
run: pnpm run docs:build

unit-test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [lts/*, latest]

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/[email protected]

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"

- run: corepack enable # https://nodejs.org/api/corepack.html

- run: pnpm install
- run: pnpm run ci
# - run: pnpm run docs:build #执行报错,暂时关闭

- name: Run unit test
run: pnpm run test
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@
"build": "pnpm -r run build",
"prerelease:alpha": "changeset pre enter alpha && changeset && changeset version",
"release:alpha": "pnpm run build && changeset publish && changeset pre exit",
"lint": "eslint packages/",
"eslint": "eslint packages/",
"prettier": "prettier --write .",
"prettier:check": "prettier -c .",
"docs:dev": "dumi dev",
"docs:build": "dumi build",
"ci": "pnpm run build && pnpm run lint && pnpm run prettier:check",
"ci:publish": "pnpm run build && changeset publish",
"ci": "pnpm run build && pnpm run eslint && pnpm run prettier:check",
"ci:publish": "changeset publish",
"test": "pnpm -r run test",
"prepare": "husky install && dumi setup"
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@edge-runtime/vm": "^3.1.7",
"@types/lodash": "^4.14.200",
"@types/node": "^18.18.8",
"@typescript-eslint/eslint-plugin": "^6.9.1",
Expand All @@ -34,6 +35,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-formatter-pretty": "^5.0.0",
"father": "^4.3.6",
"happy-dom": "^12.10.3",
"husky": "^8.0.3",
"lint-staged": "^9.5.0",
"prettier": "^3.0.3",
Expand Down
5 changes: 1 addition & 4 deletions packages/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"sideEffects": false,
"scripts": {
"build": "father build",
"test": "cross-env NODE_ENV=test vitest",
"bench": "npm run build && tachometer ./benchmarks/parser/tern/import-html-entry.html ./benchmarks/parser/tern/parser.html --timeout=1"
},
"author": "Kuitos",
Expand All @@ -26,7 +25,5 @@
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"files": [
"dist"
]
"files": ["dist"]
}
13 changes: 3 additions & 10 deletions packages/qiankun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,13 @@
"sideEffects": false,
"scripts": {
"build": "father build",
"lint": "yarn lint:js && yarn lint:prettier",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:fix": "yarn lint:js -- --fix",
"lint:prettier": "prettier -c ./src/**/*",
"prettier": "prettier --write ./src/**/*",
"ci": "yarn lint && yarn build && yarn test",
"test": "cross-env NODE_ENV=test vitest"
"test": "vitest --run"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kuitos/qiankun.git"
},
"files": [
"dist"
],
"files": ["dist"],
"author": "Kuitos",
"license": "MIT",
"bugs": {
Expand All @@ -43,6 +35,7 @@
"@qiankunjs/sandbox": "workspace:^",
"@qiankunjs/shared": "workspace:^",
"lodash": "^4.17.11",
"lru-cache": "^10.0.1",
"single-spa": "^6.0.0-beta.3"
},
"devDependencies": {
Expand Down
73 changes: 73 additions & 0 deletions packages/qiankun/src/core/__tests__/wrapFetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @vitest-environment edge-runtime

import { describe, expect, it, vi } from 'vitest';
import { wrapFetchWithLruCache } from '../wrapFetch';

const slogan = 'Hello Qiankun 3.0';

describe('wrapFetchWithLruCache', () => {
it('should just call fetch once while multiple request invoked parallel', () => {
const fetch = vi.fn(() => {
return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' }));
});
const wrappedFetch = wrapFetchWithLruCache(fetch);
const url = 'https://success.qiankun.org';
wrappedFetch(url);
wrappedFetch(url);
wrappedFetch(url);

expect(fetch).toHaveBeenCalledOnce();
});

it('should support read response body as a stream multi times', async () => {
const fetch = vi.fn(() => {
return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' }));
});
const wrappedFetch = wrapFetchWithLruCache(fetch);

const url = 'https://stream.qiankun.org';
const response1 = await wrappedFetch(url);
const bodyStream1 = response1.body!;
expect(bodyStream1.locked).toBe(false);
const reader = bodyStream1.getReader();
const { done, value } = await reader.read();
expect(done).toBe(false);
expect(value).toStrictEqual(new TextEncoder().encode('Hello Qiankun 3.0'));
expect(bodyStream1.locked).toBe(true);

const response2 = await wrappedFetch(url);
const bodyStream2 = response2.body!;
expect(bodyStream2.locked).toBe(false);
});

it('should clear cache while respond error with invalid status code', async () => {
const fetch = vi.fn(() => {
return Promise.resolve(new Response(slogan, { status: 400 }));
});
const wrappedFetch = wrapFetchWithLruCache(fetch);
const url = 'https://errorStatusCode.qiankun.org';

const response1 = await wrappedFetch(url);
const result1 = await response1.text();
expect(result1).toBe(slogan);

const response2 = await wrappedFetch(url);
const result2 = await response2.text();
expect(result2).toBe(slogan);

expect(fetch).toHaveBeenCalledTimes(2);
});

it('should clear cache while respond error', async () => {
const fetch = vi.fn(() => {
return Promise.reject(new Error('error'));
});
const wrappedFetch = wrapFetchWithLruCache(fetch);

const url = 'https://error.qiankun.org';
await expect(wrappedFetch(url)).rejects.toThrow('error');
await expect(wrappedFetch(url)).rejects.toThrow('error');

expect(fetch).toHaveBeenCalledTimes(2);
});
});
10 changes: 7 additions & 3 deletions packages/qiankun/src/core/loadApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
toArray,
} from '../utils';
import { version } from '../version';
import { wrapFetchWithLruCache } from './wrapFetch';

export type ParcelConfigObjectGetter = (remountContainer: HTMLElement) => ParcelConfigObject;

Expand All @@ -28,7 +29,8 @@ export default async function loadApp<T extends ObjectType>(
lifeCycles?: LifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
const { name: appName, entry, container } = app;
const { fetch = window.fetch, sandbox, globalContext = window, streamTransformer } = configuration || {};
const { fetch = window.fetch, sandbox, globalContext = window, ...restConfiguration } = configuration || {};
const fetchWithLruCache = wrapFetchWithLruCache(fetch);

const markName = `[qiankun] App ${appName} Loading`;
if (process.env.NODE_ENV === 'development') {
Expand All @@ -47,6 +49,8 @@ export default async function loadApp<T extends ObjectType>(
const sandboxContainer = createSandboxContainer(appName, () => sandboxMicroAppContainer, {
globalContext,
extraGlobals: {},
fetch: fetchWithLruCache,
nodeTransformer: restConfiguration.nodeTransformer,
});

sandboxInstance = sandboxContainer.instance;
Expand All @@ -56,7 +60,7 @@ export default async function loadApp<T extends ObjectType>(
unmountSandbox = () => sandboxContainer.unmount();
}

const containerOpts: LoaderOpts = { fetch, sandbox: sandboxInstance, streamTransformer };
const containerOpts: LoaderOpts = { fetch: fetchWithLruCache, sandbox: sandboxInstance, ...restConfiguration };

const lifecyclesPromise = loadEntry<MicroAppLifeCycles>(entry, sandboxMicroAppContainer, containerOpts);

Expand Down Expand Up @@ -109,7 +113,7 @@ export default async function loadApp<T extends ObjectType>(
if (mountTimes > 1) {
initContainer(mountContainer, appName, sandbox);
// html scripts should be removed to avoid repeatedly execute
const htmlString = await getPureHTMLStringWithoutScripts(entry, fetch);
const htmlString = await getPureHTMLStringWithoutScripts(entry, fetchWithLruCache);
await loadEntry(htmlString, mountContainer, containerOpts);
}
},
Expand Down
61 changes: 61 additions & 0 deletions packages/qiankun/src/core/wrapFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @author Kuitos
* @since 2023-11-06
*/
import { once } from 'lodash';
import { LRUCache } from 'lru-cache';

type Fetch = typeof window.fetch;

const getCacheKey = (input: Parameters<Fetch>[0]): string => {
return typeof input === 'string' ? input : 'url' in input ? input.url : input.href;
};

const getGlobalCache = once(() => {
return new LRUCache<string, Promise<Response>>({
max: 50,
// 10 minutes
ttl: 10 * 60 * 1000,
});
});

const isValidaResponse = (status: number): boolean => {
return status >= 200 && status < 400;
};

export const wrapFetchWithLruCache: (fetch: Fetch) => Fetch = (fetch) => {
const lruCache = getGlobalCache();

const cachedFetch: Fetch = (input, init) => {
const fetchInput = input as Parameters<Fetch>[0];
const cacheKey = getCacheKey(fetchInput);
const wrapFetchPromise = async (promise: Promise<Response>): Promise<Response> => {
try {
const res = await promise;

const { status } = res;
if (!isValidaResponse(status)) {
lruCache.delete(cacheKey);
}

// must clone the response as one response body can only be read once as a stream
return res.clone();
} catch (e) {
lruCache.delete(cacheKey);
throw e;
}
};

const cachedFetchPromise = lruCache.get(cacheKey);
if (cachedFetchPromise) {
return wrapFetchPromise(cachedFetchPromise);
}

const fetchPromise = fetch(fetchInput, init);
lruCache.set(cacheKey, fetchPromise);

return wrapFetchPromise(fetchPromise);
};

return cachedFetch;
};
2 changes: 1 addition & 1 deletion packages/qiankun/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type RegistrableApp<T extends ObjectType> = LoadableApp<T> & {
activeRule: RegisterApplicationConfig['activeWhen'];
};

export type AppConfiguration = Partial<Pick<LoaderOpts, 'fetch' | 'streamTransformer'>> & {
export type AppConfiguration = Partial<Pick<LoaderOpts, 'fetch' | 'streamTransformer' | 'nodeTransformer'>> & {
sandbox?: boolean;
globalContext?: WindowProxy;
};
Expand Down
7 changes: 2 additions & 5 deletions packages/sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
"types": "./dist/esm/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "father build",
"test": "cross-env NODE_ENV=test vitest --config ../../vitest.config.ts"
"build": "father build"
},
"files": [
"dist"
],
"files": ["dist"],
"author": "Kuitos",
"license": "MIT",
"dependencies": {
Expand Down
7 changes: 0 additions & 7 deletions packages/sandbox/src/__test__/index.test.ts

This file was deleted.

9 changes: 5 additions & 4 deletions packages/sandbox/src/core/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @since 2019-04-11
*/
import { patchAtBootstrapping, patchAtMounting } from '../../patchers';
import type { SandboxConfig } from '../../patchers/dynamicAppend/types';
import type { Free, Rebuild } from '../../patchers/types';
import type { Endowments } from '../membrane';
import { StandardSandbox } from './StandardSandbox';
Expand Down Expand Up @@ -32,9 +33,9 @@ export function createSandboxContainer(
opts: {
globalContext?: WindowProxy;
extraGlobals?: Endowments;
},
} & Pick<SandboxConfig, 'fetch' | 'nodeTransformer'>,
) {
const { globalContext, extraGlobals = {} } = opts;
const { globalContext, extraGlobals = {}, ...sandboxCfg } = opts;
let sandbox: Sandbox;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (window.Proxy) {
Expand All @@ -45,7 +46,7 @@ export function createSandboxContainer(
}

// some side effect could be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFrees = patchAtBootstrapping(appName, getContainer, sandbox);
const bootstrappingFrees = patchAtBootstrapping(appName, getContainer, { sandbox, ...sandboxCfg });
// mounting frees are one-off and should be re-init at every mounting time
let mountingFrees: Free[] = [];

Expand Down Expand Up @@ -77,7 +78,7 @@ export function createSandboxContainer(

/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
mountingFrees = patchAtMounting(appName, getContainer, sandbox);
mountingFrees = patchAtMounting(appName, getContainer, { sandbox, ...sandboxCfg });

/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
// 存在 rebuilds 则表明有些副作用需要重建
Expand Down
Loading