webpack 打包
以下針對(duì) webpack 為 5 的情況,所有依賴(lài)的版本如下:
快速上手
const path = require('path')
module.exports = {
// mode:工作模式:development俺祠, production公给, null
// 不設(shè)置默認(rèn)為 production。
// production 模式會(huì)自動(dòng)啟用一些優(yōu)化插件蜘渣,比如壓縮淌铐,打包結(jié)果無(wú)法閱讀
// development 模式會(huì)自動(dòng)優(yōu)化打包速度,添加調(diào)試過(guò)程中的輔助
// null 模式運(yùn)行最原始狀態(tài)的打包蔫缸,不做任何額外的處理
mode: 'none',
// entry:入口文件路徑腿准。 如果是相對(duì)路徑的話(huà), ./ 不能省略
entry: './src/main.js',
// output: 輸出文件路徑拾碌,是一個(gè)對(duì)象
output: {
// filename:輸出文件名
filename: 'bundle.js',
// 輸出文件路徑吐葱,必須為絕對(duì)路徑,所以使用 path.join(__dirname, xxx)
path: path.join(__dirname, 'dist'),
publicPath: 'dist/', // 打包過(guò)后的文件最終位置
},
}
Loader
在我們的項(xiàng)目中校翔,我們需要處理的不僅僅是 js 的代碼弟跑,我們可以使用加載器對(duì)不同類(lèi)型的文件進(jìn)行處理。Loader 是實(shí)現(xiàn)前端模塊化的核心防症,借助于 Loader 可以加載任何類(lèi)型的資源孟辑。
通過(guò)配置 module 來(lái)配置 loader。rules 為規(guī)則配置蔫敲。
可以將 Loader 分為幾個(gè)類(lèi)型:
- 編譯轉(zhuǎn)換類(lèi)饲嗽。例如 css-loader,將 css 代碼轉(zhuǎn)換為 js 進(jìn)行工作奈嘿。
- 文件操作類(lèi)貌虾。例如 file-loader,對(duì)文件進(jìn)行拷貝尽狠,再將文件路徑向外導(dǎo)出。
- 代碼檢查類(lèi)巫财。統(tǒng)一代碼風(fēng)格平项,從而提高代碼質(zhì)量悍及,不會(huì)修改生產(chǎn)環(huán)境的代碼心赶。例如 eslint-loader。
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/',
},
module: {
rules: [
{
test: /.js$/,
use: {
// es6+ 新特性可以使用 babel-loader 進(jìn)行編譯轉(zhuǎn)換
// webpack 只是打包工具椭符,加載器可以用來(lái)編譯轉(zhuǎn)換代碼
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
// 匹配打包過(guò)程中遇到的文件路徑
test: /.css$/,
// 如果配置了多個(gè) loader销钝,執(zhí)行時(shí)從后往前執(zhí)行
use: [
'style-loader', // 將 css-loader 轉(zhuǎn)換過(guò)后的結(jié)果通過(guò) style 標(biāo)簽的形式追加到頁(yè)面上
'css-loader' // 將css文件轉(zhuǎn)換為js模塊蒸健,
]
},
{
test: /\.(png|jpg|gif)$/i,
// 小文件使用 Data URLs似忧,減少請(qǐng)求次數(shù)
// 大文件單獨(dú)提取存放丈秩,提高加載速度
// use: 'file-loader', // 文件資源加載器
// use: 'url-loader' // 將圖片轉(zhuǎn)換為 Data Urls,圖片將被轉(zhuǎn)為 base64
// 將 use 設(shè)為對(duì)象癣籽,loader 設(shè)為 url-loader,并設(shè)置一個(gè) limit
// 此時(shí)文件小于 10kb 使用 url-loader,大于10kb則默認(rèn)使用 file-loader
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10 KB
// esModule: false, // 解決 html 中載入圖片導(dǎo)致的[Object Module]問(wèn)題
}
}
]
},
{
test: /\.html$/i,
loader: 'html-loader',
options: {
esModule: false, // 禁用 es modules 語(yǔ)法
sources: {
list: [
'...',
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
},
]
}
}
Plugin
插件機(jī)制是 webpack 中另外一個(gè)核心特性筷狼,目的是為了增強(qiáng) webpack 在項(xiàng)目自動(dòng)化方面的能力埂材。Loader 專(zhuān)注實(shí)現(xiàn)資源模塊加載俏险,從而實(shí)現(xiàn)整體項(xiàng)目打包,而 Plugin 是為了解決除資源加載以外其他的自動(dòng)化工作裤唠。eg:
- 在打包之前清除上一次的 dist 目錄
- 拷貝靜態(tài)文件至輸出目錄
- 壓縮輸出代碼
clean-webpack-plugin:用來(lái)在打包前清除 dist 的插件种蘸。
html-webpack-plugin:自動(dòng)生成使用 bundle.js 的 HTML航瞭。由于我們的HTML都是通過(guò)硬編碼的方式放在根目錄下坦辟,發(fā)布的時(shí)候需要同時(shí)發(fā)布這個(gè)HTML文件锉走,而且還要確保資源文件路徑正確挪蹭,需要手動(dòng)修改。通過(guò)這個(gè)插件就可以解決這個(gè)問(wèn)題冬骚。webpack 打包的時(shí)候知道自己生成了多少 bundle只冻,將 bundle 自動(dòng)放入 HTML 文件中喜德,這樣 html 也輸出到了 dist 目錄垮媒,而且對(duì) bundle 的引入是注入的睡雇,能夠確保路徑正確它抱。
copy-webpack-plugin: 拷貝文件。
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
// mode:工作模式:development毁嗦, production胚膊, null
// 不設(shè)置默認(rèn)為 production。
// production 模式會(huì)自動(dòng)啟用一些優(yōu)化插件,比如壓縮垢揩,打包結(jié)果無(wú)法閱讀
// development 模式會(huì)自動(dòng)優(yōu)化打包速度庶橱,添加調(diào)試過(guò)程中的輔助
// null 模式運(yùn)行最原始狀態(tài)的打包兔毙,不做任何額外的處理
mode: 'none',
// entry:入口文件路徑。 如果是相對(duì)路徑的話(huà)夕晓, ./ 不能省略
entry: './src/main.js',
// output: 輸出文件路徑,是一個(gè)對(duì)象
output: {
// filename:輸出文件名
filename: 'bundle.js',
// 輸出文件路徑析既,必須為絕對(duì)路徑眼坏,所以使用 path.join(__dirname, xxx)
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/', // 打包過(guò)后的文件最終位置
},
// 使用加載器對(duì)不同類(lèi)型的文件進(jìn)行處理
// Loader 是實(shí)現(xiàn)前端模塊化的核心酸些,借助于 Loader 就可以加載任何類(lèi)型的資源
module: {
// 規(guī)則配置
rules: [
{
test: /.js$/,
exclude: /(node_modules)|ejs$/,
use: {
// es6+ 新特性可以使用 babel-loader 進(jìn)行編譯轉(zhuǎn)換
// webpack 只是打包工具魄懂,加載器可以用來(lái)編譯轉(zhuǎn)換代碼
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
// 匹配打包過(guò)程中遇到的文件路徑
test: /.css$/,
// 如果配置了多個(gè) loader市栗,執(zhí)行時(shí)從后往前執(zhí)行
use: [
'style-loader', // 將 css-loader 轉(zhuǎn)換過(guò)后的結(jié)果通過(guò) style 標(biāo)簽的形式追加到頁(yè)面上
'css-loader' // 將css文件轉(zhuǎn)換為js模塊,
]
},
{
test: /\.(png|jpg|gif)$/i,
// 小文件使用 Data URLs智厌,減少請(qǐng)求次數(shù)
// 大文件單獨(dú)提取存放铣鹏,提高加載速度
// use: 'file-loader', // 文件資源加載器
// use: 'url-loader' // 將圖片轉(zhuǎn)換為 Data Urls,圖片將被轉(zhuǎn)為 base64
// 將 use 設(shè)為對(duì)象诚卸,loader 設(shè)為 url-loader,并設(shè)置一個(gè) limit
// 此時(shí)文件小于 10kb 使用 url-loader,大于10kb則默認(rèn)使用 file-loader
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10 KB
// esModule: false, // 解決 html 中載入圖片導(dǎo)致的[Object Module]問(wèn)題
}
}
]
},
{
test: /\.html$/i,
loader: 'html-loader',
options: {
esModule: false, // 禁用 es modules 語(yǔ)法
sources: {
list: [
'...',
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
},
{
test: /.md$/,
use: [
'html-loader',
'./markdown-loader'
]
}
]
},
// plugins: 用來(lái)配置插件
// 絕大多數(shù)插件都是導(dǎo)出一個(gè)類(lèi)型
// 使用插件就是創(chuàng)建一個(gè)這個(gè)類(lèi)型的實(shí)例合溺,將實(shí)例放入 Plugin 數(shù)組中
plugins: [
// clean-webpack-plugin 是用來(lái)在打包前清除 dist 的插件
new CleanWebpackPlugin(),
// html-webpack-plugin 是自動(dòng)生成使用 bundle.js 的 HTML
// html-webpack-plugin 中也可以傳入一個(gè) options 作為配置選項(xiàng)
// 用于生成 index.html
new HtmlWebpackPlugin({
meta: { // 設(shè)置元數(shù)據(jù)標(biāo)簽
viewport: 'width=device-width'
},
title: 'webpack plugin sample', // 標(biāo)題
// html-webpack-plugin 中的 <%= htmlWebpackPlugin.options.title %> 會(huì)被 html-loader 當(dāng)做字符串處理棠赛,所以會(huì)不生效睛约,需要將 html 模板改為 ejs 類(lèi)型
// 如果使用了 babel-loader,就會(huì)去跑 ejs 文件哲身,會(huì)報(bào)錯(cuò)勘天,所以要在 babel-loader 設(shè)置忽略 ejs 文件
template: 'src/index.ejs' // 模板捉邢,根據(jù)模板生成頁(yè)面
}),
// html-webpack-plugin 可以用于生成多個(gè) html 文件
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
// copy-webpack-plugin: 拷貝
// 開(kāi)發(fā)階段最好不要使用這個(gè)插件
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: 'public' }
]
})
]
}
webpack-dev-server
安裝依賴(lài)伏伐,然后執(zhí)行 yarn webpack serve --open
會(huì)自動(dòng)打開(kāi)瀏覽器并打開(kāi) watch 模式監(jiān)聽(tīng)頁(yè)面變化刷新頁(yè)面秘案。
webpack-dev-server 默認(rèn)會(huì)把所有可以打包的文件放到內(nèi)存里(不會(huì)寫(xiě)入磁盤(pán))。一般在開(kāi)發(fā)階段不需要將靜態(tài)資源打包茬缩,還可以通過(guò)在 devServer 中配置 contentBase吼旧,設(shè)置為靜態(tài)資源的目錄,開(kāi)發(fā)階段就可以訪(fǎng)問(wèn)到靜態(tài)資源掂为。
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/', // 打包過(guò)后的文件最終位置
},
devServer:{
// 靜態(tài)資源目錄勇哗,可以是字符串或數(shù)組欲诺,也就是說(shuō)可以配置一個(gè)或多個(gè)
contentBase:'./public'
},
}
HMR - 熱更新
自動(dòng)刷新頁(yè)面會(huì)導(dǎo)致用戶(hù)的操作狀態(tài)丟失扰法,這個(gè)時(shí)候我們可以使用 HMR - 熱更新(熱替換)毅厚,熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中吸耿,在頁(yè)面同步更新的同時(shí)保持應(yīng)用的運(yùn)行狀態(tài)不受影響。它極大程度的提高了開(kāi)發(fā)者的工作效率锤岸。
HMR 已經(jīng)集成在 webpack-dev-server 中是偷,不需要再引入依賴(lài)。
- 首先引入 webpack
const webpack = require('webpack')
- 在 devServer 中配置 hot:true
devServer: {
hot: true
}
- 在 plugins 中配置插件
// 熱更新
new webpack.HotModuleReplacementPlugin()
然后運(yùn)行項(xiàng)目馋评,發(fā)現(xiàn)修改 css 時(shí)實(shí)現(xiàn)了熱更新留特,而修改 js 時(shí)沒(méi)有實(shí)現(xiàn)熱更新玛瘸。這是因?yàn)樾枰謩?dòng)處理JS更新后的熱更新邏輯糊渊。在成熟的項(xiàng)目框架中是不需要我們手動(dòng)來(lái)處理的,因?yàn)榭蚣芤呀?jīng)為我們處理了贺喝。
Proxy - 代理
devServer: {
// 可以是字符串或數(shù)組躏鱼,也就是說(shuō)可以配置一個(gè)或多個(gè)
contentBase: './public',
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/user
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/user
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作為請(qǐng)求 github 的主機(jī)名
changeOrigin: true
}
}
},
Source Map
通過(guò) webpack 打包后的項(xiàng)目染苛,我們想要調(diào)試或者定位錯(cuò)誤信息就會(huì)變的困難篡帕,可以用過(guò) Source Map 來(lái)解決镰烧。它是一個(gè)源代碼和轉(zhuǎn)換后的代碼的映射,一個(gè)轉(zhuǎn)換過(guò)后的代碼茉唉,通過(guò) Source Map 的逆向解析度陆,就可以得到源代碼懂傀。
先簡(jiǎn)單使用一下:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/', // 打包過(guò)后的文件最終位置
},
// 在你的配置中加入這行代碼
devtool: 'source-map',
}
報(bào)錯(cuò)信息就會(huì)現(xiàn)實(shí)文件名蹬蚁,點(diǎn)擊文件名就會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的代碼,而且可以在這里進(jìn)行斷點(diǎn)調(diào)試贝乎。
devtool 不僅有 source-map 的值览效,還有很多的值虫几,他們的區(qū)別有編譯速度,重新編譯速度衡招,適用環(huán)境等。
開(kāi)發(fā)環(huán)境(cheap-module-eval-source-map):
- 能夠定位到行
- 定位到的文件會(huì)以真實(shí)代碼的樣子顯示(cheap-eval-source-map 會(huì)顯示轉(zhuǎn)譯成es5 的樣子空执,不好和源代碼定位)
- 雖然首次打包速度慢辨绊,但是重寫(xiě)打包相對(duì)較快
生產(chǎn)環(huán)境(nosources-source-map):
- Source Map 會(huì)暴露源代碼
- 可以找到報(bào)錯(cuò)源代碼的位置
webpack 不同環(huán)境下的配置
webpack.config.js 中的 module.exports 可以導(dǎo)出一個(gè)對(duì)象匹表,也可以導(dǎo)出一個(gè)數(shù)組袍镀,里邊為多組配置,同樣也可以導(dǎo)出一個(gè)函數(shù)绸吸,函數(shù)的參數(shù)是 env(環(huán)境)和 argv(運(yùn)行 cli 過(guò)程中傳入的所有參數(shù))锦茁。
module.exports = (env, argv) => {
// 這里放置所有基本配置
const config = {
...
}
if(env === 'production'){
config.mode = 'production'
config.devtool = false
config.plugins = {
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
}
}
return config
}
這樣的話(huà)码俩,執(zhí)行 yarn webpack 默認(rèn)打包的還是dev的配置歼捏,執(zhí)行 yarn webpack --env production,就相當(dāng)于給 傳遞了參數(shù) env 為 productioin笨篷,從而實(shí)現(xiàn) prod的打包冕屯。
但是更多情況下安聘,項(xiàng)目比較大浴韭,都是通過(guò)配置不同文件來(lái)實(shí)現(xiàn)的脯宿。3個(gè)文件,一個(gè)公共配置榴芳,一個(gè)dev配置跺撼,一個(gè)prod配置歉井,在dev 和prod配置文件中將公共配置文件引入,使用webpack 的依賴(lài) 'webpack-merge', 將 自己的配置 merge 到公共配置躏嚎,這樣向plugins 這種數(shù)組結(jié)構(gòu)的也可以 merge 過(guò)去卢佣,并且不會(huì)將原來(lái)的配置完全覆蓋菜谣,而是合并處理。
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode:'production',
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: 'public' }
]
}),
]
})
DefinePlugin
webpack 為我們提供了很多開(kāi)箱即用的插件媳危。
eg: DefinePlugin
為代碼注入全局成員待笑,自動(dòng)啟用暮蹂,會(huì)為全局注入 process.env.NODE_ENV 常量,用來(lái)判斷運(yùn)行環(huán)境荆陆。
適用方法:
new webpack.DefinePlugin({
// 這里的 value 是一個(gè) js 代碼片段集侯,所以傳入的是 字符串,這個(gè)字符串里邊是一個(gè)字符串
// 也可以寫(xiě)成 JSON.stringify('https://api.example.com')
// API_BASE_URL:'"https://api.example.com"'
API_BASE_URL: JSON.stringify('https://api.example.com')
})
然后在全局打印 API_BASE_URL 變量浓体,可以拿到它的值命浴。
Tree Shaking
Tree Shaking 是在打包時(shí)自動(dòng)去除項(xiàng)目中一些沒(méi)有引用的東西生闲。例如一個(gè)函數(shù)中 return 后的操作會(huì)被移除月幌,export 出去的成員沒(méi)有引用會(huì)被移除等飞醉。
Tree Shaking 在生產(chǎn)環(huán)境打包過(guò)程中會(huì)自動(dòng)開(kāi)啟缅帘。在其他環(huán)境难衰,需要自己手動(dòng)配置:
module.export = {
mode:'none',
...
// optimization: 用來(lái)集中配置 webpack 的優(yōu)化功能
optimization: {
// 模塊只導(dǎo)出被使用的成員
usedExports: true,
// 盡可能合并每一個(gè)模塊到一個(gè)函數(shù)中
// concatenateModules: true,
// 壓縮輸出結(jié)果
minimize: true
}
}
Tree Shaking 只在 ES Module 語(yǔ)法生效盖袭,但是如果項(xiàng)目打包配置了 babel-loader,它可能會(huì)將 ES Module 轉(zhuǎn)換為 CommonJS 規(guī)范弟塞,那么 Tree Shaking 將不會(huì)生效决记。不過(guò)最新版本的 babel-loader 已經(jīng)自動(dòng)關(guān)閉了ES Module 轉(zhuǎn)換的插件倍踪,所以不會(huì)出現(xiàn)這個(gè)問(wèn)題索昂,但是為了保險(xiǎn)起見(jiàn)扩借,可以對(duì) babel-loader 進(jìn)行一些配置潮罪。
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加載模塊時(shí)已經(jīng)轉(zhuǎn)換了 ESM,則會(huì)導(dǎo)致 Tree Shaking 失效
// modules 表示轉(zhuǎn)換為什么模式秉宿, 設(shè)為 false 代表不會(huì)將 ES Module 轉(zhuǎn)換為 CommonJS描睦,auto 默認(rèn)配置最新版中也會(huì)關(guān)閉轉(zhuǎn)換
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默認(rèn)配置忱叭,也就是 auto今艺,這樣 babel-loader 會(huì)自動(dòng)關(guān)閉 ESM 轉(zhuǎn)換
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
concatenateModules - 合并模塊
webpack 打包后將一個(gè)模塊打包成一個(gè)函數(shù),就會(huì)有多個(gè)模塊函數(shù)撵彻。通過(guò) concatenateModules 可以合并模塊陌僵。=创坞,載配合 minimize 進(jìn)行壓縮题涨,就會(huì)大大較少體積。
sideEffects - 副作用
它允許我們通過(guò)配置的方式標(biāo)識(shí)我們的代碼是否有副作用巡雨,從而為 Tree Shaking 提供更大的壓縮空間鸯隅。
副作用:模塊執(zhí)行時(shí)除了導(dǎo)出成員之外所做的事情。
如上圖炕舵,一般我們?cè)趯?xiě)組件的時(shí)候咽筋,會(huì)有多個(gè)組件文件奸攻,然后在 components/index 中統(tǒng)一導(dǎo)入再導(dǎo)出虱痕,但是在 index 中我們可能只引入一個(gè)組件,但是因?yàn)橐肓?'./components', 而 ‘components/index’ 又引入了所有模塊赎瑰,導(dǎo)致所有組件都被加載執(zhí)行。sideEffects 就可以解決這個(gè)問(wèn)題刮刑。
我們?cè)?webpack.config.js 中的 optimization 中設(shè)置 sideEffects:true 來(lái)開(kāi)啟這個(gè)屬性(這個(gè)屬性在生產(chǎn)環(huán)境會(huì)自動(dòng)開(kāi)啟)
optimization: {
sideEffects: true,
}
webpack 在打包的時(shí)候就會(huì)檢查 package.json 中是否有 sideEffects 的標(biāo)識(shí)穴翩,以此來(lái)判斷是否有副作用荸哟,我們來(lái)設(shè)置為 false鞍历, 表示沒(méi)有副作用,這樣打包后,沒(méi)有用到的組件就不會(huì)被打包進(jìn)來(lái)了笋轨。
使用 sideEffects 的前提是確保你的代碼真的沒(méi)有副作用爵政,否則在打包時(shí)就會(huì)誤刪掉有副作用的代碼钾挟。比如引入的 css 文件,或者引入一個(gè)擴(kuò)展的對(duì)象的原型方法的 js 文件徽千,它們沒(méi)有導(dǎo)出任何成員,所以在引入的時(shí)候也不用導(dǎo)入什么成員百框,但是在引入后可以使用它們提供的方法铐维,這就屬于這個(gè) css 或 js 的副作用慎菲。這個(gè)時(shí)候還標(biāo)識(shí)沒(méi)有副作用的話(huà)露该,這些文件就不會(huì)被打包,這時(shí)可以在 Package.json 中關(guān)掉 sideEffects 或者設(shè)置哪些文件有副作用闸拿,這樣 webpack 就不會(huì)忽略這些文件了新荤。
代碼分割
通過(guò) webpack 實(shí)現(xiàn)前端整體模塊化的優(yōu)勢(shì)固然很明顯苛骨,但是它同樣存在一些弊端苟呐,那就是我們項(xiàng)目中所有代碼最終都會(huì)被打包到一起。如果我們的應(yīng)用非常復(fù)雜严衬,模塊非常多请琳,bundle 體積就會(huì)特別的大赠幕,而大多數(shù)時(shí)候并不是每個(gè)模塊在啟動(dòng)時(shí)都是必要的榕堰,但是這些又被打包到一起,就必須把所有模塊都加載進(jìn)來(lái)才能使用圾旨。應(yīng)用運(yùn)行在瀏覽器端,這就意味著會(huì)浪費(fèi)掉很多的流量和帶寬勇蝙。所以我們需要把打包結(jié)果按照一定的規(guī)則分離到多個(gè) bundle 中味混,然后根據(jù)應(yīng)用的運(yùn)行需要按需加載诫惭。這樣就可以大大提高應(yīng)用的響應(yīng)效率以及運(yùn)行速度夕土。
前面說(shuō)過(guò) webpack 就是把我們項(xiàng)目中散落的模塊打包到一起從而提高運(yùn)行效率,這里又說(shuō)應(yīng)該分離開(kāi)來(lái)角溃,這兩個(gè)是不是自相矛盾呢减细?
其實(shí)不是的赢笨,只是物極必反。資源太大了也不行萧吠,太碎了也不行纸型。
webpack 支持一種分包的功能梅忌,也就是代碼分割。
Code Splitting - 代碼分包/代碼分割
- 多入口打包
- 動(dòng)態(tài)導(dǎo)入
多入口打包
多入口打包一般適用于傳統(tǒng)的多頁(yè)應(yīng)用程序。一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)打包入口蹋笼,對(duì)于頁(yè)面公共部分再單獨(dú)提取。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
// 將 entry 配置成一個(gè)對(duì)象
// 一個(gè)屬性就是打包的一路入口
// 屬性名就是入口名稱(chēng)圾笨,值就是入口路徑
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
// 輸出文件名動(dòng)態(tài)輸出
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
// 由于打包后會(huì)將所有的 bundle 載入html擂达,但是我們只需要載入對(duì)應(yīng)的那個(gè) bundle 載入對(duì)應(yīng)的 html
// chunks 指定載入的 bundle
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
提取公共模塊
此時(shí) index 和 album 模塊中都會(huì)引入一些公共模塊板鬓,如果引入一些大型的模塊俭令,比如vue 等部宿,就會(huì)讓每個(gè)模塊體積都很大,所以我們需要把公共模塊提取出來(lái)赫蛇。
optimizatioin: {
splitChunks: {
// 表示會(huì)把所有的公共模塊都提取到單獨(dú)的 bundle 中
chunks: 'all'
}
},
動(dòng)態(tài)導(dǎo)入
按需加載是我們開(kāi)發(fā)瀏覽器應(yīng)用一個(gè)常見(jiàn)的需求悟耘,一般我們常說(shuō)的按需加載指的是加載數(shù)據(jù)拷况,這里所說(shuō)的按需加載指的是我們應(yīng)用在運(yùn)行過(guò)程中需要用到某個(gè)模塊時(shí),再加載這個(gè)模塊粟誓。這種方式可以極大的節(jié)省我們的帶寬和流量鹰服。webpack 中支持使用動(dòng)態(tài)導(dǎo)入的方式實(shí)現(xiàn)按需加載揽咕,而且所有動(dòng)態(tài)導(dǎo)入的模塊都會(huì)被自動(dòng)的提取到單獨(dú)的 bundle 中亲善,從而實(shí)現(xiàn)分包。對(duì)比與多入口的方式顿肺,動(dòng)態(tài)導(dǎo)入更加靈活,可以通過(guò)代碼的邏輯去控制需不需要加載某個(gè)模塊旷祸,或者什么時(shí)候需要加載某個(gè)模塊讼昆。而我們分包的目的中就有很重要的一點(diǎn)是要讓模塊實(shí)現(xiàn)按需加載,從而提高應(yīng)用的響應(yīng)速度闰围。
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
// 將以上直接導(dǎo)入的方式改為這種動(dòng)態(tài)導(dǎo)入的方式
// 這是ES Module 提供的動(dòng)態(tài)導(dǎo)入辫诅,返回一個(gè) Promise 對(duì)象炕矮,用 then 方法可以接受返回值
// /* webpackChunkName: 'components' */ 為魔法注釋?zhuān)绻麤](méi)有魔法注釋?zhuān)瑢?dǎo)出的文件將會(huì)以序號(hào)命名
// 加上魔法注釋可以為組件起一個(gè)名字者冤,如果名字相同,將會(huì)打包到同一個(gè) bundle
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
只需按照 ES Module 的按需加載的方式導(dǎo)入邢滑,webpack 無(wú)需處理困后,就可以自動(dòng)分包摇予。
MiniCssExtractPlugin
MiniCssExtractPlugin 是一個(gè)可以將 css 從打包結(jié)果提取出來(lái)的插件吗跋,通過(guò)這個(gè)插件可以實(shí)現(xiàn) css 的按需加載。
- 首先引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
- 然后在 plugins 中加入
new MiniCssExtractPlugin()
- 在 loader 中我們之前是先通過(guò) css-loader 去解析酗宋,然后交給 style-loader 將樣式通過(guò) style 標(biāo)簽注入蜕猫。使用 MiniCssExtractPlugin 我們是將樣式放入文件中通過(guò) Link 的方式引入哎迄,也就不需要使用 style-loader稀颁,使用 MiniCssExtractPlugin.loader 的方式注入。
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過(guò) style 標(biāo)簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
需要注意的是租漂,如果樣式文件體積不是很大的話(huà)颊糜,提取到單個(gè)文件中效果可能適得其反衬鱼。如果 css 體積超過(guò)了 150kb 左右,才需要考慮是否將它提取到單獨(dú)文件中蒜胖,否則的話(huà) css 嵌入到代碼當(dāng)中減少了一次請(qǐng)求效果可能會(huì)更好抛蚤。
OptimizeCssAssetsWebpackPlugin
當(dāng)我們打包生產(chǎn)的包時(shí)會(huì)發(fā)現(xiàn)岁经,剛剛提取出來(lái)的 css 文件沒(méi)有被壓縮,這是因?yàn)?webpack 提供的壓縮只針對(duì) js 文件樊拓,想要對(duì) css 文件進(jìn)行壓縮就需要借助插件塘慕,苍糠,webpack 官方推薦了一個(gè)插件 - ss-assets-webpack-plugin。
在 plugins 中加入 new mizeCssAssetsWebpackPlugin()
拥娄,打包后css 文件也被壓縮了瞳筏。
但是在官方文檔中會(huì)發(fā)現(xiàn)姚炕,這個(gè)插件并不是配置在 plugins 屬性中丢烘,而是在 optimization 的 minimizer 屬性中播瞳。這是因?yàn)槿绻渲迷?plugins 下赢乓,這個(gè)插件在任何情況下都會(huì)正常工作石窑,而配置在 minimizer 中,只會(huì)在 minimize 這樣一個(gè)特性開(kāi)啟是才會(huì)工作躺屁,所以webpack 建議壓縮類(lèi)的插件應(yīng)該配置在 minimizer 中经宏,以便于可以通過(guò) minimize 這個(gè)選項(xiàng)統(tǒng)一控制烛恤。
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},
執(zhí)行 yarn webpack --mode production
可以發(fā)現(xiàn),css 文件壓縮了苹熏,但是這時(shí)又沒(méi)有壓縮了轨域,這是因?yàn)樵O(shè)置了 minimizer 數(shù)組杀餐,webpack 認(rèn)為如果配置了這個(gè)數(shù)組,就是要用自定義壓縮插件枉长,內(nèi)部的 js 壓縮器就會(huì)被覆蓋掉必峰,所以我們需要手動(dòng)再把它添加回來(lái)钻蹬。
yarn add terser-webpack-plugin --dev
const TerserWebpackPlugin = require('terser-webpack-plugin')
再將這個(gè)插件手動(dòng)添加進(jìn) minimizer
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
這時(shí)在生產(chǎn)模式打包,js 和 css 文件都可以被正常壓縮了肝匆。
輸出文件名 Hash
一般我們?cè)诓渴鹎岸速Y源文件時(shí),都會(huì)啟用服務(wù)器的靜態(tài)資源緩存枯怖。這樣的話(huà)能曾,對(duì)于用戶(hù)的瀏覽器而言借浊,可以緩存住我們應(yīng)用中的靜態(tài)資源蚂斤,后續(xù)就不再需要請(qǐng)求服務(wù)器得到靜態(tài)資源文件了槐沼。整體應(yīng)用的響應(yīng)速度就有一個(gè)大幅度提升。不過(guò)也會(huì)有一些小小的問(wèn)題纽窟,如果緩存時(shí)間設(shè)置過(guò)短臂港,效果不是特別明顯视搏,如果過(guò)期時(shí)間設(shè)置的比較長(zhǎng),在這個(gè)過(guò)程中應(yīng)用重新部署佑力,那這些更新將無(wú)法更新到客戶(hù)端打颤。所以在生產(chǎn)模式下漓滔,我們建議給文件名設(shè)置 hash 值,一旦資源文件改變反肋,文件名稱(chēng)也可以一起變化石蔗,對(duì)于客戶(hù)端而言,全新的文件名就是全新的請(qǐng)求养距,也就沒(méi)有緩存的問(wèn)題,這樣就可以把服務(wù)端的緩存時(shí)間設(shè)置的特別長(zhǎng)肾胯,也就不用擔(dān)心文件更新過(guò)后的問(wèn)題敬肚。
webpack 中 output 中的 filename 和插件中的 filename 都支持設(shè)置 hash 值束析。它們支持3中 hash,效果各不相同弄慰。
- 首先是最普通的 hash陆爽,可以通過(guò) [hash] 拿到扳缕。這種 hash 是整個(gè)項(xiàng)目級(jí)別的,也就是這個(gè)項(xiàng)目中有任何一個(gè)改動(dòng)贡必,所有的 hash 值都會(huì)改變仔拟。
- chunkhash:在打包過(guò)程中飒赃,同一路的打包 chunkhash 都是相同的载佳。比如動(dòng)態(tài)導(dǎo)入的文件是一路 chunk。
這是兩路 chunk挠乳。
一個(gè)文件改變睡扬,同一個(gè) chunk 下的 chunkhash 都會(huì)改變,如果文件被別的文件引入屎开,那引入的那個(gè)文件 chunkhash 也會(huì)被動(dòng)改變马靠。相比于普通 hash甩鳄,chunkhash 更精確。
- contenthash: 文件級(jí)別的hash第晰。文件修改時(shí)對(duì)應(yīng)的 bundle 的 contenthash 修改彬祖,引入它的文件也被動(dòng)修改储笑。
contenthash 是解決緩存問(wèn)題最好的方式突倍,因?yàn)樗_的定位到了文件級(jí)別的 hash盆昙。
如果覺(jué)得 20 位的 hash 太長(zhǎng),可以指定長(zhǎng)度秕磷,[contenthash:8] 指定 hash 長(zhǎng)度為8炼团。