使用import配合React-Router进行code split


title: 使用react-router和import
Router进行代码分片
date: 2018-03-19 08:58:50
tags: 翻译javascript


原文连接html

  • 代码分片可让你把应用分红多个包,使你的用户能逐步加载应用而变得流行起来。在这篇文章中,咱们将会看一下什么是代码分片和怎么去作,了解怎么去配合React Router去实现它。前端

  • 如今是2018年。你的用户不须要为了一小块内容而去下载整个应用。若是一个用户下载全部的代码,仅仅是为了请求一个注册页面是毫无心义的。并且用户在注册时并不须要下载用户设置页的巨大富文本编辑器代码。若是要下载那么多内容的话,是很浪费的。并且对于一些用户,他们会抱怨不尊重没有特别好带宽的他们。这个点子近年不只很热,并且实现难度以指数级下降。甚至还有有了一个很酷的名字,代码分片。java

  • 这个点子很简单,即按需加载。实践的话,它可能有一点复杂。而复杂的缘由并非代码分片自己,而是如今有各类各样的工具来作这个事情。并且每一个人对哪一个方式最好都有本身的见解。当你第一次开始着手的时候,可能很困难分析什么是什么。react

  • 最多见的两种作法是使用Webpack和它的包加载器(bundle-loader),或者使用ECMAScript的stage3提案的动态import()。任何机会不用Webpack,我就不用,所以在这篇文章中,我将会使用动态import()。npm

  • 若是你很熟悉ES模块,你应该知道它们是静态的。意思就是说你必须在编译时肯定你要引入和导出的内容,而不是运行时。这也意味着你不能基于一些条件来动态导入一个模块。导入的内容必须声明在文件的最开头不然会抛出一个错误。api

    if (!user) {
        import * as api from './api' //不能这样作,“import”和“export”只能出如今文件顶部
    }
    复制代码
  • 如今,若是import不须要是静态的怎么办?意味着上面的代码能够工做?将会给咱们带来什么好处?首先这意味着我能够按着须要加载某个模块。这很是强大,它让咱们更接近按用户须要下载代码的想象。promise

    if (editPost === true) {
        import * as edit from './editpost'
        
        edit.showEditor()
    }
    复制代码
  • 假设__editpost__包含一个很是大的富文本编辑器,咱们须要保证用户在没有使用它的时候不会去下载它。浏览器

  • 另一个很酷的例子用于遗留支持。你能够在浏览器肯定确实没有的时候才下载对应代码。react-router

  • 好消息(我在上文中曾间接说起)。这种类型的方法确实存在,它被Create React App(React项目的一种官方建立方法)支持,并且它是ECMAScript stage3的提案。不一样的是替换了你以前使用import的方式。它使用起来像一个方法,并返回一个Promise,一旦模块彻底加载,就会把这个模块resolve回来。

    if (editPost === true) {
        import('./editpost')
          .then(module => module.showEditor())
          .catch(e =>)
    }
    复制代码
  • 特别好,对吧?

  • 如今咱们知道怎么动态引入模块了,下一步是找出怎么结合React和React Router来使用它。

  • 第一个(多是最大的一个)问题,咱们对React代码分片时,咱们应该对哪里进行分片?典型的回答有两个

    1. 在路由的层次上分片
    2. 在组件的层次上分片
  • 而更加常见的作法是在路由的层次上进行分片。你已经把你的应用分红了不一样的路由,所以根据这个来代码分片是天然而然的事情。

  • 让我以一个简单的React Router例子开始。咱们将有三条路由分别是: //topics/settings

    import React, { Component } from 'react'
    import {
        BrowserRouter as Router,
        Route,
        Link,
    } from 'react-router-dom'
    
    import Home from './Home'
    import Topics from './Topics'
    import Settings from './Settings'
    
    class App extends Component {
        render() {
            return (
              <Router>
                <div>
                  <ul>
                  	<li><Link to='/'>Home</Link></li>
    			   <li><Link to='/topics'>Topics</Link></li> 
                    <li><Link to='/settings'>Settings</Link></li>
                  </ul>    
                    
                  <hr />
                  
                  <Route exact path='/' component={Home} />
                  <Route exact path='/topics' component={Topics} />
     			 <Route exact path='/settings' component={Settings} />
                </div>
              </Router>  
            )
        }
    }
    
    export default App
    复制代码
  • 如今,假设咱们的__/settings__路由内容很是多。它包含一个富文本编辑器,和一个原始超级马里奥兄弟的拷贝,和盖伊法利的高清图片。当用户不在__/settings__路由上时,咱们不想让他们下载所有这些内容。让咱们使用咱们React和动态引入(import())的知识来分片__/settings__路由。

  • 就像咱们在React里解决任何问题同样,咱们先写一个组件。咱们将叫它__DynamicImport__。这个组件的目的是动态的加载一个模块,只要模块加载好了,就把它传给它子节点(children)。

    const Settings = (props) => (
      <DynamicImport load={() => import('./Settings')}> {(Component) => Component === null ? <Loading /> : <Component {...props} />} </DynamicImport> ) 复制代码
  • 上面的代码告诉咱们两个重要的要素。第一,这个组件在执行时会接受一个属性__load__,将使用咱们前面提到的语法动态引入一个模块。第二,这个组件会接受一个函数做为他的子节点,这个函数须要和引入进来的模块一块儿调用。

  • 在咱们深刻思考__DynamicImport__的实现的以前,让咱们想一下咱们会怎么实现。第一件事咱们须要肯定的是要调用props.load。这让咱们返回一个Promise,当它resolve的时候应该返回模块。接着,一旦咱们有了模块,咱们须要一种方式去触发重渲染,所以咱们要把模块传给props.children而且调用它。怎样在React里面触发重渲染呢?设置state(setState)。经过把动态引入的模块加入到__DynamicImport__的state里面,就像咱们以前使用的同样,咱们遵循和React一样的过程- 获取数据 -> 设置到state里 -> 重渲染。而这一次咱们只是把获取数据替换成了引入模块。

  • 好了,首先,让咱们加入初始的状态到组件里。

    class DynamicImport extends Component {
        state = {
            component: null
        }
    }
    复制代码
  • 如今,咱们须要调props.load方法。这将返回一个promise同时在resolve后有一个模块

    class DynamicImport extends Component {
        state = {
            component: null
        } 
        componentWillMount () {
            this.props.load()
                .then(component => {
                	this.setState(() =>{
                      	component
                     )}           
            	})
        }
    }
    复制代码
  • 这里有一个疑难杂症。若是咱们ES模块和commonjs模块混用时,ES模块会有一个.default属性,而commonjs模块并无。让咱们改变一下代码,适应一下上面的状况。

    this.props.load()
        .then(component => {
        	this.setState(() => {
            	component: component.default ?
    component.default : component
        	})
    	})
    })
    复制代码
  • 如今咱们动态引入的模块而且把它加入到了state里面,最后一件事就是render方法长什么样了。若是你会记得,当__DynamicImport__使用的时候,它看起来像这样

    const Settings = (props) => (
    	<DynamicImport load={() => import('./Settings')}> {(Component) => Component === null ? <Loading/>
            	: <Component {...props} />} </DynamicImport>
    )
    复制代码
  • 注意咱们给组件传了一个函数做为子节点。这意味着咱们须要执行这个函数,传递的是这个引入在state里的组件。

    class DynamicImport extends Component {
        state = {
            component: null
        }
    	componentWillMount () {
        	this.props.load()
                .then((component) => {
                    this.setState({
    				  component: component.default 
                        ? component.default
                        : component
                    })
            	})
    	}
        render() {
            return this.props.children(this.state.component)
        }
    }
    复制代码
  • 欧了,如今任什么时候候咱们动态引入一个模块,咱们能够把它包裹在__DynamicImport__。若是咱们以前尝试用这种方法到咱们路由上,咱们的代码会看起来像这样

    import React, { Component } from 'react'
    import {
        BrowserRouter as Router,
        Route,
        Link
    } from 'react-router-dom'
    
    class DynamicImport extends Component {
        state = {
            component: null
        }
    	componentWillMount () {
        	this.props.load()
                .then((component) =>&emsp;{
                	this.setState({
                        component: component.default 
                        ? component.default
                        : component
                    })
            	})
    	}
    	
    	render() {
        	return this.props.children(this.state.component)
    	}
    }
    
    const Home = (props) => (
    	<DynamicImport load={() => import('./Home')}>
        	{(Component) => Component === null 
              	? <p>Loading</p>
                : <Component {...props} />
            }
        </DynamicImport>
    )
    
    const Topics = (props) => (
    	<DynamicImport load={() => import('./Settings')}>
        	{(Component) => Component === null 
            	? <p>Loading</p>
                : <Component {...props}/>
            }
        </DynamicImport>
    )
    
    class App extends Component {
        render() {
            return (
            	<Router>
                	<div>
                    	<ul>
                        	<li><Link to='/'>Home</Link></li>
                            <li><Link to='/topics'>Topics</Link></li>
                            <li><Link to='/settings'>Settings</Link></li>
                        </ul>
                        <hr />
                        <Route exact path='/' component={Home} />
                        <Route path='/topics' component={Topics} />
                        <Route path='/settings' component={Settings} />
                    </div>
                </Router>
            )
        }
    }
    
    export default App
    复制代码

    咱们怎么知道这个确实起做用而且分片了咱们的路由呢?若是你用一个React官方的Create React App建立一个应用跑一下__npm run build__,你将看到应用被分片了。

  • 每个包被一一引入进了咱们的应用

  • 你到了这一步,能够跳个舞轻松一下了

  • 还记得我讲到有两种层级的代码分片方式吗?咱们曾放在手边的引导

    1. 以路由层级分片
    2. 以组建层级分片
  • 至此,咱们只讲了路由层级的代码分片。到这里不少人就中止了。在路由层级上代码分片,就像刷牙同样,你每天刷,牙齿大部分很干净,可是还会有蛀牙。

  • 除了思考用路由的分片方式,你应该想一想怎么用组件的方式去分片。若是你在弹层里面有不少内容,路由分片仍是会下载弹层的代码,不管这个弹层是否显示。

  • 从这一点看,它更可能是在你大脑里的一种变动而不是新知识。你已经知道如何使用动态引入,如今你须要找出哪些组件是在用到时才要下载的。

  • 若是我不提React Loadable那我就是哑吧了。它是一个“经过动态引入加载组件的高阶组件”。重要的是,它处理全部咱们提到的事情,并把它作成了一个精致的API。它甚至处理了不少很边角的事情,好比咱们没有考虑服务端渲染和错误处理。看看它吧,若是你想要一个简单,开箱即用的解决方案的话。


欢迎加入DCG前端团队。 简历请投 hanshuangli@dcrays.cn 【一年16薪】【通信津贴】【交通补助】【过节福利】【带薪年假】【绩效奖金】【按期体检】【生日福利】...

相关文章
相关标签/搜索