webpack從入門到進階
第1章 課程介紹
學什么
本質上,webpack 是一個現(xiàn)代 JavaScript 應用程序的靜態(tài)模塊打包器(module bundler)曙咽。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關系圖(dependency graph)颁井,其中包含應用程序需要的每個模塊淫奔,然后將所有這些模塊打包成一個或多個 bundle津畸。
- 代碼轉譯
- 模塊合并
- 混淆壓縮
- 代碼分割
- 自動刷新
- 代碼校驗
- 自動部署
課程安排
- webpack基礎配置
- webpack高級配置
- webpack性能優(yōu)化
- tapable鉤子
- AST抽象語法樹的應用
- webpack原理分析, 手寫webpack
- 手寫常見的loader和plugin
學習前提
- JS基礎
- ES6 / ES7 語法
- node基礎
- npm的基本使用
課程目標
- 掌握webpack的安裝
- 掌握webpack的基礎配置
- 掌握loader的配置
- 掌握plugin的配置
- 了解webpack性能優(yōu)化
- 了解webpack中的tapable
- 了解AST的應用
- 深入學習webpack原理振定,手寫webpack
第2章 webpack基礎
webpack的安裝
注意:請先自行安裝nodejs最新版的環(huán)境
-
全局安裝webpack
npm i webpack webpack-cli -g
-
項目中安裝webpack (推薦)
npm i webpack webpack-cli -D
webpack的使用
webpack-cli
npm 5.2 以上的版本中提供了一個npx
命令
npx 想要解決的主要問題,就是調(diào)用項目內(nèi)部安裝的模塊肉拓,原理就是在node_modules
下的.bin
目錄中找到對應的命令執(zhí)行
使用webpack命令:npx webpack
webpack4.0之后可以實現(xiàn)0配置打包構建吩案,0配置的特點就是限制較多,無法自定義很多配置
開發(fā)中常用的還是使用webpack配置進行打包構建
webpack配置
webpack有四大核心概念:
- 入口(entry): 程序的入口js
- 輸出(output): 打包后存放的位置
- loader: 用于對模塊的源代碼進行轉換
- 插件(plugins): 插件目的在于解決 loader無法實現(xiàn)的其他事
- 配置webpack.config.js
- 運行
npx webpack
const path = require('path')
module.exports = {
// 入口文件配置
entry: './src/index.js',
// 出口文件配置項
output: {
// 輸出的路徑帝簇,webpack2起就規(guī)定必須是絕對路徑
path: path.join(__dirname, 'dist'),
// 輸出文件名字
filename: 'bundle.js'
},
mode: 'development' // 默認為production, 可以手動設置為development, 區(qū)別就是是否進行壓縮混淆
}
將npx webpack
命令配置到package.json
的腳本中
- 配置
package.json
- 運行
npm run build
{
"name": "webpack-basic",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1"
}
}
開發(fā)時自動編譯工具
每次要編譯代碼時徘郭,手動運行 npm run build
就會變得很麻煩。
webpack 中有幾個不同的選項丧肴,可以幫助你在代碼發(fā)生變化后自動編譯代碼:
- webpack's Watch Mode
- webpack-dev-server
- webpack-dev-middleware
多數(shù)場景中残揉,可能需要使用 webpack-dev-server
,但是不妨探討一下以上的所有選項芋浮。
watch
在webpack
指令后面加上--watch
參數(shù)即可
主要的作用就是監(jiān)視本地項目文件的變化, 發(fā)現(xiàn)有修改的代碼會自動編譯打包, 生成輸出文件
配置
package.json
的scripts"watch": "webpack --watch"
運行
npm run watch
以上是cli的方式設置watch的參數(shù)
還可以通過配置文件對watch的參數(shù)進行修改:
const path = require('path')
// webpack的配置文件遵循著CommonJS規(guī)范
module.exports = {
entry: './src/main.js',
output: {
// path.resolve() : 解析當前相對路徑的絕對路徑
// path: path.resolve('./dist/'),
// path: path.resolve(__dirname, './dist/'),
path: path.join(__dirname, './dist/'),
filename: 'bundle.js'
},
mode: 'development',
watch: true
}
運行npm run build
webpack-dev-server (推薦)
-
安裝
devServer
:devServer
需要依賴webpack
抱环,必須在項目依賴中安裝webpack
npm i webpack-dev-server webpack -D
index.html中修改
<script src="/bundle.js"></script>
運行:
npx webpack-dev-server
運行:
npx webpack-dev-server --hot --open --port 8090
配置
package.json
的scripts:"dev": "webpack-dev-server --hot --open --port 8090"
運行
npm run dev
devServer會在內(nèi)存中生成一個打包好的bundle.js
壳快,專供開發(fā)時使用,打包效率高镇草,修改代碼后會自動重新打包以及刷新瀏覽器眶痰,用戶體驗非常好
以上是cli的方式設置devServer的參數(shù)
還可以通過配置文件對devServer的參數(shù)進行修改:
- 修改
webpack.config.js
const path = require('path')
module.exports = {
// 入口文件配置
entry: './src/index.js',
// 出口文件配置項
output: {
// 輸出的路徑,webpack2起就規(guī)定必須是絕對路徑
path: path.join(__dirname, 'dist'),
// 輸出文件名字
filename: 'bundle.js'
},
devServer: {
port: 8090,
open: true,
hot: true
},
mode: 'development'
}
- 修改package.json的scripts:
"dev": "webpack-dev-server"
- 運行
npm run dev
html插件
- 安裝html-webpack-plugin插件
npm i html-webpack-plugin -D
- 在
webpack.config.js
中的plugins
節(jié)點下配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html'
})
]
- devServer時根據(jù)模板在express項目根目錄下生成html文件(類似于devServer生成內(nèi)存中的bundle.js)
- devServer時自動引入bundle.js
- 打包時會自動生成index.html
webpack-dev-middleware
webpack-dev-middleware
是一個容器(wrapper)梯啤,它可以把 webpack 處理后的文件傳遞給一個服務器(server)竖伯。 webpack-dev-server
在內(nèi)部使用了它,同時因宇,它也可以作為一個單獨的包來使用七婴,以便進行更多自定義設置來實現(xiàn)更多的需求。
-
安裝
express
和webpack-dev-middleware
:npm i express webpack-dev-middleware -D
-
新建
server.js
const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const config = require('./webpack.config.js'); const app = express(); const compiler = webpack(config); app.use(webpackDevMiddleware(compiler, { publicPath: '/' })); app.listen(3000, function () { console.log('http://localhost:3000'); });
配置
package.json
中的scripts:"server": "node server.js"
運行:
npm run server
注意: 如果要使用webpack-dev-middleware
, 必須使用html-webpack-plugin
插件, 否則html文件無法正確的輸出到express服務器的根目錄
小結
只有在開發(fā)時才需要使用自動編譯工具, 例如: webpack-dev-server
項目上線時都會直接使用webpack進行打包構建, 不需要使用這些自動編譯工具
自動編譯工具只是為了提高開發(fā)體驗
處理css
- 安裝
npm i css-loader style-loader -D
- 配置
webpack.config.js
module: {
rules: [
// 配置的是用來解析.css文件的loader(style-loader和css-loader)
{
// 用正則匹配當前訪問的文件的后綴名是 .css
test: /\.css$/,
use: ['style-loader', 'css-loader'] // webpack底層調(diào)用這些包的順序是從右到左
}
]
}
loader的釋義:
- css-loader: 解析css文件
- style-loader: 將解析出來的結果 放到html中, 使其生效
處理less 和 sass
npm i less less-loader sass-loader node-sass -D
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }
處理圖片和字體
npm i file-loader url-loader -D
url-loader封裝了file-loader, 所以使用url-loader時需要安裝file-loader
{
test: /\.(png|jpg|gif)/,
use: [{
loader: 'url-loader',
options: {
// limit表示如果圖片大于5KB察滑,就以路徑形式展示打厘,小于的話就用base64格式展示
limit: 5 * 1024,
// 打包輸出目錄
outputPath: 'images',
// 打包輸出圖片名稱
name: '[name]-[hash:4].[ext]'
}
}]
}
babel
npm i babel-loader @babel/core @babel/preset-env webpack -D
-
如果需要支持更高級的ES6語法, 可以繼續(xù)安裝插件:
npm i @babel/plugin-proposal-class-properties -D
也可以根據(jù)需要在babel官網(wǎng)找插件進行安裝
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/env'],
plugins: ['@babel/plugin-proposal-class-properties']
}
},
exclude: /node_modules/
}
官方更建議的做法是在項目根目錄下新建一個.babelrc
的babel配置文件
{
"presets": ["@babel/env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
如果需要使用generator
,無法直接使用babel進行轉換贺辰,因為會將generator
轉換為一個regeneratorRuntime
户盯,然后使用mark
和wrap
來實現(xiàn)generator
但由于babel并沒有內(nèi)置regeneratorRuntime
,所以無法直接使用
需要安裝插件:
`npm i @babel/plugin-transform-runtime -D`
同時還需安裝運行時依賴:
`npm i @babel/runtime -D`
在.babelrc
中添加插件:
{
"presets": [
"@babel/env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}
如果需要使用ES6/7中對象原型提供的新方法饲化,babel默認情況無法轉換先舷,即使用了transform-runtime
的插件也不支持轉換原型上的方法
需要使用另一個模塊:
`npm i @babel/polyfill -S`
該模塊需要在使用新方法的地方直接引入:
`import '@babel/polyfill'`
source map的使用
devtool
此選項控制是否生成,以及如何生成 source map滓侍。
使用 SourceMapDevToolPlugin
進行更細粒度的配置。查看 source-map-loader
來處理已有的 source map牲芋。
選擇一種 source map 格式來增強調(diào)試過程撩笆。不同的值會明顯影響到構建(build)和重新構建(rebuild)的速度。
可以直接使用
SourceMapDevToolPlugin
/EvalSourceMapDevToolPlugin
來替代使用devtool
選項缸浦,它有更多的選項夕冲,但是切勿同時使用devtool
選項和SourceMapDevToolPlugin
/EvalSourceMapDevToolPlugin
插件。因為devtool
選項在內(nèi)部添加過這些插件裂逐,所以會應用兩次插件歹鱼。
devtool | 構建速度 | 重新構建速度 | 生產(chǎn)環(huán)境 | 品質(quality) |
---|---|---|---|---|
(none) | +++ | +++ | yes | 打包后的代碼 |
eval | +++ | +++ | no | 生成后的代碼 |
cheap-eval-source-map | + | ++ | no | 轉換過的代碼(僅限行) |
cheap-module-eval-source-map | o | ++ | no | 原始源代碼(僅限行) |
eval-source-map | -- | + | no | 原始源代碼 |
cheap-source-map | + | o | no | 轉換過的代碼(僅限行) |
cheap-module-source-map | o | - | no | 原始源代碼(僅限行) |
inline-cheap-source-map | + | o | no | 轉換過的代碼(僅限行) |
inline-cheap-module-source-map | o | - | no | 原始源代碼(僅限行) |
source-map | -- | -- | yes | 原始源代碼 |
inline-source-map | -- | -- | no | 原始源代碼 |
hidden-source-map | -- | -- | yes | 原始源代碼 |
nosources-source-map | -- | -- | yes | 無源代碼內(nèi)容 |
這么多模式用哪個好?
開發(fā)環(huán)境推薦:
**cheap-module-eval-source-map**
生產(chǎn)環(huán)境推薦:
**none(不使用source map)**
原因如下:
- 使用 cheap 模式可以大幅提高 soure map 生成的效率卜高。大部分情況我們調(diào)試并不關心列信息弥姻,而且就算 source map 沒有列,有些瀏覽器引擎(例如 v8) 也會給出列信息掺涛。
- 使用 module 可支持 babel 這種預編譯工具庭敦,映射轉換前的代碼。
- 使用 eval 方式可大幅提高持續(xù)構建效率薪缆。官方文檔提供的速度對比表格可以看到 eval 模式的重新構建速度都很快秧廉。
- 使用 eval-source-map 模式可以減少網(wǎng)絡請求。這種模式開啟 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那樣疼电,瀏覽器需要發(fā)送一個完整請求去獲取 sourcemap 文件嚼锄,這會略微提高點效率。而生產(chǎn)環(huán)境中則不宜用 eval蔽豺,這樣會讓文件變得極大区丑。
插件
clean-webpack-plugin
該插件在npm run build
時自動清除dist
目錄后重新生成,非常方便
-
安裝插件
npm i clean-webpack-plugin -D
-
引入插件
const CleanWebpackPlugin = require('clean-webpack-plugin')
-
使用插件, 在plugins中直接創(chuàng)建對象即可
plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html' }), new CleanWebpackPlugin() ],
copy-webpack-plugin
-
安裝插件
npm i copy-webpack-plugin -D
-
引入插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
-
使用插件, 在plugins中插件對象并配置源和目標
from: 源, 從哪里拷貝, 可以是相對路徑或絕對路徑, 推薦絕對路徑
to: 目標, 拷貝到哪里去, 相對于
output
的路徑, 同樣可以相對路徑或絕對路徑, 但更推薦相對路徑(直接算相對dist目錄即可)plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html' }), new CleanWebpackPlugin(), new CopyWebpackPlugin([ { from: path.join(__dirname, 'assets'), to: 'assets' } ]) ],
BannerPlugin
這是一個webpack的內(nèi)置插件茫虽,用于給打包的JS文件加上版權注釋信息
-
引入webpack
const webpack = require('webpack')
-
創(chuàng)建插件對象
plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html' }), new CleanWebpackPlugin(), new CopyWebpackPlugin([ { from: path.join(__dirname, 'assets'), to: 'assets' } ]), new webpack.BannerPlugin('黑馬程序員牛逼!') ],
第3章 webpack高級配置
HTML中img標簽的圖片資源處理
安裝
npm install -S html-withimg-loader
-
在
webpack.config.js
文件中添加loader{ test: /\.(htm|html)$/i, loader: 'html-withimg-loader' }
使用時刊苍,只需要在html中正常引用圖片即可,webpack會找到對應的資源進行打包濒析,并修改html中的引用路徑
多頁應用打包
-
在
webpack.config.js
中修改入口和出口配置// 1. 修改為多入口 entry: { main: './src/main.js', other: './src/other.js' }, output: { path: path.join(__dirname, './dist/'), // filename: 'bundle.js', // 2. 多入口無法對應一個固定的出口, 所以修改filename為[name]變量 filename: '[name].js', publicPath: '/' }, plugins: [ // 3. 如果用了html插件,需要手動配置多入口對應的html文件,將指定其對應的輸出文件 new HtmlWebpackPlugin({ template: './index.html', filename: 'index.html', chunks: ['main'] }), new HtmlWebpackPlugin({ template: './index.html', filename: 'other.html', // chunks: ['other', 'main'] chunks: ['other'] }) ]
修改入口為對象正什,支持多個js入口,同時修改output輸出的文件名為
'[name].js'
表示各自已入口文件名作為輸出文件名号杏,但是html-webpack-plugin
不支持此功能婴氮,所以需要再拷貝一份插件,用于生成兩個html頁面盾致,實現(xiàn)多頁應用
第三方庫的兩種引入方式
可以通過expose-loader
進行全局變量的注入主经,同時也可以使用內(nèi)置插件webpack.ProvidePlugin
對每個模塊的閉包空間,注入一個變量庭惜,自動加載模塊罩驻,而不必到處 import
或 require
-
expose-loader 將庫引入到全局作用域
-
安裝
expose-loader
npm i -D expose-loader
-
配置loader
module: { rules: [{ test: require.resolve('jquery'), use: { loader: 'expose-loader', options: '$' } }] }
tips:
require.resolve
用來獲取模塊的絕對路徑。所以這里的loader只會作用于 jquery 模塊护赊。并且只在 bundle 中使用到它時惠遏,才進行處理。
-
-
webpack.ProvidePlugin 將庫自動加載到每個模塊
-
引入webpack
const webpack = require('webpack')
-
創(chuàng)建插件對象
要自動加載
jquery
骏啰,我們可以將兩個變量都指向對應的 node 模塊new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })
-
Development / Production不同配置文件打包
項目開發(fā)時一般需要使用兩套配置文件节吮,用于開發(fā)階段打包(不壓縮代碼,不優(yōu)化代碼判耕,增加效率)和上線階段打包(壓縮代碼透绩,優(yōu)化代碼,打包后直接上線使用)
抽取三個配置文件:
webpack.base.js
webpack.prod.js
webpack.dev.js
步驟如下:
將開發(fā)環(huán)境和生產(chǎn)環(huán)境公用的配置放入base中壁熄,不同的配置各自放入prod或dev文件中(例如:mode)
-
然后在dev和prod中使用
webpack-merge
把自己的配置與base的配置進行合并后導出npm i -D webpack-merge
將package.json中的腳本參數(shù)進行修改帚豪,通過
--config
手動指定特定的配置文件
定義環(huán)境變量
除了區(qū)分不同的配置文件進行打包,還需要在開發(fā)時知道當前的環(huán)境是開發(fā)階段或上線階段草丧,所以可以借助內(nèi)置插件DefinePlugin
來定義環(huán)境變量志鞍。最終可以實現(xiàn)開發(fā)階段與上線階段的api地址自動切換。
-
引入webpack
const webpack = require('webpack')
-
創(chuàng)建插件對象方仿,并定義環(huán)境變量
new webpack.DefinePlugin({ IS_DEV: 'false' })
在src打包的代碼環(huán)境下可以直接使用
使用devServer解決跨域問題
在開發(fā)階段很多時候需要使用到跨域固棚,何為跨域统翩?(度娘)
開發(fā)階段往往會遇到上面這種情況,也許將來上線后此洲,前端項目會和后端項目部署在同一個服務器下厂汗,并不會有跨域問題,但是由于開發(fā)時會用到webpack-dev-server呜师,所以一定會產(chǎn)生跨域的問題
目前解決跨域主要的方案有:
- jsonp(淘汰)
- cors
- http proxy
此處介紹的使用devServer解決跨域娶桦,其實原理就是http proxy
將所有ajax請求發(fā)送給devServer服務器,再由devServer服務器做一次轉發(fā)汁汗,發(fā)送給數(shù)據(jù)接口服務器
由于ajax請求是發(fā)送給devServer服務器的衷畦,所以不存在跨域,而devServer由于是用node平臺發(fā)送的http請求知牌,自然也不涉及到跨域問題祈争,可以完美解決!
服務器代碼(返回一段字符串即可):
const express = require('express')
const app = express()
// const cors = require('cors')
// app.use(cors())
app.get('/api/getUserInfo', (req, res) => {
res.send({
name: '黑馬兒',
age: 13
})
});
app.listen(9999, () => {
console.log('http://localhost:9999!');
});
前端需要配置devServer的proxy功能角寸,在webpack.dev.js
中進行配置:
devServer: {
open: true,
hot: true,
compress: true,
port: 3000,
// contentBase: './src'
proxy: {
'/api': 'http://localhost:9999'
}
},
意為前端請求/api
的url時菩混,webpack-dev-server會將請求轉發(fā)給http://localhost:9999/api
處,此時如果請求地址為http://localhost:9999/api/getUserInfo
扁藕,只需要直接寫/api/getUserInfo
即可沮峡,代碼如下:
axios.get('/api/getUserInfo').then(result => console.log(result))
HMR的使用
需要對某個模塊進行熱更新時,可以通過module.hot.accept
方法進行文件監(jiān)視
只要模塊內(nèi)容發(fā)生變化亿柑,就會觸發(fā)回調(diào)函數(shù)邢疙,從而可以重新讀取模塊內(nèi)容,做對應的操作
if (module.hot) {
module.hot.accept('./hotmodule.js', function() {
console.log('hotmodule.js更新了');
let str = require('./hotmodule.js')
console.log(str)
})
}
第4章 webpack優(yōu)化
production模式打包自帶優(yōu)化
-
tree shaking
tree shaking 是一個術語望薄,通常用于打包時移除 JavaScript 中的未引用的代碼(dead-code)疟游,它依賴于 ES6 模塊系統(tǒng)中
import
和export
的靜態(tài)結構特性。開發(fā)時引入一個模塊后式矫,如果只使用其中一個功能,上線打包時只會把用到的功能打包進bundle役耕,其他沒用到的功能都不會打包進來采转,可以實現(xiàn)最基礎的優(yōu)化
-
scope hoisting
scope hoisting的作用是將模塊之間的關系進行結果推測, 可以讓 Webpack 打包出來的代碼文件更小瞬痘、運行的更快
scope hoisting 的實現(xiàn)原理其實很簡單:分析出模塊之間的依賴關系故慈,盡可能的把打散的模塊合并到一個函數(shù)中去,但前提是不能造成代碼冗余框全。
因此只有那些被引用了一次的模塊才能被合并察绷。由于 scope hoisting 需要分析出模塊之間的依賴關系,因此源碼必須采用 ES6 模塊化語句津辩,不然它將無法生效拆撼。
原因和tree shaking一樣容劳。
-
代碼壓縮
所有代碼使用UglifyJsPlugin插件進行壓縮、混淆
css優(yōu)化
將css提取到獨立的文件中
mini-css-extract-plugin
是用于將CSS提取為獨立的文件的插件闸度,對每個包含css的js文件都會創(chuàng)建一個CSS文件竭贩,支持按需加載css和sourceMap
只能用在webpack4中,有如下優(yōu)勢:
- 異步加載
- 不重復編譯莺禁,性能很好
- 容易使用
- 只針對CSS
使用方法:
-
安裝
npm i -D mini-css-extract-plugin
-
在webpack配置文件中引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
-
創(chuàng)建插件對象留量,配置抽離的css文件名,支持placeholder語法
new MiniCssExtractPlugin({ filename: '[name].css' })
-
將原來配置的所有
style-loader
替換為MiniCssExtractPlugin.loader
{ test: /\.css$/, // webpack讀取loader時 是從右到左的讀取, 會將css文件先交給最右側的loader來處理
// loader的執(zhí)行順序是從右到左以管道的方式鏈式調(diào)用
// css-loader: 解析css文件
// style-loader: 將解析出來的結果 放到html中, 使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /.less/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },
// { test: /.s(a|c)ss/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
### 自動添加css前綴
使用`postcss`哟冬,需要用到`postcss-loader`和`autoprefixer`插件
1. 安裝
`npm i -D postcss-loader autoprefixer`
2. 修改webpack配置文件中的loader楼熄,將`postcss-loader`放置在`css-loader`的右邊(調(diào)用鏈從右到左)
```js
{
test: /\.css$/,
// webpack讀取loader時 是從右到左的讀取, 會將css文件先交給最右側的loader來處理
// loader的執(zhí)行順序是從右到左以管道的方式鏈式調(diào)用
// css-loader: 解析css文件
// style-loader: 將解析出來的結果 放到html中, 使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },
// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
項目根目錄下添加
postcss
的配置文件:postcss.config.js
-
在
postcss
的配置文件中使用插件module.exports = { plugins: [require('autoprefixer')] }
開啟css壓縮
需要使用optimize-css-assets-webpack-plugin
插件來完成css壓縮
但是由于配置css壓縮時會覆蓋掉webpack默認的優(yōu)化配置,導致JS代碼無法壓縮浩峡,所以還需要手動把JS代碼壓縮插件導入進來:terser-webpack-plugin
-
安裝
npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin
-
導入插件
const TerserJSPlugin = require('terser-webpack-plugin') const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
-
在webpack配置文件中添加配置節(jié)點
optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], },
tips: webpack4默認采用的JS壓縮插件為:uglifyjs-webpack-plugin
可岂,在mini-css-extract-plugin
上一個版本中還推薦使用該插件,但最新的v0.6中建議使用teser-webpack-plugin
來完成js代碼壓縮红符,具體原因未在官網(wǎng)說明青柄,我們就按照最新版的官方文檔來做即可
js代碼分離
Code Splitting是webpack打包時用到的重要的優(yōu)化特性之一,此特性能夠把代碼分離到不同的 bundle 中预侯,然后可以按需加載或并行加載這些文件致开。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級萎馅,如果使用合理双戳,會極大影響加載時間。
有三種常用的代碼分離方法:
- 入口起點(entry points):使用
entry
配置手動地分離代碼糜芳。 - 防止重復(prevent duplication):使用
SplitChunksPlugin
去重和分離 chunk飒货。 - 動態(tài)導入(dynamic imports):通過模塊的內(nèi)聯(lián)函數(shù)調(diào)用來分離代碼。
手動配置多入口
-
在webpack配置文件中配置多個入口
entry: { main: './src/main.js', other: './src/other.js' }, output: { // path.resolve() : 解析當前相對路徑的絕對路徑 // path: path.resolve('./dist/'), // path: path.resolve(__dirname, './dist/'), path: path.join(__dirname, '..', './dist/'), // filename: 'bundle.js', filename: '[name].bundle.js', publicPath: '/' },
-
在main.js和other.js中都引入同一個模塊峭竣,并使用其功能
main.js
import $ from 'jquery' $(function() { $('<div></div>').html('main').appendTo('body') })
other.js
import $ from 'jquery' $(function() { $('<div></div>').html('other').appendTo('body') })
-
修改package.json的腳本塘辅,添加一個使用dev配置文件進行打包的腳本(目的是不壓縮代碼檢查打包的bundle時更方便)
"scripts": { "build": "webpack --config ./build/webpack.prod.js", "dev-build": "webpack --config ./build/webpack.dev.js" }
運行
npm run dev-build
,進行打包查看打包后的結果皆撩,發(fā)現(xiàn)other.bundle.js和main.bundle.js都同時打包了jQuery源文件
這種方法存在一些問題:
- 如果入口 chunks 之間包含重復的模塊扣墩,那些重復模塊都會被引入到各個 bundle 中。
- 這種方法不夠靈活扛吞,并且不能將核心應用程序邏輯進行動態(tài)拆分代碼呻惕。
抽取公共代碼
tips: Webpack v4以上使用的插件為SplitChunksPlugin
,以前使用的CommonsChunkPlugin
已經(jīng)被移除了滥比,最新版的webpack只需要在配置文件中的optimization
節(jié)點下添加一個splitChunks
屬性即可進行相關配置
-
修改webpack配置文件
optimization: { splitChunks: { chunks: 'all' } },
運行
npm run dev-build
重新打包查看
dist
目錄查看
vendors~main~other.bundle.js
亚脆,其實就是把都用到的jQuery打包到了一個單獨的js中
動態(tài)導入 (懶加載)
webpack4默認是允許import語法動態(tài)導入的,但是需要babel的插件支持盲泛,最新版babel的插件包為:@babel/plugin-syntax-dynamic-import
濒持,以前老版本不是@babel
開頭键耕,已經(jīng)無法使用,需要注意
動態(tài)導入最大的好處是實現(xiàn)了懶加載弥喉,用到哪個模塊才會加載哪個模塊郁竟,可以提高SPA應用程序的首屏加載速度,Vue由境、React棚亩、Angular框架的路由懶加載原理一樣
-
安裝babel插件
npm install -D @babel/plugin-syntax-dynamic-import
-
修改.babelrc配置文件,添加
@babel/plugin-syntax-dynamic-import
插件{ "presets": ["@babel/env"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import" ] }
-
將jQuery模塊進行動態(tài)導入
function getComponent() { return import('jquery').then(({ default: $ }) => { return $('<div></div>').html('main') }) }
-
給某個按鈕添加點擊事件虏杰,點擊后調(diào)用getComponent函數(shù)創(chuàng)建元素并添加到頁面
window.onload = function () { document.getElementById('btn').onclick = function () { getComponent().then(item => { item.appendTo('body') }) } }
SplitChunksPlugin配置參數(shù)
webpack4之后讥蟆,使用SplitChunksPlugin
插件替代了以前CommonsChunkPlugin
而SplitChunksPlugin
的配置,只需要在webpack配置文件中的optimization
節(jié)點下的splitChunks
進行修改即可纺阔,如果沒有任何修改瘸彤,則會使用默認配置
默認的SplitChunksPlugin
配置適用于絕大多數(shù)用戶
webpack 會基于如下默認原則自動分割代碼:
- 公用代碼塊或來自 node_modules 文件夾的組件模塊。
- 打包的代碼塊大小超過 30k(最小化壓縮之前)笛钝。
- 按需加載代碼塊時质况,同時發(fā)送的請求最大數(shù)量不應該超過 5。
- 頁面初始化時玻靡,同時發(fā)送的請求最大數(shù)量不應該超過 3结榄。
以下是SplitChunksPlugin
的默認配置:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 只對異步加載的模塊進行拆分,可選值還有all | initial
minSize: 30000, // 模塊最少大于30KB才拆分
maxSize: 0, // 模塊大小無上限囤捻,只要大于30KB都拆分
minChunks: 1, // 模塊最少引用一次才會被拆分
maxAsyncRequests: 5, // 異步加載時同時發(fā)送的請求數(shù)量最大不能超過5,超過5的部分不拆分
maxInitialRequests: 3, // 頁面初始化時同時發(fā)送的請求數(shù)量最大不能超過3,超過3的部分不拆分
automaticNameDelimiter: '~', // 默認的連接符
name: true, // 拆分的chunk名,設為true表示根據(jù)模塊名和CacheGroup的key來自動生成,使用上面連接符連接
cacheGroups: { // 緩存組配置,上面配置讀取完成后進行拆分,如果需要把多個模塊拆分到一個文件,就需要緩存,所以命名為緩存組
vendors: { // 自定義緩存組名
test: /[\\/]node_modules[\\/]/, // 檢查node_modules目錄,只要模塊在該目錄下就使用上面配置拆分到這個組
priority: -10 // 權重-10,決定了哪個組優(yōu)先匹配,例如node_modules下有個模塊要拆分,同時滿足vendors和default組,此時就會分到vendors組,因為-10 > -20
},
default: { // 默認緩存組名
minChunks: 2, // 最少引用兩次才會被拆分
priority: -20, // 權重-20
reuseExistingChunk: true // 如果主入口中引入了兩個模塊,其中一個正好也引用了后一個,就會直接復用,無需引用兩次
}
}
}
}
};
noParse
在引入一些第三方模塊時臼朗,例如jQuery、bootstrap等蝎土,我們知道其內(nèi)部肯定不會依賴其他模塊视哑,因為最終我們用到的只是一個單獨的js文件或css文件
所以此時如果webpack再去解析他們的內(nèi)部依賴關系,其實是非常浪費時間的誊涯,我們需要阻止webpack浪費精力去解析這些明知道沒有依賴的庫
可以在webpack配置文件的module
節(jié)點下加上noParse
挡毅,并配置正則來確定不需要解析依賴關系的模塊
module: {
noParse: /jquery|bootstrap/
}
IgnorePlugin
在引入一些第三方模塊時,例如moment暴构,內(nèi)部會做i18n國際化處理跪呈,所以會包含很多語言包,而語言包打包時會比較占用空間丹壕,如果我們項目只需要用到中文庆械,或者少數(shù)語言薇溃,可以忽略掉所有的語言包菌赖,然后按需引入語言包
從而使得構建效率更高,打包生成的文件更小
需要忽略第三方模塊內(nèi)部依賴的其他模塊沐序,只需要三步:
- 首先要找到moment依賴的語言包是什么
- 使用IgnorePlugin插件忽略其依賴
- 需要使用某些依賴時自行手動引入
具體實現(xiàn)如下:
-
通過查看moment的源碼來分析:
function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; aliasedRequire('./locale/' + name); getSetGlobalLocale(oldLocale); } catch (e) {} } return locales[name]; }
觀察上方代碼琉用,同時查看moment目錄下確實有l(wèi)ocale目錄堕绩,其中放著所有國家的語言包,可以分析得出:locale目錄就是moment所依賴的語言包目錄
-
使用IgnorePlugin插件來忽略掉moment模塊的locale目錄
在webpack配置文件中安裝插件邑时,并傳入配置項
參數(shù)1:表示要忽略的資源路徑
參數(shù)2:要忽略的資源上下文(所在哪個目錄)
兩個參數(shù)都是正則對象
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
-
使用moment時需要手動引入語言包奴紧,否則默認使用英文
import moment from 'moment' import 'moment/locale/zh-cn' moment.locale('zh-CN') console.log(moment().subtract(6, 'days').calendar())
DllPlugin
在引入一些第三方模塊時,例如vue晶丘、react黍氮、angular等框架,這些框架的文件一般都是不會修改的浅浮,而每次打包都需要去解析它們沫浆,也會影響打包速度,哪怕做拆分滚秩,也只是提高了上線后用戶訪問速度专执,并不會提高構建速度,所以如果需要提高構建速度郁油,應該使用動態(tài)鏈接庫的方式本股,類似于Windows中的dll文件。
借助DllPlugin插件實現(xiàn)將這些框架作為一個個的動態(tài)鏈接庫桐腌,只構建一次拄显,以后每次構建都只生成自己的業(yè)務代碼,可以大大提高構建效率哩掺!
主要思想在于凿叠,將一些不做修改的依賴文件,提前打包嚼吞,這樣我們開發(fā)代碼發(fā)布的時候就不需要再對這部分代碼進行打包盒件,從而節(jié)省了打包時間。
涉及兩個插件:
-
DllPlugin
使用一個單獨webpack配置創(chuàng)建一個dll文件舱禽。并且它還創(chuàng)建一個manifest.json炒刁。DllReferencePlugin使用該json文件來做映射依賴性。(這個文件會告訴我們的哪些文件已經(jīng)提取打包好了)
配置參數(shù):
- context (可選): manifest文件中請求的上下文誊稚,默認為該webpack文件上下文翔始。
- name: 公開的dll函數(shù)的名稱,和output.library保持一致即可里伯。
- path: manifest.json生成的文件夾及名字
-
DllReferencePlugin
這個插件用于主webpack配置城瞎,它引用的dll需要預先構建的依賴關系。
context: manifest文件中請求的上下文疾瓮。
manifest: DllPlugin插件生成的manifest.json
content(可選): 請求的映射模塊id(默認為manifest.content)
name(可選): dll暴露的名稱
scope(可選): 前綴用于訪問dll的內(nèi)容
sourceType(可選): dll是如何暴露(libraryTarget)
將Vue項目中的庫抽取成Dll
-
準備一份將Vue打包成DLL的webpack配置文件
在build目錄下新建一個文件:webpack.vue.js
配置入口:將多個要做成dll的庫全放進來
配置出口:一定要設置library屬性脖镀,將打包好的結果暴露在全局
配置plugin:設置打包后dll文件名和manifest文件所在地
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'development', entry: { vue: [ 'vue/dist/vue.js', 'vue-router' ] }, output: { filename: '[name]_dll.js', path: path.resolve(__dirname, '../dist'), library: '[name]_dll' }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, '../dist/manifest.json') }) ] }
-
在webpack.base.js中進行插件的配置
使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dist/manifest.json') })
-
安裝add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D
-
配置插件自動添加script標簽到HTML中
new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dist/vue_dll.js') })
將React項目中的庫抽取成Dll
-
準備一份將React打包成DLL的webpack配置文件
在build目錄下新建一個文件:webpack.vue.js
配置入口:將多個要做成dll的庫全放進來
配置出口:一定要設置library屬性,將打包好的結果暴露在全局
配置plugin:設置打包后dll文件名和manifest文件所在地
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'development', entry: { react: [ 'react', 'react-dom' ] }, output: { filename: '[name]_dll.js', path: path.resolve(__dirname, '../dist'), library: '[name]_dll' }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, '../dist/manifest.json') }) ] }
-
在webpack.base.js中進行插件的配置
使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dist/manifest.json') })
-
安裝add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D
-
配置插件自動添加script標簽到HTML中
new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dist/react_dll.js') })
Happypack
由于webpack在node環(huán)境中運行打包構建狼电,所以是單線程的模式蜒灰,在打包眾多資源時效率會比較低下弦蹂,早期可以通過Happypack
來實現(xiàn)多進程打包。當然强窖,這個問題只出現(xiàn)在低版本的webpack中凸椿,現(xiàn)在的webpack性能已經(jīng)非常強勁了,所以無需使用Happypack也可以實現(xiàn)高性能打包
引用官網(wǎng)原文:
Maintenance mode notice
My interest in the project is fading away mainly because I'm not using JavaScript as much as I was in the past. Additionally, Webpack's native performance is improving and (I hope) it will soon make this plugin unnecessary.
See the FAQ entry about Webpack 4 and thread-loader.
Contributions are always welcome. Changes I make from this point will be restricted to bug-fixing. If someone wants to take over, feel free to get in touch.
Thanks to everyone who used the library, contributed to it and helped in refining it!!!
由此可以看出作者已經(jīng)發(fā)現(xiàn)翅溺,webpack的性能已經(jīng)強大到不需要使用該插件了脑漫,而且小項目使用該插件反而會導致性能損耗過大,因為開啟進程是需要耗時的
使用方法:
-
安裝插件
npm i -D happypack
-
在webpack配置文件中引入插件
const HappyPack = require('happypack')
-
修改loader的配置規(guī)則
{ test: /.js$/, use: { loader: 'happypack/loader' }, include: path.resolve(__dirname, '../src'), exclude: /node_modules/ }
-
配置插件
new HappyPack({ loaders: [ 'babel-loader' ] })
-
運行打包命令
npm run build
瀏覽器緩存
在做了眾多代碼分離的優(yōu)化后咙崎,其目的是為了利用瀏覽器緩存窿撬,達到提高訪問速度的效果,所以構建項目時做代碼分割是必須的叙凡,例如將固定的第三方模塊抽離劈伴,下次修改了業(yè)務代碼,重新發(fā)布上線不重啟服務器握爷,用戶再次訪問服務器就不需要再次加載第三方模塊了
但此時會遇到一個新的問題跛璧,如果再次打包上線不重啟服務器,客戶端會把以前的業(yè)務代碼和第三方模塊同時緩存新啼,再次訪問時依舊會訪問緩存中的業(yè)務代碼追城,所以會導致業(yè)務代碼也無法更新
需要在output節(jié)點的filename中使用placeholder語法,根據(jù)代碼內(nèi)容生成文件名的hash:
output: {
// path.resolve() : 解析當前相對路徑的絕對路徑
// path: path.resolve('./dist/'),
// path: path.resolve(__dirname, './dist/'),
path: path.join(__dirname, '..', './dist/'),
// filename: 'bundle.js',
filename: '[name].[contenthash:8].bundle.js',
publicPath: '/'
},
之后每次打包業(yè)務代碼時燥撞,如果有改變座柱,會生成新的hash作為文件名,瀏覽器就不會使用緩存了物舒,而第三方模塊不會重新打包生成新的名字色洞,則會繼續(xù)使用緩存
打包分析
項目構建完成后,需要通過一些工具對打包后的bundle進行分析冠胯,通過分析才能總結出一些經(jīng)驗火诸,官方推薦的分析方法有兩步完成:
-
使用
--profile --json
參數(shù),以json格式來輸出打包后的結果到某個指定文件中webpack --profile --json > stats.json
-
將stats.json文件放入工具中進行分析
官方推薦的其他四個工具:
其中webpack-bundle-analyzer是一個插件荠察,可以以插件的方式安裝到項目中
Prefetching和Preloading
在優(yōu)化訪問性能時置蜀,除了充分利用瀏覽器緩存之外,還需要涉及一個性能指標:coverage rate(覆蓋率)
可以在Chrome瀏覽器的控制臺中按:ctrl + shift + p悉盆,查找coverage盯荤,打開覆蓋率面板
開始錄制后刷新網(wǎng)頁,即可看到每個js文件的覆蓋率焕盟,以及總的覆蓋率
想提高覆蓋率滤奈,需要盡可能多的使用動態(tài)導入棚瘟,也就是懶加載功能,將一切能使用懶加載的地方都是用懶加載,這樣可以大大提高覆蓋率
但有時候使用懶加載會影響用戶體驗瑰谜,所以可以在懶加載時使用魔法注釋:Prefetching,是指在首頁資源加載完畢后兔簇,空閑時間時塘装,將動態(tài)導入的資源加載進來,這樣即可以提高首屏加載速度备图,也可以解決懶加載可能會影響用戶體驗的問題灿巧,一舉兩得!
function getComponent() {
return import(/* webpackPrefetch: true */ 'jquery').then(({ default: $ }) => {
return $('<div></div>').html('我是main')
})
}
(備注說明:學習筆記源-大地)