使用webpack4一步步搭建react專案(二)

gotcha發表於2019-04-06

前面已經實現了一個簡單webpack配置,接下來需要在前面的基礎上對webpack.config.js進行拆分。

第二章 拆分webpack配置

專案開發時,我們需要用webpack-dev-server啟動開發伺服器,當我們修改檔案時,它能自動重新打包專案並重新整理頁面。

專案打包上線時,我們希望webpack能進行更多的處理來優化打包後的程式碼。

針對不同的需求,我們需要將配置檔案拆分為webpack.common.js webpack.dev.js webpack.prod.js

專案結構

專案結構v2

程式碼

專案程式碼 Github 倉庫

配置webpack.common.js

這個檔案是共用的配置。

const path = require("path");

module.exports = {
  // 入口檔案改為 .jsx檔案
  entry: "./src/index.jsx",
  resolve: {
    extensions: [".js", ".json", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        // include告訴webpack只對src下的
        // js、jsx檔案進行babel轉譯
        // 加快webpack的打包速度
        include: path.resolve(__dirname, "src"),
        use: "babel-loader"
      }
    ]
  }
};
複製程式碼

配置webpack.dev.js

我們需要使用webpack-merge將前面配好的webpack.comm.js合併進來。而且需要webpack-dev-server來啟動開發伺服器。

安裝:

  • webpack-merge
  • webpack-dev-server
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");

// 使用webpack-merge將webpack.common.js合併進來
module.exports = merge(common, {
  // 設定為開發(development)模式
  mode: "development",
  // 設定source map,方便debugger
  devtool: "inline-source-map",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  devServer: {
    // 單頁應用的前端路由使用history模式時,這個配置很重要
    // webpack-dev-server伺服器接受的請求路徑沒有匹配的資源時
    // 他會返回index.html而不是404頁面
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: "file-loader"
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png"
    })
  ]
});

複製程式碼

配置webpack.prod.js

打包生產環境使用的程式碼需要非常多的優化和處理,所以這個檔案的配置會非常複雜。

配置思路

  • 將css樣式從main.js內抽離到單獨的.css檔案
  • 快取處理
  • 將第三方庫和webpack的runtime從main.js內抽離出來
  • js、css、html程式碼壓縮
  • 使用source-map替代inline-source-map
  • 懶載入(lazy loading)
  • 每次打包前先清空dist目錄

抽離css樣式

前面的webpack.dev.js只是簡單的使用了css-loaderstyle-loader

css-loader將專案匯入的css樣式轉為js模組,打包到main.js內。

style-loadermain.js內提供了一個能將css動態插入到html內的方法。

當使用者開啟頁面時,會先載入html,然後載入main.js,最後執行js指令碼將樣式插入到style標籤內。

這樣有多個缺點:

  • css被打包到了main.js內,增加了它的檔案大小,而且也不方便對css做快取
  • css樣式得等到main.js指令碼執行,並插入到html時才會有效果。這個空檔期雖然很短,但它會造成介面閃爍。

優點:

  • 打包速度快

所以style-loadercss-loader的組合僅適用於開發。我們需要使用mini-css-extract外掛,並用該外掛提供的loader來替代style-loader

// ...
plugins:[
  // 配置mini-css-extract外掛
  new MiniCssExtractPlugin({
    // 設定抽取出來的css名字
    filename: "[name].[contentHash].css",
    chunkFilename: "[id].[contentHash].css"
  }),
  // ...
],
// ...
複製程式碼
// ...
module:{
  rules:[
    {
      test: /\.css$/,
      // 使用MiniCssExtractPlugin.loader替代style-loader
      use: [MiniCssExtractPlugin.loader, "css-loader"]
    },
    // ...
  ]
},
// ...
複製程式碼

快取處理

快取是前端頁面效能優化的重點。我們希望瀏覽器能長久快取資源,同時又能在第一時間獲取更新後的資源。

具體思路是:後端不對index.html做任何快取處理,對css、js、圖片等資源做持久快取。將output.filename配置為"main.[contentHash].js",這樣打包後的main.js中間會加上一段contentHashcontentHash是根據打包檔案內容產生的,內容改變它才會發生改變。釋出時,由於雜湊值不同,伺服器能同時儲存著不同雜湊版本的資源。這樣保證了釋出過程中,使用者仍然能夠訪問到舊資源,並且新使用者會訪問到新資源。

