Skip to content

Commit

Permalink
feat: prefetch in viewport inbound page chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed May 21, 2020
1 parent 7a90c4f commit da4852a
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 35 deletions.
5 changes: 5 additions & 0 deletions src/client/app/components/Content.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { h } from 'vue'
import { useRoute } from '../router'
import { usePrefetch } from '../composables/preFetch'

export const Content = {
setup() {
const route = useRoute()
if (!__DEV__) {
// in prod mode, enable intersectionObserver based pre-fetch.
usePrefetch()
}
return () => (route.contentComponent ? h(route.contentComponent) : null)
}
}
89 changes: 89 additions & 0 deletions src/client/app/composables/preFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Customized pre-fetch for page chunks based on
// https://github.com/GoogleChromeLabs/quicklink

import { onMounted, onUnmounted, onUpdated } from 'vue'
import { inBrowser, pathToFile } from '../utils'

const hasFetched = new Set<string>()
const createLink = () => document.createElement('link')

const viaDOM = (url: string) => {
const link = createLink()
link.rel = `prefetch`
link.href = url
document.head.appendChild(link)
}

const viaXHR = (url: string) => {
const req = new XMLHttpRequest()
req.open('GET', url, (req.withCredentials = true))
req.send()
}

let link
const doFetch: (url: string) => void =
inBrowser &&
(link = createLink()) &&
link.relList &&
link.relList.supports &&
link.relList.supports('prefetch')
? viaDOM
: viaXHR

export function usePrefetch() {
if (!inBrowser) {
return
}

if (!window.IntersectionObserver) {
return
}

let conn
if (
(conn = (navigator as any).connection) &&
(conn.saveData || /2g/.test(conn.effectiveType))
) {
// Don't prefetch if using 2G or if Save-Data is enabled.
return
}

const rIC = (window as any).requestIdleCallback || setTimeout
let observer: IntersectionObserver | null = null

const observeLinks = () => {
if (observer) {
observer.disconnect()
}

observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const link = entry.target as HTMLAnchorElement
observer!.unobserve(link)
const { pathname } = link
if (!hasFetched.has(pathname)) {
hasFetched.add(pathname)
const pageChunkPath = pathToFile(pathname)
doFetch(pageChunkPath)
}
}
})
})

rIC(() => {
document.querySelectorAll('.vitepress-content a').forEach((link) => {
if ((link as HTMLAnchorElement).hostname === location.hostname) {
observer!.observe(link)
}
})
})
}

onMounted(observeLinks)
onUpdated(observeLinks)

onUnmounted(() => {
observer && observer.disconnect()
})
}
36 changes: 7 additions & 29 deletions src/client/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { Content } from './components/Content'
import Debug from './components/Debug.vue'
import Theme from '/@theme/index'
import { hot } from 'vite/hmr'

const inBrowser = typeof window !== 'undefined'
import { inBrowser, pathToFile } from './utils'

const NotFound = Theme.NotFound || (() => '404 Not Found')

Expand Down Expand Up @@ -38,38 +37,17 @@ export function createApp() {
let initialPath: string

const router = createRouter((route) => {
let pagePath = route.path.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}
let pagePath = pathToFile(route.path)

if (isInitialPageLoad) {
initialPath = pagePath
}

if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
// /foo/bar.html -> ./foo_bar.md

if (inBrowser) {
pagePath = pagePath.slice(__BASE__.length).replace(/\//g, '_') + '.md'
// client production build needs to account for page hash, which is
// injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath]
// use lean build if this is the initial page load or navigating back
// to the initial loaded path (the static vnodes already adopted the
// static content on that load so no need to re-fetch the page)
const ext =
isInitialPageLoad || initialPath === pagePath ? 'lean.js' : 'js'
pagePath = `${__BASE__}_assets/${pagePath}.${pageHash}.${ext}`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`
}
// use lean build if this is the initial page load or navigating back
// to the initial loaded path (the static vnodes already adopted the
// static content on that load so no need to re-fetch the page)
if (isInitialPageLoad || initialPath === pagePath) {
pagePath = pagePath.replace(/\.js$/, '.lean.js')
}

if (inBrowser) {
Expand Down
12 changes: 6 additions & 6 deletions src/client/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ export function createRouter(
(e) => {
const link = (e.target as Element).closest('a')
if (link) {
const { href, target } = link
const targetUrl = new URL(href)
const { href, protocol, hostname, pathname, hash, target } = link
const currentUrl = window.location
// only intercept inbound links
if (
target !== `_blank` &&
targetUrl.protocol === currentUrl.protocol &&
targetUrl.hostname === currentUrl.hostname
protocol === currentUrl.protocol &&
hostname === currentUrl.hostname
) {
if (targetUrl.pathname === currentUrl.pathname) {
if (pathname === currentUrl.pathname) {
// smooth scroll bewteen hash anchors in the same page
if (targetUrl.hash !== currentUrl.hash) {
if (hash !== currentUrl.hash) {
e.preventDefault()
window.scrollTo({
left: 0,
Expand Down
32 changes: 32 additions & 0 deletions src/client/app/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const inBrowser = typeof window !== 'undefined'

/**
* Converts a url path to the corresponding js chunk filename.
*/
export function pathToFile(path: string): string {
let pagePath = path.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}

if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
// /foo/bar.html -> ./foo_bar.md
if (inBrowser) {
pagePath = pagePath.slice(__BASE__.length).replace(/\//g, '_') + '.md'
// client production build needs to account for page hash, which is
// injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath]
pagePath = `${__BASE__}_assets/${pagePath}.${pageHash}.js`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`
}
}

return pagePath
}

0 comments on commit da4852a

Please sign in to comment.