熟悉 webpack 與 webpack4 配置笔横。
webpack4 相對于 3 的最主要的區(qū)別是所謂的零配置
希停,但是為了滿足我們的項目需求還是要自己進行配置克婶,不過我們可以使用一些 webpack 的預(yù)設(shè)值占键。同時 webpack 也拆成了兩部分提茁,webpack 和 webpack-cli,都需要本地安裝殿如。
我們通過實現(xiàn)一個 vue 的開發(fā)模板(vue init webpack 模板贡珊,其實跟 vue 關(guān)系不太大)來進行一次體驗最爬。在配置過程中會盡量使用 webpack4 的相關(guān)內(nèi)容。
本文不做 webpack 配置的完整介紹门岔,著重介紹配置過程中需要注意的地方爱致。查看代碼注釋閱讀效果更佳,完整配置與詳細(xì)注釋可見源代碼寒随。配置位于 build 文件夾下糠悯。
與版本 4 相關(guān)的章節(jié)會添加符號 ④。
需要注意的一點是妻往,我們的 webpack 代碼是運行在node環(huán)境下的互艾,這部分代碼可以使用 node api,但是我們的業(yè)務(wù)代碼(src下)是無法使用 node api 的蒲讯。
基本公用配置
由于 webpack 配置中的如 context忘朝,entry(chunk入口),output(輸出)和 module.rules 中 loaders 的配置在開發(fā)模式和生產(chǎn)模式基本都是公用的判帮,所以我們提取到 webpack.base.js
文件內(nèi)局嘁,供復(fù)用。其中 output 部分如下:
output: {
path: path.resolve(__dirname, '../dist/'), // 資源文件輸出時寫入的路徑
filename: 'static/js/[name].[chunkhash].js', // 使用 chunkhash 加入文件名做文件更新和緩存處理
chunkFilename: 'static/js/[name].[chunkhash].js'
}
需要注意的有:
文件名 hash
hash 是用在文件輸出的名字中的晦墙,如 [name].[hash].js
悦昵,總的來說,webpack 提供了三種 hash:
-
[hash]
:此次打包的所有內(nèi)容的 hash晌畅。 -
[chunkhash]
:每一個 chunk 都根據(jù)自身的內(nèi)容計算而來但指。 -
[contenthash]
:由 css 提取插件提供,根據(jù)自身內(nèi)容計算得來抗楔。
三種 hash 的使用棋凳,我們在優(yōu)化部分再講,先優(yōu)先使用 [chunkhash]
连躏。
loader 優(yōu)先級
loader 優(yōu)先級需要注意兩點剩岳,
-
同 test 配置內(nèi)優(yōu)先級:在同一個 test 下配置多個 loader ,優(yōu)先處理的 loader 放在配置數(shù)組的后面入热,如對 less 處理拍棕,則:
{ test: /\.less$/, use: [ 'style-loader', 'css-loader', 'postcss-loader', 'less-loader' ] }
-
不同 test 內(nèi)優(yōu)先級:如對 js 文件的處理需要兩個 test 分別配置,使用
eslint-loader
和babel-loader
勺良,但是又不能配置在一個配置對象內(nèi)绰播,可使用 enforce: 'pre' 強調(diào)優(yōu)先級,由eslint-loader
優(yōu)先處理尚困。{ test: /\.(js|vue)$/, loader: 'eslint-loader', enforce: 'pre', }, { test: /\.js$/, loader: 'babel-loader' }
css 預(yù)處理器配置
我們以 less 文件的 loader 配置 ['vue-style-loader', 'css-loader', 'postcss-loader', 'less-loader']
蠢箩,使用 @import url(demo.less)
為例:
- less-loader 先處理 less 語法
- postcss-loader 進行前綴添加等其他處理
- css-loader 將內(nèi)容引入 @import 所在的 css 文件內(nèi)
- vue-style-loader 將生成 style 標(biāo)簽,將 css 內(nèi)容插入 HTML
vue-style-loader 功能類似 style-loader
但是由于 vue 中的單文件組件,又分為兩種情況:
-
.vue 文件內(nèi)的 style:
vue-loader
會對 .vue 單文件組件進行處理谬泌,對 .vue 單文件組件內(nèi)的各種 lang="type" 我們可以在vue-loader
的 options 配置不同的 loader示弓,由于vue-loader
內(nèi)置了postcss
對 css 進行處理,所以此處我們不需要再配置postcss-loader
{ test: /\.vue$/, loader: 'vue-loader', options: { loaders: { less: ['// xxx-loaders'], scss: ['// xxx-loaders'], } } }
js 直接引入中引入樣式文件:
如 main.js 中import 'demo.less'
呵萨,這種方式引入的樣式文件,在vue-loader
處理范圍置之外跨跨,所以仍然需要配置postcss-loader
潮峦。
由于這種差異我們將 對 css 預(yù)處理器文件的配置封裝為函數(shù),由 usePostCss
參數(shù)生成對應(yīng)配置勇婴,將文件放入 utils.js
文件內(nèi)忱嘹,將 vue-loader
配置放在 vue-loader.js
文件內(nèi)。
也就是對 css 預(yù)處理器的配置我們需要在 vue-loader
內(nèi)和 webpack
內(nèi)配置兩遍耕渴。
寫這篇 README.md 期間 vue-loader 發(fā)布了 v15 版拘悦,需要配合插件使用,不用再進行兩遍配置
postcss-loader
postcss-loader 是一個強大的 css 處理工具橱脸,我們將 postcss 的配置拆分出去础米,新建 postcss.config.js
配置文件
module.exports = {
plugins: {
// 處理 @import
'postcss-import': {},
// 處理 css 中 url
'postcss-url': {},
// 自動前綴
'autoprefixer': {
"browsers": [
"> 1%",
"last 2 versions"
]
}
}
}
除了注釋中列出的需要的功能插件,我們還可能會用到 nextcss
(新的css語法的處理)添诉,px2rem/px-to-viewport
移動端適配相關(guān)的插件屁桑。
babel-loader
我們使用 babel 編譯瀏覽器不能識別的 js、類 js 語法栏赴,如轉(zhuǎn)義 ES6+蘑斧、JSX等。同樣將 babel-loader 的配置拆分出去须眷,需要創(chuàng)建 .babelrc
并配置:
{
"presets": [
[
/* *
* babel-preset-env
* 可以根據(jù)配置的目標(biāo)運行環(huán)境自動啟用需要的 babel 插件竖瘾。
*/
"env", {
"modules": false, // 關(guān)閉 babel 對 es module 的處理
"targets": { // 目標(biāo)運行環(huán)境
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
]
],
"plugins": [
"syntax-dynamic-import" // 異步加載語法編譯插件
]
}
媒體資源 loader
我們還需要對圖片、視頻花颗、字體等文件進行 loader 配置捕传,以字體文件為例子,主要用到的是 url-loader:
{
/**
* 末尾 \?.* 匹配帶 ? 資源路徑
* 我們引入的第三方 css 字體樣式對字體的引用路徑中可能帶查詢字符串的版本信息
*/
test: /\.(woff2|woff|eot|ttf|otf)(\?.*)?$/,
/**
* url-loader
* 會配合 webpack 對資源引入路徑進行復(fù)寫捎稚,如將 css 提取成獨立文件乐横,可能出現(xiàn) 404 錯誤可查看 提取 js 中的 css 部分解決
* 會以 webpack 的輸出路徑為基本路徑,以 name 配置進行具體輸出
* limit 單位為 byte今野,小于這個大小的文件會編譯為 base64 寫進 js 或 html
*/
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]',
}
}
靜態(tài)文件拷貝
直接引用(絕對路徑)和代碼執(zhí)行時確定的資源路徑應(yīng)該是以靜態(tài)文件存在的葡公,這些資源文件不會經(jīng)過 webpack 編譯處理,所以我們將它們放在獨立的文件夾(如 static)中条霜,并在代碼打包后拷貝到我們的輸出目錄催什,我們使用 copy-webpack-plugin 自動完成這個工作:
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 在開發(fā)模式下,會將文件寫入內(nèi)存
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: 'static',
ignore: ['.*']
}
])
此插件在拷貝文件過多時會崩潰宰睡,不知道解決了沒有蒲凶。
生產(chǎn)模式 production
我們先進行生產(chǎn)模式的配置气筋。
添加 script 腳本命令
在 package.json 下添加
"scripts": {
"build": "node build/build.js"`
}
那么使用 npm run build
命令就可執(zhí)行 node build/build.js
队贱,我們不直接使用 webpack webpack.prod.config.js
命令去執(zhí)行配置文件流强,而是在 build.js 中破衔,做一些文件刪除的處理幌羞,再啟動 webpack索守。
創(chuàng)建 build.js 邏輯
主要是兩個工作捏题,引入 rimraf
模塊刪除 webpack 下之前產(chǎn)生的指定文件官觅,啟動 webpack摇展,并在不同階段給出不同的提示信息刻肄。
// 在第一行設(shè)置當(dāng)前為 生產(chǎn)環(huán)境
process.env.NODE_ENV = 'production'
const webpack = require('webpack')
const rm = require('rimraf')
const webpackConfig = require('./webpack.prod')
// 刪除 webpack 輸出目錄下的內(nèi)容瓤球,也可只刪除子文件如 static 等
rm(webpackConfig.output.path, err => {
// webpack 按照生產(chǎn)模式配置啟動
webpack(webpackConfig, (err, stats) => {
// 輸出一些狀態(tài)信息
})
}
更多細(xì)節(jié)見源代碼注釋。
生產(chǎn)模式配置文件
新建 webpack.prod.js
文件敏弃,使用
const merge = require('webpack-merge') // 專用合并 webpack 配置的包
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
// 生產(chǎn)模式配置
})
合并基本配置和生產(chǎn)模式獨有配置卦羡,然后我們開始進行生產(chǎn)模式下的 webpack 的配置信息的填寫。
④ mode 預(yù)設(shè)
這是 webpack4 的新 api 麦到,有三個預(yù)設(shè)值:development
绿饵,production
,none
隅要,我們在生產(chǎn)模式選用mode: 'production'
蝴罪,webpack4在此配置下默認(rèn)啟用了:
-
插件
- FlagDependencyUsagePlugin:應(yīng)該是刪除無用代碼的,其他插件依賴
- FlagIncludedChunksPlugin:應(yīng)該是刪除無用代碼的步清,其他插件依賴
- ModuleConcatenationPlugin:作用域提升 webpack3的scope hosting
- NoEmitOnErrorsPlugin:遇到錯誤代碼不跳出
- OccurrenceOrderPlugin
- SideEffectsFlagPlugin
- UglifyJsPlugin:js代碼壓縮
- process.env.NODE_ENV 的值設(shè)為 production
所以這些默認(rèn)啟用的內(nèi)容我們不需要再配置要门。
最后一點設(shè)置 process.env.NODE_ENV 的值設(shè)為 production
其實是使用 DefinePlugin 插件:
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
})
從而我們可以在業(yè)務(wù)代碼中通過 process.env.NODE_ENV
,如進行判斷廓啊,使用開發(fā)接口還是線上接口欢搜。如果我們需要在 webpack 中判斷當(dāng)前環(huán)境,還需要單獨的設(shè)置 process.env.NODE_ENV = 'production'
谴轮,這也是我們在 build.js
中第一行做的事情炒瘟。
添加 webpack 打出的 bundles 到 HTML 文件
- 我們使用 webpack 配置入口時只能配置 js 文件作為入口,webpack 打出的 bundles 并不能自動與我們項目的 HTML 文件發(fā)生關(guān)聯(lián)第步。
- 需要我們手動添加
<script src="./bundles.js"></script>
(還可能包括后面提取出來的 css 文件)到 HTML 文件疮装。 - 我們可以使用 html-webpack-plugin 插件自動完成這個工作。
- 當(dāng)僅使用 webpack 對 js 進行打包粘都,而沒有 HTML文件需求時廓推,不需要這一步。
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/index.html'),// 文件寫入路徑
template: path.join(__dirname, '../src/index.html'),// 模板文件路徑
inject: true // js 等 bundles 插入 html 的位置 head/body等
})
]
如果不對 HtmlWebpackPlugin 進行配置翩隧,則其會創(chuàng)建一個 HTML 文件樊展,其中 filename
在開發(fā)模式下還是比較重要的。
④ 提取 js 中的 css 部分到單獨的文件
使用過 webpack3 的同學(xué)應(yīng)該對 extract-text-webpack-plugin 插件(以舊插件代稱)比較熟悉,為了嘗試webpack4专缠,我并不想使用這個插件的 @next
版本雷酪,所以選擇了新的替代插件 mini-css-extract-plugin(以新插件代稱)。
與舊插件相同涝婉,同樣需要在 webpack 的 loader 部分和 plugin 部分都進行配置哥力,不同的是新插件提供了單獨的 loader,在 loader 部分與舊插件的配置方式不太相同墩弯。配置如下:
-
loader 部分
const MiniCssExtractPlugin = require("mini-css-extract-plugin") // [ { loader: MiniCssExtractPlugin.loader, options: { // (segmentfault 這兒的多行注釋渲染有點問題 ??省骂,改成單行注釋形式) // 復(fù)寫 css 文件中資源路徑 // webpack3.x 配置在 extract-text-webpack-plugin 插件中 // 因為 css 文件中的外鏈?zhǔn)窍鄬εc css 的, // 我們抽離的 css 文件在可能會單獨放在 css 文件夾內(nèi) // 引用其他如 img/a.png 會尋址錯誤 // 這種情況下所以單獨需要配置 publicPath最住,復(fù)寫其中資源的路徑 // publicPath: '../' } }, { loader: 'css-loader', options: {} }, { loader: 'less-loader', options: {} } ]
-
plugin 部分
new MiniCssExtractPlugin({ // 輸出到單獨的 css 文件夾下 filename: "static/css/[name].[chunkhash].css" })
可以看到這個 loader 也配置在了 css 預(yù)處理器部分,在前面我們已經(jīng)把 css 預(yù)處理器的配置提取到了 utils.js 文件的函數(shù)內(nèi)怠惶,所以這里也是涨缚,我們使用 extract
參數(shù)決定是否需要提取。
回憶一下策治,之前使用的 style-loader
或 vue-style-loader
的作用脓魏,它們會創(chuàng)建標(biāo)簽將 css 的內(nèi)容直接插入到 HTML中。而提取成獨立的 css 文件之后通惫,插入到 HTML 的工作由 html-webpack-plugin
插件完成茂翔,兩者職責(zé)的這部分職責(zé)是重復(fù)的,所以我們需要使用 extract
參數(shù)做類似如下處理:
if (options.extract) {
return [MiniCssExtractPlugin.loader, ...otherLoaders]
} else {
return ['vue-style-loader', ...otherLoaders]
}
④ 拆分 js 代碼
這是 webpack 配置中很重要的一個環(huán)節(jié)履腋,影響到我們使用瀏覽器緩存的合理性珊燎,影響頁面資源的加載速度,將 js 進行合理拆分遵湖,可以有效減小我們每次更新代碼影響到的文件范圍悔政。
使用過 webpack3 的同學(xué)一定清楚,我們一般會提取出這么幾個文件 manifest.js
(webpack 運行時延旧,即webpack解析其他bundle的代碼等)谋国、vendor.js
(node_modules內(nèi)的庫)、app.js(真正的項目業(yè)務(wù)代碼)迁沫。在 webpack3 中我們使用 webpack.optimize.CommonsChunkPlugin
插件進行提取芦瘾,webpack4 中我們可以直接使用 optimization 配置項進行配置(當(dāng)然仍可使用插件配置):
/**
* 優(yōu)化部分包括代碼拆分
* 且運行時(manifest)的代碼拆分提取為了獨立的 runtimeChunk 配置
*/
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// 提取 node_modules 中代碼
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
},
commons: {
// async 設(shè)置提取異步代碼中的公用代碼
chunks: "async"
name: 'commons-async',
/**
* minSize 默認(rèn)為 30000
* 想要使代碼拆分真的按照我們的設(shè)置來
* 需要減小 minSize
*/
minSize: 0,
// 至少為兩個 chunks 的公用代碼
minChunks: 2
}
}
},
/**
* 對應(yīng)原來的 minchunks: Infinity
* 提取 webpack 運行時代碼
* 直接置為 true 或設(shè)置 name
*/
runtimeChunk: {
name: 'manifest'
}
}
也可將不會變的開發(fā)依賴配置到單獨的entry中,如:
entry: {
app: 'index.js',
vendor2: ['vue', 'vue-router', 'axios']
}
開發(fā)模式 development
開發(fā)模式與生產(chǎn)模式的不同是集畅,在開發(fā)時會頻繁運行代碼近弟,所以很多東西在開發(fā)模式是不推薦配置的,如css文件提取牡整,代碼壓縮等藐吮。所以針對一些寫入公共配置文件,但是開發(fā)模式不需要的功能,我們需要做類似修改:process.env.NODE_ENV === 'production' ? true : false
谣辞,如 css 預(yù)處理中是否需要配置提取 loader MiniCssExtractPlugin.loader
迫摔。此外還有一些是只配置在生產(chǎn)模式下的,如 MiniCssExtractPlugin
和 js 代碼拆分優(yōu)化泥从。
開發(fā)模式我們需要一個開發(fā)服務(wù)句占,幫我們完成實時更新、接口代理等功能躯嫉。我們使用 webpack-dev-server
纱烘。需要 npm 安裝。
添加 script 腳本命令
同樣祈餐,在 package.json 下添加
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js"
}
使用 --config
指定配置文件擂啥,由于命令直接調(diào)用 webpack-dev-server 運行,所以我們直接寫配置就好帆阳,可以不像生產(chǎn)模式一樣去編寫調(diào)用邏輯哺壶。
開發(fā)模式配置文件
新建 webpack.dev.js
文件,同樣使用:
// 在第一行設(shè)置當(dāng)前環(huán)境為開發(fā)環(huán)境
process.env.NODE_ENV = 'development'
const merge = require('webpack-merge') // 專用合并webpack配置的包
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
// 開發(fā)模式配置
})
④ mode 預(yù)設(shè)
同樣蜒谤,在開發(fā)模式下我們可以將 mode
配置為 development
山宾,同樣默認(rèn)啟用了一些功能:
-
插件
- NamedChunksPlugin:使用 entry 名做 chunk 標(biāo)識
- NamedModulesPlugin:使用模塊的相對路徑非自增 id 做模塊標(biāo)識
process.env.NODE_ENV 的值設(shè)為 development
開發(fā)服務(wù)配置 devServer
devServer: {
clientLogLevel: 'warning',
inline: true,
// 啟動熱更新
hot: true,
// 在頁面上全屏輸出報錯信息
overlay: {
warnings: true,
errors: true
},
// 顯示 webpack 構(gòu)建進度
progress: true,
// dev-server 服務(wù)路徑
contentBase: false,
compress: true,
host: 'localhost',
port: '8080',
// 自動打開瀏覽器
open: true,
// 可以進行接口代理配置
proxy: xxx,
// 跟 friendly-errors-webpack-plugin 插件配合
quiet: true,
publicPath: '/'
}
其他插件
devServer 使用熱更新 hot 時需要使用插件:
plugins: [
new webpack.HotModuleReplacementPlugin()
]
優(yōu)化 webpack 輸出信息,需要配置:
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
plugins: [
new FriendlyErrorsPlugin()
]
注意事項
-
熱更新:在使用熱更新時鳍徽,我們的 chunk 名中不能使用
[hash]
做標(biāo)識资锰,文件名變化無法熱更新,所以需要將原來配置在公共配置中的 output 中的文件名配置分別寫入生產(chǎn)和開發(fā)模式配置中阶祭,開發(fā)模式去掉[hash]
filename: 'static/[name].js', chunkFilename: 'static/[id].js'
-
HtmlWebpackPlugin:在生產(chǎn)模式下绷杜,我們將 html 文件寫入到 dist 下,但是在開發(fā)模式下濒募,并沒有實際的寫入過程接剩,且
devServer
啟動后的服務(wù)內(nèi)容與contentBase
有關(guān),兩者需要一致萨咳,所以我們將HtmlWebpackPlugin
的配置也分為 生產(chǎn)和開發(fā)模式懊缺,開發(fā)模式下使用:new HtmlWebpackPlugin({ filename: 'index.html', // 文件寫入路徑,前面的路徑與 devServer 中 contentBase 對應(yīng) template: path.resolve(__dirname, '../src/index.html'),// 模板文件路徑 inject: true })
優(yōu)化
配置提取
- 開發(fā)模式和生產(chǎn)模式的一些功能啟用培他,如 css 是否提取鹃两。
- 路徑配置,如文件輸出路徑和文件名舀凛、output 中的 publicPath(代碼 output 中只配置了 path俊扳,沒配置 publicPath,將這部分路徑的 static 寫到了各個資源的輸出name中猛遍,可參考Webpack中publicPath詳解)馋记、服務(wù)配置如端口等号坡。
我們可以提取到獨立的 config 文件中(本代碼沒做)。
拆分 js 代碼
在生產(chǎn)模式的 拆分 js 代碼
部分我們已經(jīng)講了如何拆分梯醒,那么為了更好的分析我們的拆分是否合理宽堆,我們可以配置一個 bundle 組成分析的插件。
const BundleAnalyzer = require('webpack-bundle-analyzer')
plugins: [
new BundleAnalyzer.BundleAnalyzerPlugin()
]
hash 固化
我們使用文件名中的 hash 變化來進行資源文件的更新茸习,那么合理利用緩存時畜隶,就要求我們合理的拆分文件,在內(nèi)容更新時最小限度的影響文件名中的 hash号胚。這里就用到了[hash]
籽慢,[chunkhash]
,[contenthash]
猫胁。然而 webpack 對 hash 的默認(rèn)處理并不盡如人意箱亿,這一部分的優(yōu)化可以參考基于 webpack 的持久化緩存方案
多頁面
多頁面配置代碼位于 muilt-pages 分支。我們只需做少量修改弃秆,以目前有 entry 頁和 index 頁為例极景。
entry 改動
將兩個頁面的 js 入口都配置在 webpack
的 entry
中:
entry: {
/**
* 入口,chunkname: 路徑
* 多入口可配置多個
*/
main: './src/main.js',
entry: './src/entry.js'
}
也可以自己設(shè)置項目結(jié)構(gòu)驾茴,使用 node api 動態(tài)讀取的方式獲取目前的多頁面入口。
HtmlWebpackPlugin 改動
需按照頁面?zhèn)€數(shù)配置多個 HtmlWebpackPlugin
:
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/main.html'),// 文件寫入路徑
template: path.join(__dirname, '../src/index.html'),// 模板文件路徑
inject: true, // 插入位置
chunks: ['manifest', 'vendors', 'common', 'main']
}),
new HtmlWebpackPlugin({
filename: path.join(__dirname, '../dist/entry.html'),// 文件寫入路徑
template: path.join(__dirname, '../src/index.html'),// 模板文件路徑
inject: true, // 插入位置
chunks: ['manifest', 'vendors', 'common', 'entry']
}),
其中需手動指定每個頁面的插入的 chunks(同步的)氢卡,否則會將其他頁面的文件也一同插入當(dāng)前頁面锈至。
④ 公共js提取
在單頁面下,一般不存在提取非異步 js 文件的公共代碼(非 node_modules)的問題译秦,在多頁面下我們的頁面間可能會公用 api峡捡、配置等文件,此時可以增加:
'common': {
// initial 設(shè)置提取同步代碼中的公用代碼
chunks: 'initial',
// test: 'xxxx', 也可使用 test 選擇提取哪些 chunks 里的代碼
name: 'common',
minSize: 0,
minChunks: 2
}
提取同步代碼中的公用代碼