webpack 打包
以下針對(duì) webpack 為 5 的情況,所有依賴的版本如下:
快速上手
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ì)路徑的話, ./ 不能省略
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ì)不同類型的文件進(jìn)行處理。Loader 是實(shí)現(xiàn)前端模塊化的核心,借助于 Loader 可以加載任何類型的資源。
通過(guò)配置 module 來(lái)配置 loader。rules 為規(guī)則配置。
可以將 Loader 分為幾個(gè)類型:
- 編譯轉(zhuǎn)換類。例如 css-loader,將 css 代碼轉(zhuǎn)換為 js 進(jìn)行工作。
- 文件操作類。例如 file-loader,對(duì)文件進(jìn)行拷貝,再將文件路徑向外導(dǎo)出。
- 代碼檢查類。統(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 專注實(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ì)路徑的話, ./ 不能省略
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ì)不同類型的文件進(jìn)行處理
// Loader 是實(shí)現(xiàn)前端模塊化的核心,借助于 Loader 就可以加載任何類型的資源
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è)類型
// 使用插件就是創(chuàng)建一個(gè)這個(gè)類型的實(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 類型
// 如果使用了 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: 拷貝
// 開發(fā)階段最好不要使用這個(gè)插件
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: 'public' }
]
})
]
}
webpack-dev-server
安裝依賴,然后執(zhí)行 yarn webpack serve --open 會(huì)自動(dòng)打開瀏覽器并打開 watch 模式監(jiān)聽頁(yè)面變化刷新頁(yè)面。
webpack-dev-server 默認(rèn)會(huì)把所有可以打包的文件放到內(nèi)存里(不會(huì)寫入磁盤)。一般在開發(fā)階段不需要將靜態(tài)資源打包,還可以通過(guò)在 devServer 中配置 contentBase,設(shè)置為靜態(tài)資源的目錄,開發(fā)階段就可以訪問(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)致用戶的操作狀態(tài)丟失,這個(gè)時(shí)候我們可以使用 HMR - 熱更新(熱替換),熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中,在頁(yè)面同步更新的同時(shí)保持應(yīng)用的運(yùn)行狀態(tài)不受影響。它極大程度的提高了開發(fā)者的工作效率。
HMR 已經(jīng)集成在 webpack-dev-server 中,不需要再引入依賴。
- 首先引入 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í)沒有實(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)境等。
開發(fā)環(huán)境(cheap-module-eval-source-map):
- 能夠定位到行
- 定位到的文件會(huì)以真實(shí)代碼的樣子顯示(cheap-eval-source-map 會(huì)顯示轉(zhuǎn)譯成es5 的樣子,不好和源代碼定位)
- 雖然首次打包速度慢,但是重寫打包相對(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
}
這樣的話,執(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 的依賴 '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 為我們提供了很多開箱即用的插件。
eg: DefinePlugin
為代碼注入全局成員,自動(dòng)啟用,會(huì)為全局注入 process.env.NODE_ENV 常量,用來(lái)判斷運(yùn)行環(huán)境。
適用方法:
new webpack.DefinePlugin({
// 這里的 value 是一個(gè) js 代碼片段,所以傳入的是 字符串,這個(gè)字符串里邊是一個(gè)字符串
// 也可以寫成 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)目中一些沒有引用的東西。例如一個(gè)函數(shù)中 return 后的操作會(huì)被移除,export 出去的成員沒有引用會(huì)被移除等。
Tree Shaking 在生產(chǎn)環(huán)境打包過(guò)程中會(huì)自動(dòng)開啟。在其他環(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)起見,可以對(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è)趯懡M件的時(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)開啟這個(gè)屬性(這個(gè)屬性在生產(chǎn)環(huán)境會(huì)自動(dòng)開啟)
optimization: {
sideEffects: true,
}
webpack 在打包的時(shí)候就會(huì)檢查 package.json 中是否有 sideEffects 的標(biāo)識(shí),以此來(lái)判斷是否有副作用,我們來(lái)設(shè)置為 false, 表示沒有副作用,這樣打包后,沒有用到的組件就不會(huì)被打包進(jìn)來(lái)了。
使用 sideEffects 的前提是確保你的代碼真的沒有副作用,否則在打包時(shí)就會(huì)誤刪掉有副作用的代碼。比如引入的 css 文件,或者引入一個(gè)擴(kuò)展的對(duì)象的原型方法的 js 文件,它們沒有導(dǎo)出任何成員,所以在引入的時(shí)候也不用導(dǎo)入什么成員,但是在引入后可以使用它們提供的方法,這就屬于這個(gè) css 或 js 的副作用。這個(gè)時(shí)候還標(biāo)識(shí)沒有副作用的話,這些文件就不會(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)該分離開來(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è)屬性就是打包的一路入口
// 屬性名就是入口名稱,值就是入口路徑
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)入
按需加載是我們開發(fā)瀏覽器應(yīng)用一個(gè)常見的需求,一般我們常說(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' */ 為魔法注釋,如果沒有魔法注釋,導(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'
]
}
需要注意的是,如果樣式文件體積不是很大的話,提取到單個(gè)文件中效果可能適得其反。如果 css 體積超過(guò)了 150kb 左右,才需要考慮是否將它提取到單獨(dú)文件中,否則的話 css 嵌入到代碼當(dāng)中減少了一次請(qǐng)求效果可能會(huì)更好。
OptimizeCssAssetsWebpackPlugin
當(dāng)我們打包生產(chǎn)的包時(shí)會(huì)發(fā)現(xiàn),剛剛提取出來(lái)的 css 文件沒有被壓縮,這是因?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è)特性開啟是才會(huì)工作,所以webpack 建議壓縮類的插件應(yīng)該配置在 minimizer 中,以便于可以通過(guò) minimize 這個(gè)選項(xiàng)統(tǒng)一控制。
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},
執(zhí)行 yarn webpack --mode production
可以發(fā)現(xiàn),css 文件壓縮了,但是這時(shí)又沒有壓縮了,這是因?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)資源緩存。這樣的話,對(duì)于用戶的瀏覽器而言,可以緩存住我們應(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ú)法更新到客戶端。所以在生產(chǎn)模式下,我們建議給文件名設(shè)置 hash 值,一旦資源文件改變,文件名稱也可以一起變化,對(duì)于客戶端而言,全新的文件名就是全新的請(qǐng)求,也就沒有緩存的問(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。
如果覺得 20 位的 hash 太長(zhǎng),可以指定長(zhǎng)度,[contenthash:8] 指定 hash 長(zhǎng)度為8。