refactor(core): prefetch/preload refactor (#7282)

This commit is contained in:
Joshua Chen 2022-05-02 12:56:58 +08:00 committed by GitHub
parent 3c24cbc2c0
commit 53564f33ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 42 additions and 57 deletions

View file

@ -346,3 +346,10 @@ declare module '*.css' {
const src: string; const src: string;
export default src; export default src;
} }
interface Window {
docusaurus: {
prefetch: (url: string) => false | Promise<void[]>;
preload: (url: string) => false | Promise<void[]>;
};
}

View file

@ -62,7 +62,9 @@ class PendingNavigation extends React.Component<Props, State> {
location: nextLocation, location: nextLocation,
})!; })!;
// Load data while the old screen remains. // Load data while the old screen remains. Force preload instead of using
// `window.docusaurus`, because we want to avoid loading screen even when
// user is on saveData
preload(nextLocation.pathname) preload(nextLocation.pathname)
.then(() => { .then(() => {
this.routeUpdateCleanupCb?.(); this.routeUpdateCleanupCb?.();

View file

@ -12,8 +12,8 @@ import prefetchHelper from './prefetch';
import preloadHelper from './preload'; import preloadHelper from './preload';
import flat from './flat'; import flat from './flat';
const fetched: {[key: string]: boolean} = {}; const fetched = new Set<string>();
const loaded: {[key: string]: boolean} = {}; const loaded = new Set<string>();
declare global { declare global {
// eslint-disable-next-line camelcase, no-underscore-dangle // eslint-disable-next-line camelcase, no-underscore-dangle
@ -25,14 +25,14 @@ declare global {
// If user is on slow or constrained connection. // If user is on slow or constrained connection.
const isSlowConnection = () => const isSlowConnection = () =>
navigator.connection?.effectiveType.includes('2g') && navigator.connection?.effectiveType.includes('2g') ||
navigator.connection?.saveData; navigator.connection?.saveData;
const canPrefetch = (routePath: string) => const canPrefetch = (routePath: string) =>
!isSlowConnection() && !loaded[routePath] && !fetched[routePath]; !isSlowConnection() && !loaded.has(routePath) && !fetched.has(routePath);
const canPreload = (routePath: string) => const canPreload = (routePath: string) =>
!isSlowConnection() && !loaded[routePath]; !isSlowConnection() && !loaded.has(routePath);
const getChunkNamesToLoad = (path: string): string[] => const getChunkNamesToLoad = (path: string): string[] =>
Object.entries(routesChunkNames) Object.entries(routesChunkNames)
@ -46,12 +46,11 @@ const getChunkNamesToLoad = (path: string): string[] =>
.flatMap(([, routeChunks]) => Object.values(flat(routeChunks))); .flatMap(([, routeChunks]) => Object.values(flat(routeChunks)));
const docusaurus = { const docusaurus = {
prefetch: (routePath: string): boolean => { prefetch(routePath: string): false | Promise<void[]> {
if (!canPrefetch(routePath)) { if (!canPrefetch(routePath)) {
return false; return false;
} }
// Prevent future duplicate prefetch of routePath. fetched.add(routePath);
fetched[routePath] = true;
// Find all webpack chunk names needed. // Find all webpack chunk names needed.
const matches = matchRoutes(routes, routePath); const matches = matchRoutes(routes, routePath);
@ -61,32 +60,30 @@ const docusaurus = {
); );
// Prefetch all webpack chunk assets file needed. // Prefetch all webpack chunk assets file needed.
chunkNamesNeeded.forEach((chunkName) => { return Promise.all(
// "__webpack_require__.gca" is a custom function provided by chunkNamesNeeded.map((chunkName) => {
// ChunkAssetPlugin. Pass it the chunkName or chunkId you want to load and // "__webpack_require__.gca" is injected by ChunkAssetPlugin. Pass it
// it will return the URL for that chunk. // the name of the chunk you want to load and it will return its URL.
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
const chunkAsset = __webpack_require__.gca(chunkName); const chunkAsset = __webpack_require__.gca(chunkName);
// In some cases, webpack might decide to optimize further & hence the // In some cases, webpack might decide to optimize further, leading to
// chunk assets are merged to another chunk/previous chunk. // the chunk assets being merged to another chunk. In this case, we can
// Hence, we can safely filter it out/don't need to load it. // safely filter it out and don't need to load it.
if (chunkAsset && !/undefined/.test(chunkAsset)) { if (chunkAsset && !/undefined/.test(chunkAsset)) {
prefetchHelper(chunkAsset); return prefetchHelper(chunkAsset);
} }
}); return Promise.resolve();
}),
return true; );
}, },
preload: (routePath: string): boolean => { preload(routePath: string): false | Promise<void[]> {
if (!canPreload(routePath)) { if (!canPreload(routePath)) {
return false; return false;
} }
loaded.add(routePath);
loaded[routePath] = true; return preloadHelper(routePath);
preloadHelper(routePath);
return true;
}, },
}; };

View file

@ -21,13 +21,6 @@ import {useBaseUrlUtils} from './useBaseUrl';
import {applyTrailingSlash} from '@docusaurus/utils-common'; import {applyTrailingSlash} from '@docusaurus/utils-common';
import type {Props} from '@docusaurus/Link'; import type {Props} from '@docusaurus/Link';
import type docusaurus from '../docusaurus';
declare global {
interface Window {
docusaurus: typeof docusaurus;
}
}
// TODO all this wouldn't be necessary if we used ReactRouter basename feature // TODO all this wouldn't be necessary if we used ReactRouter basename feature
// We don't automatically add base urls to all links, // We don't automatically add base urls to all links,

View file

@ -25,18 +25,18 @@ export default function flat(target: ChunkNames): {[keyPath: string]: string} {
const delimiter = '.'; const delimiter = '.';
const output: {[keyPath: string]: string} = {}; const output: {[keyPath: string]: string} = {};
function step(object: Tree, prefix?: string | number) { function dfs(object: Tree, prefix?: string | number) {
Object.entries(object).forEach(([key, value]) => { Object.entries(object).forEach(([key, value]) => {
const newKey = prefix ? `${prefix}${delimiter}${key}` : key; const newKey = prefix ? `${prefix}${delimiter}${key}` : key;
if (isTree(value)) { if (isTree(value)) {
step(value, newKey); dfs(value, newKey);
} else { } else {
output[newKey] = value; output[newKey] = value;
} }
}); });
} }
step(target); dfs(target);
return output; return output;
} }

View file

@ -14,7 +14,7 @@ function supports(feature: string) {
} }
} }
function linkPrefetchStrategy(url: string) { function linkPrefetchStrategy(url: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
reject(); reject();
@ -25,8 +25,8 @@ function linkPrefetchStrategy(url: string) {
link.setAttribute('rel', 'prefetch'); link.setAttribute('rel', 'prefetch');
link.setAttribute('href', url); link.setAttribute('href', url);
link.onload = resolve; link.onload = () => resolve();
link.onerror = reject; link.onerror = () => reject();
const parentElement = const parentElement =
document.getElementsByTagName('head')[0] ?? document.getElementsByTagName('head')[0] ??
@ -57,20 +57,6 @@ const supportedPrefetchStrategy = supports('prefetch')
? linkPrefetchStrategy ? linkPrefetchStrategy
: xhrPrefetchStrategy; : xhrPrefetchStrategy;
const preFetched: {[url: string]: boolean} = {};
export default function prefetch(url: string): Promise<void> { export default function prefetch(url: string): Promise<void> {
return new Promise((resolve) => { return supportedPrefetchStrategy(url).catch(() => {}); // 404s are logged to the console anyway.
if (preFetched[url]) {
resolve();
return;
}
supportedPrefetchStrategy(url)
.then(() => {
resolve();
preFetched[url] = true;
})
.catch(() => {}); // 404s are logged to the console anyway.
});
} }