保持控制:Webpack 和 React 指南,Pt。 1

已發表: 2022-03-11

在開始一個新的 React 項目時,您有許多模板可供選擇:創建 React App、 react-boilerplate和 React Starter Kit,僅舉幾例。

這些模板被成千上萬的開發人員採用,能夠支持非常大規模的應用程序開發。 但是它們讓開發人員體驗和捆綁輸出背負著各種默認值,這可能並不理想。

如果您想對構建過程保持更大程度的控制,那麼您可能會選擇投資自定義 Webpack 配置。 正如您將從本 Webpack 教程中了解到的那樣,此任務並不是很複雜,而且在排除其他人的配置問題時,這些知識甚至可能很有用。

Webpack:入門

我們今天編寫 JavaScript 的方式與瀏覽器可以執行的代碼不同。 我們經常依賴現代瀏覽器尚未支持的其他類型的資源、轉譯語言和實驗性功能。 Webpack 是一個用於 JavaScript 的模塊打包器,它可以彌補這一差距,並在開發人員體驗方面免費生成跨瀏覽器兼容的代碼。

在我們開始之前,您應該記住,本 Webpack 教程中提供的所有代碼也可以在 GitHub 上以完整的 Webpack/React 示例配置文件的形式獲得。 如果您有任何問題,請隨時在此處參考並返回本文。

基本配置

從 Legato(版本 4)開始,Webpack 不需要任何配置即可運行。 選擇構建模式將應用一組更適合目標環境的默認值。 本著本文的精神,我們將拋開這些默認設置,自己為每個目標環境實施合理的配置。

首先,我們需要安裝webpackwebpack-cli

 npm install -D webpack webpack-cli

然後我們需要使用具有以下選項的配置填充webpack.config.js

  • devtool :在開發模式下啟用源映射生成。
  • entry :我們的 React 應用程序的主文件。
  • output.path :存儲輸出文件的根目錄。
  • output.filename :用於生成文件的文件名模式。
  • output.publicPath :文件將在 Web 服務器上部署的根目錄的路徑。
 const path = require("path"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; const isDevelopment = !isProduction; return { devtool: isDevelopment && "cheap-module-source-map", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" } }; };

上述配置適用於純 JavaScript 文件。 但是在使用 Webpack 和 React 時,我們需要在將代碼發送給用戶之前執行額外的轉換。 在下一節中,我們將使用 Babel 來改變 Webpack 加載 JavaScript 文件的方式。

JS 加載器

Babel 是一個帶有許多用於代碼轉換的插件的 JavaScript 編譯器。 在本節中,我們將把它作為加載器引入到我們的 Webpack 配置中,並將其配置為將現代 JavaScript 代碼轉換為普通瀏覽器可以理解的代碼。

首先,我們需要安裝babel-loader@babel/core

 npm install -D @babel/core babel-loader

然後我們將在我們的 Webpack 配置中添加一個module部分,讓babel-loader負責加載 JavaScript 文件:

 @@ -11,6 +11,25 @@ module.exports = function(_env, argv) { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + cacheDirectory: true, + cacheCompression: false, + envName: isProduction ? "production" : "development" + } + } + } + ] + }, + resolve: { + extensions: [".js", ".jsx"] } }; };

我們將使用單獨的配置文件babel.config.js來配置 Babel。 它將使用以下功能:

  • @babel/preset-env :將現代 JavaScript 功能轉換為向後兼容的代碼。
  • @babel/preset-react :將 JSX 語法轉換為普通的 JavaScript 函數調用。
  • @babel/plugin-transform-runtime :通過將 Babel 助手提取到共享模塊中來減少代碼重複。
  • @babel/plugin-syntax-dynamic-import :在缺乏原生Promise支持的瀏覽器中啟用動態import()語法。
  • @babel/plugin-proposal-class-properties :啟用對公共實例字段語法提案的支持,用於編寫基於類的 React 組件。

