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

weixin_34402408發表於2018-03-19

title: 使用react-router和import
Router進行程式碼分片
date: 2018-03-19 08:58:50
tags: 翻譯


本文由林子翔@理工數傳翻譯自原文連結

  • 程式碼分片可以讓你把應用分成多個包,使你的使用者能逐步載入應用而變得流行起來。在這篇文章中,我們將會看一下什麼是程式碼分片和怎麼去做,瞭解怎麼去配合React Router去實現它。

  • 現在是2018年。你的使用者不需要為了一小塊內容而去下載整個應用。如果一個使用者下載所有的程式碼,僅僅是為了請求一個註冊頁面是毫無意義的。而且使用者在註冊時並不需要下載使用者設定頁的巨大富文字編輯器程式碼。如果要下載那麼多內容的話,是很浪費的。而且對於一些使用者,他們會抱怨不尊重沒有特別好頻寬的他們。這個點子近年不僅很熱,而且實現難度以指數級降低。甚至還有有了一個很酷的名字,程式碼分片。

  • 這個點子很簡單,即按需載入。實踐的話,它可能有一點複雜。而複雜的原因並不是程式碼分片本身,而是現在有各種各樣的工具來做這個事情。而且每個人對哪個方式最好都有自己的看法。當你第一次開始著手的時候,可能很困難分析什麼是什麼。

  • 最常見的兩種做法是使用Webpack和它的包載入器(bundle-loader),或者使用ECMAScript的stage3提案的動態import()。任何機會不用Webpack,我就不用,因此在這篇文章中,我將會使用動態import()。

  • 如果你很熟悉ES模組,你應該知道它們是靜態的。意思就是說你必須在編譯時確定你要引入和匯出的內容,而不是執行時。這也意味著你不能基於一些條件來動態匯入一個模組。匯入的內容必須宣告在檔案的最開頭否則會丟擲一個錯誤。

    if (!user) {
        import * as api from './api' //不能這樣做,“import”和“export”只能出現在檔案頂部
    }
    複製程式碼
  • 現在,如果import不需要是靜態的怎麼辦?意味著上面的程式碼可以工作?將會給我們帶來什麼好處?首先這意味著我可以按著需要載入某個模組。這非常強大,它讓我們更接近按使用者需要下載程式碼的想象。

    if (editPost === true) {
        import * as edit from './editpost'
        
        edit.showEditor()
    }
    複製程式碼
  • 假設__editpost__包含一個非常大的富文字編輯器,我們需要保證使用者在沒有使用它的時候不會去下載它。

  • 另外一個很酷的例子用於遺留支援。你可以在瀏覽器確定確實沒有的時候才下載對應程式碼。

  • 好訊息(我在上文中曾間接提及)。這種型別的方法確實存在,它被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。它甚至處理了很多很邊角的事情,比如我們沒有考慮服務端渲染和錯誤處理。看看它吧,如果你想要一個簡單,開箱即用的解決方案的話。

    (完,逃)

相關文章