1. 入門(一起來用這些小例子讓你熟悉webpack的配置)
1.1 初始化項(xiàng)目
新建一個(gè)目錄,初始化npm
npm init
webpack是運(yùn)行在node環(huán)境中的,我們需要安裝以下兩個(gè)npm包
npm i -D webpack webpack-cli
- npm i -D 為npm install --save-dev的縮寫
- npm i -S 為npm install --save的縮寫
新建一個(gè)文件夾src ,然后新建一個(gè)文件main.js,寫一點(diǎn)代碼測(cè)試一下
console.log('call me 老yuan')
配置package.json命令

執(zhí)行
npm run build
此時(shí)如果生成了一個(gè)dist文件夾,并且內(nèi)部含有main.js說明已經(jīng)打包成功了
1.2 開始我們自己的配置
上面一個(gè)簡(jiǎn)單的例子只是webpack自己默認(rèn)的配置,下面我們要實(shí)現(xiàn)更加豐富的自定義配置
新建一個(gè)build文件夾,里面新建一個(gè)webpack.config.js
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development', // 開發(fā)模式
entry: path.resolve(__dirname, '../src/main.js'),
// 入口文件
output: {
filename: 'output.js', // 打包后的文件名稱
path: path.resolve(__dirname, '../dist') // 打包后的目錄
}
}
更改我們的打包命令

執(zhí)行 npm run build
會(huì)發(fā)現(xiàn)生成了以下目錄(圖片)
其中dist文件夾中的main.js就是我們需要在瀏覽器中實(shí)際運(yùn)行的文件
當(dāng)然實(shí)際運(yùn)用中不會(huì)僅僅如此,下面讓我們通過實(shí)際案例帶你快速入手webpack
1.3 配置html模板
js文件打包好了,但是我們不可能每次在html文件中手動(dòng)引入打包好的js
這里可能有的朋友會(huì)認(rèn)為我們打包js文件名稱不是一直是固定的嘛(
output.js)?這樣每次就不用改動(dòng)引入文件名稱了呀?實(shí)際上我們?nèi)粘i_發(fā)中往往會(huì)這樣配置:
module.exports = { // 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名稱
path: path.resolve(__dirname, '../dist') // 打包后的目錄
}
}
這時(shí)候生成的dist目錄文件如下

為了緩存,你會(huì)發(fā)現(xiàn)打包好的js文件的名稱每次都不一樣。webpack打包出來的js文件我們需要引入到html中,但是每次我們都手動(dòng)修改js文件名顯得很麻煩,因此我們需要一個(gè)插件來幫我們完成這件事情
npm i -D html-webpack-plugin
新建一個(gè)build同級(jí)的文件夾public,里面新建一個(gè)index.html
具體配置文件如下
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = {
mode: 'development', // 開發(fā)模式
entry: path.resolve(__dirname, '../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名稱
path: path.resolve(__dirname, '../dist') // 打包后的目錄
},
plugins: [new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html')
})]
}
生成目錄如下(圖片)

可以發(fā)現(xiàn)打包生成的js文件已經(jīng)被自動(dòng)引入html文件中
1.3.1. 多入口文件如何開發(fā)
生成多個(gè)
html-webpack-plugin實(shí)例來解決這個(gè)問題
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development', // 開發(fā)模式
entry: {
main: path.resolve(__dirname, '../src/main.js'),
header: path.resolve(__dirname, '../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名稱
path: path.resolve(__dirname, '../dist') // 打包后的目錄
},
plugins: [new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
filename: 'index.html',
chunks: ['main'] // 與入口文件對(duì)應(yīng)的模塊名
}), new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/header.html'),
filename: 'header.html',
chunks: ['header'] // 與入口文件對(duì)應(yīng)的模塊名
}), ]
}
此時(shí)會(huì)發(fā)現(xiàn)生成以下目錄

1.3.2 clean-webpack-plugin
每次執(zhí)行npm run build 會(huì)發(fā)現(xiàn)dist文件夾里會(huì)殘留上次打包的文件,這里我們推薦一個(gè)plugin來幫我們?cè)诖虬敵銮扒蹇瘴募A
clean-webpack-plugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = { // ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
1.4 引用CSS
我們的入口文件是js,所以我們?cè)谌肟趈s中引入我們的css文件