加上contentHash後打包檔名變成main.xxxxxx.js,其中xxxxxx代表一串很長的雜湊值。

但是,現在我們的業務程式碼、引用的第三方庫,還有webpack生成的runtime都被捆綁打包到了main.xxxxxx.js內。第三方庫和webpack的runtime變動的頻率非常低,所以我們不希望每次業務程式碼的改動導致使用者得連同它們一起重新下載一遍。因此我們需要將它們從main.xxxxxx.js內抽離出來。

還有一點需要注意,webpack以前的版本有個小小的問題。在打包檔案內容沒發生變化的情況下contentHash任然會發生改變。此時需要使用webpack.HashedModuleIdsPlugin外掛來替代預設的雜湊生成。雖然webpack4修復了這個問題,但是官方文件還是推薦我們使用webpack.HashedModuleIdsPlugin外掛。

抽離第三方庫與webpack runtime

前面已經說明了為什麼要抽離他們。

以前的版本使用commons-chunk-plugin外掛來抽離第三方庫,webpack 4通過配置optimization.splitChunks來抽取。內部其實使用了split-chunks-plugin外掛。

optimization.runtimeChunk選項配置為single,可以將webpack runtime抽離到單檔案中。

// ...
optimization: {
  // 抽離webpack runtime到單檔案
  runtimeChunk: "single",
  splitChunks: {
    chunks: "all",
    // 最大初始請求數量
    maxInitialRequests: Infinity,
    // 抽離體積大於80kb的chunk
    minSize: 80 * 1024,
    // 抽離被多個入口引用次數大於等於1的chunk
    minChunks: 1,
    cacheGroups: {
      // 抽離node_modules下面的第三方庫
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        // 從模組的路徑地址中獲得庫的名稱
        name: function(module, chunks, chacheGroupKey) {
          const packageName = module.context.match(
            /[\\/]node_modules[\\/](.*?)([\\/]|$)/
          )[1];
          return `vendor_${packageName.replace("@", "")}`;
        }
      }
    }
  },
  // ...
},
// ...
複製程式碼

js、css、html程式碼壓縮

打包後的js、css、html會有註釋、空格、換行,開啟程式碼壓縮可以大幅度減少資源的體積。

mode設為"production"時,會預設使用terser-webpack-plugin外掛對js進行壓縮。我們還要開啟html與css的壓縮,所以要重寫optimization.minimizer選項。

// ...
optimization: {
  minimizer: [
    // 壓縮css
    new OptimizeCssAssetsWebpackPlugin(),
    // 壓縮js,記得sourceMap設為true
    new TerserWebpackPlugin({ sourceMap: true }),
    // 該外掛還能對html進行壓縮
    new HtmlWebpackPlugin({
      template: "./src/template.html",
      favicon: "./src/assets/favicon-32x32-next.png",
      minify: {
        // 摺疊空白符(去除換行符和空格)
        collapseWhitespace: true,
        // 移除註釋
        removeComments: true,
        // 移除屬性上不必要的引號
        removeAttributeQuotes: true
      }
    })
  ],
  // ...
}
// ...
複製程式碼

使用source-map替代inline-source-map

發生bug時,我們很難通過打包後的程式碼找出錯誤的源頭。所以我們需要source map將程式碼對映為原來我們手寫時候的樣子。

前面的webpack.dev.js內使用的是inline-source-map。它的缺點是將map內斂到了程式碼內,這樣使用者會連同資源將map一起下載。

所以我們使用source-map,它會給打包後的每個js單獨生成.map檔案。

懶載入(lazy loading)

懶載入也叫按需載入。我們當前打包的所有js會在頁面載入過程中被載入執行。但是大多數情況下,使用者並不會訪問應用的所有頁面與功能。我們可以將每個頁面的程式碼或一些不常使用的功能模組做成按需載入,這樣可以大大減小使用者初次訪問時所要載入的資源大小。

懶載入是webpack4預設支援的,不需要任何配置。前端人員需要在開發時使用dynamic import按需引入模組。webpack會自動將dynamic import引入的模組單獨打包為一個chunk(注意:dynamic import語法上需要babel外掛的支援,會在下一章節提到該外掛)。

webpack官網提供的例子:

// print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};
複製程式碼
// src/index.js

// ...
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
  var print = module.default;

  print();
});
// ...
複製程式碼

通過點選按鈕,觸發載入print模組。/* webpackChunkName: "print" */這個註釋告訴webpack該模組打包成的chunk名字叫print

