[翻譯]基於Webpack4使用懶加載分離打包React代碼

原文地址: https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec60140ec5a
做者: Aviv Shafir
摘要:Innovid網站使用Webpack4對一個React項目進行了優化改造。主要使用了新的optimization配置和動態注入功能。

Hey,這裏是Innovid,一個領先的視頻廣告平臺。咱們天天處理130萬小時的視頻,而在咱們的web項目中,常常會使用到Webpack。咱們很是喜歡這個工具。html

最近,咱們將一個項目遷移到了最新的Webpack4。它給咱們帶來了一些開箱即用的新特性,好比在構建時間上進行了很是大的優化。node

在本次遷移中,咱們決定使用懶加載這一Webpack最吸引人的特性來分割app中的主要代碼部分。react

代碼分割可以幫助你延遲加載用戶當前須要的內容,同時也能顯著地提高用戶體驗。儘管你沒有減小app的總代碼量,但你已經避免加載一些用戶也許永遠也用不到的代碼了。並且還可以在初始加載時減小加載的代碼數量。
—— React 文檔

Webpack根據你的應用程序構建了一個依賴關係圖。從你的入口文件開始,它遞歸遍歷全部文件和它們的依賴文件,使用loader和plugin對你的文件施了點魔法,最後就輸出了提供給用戶的生成包。webpack

咱們如今將生成包分爲app.js(咱們的應用代碼)和vendors.js(第三方庫)。
咱們使用webpack-bundle-analyzer插件來可視化兩個生成包:
初始包git

app.js大小116KB,vendors.js大小399KB

Webpack配置

app.js是咱們程序的入口,因此自動打包成app.js。而第三方包vendors.js是使用了新的optimization配置,將從node_modules文件夾中引入的全部文件打包生成的。es6

mode: "production",
  entry: {
    app: path.join(__dirname, "index.tsx"),
  },
  output: {
    path: path.resolve(__dirname, "public/dist"),
    publicPath: "",
    chunkFilename: "[name].js",
    filename: "[name].js"
  },
  optimization: {
        runtimeChunk: {
            name: "manifest"
        },
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    priority: -20,
                    chunks: "all"
                }
            }
        }
   }

注意: 在Webpack4中,咱們再也不使用CommonChunkPlugin了,它被splitChunksruntimeChunk這兩個新API所取代。github

懶加載React組件

如今的vendors和app包都是用戶在第一次打開頁面室加載的。咱們發現能夠將一些「重量級」的組件懶加載來提高首屏體驗,而且減小初始包的體積。web

好比說:redux-form是一個管理react應用表單的庫,它只在一個名爲GenerateTags的大型組件中使用。因爲它體積較大而且只在特定場景下被使用,因此用它來做爲懶加載的實驗對象是再好不過了。redux-form和GenerateTags組件能夠被抽取到單獨一個chunk中,這樣咱們在渲染首屏時請求的包體積更小。typescript

讓咱們看看如今流行的動態導入工具庫:react-loadable。它基礎封裝了將來JS的新語法import()json

const GenerateTags = Loadable({
  loader: () =>
    import(/* webpackChunkName: "generateTags" */ "./GenerateTags"),
    loading: LoadingSpinner
});

使用以後,咱們的包變成了下面這樣:
抽取組件

GenerateTags已經被抽取到單獨的一個chunk中,但redux-form仍然在vendor.js包裏。

結果不盡如人意,由於redux-form仍然在vendors.js包中,但咱們但願它跟GenerateTags都被抽取到一個不一樣的chunk中來實現按需加載。

之因此會出現這樣的狀況,是由於咱們在別的文件中也引用了redux-form。好比說咱們在combineReducers 中編寫了下面的代碼:

import { reducer as formReducer } from "redux-form";
const applicationReducer: Reducer<any> = combineReducers({
    user,
    sidenav,
    navigation,
    //...
    form: formReducer
});

這段代碼頂部的靜態導入語句致使redux-form庫成了咱們vendors包的一部分。也就是說,Webpack認爲它已經被靜態導入成咱們的app入口依賴樹的一部分,因此不能被懶加載。

爲了解決這個問題,咱們決定動態注入redux-form reducer。首先,咱們移除了導入redux-form reducer的語句,而且加了下面的代碼來實現動態注入redux reducer:

export function injectAsyncReducer(store, name, asyncReducer) {
  if (store.asyncReducers[name]) {
    return;
  }
  store.asyncReducers[name] = asyncReducer;
  store.replaceReducer(createReducer(store.asyncReducers));
}

export const configureStore = (initialState: AppState) => {
  const enhancer = compose(applyMiddleware(...getMiddleware()));
  const store: any = createStore(createReducer(), initialState, enhancer);
  store.asyncReducers = {};
  return store;
};


const createReducer = (asyncReducers = {}) => {
    return combineReducers({
        user,
        sidenav,
        navigation,
        //...
        ...asyncReducers
    });
};

最後,咱們在GenerateTags組件的componentDidMount中調用injectAsyncReducer方法。

public componentDidMount() {
    const reduxFormReducer = require("redux-form").reducer;
    injectAsyncReducer(store, "form", reduxFormReducer);
  }

注意,不推薦從組件直接獲取一個store的引用,由於這樣會致使你在作服務端渲染時出現一些問題。
這裏你能夠閱讀更多注入異步代碼和使用HOC的知識。

TypeScript配置

咱們在項目中使用了typescript。咱們必須在tsconfig.json中更新esnext的module配置,以及設置removeCommentsfalse(要支持動態注入,TS的版本必須高於2.4)。這樣,以前的動態注入纔會起做用。經過「告訴」typescript編譯器避開咱們的import語句,而且不要對它們進行轉碼來讓Webpack正常工做。

{
  "compilerOptions": {
    "target": "es5",
    "sourceMap": false,
    "inlineSourceMap": true,
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "react",
    "preserveConstEnums": true,
    "removeComments": false,
    "lib": ["es6", "dom"]
  },
  "types": ["node"]
}

最後的結果就像下面這樣:

vendors.js 314 KiB, app.js 96.6 KiB, generateTags.js 23.2 KiB, vendors~generateTags.js 90.2 KiB

最後咱們成功了,GenerateTags和它的依賴文件redux-form被提取出vendor.js而且可以被按需加載。

總結

咱們推薦你閱讀這個文章來優化Webpack。

  • 使用動態注入能夠減小最終包的體積。還能疼痛感異步加載提供更快的首屏加載速度。
  • typescript從2.4版本開始支持動態注入,你只須要記住修改一部分配置就能使用這個功能。
  • 遷移到Webpack4並不不復雜,可是目前尚未關於新配置和API的介紹文檔。但我相信很快它們都會有的。
  • 動態注入redux reducer是一個頗有用的小技巧,它可以幫助咱們的app在使用redux reducer時延遲加載一些庫。
查看更多我翻譯的Medium文章請訪問:
項目地址: https://github.com/WhiteYin/translation
SF專欄: https://segmentfault.com/blog/yin-translation
相關文章
相關標籤/搜索