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) {
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
}

View file

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

View file

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

View file

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

View file

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

View file

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