1
0
Fork 0
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:
Arunoda Susiripala 2017-04-18 01:45:50 +05:30
parent f51300f14b
commit dfa28815a5
12 changed files with 198 additions and 46 deletions

View file

@ -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
View file

@ -0,0 +1 @@
module.exports = require('./dist/lib/dynamic')

View file

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

View file

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

View file

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

View file

@ -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 () {}
}
)
`))

View file

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

View 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()
})
}
}

View file

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

View file

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

View file

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