同時(shí)我們也需要一些loader來解析我們的css文件
npm i -D style-loader css-loader
如果我們使用less來構(gòu)建樣式,則需要多安裝兩個(gè)
npm i -D less less-loader
配置文件如下
// webpack.config.js
module.exports = { // ...省略其他配置
module: {
rules: [{
test: /.css$/,
use: ['style-loader', 'css-loader'] // 從右向左解析原則
}, {
test: /.less$/,
use: ['style-loader', 'css-loader', 'less-loader'] // 從右向左解析原則
}]
}
}
瀏覽器打開html如下

1.4.1 為css添加瀏覽器前綴
npm i -D postcss-loader autoprefixer
配置如下
// webpack.config.js
module.exports = {
module: {
rules: [test / .less$ / , use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'] // 從右向左解析原則
]
}
}
接下來,我們還需要引入autoprefixer使其生效,這里有兩種方式
1,在項(xiàng)目根目錄下創(chuàng)建一個(gè)postcss.config.js文件,配置如下:
module.exports = { plugins: [require('autoprefixer')] // 引用該插件即可了}
2,直接在webpack.config.js里配置
// webpack.config.js
module.exports = { //...省略其他配置
module: {
rules: [{
test: /.less$/,
use: ['style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}, 'less-loader'] // 從右向左解析原則
}]
}
}
這時(shí)候我們發(fā)現(xiàn)css通過style標(biāo)簽的方式添加到了html文件中,但是如果樣式文件很多,全部添加到html中,難免顯得混亂。這時(shí)候我們想用把css拆分出來用外鏈的形式引入css文件怎么做呢?這時(shí)候我們就需要借助插件來幫助我們
1.4.2 拆分css
npm i -D mini-css-extract-plugin
webpack 4.0以前,我們通過
extract-text-webpack-plugin插件,把css樣式從js文件中提取到單獨(dú)的css文件中。webpack4.0以后,官方推薦使用mini-css-extract-plugin插件來打包c(diǎn)ss文件
配置文件如下
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { //...省略其他配置
module: {
rules: [{
test: /.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
}]
},
plugins: [new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})]
}
1.4.3 拆分多個(gè)css
這里需要說的細(xì)一點(diǎn),上面我們所用到的
mini-css-extract-plugin會(huì)將所有的css樣式合并為一個(gè)css文件。如果你想拆分為一一對(duì)應(yīng)的多個(gè)css文件,我們需要使用到extract-text-webpack-plugin,而目前mini-css-extract-plugin還不支持此功能。我們需要安裝@next版本的extract-text-webpack-plugin
npm i - D extract - text - webpack - plugin @next // webpack.config.js
const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module: {
rules: [{
test: /.css$/,
use: indexCss.extract({
use: ['css-loader']
})
}, {
test: /.less$/,
use: indexLess.extract({
use: ['css-loader', 'less-loader']
})
}]
},
plugins: [indexLess, indexCss]
}
1.5 打包 圖片、字體、媒體、等文件
file-loader就是將文件在進(jìn)行一些處理后(主要是處理文件名和路徑、解析文件url),并將文件移動(dòng)到輸出的目錄中url-loader 一般與file-loader搭配使用,功能與 file-loader 類似,如果文件小于限制的大小。則會(huì)返回 base64 編碼,否則使用 file-loader 將文件移動(dòng)到輸出的目錄中
// webpack.config.js
module.exports = { // 省略其它配置 ...
module: {
rules: [ // ...
{
test: /.(jpe?g|png|gif)$/i, //圖片文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}]
}, {
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/, //媒體文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}]
}, {
test: /.(woff2?|eot|ttf|otf)(?.*)?$/i, // 字體
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}]
},
]
}
}
1.6 用babel轉(zhuǎn)義js文件
為了使我們的js代碼兼容更多的環(huán)境我們需要安裝依賴
npm i babel-loader @babel/preset-env @babel/core
- 注意
babel-loader與babel-core的版本對(duì)應(yīng)關(guān)系
-
babel-loader8.x 對(duì)應(yīng)babel-core7.x -
babel-loader7.x 對(duì)應(yīng)babel-core6.x
配置如下
// webpack.config.js
module.exports = { // 省略其它配置 ...
module: {
rules: [{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}, ]
}
}
上面的babel-loader只會(huì)將 ES6/7/8語法轉(zhuǎn)換為ES5語法,但是對(duì)新api并不會(huì)轉(zhuǎn)換 例如(promise、Generator、Set、Maps、Proxy等)
此時(shí)我們需要借助babel-polyfill來幫助我們轉(zhuǎn)換
npm i @babel/polyfill
// webpack.config.js
const path = require('path')module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件}
- 手動(dòng)把上面的demo敲一遍對(duì)閱讀下面的文章更有益,建議入門的同學(xué)敲三遍以上
上面的實(shí)踐是我們對(duì)webpack的功能有了一個(gè)初步的了解,但是要想熟練應(yīng)用于開發(fā)中,我們需要一個(gè)系統(tǒng)的實(shí)戰(zhàn)。讓我們一起擺脫腳手架嘗試自己搭建一個(gè)vue開發(fā)環(huán)境
2. 搭建vue開發(fā)環(huán)境
上面的小例子已經(jīng)幫助而我們實(shí)現(xiàn)了打包c(diǎn)ss、圖片、js、html等文件。
但是我們還需要以下幾種配置
2.1 解析.vue文件
npm i -D vue-loader vue-template-compiler vue-style-loadernpm i -S vue
vue-loader 用于解析.vue文件vue-template-compiler 用于編譯模板
配置如下
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [{
test: /.vue$/,
use: ['vue-loader']
}, ]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js',
' @': path.resolve(__dirname, '../src')
},
extensions: ['*', '.js', '.json', '.vue']
},
plugins: [new vueLoaderPlugin()]
}
2.2 配置webpack-dev-server進(jìn)行熱更新
npm i -D webpack-dev-server
配置如下
const Webpack = require('webpack')
module.exports = { // ...省略其他配置
devServer: {
port: 3000,
hot: true,
contentBase: '../dist'
},
plugins: [new Webpack.HotModuleReplacementPlugin()]
}
完整配置如下
// webpack.config.js
const path = require('path');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode: 'development', // 開發(fā)模式
entry: {
main: path.resolve(__dirname, '../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名稱
path: path.resolve(__dirname, '../dist') // 打包后的目錄
},
module: {
rules: [{
test: /.vue$/,
use: ['vue-loader']
}, {
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}, {
test: /.css$/,
use: ['vue-style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}]
}, {
test: /.less$/,
use: ['vue-style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}, 'less-loader']
}]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js',
' @': path.resolve(__dirname, '../src')
},
extensions: ['*', '.js', '.json', '.vue']
},
devServer: {
port: 3000,
hot: true,
contentBase: '../dist'
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
filename: 'index.html'
}), new vueLoaderPlugin(), new Webpack.HotModuleReplacementPlugin()]
}
2.3 配置打包命令

