banner
他山之石

他山之石

使用webpack4和react-router 4實現代碼分割

Webpack 的 Code Splitting 特性能夠把代碼分離到不同的 bundle 中,然後可以按需加載或並行加載這些文件。

image

什麼是代碼分割(code splitting)#

我們知道,在使用 webpack 打包 react 應用時,webpack 將整個應用打包成一個 js 文件,當用戶訪問首屏時,會一次性加載整個 js 文件,這就造成了首屏渲染速度變慢的問題。於是,webpack 開發了代碼分割的特性,
此特性能夠把代碼分離到不同的 bundle 中,然後可以按需加載或並行加載這些文件。代碼分離可以用於獲取更小的 bundle,以及控制資源加載優先級,如果使用合理,會極大影響加載時間。
有三種常用的代碼分離方法:

  • 入口起點:使用 entry 配置手動地分離代碼。
  • 防止重複:使用 CommonsChunkPlugin 去重和分離 chunk。
  • 動態導入:通過模塊的內聯函數調用來分離代碼。
    本文只討論動態導入(dynamic imports)的方法。

動態導入#

當涉及到動態代碼拆分時,webpack 提供了兩個類似的技術。對於動態導入,第一種,也是優先選擇的方式是,使用符合 ECMAScript 提案 的 import () 語法。第二種,則是使用 webpack 特定的 require.ensure。讓我們先嘗試使用第一種……

注意:import () 調用會在內部用到 promises。如果在舊有版本瀏覽器中使用 import (),記得使用 一個 polyfill 庫(例如 es6-promise 或 promise-polyfill),來 shim Promise。

下面結合 react-router 4 來實現 react 的代碼分割。

react code splitting and lazy load#

在使用 react-router4 進行代碼分割的路上,社區已經有成熟的第三方庫進行了實現,如 react-loadable。在此處將介紹如何不借助第三方庫實現代碼分割。

此處我們假設你已經對 react、react-router4、webpack 有基本的了解,可以搭建簡單的開發環境。下面先看一下我搭建的項目入口文件。

src 下 index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';


ReactDOM.render(<App />, document.getElementById('app'));

src 下 App.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Routes from './routes';

export default () => (
  <BrowserRouter>
    <Switch>
      {
        Routes.map(({name, path, exact = true, component }) => (
          <Route path={path} exact={exact} component={component} key={name} />
        ))
      }
    </Switch>
  </BrowserRouter>
)

在 App.js 中,使用了 react-router-dom 來做路由,routes.js 文件作為路由配置文件,下面使用路由配置遍歷生成路由組件。
下面來看一下路由配置 routes.js。

import AsyncComponent from './components/acync-component';

export default [
  {
    name: '首頁',
    icon: 'home',
    path: '/',
    component: AsyncComponent(() => import('./containers/home'))
  },
  {
    name: '詳情',
    icon: 'detail',
    path: '/detail',
    component: AsyncComponent(() => import('./containers/detail'))
  }
]

routes.js 中配置了路由組件需要的參數,需要注意的是在路由參數中使用了異步組件 AsyncComponent,注意這裡並沒有進行組件的引入,而是傳給了 AsyncComponent 一個函數,它將在 AsyncComponent(() => import('./containers/home')) 組件被創建時進行動態引入。同時,這種傳入一個函數作為參數,而非直接傳入一個字符串的寫法能夠讓 webpack 意識到此處需要進行代碼分割。

使用 import()需要使用像 Babel 預處理器和 Syntax Dynamic Import Babel Plugin。由於 import () 會返回一個 promise,因此它可以和 async 函數一起使用,使用 acync 函數需要使用 babel-plugin-transform-runtime。

安裝 babel 插件:

npm i -D babel-plugin-syntax-dynamic-import babel-plugin-transform-runtime

本項目使用的其他 babel 插件還有
babel-core、babel-loader、babel-preset-env、babel-preset-react 等,主要用於 jsx 編譯
配置.babelrc
根目錄下新建.babelrc,配置如下:

{
  "presets":["react","env"],
  "plugins": ["syntax-dynamic-import", "transform-runtime"]
}

異步組件 AsyncComponent#

本項目的 AsyncComponent 放在 src/components/async-component/index.js 中,代碼如下:

import React, { Component } from 'react';

export default (loadComponent, placeholder = '拼命加載中。。。。') => {
  return class AsyncComponent extends Component {
    
    constructor() {
      super();

      this.state = {
        Child: null
      }

      this.unmount = false;
    }

    componentWillUnmount() {
      this.unmount = true;
    }

    async componentDidMount() {
      const { default: Child } = await loadComponent();

      if (this.unmount) return;

      this.setState({
        Child
      })
    }

    render() {
      const { Child } = this.state;

      return (
        Child ? <Child {...this.props} /> : placeholder
      )
    }
  }
}

首先,這是一個高階組件,返回一個新的組件,傳入兩個參數,一個是需要動態加載組件的方法,第二個是動態加載時的佔位符,也可以傳入一個 Loading 組件。

在返回的 AsyncComponent 內部,constructor 中,初始化一個 state 為 Child,值為 null,並定義 this.unmount =false,用於表示組件是否被卸載。

使用 acync 定義異步方法,componentDidMount 中,使用 await 異步執行傳入的第一個參數,用於動態加載當前路由的組件。

注意:
注意當調用 ES6 模塊的 import () 方法(引入模塊)時,必須指向模塊的 .default 值,因為它才是 promise 被處理後返回的實際的 module 對象。

故此處使用 ES6 的對象解構獲取到模塊的 default 並賦值到 Child 上。
然後判斷組件被卸載的狀態,被卸載即返回。
下面將 Child 設置到 state 上。
在 render 方法中,從 state 中獲取 Child,然後使用三元運算符判斷 Child 是否為 true,為 true 則渲染 Child 組件,並注入 this.props,否則渲染佔位符。
組件 componentWillUnmount 時,設置 this.unmout 為 true。

測試驗證#

在 containers 中新建兩個文件夾 home 和 detail,在兩個文件夾下編寫 index.js 作為兩個路由組件。代碼如下:
home/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import '../../style.css';
import icon from '../../assets/404.png';

export default class Home extends React.Component {
  render() {
    return (
      <div>
        <h1>hello webpack</h1>
        <img src={icon} alt=""/>
        <Link to="/detail">詳情</Link>
      </div>
    )
  }
}

detail/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import '../../style.css';
import icon from '../../assets/404.png';

export default class Detail extends React.Component {
  render() {
    return (
      <div>
        <h1>hello webpack detail</h1>
        <img src={icon} alt=""/>
      </div>
    )
  }
}

在根目錄下 package.json 配置啟動腳本:

"scripts": {
    "start": "webpack-dev-server --mode development",
    "build": "webpack --mode production"
  },

然後運行 npm start 啟動項目:
打開瀏覽器訪問 localhost:8080

image

可以看到頁面先加載了 main.js 和 0.js,點擊詳情按鈕跳轉到http://localhost:8080/detail。

image

馬上加載了 1.js,這樣就實現了代碼分割,每個路由都是動態加載的。提升了首屏渲染速度。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。