清空dist目錄

使用clean-webpack-plugin在打包前清空dist目錄。

webpack.prod.js的完整配置

安裝:

  • mini-css-extract-plugin
  • clean-webpack-plugin
  • terser-webpack-plugin optimize-css-assets-webpack-plugin
  • url-loader
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
const common = require("./webpack.common");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
const webpack = require("webpack");

module.exports = merge(common, {
  // 設定為生產(production)模式
  mode: "production",
  // 在生產環境中使用"source-map"而不是"inline-source-map"
  devtool: "source-map",
  output: {
    // 這裡新增contentHash
    // 由於我們的entry中沒有配置入口的名稱
    // webpack會預設取名為main
    // 因此這裡的配置會生成"main.xxxxxx.js"
    filename: "[name].[contentHash].js",
    // 通過splitChunks抽離的js檔名格式
    chunkFilename: "[name].[contentHash].chunk.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 這裡使用MiniCssExtractPlugin.loader替代style-loader
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: {
          // 這裡使用url-loader替代file-loader
          loader: "url-loader",
          options: {
            // 當圖片小於8kb時,url-loader會將圖片轉為base64
            // 這樣可以減少http請求的數量
            // 如果大於8kb的話,url-loader會將圖片交給file-loader處理
            // 所以url-loader需要依賴file-loader
            limit: 1024 * 8,
            name: "img/[name].[hash:8].[ext]"
          }
        }
      }
    ]
  },
  optimization: {
    // 抽離webpack runtime到單檔案
    runtimeChunk: "single",
    // 壓縮器
    minimizer: [
      // 壓縮css
      new OptimizeCssAssetsWebpackPlugin(),
      // 壓縮js,記得將sourceMap設為true
      // 否則會無法生成source map
      new TerserWebpackPlugin({ sourceMap: true }),
      // 該外掛還能壓縮html
      new HtmlWebpackPlugin({
        template: "./src/template.html",
        favicon: "./src/assets/favicon-32x32-next.png",
        minify: {
          // 摺疊空白符
          collapseWhitespace: true,
          // 移除註釋
          removeComments: true,
          // 移除屬性多餘的引號
          removeAttributeQuotes: true
        }
      })
    ],
    splitChunks: {
      chunks: "all",
      // 最大初始請求數
      maxInitialRequests: Infinity,
      // 80kb以上的chunk抽離為單獨的js檔案
      // 配合上面的 maxInitialRequests: Infinity
      // 小於80kb的所有chunk會被打包一起
      // 這樣可以減少初始請求數
      // 大家可以根據自己的情況設定
      minSize: 80 * 1024,
      // 抽離多入口引用次數1以上的chunk
      minChunks: 1,
      cacheGroups: {
        // 抽離node_modules內的第三方庫
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          // 根據路徑獲得第三方庫的名稱
          // 並將抽離的chunk以"vendor_thirdPartyLibrary"格式命名
          name: function(module, chunks, chacheGroupKey) {
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `vendor_${packageName.replace("@", "")}`;
          }
        }
      }
    }
  },
  plugins: [
    // 每次打包前,先清除輸出目錄
    new CleanWebpackPlugin(),
    // 抽離css
    new MiniCssExtractPlugin({
      filename: "[name].[contentHash].css",
      chunkFilename: "[id].[contentHash].css"
    }),
    // 確保在檔案沒發生改變時,contentHash也不會變化
    new webpack.HashedModuleIdsPlugin()
  ]
});

複製程式碼

配置npm指令碼

// ...
"scripts": {
  "start": "webpack-dev-server --config webpack.dev.js",
  "build": "webpack --config webpack.prod.js"
},
// ...
複製程式碼

結尾

基本配置完成,可以安裝react-router redux進行單頁應用開發了

npm i react-router-dom redux react-redux redux-thunk
複製程式碼

下章節內容:新增babel外掛支援decorator、類屬性與dynamic import;新增sass預處理;新增postcss Autoprefixer自動補充瀏覽器廠商字首;使用.browserslistrc配置需要相容的瀏覽器範圍;新增Prettier ESLint來規範與格式化程式碼;

其他章節

參考

Learn Webpack

Webpack 的 Bundle Split 和 Code Split 區別和應用

webpack guides

learn Webpack step by step

webpack 持久化快取實踐

大公司裡怎樣開發和部署前端程式碼?

相關文章