打包文件已經(jīng)配置完畢,接下來讓我們測(cè)試一下
首先在src新建一個(gè)main.js

新建一個(gè)App.vue

新建一個(gè)public文件夾,里面新建一個(gè)index.html

執(zhí)行npm run dev這時(shí)候如果瀏覽器出現(xiàn)Vue開發(fā)環(huán)境運(yùn)行成功,那么恭喜你,已經(jīng)成功邁出了第一步
2.4 區(qū)分開發(fā)環(huán)境與生產(chǎn)環(huán)境
實(shí)際應(yīng)用到項(xiàng)目中,我們需要區(qū)分開發(fā)環(huán)境與生產(chǎn)環(huán)境,我們?cè)谠瓉韜ebpack.config.js的基礎(chǔ)上再新增兩個(gè)文件
-
webpack.dev.js開發(fā)環(huán)境配置文件
開發(fā)環(huán)境主要實(shí)現(xiàn)的是熱更新,不要壓縮代碼,完整的sourceMap
-
webpack.prod.js生產(chǎn)環(huán)境配置文件
生產(chǎn)環(huán)境主要實(shí)現(xiàn)的是壓縮代碼、提取css文件、合理的sourceMap、分割代碼需要安裝以下模塊:npm i -D webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
-
webpack-merge合并配置 -
copy-webpack-plugin拷貝靜態(tài)資源 -
optimize-css-assets-webpack-plugin壓縮css -
uglifyjs-webpack-plugin壓縮js
webpack mode設(shè)置production的時(shí)候會(huì)自動(dòng)壓縮js代碼。原則上不需要引入uglifyjs-webpack-plugin進(jìn)行重復(fù)工作。但是optimize-css-assets-webpack-plugin壓縮css的同時(shí)會(huì)破壞原有的js壓縮,所以這里我們引入uglifyjs進(jìn)行壓縮
2.4.1 webpack.config.js
const path = require('path')
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry: {
main: path.resolve(__dirname, '../src/main.js')
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].[hash:8].js',
chunkFilename: 'js/[name].[hash:8].js'
},
module: {
rules: [{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}, {
test: /.vue$/,
use: ['cache-loader', 'thread-loader', {
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}]
}, {
test: /.css$/,
use: [{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options: {
publicPath: "../dist/css/",
hmr: devMode
}
}, 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}]
}, {
test: /.less$/,
use: [{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options: {
publicPath: "../dist/css/",
hmr: devMode
}
}, 'css-loader', 'less-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}]
}, {
test: /.(jep?g|png|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
}, {
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
}, {
test: /.(woff2?|eot|ttf|otf)(?.*)?$/i,
use: {
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
}]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js',
' @': path.resolve(__dirname, '../src')
},
extensions: ['*', '.js', '.json', '.vue']
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html')
}), new vueLoaderPlugin(), new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})]
}
2.4.2 webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge(webpackConfig, {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
port: 3000,
hot: true,
contentBase: '../dist'
},
plugins: [new Webpack.HotModuleReplacementPlugin()]
})
2.4.3 webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = WebpackMerge(webpackConfig, {
mode: 'production',
devtool: 'cheap-module-source-map',
plugins: [
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist')
}]),
],
optimization: {
minimizer: [new UglifyJsPlugin({ //壓縮js
cache: true,
parallel: true,
sourceMap: true
}), new OptimizeCssAssetsPlugin({})],
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
name: "chunk-libs",
test: /[/]node_modules[/]/,
priority: 10,
chunks: "initial" // 只打包初始時(shí)依賴的第三方
}
}
}
}
})
2.5 優(yōu)化webpack配置
看到這里你或許有些累了,但是要想獲取更好的offer,更高的薪水,下面必須繼續(xù)深入

優(yōu)化配置對(duì)我們來說非常有實(shí)際意義,這實(shí)際關(guān)系到你打包出來文件的大小,打包的速度等。
具體優(yōu)化可以分為以下幾點(diǎn):
2.5.1 優(yōu)化打包速度
構(gòu)建速度指的是我們每次修改代碼后熱更新的速度以及發(fā)布前打包文件的速度。
2.5.1.1 合理的配置mode參數(shù)與devtool參數(shù)
mode可設(shè)置development production兩個(gè)參數(shù)
如果沒有設(shè)置,webpack4 會(huì)將 mode 的默認(rèn)值設(shè)置為 production``production模式下會(huì)進(jìn)行tree shaking(去除無用代碼)和uglifyjs(代碼壓縮混淆)
2.5.1.2 縮小文件的搜索范圍(配置include exclude alias noParse extensions)
-
alias: 當(dāng)我們代碼中出現(xiàn)import 'vue'時(shí), webpack會(huì)采用向上遞歸搜索的方式去node_modules目錄下找。為了減少搜索范圍我們可以直接告訴webpack去哪個(gè)路徑下查找。也就是別名(alias)的配置。 -
include exclude同樣配置include exclude也可以減少webpack loader的搜索轉(zhuǎn)換時(shí)間。 -
noParse當(dāng)我們代碼中使用到import jq from 'jquery'時(shí),webpack會(huì)去解析jq這個(gè)庫是否有依賴其他的包。但是我們對(duì)類似jquery這類依賴庫,一般會(huì)認(rèn)為不會(huì)引用其他的包(特殊除外,自行判斷)。增加noParse屬性,告訴webpack不必解析,以此增加打包速度。 -
extensionswebpack會(huì)根據(jù)extensions定義的后綴查找文件(頻率較高的文件類型優(yōu)先寫在前面)

