Skip to content

Commit

Permalink
feat(Link): add prefetch
Browse files Browse the repository at this point in the history
  • Loading branch information
harrytothemoon authored Jun 22, 2022
1 parent 8c1ef4d commit 5c04f5b
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 6 deletions.
11 changes: 10 additions & 1 deletion packages/platform-shared/src/runtime/helper/getAppData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type IAppData<Data = {}, appState = any> = {
routeProps?: { [x: string]: any };
loadersData?: { [x: string]: any };
appState?: appState;
clientManifestPath: Record<string, string[]>;
} & {
[K in keyof Data]: Data[K];
};
Expand All @@ -25,12 +26,20 @@ export function getAppData(): IAppData {
return appData;
}

if (typeof window === 'undefined') {
return {
ssr: false,
clientManifestPath: {}
};
}

const el = document.getElementById(CLIENT_APPDATA_ID);
if (!el || !el.textContent) {
return {
ssr: false,
pageData: {},
loadersData: {}
loadersData: {},
clientManifestPath: {}
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IManifest } from '@shuvi/toolpack/lib/webpack/types';

export default function generateClientManifestPath(
assetMap: IManifest,
getAssetPublicUrl: Function
): Record<string, string[]> {
let clientManifestPath: Record<string, string[]> = {};
const loadable = assetMap.loadble;

for (const path_full in loadable) {
let path_short: string = path_full
.replace(/^.*\/pages\//, '/')
.replace(/\.js.*|\.ts.*|\?.*$/, '');
if (path_short === '/index') path_short = '/';
clientManifestPath[path_short] = assetMap.loadble[path_full].files.map(
file => getAssetPublicUrl(file)
);
}
return clientManifestPath;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
documentPath
} from '@shuvi/service/lib/resources';
import { parseTemplateFile, renderTemplate } from '../viewTemplate';
import generateClientManifestPath from '../generateClientManifestPath';
import { tag, stringifyTag, stringifyAttrs } from './htmlTag';
import { IDocumentProps, ITemplateData } from './types';

Expand Down Expand Up @@ -196,6 +197,10 @@ export abstract class BaseRenderer {
}

protected _getInlineAppData(appData: IAppData): IHtmlTag {
appData.clientManifestPath = generateClientManifestPath(
clientManifest,
this._serverPluginContext.getAssetPublicUrl
);
const data = JSON.stringify(appData);
return tag(
'script',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export class SpaRenderer extends BaseRenderer {
const appData: IAppData = {
pageData: {},
ssr: serverPluginContext.config.ssr,
loadersData: {}
loadersData: {},
clientManifestPath: {}
};
return {
htmlAttrs: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export class SsrRenderer extends BaseRenderer {
const appData: IAppData = {
...result.appData,
pageData,
ssr: serverPluginContext.config.ssr
ssr: serverPluginContext.config.ssr,
clientManifestPath: {}
};
appData.runtimeConfig = getPublicRuntimeConfig() || {};

Expand Down
6 changes: 5 additions & 1 deletion packages/platform-web/src/lib/targets/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ const webReactMainPlugin = createPlugin({
{
source: resolveLib('@shuvi/router-react'),
exported:
'{ useParams, useRouter, useCurrentRoute, Link, RouterView, withRouter }'
'{ useParams, useRouter, useCurrentRoute, RouterView, withRouter }'
},
{
source: resolveAppFile('react/Link'),
exported: '{ Link }'
}
]
});
Expand Down
77 changes: 77 additions & 0 deletions packages/platform-web/src/shuvi-app/react/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import {
Link as LinkFromRouterReact,
useHref,
LinkProps
} from '@shuvi/router-react';
import useIntersection from './utils/useIntersection';
import { prefetchFn, isAbsoluteUrl } from './utils/prefetch';
import { getAppData } from '@shuvi/platform-shared/lib/runtime';

export const Link = function LinkWithPrefetch({
prefetch,
onMouseEnter,
to,
ref,
...rest
}: LinkWrapperProps) {
const { clientManifestPath } = getAppData();
const href = useHref(to);
const previousHref = React.useRef<string>(href);
const [setIntersectionRef, isVisible, resetVisible] = useIntersection({});
const setRef = React.useCallback(
(el: Element) => {
// Before the link getting observed, check if visible state need to be reset
if (previousHref.current !== href) {
resetVisible();
previousHref.current = href;
}

if (prefetch !== false) setIntersectionRef(el);

if (ref) {
if (typeof ref === 'function') ref(el);
else if (typeof ref === 'object') {
ref.current = el;
}
}
},
[href, resetVisible, setIntersectionRef, ref]
);

React.useEffect(() => {
const shouldPrefetch =
prefetch !== false && isVisible && !isAbsoluteUrl(href);
if (shouldPrefetch) {
prefetchFn(href, clientManifestPath);
}
}, [href, prefetch, isVisible]);

const childProps: {
ref?: any;
onMouseEnter: React.MouseEventHandler;
} = {
ref: setRef,
onMouseEnter: (e: React.MouseEvent) => {
if (typeof onMouseEnter === 'function') {
onMouseEnter(e);
}
if (!isAbsoluteUrl(href)) {
prefetchFn(href, clientManifestPath);
}
}
};

return (
<LinkFromRouterReact
prefetch={prefetch}
to={to}
{...rest}
{...childProps}
/>
);
};

interface LinkWrapperProps extends LinkProps {
ref?: any;
}
76 changes: 76 additions & 0 deletions packages/platform-web/src/shuvi-app/react/utils/prefetch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
function hasSupportPrefetch() {
try {
const link: HTMLLinkElement = document.createElement('link');
return link.relList.supports('prefetch');
} catch (e) {
return false;
}
}

function prefetchViaDom(
href: string,
as: string,
link?: HTMLLinkElement
): Promise<any> {
return new Promise<void>((res, rej) => {
const selector = `
link[rel="prefetch"][href^="${href}"],
script[src^="${href}"]`;
if (document.querySelector(selector)) {
return res();
}

link = document.createElement('link');

// The order of property assignment here is intentional:
if (as) link!.as = as;
link!.rel = `prefetch`;
link!.onload = res as any;
link!.onerror = rej;

// `href` should always be last:
link!.href = href;

document.head.appendChild(link);
});
}

function getFilesForRoute(
route: string,
clientManifestPath: Record<string, string[]>
): Promise<any> {
return Promise.resolve(clientManifestPath).then(manifest => {
if (!manifest || !(route in manifest)) {
throw new Error(`Failed to lookup route: ${route}`);
}

const allFiles = manifest[route].map(entry => encodeURI(entry));

return {
scripts: allFiles.filter(v => v.endsWith('.js')),
css: allFiles.filter(v => v.endsWith('.css'))
};
});
}

export async function prefetchFn(
route: string,
clientManifestPath: Record<string, string[]>
): Promise<Promise<void> | void> {
if (process.env.NODE_ENV !== 'production') return;
if (typeof window === 'undefined' || !route) return;
const output = await getFilesForRoute(route, clientManifestPath);
const canPrefetch: boolean = hasSupportPrefetch();
await Promise.all(
canPrefetch
? output.scripts.map((script: { toString: () => string }) =>
prefetchViaDom(script.toString(), 'script')
)
: []
);
}

export const isAbsoluteUrl = (url: string) => {
const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/;
return ABSOLUTE_URL_REGEX.test(url);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const requestIdleCallback =
(typeof self !== 'undefined' &&
self.requestIdleCallback &&
self.requestIdleCallback.bind(window)) ||
function (cb: IdleRequestCallback): number {
let start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1) as unknown as number;
};

export const cancelIdleCallback =
(typeof self !== 'undefined' &&
self.cancelIdleCallback &&
self.cancelIdleCallback.bind(window)) ||
function (id: number) {
return clearTimeout(id);
};
Loading

0 comments on commit 5c04f5b

Please sign in to comment.