mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Add CDN support with assetPrefix (#1700)
* Introduce script tag based page loading system. * Call ensurePage only in the dev mode. * Implement router using the page-loader. * Fix a typo and remove unwanted code. * Fix some issues related to rendering. * Fix production tests. * Fix ondemand test cases. * Fix unit tests. * Get rid of eval completely. * Remove all the inline code. * Remove the json-pages plugin. * Rename NEXT_PAGE_LOADER into __NEXT_PAGE_LOADER__ * Rename NEXT_LOADED_PAGES into __NEXT_LOADED_PAGES__ * Remove some unwanted code. * Load everything async. * Remove lib/eval-script.js We no longer need it. * Move webpack idle wait code to the page-loader. Because that's the place to do it. * Remove pageNotFound key from the error. * Remove unused error field 'buildError' * Add much better logic to normalize routes. * Get rid of mitt. * Introduce a better way to register pages. * Came back to the mitt() based page-loader. * Add link rel=preload support. * Add assetPrefix support to add support for CDNs. * Add assetPrefix support for preload links. * Update readme.md
This commit is contained in:
parent
bdc30bc089
commit
dec85fe6c4
|
@ -4,9 +4,9 @@ import mitt from 'mitt'
|
|||
import HeadManager from './head-manager'
|
||||
import { createRouter } from '../lib/router'
|
||||
import App from '../lib/app'
|
||||
import evalScript from '../lib/eval-script'
|
||||
import { loadGetInitialProps, getURL } from '../lib/utils'
|
||||
import ErrorDebugComponent from '../lib/error-debug'
|
||||
import PageLoader from '../lib/page-loader'
|
||||
|
||||
// Polyfill Promise globally
|
||||
// This is needed because Webpack2's dynamic loading(common chunks) code
|
||||
|
@ -19,31 +19,50 @@ if (!window.Promise) {
|
|||
|
||||
const {
|
||||
__NEXT_DATA__: {
|
||||
component,
|
||||
errorComponent,
|
||||
props,
|
||||
err,
|
||||
pathname,
|
||||
query
|
||||
query,
|
||||
buildId,
|
||||
assetPrefix
|
||||
},
|
||||
location
|
||||
} = window
|
||||
|
||||
const Component = evalScript(component).default
|
||||
const ErrorComponent = evalScript(errorComponent).default
|
||||
let lastAppProps
|
||||
|
||||
export const router = createRouter(pathname, query, getURL(), {
|
||||
Component,
|
||||
ErrorComponent,
|
||||
err
|
||||
const pageLoader = new PageLoader(buildId, assetPrefix)
|
||||
window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
|
||||
pageLoader.registerPage(route, fn)
|
||||
})
|
||||
delete window.__NEXT_LOADED_PAGES__
|
||||
|
||||
window.__NEXT_REGISTER_PAGE = pageLoader.registerPage.bind(pageLoader)
|
||||
|
||||
const headManager = new HeadManager()
|
||||
const appContainer = document.getElementById('__next')
|
||||
const errorContainer = document.getElementById('__next-error')
|
||||
|
||||
export default () => {
|
||||
let lastAppProps
|
||||
export let router
|
||||
export let ErrorComponent
|
||||
let Component
|
||||
|
||||
export default async () => {
|
||||
ErrorComponent = await pageLoader.loadPage('/_error')
|
||||
|
||||
try {
|
||||
Component = await pageLoader.loadPage(pathname)
|
||||
} catch (err) {
|
||||
console.error(`${err.message}\n${err.stack}`)
|
||||
Component = ErrorComponent
|
||||
}
|
||||
|
||||
router = createRouter(pathname, query, getURL(), {
|
||||
pageLoader,
|
||||
Component,
|
||||
ErrorComponent,
|
||||
err
|
||||
})
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
router.subscribe(({ Component, props, hash, err }) => {
|
||||
|
@ -57,7 +76,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
|
||||
}
|
||||
|
@ -103,7 +125,7 @@ async function doRender ({ Component, props, hash, err, emitter }) {
|
|||
}
|
||||
|
||||
if (emitter) {
|
||||
emitter.emit('before-reactdom-render', { Component })
|
||||
emitter.emit('before-reactdom-render', { Component, ErrorComponent })
|
||||
}
|
||||
|
||||
Component = Component || lastAppProps.Component
|
||||
|
@ -118,6 +140,6 @@ async function doRender ({ Component, props, hash, err, emitter }) {
|
|||
ReactDOM.render(createElement(App, appProps), appContainer)
|
||||
|
||||
if (emitter) {
|
||||
emitter.emit('after-reactdom-render', { Component })
|
||||
emitter.emit('after-reactdom-render', { Component, ErrorComponent })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,40 @@
|
|||
import evalScript from '../lib/eval-script'
|
||||
import 'react-hot-loader/patch'
|
||||
import ReactReconciler from 'react-dom/lib/ReactReconciler'
|
||||
|
||||
const { __NEXT_DATA__: { errorComponent } } = window
|
||||
const ErrorComponent = evalScript(errorComponent).default
|
||||
|
||||
require('react-hot-loader/patch')
|
||||
import initOnDemandEntries from './on-demand-entries-client'
|
||||
import initWebpackHMR from './webpack-hot-middleware-client'
|
||||
|
||||
const next = window.next = require('./')
|
||||
|
||||
const emitter = next.default()
|
||||
next.default()
|
||||
.then((emitter) => {
|
||||
initOnDemandEntries()
|
||||
initWebpackHMR()
|
||||
|
||||
let lastScroll
|
||||
|
||||
emitter.on('before-reactdom-render', ({ Component, ErrorComponent }) => {
|
||||
// Remember scroll when ErrorComponent is being rendered to later restore it
|
||||
if (!lastScroll && Component === ErrorComponent) {
|
||||
const { pageXOffset, pageYOffset } = window
|
||||
lastScroll = {
|
||||
x: pageXOffset,
|
||||
y: pageYOffset
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
emitter.on('after-reactdom-render', ({ Component, ErrorComponent }) => {
|
||||
if (lastScroll && Component !== ErrorComponent) {
|
||||
// Restore scroll after ErrorComponent was replaced with a page component by HMR
|
||||
const { x, y } = lastScroll
|
||||
window.scroll(x, y)
|
||||
lastScroll = null
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`${err.message}\n${err.stack}`)
|
||||
})
|
||||
|
||||
// This is a patch to catch most of the errors throw inside React components.
|
||||
const originalMountComponent = ReactReconciler.mountComponent
|
||||
|
@ -21,25 +47,3 @@ ReactReconciler.mountComponent = function (...args) {
|
|||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
let lastScroll
|
||||
|
||||
emitter.on('before-reactdom-render', ({ Component }) => {
|
||||
// Remember scroll when ErrorComponent is being rendered to later restore it
|
||||
if (!lastScroll && Component === ErrorComponent) {
|
||||
const { pageXOffset, pageYOffset } = window
|
||||
lastScroll = {
|
||||
x: pageXOffset,
|
||||
y: pageYOffset
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
emitter.on('after-reactdom-render', ({ Component }) => {
|
||||
if (lastScroll && Component !== ErrorComponent) {
|
||||
// Restore scroll after ErrorComponent was replaced with a page component by HMR
|
||||
const { x, y } = lastScroll
|
||||
window.scroll(x, y)
|
||||
lastScroll = null
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import next from './'
|
||||
|
||||
next()
|
||||
.catch((err) => {
|
||||
console.error(`${err.message}\n${err.stack}`)
|
||||
})
|
||||
|
|
|
@ -3,31 +3,33 @@
|
|||
import Router from '../lib/router'
|
||||
import fetch from 'unfetch'
|
||||
|
||||
Router.ready(() => {
|
||||
Router.router.events.on('routeChangeComplete', ping)
|
||||
})
|
||||
|
||||
async function ping () {
|
||||
try {
|
||||
const url = `/_next/on-demand-entries-ping?page=${Router.pathname}`
|
||||
const res = await fetch(url)
|
||||
const payload = await res.json()
|
||||
if (payload.invalid) {
|
||||
location.reload()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error with on-demand-entries-ping: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function runPinger () {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await ping()
|
||||
}
|
||||
}
|
||||
|
||||
runPinger()
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
export default () => {
|
||||
Router.ready(() => {
|
||||
Router.router.events.on('routeChangeComplete', ping)
|
||||
})
|
||||
|
||||
async function ping () {
|
||||
try {
|
||||
const url = `/_next/on-demand-entries-ping?page=${Router.pathname}`
|
||||
const res = await fetch(url)
|
||||
const payload = await res.json()
|
||||
if (payload.invalid) {
|
||||
location.reload()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error with on-demand-entries-ping: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function runPinger () {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await ping()
|
||||
}
|
||||
}
|
||||
|
||||
runPinger()
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true&path=/_next/webpack-hmr'
|
||||
import Router from '../lib/router'
|
||||
|
||||
const handlers = {
|
||||
reload (route) {
|
||||
if (route === '/_error') {
|
||||
for (const r of Object.keys(Router.components)) {
|
||||
const { err } = Router.components[r]
|
||||
if (err) {
|
||||
// reload all error routes
|
||||
// which are expected to be errors of '/_error' routes
|
||||
Router.reload(r)
|
||||
export default () => {
|
||||
const handlers = {
|
||||
reload (route) {
|
||||
if (route === '/_error') {
|
||||
for (const r of Object.keys(Router.components)) {
|
||||
const { err } = Router.components[r]
|
||||
if (err) {
|
||||
// reload all error routes
|
||||
// which are expected to be errors of '/_error' routes
|
||||
Router.reload(r)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (route === '/_document') {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
if (route === '/_document') {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
Router.reload(route)
|
||||
},
|
||||
|
||||
change (route) {
|
||||
if (route === '/_document') {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const { err } = Router.components[route] || {}
|
||||
if (err) {
|
||||
// reload to recover from runtime errors
|
||||
Router.reload(route)
|
||||
},
|
||||
|
||||
change (route) {
|
||||
if (route === '/_document') {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const { err } = Router.components[route] || {}
|
||||
if (err) {
|
||||
// reload to recover from runtime errors
|
||||
Router.reload(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webpackHotMiddlewareClient.subscribe((obj) => {
|
||||
const fn = handlers[obj.action]
|
||||
if (fn) {
|
||||
const data = obj.data || []
|
||||
fn(...data)
|
||||
} else {
|
||||
throw new Error('Unexpected action ' + obj.action)
|
||||
}
|
||||
})
|
||||
webpackHotMiddlewareClient.subscribe((obj) => {
|
||||
const fn = handlers[obj.action]
|
||||
if (fn) {
|
||||
const data = obj.data || []
|
||||
fn(...data)
|
||||
} else {
|
||||
throw new Error('Unexpected action ' + obj.action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ 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)
|
||||
static getInitialProps ({ res, err }) {
|
||||
const statusCode = res ? res.statusCode : (err ? err.statusCode : null)
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/**
|
||||
* IMPORTANT: This module is compiled *without* `use strict`
|
||||
* so that when we `eval` a dependency below, we don't enforce
|
||||
* `use strict` implicitly.
|
||||
*
|
||||
* Otherwise, modules like `d3` get `eval`d and forced into
|
||||
* `use strict` where they don't work (at least in current versions)
|
||||
*
|
||||
* To see the compilation details, look at `flyfile.js` and the
|
||||
* usage of `babel-plugin-transform-remove-strict-mode`.
|
||||
*/
|
||||
|
||||
export default function evalScript (script) {
|
||||
const module = { exports: {} }
|
||||
|
||||
eval(script) // eslint-disable-line no-eval
|
||||
return module.exports
|
||||
}
|
102
lib/page-loader.js
Normal file
102
lib/page-loader.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/* global window, document */
|
||||
import mitt from 'mitt'
|
||||
|
||||
const webpackModule = module
|
||||
|
||||
export default class PageLoader {
|
||||
constructor (buildId, assetPrefix) {
|
||||
this.buildId = buildId
|
||||
this.assetPrefix = assetPrefix
|
||||
|
||||
this.pageCache = {}
|
||||
this.pageLoadedHandlers = {}
|
||||
this.registerEvents = mitt()
|
||||
this.loadingRoutes = {}
|
||||
}
|
||||
|
||||
normalizeRoute (route) {
|
||||
if (route[0] !== '/') {
|
||||
throw new Error('Route name should start with a "/"')
|
||||
}
|
||||
|
||||
return route.replace(/index$/, '')
|
||||
}
|
||||
|
||||
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) => {
|
||||
const fire = ({ error, page }) => {
|
||||
this.registerEvents.off(route, fire)
|
||||
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(page)
|
||||
}
|
||||
}
|
||||
|
||||
this.registerEvents.on(route, fire)
|
||||
|
||||
// Load the script if not asked to load yet.
|
||||
if (!this.loadingRoutes[route]) {
|
||||
this.loadScript(route)
|
||||
this.loadingRoutes[route] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadScript (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
|
||||
const script = document.createElement('script')
|
||||
const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
||||
script.src = url
|
||||
script.type = 'text/javascript'
|
||||
script.onerror = () => {
|
||||
const error = new Error(`Error when loading route: ${route}`)
|
||||
this.registerEvents.emit(route, { error })
|
||||
}
|
||||
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
|
||||
// This method if called by the route code.
|
||||
registerPage (route, regFn) {
|
||||
const register = () => {
|
||||
const { error, page } = regFn()
|
||||
this.pageCache[route] = { error, page }
|
||||
this.registerEvents.emit(route, { error, page })
|
||||
}
|
||||
|
||||
// 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') {
|
||||
console.log(`Waiting webpack to became "idle" to initialize the page: "${route}"`)
|
||||
|
||||
const check = (status) => {
|
||||
if (status === 'idle') {
|
||||
webpackModule.hot.removeStatusHandler(check)
|
||||
register()
|
||||
}
|
||||
}
|
||||
webpackModule.hot.status(check)
|
||||
} else {
|
||||
register()
|
||||
}
|
||||
}
|
||||
|
||||
clearCache (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
delete this.pageCache[route]
|
||||
delete this.loadingRoutes[route]
|
||||
}
|
||||
}
|
|
@ -1,28 +1,28 @@
|
|||
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'
|
||||
import { _notifyBuildIdMismatch } from './'
|
||||
|
||||
const webpackModule = module
|
||||
|
||||
export default class Router {
|
||||
constructor (pathname, query, as, { Component, ErrorComponent, err } = {}) {
|
||||
constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
|
||||
// represents the current component key
|
||||
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()
|
||||
|
||||
this.pageLoader = pageLoader
|
||||
this.prefetchQueue = new PQueue({ concurrency: 2 })
|
||||
this.ErrorComponent = ErrorComponent
|
||||
this.pathname = pathname
|
||||
|
@ -77,7 +77,7 @@ export default class Router {
|
|||
|
||||
async reload (route) {
|
||||
delete this.components[route]
|
||||
delete this.fetchingRoutes[route]
|
||||
this.pageLoader.clearCache(route)
|
||||
|
||||
if (route !== this.route) return
|
||||
|
||||
|
@ -186,11 +186,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 +199,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.statusCode === 404) {
|
||||
// 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 +282,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 +294,7 @@ export default class Router {
|
|||
this.componentLoadCancel = null
|
||||
}
|
||||
|
||||
return newData
|
||||
return Component
|
||||
}
|
||||
|
||||
async getInitialProps (Component, ctx) {
|
||||
|
@ -324,24 +317,8 @@ export default class Router {
|
|||
return props
|
||||
}
|
||||
|
||||
fetchRoute (route) {
|
||||
let promise = this.fetchingRoutes[route]
|
||||
if (!promise) {
|
||||
promise = this.fetchingRoutes[route] = this.doFetchRoute(route)
|
||||
}
|
||||
|
||||
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' }
|
||||
})
|
||||
async fetchRoute (route) {
|
||||
return await this.pageLoader.loadPage(route)
|
||||
}
|
||||
|
||||
abortComponentLoad (as) {
|
||||
|
@ -365,22 +342,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 }
|
||||
}
|
||||
|
|
15
readme.md
15
readme.md
|
@ -34,6 +34,7 @@ Next.js is a minimalistic framework for server-rendered React applications.
|
|||
- [Custom configuration](#custom-configuration)
|
||||
- [Customizing webpack config](#customizing-webpack-config)
|
||||
- [Customizing babel config](#customizing-babel-config)
|
||||
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
|
||||
- [Production deployment](#production-deployment)
|
||||
- [FAQ](#faq)
|
||||
- [Contributing](#contributing)
|
||||
|
@ -704,6 +705,20 @@ Here's an example `.babelrc` file:
|
|||
}
|
||||
```
|
||||
|
||||
### CDN support with Asset Prefix
|
||||
|
||||
To set up a CDN, you can set up the `assetPrefix` setting and configure your CDN's origin to resolve to the domain that Next.js is hosted on.
|
||||
|
||||
```js
|
||||
const isProd = process.NODE_ENV === 'production'
|
||||
module.exports = {
|
||||
// You may only need to add assetPrefix in the production.
|
||||
assetPrefix: isProd ? 'https://cdn.mydomain.com' : ''
|
||||
}
|
||||
```
|
||||
|
||||
Note: Next.js will automatically use that prefix the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration).
|
||||
|
||||
## Production deployment
|
||||
|
||||
To deploy, instead of running `next`, you want to build for production usage ahead of time. Therefore, building and starting are separate commands:
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
export default class JsonPagesPlugin {
|
||||
apply (compiler) {
|
||||
compiler.plugin('after-compile', (compilation, callback) => {
|
||||
const pages = Object
|
||||
.keys(compilation.assets)
|
||||
.filter((filename) => /^bundles[/\\]pages.*\.js$/.test(filename))
|
||||
|
||||
pages.forEach((pageName) => {
|
||||
const page = compilation.assets[pageName]
|
||||
delete compilation.assets[pageName]
|
||||
|
||||
const content = page.source()
|
||||
const newContent = JSON.stringify({ component: content })
|
||||
|
||||
compilation.assets[`${pageName}on`] = {
|
||||
source: () => newContent,
|
||||
size: () => newContent.length
|
||||
}
|
||||
})
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
}
|
33
server/build/plugins/pages-plugin.js
Normal file
33
server/build/plugins/pages-plugin.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
export default class PagesPlugin {
|
||||
apply (compiler) {
|
||||
const isBundledPage = /^bundles[/\\]pages.*\.js$/
|
||||
const matchRouteName = /^bundles[/\\]pages[/\\](.*)\.js$/
|
||||
|
||||
compiler.plugin('after-compile', (compilation, callback) => {
|
||||
const pages = Object
|
||||
.keys(compilation.namedChunks)
|
||||
.map(key => compilation.namedChunks[key])
|
||||
.filter(chunk => isBundledPage.test(chunk.name))
|
||||
|
||||
pages.forEach((chunk) => {
|
||||
const page = compilation.assets[chunk.name]
|
||||
const pageName = matchRouteName.exec(chunk.name)[1]
|
||||
const routeName = `/${pageName.replace(/[/\\]?index$/, '')}`
|
||||
|
||||
const content = page.source()
|
||||
const newContent = `
|
||||
window.__NEXT_REGISTER_PAGE('${routeName}', function() {
|
||||
var comp = ${content}
|
||||
return { page: comp.default }
|
||||
})
|
||||
`
|
||||
// Replace the exisiting chunk with the new content
|
||||
compilation.assets[chunk.name] = {
|
||||
source: () => newContent,
|
||||
size: () => newContent.length
|
||||
}
|
||||
})
|
||||
callback()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import WriteFilePlugin from 'write-file-webpack-plugin'
|
|||
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
|
||||
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
|
||||
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
|
||||
import JsonPagesPlugin from './plugins/json-pages-plugin'
|
||||
import PagesPlugin from './plugins/pages-plugin'
|
||||
import CombineAssetsPlugin from './plugins/combine-assets-plugin'
|
||||
import getConfig from '../config'
|
||||
import * as babelCore from 'babel-core'
|
||||
|
@ -114,7 +114,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
|
||||
}),
|
||||
new JsonPagesPlugin(),
|
||||
new PagesPlugin(),
|
||||
new CaseSensitivePathPlugin()
|
||||
]
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ const cache = new Map()
|
|||
const defaultConfig = {
|
||||
webpack: null,
|
||||
poweredByHeader: true,
|
||||
distDir: '.next'
|
||||
distDir: '.next',
|
||||
assetPrefix: ''
|
||||
}
|
||||
|
||||
export default function getConfig (dir) {
|
||||
|
|
|
@ -34,9 +34,45 @@ export class Head extends Component {
|
|||
_documentProps: PropTypes.any
|
||||
}
|
||||
|
||||
getChunkPreloadLink (filename) {
|
||||
const { __NEXT_DATA__ } = this.context._documentProps
|
||||
let { buildStats, assetPrefix } = __NEXT_DATA__
|
||||
const hash = buildStats ? buildStats[filename].hash : '-'
|
||||
|
||||
return (
|
||||
<link
|
||||
key={filename}
|
||||
rel='preload'
|
||||
href={`${assetPrefix}/_next/${hash}/${filename}`}
|
||||
as='script'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getPreloadMainLinks () {
|
||||
const { dev } = this.context._documentProps
|
||||
if (dev) {
|
||||
return [
|
||||
this.getChunkPreloadLink('manifest.js'),
|
||||
this.getChunkPreloadLink('commons.js'),
|
||||
this.getChunkPreloadLink('main.js')
|
||||
]
|
||||
}
|
||||
|
||||
// In the production mode, we have a single asset with all the JS content.
|
||||
return [
|
||||
this.getChunkPreloadLink('app.js')
|
||||
]
|
||||
}
|
||||
|
||||
render () {
|
||||
const { head, styles } = this.context._documentProps
|
||||
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { pathname, buildId, assetPrefix } = __NEXT_DATA__
|
||||
|
||||
return <head>
|
||||
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pathname}`} as='script' />
|
||||
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error`} as='script' />
|
||||
{this.getPreloadMainLinks()}
|
||||
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
|
||||
{styles || null}
|
||||
{this.props.children}
|
||||
|
@ -67,13 +103,13 @@ export class NextScript extends Component {
|
|||
|
||||
getChunkScript (filename, additionalProps = {}) {
|
||||
const { __NEXT_DATA__ } = this.context._documentProps
|
||||
let { buildStats } = __NEXT_DATA__
|
||||
let { buildStats, assetPrefix } = __NEXT_DATA__
|
||||
const hash = buildStats ? buildStats[filename].hash : '-'
|
||||
|
||||
return (
|
||||
<script
|
||||
type='text/javascript'
|
||||
src={`/_next/${hash}/${filename}`}
|
||||
src={`${assetPrefix}/_next/${hash}/${filename}`}
|
||||
{...additionalProps}
|
||||
/>
|
||||
)
|
||||
|
@ -98,11 +134,22 @@ export class NextScript extends Component {
|
|||
|
||||
render () {
|
||||
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { pathname, buildId, assetPrefix } = __NEXT_DATA__
|
||||
|
||||
return <div>
|
||||
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
||||
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
|
||||
__html: `
|
||||
__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}
|
||||
module={}
|
||||
__NEXT_LOADED_PAGES__ = []
|
||||
|
||||
__NEXT_REGISTER_PAGE = function (route, fn) {
|
||||
__NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
|
||||
}
|
||||
`
|
||||
}} />}
|
||||
<script async type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pathname}`} />
|
||||
<script async type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error`} />
|
||||
{staticMarkup ? null : this.getScripts()}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import onDemandEntryHandler from './on-demand-entry-handler'
|
|||
import isWindowsBash from 'is-windows-bash'
|
||||
import webpack from './build/webpack'
|
||||
import clean from './build/clean'
|
||||
import readPage from './read-page'
|
||||
import getConfig from './config'
|
||||
|
||||
export default class HotReloader {
|
||||
|
@ -205,7 +204,6 @@ export default class HotReloader {
|
|||
|
||||
function deleteCache (path) {
|
||||
delete require.cache[path]
|
||||
delete readPage.cache[path]
|
||||
}
|
||||
|
||||
function diff (a, b) {
|
||||
|
|
|
@ -6,10 +6,10 @@ import http, { STATUS_CODES } from 'http'
|
|||
import {
|
||||
renderToHTML,
|
||||
renderErrorToHTML,
|
||||
renderJSON,
|
||||
renderErrorJSON,
|
||||
sendHTML,
|
||||
serveStatic
|
||||
serveStatic,
|
||||
renderScript,
|
||||
renderScriptError
|
||||
} from './render'
|
||||
import Router from './router'
|
||||
import HotReloader from './hot-reloader'
|
||||
|
@ -41,7 +41,8 @@ export default class Server {
|
|||
dir: this.dir,
|
||||
hotReloader: this.hotReloader,
|
||||
buildStats: this.buildStats,
|
||||
buildId: this.buildId
|
||||
buildId: this.buildId,
|
||||
assetPrefix: this.config.assetPrefix.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
this.defineRoutes()
|
||||
|
@ -122,17 +123,44 @@ export default class Server {
|
|||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_next/:buildId/pages/:path*': async (req, res, params) => {
|
||||
'/_next/:buildId/page/_error': async (req, res, params) => {
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ buildIdMismatch: true }))
|
||||
return
|
||||
const error = new Error('INVALID_BUILD_ID')
|
||||
const customFields = { buildIdMismatched: true }
|
||||
|
||||
return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
|
||||
}
|
||||
|
||||
const paths = params.path || ['index']
|
||||
const pathname = `/${paths.join('/')}`
|
||||
const p = join(this.dir, '.next/bundles/pages/_error.js')
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
await this.renderJSON(req, res, pathname)
|
||||
'/_next/:buildId/page/:path*': async (req, res, params) => {
|
||||
const paths = params.path || ['']
|
||||
const page = `/${paths.join('/')}`
|
||||
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
const error = new Error('INVALID_BUILD_ID')
|
||||
const customFields = { buildIdMismatched: true }
|
||||
|
||||
return await renderScriptError(req, res, page, error, customFields, this.renderOpts)
|
||||
}
|
||||
|
||||
if (this.dev) {
|
||||
try {
|
||||
await this.hotReloader.ensurePage(page)
|
||||
} catch (error) {
|
||||
return await renderScriptError(req, res, page, error, {}, this.renderOpts)
|
||||
}
|
||||
|
||||
const compilationErr = this.getCompilationError(page)
|
||||
if (compilationErr) {
|
||||
const customFields = { statusCode: 500 }
|
||||
return await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts)
|
||||
}
|
||||
}
|
||||
|
||||
await renderScript(req, res, page, this.renderOpts)
|
||||
},
|
||||
|
||||
'/_next/:path+': async (req, res, params) => {
|
||||
|
@ -256,40 +284,6 @@ export default class Server {
|
|||
return this.renderError(null, req, res, pathname, query)
|
||||
}
|
||||
|
||||
async renderJSON (req, res, page) {
|
||||
if (this.dev) {
|
||||
const compilationErr = this.getCompilationError(page)
|
||||
if (compilationErr) {
|
||||
return this.renderErrorJSON(compilationErr, req, res)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await renderJSON(req, res, page, this.renderOpts)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.statusCode = 404
|
||||
return this.renderErrorJSON(null, req, res)
|
||||
} else {
|
||||
if (!this.quiet) console.error(err)
|
||||
res.statusCode = 500
|
||||
return this.renderErrorJSON(err, req, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renderErrorJSON (err, req, res) {
|
||||
if (this.dev) {
|
||||
const compilationErr = this.getCompilationError('/_error')
|
||||
if (compilationErr) {
|
||||
res.statusCode = 500
|
||||
return renderErrorJSON(compilationErr, req, res, this.renderOpts)
|
||||
}
|
||||
}
|
||||
|
||||
return renderErrorJSON(err, req, res, this.renderOpts)
|
||||
}
|
||||
|
||||
async serveStatic (req, res, path) {
|
||||
try {
|
||||
return await serveStatic(req, res, path)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import fs from 'mz/fs'
|
||||
import resolve from './resolve'
|
||||
|
||||
/**
|
||||
* resolve a JSON page like `require.resolve`,
|
||||
* and read and cache the file content
|
||||
*/
|
||||
|
||||
async function readPage (path) {
|
||||
const f = await resolve(path)
|
||||
if (cache.hasOwnProperty(f)) {
|
||||
return cache[f]
|
||||
}
|
||||
|
||||
const source = await fs.readFile(f, 'utf8')
|
||||
const { component } = JSON.parse(source)
|
||||
|
||||
cache[f] = component
|
||||
return component
|
||||
}
|
||||
|
||||
export default readPage
|
||||
export const cache = {}
|
||||
|
||||
readPage.cache = cache
|
|
@ -5,7 +5,6 @@ import send from 'send'
|
|||
import requireModule from './require'
|
||||
import getConfig from './config'
|
||||
import resolvePath from './resolve'
|
||||
import readPage from './read-page'
|
||||
import { Router } from '../lib/router'
|
||||
import { loadGetInitialProps } from '../lib/utils'
|
||||
import Head, { defaultHead } from '../lib/head'
|
||||
|
@ -36,6 +35,7 @@ async function doRender (req, res, pathname, query, {
|
|||
buildId,
|
||||
buildStats,
|
||||
hotReloader,
|
||||
assetPrefix,
|
||||
dir = process.cwd(),
|
||||
dev = false,
|
||||
staticMarkup = false
|
||||
|
@ -53,16 +53,7 @@ async function doRender (req, res, pathname, query, {
|
|||
Component = Component.default || Component
|
||||
Document = Document.default || Document
|
||||
const ctx = { err, req, res, pathname, query }
|
||||
|
||||
const [
|
||||
props,
|
||||
component,
|
||||
errorComponent
|
||||
] = await Promise.all([
|
||||
loadGetInitialProps(Component, ctx),
|
||||
readPage(join(dir, dist, 'bundles', 'pages', page)),
|
||||
readPage(join(dir, dist, 'bundles', 'pages', '_error'))
|
||||
])
|
||||
const props = await loadGetInitialProps(Component, ctx)
|
||||
|
||||
// the response might be finshed on the getinitialprops call
|
||||
if (res.finished) return
|
||||
|
@ -98,13 +89,12 @@ async function doRender (req, res, pathname, query, {
|
|||
|
||||
const doc = createElement(Document, {
|
||||
__NEXT_DATA__: {
|
||||
component,
|
||||
errorComponent,
|
||||
props,
|
||||
pathname,
|
||||
query,
|
||||
buildId,
|
||||
buildStats,
|
||||
assetPrefix,
|
||||
err: (err && dev) ? errorToJSON(err) : null
|
||||
},
|
||||
dev,
|
||||
|
@ -115,21 +105,47 @@ async function doRender (req, res, pathname, query, {
|
|||
return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
|
||||
}
|
||||
|
||||
export async function renderJSON (req, res, page, { dir = process.cwd(), hotReloader } = {}) {
|
||||
const dist = getConfig(dir).distDir
|
||||
await ensurePage(page, { dir, hotReloader })
|
||||
const pagePath = await resolvePath(join(dir, dist, 'bundles', 'pages', page))
|
||||
return serveStatic(req, res, pagePath)
|
||||
export async function renderScript (req, res, page, opts) {
|
||||
try {
|
||||
const path = join(opts.dir, '.next', 'bundles', 'pages', page)
|
||||
const realPath = await resolvePath(path)
|
||||
await serveStatic(req, res, realPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
renderScriptError(req, res, page, err, {}, opts)
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) {
|
||||
const dist = getConfig(dir).distDir
|
||||
const component = await readPage(join(dir, dist, 'bundles', 'pages', '_error'))
|
||||
export async function renderScriptError (req, res, page, error, customFields, opts) {
|
||||
if (error.code === 'ENOENT') {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
res.end(`
|
||||
window.__NEXT_REGISTER_PAGE('${page}', function() {
|
||||
var error = new Error('Page not exists: ${page}')
|
||||
error.statusCode = 404
|
||||
|
||||
sendJSON(res, {
|
||||
component,
|
||||
err: err && dev ? errorToJSON(err) : null
|
||||
}, req.method)
|
||||
return { error: error }
|
||||
})
|
||||
`)
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
const errorJson = {
|
||||
...errorToJSON(error),
|
||||
...customFields
|
||||
}
|
||||
|
||||
res.end(`
|
||||
window.__NEXT_REGISTER_PAGE('${page}', function() {
|
||||
var error = ${JSON.stringify(errorJson)}
|
||||
return { error: error }
|
||||
})
|
||||
`)
|
||||
}
|
||||
|
||||
export function sendHTML (res, html, method) {
|
||||
|
|
|
@ -31,13 +31,13 @@ describe('On Demand Entries', () => {
|
|||
})
|
||||
|
||||
it('should compile pages for JSON page requests', async () => {
|
||||
const pageContent = await renderViaHTTP(context.appPort, '/_next/-/pages/about')
|
||||
const pageContent = await renderViaHTTP(context.appPort, '/_next/-/page/about')
|
||||
expect(pageContent.includes('About Page')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should dispose inactive pages', async () => {
|
||||
await renderViaHTTP(context.appPort, '/_next/-/pages/about')
|
||||
const aboutPagePath = resolve(__dirname, '../.next/bundles/pages/about.json')
|
||||
const aboutPagePath = resolve(__dirname, '../.next/bundles/pages/about.js')
|
||||
expect(existsSync(aboutPagePath)).toBeTruthy()
|
||||
|
||||
// Wait maximum of jasmine.DEFAULT_TIMEOUT_INTERVAL checking
|
||||
|
|
3
test/integration/production/pages/about.js
Normal file
3
test/integration/production/pages/about.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<div className='about-page'>About Page</div>
|
||||
)
|
|
@ -1,3 +1,8 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<div>Hello World</div>
|
||||
<div>
|
||||
<Link href='/about'><a>About Page</a></Link>
|
||||
<p>Hello World</p>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* global jasmine, describe, it, expect, beforeAll, afterAll */
|
||||
|
||||
import fetch from 'node-fetch'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
nextServer,
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
stopApp,
|
||||
renderViaHTTP
|
||||
} from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
|
||||
const appDir = join(__dirname, '../')
|
||||
let appPort
|
||||
|
@ -37,32 +37,16 @@ describe('Production Usage', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('JSON pages', () => {
|
||||
describe('when asked for a normal page', () => {
|
||||
it('should serve the normal page', async () => {
|
||||
const url = `http://localhost:${appPort}/_next/${app.renderOpts.buildId}/pages`
|
||||
const res = await fetch(url, { compress: false })
|
||||
expect(res.headers.get('Content-Encoding')).toBeNull()
|
||||
describe('With navigation', () => {
|
||||
it('should navigate via client side', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
const text = await browser
|
||||
.elementByCss('a').click()
|
||||
.waitForElementByCss('.about-page')
|
||||
.elementByCss('div').text()
|
||||
|
||||
const page = await res.json()
|
||||
expect(page.component).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when asked for a page with an unknown encoding', () => {
|
||||
it('should serve the normal page', async () => {
|
||||
const url = `http://localhost:${appPort}/_next/${app.renderOpts.buildId}/pages`
|
||||
const res = await fetch(url, {
|
||||
compress: false,
|
||||
headers: {
|
||||
'Accept-Encoding': 'br'
|
||||
}
|
||||
})
|
||||
expect(res.headers.get('Content-Encoding')).toBeNull()
|
||||
|
||||
const page = await res.json()
|
||||
expect(page.component).toBeDefined()
|
||||
})
|
||||
expect(text).toBe('About Page')
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,52 +1,53 @@
|
|||
/* global describe, it, expect */
|
||||
import Router from '../../dist/lib/router/router'
|
||||
|
||||
class PageLoader {
|
||||
constructor (options = {}) {
|
||||
this.options = options
|
||||
this.loaded = {}
|
||||
}
|
||||
|
||||
loadPage (route) {
|
||||
this.loaded[route] = true
|
||||
|
||||
if (this.options.delay) {
|
||||
return new Promise((resolve) => setTimeout(resolve, this.options.delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Router', () => {
|
||||
const request = { clone: () => null }
|
||||
describe('.prefetch()', () => {
|
||||
it('should prefetch a given page', async () => {
|
||||
const router = new Router('/', {})
|
||||
const promise = Promise.resolve(request)
|
||||
const route = 'routex'
|
||||
router.doFetchRoute = (r) => {
|
||||
expect(r).toBe(route)
|
||||
return promise
|
||||
}
|
||||
const pageLoader = new PageLoader()
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
const route = '/routex'
|
||||
await router.prefetch(route)
|
||||
|
||||
expect(router.fetchingRoutes[route]).toBe(promise)
|
||||
})
|
||||
|
||||
it('should stop if it\'s prefetching already', async () => {
|
||||
const router = new Router('/', {})
|
||||
const route = 'routex'
|
||||
router.fetchingRoutes[route] = Promise.resolve(request)
|
||||
router.doFetchRoute = () => { throw new Error('Should not happen') }
|
||||
await router.prefetch(route)
|
||||
expect(pageLoader.loaded['/routex']).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should only run two jobs at a time', async () => {
|
||||
const router = new Router('/', {})
|
||||
let count = 0
|
||||
|
||||
router.doFetchRoute = () => {
|
||||
count++
|
||||
return new Promise((resolve) => {})
|
||||
}
|
||||
// delay loading pages for an hour
|
||||
const pageLoader = new PageLoader({ delay: 1000 * 3600 })
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
|
||||
router.prefetch('route1')
|
||||
router.prefetch('route2')
|
||||
router.prefetch('route3')
|
||||
router.prefetch('route4')
|
||||
|
||||
// Wait for a bit
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(count).toBe(2)
|
||||
expect(Object.keys(router.fetchingRoutes)).toEqual(['route1', 'route2'])
|
||||
expect(Object.keys(pageLoader.loaded).length).toBe(2)
|
||||
expect(Object.keys(pageLoader.loaded)).toEqual(['route1', 'route2'])
|
||||
})
|
||||
|
||||
it('should run all the jobs', async () => {
|
||||
const router = new Router('/', {})
|
||||
const pageLoader = new PageLoader()
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
const routes = ['route1', 'route2', 'route3', 'route4']
|
||||
|
||||
router.doFetchRoute = () => Promise.resolve(request)
|
||||
|
@ -56,7 +57,7 @@ describe('Router', () => {
|
|||
await router.prefetch(routes[2])
|
||||
await router.prefetch(routes[3])
|
||||
|
||||
expect(Object.keys(router.fetchingRoutes)).toEqual(routes)
|
||||
expect(Object.keys(pageLoader.loaded)).toEqual(routes)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue