1. 入門(一起來(lái)用這些小例子讓你熟悉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說(shuō)明已經(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ì)僅僅如此,下面讓我們通過(guò)實(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打包出來(lái)的js文件我們需要引入到html中,但是每次我們都手動(dòng)修改js文件名顯得很麻煩丐谋,因此我們需要一個(gè)插件來(lái)幫我們完成這件事情
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í)例來(lái)解決這個(gè)問(wèn)題
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來(lái)幫我們?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來(lái)解析我們的css文件
npm i -D style-loader css-loader
如果我們使用less來(lái)構(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'] // 從右向左解析原則
]
}
}
接下來(lái)师枣,我們還需要引入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通過(guò)style標(biāo)簽的方式添加到了html文件中,但是如果樣式文件很多找岖,全部添加到html中陨倡,難免顯得混亂。這時(shí)候我們想用把css拆分出來(lái)用外鏈的形式引入css文件怎么做呢许布?這時(shí)候我們就需要借助插件來(lái)幫助我們
1.4.2 拆分css
npm i -D mini-css-extract-plugin
webpack 4.0以前兴革,我們通過(guò)
extract-text-webpack-plugin
插件,把css樣式從js文件中提取到單獨(dú)的css文件中蜜唾。webpack4.0以后杂曲,官方推薦使用mini-css-extract-plugin
插件來(lái)打包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
這里需要說(shuō)的細(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-loader
8.x 對(duì)應(yīng)babel-core
7.x -
babel-loader
7.x 對(duì)應(yīng)babel-core
6.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語(yǔ)法轉(zhuǎn)換為ES5語(yǔ)法,但是對(duì)新api并不會(huì)轉(zhuǎn)換 例如(promise恶阴、Generator诈胜、Set、Maps存淫、Proxy等)
此時(shí)我們需要借助babel-polyfill來(lái)幫助我們轉(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)配置完畢籍茧,接下來(lái)讓我們測(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è)谠瓉?lái)webpack.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ì)我們來(lái)說(shuō)非常有實(shí)際意義汹族,這實(shí)際關(guān)系到你打包出來(lái)文件的大小籍铁,打包的速度等江兢。
具體優(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ù)
如果沒(méi)有設(shè)置耗帕,webpack4
會(huì)將 mode
的默認(rèn)值設(shè)置為 production``production
模式下會(huì)進(jìn)行tree shaking
(去除無(wú)用代碼)和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è)庫(kù)是否有依賴其他的包产阱。但是我們對(duì)類似jquery
這類依賴庫(kù)婉称,一般會(huì)認(rèn)為不會(huì)引用其他的包(特殊除外,自行判斷)。增加noParse
屬性,告訴webpack
不必解析构蹬,以此增加打包速度王暗。 -
extensions
webpack
會(huì)根據(jù)extensions
定義的后綴查找文件(頻率較高的文件類型優(yōu)先寫在前面)
2.5.1.3 使用HappyPack開啟多進(jìn)程Loader轉(zhuǎn)換
在webpack構(gòu)建過(guò)程中,實(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ì)再去編譯第三方庫(kù)脊岳。以后只要我們不升級(jí)第三方包的時(shí)候,那么webpack
就不會(huì)對(duì)這些庫(kù)去打包垛玻,這樣可以快速的提高打包的速度割捅。
這里我們使用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"
接下來(lái)在我們的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>
這樣如果我們沒(méi)有更新第三方依賴包帚桩,就不必npm run dll
亿驾。直接執(zhí)行npm run dev npm run build
的時(shí)候會(huì)發(fā)現(xiàn)我們的打包速度明顯有所提升。因?yàn)槲覀円呀?jīng)通過(guò)dllPlugin
將第三方依賴包抽離出來(lái)了账嚎。
2.5.1.6 配置緩存
我們每次執(zhí)行構(gòu)建都會(huì)把所有的文件都重復(fù)編譯一遍莫瞬,這樣的重復(fù)工作是否可以被緩存下來(lái)呢,答案是可以的郭蕉,目前大部分
loader
都提供了cache
配置項(xiàng)疼邀。比如在babel-loader
中,可以通過(guò)設(shè)置cacheDirectory
來(lái)開啟緩存召锈,babel-loader?cacheDirectory=true
就會(huì)將每次的編譯結(jié)果寫進(jìn)硬盤文件(默認(rèn)是在項(xiàng)目根目錄下的node_modules/.cache/babel-loader
目錄內(nèi)旁振,當(dāng)然你也可以自定義)
<p>但如果loader
不支持緩存呢?我們也有方法,我們可以通過(guò)cache-loader
涨岁,它所做的事情很簡(jiǎn)單拐袜,就是babel-loader
開啟cache
后做的事情,將loader
的編譯結(jié)果寫入硬盤緩存梢薪。再次構(gòu)建會(huì)先比較一下蹬铺,如果文件較之前的沒(méi)有發(fā)生變化則會(huì)直接使用緩存。使用方法如官方 demo 所示秉撇,在一些性能開銷較大的 loader 之前添加此 loader即可</p>
npm i -D cache-loader
2.5.2 優(yōu)化打包文件體積
打包的速度我們是進(jìn)行了優(yōu)化甜攀,但是打包后的文件體積卻是十分大,造成了頁(yè)面加載緩慢畜疾,浪費(fèi)流量等赴邻,接下來(lá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
接下來(lái)在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"
接下來(lái)npm run analyz
瀏覽器會(huì)自動(dòng)打開文件依賴圖的網(wǎng)頁(yè)
2.5.2.3 externals
按照官方文檔的解釋啡捶,如果我們想引用一個(gè)庫(kù)姥敛,但是又不想讓
webpack
打包,并且又不影響我們?cè)诔绦蛑幸?CMD瞎暑、AMD
或者window/global
全局等方式進(jìn)行使用彤敛,那就可以通過(guò)配置Externals
与帆。這個(gè)功能主要是用在創(chuàng)建一個(gè)庫(kù)的時(shí)候用的,但是也可以在我們項(xiàng)目開發(fā)中充分使用Externals
的方式墨榄,我們將這些不需要打包的靜態(tài)資源從構(gòu)建邏輯中剔除出去玄糟,而使用CDN
的方式,去引用它們袄秩。
<p>有時(shí)我們希望我們通過(guò)script
引入的庫(kù)阵翎,如用CDN的方式引入的jquery
,我們?cè)谑褂脮r(shí)之剧,依舊用require
的方式來(lái)使用郭卫,但是卻不希望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
的主要作用是用來(lái)清除代碼中無(wú)用的部分。目前在webpack4
我們?cè)O(shè)置mode
為production
的時(shí)候已經(jīng)自動(dòng)開啟了tree-shaking
蟹肘。但是要想使其生效词疼,生成的代碼必須是ES6模塊。不能使用其它類型的模塊如CommonJS
之流帘腹。如果使用Babel
的話贰盗,這里有一個(gè)小問(wèn)題,因?yàn)?Babel
的預(yù)案(preset)默認(rèn)會(huì)將任何模塊類型都轉(zhuǎn)譯成CommonJS
類型竹椒。修正這個(gè)問(wèn)題也很簡(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)歷過(guò)上面兩個(gè)系列的洗禮胸完,到現(xiàn)在我們成為了一名合格的webpack配置工程師书释。但是光擰螺絲,自身的可替代性還是很高赊窥,下面我們將深入webpack的原理中去
3.手寫webpack系列
經(jīng)歷過(guò)上面兩個(gè)部分爆惧,我們已經(jīng)可以熟練的運(yùn)用相關(guān)的loader和plugin對(duì)我們的代碼進(jìn)行轉(zhuǎn)換、解析锨能。接下來(lái)我們自己手動(dòng)實(shí)現(xiàn)loader與plugin扯再,使其在平時(shí)的開發(fā)中獲得更多的樂(lè)趣。
3.1 手寫webpack loader
loader
從本質(zhì)上來(lái)說(shuō)其實(shí)就是一個(gè)node
模塊址遇。相當(dāng)于一臺(tái)榨汁機(jī)(loader)
將相關(guān)類型的文件代碼(code)
給它熄阻。根據(jù)我們?cè)O(shè)置的規(guī)則,經(jīng)過(guò)它的一系列加工后還給我們加工好的果汁(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
通俗的來(lái)說(shuō)哀九,假設(shè)我們有一個(gè)文件a.js
,我們對(duì)a.js
里面的1000行進(jìn)行一些操作處理,比如為所有的await
增加try catch
,以及其他操作,但是a.js
里面的代碼本質(zhì)上來(lái)說(shuō)就是一堆字符串搅幅。那我們?cè)趺崔k呢勾栗,那就是轉(zhuǎn)換為帶標(biāo)記信息的對(duì)象(抽象語(yǔ)法樹)我們方便進(jìn)行增刪改查。這個(gè)帶標(biāo)記的對(duì)象(抽象語(yǔ)法樹)就是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
通過(guò)該模塊對(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ī)通過(guò)Webpack
提供的API
改變輸出結(jié)果纳令。通俗來(lái)說(shuō):一盤美味的 鹽豆炒雞蛋 需要經(jīng)歷燒油 炒制 調(diào)味到最后的裝盤等過(guò)程,而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
只能一件事捏雌,比如說(shuō)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ái)訪問(wèn)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只是代表了一次新的編譯過(guò)程
- 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`
} // 通過(guò)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
由于篇幅過(guò)長(zhǎng),且原理深入較多在塔。鑒于本篇以快速上手應(yīng)用于實(shí)際開發(fā)的原則幻件,這里決定另起一篇新的文章去詳細(xì)剖析
webpack
原理以及實(shí)現(xiàn)一個(gè)demo
版本。待格式校準(zhǔn)后蛔溃,將會(huì)貼出文章鏈接在下方
4. webpack5.0的時(shí)代
無(wú)論是前端框架還是構(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 版本疟位。在之前作者也曾提過(guò)webpack5.0
旨在減少配置的復(fù)雜度,使其更容易上手(webpack4
的時(shí)候也說(shuō)了這句話)喘垂,以及一些性能上的提升
- 使用持久化緩存提高構(gòu)建性能甜刻;
- 使用更好的算法和默認(rèn)值改進(jìn)長(zhǎng)期緩存(long-term caching);
- 清理內(nèi)部結(jié)構(gòu)而不引入任何破壞性的變化正勒;
- 引入一些breaking changes得院,以便盡可能長(zhǎng)的使用v5版本。
目前來(lái)看章贞,維護(hù)者的更新很頻繁祥绞,相信用不了多久webpack5.0
將會(huì)擁抱大眾。感興趣的同學(xué)可以先安裝beta
版本嘗嘗鮮。不過(guò)在此之前建議大家先對(duì)webpack4
進(jìn)行一番掌握,這樣后面的路才會(huì)越來(lái)越好走蜕径。
原作者姓名:前端小布
原出處:segmentfault
(收藏)