公司項(xiàng)目一般都是使用集團(tuán)封裝好的腳手架,腳手架內(nèi)部實(shí)現(xiàn)咱看不到也摸不著,好不容易組內(nèi)推行新的UI框架,需要自行定義 webpack 配置,這可是個(gè)絕佳的好機(jī)會(huì),我對(duì)配置過(guò)程進(jìn)行了梳理,把商業(yè)項(xiàng)目的成熟配置小跑著送上。
初始化
首先新建一個(gè)空文件夾,執(zhí)行 npm init 初始化生成 package.json 文件。
創(chuàng)建 src 文件夾,項(xiàng)目的業(yè)務(wù)代碼都放在這里,再創(chuàng)建 index.js,這是項(xiàng)目的入口文件,待會(huì)使用 webpack 來(lái)打包項(xiàng)目,就從 index.js 文件開(kāi)始。
大家都知道,webpack 在開(kāi)發(fā)和生產(chǎn)時(shí)部分配置是不同的,比如在開(kāi)發(fā)時(shí),我們希望代碼能夠即改即生效,能夠立馬在頁(yè)面中看到效果,需要 devServer 來(lái)幫助我們實(shí)現(xiàn),而在開(kāi)發(fā)中,我們希望能對(duì) node_modules 資源進(jìn)行分包,需要借助 splitChunks 屬性,而還有一些屬性是開(kāi)發(fā)和生產(chǎn)共用的,比如 js 、css 資源的處理。
創(chuàng)建 webpack-config 文件夾,里面包含 webpack.dev.js,webpack.prod.js,webpack.base.js,分別定義 webpack 的開(kāi)發(fā)環(huán)境、生產(chǎn)環(huán)境、公共的配置。
公共配置
開(kāi)發(fā)和生產(chǎn)的配置都需要依賴公共配置,所以我們首先來(lái)定義 webpack.base.js。
項(xiàng)目入口/出口
執(zhí)行 npm install webpack webpack-cli 安裝 webpack 基礎(chǔ)依賴,再定義 webpack 配置文件的基礎(chǔ)框架,entry 定義項(xiàng)目入口文件,output 定義編譯后的出口,我們把編譯后文件放在項(xiàng)目根目錄的 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 處理規(guī)則,首先處理樣式資源,我們項(xiàng)目中通常使用 scss 來(lái)作為項(xiàng)目的預(yù)處理器,所以這里指定 css 及 scss 處理方式(less 或其他預(yù)處理器同理)。
執(zhí)行 npm install sass sass-loader postcss postcss-loader postcss-preset-env css-loader mini-css-extract-plugin --save 指令,安裝所需要的 loader 和 plugin。
css 和 scss 文件的區(qū)別僅在于 scss 需要先通過(guò) 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",
}),
],
};
再來(lái)看處理 sass 和 css 資源的公共 loader,首先使用 postcss-loader 進(jìn)行兼容性處理,然后通過(guò) css-loader 解析 css 語(yǔ)法,最后通過(guò) mini-css-extract-plugin 的將 css 從 js 中分離出來(lái)并生成新的 css 文件,也就是下面代碼的 MiniCssExtractPlugin.loader 和上面代碼的 new MiniCssExtractPlugin 操作 。
const commonStyleLoader = [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
];
另外,postcss-loader 在進(jìn)行兼容性處理的時(shí)候,需要配置 browserslist 來(lái)告知兼容的要求,可以直接定義在 package.json 中,開(kāi)發(fā)環(huán)境兼容要求為近一個(gè)版本的chrome/safari/firefox瀏覽器,生產(chǎn)環(huán)境兼容市場(chǎng)份額大于 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,所以可以一起來(lái)定義 loader 匹配規(guī)則。
執(zhí)行 npm install babel @babel/core babel-loader @babel/preset-env core-js@3 @babel/preset-react --save 指令來(lái)安裝相關(guān)資源。
module.exports = {
module: {
resolve: {
extensions: [".jsx", ".js", ".json"],
},
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
],
},
};
extensions 中配置上常用文件后綴名,這樣引入jsx文件時(shí),就可省略后綴。
babel-loader 使用 babel 工具 來(lái)處理資源,需要配置處理規(guī)則,babel 中有很多 plugins 和 presets,presets 包括很多 plugins,所以直接配置 presets 會(huì)方便很多。
新增 babel.config.js 文件,使用 @babel/preset-react 處理 react 資源,通過(guò) @babel/preset-env 處理 js 資源,再添加 corejs 的配置增加 js 兼容處理 polyfills。
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3,
},
],
"@babel/preset-react",
],
};
圖片
在 webpack5 之前,處理字體圖片等資源需要通過(guò) url-loader 和 file-loader,webpack5 新增 asset module type,通過(guò)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,可以自行定義根據(jù)圖片大小導(dǎo)出資源為 Data URI 內(nèi)聯(lián)到 js 文件中,還是生成一個(gè)單獨(dú)的文件,這里定義以資源大小 8kb 為分界線。
html
html 文件處理非常的簡(jiǎn)單,通過(guò) 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 資源的地址、壓縮方式。
運(yùn)行測(cè)試
以上就完成了公共配置部分,現(xiàn)在我們來(lái)測(cè)試一下配置是否生效。
在 public 目錄下創(chuàng)建 index.html 文件,初始化之后在 body 標(biāo)簽中增加 react 需要的掛載容器。
<div id="container"></div>
再執(zhí)行 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,里面隨意編寫(xiě) jsx 代碼。
再通過(guò)命令行工具執(zhí)行 npx webpack -c webpack-config/webpack.base.js,運(yùn)行結(jié)果沒(méi)有報(bào)錯(cuò)就代表配置正確啦~ 有 warning ? The 'mode' option has not been set 是沒(méi)關(guān)系的,后續(xù)結(jié)合開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境的配置一起執(zhí)行時(shí),mode 屬性會(huì)被配置上。
開(kāi)發(fā)環(huán)境配置
開(kāi)發(fā)環(huán)境的配置很簡(jiǎn)單,只要使用 webpack-dev-server 開(kāi)啟本地服務(wù)就行。
執(zhí)行 npm install webpack-dev-server webpack-merge -D 指令安裝依賴,在 config.dev.js 中定義開(kāi)發(fā)時(shí)所需要的配置。
使用 webpack-merge 工具來(lái)合并 webpack.base.js 中的公共配置,將 dev 配置和公共配置合并導(dǎo)出。
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);
運(yùn)行命令 npx webpack serve -c ./webpack-config/webpack.dev.js,如果能在瀏覽器自動(dòng)打開(kāi) http://localhost:8001 頁(yè)面就表示開(kāi)發(fā)環(huán)境配置成功。
我們通常會(huì)把指令配置到 package.json 文件的 script 屬性中,執(zhí)行起來(lái)更為方便。
"scripts": {
"dev": "webpack serve -c ./webpack-config/webpack.dev.js"
}
這樣,在命令行工具中,我們只需要執(zhí)行 npm run dev 即可。
生產(chǎn)環(huán)境配置
生產(chǎn)環(huán)境配置的框架和開(kāi)發(fā)環(huán)境類似,都是使用 webpack-merge 合并公共配置
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");
const prodConfig = {
mode: "production",
};
module.exports = merge(prodConfig, baseConfig);
當(dāng) mode 定義為 production 時(shí),webpack 會(huì)自動(dòng)給我們做一些代碼壓縮和 tree shaking 等操作,我們可以自己再對(duì)編譯出來(lái)的 js 和 css 文件進(jìn)行壓縮。
js/css壓縮
執(zhí)行 npm install css-minimizer-webpack-plugin terser-webpack-plugin clean-webpack-plugin --save 指令安裝依賴。
使用 css-minimizer-webpack-plugin 壓縮 css 資源,通過(guò) terser-webpack-plugin 開(kāi)啟多進(jìn)程并發(fā)運(yùn)行以提高構(gòu)建速度。
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const prodConfig = {
optimization: {
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin({
parallel: true,
}),
],
},
};
splitChunks
目前除了異步加載引入的模塊,其他的內(nèi)容都打包在了一個(gè)入口文件中,其中包含 node_modules 中使用到的依賴,如 React、RecatDom 等,這些依賴通常是不會(huì)變化的,打包到一個(gè)公共的文件中,可以利用瀏覽器的緩存策略,不需要每次都重新拉取資源。
const prodConfig = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: '[name].bundle.js'
},
},
},
},
};
這樣 node_modules中使用到的資源文件會(huì)被打包成一個(gè)以 bundle.js 結(jié)尾的文件
clean-webpack-plugin
當(dāng)我們執(zhí)行 npm run build 后,編譯后文件會(huì)放到 dist 文件夾下,為了避免 dist 文件夾內(nèi)容越來(lái)越來(lái)多不便查看,每次我們都要手動(dòng)刪除文件夾,非常不方便。clean-webpack-plugin 可以幫助我們解決這個(gè)問(wèn)題。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const prodConfig = {
plugins: [
new CleanWebpackPlugin()
]
};
externals
有些資源,我們會(huì)使用穩(wěn)定可靠的 cdn 資源,提升頁(yè)面加載速度,減少打包出來(lái)的 bundle 體積,這時(shí)候就要配合 externals 使用。
如在 index.html 頁(yè)面引入 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 當(dāng)遇到 react、react-dom 時(shí),不要將其打包,期望在全局環(huán)境中找到 React、ReactDOM 的配置。
const prodConfig = {
externals: {
react: "React",
"react-dom": "ReactDOM",
},
};
同樣,將生產(chǎn)環(huán)境的編譯指令配置到 package.json 文件的 script 屬性中。
"scripts": {
"build": "webpack -c ./webpack-config/webpack.prod.js"
}
這樣,在命令行工具中,我們只需要執(zhí)行 npm run build 即可查看編譯產(chǎn)物。
總結(jié)
在真實(shí)的開(kāi)發(fā)場(chǎng)景,還有生產(chǎn)模式使用 CopyWebpackPlugin 在構(gòu)建過(guò)程復(fù)制文件到輸出目錄,不經(jīng)過(guò) webpack 打包的情況,也有根據(jù)項(xiàng)目部署域名來(lái)定義 publicPath 的情況。這些都比較定制化,不同項(xiàng)目使用的配置也不相同。
另外還有前端工程化的配置大家也可以加上,如 eslint、prettier、stylelint、git hooks,為react項(xiàng)目添加開(kāi)發(fā)/提交規(guī)范中有詳細(xì)的講述,可點(diǎn)擊查看。
以上就是開(kāi)發(fā)和生產(chǎn)環(huán)境的基礎(chǔ)配置,大家可以在這個(gè)基礎(chǔ)上增加自己項(xiàng)目的不同配置。更多有關(guān) 前端、工程化 的內(nèi)容可以參考我其它的博文,持續(xù)更新中~
附上完整配置代碼。
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);