Skip to content

Commit

Permalink
Add server side rendering for dynamic imports.
Browse files Browse the repository at this point in the history
  • Loading branch information
arunoda committed Apr 17, 2017
1 parent f51300f commit dfa2881
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 46 deletions.
14 changes: 13 additions & 1 deletion client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const {
err,
pathname,
query,
buildId
buildId,
chunks
},
location
} = window
Expand All @@ -34,7 +35,13 @@ window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
})
delete window.__NEXT_LOADED_PAGES__

window.__NEXT_LOADED_CHUNKS__.forEach(({ chunkName, fn }) => {
pageLoader.registerChunk(chunkName, fn)
})
delete window.__NEXT_LOADED_CHUNKS__

window.__NEXT_REGISTER_PAGE = pageLoader.registerPage.bind(pageLoader)
window.__NEXT_REGISTER_CHUNK = pageLoader.registerChunk.bind(pageLoader)

const headManager = new HeadManager()
const appContainer = document.getElementById('__next')
Expand All @@ -46,6 +53,11 @@ export let ErrorComponent
let Component

export default async () => {
// Wait for all the dynamic chunks to get loaded
for (const chunkName of chunks) {
await pageLoader.waitForChunk(chunkName)
}

ErrorComponent = await pageLoader.loadPage('/_error')

try {
Expand Down
1 change: 1 addition & 0 deletions dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/lib/dynamic')
23 changes: 0 additions & 23 deletions examples/with-dynamic-import/lib/with-import.js

This file was deleted.

4 changes: 2 additions & 2 deletions examples/with-dynamic-import/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import Header from '../components/Header'
import Counter from '../components/Counter'
import withImport from '../lib/with-import'
import dynamic from 'next/dynamic'

const DynamicComponent = withImport(import('../components/hello'))
const DynamicComponent = dynamic(import('../components/hello'))

export default () => (
<div>
Expand Down
44 changes: 44 additions & 0 deletions lib/dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'

let currentChunks = []

export default function dynamicComponent (promise, Loading = () => (<p>Loading...</p>)) {
return class Comp extends React.Component {
constructor (...args) {
super(...args)
this.state = { AsyncComponent: null }
this.isServer = typeof window === 'undefined'
this.loadComponent()
}

loadComponent () {
promise.then((AsyncComponent) => {
if (this.mounted) {
this.setState({ AsyncComponent })
} else {
if (this.isServer) {
currentChunks.push(AsyncComponent.__webpackChunkName)
}
this.state.AsyncComponent = AsyncComponent
}
})
}

componentDidMount () {
this.mounted = true
}

render () {
const { AsyncComponent } = this.state
if (!AsyncComponent) return (<Loading {...this.props} />)

return <AsyncComponent {...this.props} />
}
}
}

export function flushChunks () {
const chunks = currentChunks
currentChunks = []
return chunks
}
35 changes: 30 additions & 5 deletions lib/page-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ export default class PageLoader {
this.buildId = buildId
this.pageCache = {}
this.pageLoadedHandlers = {}
this.registerEvents = mitt()
this.pageRegisterEvents = mitt()
this.loadingRoutes = {}

this.chunkRegisterEvents = mitt()
this.loadedChunks = {}
}

normalizeRoute (route) {
Expand All @@ -33,7 +36,7 @@ export default class PageLoader {

return new Promise((resolve, reject) => {
const fire = ({ error, page }) => {
this.registerEvents.off(route, fire)
this.pageRegisterEvents.off(route, fire)

if (error) {
reject(error)
Expand All @@ -42,7 +45,7 @@ export default class PageLoader {
}
}

this.registerEvents.on(route, fire)
this.pageRegisterEvents.on(route, fire)

// Load the script if not asked to load yet.
if (!this.loadingRoutes[route]) {
Expand All @@ -61,7 +64,7 @@ export default class PageLoader {
script.type = 'text/javascript'
script.onerror = () => {
const error = new Error(`Error when loading route: ${route}`)
this.registerEvents.emit(route, { error })
this.pageRegisterEvents.emit(route, { error })
}

document.body.appendChild(script)
Expand All @@ -72,7 +75,7 @@ export default class PageLoader {
const register = () => {
const { error, page } = regFn()
this.pageCache[route] = { error, page }
this.registerEvents.emit(route, { error, page })
this.pageRegisterEvents.emit(route, { error, page })
}

// Wait for webpack to became idle if it's not.
Expand All @@ -92,6 +95,28 @@ export default class PageLoader {
}
}

registerChunk (chunkName, regFn) {
const chunk = regFn()
this.loadedChunks[chunkName] = true
this.chunkRegisterEvents.emit(chunkName, chunk)
}

waitForChunk (chunkName, regFn) {
const loadedChunk = this.loadedChunks[chunkName]
if (loadedChunk) {
return Promise.resolve(true)
}

return new Promise((resolve) => {
const register = (chunk) => {
this.chunkRegisterEvents.off(chunkName, register)
resolve(chunk)
}

this.chunkRegisterEvents.on(chunkName, register)
})
}

clearCache (route) {
route = this.normalizeRoute(route)
delete this.pageCache[route]
Expand Down
39 changes: 28 additions & 11 deletions server/build/babel/plugins/handle-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,35 @@ const TYPE_IMPORT = 'Import'

const buildImport = (args) => (template(`
(
new Promise((resolve) => {
if (process.pid) {
eval('require.ensure = (deps, callback) => (callback(require))')
}
typeof window === 'undefined' ?
{
then(cb) {
eval('require.ensure = function (deps, callback) { callback(require) }')
require.ensure([], (require) => {
let m = require(SOURCE)
m = m.default || m
m.__webpackChunkName = '${args.name}.js'
cb(m);
}, 'chunks/${args.name}.js');
},
catch() {}
} :
{
then(cb) {
const weakId = require.resolveWeak(SOURCE)
try {
const weakModule = __webpack_require__(weakId)
return cb(weakModule.default || weakModule)
} catch (err) {}
require.ensure([], (require) => {
let m = require(SOURCE)
m = m.default || m
m.__webpackChunkName = '${args.name}.js'
resolve(m);
}, 'chunks/${args.name}.js');
})
require.ensure([], (require) => {
let m = require(SOURCE)
m = m.default || m
cb(m);
}, 'chunks/${args.name}.js');
},
catch () {}
}
)
`))

Expand Down
1 change: 1 addition & 0 deletions server/build/babel/preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
'next/link': relativeResolve('../../../lib/link'),
'next/prefetch': relativeResolve('../../../lib/prefetch'),
'next/css': relativeResolve('../../../lib/css'),
'next/dynamic': relativeResolve('../../../lib/dynamic'),
'next/head': relativeResolve('../../../lib/head'),
'next/document': relativeResolve('../../../server/document'),
'next/router': relativeResolve('../../../lib/router'),
Expand Down
33 changes: 33 additions & 0 deletions server/build/plugins/dynamic-chunks-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export default class PagesPlugin {
apply (compiler) {
const isImportChunk = /^chunks[/\\].*\.js$/
const matchChunkName = /^chunks[/\\](.*)$/

compiler.plugin('after-compile', (compilation, callback) => {
const chunks = Object
.keys(compilation.namedChunks)
.map(key => compilation.namedChunks[key])
.filter(chunk => isImportChunk.test(chunk.name))

chunks.forEach((chunk) => {
const asset = compilation.assets[chunk.name]
if (!asset) return

const chunkName = matchChunkName.exec(chunk.name)[1]

const content = asset.source()
const newContent = `
window.__NEXT_REGISTER_CHUNK('${chunkName}', function() {
${content}
})
`
// Replace the exisiting chunk with the new content
compilation.assets[chunk.name] = {
source: () => newContent,
size: () => newContent.length
}
})
callback()
})
}
}
3 changes: 3 additions & 0 deletions server/build/webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
import PagesPlugin from './plugins/pages-plugin'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
import CombineAssetsPlugin from './plugins/combine-assets-plugin'
import getConfig from '../config'
import * as babelCore from 'babel-core'
Expand Down Expand Up @@ -117,6 +118,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
}),
new PagesPlugin(),
new DynamicChunksPlugin(),
new CaseSensitivePathPlugin()
]

Expand Down Expand Up @@ -221,6 +223,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
'next/link': relativeResolve('../../lib/link'),
'next/prefetch': relativeResolve('../../lib/prefetch'),
'next/css': relativeResolve('../../lib/css'),
'next/dynamic': relativeResolve('../../lib/dynamic'),
'next/head': relativeResolve('../../lib/head'),
'next/document': relativeResolve('../../server/document'),
'next/router': relativeResolve('../../lib/router'),
Expand Down
43 changes: 40 additions & 3 deletions server/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import flush from 'styled-jsx/server'

export default class Document extends Component {
static getInitialProps ({ renderPage }) {
const { html, head, errorHtml } = renderPage()
const { html, head, errorHtml, chunks } = renderPage()
const styles = flush()
return { html, head, errorHtml, styles }
return { html, head, errorHtml, chunks, styles }
}

static childContextTypes = {
Expand Down Expand Up @@ -64,13 +64,26 @@ export class Head extends Component {
]
}

getPreloadDynamicChunks () {
const { chunks } = this.context._documentProps
return chunks.map((chunk) => (
<link
key={chunk}
rel='preload'
href={`/_webpack/chunks/${chunk}`}
as='script'
/>
))
}

render () {
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
const { pathname, buildId } = __NEXT_DATA__

return <head>
<link rel='preload' href={`/_next/${buildId}/page${pathname}`} as='script' />
<link rel='preload' href={`/_next/${buildId}/page/_error`} as='script' />
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
{styles || null}
Expand Down Expand Up @@ -131,24 +144,48 @@ export class NextScript extends Component {
return this.getChunkScript('app.js', { async: true })
}

getDynamicChunks () {
const { chunks } = this.context._documentProps
return (
<div>
{chunks.map((chunk) => (
<script
async
key={chunk}
type='text/javascript'
src={`/_webpack/chunks/${chunk}`}
/>
))}
</div>
)
}

render () {
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps
const { pathname, buildId } = __NEXT_DATA__

__NEXT_DATA__.chunks = chunks

return <div>
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
__html: `
__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}
module={}
__NEXT_LOADED_PAGES__ = []
__NEXT_LOADED_CHUNKS__ = []
__NEXT_REGISTER_PAGE = function (route, fn) {
__NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
}
__NEXT_REGISTER_CHUNK = function (chunkName, fn) {
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
}
`
}} />}
<script async type='text/javascript' src={`/_next/${buildId}/page${pathname}`} />
<script async type='text/javascript' src={`/_next/${buildId}/page/_error`} />
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}
</div>
}
Expand Down
Loading

0 comments on commit dfa2881

Please sign in to comment.