mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Implement router using the page-loader.
This commit is contained in:
parent
8938843025
commit
c95d2b28d0
|
@ -61,7 +61,10 @@ export default () => {
|
|||
}
|
||||
|
||||
export async function render (props) {
|
||||
if (props.err) {
|
||||
// There are some errors we should ignore.
|
||||
// Next.js rendering logic knows how to handle them.
|
||||
// These are specially 404 errors
|
||||
if (props.err && !props.err.ignore) {
|
||||
await renderError(props.err)
|
||||
return
|
||||
}
|
||||
|
|
11
lib/error.js
11
lib/error.js
|
@ -3,14 +3,15 @@ import HTTPStatus from 'http-status'
|
|||
import Head from './head'
|
||||
|
||||
export default class Error extends React.Component {
|
||||
static getInitialProps ({ res, jsonPageRes }) {
|
||||
const statusCode = res ? res.statusCode : (jsonPageRes ? jsonPageRes.status : null)
|
||||
return { statusCode }
|
||||
static getInitialProps ({ res, err }) {
|
||||
const statusCode = res ? res.statusCode : (err ? err.statusCode : null)
|
||||
const pageNotFound = statusCode === 404 || (err ? err.pageNotFound : false)
|
||||
return { statusCode, pageNotFound }
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusCode } = this.props
|
||||
const title = statusCode === 404
|
||||
const { statusCode, pageNotFound } = this.props
|
||||
const title = pageNotFound
|
||||
? 'This page could not be found'
|
||||
: HTTPStatus[statusCode] || 'An unexpected error has occurred'
|
||||
|
||||
|
|
|
@ -10,15 +10,23 @@ export default class PageLoader {
|
|||
this.loadingRoutes = {}
|
||||
}
|
||||
|
||||
loadPage (route) {
|
||||
normalizeRoute (route) {
|
||||
if (route[0] !== '/') {
|
||||
throw new Error('Route name should start with a "/"')
|
||||
}
|
||||
|
||||
route = route.replace(/index$/, '')
|
||||
return route.replace(/index$/, '')
|
||||
}
|
||||
|
||||
if (this.pageCache[route]) {
|
||||
return Promise.resolve(this.pageCache[route])
|
||||
loadPage (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
|
||||
const cachedPage = this.pageCache[route]
|
||||
if (cachedPage) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (cachedPage.error) return reject(cachedPage.error)
|
||||
return resolve(cachedPage.page)
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -43,6 +51,8 @@ export default class PageLoader {
|
|||
}
|
||||
|
||||
loadScript (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
|
||||
const script = document.createElement('script')
|
||||
const url = `/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
||||
script.src = url
|
||||
|
@ -57,8 +67,16 @@ export default class PageLoader {
|
|||
|
||||
// This method if called by the route code.
|
||||
registerPage (route, error, page) {
|
||||
route = this.normalizeRoute(route)
|
||||
|
||||
// add the page to the cache
|
||||
this.pageCache[route] = page
|
||||
this.pageCache[route] = { error, page }
|
||||
this.registerEvents.emit(route, { error, page })
|
||||
}
|
||||
|
||||
clearCache (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
delete this.pageCache[route]
|
||||
delete this.loadingRoutes[route]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global NEXT_PAGE_LOADER */
|
||||
|
||||
import { parse, format } from 'url'
|
||||
import mitt from 'mitt'
|
||||
import fetch from 'unfetch'
|
||||
import evalScript from '../eval-script'
|
||||
import shallowEquals from '../shallow-equals'
|
||||
import PQueue from '../p-queue'
|
||||
import { loadGetInitialProps, getURL } from '../utils'
|
||||
|
@ -15,10 +15,13 @@ export default class Router {
|
|||
this.route = toRoute(pathname)
|
||||
|
||||
// set up the component cache (by route keys)
|
||||
this.components = { [this.route]: { Component, err } }
|
||||
|
||||
// contain a map of promise of fetch routes
|
||||
this.fetchingRoutes = {}
|
||||
this.components = {}
|
||||
// We should not keep the cache, if there's an error
|
||||
// Otherwise, this cause issues when when going back and
|
||||
// come again to the errored page.
|
||||
if (Component !== ErrorComponent) {
|
||||
this.components[this.route] = { Component, err }
|
||||
}
|
||||
|
||||
// Handling Router Events
|
||||
this.events = mitt()
|
||||
|
@ -77,7 +80,7 @@ export default class Router {
|
|||
|
||||
async reload (route) {
|
||||
delete this.components[route]
|
||||
delete this.fetchingRoutes[route]
|
||||
NEXT_PAGE_LOADER.clearCache(route)
|
||||
|
||||
if (route !== this.route) return
|
||||
|
||||
|
@ -186,11 +189,11 @@ export default class Router {
|
|||
try {
|
||||
routeInfo = this.components[route]
|
||||
if (!routeInfo) {
|
||||
routeInfo = await this.fetchComponent(route, as)
|
||||
routeInfo = { Component: await this.fetchComponent(route, as) }
|
||||
}
|
||||
|
||||
const { Component, err, jsonPageRes } = routeInfo
|
||||
const ctx = { err, pathname, query, jsonPageRes }
|
||||
const { Component } = routeInfo
|
||||
const ctx = { pathname, query }
|
||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||
|
||||
this.components[route] = routeInfo
|
||||
|
@ -199,13 +202,27 @@ export default class Router {
|
|||
return { error: err }
|
||||
}
|
||||
|
||||
if (err.buildIdMismatched) {
|
||||
// Now we need to reload the page or do the action asked by the user
|
||||
_notifyBuildIdMismatch(as)
|
||||
// We also need to cancel this current route change.
|
||||
// We do it like this.
|
||||
err.cancelled = true
|
||||
return { error: err }
|
||||
}
|
||||
|
||||
if (err.pageNotFound) {
|
||||
// Indicate main error display logic to
|
||||
// ignore rendering this error as a runtime error.
|
||||
err.ignore = true
|
||||
}
|
||||
|
||||
const Component = this.ErrorComponent
|
||||
routeInfo = { Component, err }
|
||||
const ctx = { err, pathname, query }
|
||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||
|
||||
routeInfo.error = err
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
return routeInfo
|
||||
|
@ -268,28 +285,7 @@ export default class Router {
|
|||
cancelled = true
|
||||
}
|
||||
|
||||
const jsonPageRes = await this.fetchRoute(route)
|
||||
let jsonData
|
||||
// We can call .json() only once for a response.
|
||||
// That's why we need to keep a copy of data if we already parsed it.
|
||||
if (jsonPageRes.data) {
|
||||
jsonData = jsonPageRes.data
|
||||
} else {
|
||||
jsonData = jsonPageRes.data = await jsonPageRes.json()
|
||||
}
|
||||
|
||||
if (jsonData.buildIdMismatch) {
|
||||
_notifyBuildIdMismatch(as)
|
||||
|
||||
const error = Error('Abort due to BUILD_ID mismatch')
|
||||
error.cancelled = true
|
||||
throw error
|
||||
}
|
||||
|
||||
const newData = {
|
||||
...await loadComponent(jsonData),
|
||||
jsonPageRes
|
||||
}
|
||||
const Component = await this.fetchRoute(route)
|
||||
|
||||
if (cancelled) {
|
||||
const error = new Error(`Abort fetching component for route: "${route}"`)
|
||||
|
@ -301,7 +297,7 @@ export default class Router {
|
|||
this.componentLoadCancel = null
|
||||
}
|
||||
|
||||
return newData
|
||||
return Component
|
||||
}
|
||||
|
||||
async getInitialProps (Component, ctx) {
|
||||
|
@ -324,26 +320,24 @@ export default class Router {
|
|||
return props
|
||||
}
|
||||
|
||||
fetchRoute (route) {
|
||||
let promise = this.fetchingRoutes[route]
|
||||
if (!promise) {
|
||||
promise = this.fetchingRoutes[route] = this.doFetchRoute(route)
|
||||
async fetchRoute (route) {
|
||||
// Wait for webpack to became idle if it's not.
|
||||
// More info: https://github.com/zeit/next.js/pull/1511
|
||||
if (webpackModule && webpackModule.hot && webpackModule.hot.status() !== 'idle') {
|
||||
await new Promise((resolve) => {
|
||||
const check = (status) => {
|
||||
if (status === 'idle') {
|
||||
webpackModule.hot.removeStatusHandler(check)
|
||||
resolve()
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
doFetchRoute (route) {
|
||||
const { buildId } = window.__NEXT_DATA__
|
||||
const url = `/_next/${encodeURIComponent(buildId)}/pages${route}`
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
webpackModule.hot.status(check)
|
||||
})
|
||||
}
|
||||
|
||||
return await NEXT_PAGE_LOADER.loadPage(route)
|
||||
}
|
||||
|
||||
abortComponentLoad (as) {
|
||||
if (this.componentLoadCancel) {
|
||||
this.events.emit('routeChangeError', new Error('Route Cancelled'), as)
|
||||
|
@ -365,22 +359,3 @@ export default class Router {
|
|||
function toRoute (path) {
|
||||
return path.replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
async function loadComponent (jsonData) {
|
||||
if (webpackModule && webpackModule.hot && webpackModule.hot.status() !== 'idle') {
|
||||
await new Promise((resolve) => {
|
||||
const check = (status) => {
|
||||
if (status === 'idle') {
|
||||
webpackModule.hot.removeStatusHandler(check)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
webpackModule.hot.status(check)
|
||||
})
|
||||
}
|
||||
|
||||
const module = evalScript(jsonData.component)
|
||||
const Component = module.default || module
|
||||
|
||||
return { Component, err: jsonData.err }
|
||||
}
|
||||
|
|
|
@ -9,11 +9,13 @@ import {
|
|||
renderJSON,
|
||||
renderErrorJSON,
|
||||
sendHTML,
|
||||
serveStatic
|
||||
serveStatic,
|
||||
renderScript,
|
||||
renderScriptError
|
||||
} from './render'
|
||||
import Router from './router'
|
||||
import HotReloader from './hot-reloader'
|
||||
import resolvePath, { resolveFromList } from './resolve'
|
||||
import { resolveFromList } from './resolve'
|
||||
import getConfig from './config'
|
||||
// We need to go up one more level since we are in the `dist` directory
|
||||
import pkg from '../../package'
|
||||
|
@ -114,41 +116,28 @@ export default class Server {
|
|||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_next/:buildId/pages/:path*': async (req, res, params) => {
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ buildIdMismatch: true }))
|
||||
return
|
||||
}
|
||||
|
||||
const paths = params.path || ['index']
|
||||
const pathname = `/${paths.join('/')}`
|
||||
|
||||
await this.renderJSON(req, res, pathname)
|
||||
},
|
||||
|
||||
'/_next/:buildId/page/:path*': async (req, res, params) => {
|
||||
const paths = params.path || ['']
|
||||
const pathname = `/${paths.join('/')}`
|
||||
|
||||
if (this.dev) {
|
||||
await this.hotReloader.ensurePage(pathname)
|
||||
}
|
||||
const page = `/${paths.join('/')}`
|
||||
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
// TODO: Handle buildId mismatches properly.
|
||||
res.end(`
|
||||
var error = new Error('INVALID_BUILD_ID')
|
||||
error.buildIdMismatched = true
|
||||
NEXT_PAGE_LOADER.registerPage('${pathname}', error)
|
||||
`)
|
||||
const error = new Error('INVALID_BUILD_ID')
|
||||
const customFields = { buildIdMismatched: true }
|
||||
|
||||
await renderScriptError(req, res, page, error, customFields, this.renderOpts)
|
||||
return
|
||||
}
|
||||
|
||||
const path = join(this.dir, '.next', 'client-bundles', 'pages', pathname)
|
||||
const realPath = await resolvePath(path)
|
||||
await this.serveStatic(req, res, realPath)
|
||||
if (this.dev) {
|
||||
const compilationErr = this.getCompilationError(page)
|
||||
if (compilationErr) {
|
||||
const customFields = { buildError: true }
|
||||
await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await renderScript(req, res, page, this.renderOpts)
|
||||
},
|
||||
|
||||
'/_next/:path+': async (req, res, params) => {
|
||||
|
@ -314,6 +303,10 @@ export default class Server {
|
|||
}
|
||||
}
|
||||
|
||||
serveScript (req, res, path) {
|
||||
return serveStatic(req, res, path)
|
||||
}
|
||||
|
||||
readBuildId () {
|
||||
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
|
||||
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
||||
|
|
|
@ -118,6 +118,44 @@ export async function renderJSON (req, res, page, { dir = process.cwd(), hotRelo
|
|||
return serveStatic(req, res, pagePath)
|
||||
}
|
||||
|
||||
export async function renderScript (req, res, page, opts) {
|
||||
try {
|
||||
if (opts.dev) {
|
||||
await opts.hotReloader.ensurePage(page)
|
||||
}
|
||||
|
||||
const path = join(opts.dir, '.next', 'client-bundles', 'pages', page)
|
||||
const realPath = await resolvePath(path)
|
||||
await serveStatic(req, res, realPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
res.end(`
|
||||
var error = new Error('Page not exists: ${page}')
|
||||
error.pageNotFound = true
|
||||
error.statusCode = 404
|
||||
NEXT_PAGE_LOADER.registerPage('${page}', error)
|
||||
`)
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderScriptError (req, res, page, error, customFields, opts) {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
const errorJson = {
|
||||
...errorToJSON(error),
|
||||
...customFields
|
||||
}
|
||||
|
||||
res.end(`
|
||||
var error = ${JSON.stringify(errorJson)}
|
||||
NEXT_PAGE_LOADER.registerPage('${page}', error)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) {
|
||||
const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error'))
|
||||
|
||||
|
|
Loading…
Reference in a new issue