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) {
|
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)
|
await renderError(props.err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
11
lib/error.js
11
lib/error.js
|
@ -3,14 +3,15 @@ import HTTPStatus from 'http-status'
|
||||||
import Head from './head'
|
import Head from './head'
|
||||||
|
|
||||||
export default class Error extends React.Component {
|
export default class Error extends React.Component {
|
||||||
static getInitialProps ({ res, jsonPageRes }) {
|
static getInitialProps ({ res, err }) {
|
||||||
const statusCode = res ? res.statusCode : (jsonPageRes ? jsonPageRes.status : null)
|
const statusCode = res ? res.statusCode : (err ? err.statusCode : null)
|
||||||
return { statusCode }
|
const pageNotFound = statusCode === 404 || (err ? err.pageNotFound : false)
|
||||||
|
return { statusCode, pageNotFound }
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusCode } = this.props
|
const { statusCode, pageNotFound } = this.props
|
||||||
const title = statusCode === 404
|
const title = pageNotFound
|
||||||
? 'This page could not be found'
|
? 'This page could not be found'
|
||||||
: HTTPStatus[statusCode] || 'An unexpected error has occurred'
|
: HTTPStatus[statusCode] || 'An unexpected error has occurred'
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,23 @@ export default class PageLoader {
|
||||||
this.loadingRoutes = {}
|
this.loadingRoutes = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPage (route) {
|
normalizeRoute (route) {
|
||||||
if (route[0] !== '/') {
|
if (route[0] !== '/') {
|
||||||
throw new Error('Route name should start with a "/"')
|
throw new Error('Route name should start with a "/"')
|
||||||
}
|
}
|
||||||
|
|
||||||
route = route.replace(/index$/, '')
|
return route.replace(/index$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pageCache[route]) {
|
loadPage (route) {
|
||||||
return Promise.resolve(this.pageCache[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) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -43,6 +51,8 @@ export default class PageLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScript (route) {
|
loadScript (route) {
|
||||||
|
route = this.normalizeRoute(route)
|
||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
const url = `/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
const url = `/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
||||||
script.src = url
|
script.src = url
|
||||||
|
@ -57,8 +67,16 @@ export default class PageLoader {
|
||||||
|
|
||||||
// This method if called by the route code.
|
// This method if called by the route code.
|
||||||
registerPage (route, error, page) {
|
registerPage (route, error, page) {
|
||||||
|
route = this.normalizeRoute(route)
|
||||||
|
|
||||||
// add the page to the cache
|
// add the page to the cache
|
||||||
this.pageCache[route] = page
|
this.pageCache[route] = { error, page }
|
||||||
this.registerEvents.emit(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 { parse, format } from 'url'
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
import fetch from 'unfetch'
|
|
||||||
import evalScript from '../eval-script'
|
|
||||||
import shallowEquals from '../shallow-equals'
|
import shallowEquals from '../shallow-equals'
|
||||||
import PQueue from '../p-queue'
|
import PQueue from '../p-queue'
|
||||||
import { loadGetInitialProps, getURL } from '../utils'
|
import { loadGetInitialProps, getURL } from '../utils'
|
||||||
|
@ -15,10 +15,13 @@ export default class Router {
|
||||||
this.route = toRoute(pathname)
|
this.route = toRoute(pathname)
|
||||||
|
|
||||||
// set up the component cache (by route keys)
|
// set up the component cache (by route keys)
|
||||||
this.components = { [this.route]: { Component, err } }
|
this.components = {}
|
||||||
|
// We should not keep the cache, if there's an error
|
||||||
// contain a map of promise of fetch routes
|
// Otherwise, this cause issues when when going back and
|
||||||
this.fetchingRoutes = {}
|
// come again to the errored page.
|
||||||
|
if (Component !== ErrorComponent) {
|
||||||
|
this.components[this.route] = { Component, err }
|
||||||
|
}
|
||||||
|
|
||||||
// Handling Router Events
|
// Handling Router Events
|
||||||
this.events = mitt()
|
this.events = mitt()
|
||||||
|
@ -77,7 +80,7 @@ export default class Router {
|
||||||
|
|
||||||
async reload (route) {
|
async reload (route) {
|
||||||
delete this.components[route]
|
delete this.components[route]
|
||||||
delete this.fetchingRoutes[route]
|
NEXT_PAGE_LOADER.clearCache(route)
|
||||||
|
|
||||||
if (route !== this.route) return
|
if (route !== this.route) return
|
||||||
|
|
||||||
|
@ -186,11 +189,11 @@ export default class Router {
|
||||||
try {
|
try {
|
||||||
routeInfo = this.components[route]
|
routeInfo = this.components[route]
|
||||||
if (!routeInfo) {
|
if (!routeInfo) {
|
||||||
routeInfo = await this.fetchComponent(route, as)
|
routeInfo = { Component: await this.fetchComponent(route, as) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Component, err, jsonPageRes } = routeInfo
|
const { Component } = routeInfo
|
||||||
const ctx = { err, pathname, query, jsonPageRes }
|
const ctx = { pathname, query }
|
||||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||||
|
|
||||||
this.components[route] = routeInfo
|
this.components[route] = routeInfo
|
||||||
|
@ -199,13 +202,27 @@ export default class Router {
|
||||||
return { error: err }
|
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
|
const Component = this.ErrorComponent
|
||||||
routeInfo = { Component, err }
|
routeInfo = { Component, err }
|
||||||
const ctx = { err, pathname, query }
|
const ctx = { err, pathname, query }
|
||||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||||
|
|
||||||
routeInfo.error = err
|
routeInfo.error = err
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return routeInfo
|
return routeInfo
|
||||||
|
@ -268,28 +285,7 @@ export default class Router {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonPageRes = await this.fetchRoute(route)
|
const Component = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
const error = new Error(`Abort fetching component for route: "${route}"`)
|
const error = new Error(`Abort fetching component for route: "${route}"`)
|
||||||
|
@ -301,7 +297,7 @@ export default class Router {
|
||||||
this.componentLoadCancel = null
|
this.componentLoadCancel = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return newData
|
return Component
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInitialProps (Component, ctx) {
|
async getInitialProps (Component, ctx) {
|
||||||
|
@ -324,24 +320,22 @@ export default class Router {
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRoute (route) {
|
async fetchRoute (route) {
|
||||||
let promise = this.fetchingRoutes[route]
|
// Wait for webpack to became idle if it's not.
|
||||||
if (!promise) {
|
// More info: https://github.com/zeit/next.js/pull/1511
|
||||||
promise = this.fetchingRoutes[route] = this.doFetchRoute(route)
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return await NEXT_PAGE_LOADER.loadPage(route)
|
||||||
}
|
|
||||||
|
|
||||||
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' }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abortComponentLoad (as) {
|
abortComponentLoad (as) {
|
||||||
|
@ -365,22 +359,3 @@ export default class Router {
|
||||||
function toRoute (path) {
|
function toRoute (path) {
|
||||||
return path.replace(/\/$/, '') || '/'
|
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,
|
renderJSON,
|
||||||
renderErrorJSON,
|
renderErrorJSON,
|
||||||
sendHTML,
|
sendHTML,
|
||||||
serveStatic
|
serveStatic,
|
||||||
|
renderScript,
|
||||||
|
renderScriptError
|
||||||
} from './render'
|
} from './render'
|
||||||
import Router from './router'
|
import Router from './router'
|
||||||
import HotReloader from './hot-reloader'
|
import HotReloader from './hot-reloader'
|
||||||
import resolvePath, { resolveFromList } from './resolve'
|
import { resolveFromList } from './resolve'
|
||||||
import getConfig from './config'
|
import getConfig from './config'
|
||||||
// We need to go up one more level since we are in the `dist` directory
|
// We need to go up one more level since we are in the `dist` directory
|
||||||
import pkg from '../../package'
|
import pkg from '../../package'
|
||||||
|
@ -114,41 +116,28 @@ export default class Server {
|
||||||
await this.serveStatic(req, res, p)
|
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) => {
|
'/_next/:buildId/page/:path*': async (req, res, params) => {
|
||||||
const paths = params.path || ['']
|
const paths = params.path || ['']
|
||||||
const pathname = `/${paths.join('/')}`
|
const page = `/${paths.join('/')}`
|
||||||
|
|
||||||
if (this.dev) {
|
|
||||||
await this.hotReloader.ensurePage(pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.handleBuildId(params.buildId, res)) {
|
if (!this.handleBuildId(params.buildId, res)) {
|
||||||
res.setHeader('Content-Type', 'text/javascript')
|
const error = new Error('INVALID_BUILD_ID')
|
||||||
// TODO: Handle buildId mismatches properly.
|
const customFields = { buildIdMismatched: true }
|
||||||
res.end(`
|
|
||||||
var error = new Error('INVALID_BUILD_ID')
|
await renderScriptError(req, res, page, error, customFields, this.renderOpts)
|
||||||
error.buildIdMismatched = true
|
|
||||||
NEXT_PAGE_LOADER.registerPage('${pathname}', error)
|
|
||||||
`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = join(this.dir, '.next', 'client-bundles', 'pages', pathname)
|
if (this.dev) {
|
||||||
const realPath = await resolvePath(path)
|
const compilationErr = this.getCompilationError(page)
|
||||||
await this.serveStatic(req, res, realPath)
|
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) => {
|
'/_next/:path+': async (req, res, params) => {
|
||||||
|
@ -314,6 +303,10 @@ export default class Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serveScript (req, res, path) {
|
||||||
|
return serveStatic(req, res, path)
|
||||||
|
}
|
||||||
|
|
||||||
readBuildId () {
|
readBuildId () {
|
||||||
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
|
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
|
||||||
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
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)
|
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 } = {}) {
|
export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) {
|
||||||
const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error'))
|
const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error'))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue