mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Add server side rendering for dynamic imports.
This commit is contained in:
parent
f51300f14b
commit
dfa28815a5
|
@ -23,7 +23,8 @@ const {
|
|||
err,
|
||||
pathname,
|
||||
query,
|
||||
buildId
|
||||
buildId,
|
||||
chunks
|
||||
},
|
||||
location
|
||||
} = window
|
||||
|
@ -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')
|
||||
|
@ -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 {
|
||||
|
|
1
dynamic.js
Normal file
1
dynamic.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('./dist/lib/dynamic')
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function withImport (promise, Loading = () => (<p>Loading...</p>)) {
|
||||
return class Comp extends React.Component {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
this.state = { AsyncComponent: null }
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
promise.then((AsyncComponent) => {
|
||||
this.setState({ AsyncComponent })
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { AsyncComponent } = this.state
|
||||
if (!AsyncComponent) return (<Loading {...this.props} />)
|
||||
|
||||
return <AsyncComponent {...this.props} />
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
44
lib/dynamic.js
Normal file
44
lib/dynamic.js
Normal file
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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]) {
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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]
|
||||
|
|
|
@ -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 () {}
|
||||
}
|
||||
)
|
||||
`))
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
33
server/build/plugins/dynamic-chunks-plugin.js
Normal file
33
server/build/plugins/dynamic-chunks-plugin.js
Normal file
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
@ -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()
|
||||
]
|
||||
|
||||
|
@ -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'),
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -64,6 +64,18 @@ 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__
|
||||
|
@ -71,6 +83,7 @@ export class Head extends Component {
|
|||
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}
|
||||
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { loadGetInitialProps } from '../lib/utils'
|
|||
import Head, { defaultHead } from '../lib/head'
|
||||
import App from '../lib/app'
|
||||
import ErrorDebug from '../lib/error-debug'
|
||||
import { flushChunks } from '../lib/dynamic'
|
||||
|
||||
export async function render (req, res, pathname, query, opts) {
|
||||
const html = await renderToHTML(req, res, pathname, opts)
|
||||
|
@ -74,12 +75,13 @@ async function doRender (req, res, pathname, query, {
|
|||
} finally {
|
||||
head = Head.rewind() || defaultHead()
|
||||
}
|
||||
const chunks = flushChunks()
|
||||
|
||||
if (err && dev) {
|
||||
errorHtml = render(createElement(ErrorDebug, { error: err }))
|
||||
}
|
||||
|
||||
return { html, head, errorHtml }
|
||||
return { html, head, errorHtml, chunks }
|
||||
}
|
||||
|
||||
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
|
||||
|
|
Loading…
Reference in a new issue