2.5.1.3 使用HappyPack開啟多進(jìn)程Loader轉(zhuǎn)換
在webpack構(gòu)建過程中,實(shí)際上耗費(fèi)時(shí)間大多數(shù)用在loader解析轉(zhuǎn)換以及代碼的壓縮中。日常開發(fā)中我們需要使用Loader對(duì)js,css,圖片,字體等文件做轉(zhuǎn)換操作,并且轉(zhuǎn)換的文件數(shù)據(jù)量也是非常大。由于js單線程的特性使得這些轉(zhuǎn)換操作不能并發(fā)處理文件,而是需要一個(gè)個(gè)文件進(jìn)行處理。HappyPack的基本原理是將這部分任務(wù)分解到多個(gè)子進(jìn)程中去并行處理,子進(jìn)程處理完成后把結(jié)果發(fā)送到主進(jìn)程中,從而減少總的構(gòu)建時(shí)間
npm i -D happypack

2.5.1.4 使用webpack-parallel-uglify-plugin 增強(qiáng)代碼壓縮
上面對(duì)于loader轉(zhuǎn)換已經(jīng)做優(yōu)化,那么下面還有另一個(gè)難點(diǎn)就是優(yōu)化代碼的壓縮時(shí)間。
npm i -D webpack-parallel-uglify-plugin

2.5.1.5 抽離第三方模塊
對(duì)于開發(fā)項(xiàng)目中不經(jīng)常會(huì)變更的靜態(tài)依賴文件。類似于我們的
elementUi、vue全家桶等等。因?yàn)楹苌贂?huì)變更,所以我們不希望這些依賴要被集成到每一次的構(gòu)建邏輯中去。 這樣做的好處是每次更改我本地代碼的文件的時(shí)候,webpack只需要打包我項(xiàng)目本身的文件代碼,而不會(huì)再去編譯第三方庫。以后只要我們不升級(jí)第三方包的時(shí)候,那么webpack就不會(huì)對(duì)這些庫去打包,這樣可以快速的提高打包的速度。
這里我們使用webpack內(nèi)置的DllPlugin DllReferencePlugin進(jìn)行抽離
在與webpack配置文件同級(jí)目錄下新建webpack.dll.config.js
代碼如下
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = { // 你想要打包的模塊的數(shù)組
entry: {
vendor: ['vue', 'element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件輸出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 這里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
在package.json中配置如下命令
"dll": "webpack --config build/webpack.dll.config.js"
接下來在我們的webpack.config.js中增加以下代碼
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷貝生成的文件到dist目錄 這樣每次不必手動(dòng)去cv {from: 'static', to:'static'} ]),
]
};
執(zhí)行
npm run dll
會(huì)發(fā)現(xiàn)生成了我們需要的集合第三地方
代碼的vendor.dll.js
我們需要在html文件中手動(dòng)引入這個(gè)js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
這樣如果我們沒有更新第三方依賴包,就不必npm run dll。直接執(zhí)行npm run dev npm run build的時(shí)候會(huì)發(fā)現(xiàn)我們的打包速度明顯有所提升。因?yàn)槲覀円呀?jīng)通過dllPlugin將第三方依賴包抽離出來了。
2.5.1.6 配置緩存
我們每次執(zhí)行構(gòu)建都會(huì)把所有的文件都重復(fù)編譯一遍,這樣的重復(fù)工作是否可以被緩存下來呢,答案是可以的,目前大部分
loader都提供了cache配置項(xiàng)。比如在babel-loader中,可以通過設(shè)置cacheDirectory來開啟緩存,babel-loader?cacheDirectory=true就會(huì)將每次的編譯結(jié)果寫進(jìn)硬盤文件(默認(rèn)是在項(xiàng)目根目錄下的node_modules/.cache/babel-loader目錄內(nèi),當(dāng)然你也可以自定義)
<p>但如果loader不支持緩存呢?我們也有方法,我們可以通過cache-loader,它所做的事情很簡(jiǎn)單,就是babel-loader開啟cache后做的事情,將loader的編譯結(jié)果寫入硬盤緩存。再次構(gòu)建會(huì)先比較一下,如果文件較之前的沒有發(fā)生變化則會(huì)直接使用緩存。使用方法如官方 demo 所示,在一些性能開銷較大的 loader 之前添加此 loader即可</p>
npm i -D cache-loader