我們還將啟用一些特定於 React 的生產優化:

  • babel-plugin-transform-react-remove-prop-types -types 從生產代碼中刪除不必要的 prop-types。
  • @babel/plugin-transform-react-inline-elements在編譯期間評估React.createElement並內聯結果。
  • @babel/plugin-transform-react-constant-elements靜態 React 元素提取為常量。

下面的命令將安裝所有必要的依賴項:

 npm install -D @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-class-properties babel-plugin-transform-react-remove-prop-types @babel/plugin-transform-react-inline-elements @babel/plugin-transform-react-constant-elements

然後我們將使用這些設置填充babel.config.js

 module.exports = { presets: [ [ "@babel/preset-env", { modules: false } ], "@babel/preset-react" ], plugins: [ "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties" ], env: { production: { only: ["src"], plugins: [ [ "transform-react-remove-prop-types", { removeImport: true } ], "@babel/plugin-transform-react-inline-elements", "@babel/plugin-transform-react-constant-elements" ] } } };

這種配置允許我們以與所有相關瀏覽器兼容的方式編寫現代 JavaScript。 在 React 應用程序中我們可能需要其他類型的資源,我們將在以下部分中介紹。

CSS 加載器

在設計 React 應用程序的樣式時,我們至少需要能夠包含純 CSS 文件。 我們將使用以下加載器在 Webpack 中執行此操作:

  • css-loader :解析 CSS 文件,解析外部資源,例如圖像、字體和其他樣式導入。
  • style-loader :在開發過程中,在運行時將加載的樣式註入到文檔中。
  • mini-css-extract-plugin :將加載的樣式提取到單獨的文件中以供生產使用,以利用瀏覽器緩存。

讓我們安裝上面的 CSS 加載器:

 npm install -D css-loader style-loader mini-css-extract-plugin

然後我們將在 webpack 配置的module.rules部分添加一個新規則:

 @@ -1,4 +1,5 @@ const path = require("path"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -25,6 +26,13 @@ module.exports = function(_env, argv) { envName: isProduction ? "production" : "development" } } + }, + { + test: /\.css$/, + use: [ + isProduction ? MiniCssExtractPlugin.loader : "style-loader", + "css-loader" + ] } ] },

我們還將MiniCssExtractPlugin添加到plugins部分,我們將僅在生產模式下啟用:

 @@ -38,6 +38,13 @@ module.exports = function(_env, argv) { }, resolve: { extensions: [".js", ".jsx"] - } + }, + plugins: [ + isProduction && + new MiniCssExtractPlugin({ + filename: "assets/css/[name].[contenthash:8].css", + chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" + }) + ].filter(Boolean) }; };

此配置適用於純 CSS 文件,並且可以擴展為與各種 CSS 處理器一起使用,例如 Sass 和 PostCSS,我們將在下一篇文章中討論。

圖像加載器

Webpack 還可以用於加載靜態資源,例如圖像、視頻和其他二進製文件。 處理此類文件的最通用方法是使用file-loaderurl-loader ,它將為其消費者提供所需資源的 URL 引用。

在本節中,我們將添加url-loader來處理常見的圖像格式。 url-loaderfile-loader的不同之處在於,如果原始文件的大小小於給定的閾值,它將把整個文件作為 base64 編碼的內容嵌入到 URL 中,從而無需額外的請求。

首先我們安裝url-loader

 npm install -D url-loader

然後我們在 Webpack 配置的module.rules部分添加一個新規則:

 @@ -34,6 +34,16 @@ module.exports = function(_env, argv) { isProduction ? MiniCssExtractPlugin.loader : "style-loader", "css-loader" ] + }, + { + test: /\.(png|jpg|gif)$/i, + use: { + loader: "url-loader", + options: { + limit: 8192, + name: "static/media/[name].[hash:8].[ext]" + } + } } ] },

SVG

對於 SVG 圖像,我們將使用@svgr/webpack加載器,它將導入的文件轉換為 React 組件。

我們安裝@svgr/webpack

 npm install -D @svgr/webpack

然後我們在 Webpack 配置的module.rules部分添加一個新規則:

 @@ -44,6 +44,10 @@ module.exports = function(_env, argv) { name: "static/media/[name].[hash:8].[ext]" } } + }, + { + test: /\.svg$/, + use: ["@svgr/webpack"] } ] },

