在基於 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