2.5.2 優(yōu)化打包文件體積
打包的速度我們是進(jìn)行了優(yōu)化,但是打包后的文件體積卻是十分大,造成了頁面加載緩慢,浪費(fèi)流量等,接下來讓我們從文件體積上繼續(xù)優(yōu)化
2.5.2.1 引入webpack-bundle-analyzer分析打包后的文件
webpack-bundle-analyzer將打包后的內(nèi)容束展示為方便交互的直觀樹狀圖,讓我們知道我們所構(gòu)建包中真正引入的內(nèi)容
npm i -D webpack-bundle-analyzer

接下來在package.json里配置啟動(dòng)命令
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
windows請(qǐng)安裝npm i -D cross-env
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
接下來npm run analyz瀏覽器會(huì)自動(dòng)打開文件依賴圖的網(wǎng)頁
2.5.2.3 externals
按照官方文檔的解釋,如果我們想引用一個(gè)庫,但是又不想讓
webpack打包,并且又不影響我們?cè)诔绦蛑幸?CMD、AMD或者window/global全局等方式進(jìn)行使用,那就可以通過配置Externals。這個(gè)功能主要是用在創(chuàng)建一個(gè)庫的時(shí)候用的,但是也可以在我們項(xiàng)目開發(fā)中充分使用Externals的方式,我們將這些不需要打包的靜態(tài)資源從構(gòu)建邏輯中剔除出去,而使用CDN
的方式,去引用它們。
<p>有時(shí)我們希望我們通過script引入的庫,如用CDN的方式引入的jquery,我們?cè)谑褂脮r(shí),依舊用require的方式來使用,但是卻不希望webpack將它又編譯進(jìn)文件中。這里官網(wǎng)案例已經(jīng)足夠清晰明了,大家有興趣可以點(diǎn)擊了解 </p>
webpack
官網(wǎng)案例如下
<script src="https://code.jquery.com/jquery-3.1.0.js" integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk=" crossorigin="anonymous"></script>module.exports = {
//...
externals: { jquery: 'jQuery' }};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
2.5.2.3 Tree-shaking
這里單獨(dú)提一下
tree-shaking,是因?yàn)檫@里有個(gè)坑。tree-shaking的主要作用是用來清除代碼中無用的部分。目前在webpack4我們?cè)O(shè)置mode為production的時(shí)候已經(jīng)自動(dòng)開啟了tree-shaking。但是要想使其生效,生成的代碼必須是ES6模塊。不能使用其它類型的模塊如CommonJS之流。如果使用Babel的話,這里有一個(gè)小問題,因?yàn)?Babel的預(yù)案(preset)默認(rèn)會(huì)將任何模塊類型都轉(zhuǎn)譯成CommonJS類型。修正這個(gè)問題也很簡(jiǎn)單,在.babelrc文件或在webpack.config.js文件中設(shè)置modules: false就好了
// .babelrc{ "presets": [ ["@babel/preset-env", { "modules": false } ] ]}
或者
// webpack.config.js
module: {
rules: [{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', {
modules: false
}]
}
},
exclude: /(node_modules)/
}]
}
經(jīng)歷過上面兩個(gè)系列的洗禮,到現(xiàn)在我們成為了一名合格的webpack配置工程師。但是光擰螺絲,自身的可替代性還是很高,下面我們將深入webpack的原理中去

