1
0
Fork 0
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:
Arunoda Susiripala 2017-04-05 01:25:56 +05:30
parent 8938843025
commit c95d2b28d0
6 changed files with 138 additions and 110 deletions

View file

@ -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
} }

View file

@ -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'

View file

@ -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]
}
} }

View file

@ -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 }
}

View file

@ -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')

View file

@ -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'))