react /async /javascript /代碼分片 /按需加載

在基於 create-react-app 的React項目中進行代碼分片、按需加載(code splitting)/ 免webpack配置 react /async /javascript /代碼分片 /按需加載 爲何須要代碼分片 Facebook 的 create-react-app 是一款很是優秀的開發腳手架。它爲咱們生成了 React 開發環境,自帶 webpack 默認配置。 它會經過 webpack 打包咱們的應用,產生一個 bundle.js 文件。隨着咱們的項目越寫越複雜,bundle.js 文件會隨之增大。javascript

因爲該文件是惟一的,因此無論用戶查看哪一個頁面、使用哪一個功能,都必須先下載全部的功能代碼。java

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

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

代碼分片的原理 ES模塊(ECMAScript modules)都是靜態的。編譯時就必須指明 肯定的導入(import)和導出(export)。 這也是規定 import 聲明必須出如今模塊頂部的緣由所在。web

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

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

以路由爲中心進行代碼分片 React 項目中的路由通常用 React Router,它能夠將多頁面的應用構建爲 SPA ,即單頁面應用。網絡

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

分片前異步

... ... 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.divmin-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 事件時就預先加載之)、服務端渲染 等等。

在此就很少作介紹了。

參考資料 ES proposal: import() – dynamically importing ES modules Code Splitting in Create React App Component-centric code splitting and loading in React @dan_abramov

相關文章
相關標籤/搜索