從0到1:React專案中的Webpack配置實戰

一颗冰淇淋發表於2024-08-25

公司專案一般都是使用集團封裝好的腳手架,腳手架內部實現咱看不到也摸不著,好不容易組內推行新的UI框架,需要自行定義 webpack 配置,這可是個絕佳的好機會,我對配置過程進行了梳理,把商業專案的成熟配置小跑著送上。

初始化

首先新建一個空資料夾,執行 npm init 初始化生成 package.json 檔案。

建立 src 資料夾,專案的業務程式碼都放在這裡,再建立 index.js,這是專案的入口檔案,待會使用 webpack 來打包專案,就從 index.js 檔案開始。

大家都知道,webpack 在開發和生產時部分配置是不同的,比如在開發時,我們希望程式碼能夠即改即生效,能夠立馬在頁面中看到效果,需要 devServer 來幫助我們實現,而在開發中,我們希望能對 node_modules 資源進行分包,需要藉助 splitChunks 屬性,而還有一些屬性是開發和生產共用的,比如 js 、css 資源的處理。

建立 webpack-config 資料夾,裡面包含 webpack.dev.js,webpack.prod.js,webpack.base.js,分別定義 webpack 的開發環境、生產環境、公共的配置。

公共配置

開發和生產的配置都需要依賴公共配置,所以我們首先來定義 webpack.base.js。

專案入口/出口

執行 npm install webpack webpack-cli 安裝 webpack 基礎依賴,再定義 webpack 配置檔案的基礎框架,entry 定義專案入口檔案,output 定義編譯後的出口,我們把編譯後檔案放在專案根目錄的 dist 資料夾下。

const path = require("path");
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.join(__dirname, "../dist"),
    filename: "[name].[contenthash:8].js",
  },
  module: {},
  plugins: [],
};

樣式資源

module 中放置各種檔案的 loader 處理規則,首先處理樣式資源,我們專案中通常使用 scss 來作為專案的前處理器,所以這裡指定 css 及 scss 處理方式(less 或其他前處理器同理)。

執行 npm install sass sass-loader postcss postcss-loader postcss-preset-env css-loader mini-css-extract-plugin --save 指令,安裝所需要的 loader 和 plugin。

css 和 scss 檔案的區別僅在於 scss 需要先透過 sass-loader 處理為 css 資源,其他的配置是一致的,這裡提取到 commonStyleLoader。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: commonStyleLoader,
      },
      {
        test: /\.scss$/,
        exclude: /node_modules/,
        use: [...commonStyleLoader, "sass-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "style/[name].[contenthash:8].css",
    }),
  ],
};

再來看處理 sass 和 css 資源的公共 loader,首先使用 postcss-loader 進行相容性處理,然後透過 css-loader 解析 css 語法,最後透過 mini-css-extract-plugin 的將 css 從 js 中分離出來並生成新的 css 檔案,也就是下面程式碼的 MiniCssExtractPlugin.loader 和上面程式碼的 new MiniCssExtractPlugin 操作 。

const commonStyleLoader = [
  MiniCssExtractPlugin.loader,
  "css-loader",
  {
    loader: "postcss-loader",
    options: {
      postcssOptions: {
        plugins: ["postcss-preset-env"],
      },
    },
  },
];

另外,postcss-loader 在進行相容性處理的時候,需要配置 browserslist 來告知相容的要求,可以直接定義在 package.json 中,開發環境相容要求為近一個版本的chrome/safari/firefox瀏覽器,生產環境相容市場份額大於 0.2% 且活躍的瀏覽器。

"browserslist": {
    "development": [
      "last 1 chrome version",
      "last 1 safari version",
      "last 1 firefox version"
    ],
    "production": [
      ">0.2%",
      "not dead"
    ]
  }

js/jsx

js 和 react 需要使用的 jsx 資源的處理方式相同,都使用 babel-loader,所以可以一起來定義 loader 匹配規則。

執行 npm install babel @babel/core babel-loader @babel/preset-env core-js@3 @babel/preset-react --save 指令來安裝相關資源。

module.exports = {
  module: {
    resolve: {
      extensions: [".jsx", ".js", ".json"],
    },
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
    ],
  },
};

