1
0
Fork 0
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:
Arunoda Susiripala 2017-06-16 18:49:34 +05:30 committed by GitHub
parent f36b4f9b4a
commit 9df59c1176
12 changed files with 439 additions and 41 deletions

View file

@ -0,0 +1,3 @@
export default () => (
<p>Hello World 6 (imported dynamiclly) </p>
)

View file

@ -0,0 +1,3 @@
export default () => (
<p>Hello World 7 (imported dynamiclly) </p>
)

View file

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

View file

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

View file

@ -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"/>
)
```

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

View file

@ -0,0 +1,3 @@
export default () => (
<p>Hello World 2</p>
)

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

View file

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

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

View file

@ -0,0 +1,3 @@
export default () => (
<p>Hello World 2</p>
)

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