3.手寫webpack系列
經(jīng)歷過上面兩個(gè)部分,我們已經(jīng)可以熟練的運(yùn)用相關(guān)的loader和plugin對(duì)我們的代碼進(jìn)行轉(zhuǎn)換、解析。接下來我們自己手動(dòng)實(shí)現(xiàn)loader與plugin,使其在平時(shí)的開發(fā)中獲得更多的樂趣。
3.1 手寫webpack loader
loader從本質(zhì)上來說其實(shí)就是一個(gè)node模塊。相當(dāng)于一臺(tái)榨汁機(jī)(loader)將相關(guān)類型的文件代碼(code)給它。根據(jù)我們?cè)O(shè)置的規(guī)則,經(jīng)過它的一系列加工后還給我們加工好的果汁(code)。
loader編寫原則
- 單一原則: 每個(gè)
Loader只做一件事; - 鏈?zhǔn)秸{(diào)用:
Webpack會(huì)按順序鏈?zhǔn)秸{(diào)用每個(gè)Loader; - 統(tǒng)一原則: 遵循
Webpack制定的設(shè)計(jì)規(guī)則和結(jié)構(gòu),輸入與輸出均為字符串,各個(gè)Loader完全獨(dú)立,即插即用;
在日常開發(fā)環(huán)境中,為了方便調(diào)試我們往往會(huì)加入許多console打印。但是我們不希望在生產(chǎn)環(huán)境中存在打印的值。那么這里我們自己實(shí)現(xiàn)一個(gè)loader去除代碼中的console
知識(shí)點(diǎn)普及之
AST。AST通俗的來說,假設(shè)我們有一個(gè)文件a.js,我們對(duì)a.js里面的1000行進(jìn)行一些操作處理,比如為所有的await增加try catch,以及其他操作,但是a.js里面的代碼本質(zhì)上來說就是一堆字符串。那我們?cè)趺崔k呢,那就是轉(zhuǎn)換為帶標(biāo)記信息的對(duì)象(抽象語法樹)我們方便進(jìn)行增刪改查。這個(gè)帶標(biāo)記的對(duì)象(抽象語法樹)就是AST。這里推薦一篇不錯(cuò)的AST文章 AST快速入門
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
-
@babel/parser將源代碼解析成AST -
@babel/traverse對(duì)AST節(jié)點(diǎn)進(jìn)行遞歸遍歷,生成一個(gè)便于操作、轉(zhuǎn)換的path對(duì)象 -
@babel/generator將AST解碼生成js代碼 -
@babel/types通過該模塊對(duì)具體的AST節(jié)點(diǎn)進(jìn)行進(jìn)行增、刪、改、查
新建 drop-console.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports = function(source) {
const ast = parser.parse(source, {
sourceType: 'module'
}) traverse(ast, {
CallExpression(path) {
if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {
name: "console"
})) {
path.remove()
}
}
}) const output = generator(ast, {}, source);
return output.code
}
如何使用
const path = require('path')
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, 'index.js'),
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
test: /.js$/,
use: path.resolve(__dirname, 'drop-console.js')
}]
}
}
實(shí)際上在
webpack4中已經(jīng)集成了去除console功能,在minimizer中可配置 去除console
附上官網(wǎng) 如何編寫一個(gè)loader
3.2 手寫webpack plugin
在
Webpack運(yùn)行的生命周期中會(huì)廣播出許多事件,Plugin可以監(jiān)聽這些事件,在合適的時(shí)機(jī)通過Webpack提供的API改變輸出結(jié)果。通俗來說:一盤美味的 鹽豆炒雞蛋 需要經(jīng)歷燒油 炒制 調(diào)味到最后的裝盤等過程,而plugin相當(dāng)于可以監(jiān)控每個(gè)環(huán)節(jié)并進(jìn)行操作,比如可以寫一個(gè)少放胡椒粉plugin,監(jiān)控webpack暴露出的生命周期事件(調(diào)味),在調(diào)味的時(shí)候執(zhí)行少放胡椒粉操作。那么它與loader的區(qū)別是什么呢?上面我們也提到了loader的單一原則,loader只能一件事,比如說less-loader,只能解析less文件,plugin則是針對(duì)整個(gè)流程執(zhí)行廣泛的任務(wù)。
一個(gè)基本的plugin插件結(jié)構(gòu)如下
class firstPlugin {
constructor(options) {
console.log('firstPlugin options', options)
}
apply(compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')))
}
}
module.exports = firstPlugin
compiler 、compilation是什么?
-
compiler對(duì)象包含了Webpack環(huán)境所有的的配置信息。這個(gè)對(duì)象在啟動(dòng)webpack時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括options,loader和plugin。當(dāng)在webpack環(huán)境中應(yīng)用一個(gè)插件時(shí),插件將收到此compiler對(duì)象的引用??梢允褂盟鼇碓L問webpack的主環(huán)境。 -
compilation對(duì)象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng)運(yùn)行webpack開發(fā)環(huán)境中間件時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的compilation,從而生成一組新的編譯資源。compilation對(duì)象也提供了很多關(guān)鍵時(shí)機(jī)的回調(diào),以供插件做自定義處理時(shí)選擇使用。
compiler和 compilation的區(qū)別在于
- compiler代表了整個(gè)webpack從啟動(dòng)到關(guān)閉的生命周期,而compilation只是代表了一次新的編譯過程
- compiler和compilation暴露出許多鉤子,我們可以根據(jù)實(shí)際需求的場(chǎng)景進(jìn)行自定義處理
compiler鉤子文檔
compilation鉤子文檔
下面我們手動(dòng)開發(fā)一個(gè)簡(jiǎn)單的需求,在生成打包文件之前自動(dòng)生成一個(gè)關(guān)于打包出文件的大小信息
新建一個(gè)webpack-firstPlugin.js
class firstPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
let str = ''
for (let filename in compilation.assets) {
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}n`
} // 通過compilation.assets可以獲取打包后靜態(tài)資源信息,同樣也可以寫入資源
compilation.assets['fileSize.md'] = {
source: function() {
return str
},
size: function() {
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
如何使用
const path = require('path')const firstPlugin = require('webpack-firstPlugin.js')module.exports = { // 省略其他代碼 plugins:[ new firstPlugin() ]}
執(zhí)行 npm run build即可看到在dist文件夾中生成了一個(gè)包含打包文件信息的fileSize.md
上面兩個(gè)
loader與plugin案例只是一個(gè)引導(dǎo),實(shí)際開發(fā)需求中的loader與plugin要考慮的方面很多,建議大家自己多動(dòng)手嘗試一下。
附上官網(wǎng) 如何編寫一個(gè)plugin
3.3 手寫webpack
由于篇幅過長(zhǎng),且原理深入較多。鑒于本篇以快速上手應(yīng)用于實(shí)際開發(fā)的原則,這里決定另起一篇新的文章去詳細(xì)剖析
webpack原理以及實(shí)現(xiàn)一個(gè)demo版本。待格式校準(zhǔn)后,將會(huì)貼出文章鏈接在下方
4. webpack5.0的時(shí)代
無論是前端框架還是構(gòu)建工具的更新速度遠(yuǎn)遠(yuǎn)超乎了我們的想象,前幾年的jquery一把梭的時(shí)代一去不復(fù)返。我們要擁抱的是不斷更新迭代的vue、react、node、serverless、docker、k8s....

不甘落后的webpack也已經(jīng)在近日發(fā)布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提過webpack5.0旨在減少配置的復(fù)雜度,使其更容易上手(webpack4的時(shí)候也說了這句話),以及一些性能上的提升
- 使用持久化緩存提高構(gòu)建性能;
- 使用更好的算法和默認(rèn)值改進(jìn)長(zhǎng)期緩存(long-term caching);
- 清理內(nèi)部結(jié)構(gòu)而不引入任何破壞性的變化;
- 引入一些breaking changes,以便盡可能長(zhǎng)的使用v5版本。
目前來看,維護(hù)者的更新很頻繁,相信用不了多久webpack5.0將會(huì)擁抱大眾。感興趣的同學(xué)可以先安裝beta版本嘗嘗鮮。不過在此之前建議大家先對(duì)webpack4進(jìn)行一番掌握,這樣后面的路才會(huì)越來越好走。

原作者姓名:前端小布
原出處:segmentfault
(收藏)