extensions 中配置上常用檔案字尾名,這樣引入jsx檔案時,就可省略字尾。

babel-loader 使用 babel 工具 來處理資源,需要配置處理規則,babel 中有很多 pluginspresets,presets 包括很多 plugins,所以直接配置 presets 會方便很多。

新增 babel.config.js 檔案,使用 @babel/preset-react 處理 react 資源,透過 @babel/preset-env 處理 js 資源,再新增 corejs 的配置增加 js 相容處理 polyfills

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
    "@babel/preset-react",
  ],
};

圖片

在 webpack5 之前,處理字型圖片等資源需要透過 url-loaderfile-loader,webpack5 新增 asset module type,透過4種新的模組型別替代以前的loader。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jp(e)g|png|gif)$/,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: "img/[name].[contenthash:8].[ext]",
        },
      },
    ],
  },
};

其中模組型別 asset 替代 url-loader,可以自行定義根據圖片大小匯出資源為 Data URI 內聯到 js 檔案中,還是生成一個單獨的檔案,這裡定義以資源大小 8kb 為分界線。

html

html 檔案處理非常的簡單,透過 html-webpack-plugin 生成 HTML 檔案並注入 webpack 輸出的 javaScript 檔案和 css 檔案即可。

const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      inject: "body",
      minify: {
        collapseWhitespace: true,
      },
    }),
  ],
};

可以在 plugin 中配置模板、引入 webpack 輸出 js 資源的地址、壓縮方式。

執行測試

以上就完成了公共配置部分,現在我們來測試一下配置是否生效。

在 public 目錄下建立 index.html 檔案,初始化之後在 body 標籤中增加 react 需要的掛載容器。

<div id="container"></div>

再執行 npm install react react-dom --save 指令,在 src/index.js 檔案中定義渲染邏輯

import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
ReactDom.render(<App/>, document.getElementById('container'))

然後在 src 目錄下新建 App.js,裡面隨意編寫 jsx 程式碼。

再透過命令列工具執行 npx webpack -c webpack-config/webpack.base.js,執行結果沒有報錯就代表配置正確啦~ 有 warning ⚠ The 'mode' option has not been set 是沒關係的,後續結合開發環境和生產環境的配置一起執行時,mode 屬性會被配置上。

開發環境配置

開發環境的配置很簡單,只要使用 webpack-dev-server 開啟本地服務就行。

執行 npm install webpack-dev-server webpack-merge -D 指令安裝依賴,在 config.dev.js 中定義開發時所需要的配置。

使用 webpack-merge 工具來合併 webpack.base.js 中的公共配置,將 dev 配置和公共配置合併匯出。

const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");

const devConfig = {
  mode: "development",
  devServer: {
    open: true,
    port: 8001,
    compress: true,
    hot: true
  },
};
module.exports = merge(devConfig, baseConfig);

執行命令 npx webpack serve -c ./webpack-config/webpack.dev.js,如果能在瀏覽器自動開啟 http://localhost:8001 頁面就表示開發環境配置成功。

我們通常會把指令配置到 package.json 檔案的 script 屬性中,執行起來更為方便。

"scripts": {
    "dev": "webpack serve -c ./webpack-config/webpack.dev.js"
}

這樣,在命令列工具中,我們只需要執行 npm run dev 即可。

生產環境配置

生產環境配置的框架和開發環境類似,都是使用 webpack-merge 合併公共配置

const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");

const prodConfig = {
  mode: "production",
};
module.exports = merge(prodConfig, baseConfig);

當 mode 定義為 production 時,webpack 會自動給我們做一些程式碼壓縮和 tree shaking 等操作,我們可以自己再對編譯出來的 js 和 css 檔案進行壓縮。

js/css壓縮

執行 npm install css-minimizer-webpack-plugin terser-webpack-plugin clean-webpack-plugin --save 指令安裝依賴。

使用 css-minimizer-webpack-plugin 壓縮 css 資源,透過 terser-webpack-plugin 開啟多程序併發執行以提高構建速度。

const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");

const prodConfig = {
  optimization: {
    minimizer: [
      new CssMinimizerWebpackPlugin(),
      new TerserWebpackPlugin({
        parallel: true,
      }),
    ],
  },
};

