Skip to content

Commit

Permalink
Feat/prefetch (#278)
Browse files Browse the repository at this point in the history
* feat: 🎸 prefetch

* feat: 🎸 support AppRouter

* feat: 🎸 Optimize the details
  • Loading branch information
maoxiaoke committed Mar 26, 2021
1 parent 622b0a6 commit 3f4ec98
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 25 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/icestark/releases) for what has changed in each version of icestark.

## 2.3.0

- [feat] support `prefetch` sub-application, which let your micro application fly. ([#188](https://github.com/ice-lab/icestark/issues/188))

## 2.2.2

- [fix] `basename` of `AppRouter` makes effect. ([#241](https://github.com/ice-lab/icestark/issues/241))
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark",
"version": "2.2.2",
"version": "2.3.0",
"description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
"scripts": {
"install:deps": "rm -rf node_modules && rm -rf ./packages/*/node_modules && yarn install && lerna exec -- npm install",
Expand Down
43 changes: 41 additions & 2 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import appHistory from './appHistory';
import renderComponent from './util/renderComponent';
import { ICESTSRK_ERROR, ICESTSRK_NOT_FOUND } from './util/constant';
import { setCache } from './util/cache';
import start, { unload, Fetch, defaultFetch } from './start';
import start, { unload, Fetch, defaultFetch, Prefetch } from './start';
import { matchActivePath, PathData, addLeadingSlash } from './util/matchPath';
import { AppConfig } from './apps';
import { doPrefetch } from './util/prefetch';

type RouteType = 'pushState' | 'replaceState';

Expand All @@ -29,6 +30,7 @@ export interface AppRouterProps {
) => boolean;
basename?: string;
fetch?: Fetch;
prefetch?: Prefetch;
}

interface AppRouterState {
Expand Down Expand Up @@ -68,15 +70,22 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
onAppLeave: () => {},
basename: '',
fetch: defaultFetch,
prefetch: false,
};

constructor(props: AppRouterProps) {
constructor(props) {
super(props);
this.state = {
url: location.href,
appLoading: '',
started: false,
};

const { fetch, prefetch: strategy, children } = props;

if (strategy) {
this.prefetch(strategy, children, fetch);
}
}

componentDidMount() {
Expand Down Expand Up @@ -109,6 +118,36 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
this.setState({ started: false });
}

/**
* prefetch for resources.
* no worry to excute `prefetch` many times, for all prefetched resources have been cached, and never request twice.
*/
prefetch = (strategy: Prefetch, children: React.ReactNode, fetch = window.fetch) => {
const apps: AppConfig[] = React.Children
/**
* we can do prefetch for url, entry and entryContent.
*/
.map(children, childElement => {
if (React.isValidElement(childElement)) {
const { url, entry, entryContent, name, path } = childElement.props as AppRouteProps;
if (url || entry || entryContent) {
return {
...childElement.props,
/**
* name of AppRoute may be not provided, use `path` instead.
*/
name: name || converArray2String(path),
};
}

}
return false;
})
.filter(Boolean);

doPrefetch(apps, strategy, fetch);
}

/**
* Trigger Error
*/
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { default as AppRoute } from './AppRoute';
export { default as AppLink } from './AppLink';
export { default as appHistory } from './appHistory';
export { createMicroApp, registerMicroApps, unmountMicroApp, unloadMicroApp, removeMicroApps, AppConfig } from './apps';
export { prefetchApps } from './util/prefetch';
export { default as start } from './start';
18 changes: 17 additions & 1 deletion src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { AppConfig, getMicroApps, createMicroApp, unmountMicroApp, clearMicroApps } from './apps';
import { emptyAssets, recordAssets } from './util/handleAssets';
import { LOADING_ASSETS, MOUNTED } from './util/constant';
import { doPrefetch } from './util/prefetch';

if (!window?.fetch) {
throw new Error('[icestark] window.fetch not found, you need polyfill it');
Expand All @@ -19,6 +20,10 @@ if (!window?.fetch) {
export const defaultFetch = window?.fetch.bind(window);

export type Fetch = typeof window.fetch | ((url: string) => Promise<Response>);
export type Prefetch =
| boolean
| string[]
| ((app: AppConfig) => boolean);

export interface StartConfiguration {
shouldAssetsRemove?: (
Expand All @@ -40,6 +45,7 @@ export interface StartConfiguration {
onActiveApps?: (appConfigs: AppConfig[]) => void;
reroute?: (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange') => void;
fetch?: Fetch;
prefetch?: Prefetch;
}

const globalConfiguration: StartConfiguration = {
Expand All @@ -53,6 +59,7 @@ const globalConfiguration: StartConfiguration = {
onActiveApps: () => {},
reroute,
fetch: defaultFetch,
prefetch: false,
};

interface OriginalStateFunction {
Expand Down Expand Up @@ -84,7 +91,7 @@ export function reroute (url: string, type: RouteType | 'init' | 'popstate'| 'ha
// trigger onRouteChange when url is changed
if (lastUrl !== url) {
globalConfiguration.onRouteChange(url, pathname, query, hash, type);

const unmountApps = [];
const activeApps = [];
getMicroApps().forEach((microApp: AppConfig) => {
Expand Down Expand Up @@ -191,11 +198,20 @@ function start(options?: StartConfiguration) {
return;
}
started = true;

recordAssets();

// update globalConfiguration
Object.keys(options || {}).forEach((configKey) => {
globalConfiguration[configKey] = options[configKey];
});

const { prefetch, fetch } = globalConfiguration;
if (prefetch) {
doPrefetch(getMicroApps(), prefetch, fetch);
}

// hajack history & eventListener
hijackHistory();
hijackEventListener();

Expand Down
80 changes: 59 additions & 21 deletions src/util/handleAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const COMMENT_REGEX = /<!--.*?-->/g;
const EMPTY_STRING = '';
const STYLESHEET_LINK_TYPE = 'stylesheet';

const cachedScriptsContent: object = {};
const cachedStyleContent: object = {};

export enum AssetTypeEnum {
INLINE = 'inline',
EXTERNAL = 'external',
Expand Down Expand Up @@ -53,7 +56,7 @@ export function appendCSS(
asset: string | Asset,
id: string,
): Promise<string> {
return new Promise<string>((resolve, reject) => {
return new Promise<string>(async (resolve, reject) => {
const { type, content } = (asset as Asset);
if (!root) reject(new Error(`no root element for css assert: ${content || asset}`));

Expand All @@ -65,23 +68,43 @@ export function appendCSS(
return;
}

const element: HTMLLinkElement = document.createElement('link');
element.setAttribute(PREFIX, DYNAMIC);
element.id = id;
element.rel = 'stylesheet';
element.href = content || (asset as string);
/**
* if external resource is cached by prefetch, use cached content instead.
* For cachedStyleContent may fail to fetch (cors, and so on),recover to original way
*/
let useExternalLink = true;
if (type && type === AssetTypeEnum.EXTERNAL && cachedStyleContent[content]) {
try {
const styleElement: HTMLStyleElement = document.createElement('style');
styleElement.innerHTML = await cachedStyleContent[content];
root.appendChild(styleElement);
useExternalLink = false;
resolve();
} catch (e) {
useExternalLink = true;
}
}

element.addEventListener(
'error',
() => {
error(`css asset loaded error: ${content || asset}`);
return resolve();
},
false,
);
element.addEventListener('load', () => resolve(), false);
if (useExternalLink) {
const element: HTMLLinkElement = document.createElement('link');
element.setAttribute(PREFIX, DYNAMIC);
element.id = id;
element.rel = 'stylesheet';
element.href = content || (asset as string);

element.addEventListener(
'error',
() => {
error(`css asset loaded error: ${content || asset}`);
return resolve();
},
false,
);
element.addEventListener('load', () => resolve(), false);

root.appendChild(element);
}

root.appendChild(element);
});
}

Expand Down Expand Up @@ -146,8 +169,6 @@ export function getUrlAssets(url: string | string[]) {
return { jsList, cssList };
}

const cachedScriptsContent: object = {};

export function fetchScripts(jsList: Asset[], fetch = defaultFetch ) {
return Promise.all(jsList.map((asset) => {
const { type, content } = asset;
Expand All @@ -159,6 +180,20 @@ export function fetchScripts(jsList: Asset[], fetch = defaultFetch ) {
}
}));
}

// for prefetch
export function fetchStyles(cssList: Asset[], fetch = defaultFetch) {
return Promise.all(
cssList.map((asset) => {
const { type, content} = asset;
if (type === AssetTypeEnum.INLINE) {
return content;
}
return cachedStyleContent[content] || (cachedStyleContent[content] = fetch(content).then(res => res.text()));
})
);
}

export async function appendAssets(assets: Assets, sandbox?: Sandbox, fetch = defaultFetch) {
await loadAndAppendCssAssets(assets);
await loadAndAppendJsAssets(assets, sandbox, fetch);
Expand Down Expand Up @@ -300,10 +335,10 @@ export async function getEntryAssets({
entry,
entryContent,
assetsCacheKey,
href,
href = location.href,
fetch = defaultFetch,
}: {
root: HTMLElement | ShadowRoot;
root?: HTMLElement | ShadowRoot;
entry?: string;
entryContent?: string;
assetsCacheKey: string;
Expand All @@ -330,7 +365,10 @@ export async function getEntryAssets({
}

const { html } = cachedContent;
root.appendChild(html);

if (root) {
root.appendChild(html);
}

return cachedContent.assets;
}
Expand Down
Loading

0 comments on commit 3f4ec98

Please sign in to comment.