-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8c1ef4d
commit 5c04f5b
Showing
18 changed files
with
496 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
packages/platform-web/src/lib/features/html-render/lib/generateClientManifestPath.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
76
packages/platform-web/src/shuvi-app/react/utils/prefetch.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
23 changes: 23 additions & 0 deletions
23
packages/platform-web/src/shuvi-app/react/utils/requestIdleCallback.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
Oops, something went wrong.