splitChunks

目前除了非同步載入引入的模組,其他的內容都打包在了一個入口檔案中,其中包含 node_modules 中使用到的依賴,如 React、RecatDom 等,這些依賴通常是不會變化的,打包到一個公共的檔案中,可以利用瀏覽器的快取策略,不需要每次都重新拉取資源。

const prodConfig = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          filename: '[name].bundle.js'
        },
      },
    },
  },
};

這樣 node_modules中使用到的資原始檔會被打包成一個以 bundle.js 結尾的檔案

clean-webpack-plugin

當我們執行 npm run build 後,編譯後檔案會放到 dist 資料夾下,為了避免 dist 資料夾內容越來越來多不便檢視,每次我們都要手動刪除資料夾,非常不方便。clean-webpack-plugin 可以幫助我們解決這個問題。

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const prodConfig = {
  plugins: [
    new CleanWebpackPlugin()
  ]
};

externals

有些資源,我們會使用穩定可靠的 cdn 資源,提升頁面載入速度,減少打包出來的 bundle 體積,這時候就要配合 externals 使用。

如在 index.html 頁面引入 react、react-dom

    <script src="https://cdn.bootcdn.net/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>

在 webpack.prod.js 中定義 externals,告訴 webpack 當遇到 react、react-dom 時,不要將其打包,期望在全域性環境中找到 React、ReactDOM 的配置。

const prodConfig = {
  externals: {
     react: "React",
     "react-dom": "ReactDOM",
   },
};

同樣,將生產環境的編譯指令配置到 package.json 檔案的 script 屬性中。

"scripts": {
    "build": "webpack -c ./webpack-config/webpack.prod.js"
}

這樣,在命令列工具中,我們只需要執行 npm run build 即可檢視編譯產物。

總結

在真實的開發場景,還有生產模式使用 CopyWebpackPlugin 在構建過程複製檔案到輸出目錄,不經過 webpack 打包的情況,也有根據專案部署域名來定義 publicPath 的情況。這些都比較定製化,不同專案使用的配置也不相同。

另外還有前端工程化的配置大家也可以加上,如 eslint、prettier、stylelint、git hooks,為react專案新增開發/提交規範中有詳細的講述,可點選檢視。

以上就是開發和生產環境的基礎配置,大家可以在這個基礎上增加自己專案的不同配置。更多有關 前端工程化 的內容可以參考我其它的博文,持續更新中~

附上完整配置程式碼。

webpack.base.js 檔案

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const commonStyleLoader = [
  MiniCssExtractPlugin.loader,
  "css-loader",
  {
    loader: "postcss-loader",
    options: {
      postcssOptions: {
        plugins: ["postcss-preset-env"],
      },
    },
  },
];
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.join(__dirname, "../dist"),
    filename: "[name].[contenthash:8].js",
  },
  resolve: {
    extensions: [".jsx", ".js", ".json"],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: commonStyleLoader,
      },
      {
        test: /\.scss$/,
        exclude: /node_modules/,
        use: [...commonStyleLoader, "sass-loader"],
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
      {
        test: /\.(jp(e)g|png|gif)$/,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: "img/[name].[contenthash:8].[ext]",
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "style/[name].[contenthash:8].css",
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      inject: 'body',
      minify: {
        collapseWhitespace: true,
      }
    }),
  ],
};

webpack.dev.js 檔案

const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");
const devConfig = {
  mode: "development",
  devServer: {
    open: true,
    port: 8001,
    compress: true,
    hot: true
  },
};
module.exports = merge(devConfig, baseConfig);

webpack.prod.js 檔案

const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const prodConfig = {
  mode: "production",
  optimization: {
    minimizer: [
      new CssMinimizerWebpackPlugin(),
      new TerserWebpackPlugin({
        parallel: true,
      }),
    ],
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          filename: "[name].bundle.js",
        },
      },
    },
  },
  externals: {
    react: "React",
    "react-dom": "ReactDOM",
  },
  plugins: [new CleanWebpackPlugin()],
};
module.exports = merge(prodConfig, baseConfig);

相關文章