2018-11-25 00:47:39 +01:00
|
|
|
/* global document */
|
2019-01-04 21:49:21 +01:00
|
|
|
import mitt from 'next-server/dist/lib/mitt'
|
2019-04-04 23:54:01 +02:00
|
|
|
import unfetch from 'unfetch'
|
2017-04-11 18:37:59 +02:00
|
|
|
|
2018-11-25 00:47:39 +01:00
|
|
|
// smaller version of https://gist.github.com/igrigorik/a02f2359f3bc50ca7a9c
|
2018-11-26 23:58:40 +01:00
|
|
|
function supportsPreload (list) {
|
2018-11-25 00:47:39 +01:00
|
|
|
if (!list || !list.supports) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
try {
|
2018-11-26 23:58:40 +01:00
|
|
|
return list.supports('preload')
|
2018-11-25 00:47:39 +01:00
|
|
|
} catch (e) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-26 23:58:40 +01:00
|
|
|
const hasPreload = supportsPreload(document.createElement('link').relList)
|
2017-04-06 08:49:00 +02:00
|
|
|
|
2017-04-03 20:10:24 +02:00
|
|
|
export default class PageLoader {
|
2017-04-18 06:18:43 +02:00
|
|
|
constructor (buildId, assetPrefix) {
|
2017-04-03 20:10:24 +02:00
|
|
|
this.buildId = buildId
|
2017-04-18 06:18:43 +02:00
|
|
|
this.assetPrefix = assetPrefix
|
|
|
|
|
2017-04-03 20:10:24 +02:00
|
|
|
this.pageCache = {}
|
2018-11-25 00:47:39 +01:00
|
|
|
this.prefetchCache = new Set()
|
2019-01-04 21:49:21 +01:00
|
|
|
this.pageRegisterEvents = mitt()
|
2017-04-03 20:10:24 +02:00
|
|
|
this.loadingRoutes = {}
|
2019-04-04 23:54:01 +02:00
|
|
|
this.promisedBuildId = Promise.resolve()
|
2017-04-03 20:10:24 +02:00
|
|
|
}
|
|
|
|
|
2017-04-04 21:55:56 +02:00
|
|
|
normalizeRoute (route) {
|
2017-04-03 20:10:24 +02:00
|
|
|
if (route[0] !== '/') {
|
2017-06-20 21:43:38 +02:00
|
|
|
throw new Error(`Route name should start with a "/", got "${route}"`)
|
2017-04-03 20:10:24 +02:00
|
|
|
}
|
2017-07-09 06:20:30 +02:00
|
|
|
route = route.replace(/\/index$/, '/')
|
2017-04-03 20:10:24 +02:00
|
|
|
|
2017-05-02 01:26:18 +02:00
|
|
|
if (route === '/') return route
|
2017-05-04 22:05:47 +02:00
|
|
|
return route.replace(/\/$/, '')
|
2017-04-04 21:55:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
loadPage (route) {
|
|
|
|
route = this.normalizeRoute(route)
|
2017-04-03 20:10:24 +02:00
|
|
|
|
2017-04-11 18:37:59 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const fire = ({ error, page }) => {
|
2017-04-17 22:15:50 +02:00
|
|
|
this.pageRegisterEvents.off(route, fire)
|
2017-06-23 07:16:55 +02:00
|
|
|
delete this.loadingRoutes[route]
|
2017-04-03 20:10:24 +02:00
|
|
|
|
2017-04-11 18:37:59 +02:00
|
|
|
if (error) {
|
|
|
|
reject(error)
|
|
|
|
} else {
|
|
|
|
resolve(page)
|
|
|
|
}
|
|
|
|
}
|
2017-04-03 20:10:24 +02:00
|
|
|
|
2017-06-23 07:16:55 +02:00
|
|
|
// If there's a cached version of the page, let's use it.
|
|
|
|
const cachedPage = this.pageCache[route]
|
|
|
|
if (cachedPage) {
|
|
|
|
const { error, page } = cachedPage
|
|
|
|
error ? reject(error) : resolve(page)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register a listener to get the page
|
2017-04-17 22:15:50 +02:00
|
|
|
this.pageRegisterEvents.on(route, fire)
|
2017-04-03 20:10:24 +02:00
|
|
|
|
2017-05-09 03:20:50 +02:00
|
|
|
// If the page is loading via SSR, we need to wait for it
|
|
|
|
// rather downloading it again.
|
|
|
|
if (document.getElementById(`__NEXT_PAGE__${route}`)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-11 18:37:59 +02:00
|
|
|
// Load the script if not asked to load yet.
|
|
|
|
if (!this.loadingRoutes[route]) {
|
|
|
|
this.loadScript(route)
|
|
|
|
this.loadingRoutes[route] = true
|
|
|
|
}
|
2017-04-03 20:10:24 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-04 23:54:01 +02:00
|
|
|
onDynamicBuildId () {
|
|
|
|
this.promisedBuildId = new Promise(resolve => {
|
|
|
|
unfetch(`${this.assetPrefix}/_next/static/HEAD_BUILD_ID`)
|
|
|
|
.then(res => {
|
|
|
|
if (res.ok) {
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
const err = new Error('Failed to fetch HEAD buildId')
|
|
|
|
err.res = res
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
.then(res => res.text())
|
|
|
|
.then(buildId => {
|
|
|
|
this.buildId = buildId.trim()
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
// When this fails it's not a _huge_ deal, preload wont work and page
|
|
|
|
// navigation will 404, triggering a SSR refresh
|
|
|
|
console.warn(
|
|
|
|
'Failed to load BUILD_ID from server. ' +
|
|
|
|
'The following client-side page transition will likely 404 and cause a SSR.\n' +
|
|
|
|
'http://err.sh/zeit/next.js/head-build-id'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.then(resolve, resolve)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadScript (route) {
|
|
|
|
await this.promisedBuildId
|
|
|
|
|
2017-04-04 21:55:56 +02:00
|
|
|
route = this.normalizeRoute(route)
|
2018-01-13 08:34:48 +01:00
|
|
|
const scriptRoute = route === '/' ? '/index.js' : `${route}.js`
|
2017-05-11 17:24:27 +02:00
|
|
|
|
2017-04-03 20:10:24 +02:00
|
|
|
const script = document.createElement('script')
|
2018-07-25 13:45:42 +02:00
|
|
|
const url = `${this.assetPrefix}/_next/static/${encodeURIComponent(this.buildId)}/pages${scriptRoute}`
|
2018-12-13 01:05:21 +01:00
|
|
|
script.crossOrigin = process.crossOrigin
|
2017-04-03 20:10:24 +02:00
|
|
|
script.src = url
|
2017-04-11 18:37:59 +02:00
|
|
|
script.onerror = () => {
|
|
|
|
const error = new Error(`Error when loading route: ${route}`)
|
2018-02-21 18:41:25 +01:00
|
|
|
error.code = 'PAGE_LOAD_ERROR'
|
2017-04-17 22:15:50 +02:00
|
|
|
this.pageRegisterEvents.emit(route, { error })
|
2017-04-03 20:10:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
document.body.appendChild(script)
|
|
|
|
}
|
|
|
|
|
|
|
|
// This method if called by the route code.
|
2017-04-06 08:49:00 +02:00
|
|
|
registerPage (route, regFn) {
|
2017-04-11 16:33:18 +02:00
|
|
|
const register = () => {
|
2017-06-16 13:13:55 +02:00
|
|
|
try {
|
|
|
|
const { error, page } = regFn()
|
|
|
|
this.pageCache[route] = { error, page }
|
|
|
|
this.pageRegisterEvents.emit(route, { error, page })
|
|
|
|
} catch (error) {
|
|
|
|
this.pageCache[route] = { error }
|
|
|
|
this.pageRegisterEvents.emit(route, { error })
|
|
|
|
}
|
2017-04-06 08:49:00 +02:00
|
|
|
}
|
|
|
|
|
2018-12-31 19:06:36 +01:00
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
|
|
// Wait for webpack to become idle if it's not.
|
|
|
|
// More info: https://github.com/zeit/next.js/pull/1511
|
|
|
|
if (module.hot && module.hot.status() !== 'idle') {
|
|
|
|
console.log(`Waiting for webpack to become "idle" to initialize the page: "${route}"`)
|
|
|
|
|
|
|
|
const check = (status) => {
|
|
|
|
if (status === 'idle') {
|
|
|
|
module.hot.removeStatusHandler(check)
|
|
|
|
register()
|
|
|
|
}
|
2017-04-06 08:49:00 +02:00
|
|
|
}
|
2018-12-31 19:06:36 +01:00
|
|
|
module.hot.status(check)
|
|
|
|
return
|
2017-04-06 08:49:00 +02:00
|
|
|
}
|
|
|
|
}
|
2018-12-31 19:06:36 +01:00
|
|
|
|
|
|
|
register()
|
2017-04-03 20:10:24 +02:00
|
|
|
}
|
2017-04-04 21:55:56 +02:00
|
|
|
|
2018-11-25 00:47:39 +01:00
|
|
|
async prefetch (route) {
|
|
|
|
route = this.normalizeRoute(route)
|
|
|
|
const scriptRoute = route === '/' ? '/index.js' : `${route}.js`
|
|
|
|
if (this.prefetchCache.has(scriptRoute)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.prefetchCache.add(scriptRoute)
|
|
|
|
|
2018-12-13 19:08:23 +01:00
|
|
|
// Inspired by quicklink, license: https://github.com/GoogleChromeLabs/quicklink/blob/master/LICENSE
|
|
|
|
// Don't prefetch if the user is on 2G / Don't prefetch if Save-Data is enabled
|
|
|
|
if ('connection' in navigator) {
|
|
|
|
if ((navigator.connection.effectiveType || '').indexOf('2g') !== -1 || navigator.connection.saveData) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-26 23:58:40 +01:00
|
|
|
// Feature detection is used to see if preload is supported
|
|
|
|
// If not fall back to loading script tags before the page is loaded
|
2018-11-25 00:47:39 +01:00
|
|
|
// https://caniuse.com/#feat=link-rel-preload
|
2018-11-26 23:58:40 +01:00
|
|
|
if (hasPreload) {
|
2019-04-04 23:54:01 +02:00
|
|
|
await this.promisedBuildId
|
|
|
|
|
2018-11-26 23:58:40 +01:00
|
|
|
const link = document.createElement('link')
|
|
|
|
link.rel = 'preload'
|
2018-12-13 01:05:21 +01:00
|
|
|
link.crossOrigin = process.crossOrigin
|
2018-11-26 23:58:40 +01:00
|
|
|
link.href = `${this.assetPrefix}/_next/static/${encodeURIComponent(this.buildId)}/pages${scriptRoute}`
|
|
|
|
link.as = 'script'
|
|
|
|
document.head.appendChild(link)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (document.readyState === 'complete') {
|
2019-03-20 12:11:35 +01:00
|
|
|
return this.loadPage(route).catch(() => {})
|
2018-11-26 23:58:40 +01:00
|
|
|
} else {
|
2019-03-20 12:11:35 +01:00
|
|
|
return new Promise((resolve) => {
|
2018-11-26 23:58:40 +01:00
|
|
|
window.addEventListener('load', () => {
|
2019-03-20 10:58:26 +01:00
|
|
|
this.loadPage(route).then(() => resolve(), () => resolve())
|
2018-11-26 23:58:40 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2018-11-25 00:47:39 +01:00
|
|
|
}
|
|
|
|
|
2017-04-04 21:55:56 +02:00
|
|
|
clearCache (route) {
|
|
|
|
route = this.normalizeRoute(route)
|
|
|
|
delete this.pageCache[route]
|
|
|
|
delete this.loadingRoutes[route]
|
2017-05-09 09:42:48 +02:00
|
|
|
|
|
|
|
const script = document.getElementById(`__NEXT_PAGE__${route}`)
|
|
|
|
if (script) {
|
|
|
|
script.parentNode.removeChild(script)
|
|
|
|
}
|
2017-04-04 21:55:56 +02:00
|
|
|
}
|
2017-04-03 20:10:24 +02:00
|
|
|
}
|