React項目:代碼分片、按需加載(code splitting)/ 免webpack配置

爲什麼需要代碼分片

Facebook 的 create-react-app 是一款非常優秀的開發腳手架。它爲我們生成了 React 開發環境,自帶 webpack 默認配置。 它會通過 webpack 打包我們的應用,產生一個 bundle.js 文件。隨着我們的項目越寫越複雜,bundle.js 文件會隨之增大。 

由於該文件是唯一的,所以不管用戶查看哪個頁面、使用哪個功能,都必須先下載所有的功能代碼。 

當 bundle.js 大到一定程度,就會明顯影響用戶體驗。

此時,我們就需要 code splitting ,將代碼分片,實現按需異步加載,從而優化應用的性能。

代碼分片的原理

ES模塊(ECMAScript modules)都是靜態的。編譯時就必須指明 確定的導入(import)和導出(export)。 這也是規定 import 聲明必須出現在模塊頂部的原因所在。

但是我們可以通過 dynamic import() 來實現動態加載的功能。 dynamic import() 是 stage 3 中的一個提案。這是一個 運算符 operator 而非函數 function 。 我們把模塊的名字作爲參數傳入,它會返回一個 Promise ,當模塊加載完成後,該 Promise 就會 fulfilled。

當你在代碼中新增了一個 import() ,用它動態導入模塊時, Webpack 2 會自動據此完成代碼分片,不需要任何額外的手動配置

以路由爲中心進行代碼分片

React 項目中的路由一般用 React Router,它可以將多頁面的應用構建爲 SPA ,即單頁面應用。 

此處,我們以其最新版 React Router v4 爲例。

分片前

... ...
import {requireAuthentication} from './CheckToken'
import Home from '../components/Home/Home'
import Login from './LoginContainer'
import Signup from './SignupContainer'
import Profile from './ProfileContainer'
... ...
<Router>
       <Switch>
         <Route exact path='/' component={Home} />
         <Route path='/login' component={Login} />
         <Route path='/signup' component={Signup} />
         <Route path='/profile' component={requireAuthentication(Profile)} />
... ...

分片後

新增 AsyncComponent,它將接受一個函數作爲參數,實現異步地動態加載組件。例如:

const AsyncLogin = asyncComponent(() => import('./LoginContainer'))

至於爲什麼是以 () => import('./LoginContainer') 這樣的箭頭函數爲參數,而非 './LoginContainer' 這樣的字符串,和 Webpack 的進行代碼分片的機制有關。 

這麼寫看起來囉嗦,但可以讓我們控制生成多少個 .chunk.js 這樣的分片文件。

代碼:

import React, { Component } from 'react'

export default function asyncComponent(importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props)

      this.state = {
        component: null
      }
    }

    async componentDidMount() {
      const { default: component } = await importComponent()

      this.setState({
        component: component
      })
    }

    render() {
      const C = this.state.component

      return C ? <C {...this.props} /> : null
    }
  }

  return AsyncComponent
}

路由

... ...
import {requireAuthentication} from './CheckToken'
import asyncComponent from './AsyncComponent'

const AsyncHome = asyncComponent(() => import('../components/Home/Home'))
const AsyncLogin = asyncComponent(() => import('./LoginContainer'))
const AsyncSignup = asyncComponent(() => import('./SignupContainer'))
const AsyncProfile = asyncComponent(() => import('./ProfileContainer'))
... ...
<Router>
       <Switch>
         <Route exact path='/' component={AsyncHome} />
         <Route path='/login' component={AsyncLogin} />
         <Route path='/signup' component={AsyncSignup} />
         <Route path='/profile' component={requireAuthentication(AsyncProfile)} />
... ...

此時再運行 npm run build,看編譯的log,以及 build/static/js/ 目錄下的 js 文件,會發現多出了若干文件名 .chunk.js 結尾的文件。

  npm start 把項目跑起來,在 chrome 的 devTool 中,打開 Network ,查看 JS ,就可以看到異步動態按需加載分片文件的效果了。

以組件爲中心進行代碼分片

上面一小節是以路由爲中心進行代碼分片的思路與實現。但是 React Router 官網說得明白,React Router 是導航組件的集合。 

即,路由本身並沒有什麼特別的,它們也是組件。

如果以組件爲中心進行代碼分片,會帶來額外的好處:

  • 除了路由此外,還有很多地方可以進行代碼分片。廣闊天地,大有作爲。
  • 同一個組件中,針對不急着顯示的東西,可以延遲其加載。
  • ... ...

這裏介紹 React Loadable 。

通過它,我們可以用使用 React 高階組件 (Higher Order Component / HOC)實現異步加載 React 組件的功能,同時處理操作失敗、網絡錯誤等等邊緣情況。