SVG 圖像作為 React 組件可以很方便, @svgr/webpack使用 SVGO 進行優化。

注意:對於某些動畫甚至鼠標懸停效果,您可能需要使用 JavaScript 操作 SVG。 幸運的是, @svgr/webpack將 SVG 內容嵌入到 JavaScript 包中,允許您繞過為此所需的安全限制。

文件加載器

當我們需要引用任何其他類型的文件時,通用file-loader將完成這項工作。 它的工作方式類似於url-loader ,為需要它的代碼提供一個資產 URL,但它沒有嘗試對其進行優化。

與往常一樣,首先我們安裝 Node.js 模塊。 在這種情況下, file-loader

 npm install -D file-loader

然後我們將新規則添加到 Webpack 配置的module.rules部分。 例如:

 @@ -48,6 +48,13 @@ module.exports = function(_env, argv) { { test: /\.svg$/, use: ["@svgr/webpack"] + }, + { + test: /\.(eot|otf|ttf|woff|woff2)$/, + loader: require.resolve("file-loader"), + options: { + name: "static/media/[name].[hash:8].[ext]" + } } ] },

在這裡,我們添加了用於加載字體file-loader ,您可以從 CSS 文件中引用它。 您可以擴展此示例以加載您需要的任何其他類型的文件。

環境插件

我們可以使用 Webpack 的DefinePlugin()將環境變量從構建環境暴露給我們的應用程序代碼。 例如:

 @@ -1,5 +1,6 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const webpack = require("webpack"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -65,7 +66,12 @@ module.exports = function(_env, argv) { new MiniCssExtractPlugin({ filename: "assets/css/[name].[contenthash:8].css", chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" - }) + }), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify( + isProduction ? "production" : "development" + ) + }) ].filter(Boolean) }; };

在這裡,我們將process.env.NODE_ENV替換為表示構建模式的字符串: "development""production"

HTML 插件

在沒有index.html文件的情況下,我們的 JavaScript 包毫無用處,只是坐在那裡沒有人能找到它。 在本節中,我們將介紹html-webpack-plugin為我們生成一個 HTML 文件。

我們安裝html-webpack-plugin

 npm install -D html-webpack-plugin

然後我們將html-webpack-plugin添加到 Webpack 配置的plugins部分:

 @@ -1,6 +1,7 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -71,6 +72,10 @@ module.exports = function(_env, argv) { "process.env.NODE_ENV": JSON.stringify( isProduction ? "production" : "development" ) + }), + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, "public/index.html"), + inject: true }) ].filter(Boolean) };

生成的public/index.html文件將加載我們的包並引導我們的應用程序。

優化

我們可以在構建過程中使用多種優化技術。 我們將從代碼縮小開始,通過這個過程,我們可以在不犧牲功能的情況下減小包的大小。 我們將使用兩個插件來最小化我們的代碼:用於 JavaScript 代碼的terser-webpack-plugin和用於 CSS optimize-css-assets-webpack-plugin

讓我們安裝它們:

 npm install -D terser-webpack-plugin optimize-css-assets-webpack-plugin

然後我們將在我們的配置中添加一個optimization部分:

 @@ -2,6 +2,8 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const TerserWebpackPlugin = require("terser-webpack-plugin"); +const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -75,6 +77,27 @@ module.exports = function(_env, argv) { isProduction ? "production" : "development" ) }) - ].filter(Boolean) + ].filter(Boolean), + optimization: { + minimize: isProduction, + minimizer: [ + new TerserWebpackPlugin({ + terserOptions: { + compress: { + comparisons: false + }, + mangle: { + safari10: true + }, + output: { + comments: false, + ascii_only: true + }, + warnings: false + } + }), + new OptimizeCssAssetsPlugin() + ] + } }; };

上述設置將確保代碼與所有現代瀏覽器兼容。

代碼拆分

