前言
2020年即將到來,在衆多前端的招聘要求裏,webpack
、工程化
這些字眼頻率越來越高。日常開發者中,我們常常在用諸如vue-cli
、create-react-app
的腳手架來構建我們的項目。但是如果你想在團隊脫穎而出(鶴立雞羣)、拿到更好的offer(還房貸),那麼你必須去深刻的認識下我們經常打交道的webpack
本文共分爲三個部分帶你快速掌握webpack,閱讀本篇大概需要60分鐘。如有不足之處,懇請斧正
本文編寫基於
webpack 4.41.2
版本node: 10.15.3
版本
1 入門(一起來用這些小例子讓你熟悉webpack的配置)
1.1 初始化項目
新建一個目錄,初始化npm
npm init
webpack是運行在node環境中的,我們需要安裝以下兩個npm包
npm i -D webpack webpack-cli
- npm i -D 爲npm install --save-dev的縮寫
- npm i -S 爲npm install --save的縮寫
新建一個文件夾src
,然後新建一個文件main.js
,寫一點代碼測試一下
console.log('test 1...')
配置package.json命令
"scripts": {
"build": "webpack src/main.js"
},
執行
npm run build
此時如果生成了一個dist文件夾,並且內部含有main.js說明已經打包成功了
1.2 開始我們自己的配置
上面一個簡單的例子只是webpack自己默認的配置,下面我們要實現更加豐富的自定義配置
新建一個build
文件夾,裏面新建一個webpack.config.js
// 自定義配置文件:webpack.config.js
const path = require('path')
module.exports = {
mode: 'development', // 開發模式
// mode: 'production',
entry: path.resolve(__dirname, '../src/main.js'), // 項目入口文件
output: {
filename: 'output.js', // 打包後的文件名稱
path: path.resolve(__dirname, '../dist') // 打包後的目錄
}
}
更改我們的打包命令
"scripts": {
"build": "webpack --config build/webpack.config.js"
},
執行 npm run build
會發現生成了以下目錄(圖片)
其中dist
文件夾中的main.js
就是我們需要在瀏覽器中實際運行的文件
當然實際運用中不會僅僅如此,下面讓我們通過實際案例帶你快速入手webpack
1.3 配置html模板
js文件打包好了,但是我們不可能每次在html
文件中手動引入打包好的js
這裏可能有的朋友會認爲我們打包js文件名稱不是一直是固定的嘛(output.js)?這樣每次就不用改動引入文件名稱了呀?實際上我們日常開發中往往會這樣配置:
module.exports = {
mode: 'development', // 開發模式
// mode: 'production',
entry: path.resolve(__dirname, '../src/main.js'), // 項目入口文件
output: {
// filename: 'output.js', // 打包後的文件名稱
filename: '[name].[hash:8].js', // 日常開發中的配置
path: path.resolve(__dirname, '../dist') // 打包後的目錄
}
}
這時候生成的dist
目錄文件如:
dist/main.9fle44ba.js
爲了緩存,你會發現打包好的js文件的名稱每次都不一樣。webpack打包出來的js文件我們需要引入到html中,但是每次我們都手動修改js文件名顯得很麻煩,因此我們需要一個插件來幫我們完成這件事情
npm i -D html-webpack-plugin
新建一個build
同級的文件夾public
,裏面新建一個index.html
具體配置文件如下
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 開發模式
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')
})
]
}
可以發現打包生成的js文件已經被自動引入html文件中
1.3.1 多入口文件如何開發
生成多個html-webpack-plugin實例來解決這個問題
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 開發模式
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'] // 與入口文件對應的模塊名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 與入口文件對應的模塊名
}),
]
}
1.3.2 clean-webpack-plugin
每次執行npm run build 會發現dist文件夾裏會殘留上次打包的文件,這裏我們推薦一個plugin來幫我們在打包輸出前清空文件夾
clean-webpack-plugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
1.4 引用CSS
我們的入口文件是js,所以我們在入口js中引入我們的css文件
同時我們也需要一些loader來解析我們的css文件npm i -D style-loader css-loader
如果我們使用less來構建樣式,則需要多安裝兩個
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,在項目根目錄下創建一個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'] // 從右向左解析原則
}]
}
}
這時候我們發現css通過style標籤的方式添加到了html文件中,但是如果樣式文件很多,全部添加到html中,難免顯得混亂。這時候我們想用把css拆分出來用外鏈的形式引入css文件怎麼做呢?這時候我們就需要藉助插件來幫助我們
1.4.2 拆分css
npm i -D mini-css-extract-plugin
webpack 4.0以前,我們通過
extract-text-webpack-plugin
插件,把css樣式從js文件中提取到單獨的css文件中。webpack4.0以後,官方推薦使用mini-css-extract-plugin
插件來打包css文件
配置文件如下
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 拆分多個css
這裏需要說的細一點,上面我們所用到的
mini-css-extract-plugin
會將所有的css樣式合併爲一個css文件。如果你想拆分爲一一對應的多個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
就是將文件在進行一些處理後(主要是處理文件名和路徑、解析文件url),並將文件移動到輸出的目錄中
url-loader
一般與file-loader
搭配使用,功能與 file-loader 類似,如果文件小於限制的大小。則會返回 base64 編碼,否則使用 file-loader 將文件移動到輸出的目錄中
// 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轉義js文件
爲了使我們的js代碼兼容更多的環境我們需要安裝依賴
npm i babel-loader @babel/preset-env @babel/core
- 注意
babel-loader
與babel-core
的版本對應關係
babel-loader
8.x 對應babel-core
7.xbabel-loader
7.x 對應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
只會將 ES6/7/8語法轉換爲ES5語法,但是對新api並不會轉換 例如(promise、Generator、Set、Maps、Proxy等)
此時我們需要藉助babel-polyfill來幫助我們轉換
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
手動把上面的demo敲一遍對閱讀下面的文章更有益,建議入門的同學敲三遍以上
上面的實踐是我們對webpack的功能有了一個初步的瞭解,但是要想熟練應用於開發中,我們需要一個系統的實戰。讓我們一起擺脫腳手架嘗試自己搭建一個vue開發環境
2 搭建vue開發環境
上面的小例子已經幫助而我們實現了打包css、圖片、js、html等文件。 但是我們還需要以下幾種配置
2.1 解析.vue文件
npm i -D vue-loader vue-template-compiler vue-style-loader
npm 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進行熱更新
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', // 開發模式
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 配置打包命令
"scripts": {
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js",
}
打包文件已經配置完畢,接下來讓我們測試一下
首先在src新建一個main.js
import Vue form 'vue'
import App from './app'
new Vue({
render: h => h(App)
}).$mount('#app')
新建一個App.vue
新建一個public文件夾,裏面新建一個index.html
執行npm run dev
這時候如果瀏覽器出現Vue開發環境運行成功,那麼恭喜你,已經成功邁出了第一步
2.4 區分開發環境與生產環境
實際應用到項目中,我們需要區分開發環境與生產環境,我們在原來webpack.config.js的基礎上再新增兩個文件
webpack.dev.js
開發環境配置文件
開發環境主要實現的是熱更新,不要壓縮代碼,完整的sourceMap
webpack.prod.js
生產環境配置文件
生產環境主要實現的是壓縮代碼、提取css文件、合理的sourceMap、分割代碼
需要安裝以下模塊:
npm i -D webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
webpack-merge
合併配置copy-webpack-plugin
拷貝靜態資源optimize-css-assets-webpack-plugin
壓縮cssuglifyjs-webpack-plugin
壓縮js
webpack mode
設置production
的時候會自動壓縮js代碼。原則上不需要引入uglifyjs-webpack-plugin
進行重複工作。但是optimize-css-assets-webpack-plugin
壓縮css的同時會破壞原有的js壓縮,所以這裏我們引入uglifyjs
進行壓縮
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" // 只打包初始時依賴的第三方
}
}
}
}
})
2.5 優化webpack配置
看到這裏你或許有些累了,但是要想獲取更好的offer,更高的薪水,下面必須繼續深入
優化配置對我們來說非常有實際意義,這實際關係到你打包出來文件的大小,打包的速度等。 具體優化可以分爲以下幾點:2.5.1 優化打包速度
構建速度指的是我們每次修改代碼後熱更新的速度以及發佈前打包文件的速度。
2.5.1.1 合理的配置mode參數與devtool參數
mode
可設置development
production
兩個參數
如果沒有設置,webpack4
會將 mode
的默認值設置爲 production
production
模式下會進行tree shaking
(去除無用代碼)和uglifyjs
(代碼壓縮混淆)
2.5.1.2 縮小文件的搜索範圍(配置include exclude alias noParse extensions)
alias
: 當我們代碼中出現import 'vue'
時, webpack會採用向上遞歸搜索的方式去node_modules
目錄下找。爲了減少搜索範圍我們可以直接告訴webpack去哪個路徑下查找。也就是別名(alias
)的配置。include exclude
同樣配置include exclude
也可以減少webpack loader
的搜索轉換時間。noParse
當我們代碼中使用到import jq from 'jquery'
時,webpack
會去解析jq這個庫是否有依賴其他的包。但是我們對類似jquery
這類依賴庫,一般會認爲不會引用其他的包(特殊除外,自行判斷)。增加noParse
屬性,告訴webpack
不必解析,以此增加打包速度。extensions
webpack
會根據extensions
定義的後綴查找文件(頻率較高的文件類型優先寫在前面)
2.5.1.3 使用HappyPack開啓多進程Loader轉換
在webpack構建過程中,實際上耗費時間大多數用在loader解析轉換以及代碼的壓縮中。日常開發中我們需要使用Loader對js,css,圖片,字體等文件做轉換操作,並且轉換的文件數據量也是非常大。由於js單線程的特性使得這些轉換操作不能併發處理文件,而是需要一個個文件進行處理。HappyPack的基本原理是將這部分任務分解到多個子進程中去並行處理,子進程處理完成後把結果發送到主進程中,從而減少總的構建時間
npm i -D happypack
2.5.1.4 使用webpack-parallel-uglify-plugin 增強代碼壓縮
上面對於loader轉換已經做優化,那麼下面還有另一個難點就是優化代碼的壓縮時間。
npm i -D webpack-parallel-iuglify-plugin
2.5.1.5 抽離第三方模塊
對於開發項目中不經常會變更的靜態依賴文件。類似於我們的
elementUi、vue
全家桶等等。因爲很少會變更,所以我們不希望這些依賴要被集成到每一次的構建邏輯中去。 這樣做的好處是每次更改我本地代碼的文件的時候,webpack
只需要打包我項目本身的文件代碼,而不會再去編譯第三方庫。以後只要我們不升級第三方包的時候,那麼webpack
就不會對這些庫去打包,這樣可以快速的提高打包的速度。
這裏我們使用webpack
內置的DllPlugin DllReferencePlugin
進行抽離
在與webpack
配置文件同級目錄下新建webpack.dll.config.js
代碼如下
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模塊的數組
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目錄 這樣每次不必手動去cv
{from: 'static', to:'static'}
]),
]
};
執行
npm run dll
會發現生成了我們需要的集合第三地方
代碼的vendor.dll.js
我們需要在html
文件中手動引入這個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
。直接執行npm run dev npm run build
的時候會發現我們的打包速度明顯有所提升。因爲我們已經通過dllPlugin
將第三方依賴包抽離出來了。
2.5.1.6 配置緩存
我們每次執行構建都會把所有的文件都重複編譯一遍,這樣的重複工作是否可以被緩存下來呢,答案是可以的,目前大部分
loader
都提供了cache
配置項。比如在babel-loader
中,可以通過設置cacheDirectory
來開啓緩存,babel-loader?cacheDirectory=true
就會將每次的編譯結果寫進硬盤文件(默認是在項目根目錄下的node_modules/.cache/babel-loader
目錄內,當然你也可以自定義)
但如果 loader
不支持緩存呢?我們也有方法,我們可以通過cache-loader
,它所做的事情很簡單,就是 babel-loader
開啓 cache
後做的事情,將 loader
的編譯結果寫入硬盤緩存。再次構建會先比較一下,如果文件較之前的沒有發生變化則會直接使用緩存。使用方法如官方 demo 所示,在一些性能開銷較大的 loader 之前添加此 loader即可
npm i -D cache-loader
2.5.2 優化打包文件體積
打包的速度我們是進行了優化,但是打包後的文件體積卻是十分大,造成了頁面加載緩慢,浪費流量等,接下來讓我們從文件體積上繼續優化
2.5.2.1 引入webpack-bundle-analyzer分析打包後的文件
webpack-bundle-analyzer
將打包後的內容束展示爲方便交互的直觀樹狀圖,讓我們知道我們所構建包中真正引入的內容
npm i -D webpack-bundle-analyzer
接下來在package.json
裏配置啓動命令
"analyz": "MODE_ENV=production npm_config_report=true npm run build"
windows請安裝npm i -D cross-env
"analyz": "cross-env MODE_ENV=production npm_config_report=true npm run build"
接下來npm run analyz
瀏覽器會自動打開文件依賴圖的網頁
2.5.2.3 externals
按照官方文檔的解釋,如果我們想引用一個庫,但是又不想讓
webpack
打包,並且又不影響我們在程序中以CMD、AMD
或者window/global
全局等方式進行使用,那就可以通過配置Externals
。這個功能主要是用在創建一個庫的時候用的,但是也可以在我們項目開發中充分使用Externals
的方式,我們將這些不需要打包的靜態資源從構建邏輯中剔除出去,而使用CDN
的方式,去引用它們。
有時我們希望我們通過script
引入的庫,如用CDN的方式引入的jquery
,我們在使用時,依舊用require
的方式來使用,但是卻不希望webpack
將它又編譯進文件中。這裏官網案例已經足夠清晰明瞭,大家有興趣可以點擊瞭解
webpack 官網案例如下
<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
這裏單獨提一下
tree-shaking
,是因爲這裏有個坑。tree-shaking
的主要作用是用來清除代碼中無用的部分。目前在webpack4
我們設置mode
爲production
的時候已經自動開啓了tree-shaking
。但是要想使其生效,生成的代碼必須是ES6模塊。不能使用其它類型的模塊如CommonJS
之流。如果使用Babel
的話,這裏有一個小問題,因爲Babel
的預案(preset)默認會將任何模塊類型都轉譯成CommonJS
類型,這樣會導致tree-shaking
失效。修正這個問題也很簡單,在.babelrc
文件或在webpack.config.js
文件中設置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)/
}
]
}
經歷過上面兩個系列的洗禮,到現在我們成爲了一名合格的webpack配置工程師。但是光擰螺絲,自身的可替代性還是很高,下面我們將深入webpack的原理中去
3 手寫webpack系列
經歷過上面兩個部分,我們已經可以熟練的運用相關的loader和plugin對我們的代碼進行轉換、解析。接下來我們自己手動實現loader與plugin,使其在平時的開發中獲得更多的樂趣。
3.1 手寫webpack loader
loader
從本質上來說其實就是一個node
模塊。相當於一臺榨汁機(loader)
將相關類型的文件代碼(code)
給它。根據我們設置的規則,經過它的一系列加工後還給我們加工好的果汁(code)
。
loader
編寫原則
- 單一原則: 每個
Loader
只做一件事; - 鏈式調用:
Webpack
會按順序鏈式調用每個Loader
; - 統一原則: 遵循
Webpack
制定的設計規則和結構,輸入與輸出均爲字符串,各個Loader
完全獨立,即插即用;
在日常開發環境中,爲了方便調試我們往往會加入許多console
打印。但是我們不希望在生產環境中存在打印的值。那麼這裏我們自己實現一個loader
去除代碼中的console
知識點普及之
AST
。AST
通俗的來說,假設我們有一個文件a.js
,我們對a.js
裏面的1000行進行一些操作處理,比如爲所有的await
增加try catch
,以及其他操作,但是a.js
裏面的代碼本質上來說就是一堆字符串。那我們怎麼辦呢,那就是轉換爲帶標記信息的對象(抽象語法樹)我們方便進行增刪改查。這個帶標記的對象(抽象語法樹)就是AST
。這裏推薦一篇不錯的AST文章 AST快速入門
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
@babel/parser
將源代碼解析成AST
@babel/traverse
對AST
節點進行遞歸遍歷,生成一個便於操作、轉換的path
對象@babel/generator
將AST
解碼生成js
代碼@babel/types
通過該模塊對具體的AST
節點進行進行增、刪、改、查
新建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')
}
]
}
}
實際上在
webpack4
中已經集成了去除console
功能,在minimizer
中可配置 去除console
附上官網 如何編寫一個loader
3.2 手寫webpack plugin
在
Webpack
運行的生命週期中會廣播出許多事件,Plugin
可以監聽這些事件,在合適的時機通過Webpack
提供的API
改變輸出結果。通俗來說:一盤美味的 鹽豆炒雞蛋 需要經歷燒油 炒制 調味到最後的裝盤等過程,而plugin
相當於可以監控每個環節並進行操作,比如可以寫一個少放胡椒粉plugin
,監控webpack
暴露出的生命週期事件(調味),在調味的時候執行少放胡椒粉操作。那麼它與loader
的區別是什麼呢?上面我們也提到了loader
的單一原則,loader
只能一件事,比如說less-loader
,只能解析less
文件,plugin
則是針對整個流程執行廣泛的任務。
一個基本的plugin插件結構如下
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}
module.exports = firstPlugin
compiler 、compilation是什麼?
compiler
對象包含了Webpack
環境所有的的配置信息。這個對象在啓動webpack
時被一次性建立,並配置好所有可操作的設置,包括options
,loader
和plugin
。當在webpack
環境中應用一個插件時,插件將收到此compiler
對象的引用。可以使用它來訪問webpack
的主環境。compilation
對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當運行webpack
開發環境中間件時,每當檢測到一個文件變化,就會創建一個新的compilation
,從而生成一組新的編譯資源。compilation
對象也提供了很多關鍵時機的回調,以供插件做自定義處理時選擇使用。
compiler和 compilation的區別在於
-
compiler代表了整個webpack從啓動到關閉的生命週期,而compilation 只是代表了一次新的編譯過程
-
compiler和compilation暴露出許多鉤子,我們可以根據實際需求的場景進行自定義處理
下面我們手動開發一個簡單的需求,在生成打包文件之前自動生成一個關於打包出文件的大小信息
新建一個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可以獲取打包後靜態資源信息,同樣也可以寫入資源
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')
moudle.exports = {
//...
plugins: [
new firstPlugin
]
}
執行 npm run build
即可看到在dist
文件夾中生成了一個包含打包文件信息的fileSize.md
上面兩個
loader
與plugin
案例只是一個引導,實際開發需求中的loader
與plugin
要考慮的方面很多,建議大家自己多動手嘗試一下。
附上官網 如何編寫一個plugin
3.3 手寫webpack
由於篇幅過長,且原理深入較多。鑑於本篇以快速上手應用於實際開發的原則,這裏決定另起一篇新的文章去詳細剖析
webpack
原理以及實現一個demo
版本。待格式校準後,將會貼出文章鏈接在下方
4 webpack5.0的時代
無論是前端框架還是構建工具的更新速度遠遠超乎了我們的想象,前幾年的jquery
一把梭的時代一去不復返。我們要擁抱的是不斷更新迭代的vue、react、node、serverless、docker、k8s
....
webpack5.0
旨在減少配置的複雜度,使其更容易上手(webpack4
的時候也說了這句話),以及一些性能上的提升
- 使用持久化緩存提高構建性能;
- 使用更好的算法和默認值改進長期緩存(long-term caching);
- 清理內部結構而不引入任何破壞性的變化;
- 引入一些breaking changes,以便儘可能長的使用v5版本。
目前來看,維護者的更新很頻繁,相信用不了多久webpack5.0
將會擁抱大衆。感興趣的同學可以先安裝beta
版本嚐嚐鮮。不過在此之前建議大家先對webpack4
進行一番掌握,這樣後面的路纔會越來越好走。