Skip to content

Commit

Permalink
Add CDN support with assetPrefix (vercel#1700)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
arunoda authored and rauchg committed Apr 18, 2017
1 parent bdc30bc commit dec85fe
Show file tree
Hide file tree
Showing 24 changed files with 504 additions and 381 deletions.
54 changes: 38 additions & 16 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }) => {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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 })
}
}
62 changes: 33 additions & 29 deletions client/next-dev.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
})
3 changes: 3 additions & 0 deletions client/next.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import next from './'

next()
.catch((err) => {
console.error(`${err.message}\n${err.stack}`)
})
46 changes: 24 additions & 22 deletions client/on-demand-entries-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,33 @@
import Router from '../lib/router'
import fetch from 'unfetch'

Router.ready(() => {
Router.router.events.on('routeChangeComplete', ping)
})
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()
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}`)
}
} 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()
async function runPinger () {
while (true) {
await new Promise((resolve) => setTimeout(resolve, 5000))
await ping()
}
}
}

runPinger()
.catch((err) => {
console.error(err)
})
runPinger()
.catch((err) => {
console.error(err)
})
}
74 changes: 38 additions & 36 deletions client/webpack-hot-middleware-client.js
Original file line number Diff line number Diff line change
@@ -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)
},
Router.reload(route)
},

change (route) {
if (route === '/_document') {
window.location.reload()
return
}
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)
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)
}
})
}
4 changes: 2 additions & 2 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down
18 changes: 0 additions & 18 deletions lib/eval-script.js

This file was deleted.

Loading

0 comments on commit dec85fe

Please sign in to comment.