代碼拆分是我們可以用來提高應用程序性能的另一種技術。 代碼拆分可以指兩種不同的方法:

  1. 使用動態import()語句,我們可以提取應用程序中占我們包大小的重要部分的部分,並按需加載它們。
  2. 我們可以提取更改頻率較低的代碼,以利用瀏覽器緩存並提高重複訪問者的性能。

我們將使用將第三方依賴項和公共塊提取到單獨文件中的設置填充 Webpack 配置的optimization.splitChunks部分:

 @@ -99,7 +99,29 @@ module.exports = function(_env, argv) { sourceMap: true }), new OptimizeCssAssetsPlugin() - ] + ], + splitChunks: { + chunks: "all", + minSize: 0, + maxInitialRequests: 20, + maxAsyncRequests: 20, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name(module, chunks, cacheGroupKey) { + const packageName = module.context.match( + /[\\/]node_modules[\\/](.*?)([\\/]|$)/ + )[1]; + return `${cacheGroupKey}.${packageName.replace("@", "")}`; + } + }, + common: { + minChunks: 2, + priority: -10 + } + } + }, + runtimeChunk: "single" } }; };

讓我們更深入地看看我們在這裡使用的選項:

  • chunks: "all" :默認情況下,普通塊提取僅影響使用動態import()加載的模塊。 此設置還可以優化入口點加載。
  • minSize: 0 :默認情況下,只有超過一定大小閾值的塊才有資格被提取。 此設置可以優化所有常見代碼,無論其大小。
  • maxInitialRequests: 20maxAsyncChunks: 20 :這些設置分別增加了入口點導入和分割點導入可以並行加載的源文件的最大數量。

此外,我們指定以下cacheGroups配置:

  • vendors :配置第三方模塊的提取。
    • test: /[\\/]node_modules[\\/]/ :用於匹配第三方依賴項的文件名模式。
    • name(module, chunks, cacheGroupKey) :通過給它們一個通用名稱,將來自同一模塊的不同塊組合在一起。
  • common :配置從應用程序代碼中提取公共塊。
    • minChunks: 2 :如果從至少兩個模塊中引用,則認為一個塊是通用的。
    • priority: -10 :為common緩存組分配負優先級,以便首先考慮vendors緩存組的塊。

我們還通過指定runtimeChunk: "single"將 Webpack 運行時代碼提取到可以在多個入口點之間共享的單個塊中。

開發服務器

到目前為止,我們一直專注於創建和優化應用程序的生產構建,但 Webpack 也有自己的 Web 服務器,具有實時重新加載和錯誤報告功能,這將有助於我們的開發過程。 它叫做webpack-dev-server ,我們需要單獨安裝它:

 npm install -D webpack-dev-server

在這個片段中,我們在 Webpack 配置中引入了一個devServer部分:

 @@ -120,6 +120,12 @@ module.exports = function(_env, argv) { } }, runtimeChunk: "single" + }, + devServer: { + compress: true, + historyApiFallback: true, + open: true, + overlay: true } }; };

在這裡,我們使用了以下選項:

  • compress: true : 啟用資產壓縮以更快地重新加載。
  • historyApiFallback: true : 為基於歷史的路由啟用對index.html的回退。
  • open: true : 啟動開發服務器後打開瀏覽器。
  • overlay: true : 在瀏覽器窗口中顯示 Webpack 錯誤。

您可能還需要配置代理設置以將 API 請求轉發到後端服務器。

Webpack 和 React:性能優化並準備就緒!

我們剛剛學習瞭如何使用 Webpack 加載各種資源類型,如何在開發環境中使用 Webpack,以及優化生產構建的幾種技術。 如果需要,您可以隨時查看完整的配置文件以獲取您自己的 React/Webpack 設置的靈感。 對於任何提供 React 開發服務的人來說,提高這些技能是標準的。

在本系列的下一部分中,我們將使用針對更具體用例的說明擴展此配置,包括 TypeScript 使用、CSS 預處理器以及涉及服務器端渲染和 ServiceWorkers 的高級優化技術。 請繼續關注以了解您需要了解的有關 Webpack 的所有信息,以便將您的 React 應用程序投入生產。