前言
本篇文章將介紹一些webpack的進階用法,演示內容繼承自上一篇文章的內容,所以沒看過上一篇文章的建議先學習上一篇內容再閲讀此篇內容,會更有利於此篇的學習~
文件指紋
文件指紋指的是打包輸出的文件名後綴,一般用來做版本管理、緩存等
webpack的指紋策略有三種:hash、chunkhash、contenthash,它們之間最主要的區別就是每種hash影響的範圍不同。
佔位符
webpack提供佔位符用於將特定信息附加在打包輸出的文件上
| 名稱 | 含義 |
|---|---|
| [ext] | 資源後綴名 |
| [id] | 文件標識符 |
| [name] | 文件名稱 |
| [path] | 文件的相對路徑 |
| [folder] | 文件所在的文件夾 |
| [hash] | 模塊標識符的 hash,默認是 md5 生成 |
| [chunkhash] | chunk 內容的 hash,默認是 md5 生成 |
| [contenthash] | 文件內容的 hash,默認是 md5 生成 |
| [query] | 文件的 query,例如,文件名 ? 後面的字符串 |
| [emoji] | 一個隨機的指代文件內容的 emoji |
我們可以使用特定的語法,對 hash 、 chunkhash、contenthash 進行切片:[chunkhash:4],像 8c4cbfdb91ff93f3f3c5 這樣的哈希會最後會變為 8c4c。
hash
與整個項目的構建有關,只要項目內文件有修改,整個項目構建的hash值就會改變
我們使用多入口打包來體驗一下:
// webpack.config.js
module.exports = {
entry: {
main: './src/main.js',
index: './src/index.js'
},
output: {
filename: '[name].[hash:6].js',
path: __dirname + '/dist',
clean: true
},
// ...
}
此時我們使用了佔位符來設置文件指紋[name].[hash:6].js代表的是文件名+6位hash
此時我們執行npm run build,看打包出來的內容如下:
此時兩個js文件的hash都是207495
我們修改一下index.js的內容,再打包一次
我們會發現此時兩個js文件的hash都變成了9f0e2d
chunkhash
chunkhash 是和 webpack 打包的模塊相關,每一個 entry 作為一個模塊,會產生不同的 Chunkhash 值,文件改變時只會影響當前chunk組的hash值
我們再來看看chunkhash
// webpack.config.js
module.exports = {
entry: {
main: './src/main.js',
index: './src/index.js'
},
output: {
filename: '[name].[chunkhash:6].js',
path: __dirname + '/dist',
clean: true
},
// ...
}
還是延用上面的例子,這次我們只修改main.js文件內容
修改前兩個文件的hash值如下:
修改後:
此時只有main.js的打包產物的hash發生了變化
contenthash
contenthash 是和根據文件內容相關,單個文件發生變化,只會引起此文件的hash值
這裏我們使用miniCssExtractPlugin將CSS內容提取成文件,併為它設置contenthash
// webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
main: './src/main.js',
index: './src/index.js'
},
output: {
filename: '[name].[contenthash:6].js',
path: __dirname + '/dist',
clean: true
},
mudole: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader']
},
// ...
]
},
plugins: [
// ...
new miniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css'
}),
]
// ...
}
然後打包看一下此時的hash:
我們修改index.css內容再打包一次
此時只有index.css的打包產物hash值發生了變化。
根據不同的文件類型一般選擇不同的文件指紋策略,通常情況下:
- JS文件採用[chunkhash]文件指紋策略
- CSS文件採用[contenthash]文件指紋策略
- 圖片資源採用[hash]文件指紋策略
代碼壓縮
壓縮JS
目前最成熟的JavaScript代碼壓縮工具是UglifyJS,它能夠分析JavaScript語法樹,理解代碼含義,從而能做到諸如去掉無效代碼、去掉日誌輸出代碼、縮短變量名等優化。但很遺憾的是UglifyJS不再維護,並且它不支持 ES6 + 。
現在推薦使用的是Terser,它在 UglifyJS 基礎上增加了 ES6 語法支持,並重構代碼解析、壓縮算法,使得執行效率與壓縮率都有較大提升,並且Webpack5.0 後默認使用 Terser 作為 JavaScript 代碼壓縮器
簡單實用:
// webpack.config.js
module.exports = {
//...
optimization: {
minimize: true
}
}
需要注意的是在生產模式中構建時,Terser壓縮是默認開啓的
當然它也允許你通過提供一個或多個定製過的TerserPlugin實例,覆蓋默認的壓縮工具,實現更精細的壓縮功能
// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
//...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
},
})
]
}
}
在Webpack4中 默認使用 uglifyjs-webpack-plugin壓縮代碼,也可以通過 minimizer 數組替換為 Terser 插件
壓縮CSS
CSS代碼同樣也可以使用webpack來進行壓縮,比較常見的CSS壓縮工具有:cssnano、css-minimizer-webpack-plugin
對於 webpack5 或更高版本,官方推薦使用 CssMinimizerWebpackPlugin,該插件是使用 cssnano 優化和壓縮 CSS,支持緩存和併發模式下運行。
安裝:
npm i css-minimizer-webpack-plugin
配置:
// webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 用壓縮css
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 用來提取css成單獨的文件
module.exports = {
//...
module: {
rules: [
{
test: /.css$/,
// 注意,MiniCssExtractPlugin.loader 與 style-loader不能同時使用
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
optimization: {
minimize: true,
minimizer: [
// Webpack5 之後,約定使用 '...' 字面量保留默認 minimizer 配置
"...",
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};
⚠️這裏需要注意的是需要使用 mini-css-extract-plugin 將 CSS 代碼抽取為單獨的 CSS 產物文件,這樣才能命中 css-minimizer-webpack-plugin 默認的 test 邏輯。
壓縮HTML
我們之前使用的html-webpack-plugin,它除了可以生成html模版,也可以用來對html進行壓縮。
htmlWebpackPlugin常見參數
template:模板的路徑,默認會去尋找src/index.ejs是否存在。filename:輸出文件的名稱,默認為index.html。inject:是否將資源注入到模版中,默認為true。minify:壓縮參數。在生產模式下(production),默認為true;否則,默認為false。
// webpack.config.js
module.exports = {
// ...
plugins: [
// ...
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
minify: true
}),
]
}
生成的 HTML 將使用 html-minifier-terser 和以下選項進行壓縮,所以它實際上的壓縮功能其實是html-minifier-terser來實現的,更多配置可以查看這個工具文檔
{
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}
禁止生成LICENSE文件
經過上面這些配置後,我發現了一個奇怪的問題,那就是每個bundle產物都多了一個同名的LICENSE.txt文件,打開一看裏面都是一些註釋內容。
為什麼會生成這些文件,帶着疑惑我去翻了下官方文檔,Webpack5 默認壓縮代碼工具為terser-webpack-plugin,那就先從它入手吧。
在它的配置中找到了extractComments參數,默認值為true,表示將註釋剝離到單獨的文件中。
如果我們不想要,直接關掉該配置就行了
module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
new cssMinimizerPlugin(),
new terserPlugin({
extractComments: false, // 關閉註釋剝離功能
}),
'...'
]
},
}
CSS增強(autoprefixer)
前端最頭疼的問題莫過於處理兼容性,因為前端的運行環境並不固定,可以在各種瀏覽器以及各種webview中運行,並且每個瀏覽器廠商對CSS的寫法也各不相同,這就勢必會導致出現一些問題。
比如為了兼容各種瀏覽器內核,圓角屬性應該這樣寫:
.container {
-moz-border-radius: 16px;
-webkit-border-radius: 16px;
-o-border-radius: 16px;
border-radius: 16px;
}
試想一下如果在開發中需要你這樣寫,那是不是太不合理了?
我們一般都會通過webpack配置插件來幫我們解決這個問題,處理CSS我們首先會想到postcss,沒錯webpack也有使用postcss處理CSS的loader --- postcss-loader,然後我們還需要使用postcss的插件autoprefixer來幫我們自動添加瀏覽器前綴。
安裝:
npm i postcss postcss-loader autoprefixer
修改配置:
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
//...
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
}]
},
]
}
//...
}
⚠️這裏需要注意的是,如果你想自定義轉換的規則,最好是將 autoprefixer 的 browsers選項替換為 browserslist 配置。在 package.json 或。Browserslistrc 文件。使用 browsers選項可能導致錯誤,並且browserslist 配置可以用於 babel、 autoprefixer、 postcss-norize 等工具。
比如package.json中配置browserslist:
// package.json
{
//...
"browserslist": [
"last 10 Chrome versions",
"last 5 Firefox versions",
"Safari >= 6",
"ie> 8"
]
}
此時我們打包的CSS的產物就會自動添加瀏覽器前綴
靜態資源拷貝
假如我們需要在html中引用一些不需要打包處理的資源,比如下面這種情況
在index.html中引入了一些日誌的工具函數,這時候我們直接跑起來會發現這個文件直接404了,這是怎麼回事?
首先我們寫的路徑肯定是沒問題的,問題在於我們打包後這個utils文件肯定是不在這個位置了,所以會報404
所以這裏我們需要使用copy-webpack-plugin將文件拷貝至dist目錄下
// webpack.config.js
const copyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
// ...
plugins: [
new copyWebpackPlugin({
patterns: [
{from: 'module', to: __dirname + '/dist/module/'}
]
}),
]
}
此時再打包,我們會發現dist目錄下已經有了module/utils.js,並且頁面也不會再報404了
sourcemap
SourceMap 就是一個信息文件,裏面儲存着代碼的位置信息。這種文件主要用於開發調試,現在代碼都會經過壓縮混淆,這樣報錯提示會很難定位代碼。通過 SourceMap 能快速定位到源代碼,並進行調試。
比如我們沒有開啓sourcemap,然後開發過程中報錯了,它的報錯信息是這樣的:
定位過去是打包後的內容,這樣的話對我們排查報錯非常不方便。
當我們開啓sourcemap,再來看看這個同樣的報錯是怎樣的:
// webpack.config.js
module.exports = {
// ...
devtool: 'eval-cheap-module-source-map',
}
此時的報錯指向就非常清晰了~
關鍵字
devtool的值有20多種,並且都是由以下七種關鍵字的一個或多個組成
eval關鍵字
當 devtool 值包含 eval 時,生成的模塊代碼會被包裹進一段 eval 函數中,且模塊的 Sourcemap 信息通過 //# sourceURL 直接掛載在模塊代碼內
source-map關鍵字
當 devtool 包含 source-map 時,Webpack 才會生成 Sourcemap 內容
cheap關鍵字
當 devtool 包含 cheap 時,生成的 Sourcemap 內容會拋棄列維度的信息,這就意味着瀏覽器只能映射到代碼行維度
module關鍵字
module 關鍵字只在 cheap 場景下生效,例如 cheap-module-source-map、eval-cheap-module-source-map。當 devtool 包含 cheap 時,Webpack 根據 module 關鍵字判斷按 loader 聯調處理結果作為 source,還是按處理之前的代碼作為 source
nosources關鍵字
當 devtool 包含 nosources 時,生成的 Sourcemap 內容中不包含源碼內容 —— 即 sourcesContent 字段
inline關鍵字
當 devtool 包含 inline 時,Webpack 會將 Sourcemap 內容編碼為 Base64 DataURL,直接追加到產物文件中
hidden關鍵字
通常,產物中必須攜帶 //# sourceMappingURL= 指令,瀏覽器才能正確找到 Sourcemap 文件,當 devtool 包含 hidden 時,編譯產物中不包含 //# sourceMappingURL= 指令
devtool的值以及各自的功能可以在webpack文檔上查看
如何選擇
-
對於開發環境,適合使用:
eval:速度極快,但只能看到原始文件結構,看不到打包前的代碼內容;cheap-eval-source-map:速度比較快,可以看到打包前的代碼內容,但看不到 loader 處理之前的源碼;cheap-module-eval-source-map:速度比較快,可以看到 loader 處理之前的源碼,不過定位不到列級別;eval-source-map:初次編譯較慢,但定位精度最高;
-
對於生產環境,則適合使用:
source-map:信息最完整,但安全性最低,外部用户可輕易獲取到壓縮、混淆之前的源碼,慎重使用;hidden-source-map:信息較完整,安全性較低,外部用户獲取到.map文件地址時依然可以拿到源碼,慎重使用;nosources-source-map:源碼信息缺失,但安全性較高,需要配合 Sentry 等工具實現完整的 Sourcemap 映射。
解決跨域
在開發過程中,我們勢必會遇到跨域問題,對於本地開發我們一般可以通過配置代理來解決
我們先來簡單寫一個接口:
const express = require('express')
const app = express()
app.get('/api/getInfo', (req, res) => {
res.json({
code: 200,
data: {
name: 'nanjiu',
age: 18
}
})
})
app.listen(3000, () => {
console.log('服務已啓動~')
})
然後把服務跑起來,再到vue項目中去調用
const getInfo = async () => {
try {
const res = await axios.get('http://localhost:3000/api/getInfo')
console.log(res)
} catch(err) {
console.log(err)
}
}
這時候你會發現接口調用跨域了
配置代理
接着我們再來通過webpack配置代理解決跨域問題,由於我們本地使用了webpack-dev-server,所以我們可以直接通過它來配置
// webpack.config.js
module.exports = {
// ...
devServer: {
hot: true,
open: true,
proxy: {
'/api': 'http://localhost:3000'
}
}
}
這個時候我們的接口請求就正常了
由於篇幅問題,這篇文章就介紹到這裏了,後面會接着更新webpack更多高級用法。
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新文章~