Code Splitting | 代码拆分

Code Spliting 是一个非常酷的特性,它能让我们用户无需完整下载整个 app 即可使用。你可以将他认为是增量加载我们的应用。尽管有一些其他工具也能实现相关特性,但在这个教程里我们使用 Webpackbundle loader

你可以通过 <Bundle> 来实现网站的 code splitting,值得注意的是,router 并没有为实现 code splitting 而做额外的工作。当你“进入一个路由”仅仅意味着“你渲染了一个组件”,所以我们能在用户导航到某个路由时动态引入这个组件。你可以通过这种方式在应用的任何地方使用 code splitting。

  1. import loadSomething from 'bundle-loader?lazy!./Something'
  2. <Bundle load={loadSomething}>
  3. {(mod) => (
  4. // do something w/ the module
  5. )}
  6. </Bundle>

如果模块是一个 component 的话,我们可以像这样渲染这个组件:

  1. <Bundle load={loadSomething}>
  2. {(Comp) => Comp
  3. ? <Comp/>
  4. : <Loading/>
  5. )}
  6. </Bundle>

This component takes a prop called load we get from the webpack bundle loader. We’ll talk about why we use that in a minute. When the component mounts or gets a new load prop, it will call load, then place the returned value in state. Finally, it calls back in render with the module.

  1. import React, { Component } from 'react'
  2. class Bundle extends Component {
  3. state = {
  4. // short for "module" but that's a keyword in js, so "mod"
  5. mod: null
  6. }
  7. componentWillMount() {
  8. this.load(this.props)
  9. }
  10. componentWillReceiveProps(nextProps) {
  11. if (nextProps.load !== this.props.load) {
  12. this.load(nextProps)
  13. }
  14. }
  15. load(props) {
  16. this.setState({
  17. mod: null
  18. })
  19. props.load((mod) => {
  20. this.setState({
  21. // handle both es imports and cjs
  22. mod: mod.default ? mod.default : mod
  23. })
  24. })
  25. }
  26. render() {
  27. return this.props.children(this.state.mod)
  28. }
  29. }
  30. export default Bundle

You’ll notice that render calls back with a null state.mod on any renders before the module has been fetched. This is important so you can indicate to the user we’re waiting for something.

Why bundle loader, and not import()?

We’ve been using it for years and it continues to work while TC39 continues to come up with an official dynamic import. The latest proposal is import(), and we could adjust our Bundle component to use import() instead:

  1. <Bundle load={() => import('./something')}>
  2. {(mod) => ()}
  3. </Bundle>

Another HUGE benefit of bundle loader is that the second time it calls back synchronously, which prevents flashing the loading screen every time you visit a code-split screen.

Regardless of the way you import, the idea is the same: a component that handles the code loading when it renders. Now all you do is render a <Bundle> wherever you want to load code dynamically.

Loading after rendering is complete

The Bundle component is great for loading as you approach a new screen, but it’s also beneficial to preload the rest of the app in the background.

  1. import loadAbout from 'bundle-loader?lazy!./loadAbout'
  2. import loadDashboard from 'bundle-loader?lazy!./loadDashboard'
  3. // components load their module for initial visit
  4. const About = () => (
  5. <Bundle load={loadAbout}>
  6. {(About) => <About/>}
  7. </Bundle>
  8. )
  9. const Dashboard = () => (
  10. <Bundle load={loadDashboard}>
  11. {(Dashboard) => <Dashboard/>}
  12. </Bundle>
  13. )
  14. class App extends React.Component {
  15. componentDidMount() {
  16. // preloads the rest
  17. loadAbout(() => {})
  18. loadDashbaord(() => {})
  19. }
  20. render() {
  21. return (
  22. <div>
  23. <h1>Welcome!</h1>
  24. <Route path="/about" component={About}/>
  25. <Route path="/dashboard" component={Dashboard}/>
  26. </div>
  27. )
  28. }
  29. }

When, and how much, of your app to load is your own decision. It need not be tied to specific routes. Maybe you only wnat to do it when the user is inactive, maybe only when they visit a route, maybe you want to preload the rest of the app after the initial render:

  1. ReactDOM.render(<App/>, preloadTheRestOfTheApp)

Code-splitting + server rendering

We’ve tried and failed a couple of times. What we learned:

  1. You need synchronous module resolution on the server so you can get those bundles in the initial render.
  2. You need to load all the bundles in the client that were involved in the server render before rendering so that the client render is the same as the server render. (The trickiest part, I think its possible but this is where I gave up.)
  3. You need asynchronous resolution for the rest of the client app’s life.

We determined that google was indexing our sites well enough for our needs without server rendering, so we dropped it in favor of code-splitting + service worker caching. Godspeed those who attempt the server-rendered, code-split apps.