使用webpack4一步步搭建react專案(二)
前面已經實現了一個簡單webpack配置,接下來需要在前面的基礎上對 webpack.config.js
進行拆分。
第二章 拆分webpack配置
專案開發時,我們需要用 webpack-dev-server
啟動開發伺服器,當我們修改檔案時,它能自動重新打包專案並重新整理頁面。
專案打包上線時,我們希望webpack能進行更多的處理來優化打包後的程式碼。
針對不同的需求,我們需要將配置檔案拆分為 webpack.common.js
webpack.dev.js
webpack.prod.js
。
專案結構

程式碼
配置 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-loader
和 style-loader
。
css-loader
將專案匯入的css樣式轉為js模組,打包到 main.js
內。
style-loader
在 main.js
內提供了一個能將css動態插入到html內的方法。
當用戶開啟頁面時,會先載入html,然後載入 main.js
,最後執行js指令碼將樣式插入到 style
標籤內。
這樣有多個缺點:
main.js main.js
優點:
- 打包速度快
所以 style-loader
與 css-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
中間會加上一段 contentHash
。 contentHash
是根據打包檔案內容產生的,內容改變它才會發生改變。釋出時,由於雜湊值不同,伺服器能同時儲存著不同雜湊版本的資源。這樣保證了釋出過程中,使用者仍然能夠訪問到舊資源,並且新使用者會訪問到新資源。
加上 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來規範與格式化程式碼;