mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Dynamic component support with multiple modules (#2235)
* Layout ground works for next/async * Implement the Dynamic Bundle feature. * Add some test cases. * Update README. * Implement props aware dynamic bundle API. * Update tests and README. * Add a test case for React Context support.
This commit is contained in:
parent
f36b4f9b4a
commit
9df59c1176
3
examples/with-dynamic-import/components/hello6.js
Normal file
3
examples/with-dynamic-import/components/hello6.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<p>Hello World 6 (imported dynamiclly) </p>
|
||||
)
|
3
examples/with-dynamic-import/components/hello7.js
Normal file
3
examples/with-dynamic-import/components/hello7.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<p>Hello World 7 (imported dynamiclly) </p>
|
||||
)
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Header from '../components/Header'
|
||||
import Counter from '../components/Counter'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
@ -22,22 +23,64 @@ const DynamicComponentWithAsyncReactor = asyncReactor(async () => {
|
|||
|
||||
const DynamicComponent5 = dynamic(import('../components/hello5'))
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<DynamicComponent />
|
||||
<DynamicComponentWithCustomLoading />
|
||||
<DynamicComponentWithNoSSR />
|
||||
<DynamicComponentWithAsyncReactor />
|
||||
{
|
||||
/*
|
||||
Since DynamicComponent5 does not render in the client,
|
||||
it won't get downloaded.
|
||||
*/
|
||||
const DynamicBundle = dynamic({
|
||||
modules: (props) => {
|
||||
const components = {
|
||||
Hello6: import('../components/hello6')
|
||||
}
|
||||
{ React.noSuchField === true ? <DynamicComponent5 /> : null }
|
||||
|
||||
<p>HOME PAGE is here!</p>
|
||||
<Counter />
|
||||
</div>
|
||||
)
|
||||
if (props.showMore) {
|
||||
components.Hello7 = import('../components/hello7')
|
||||
}
|
||||
|
||||
return components
|
||||
},
|
||||
render: (props, { Hello6, Hello7 }) => (
|
||||
<div style={{padding: 10, border: '1px solid #888'}}>
|
||||
<Hello6 />
|
||||
{Hello7 ? <Hello7 /> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default class Index extends React.Component {
|
||||
static getInitialProps ({ query }) {
|
||||
return { showMore: Boolean(query.showMore) }
|
||||
}
|
||||
|
||||
toggleShowMore () {
|
||||
const { showMore } = this.props
|
||||
if (showMore) {
|
||||
Router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
Router.push('/?showMore=1')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showMore } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<DynamicComponent />
|
||||
<DynamicComponentWithCustomLoading />
|
||||
<DynamicComponentWithNoSSR />
|
||||
<DynamicComponentWithAsyncReactor />
|
||||
<DynamicBundle showMore={showMore} />
|
||||
<button onClick={() => this.toggleShowMore()}>Toggle Show More</button>
|
||||
{
|
||||
/*
|
||||
Since DynamicComponent5 does not render in the client,
|
||||
it won't get downloaded.
|
||||
*/
|
||||
}
|
||||
{ React.noSuchField === true ? <DynamicComponent5 /> : null }
|
||||
|
||||
<p>HOME PAGE is here!</p>
|
||||
<Counter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
120
lib/dynamic.js
120
lib/dynamic.js
|
@ -1,8 +1,29 @@
|
|||
import React from 'react'
|
||||
|
||||
let currentChunks = []
|
||||
let currentChunks = new Set()
|
||||
|
||||
export default function dynamicComponent (p, o) {
|
||||
let promise
|
||||
let options
|
||||
|
||||
if (p instanceof SameLoopPromise) {
|
||||
promise = p
|
||||
options = o || {}
|
||||
} else {
|
||||
// Now we are trying to use the modules and render fields in options to load modules.
|
||||
if (!p.modules || !p.render) {
|
||||
const errorMessage = 'Options to `next/dynamic` should contains `modules` and `render` fields.'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
if (o) {
|
||||
const errorMessage = 'Include options in the first argument which contains `modules` and `render` fields.'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
options = p
|
||||
}
|
||||
|
||||
export default function dynamicComponent (promise, options = {}) {
|
||||
return class DynamicComponent extends React.Component {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
|
@ -10,11 +31,24 @@ export default function dynamicComponent (promise, options = {}) {
|
|||
this.LoadingComponent = options.loading ? options.loading : () => (<p>loading...</p>)
|
||||
this.ssr = options.ssr === false ? options.ssr : true
|
||||
|
||||
this.state = { AsyncComponent: null }
|
||||
this.state = { AsyncComponent: null, asyncElement: null }
|
||||
this.isServer = typeof window === 'undefined'
|
||||
|
||||
// This flag is used to load the bundle again, if needed
|
||||
this.loadBundleAgain = null
|
||||
// This flag keeps track of the whether we are loading a bundle or not.
|
||||
this.loadingBundle = false
|
||||
|
||||
if (this.ssr) {
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
|
||||
load () {
|
||||
if (promise) {
|
||||
this.loadComponent()
|
||||
} else {
|
||||
this.loadBundle(this.props)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,33 +64,95 @@ export default function dynamicComponent (promise, options = {}) {
|
|||
this.setState({ AsyncComponent })
|
||||
} else {
|
||||
if (this.isServer) {
|
||||
currentChunks.push(AsyncComponent.__webpackChunkName)
|
||||
registerChunk(AsyncComponent.__webpackChunkName)
|
||||
}
|
||||
this.state.AsyncComponent = AsyncComponent
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadBundle (props) {
|
||||
this.loadBundleAgain = null
|
||||
this.loadingBundle = true
|
||||
|
||||
// Run this for prop changes as well.
|
||||
const modulePromiseMap = options.modules(props)
|
||||
const moduleNames = Object.keys(modulePromiseMap)
|
||||
let remainingPromises = moduleNames.length
|
||||
const moduleMap = {}
|
||||
|
||||
const renderModules = () => {
|
||||
if (this.loadBundleAgain) {
|
||||
this.loadBundle(this.loadBundleAgain)
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingBundle = false
|
||||
DynamicComponent.displayName = 'DynamicBundle'
|
||||
const asyncElement = options.render(props, moduleMap)
|
||||
if (this.mounted) {
|
||||
this.setState({ asyncElement })
|
||||
} else {
|
||||
this.state.asyncElement = asyncElement
|
||||
}
|
||||
}
|
||||
|
||||
const loadModule = (name) => {
|
||||
const promise = modulePromiseMap[name]
|
||||
promise.then((Component) => {
|
||||
if (this.isServer) {
|
||||
registerChunk(Component.__webpackChunkName)
|
||||
}
|
||||
moduleMap[name] = Component
|
||||
remainingPromises--
|
||||
if (remainingPromises === 0) {
|
||||
renderModules()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
moduleNames.forEach(loadModule)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.mounted = true
|
||||
if (!this.ssr) {
|
||||
this.loadComponent()
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { AsyncComponent } = this.state
|
||||
const { LoadingComponent } = this
|
||||
if (!AsyncComponent) return (<LoadingComponent {...this.props} />)
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (promise) return
|
||||
|
||||
return <AsyncComponent {...this.props} />
|
||||
this.setState({ asyncElement: null })
|
||||
|
||||
if (this.loadingBundle) {
|
||||
this.loadBundleAgain = nextProps
|
||||
return
|
||||
}
|
||||
|
||||
this.loadBundle(nextProps)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { AsyncComponent, asyncElement } = this.state
|
||||
const { LoadingComponent } = this
|
||||
|
||||
if (asyncElement) return asyncElement
|
||||
if (AsyncComponent) return (<AsyncComponent {...this.props} />)
|
||||
|
||||
return (<LoadingComponent {...this.props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerChunk (chunk) {
|
||||
currentChunks.add(chunk)
|
||||
}
|
||||
|
||||
export function flushChunks () {
|
||||
const chunks = currentChunks
|
||||
currentChunks = []
|
||||
const chunks = Array.from(currentChunks)
|
||||
currentChunks.clear()
|
||||
return chunks
|
||||
}
|
||||
|
||||
|
|
34
readme.md
34
readme.md
|
@ -657,23 +657,33 @@ export default () => (
|
|||
)
|
||||
```
|
||||
|
||||
#### 4. With [async-reactor](https://github.com/xtuc/async-reactor)
|
||||
|
||||
> SSR support is not available here
|
||||
#### 4. With Multiple Modules At Once
|
||||
|
||||
```js
|
||||
import { asyncReactor } from 'async-reactor'
|
||||
const DynamicComponentWithAsyncReactor = asyncReactor(async () => {
|
||||
const Hello4 = await import('../components/hello4')
|
||||
return (<Hello4 />)
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const HelloBundle = dynamic({
|
||||
modules: (props) => {
|
||||
const components {
|
||||
Hello1: import('../components/hello1'),
|
||||
Hello2: import('../components/hello2')
|
||||
}
|
||||
|
||||
// Add remove components based on props
|
||||
|
||||
return components
|
||||
},
|
||||
render: (props, { Hello1, Hello2 }) => (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
<Hello1 />
|
||||
<Hello2 />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<DynamicComponentWithAsyncReactor />
|
||||
<p>HOME PAGE is here!</p>
|
||||
</div>
|
||||
<HelloBundle title="Dynamic Bundle"/>
|
||||
)
|
||||
```
|
||||
|
||||
|
|
15
test/integration/basic/components/hello-context.js
Normal file
15
test/integration/basic/components/hello-context.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class extends React.Component {
|
||||
static contextTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data } = this.context
|
||||
return (
|
||||
<div>{data.title}</div>
|
||||
)
|
||||
}
|
||||
}
|
3
test/integration/basic/components/hello2.js
Normal file
3
test/integration/basic/components/hello2.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<p>Hello World 2</p>
|
||||
)
|
68
test/integration/basic/pages/dynamic/bundle.js
Normal file
68
test/integration/basic/pages/dynamic/bundle.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Router from 'next/router'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const HelloBundle = dynamic({
|
||||
modules: (props) => {
|
||||
const components = {
|
||||
HelloContext: import('../../components/hello-context'),
|
||||
Hello1: import('../../components/hello1')
|
||||
}
|
||||
|
||||
if (props.showMore) {
|
||||
components.Hello2 = import('../../components/hello2')
|
||||
}
|
||||
|
||||
return components
|
||||
},
|
||||
render: (props, { HelloContext, Hello1, Hello2 }) => (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
<HelloContext />
|
||||
<Hello1 />
|
||||
{Hello2? <Hello2 /> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default class Bundle extends React.Component {
|
||||
static getInitialProps ({ query }) {
|
||||
return { showMore: Boolean(query.showMore) }
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
data: { title: 'ZEIT Rocks' }
|
||||
}
|
||||
}
|
||||
|
||||
toggleShowMore () {
|
||||
if (this.props.showMore) {
|
||||
Router.push('/dynamic/bundle')
|
||||
return
|
||||
}
|
||||
|
||||
Router.push('/dynamic/bundle?showMore=1')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showMore } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HelloBundle showMore={showMore} title="Dynamic Bundle"/>
|
||||
<button
|
||||
id="toggle-show-more"
|
||||
onClick={() => this.toggleShowMore()}
|
||||
>
|
||||
Toggle Show More
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -25,6 +25,22 @@ export default (context, render) => {
|
|||
const $ = await get$('/dynamic/no-ssr-custom-loading')
|
||||
expect($('p').text()).toBe('LOADING')
|
||||
})
|
||||
|
||||
it('should render dynamic imports bundle', async () => {
|
||||
const $ = await get$('/dynamic/bundle')
|
||||
const bodyText = $('body').text()
|
||||
expect(/Dynamic Bundle/.test(bodyText)).toBe(true)
|
||||
expect(/Hello World 1/.test(bodyText)).toBe(true)
|
||||
expect(/Hello World 2/.test(bodyText)).toBe(false)
|
||||
})
|
||||
|
||||
it('should render dynamic imports bundle with additional components', async () => {
|
||||
const $ = await get$('/dynamic/bundle?showMore=1')
|
||||
const bodyText = $('body').text()
|
||||
expect(/Dynamic Bundle/.test(bodyText)).toBe(true)
|
||||
expect(/Hello World 1/.test(bodyText)).toBe(true)
|
||||
expect(/Hello World 2/.test(bodyText)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with browser', () => {
|
||||
|
@ -56,6 +72,61 @@ export default (context, render) => {
|
|||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
describe('with bundle', () => {
|
||||
it('should render components', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
!(/Hello World 2/.test(bodyText))
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should render support React context', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/ZEIT Rocks/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should load new components and render for prop changes', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/bundle')
|
||||
|
||||
await browser
|
||||
.waitForElementByCss('#toggle-show-more')
|
||||
.elementByCss('#toggle-show-more').click()
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Dynamic Bundle/.test(bodyText) &&
|
||||
/Hello World 1/.test(bodyText) &&
|
||||
/Hello World 2/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
15
test/integration/production/components/hello-context.js
Normal file
15
test/integration/production/components/hello-context.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class extends React.Component {
|
||||
static contextTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data } = this.context
|
||||
return (
|
||||
<div>{data.title}</div>
|
||||
)
|
||||
}
|
||||
}
|
3
test/integration/production/components/hello2.js
Normal file
3
test/integration/production/components/hello2.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<p>Hello World 2</p>
|
||||
)
|
68
test/integration/production/pages/dynamic/bundle.js
Normal file
68
test/integration/production/pages/dynamic/bundle.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Router from 'next/router'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const HelloBundle = dynamic({
|
||||
modules: (props) => {
|
||||
const components = {
|
||||
HelloContext: import('../../components/hello-context'),
|
||||
Hello1: import('../../components/hello1')
|
||||
}
|
||||
|
||||
if (props.showMore) {
|
||||
components.Hello2 = import('../../components/hello2')
|
||||
}
|
||||
|
||||
return components
|
||||
},
|
||||
render: (props, { HelloContext, Hello1, Hello2 }) => (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
<HelloContext />
|
||||
<Hello1 />
|
||||
{Hello2? <Hello2 /> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default class Bundle extends React.Component {
|
||||
static getInitialProps ({ query }) {
|
||||
return { showMore: Boolean(query.showMore) }
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
data: { title: 'ZEIT Rocks' }
|
||||
}
|
||||
}
|
||||
|
||||
toggleShowMore () {
|
||||
if (this.props.showMore) {
|
||||
Router.push('/dynamic/bundle')
|
||||
return
|
||||
}
|
||||
|
||||
Router.push('/dynamic/bundle?showMore=1')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showMore } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HelloBundle showMore={showMore} title="Dynamic Bundle"/>
|
||||
<button
|
||||
id="toggle-show-more"
|
||||
onClick={() => this.toggleShowMore()}
|
||||
>
|
||||
Toggle Show More
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue