mirror of https://gitee.com/antv-l7/antv-l7
419 lines
11 KiB
JavaScript
419 lines
11 KiB
JavaScript
import prefetchHelper from "./prefetch"
|
|
import emitter from "./emitter"
|
|
import { setMatchPaths, findPath, findMatchPath } from "./find-path"
|
|
|
|
const preferDefault = m => (m && m.default) || m
|
|
|
|
const stripSurroundingSlashes = s => {
|
|
s = s[0] === `/` ? s.slice(1) : s
|
|
s = s.endsWith(`/`) ? s.slice(0, -1) : s
|
|
return s
|
|
}
|
|
|
|
const createPageDataUrl = path => {
|
|
const fixedPath = path === `/` ? `index` : stripSurroundingSlashes(path)
|
|
return `${__PATH_PREFIX__}/page-data/${fixedPath}/page-data.json`
|
|
}
|
|
|
|
const doFetch = (url, method = `GET`) =>
|
|
new Promise((resolve, reject) => {
|
|
const req = new XMLHttpRequest()
|
|
req.open(method, url, true)
|
|
req.onreadystatechange = () => {
|
|
if (req.readyState == 4) {
|
|
resolve(req)
|
|
}
|
|
}
|
|
req.send(null)
|
|
})
|
|
|
|
const loadPageDataJson = loadObj => {
|
|
const { pagePath, retries = 0 } = loadObj
|
|
const url = createPageDataUrl(pagePath)
|
|
return doFetch(url).then(req => {
|
|
const { status, responseText } = req
|
|
|
|
// Handle 200
|
|
if (status === 200) {
|
|
try {
|
|
const jsonPayload = JSON.parse(responseText)
|
|
if (jsonPayload.path === undefined) {
|
|
throw new Error(`not a valid pageData response`)
|
|
}
|
|
|
|
return Object.assign(loadObj, {
|
|
status: `success`,
|
|
payload: jsonPayload,
|
|
})
|
|
} catch (err) {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
// Handle 404
|
|
if (status === 404 || status === 200) {
|
|
// If the request was for a 404 page and it doesn't exist, we're done
|
|
if (pagePath === `/404.html`) {
|
|
return Object.assign(loadObj, {
|
|
status: `failure`,
|
|
})
|
|
}
|
|
|
|
// Need some code here to cache the 404 request. In case
|
|
// multiple loadPageDataJsons result in 404s
|
|
return loadPageDataJson(
|
|
Object.assign(loadObj, { pagePath: `/404.html`, notFound: true })
|
|
)
|
|
}
|
|
|
|
// handle 500 response (Unrecoverable)
|
|
if (status === 500) {
|
|
return Object.assign(loadObj, {
|
|
status: `error`,
|
|
})
|
|
}
|
|
|
|
// Handle everything else, including status === 0, and 503s. Should retry
|
|
if (retries < 3) {
|
|
return loadPageDataJson(Object.assign(loadObj, { retries: retries + 1 }))
|
|
}
|
|
|
|
// Retried 3 times already, result is a failure.
|
|
return Object.assign(loadObj, {
|
|
status: `error`,
|
|
})
|
|
})
|
|
}
|
|
|
|
const doesConnectionSupportPrefetch = () => {
|
|
if (
|
|
`connection` in navigator &&
|
|
typeof navigator.connection !== `undefined`
|
|
) {
|
|
if ((navigator.connection.effectiveType || ``).includes(`2g`)) {
|
|
return false
|
|
}
|
|
if (navigator.connection.saveData) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
const toPageResources = (pageData, component = null) => {
|
|
const page = {
|
|
componentChunkName: pageData.componentChunkName,
|
|
path: pageData.path,
|
|
webpackCompilationHash: pageData.webpackCompilationHash,
|
|
matchPath: pageData.matchPath,
|
|
}
|
|
|
|
return {
|
|
component,
|
|
json: pageData.result,
|
|
page,
|
|
}
|
|
}
|
|
|
|
export class BaseLoader {
|
|
constructor(loadComponent, matchPaths) {
|
|
// Map of pagePath -> Page. Where Page is an object with: {
|
|
// status: `success` || `error`,
|
|
// payload: PageResources, // undefined if `error`
|
|
// }
|
|
// PageResources is {
|
|
// component,
|
|
// json: pageData.result,
|
|
// page: {
|
|
// componentChunkName,
|
|
// path,
|
|
// webpackCompilationHash,
|
|
// }
|
|
// }
|
|
this.pageDb = new Map()
|
|
this.inFlightDb = new Map()
|
|
this.pageDataDb = new Map()
|
|
this.prefetchTriggered = new Set()
|
|
this.prefetchCompleted = new Set()
|
|
this.loadComponent = loadComponent
|
|
setMatchPaths(matchPaths)
|
|
}
|
|
|
|
setApiRunner(apiRunner) {
|
|
this.apiRunner = apiRunner
|
|
this.prefetchDisabled = apiRunner(`disableCorePrefetching`).some(a => a)
|
|
}
|
|
|
|
loadPageDataJson(rawPath) {
|
|
const pagePath = findPath(rawPath)
|
|
if (this.pageDataDb.has(pagePath)) {
|
|
return Promise.resolve(this.pageDataDb.get(pagePath))
|
|
}
|
|
|
|
return loadPageDataJson({ pagePath }).then(pageData => {
|
|
this.pageDataDb.set(pagePath, pageData)
|
|
|
|
return pageData
|
|
})
|
|
}
|
|
|
|
findMatchPath(rawPath) {
|
|
return findMatchPath(rawPath)
|
|
}
|
|
|
|
// TODO check all uses of this and whether they use undefined for page resources not exist
|
|
loadPage(rawPath) {
|
|
const pagePath = findPath(rawPath)
|
|
if (this.pageDb.has(pagePath)) {
|
|
const page = this.pageDb.get(pagePath)
|
|
return Promise.resolve(page.payload)
|
|
}
|
|
if (this.inFlightDb.has(pagePath)) {
|
|
return this.inFlightDb.get(pagePath)
|
|
}
|
|
|
|
const inFlight = Promise.all([
|
|
this.loadAppData(),
|
|
this.loadPageDataJson(pagePath),
|
|
])
|
|
.then(allData => {
|
|
const result = allData[1]
|
|
if (result.status === `error`) {
|
|
return {
|
|
status: `error`,
|
|
}
|
|
}
|
|
if (result.status === `failure`) {
|
|
// throw an error so error trackers can pick this up
|
|
throw new Error(
|
|
`404 page could not be found. Checkout https://www.gatsbyjs.org/docs/add-404-page/`
|
|
)
|
|
}
|
|
|
|
let pageData = result.payload
|
|
const { componentChunkName } = pageData
|
|
return this.loadComponent(componentChunkName).then(component => {
|
|
const finalResult = { createdAt: new Date() }
|
|
let pageResources
|
|
if (!component) {
|
|
finalResult.status = `error`
|
|
} else {
|
|
finalResult.status = `success`
|
|
if (result.notFound === true) {
|
|
finalResult.notFound = true
|
|
}
|
|
pageData = Object.assign(pageData, {
|
|
webpackCompilationHash: allData[0]
|
|
? allData[0].webpackCompilationHash
|
|
: ``,
|
|
})
|
|
pageResources = toPageResources(pageData, component)
|
|
finalResult.payload = pageResources
|
|
emitter.emit(`onPostLoadPageResources`, {
|
|
page: pageResources,
|
|
pageResources,
|
|
})
|
|
}
|
|
this.pageDb.set(pagePath, finalResult)
|
|
// undefined if final result is an error
|
|
return pageResources
|
|
})
|
|
})
|
|
// prefer duplication with then + catch over .finally to prevent problems in ie11 + firefox
|
|
.then(response => {
|
|
this.inFlightDb.delete(pagePath)
|
|
return response
|
|
})
|
|
.catch(err => {
|
|
this.inFlightDb.delete(pagePath)
|
|
throw err
|
|
})
|
|
|
|
this.inFlightDb.set(pagePath, inFlight)
|
|
return inFlight
|
|
}
|
|
|
|
// returns undefined if loading page ran into errors
|
|
loadPageSync(rawPath) {
|
|
const pagePath = findPath(rawPath)
|
|
if (this.pageDb.has(pagePath)) {
|
|
return this.pageDb.get(pagePath).payload
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
shouldPrefetch(pagePath) {
|
|
// Skip prefetching if we know user is on slow or constrained connection
|
|
if (!doesConnectionSupportPrefetch()) {
|
|
return false
|
|
}
|
|
|
|
// Check if the page exists.
|
|
if (this.pageDb.has(pagePath)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
prefetch(pagePath) {
|
|
if (!this.shouldPrefetch(pagePath)) {
|
|
return false
|
|
}
|
|
|
|
// Tell plugins with custom prefetching logic that they should start
|
|
// prefetching this path.
|
|
if (!this.prefetchTriggered.has(pagePath)) {
|
|
this.apiRunner(`onPrefetchPathname`, { pathname: pagePath })
|
|
this.prefetchTriggered.add(pagePath)
|
|
}
|
|
|
|
// If a plugin has disabled core prefetching, stop now.
|
|
if (this.prefetchDisabled) {
|
|
return false
|
|
}
|
|
|
|
const realPath = findPath(pagePath)
|
|
// Todo make doPrefetch logic cacheable
|
|
// eslint-disable-next-line consistent-return
|
|
this.doPrefetch(realPath).then(() => {
|
|
if (!this.prefetchCompleted.has(pagePath)) {
|
|
this.apiRunner(`onPostPrefetchPathname`, { pathname: pagePath })
|
|
this.prefetchCompleted.add(pagePath)
|
|
}
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
doPrefetch(pagePath) {
|
|
throw new Error(`doPrefetch not implemented`)
|
|
}
|
|
|
|
hovering(rawPath) {
|
|
this.loadPage(rawPath)
|
|
}
|
|
|
|
getResourceURLsForPathname(rawPath) {
|
|
const pagePath = findPath(rawPath)
|
|
const page = this.pageDataDb.get(pagePath)
|
|
if (page) {
|
|
const pageResources = toPageResources(page.payload)
|
|
|
|
return [
|
|
...createComponentUrls(pageResources.page.componentChunkName),
|
|
createPageDataUrl(pagePath),
|
|
]
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
|
|
isPageNotFound(rawPath) {
|
|
const pagePath = findPath(rawPath)
|
|
const page = this.pageDb.get(pagePath)
|
|
return page && page.notFound === true
|
|
}
|
|
|
|
loadAppData(retries = 0) {
|
|
return doFetch(`${__PATH_PREFIX__}/page-data/app-data.json`).then(req => {
|
|
const { status, responseText } = req
|
|
|
|
let appData
|
|
|
|
if (status !== 200 && retries < 3) {
|
|
// Retry 3 times incase of failures
|
|
return this.loadAppData(retries + 1)
|
|
}
|
|
|
|
// Handle 200
|
|
if (status === 200) {
|
|
try {
|
|
const jsonPayload = JSON.parse(responseText)
|
|
if (jsonPayload.webpackCompilationHash === undefined) {
|
|
throw new Error(`not a valid app-data response`)
|
|
}
|
|
|
|
appData = jsonPayload
|
|
} catch (err) {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
return appData
|
|
})
|
|
}
|
|
}
|
|
|
|
const createComponentUrls = componentChunkName =>
|
|
window.___chunkMapping[componentChunkName].map(
|
|
chunk => __PATH_PREFIX__ + chunk
|
|
)
|
|
|
|
export class ProdLoader extends BaseLoader {
|
|
constructor(asyncRequires, matchPaths) {
|
|
const loadComponent = chunkName =>
|
|
asyncRequires.components[chunkName]().then(preferDefault)
|
|
|
|
super(loadComponent, matchPaths)
|
|
}
|
|
|
|
doPrefetch(pagePath) {
|
|
const pageDataUrl = createPageDataUrl(pagePath)
|
|
return prefetchHelper(pageDataUrl)
|
|
.then(() =>
|
|
// This was just prefetched, so will return a response from
|
|
// the cache instead of making another request to the server
|
|
this.loadPageDataJson(pagePath)
|
|
)
|
|
.then(result => {
|
|
if (result.status !== `success`) {
|
|
return Promise.resolve()
|
|
}
|
|
const pageData = result.payload
|
|
const chunkName = pageData.componentChunkName
|
|
const componentUrls = createComponentUrls(chunkName)
|
|
return Promise.all(componentUrls.map(prefetchHelper)).then(
|
|
() => pageData
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
let instance
|
|
|
|
export const setLoader = _loader => {
|
|
instance = _loader
|
|
}
|
|
|
|
export const publicLoader = {
|
|
// Deprecated methods. As far as we're aware, these are only used by
|
|
// core gatsby and the offline plugin, however there's a very small
|
|
// chance they're called by others.
|
|
getResourcesForPathname: rawPath => {
|
|
console.warn(
|
|
`Warning: getResourcesForPathname is deprecated. Use loadPage instead`
|
|
)
|
|
return instance.i.loadPage(rawPath)
|
|
},
|
|
getResourcesForPathnameSync: rawPath => {
|
|
console.warn(
|
|
`Warning: getResourcesForPathnameSync is deprecated. Use loadPageSync instead`
|
|
)
|
|
return instance.i.loadPageSync(rawPath)
|
|
},
|
|
enqueue: rawPath => instance.prefetch(rawPath),
|
|
|
|
// Real methods
|
|
getResourceURLsForPathname: rawPath =>
|
|
instance.getResourceURLsForPathname(rawPath),
|
|
loadPage: rawPath => instance.loadPage(rawPath),
|
|
loadPageSync: rawPath => instance.loadPageSync(rawPath),
|
|
prefetch: rawPath => instance.prefetch(rawPath),
|
|
isPageNotFound: rawPath => instance.isPageNotFound(rawPath),
|
|
hovering: rawPath => instance.hovering(rawPath),
|
|
loadAppData: () => instance.loadAppData(),
|
|
}
|
|
|
|
export default publicLoader
|