使用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薪】【通信津貼】【交通補助】【過節福利】【帶薪年假】【績效獎金】【按期體檢】【生日福利】...

相關文章
相關標籤/搜索