注:一個高階組件,簡言之就是一個函數,它接受的參數是 React 組件,返回的結果也是 React 組件。

React Loadable 可以通過 npm 安裝 react-loadable

首先,我們用 React Loadable 來重構剛纔的代碼

處理邊緣情況的組件

import React from 'react'

const MyLoadingComponent = ({isLoading, error}) => {
  // 加載中
  if (isLoading) {
    return <div>Loading...</div>
  }
  // 加載出錯
  else if (error) {
    return <div>Sorry, there was a problem loading the page.</div>
  }
  else {
    return null
  }
}

export default LoadingComponent

路由

... ...
import {requireAuthentication} from './CheckToken'
import Loadable from 'react-loadable'
import LoadingComponent from '../components/common/Loading'

const AsyncHome = Loadable({
  loader: () => import('../components/Home/Home'),
  loading: LoadingComponent
})
const AsyncSignup = Loadable({
  loader: () => import('./SignupContainer'),
  loading: LoadingComponent
})
const AsyncLogin = Loadable({
  loader: () => import('./LoginContainer'),
  loading: LoadingComponent
})
const AsyncProfile = Loadable({
  loader: () => import('./ProfileContainer'),
  loading: LoadingComponent
})

... ...
      <Router>
        <Switch>
          <Route exact path='/' component={AsyncHome} />
          <Route path='/login' component={AsyncLogin} />
          <Route path='/signup' component={AsyncSignup} />
          <Route path='/profile' component={requireAuthentication(AsyncProfile)} />
... ...

進一步優化

重新運行項目,發現了可以進一步改進的地方。

防止 Loading 組件閃現

在頁面跳轉的時候,屏幕上會短暫的閃過 LoadingComponent 組件。

我們添加該組件的初衷,是在網絡差的時候,給用戶一個提示:“應用運行正常,只是正在加載中,請稍等。”

顯然,如果網絡良好,跳轉足夠快,LoadingComponent 組件根本沒有必要出現。

React Loadable 可以很容易地實現這個功能。

LoadingComponent 組件接收一個 pastDelay 屬性,該屬性僅僅在延遲超過一個規定的值後才爲 true 。

默認的延遲是 200ms,我們也可以自己指定別的時長。操作如下,我們將其設置爲 300ms。

... ...
const AsyncLogin = Loadable({
  loader: () => import('./LoginContainer'),
  loading: LoadingComponent,
  delay: 300
})
... ...

LoadingComponent 組件做相應調整。同時增加一些簡單的樣式。

import React from 'react'
import Footer from '../Footer/Footer'
import styled from 'styled-components'

const Wrap = styled.div`
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background-color: #B2EBF2;
  text-align: center;
`

const LoadingComponent = (props) => {
  if (props.error) {
    return (
      <Wrap>
        <div>Error!</div>
        <Footer />
      </Wrap>
    )
  } else if (props.pastDelay) {
    // 300ms 之後顯示
    return (
      <Wrap>
        <div>信息請求中...</div>
        <Footer />
      </Wrap>
    )
  } else {
    return null
  }
}

export default LoadingComponent

同一個組件中,延遲加載不急着顯示的內容

例如這個組件,TopHeader 是優先顯然的內容,Notification 是不一定顯示的內容。我們可以推遲後者的加載。

... ...
import TopHeader from '../components/Header/TopHeader'
import Notification from './NotificationContainer'

class TopHeaderContainer extends Component {
  ... ...

    return (
      <div>
        <TopHeader
          sideButtons={tempIsAuthenticated}
          logout={this.logout}
        />
        <Notification />
      </div>

    )
}
... ...
export default connect(mapStateToProps, { logout })(TopHeaderContainer)

優化後

... ...
import TopHeader from '../components/Header/TopHeader'

import Loadable from 'react-loadable'
import LoadingComponent from '../components/common/Loading'

const AsyncNotification = Loadable({
  loader: () => import('./NotificationContainer'),
  loading: LoadingComponent,
  delay: 300
})
... ...
class TopHeaderContainer extends Component {
  ... ...
  
    return (
      <div>
        <TopHeader
          sideButtons={tempIsAuthenticated}
          logout={this.logout}
        />
        <AsyncNotification />
      </div>
    )
  }
}
... ...
export default connect(mapStateToProps, { logout })(TopHeaderContainer)
... ...

此外, 還可以實現 預加載(如 click 按鈕顯示某組件,那麼在 hover 事件時就預先加載之)、服務端渲染 等等。

在此就不多做介紹了。

參考資料

發佈了49 篇原創文章 · 獲贊 40 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章