查漏補缺
通過如何利用webpack來提升前端開發效率(一)的學習,我們已經能夠通過webpack
的loader
和piugin
機制來處理各種檔案資源。細心的小夥伴們發現了缺少了對字型檔案和HTML
中<img>
標籤的資源處理,那讓我們先來解決這個問題。
接上篇文章,我們的目錄結構,如圖所示:
首先是對字型檔案的處理,修改webpack.config.js
// webpack.config.js
// 新增對字型的loader
{
test: /\.(eot|woff|woff2|ttf)$/,
use: [{
loader: 'url-loader',
options: {
name: '[name].[hash:7].[ext]',
limit: 8192,
outputPath: 'font', // 打包到 dist/font 目錄下
}
}]
},
複製程式碼
如何我們從網上隨意下載了一種字型,放置於src
資料夾下,並修改src/index.html
<!-- src/index.html -->
<!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>my-webpack</title>
</head>
<body>
<h1>webpack大法好!!前端大法好!!</h1>
</body>
</html>
複製程式碼
在index.scss
中引入字型
/* src/index.scss */
/* 新增以下樣式 */
@font-face {
font-family: 'myFont';
src: url('./font/ZaoZiGongFangQiaoPinTi-2.ttf');
}
h1 {
font-family: 'myFont';
}
複製程式碼
在此之前,每次重新打包都要刪除dist
資料夾,實在是麻煩,現在我們可以藉助clean-webpack-plugin
,它能夠在每次打包時刪除指定的資料夾,我們在命令列執行 npm i clean-webpack-plugin -D
修改webpack.config.js
// webpack.config.js
// 新增以下引入
const CleanWebpackPlugin = require('clean-webpack-plugin');
// 新增以下外掛
plugins: [
new CleanWebpackPlugin(['dist']) // 在最新的v2版本中,如果預設刪除dist資料夾,只需new CleanWebpackPlugin()
],
複製程式碼
隨後在命令列執行npm run build
,我們的dist
資料夾會被自動刪除,並輸出以下結果,可以看到我們雖然成功打包了字型檔案,但字型檔案是在太大,連webpack
都發出了警告[big]
。
- 對字型檔案開啟
CDN
加速 - 通過設計給出已經制作好的樣式圖
- 利用
font-spider
對字型進行壓縮
我們實踐一下第三種方案,也是我推薦的方案 在命令列依次執行
npm i font-spider -D
font-spider ./dist/index.html
複製程式碼
可以看到將近4MB的字型檔案體積瞬間壓縮至不足6KB!!!而頁面效果和之前一模一樣。
而對於HTML
文件中<img>標籤的引入問題
,我們需要藉助html-loader
,它能將HTML
文件中img.src
解析成require
,從而實現引入圖片,話不多說,我們直接看效果。在命令列執行npm i html-loader -D
修改以下檔案
// webpack.config.js
// 新增對html的loader
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src'] // img代表解析標籤,src代表要解析的值,以key:value形式存在於attrs陣列中
}
}
}
複製程式碼
<!-- src/index.html -->
<body>
+ <img src="./leaf.png" alt="">
</body>
複製程式碼
在命令列執行npm run build
,檢視dist/index.html
,看來已經成功啦
動態載入
設想如果我們的入口檔案很大(包含了所有的業務邏輯程式碼),就會造成首屏載入變慢,使用者體驗感下降。 這裡我們從兩個方面解決:
- 模組解耦,將入口檔案解耦,將基礎模組(UI,工具類)和業務模組分離,即能方便程式碼維護擴充,也能減少入口檔案的體積。
- 動態載入,使用者不可能一開始就用到所有的功能,這時候我們可以將次要的,需要事件觸發的模組,在之後的互動過程中,動態引入。
在
src
目錄下新增dynamic.js
// dynamic.js
export default () => {
console.log('Im dynamically loaded.');
}
複製程式碼
修改以下檔案
<!-- src/index.html -->
<body>
+ <button id="btn">點選我,動態載入dynamic.js</button>
</body>
複製程式碼
// src/index.js
// 新增以下內容
const btn = document.getElementById('btn');
// 點選按鈕,動態載入dynamic.js
btn.onclick = () => {
import(/* webpackChunkName: "dynamic" */ './dynamic.js').then(function (module) {
const fn = module.default;
fn();
})
}
複製程式碼
執行npm run build
,可以看到
/* webpackChunkName: "dynamic" */
,則是
可以得出得結論是:設定ChunkName
為 "dynamic"
是必要的,否則打包完成會是以自動分配的、可讀性很差的id
命名的JS
檔案。且沒有Chunk Names
標識。
現在我們開啟dist/index.html
,此時
dynamic.js
至此,我們成功實現了動態載入。
分離開發環境和生產環境
回頭看我們的webpack.config.js
,不知不覺就寫了這麼多程式碼,鑑於我們在開發實際專案時,是開發和生產兩套工作模式,各司其職,我們不如做個了斷,分離配置。
命令列執行npm i webpack-merge cross-env -D
webpack-merge
可以合併webpack配置項,cross-env
可以設定及使用環境變數。
新增webpack.base.js
,提供基本的webpack loader plugin
配置
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);
const devMode = process.env.NODE_ENV !== 'production';
// 在node中,有全域性變數process表示的是當前的node程式。
// process.env包含著關於系統環境的資訊。
// NODE_ENV是使用者一個自定義的變數,在webpack中它的用途是來判斷當前是生產環境或開發環境。
// 我們可以通過 cross-env 將 NODE_ENV=development 寫入 npm run dev的指令中,從而注入NODE_ENV變數。
module.exports = {
entry: {
index: pathResolve('js/index.js')
},
output: {
path: pathResolve('dist'),
},
module: {
rules: [
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src']
},
},
},
{
test: /\.(eot|woff|woff2|ttf)$/,
use: [{
loader: 'url-loader',
options: {
name: '[name].[hash:7].[ext]',
limit: 8192,
outputPath: 'font',
},
}],
},
{
test: /\.(sa|sc|c)ss$/,
use: [
devMode ? 'style-loader' : { // 如果處於開發模式,則無需再外鏈CSS,直接插入到<style>標籤中
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../'
}
},
'css-loader',
'postcss-loader',
'sass-loader',
],
},
{
test: /\.(png|jpg|jpeg|svg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash:7].[ext]',
outputPath: 'img',
},
}],
},
],
},
plugins: [
new htmlWebpackPlugin({
minify: {
collapseWhitespace: true, // 移除空格
removeAttributeQuotes: true, // 移除引號
removeComments: true // 移除註釋
},
filename: pathResolve('dist/index.html'),
template: pathResolve('src/index.html'),
})
]
};
複製程式碼
新增webpack.dev.js
,服務於開發模式下
const path = require('path');
const webpack = require('webpack');
const base = require('./webpack.base.js');
const { smart } = require('webpack-merge');
const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);
module.exports = smart(base, {
mode: 'development',
output: {
filename: 'js/[name].[hash:7].js'
},
devServer: {
contentBase: pathResolve('dist'),
port: '8080',
inline: true,
historyApiFallback: true,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin()
]
})
複製程式碼
新增webpack.prod.js
,服務於生產模式下
const path = require('path');
const base = require('./webpack.base.js');
const { smart } = require('webpack-merge');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);
module.exports = smart(base, {
mode: 'production',
devtool: 'source-map', // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度,適用於打包後的程式碼查錯
output: {
filename: 'js/[name].[chunkhash:7].js',
chunkFilename: 'js/[name].[chunkhash:7].js',
},
plugins: [
new CleanWebpackPlugin(['dist']),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:7].css',
}),
],
});
複製程式碼
相應的,package.json
也需要修改
// 新增以下兩條命令
// cross-env 決定執行環境 --config 決定執行哪個配置檔案
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js "
複製程式碼
快取之爭
快取在前端的地位毋庸置疑,正確的利用快取就能極大地提高應用的載入速度和效能。
webpack
利用了hash
值作為檔名的組成部分,能有效利用快取。當修改檔案,重新打包時,hash值就會改變,導致快取失效,HTTP請求重新拉取資源。
而webpack
有三種hash處理策略,分別是:
hash
屬於專案工程級別的,即每次修改任何一個檔案,所有檔名的hash
值都將改變。所以一旦修改了任何一個檔案,整個專案的檔案快取都將失效。如將整個專案的filename
的命名策略改為name.[hash:7]
(:7的意思是從完整hash值中擷取前七位),我們可以看到,打包後的檔案hash值是一樣的,所以對於沒有改變的模組而言,hash也被更新了,導致快取失效了。
chunkhash
chunkhash
根據不同的入口檔案(Entry
)進行依賴檔案解析、構建對應的chunk
,生成對應的雜湊值。如將整個專案filename
的命名策略改為name.[chunkhash:7]
,我們可以看到Chunk Names
為"index"
的檔案hash值一致,而不同chunk
的hash
值不同。這也就避免了修改某個檔案,整個工程hash值都將改變的情況。
contenthash
但問題隨之而來,index.scss
是作為模組匯入到index.js
中的,其chunkhash
值是一致的,只要其中之一改變,與其關聯的檔案chunkhash
值也會改變。這時候就要用到contenthash
,它是根據檔案的內容計算,該檔案的內容改變了,contenthash值才會改變。我們將css檔案的命名策略改為name.[contenthash:7]
,並修改src/index.js
,不改動其他檔案,再次打包,發現:
生產環境的配置優化
tree-shaking
字面意思理解為從一棵樹上把葉子搖晃下來,這樣數的重量就減輕了,類比程式,就如同從我們的應用上刪除沒用的程式碼,從而減少體積。借於ES6
的模組引入是靜態分析的,故而webpack
可以在編譯時正確判斷到底載入了什麼程式碼,即沒有被引用的模組不會被打包進來,減少我們的包大小,縮小應用的載入時間,呈現給使用者更佳的體驗。那麼怎麼使用呢?
新建src/utils.js
// src/utils.js
const square = (num) => num ** 2;
const cube = num => num * num * num;
// 匯出了兩個方法
export {
square,
cube
}
複製程式碼
新建src/shake.js
// src/shake.js
import { cube } from './utils.js';
// 只使用了cube方法
console.log('cube(3) is' + cube(3));
複製程式碼
在webpack.base.js
中新增入口檔案shake.js
entry: {
+ shake: pathResolve('src/shake.js')
},
複製程式碼
命令列執行npm run build
,檢視打包後的shake.js
,並沒有發現square
方法沒有被打包進來,說明tree-shaking
起作用了。
而這一切都是webpack
在production
環境下自動為我們實現的。
splitChunks
字面意思為拆分程式碼塊,預設情況下它將只會影響按需載入的程式碼塊,因為改變初始化的程式碼塊將會影響HTML
中執行專案需要包含的script
標籤。還記得我們在src/index.js
中動態引入了src/dynamic.js
嗎,最終dynamic.js
被獨立打包,就是歸功於splitChunks
。
在實際生產中,我們經常會引入第三方庫(JQuery
,Lodash
),往往這些第三方庫體積高達幾十KB摻雜在業務程式碼中,並且不會像業務程式碼一樣經常更新,這時候我們就需要將他們拆分出來,既能保持第三方庫持久快取,又能縮減業務程式碼的體積。
修改webpack.prod.js
// 在module.exports中新增如下內容
optimization: {
runtimeChunk: {
name: 'manifest', // 被注入了webpackJsonp的定義及非同步載入相關的定義,單獨打包模組資訊清單,利於快取
},
splitChunks: {
cacheGroups: { // 快取組,預設將所有來源於node_modules的模組分配到叫做'venders'的快取組,所有引用超過兩次的模組分配到'default'快取組.
vendor: {
chunks: "all", // all, async, initial 三選一, 外掛作用的chunks範圍,推薦all
test: /[\\/]node_modules[\\/]/, // 快取組所選擇的的模組範圍
name: "vendor", // Chunk Names及打包出來的檔名
minChunks: 1, // 引用次數>=1
maxInitialRequests: 5, // 頁面初始化時載入程式碼塊的請求數量應該<=5
minSize: 0, // 程式碼塊的最小尺寸
priority: 100, // 快取優先順序權重
},
}
}
},
複製程式碼
命令列執行npm i lodash -S
修改src/index.js
// 新增以下內容
import _ from 'lodash';
複製程式碼
執行npm run build
,可以看到優化前lodash
被打包進index.js
,優化後lodash
被打包進vendor.js
。
壓縮程式碼,去除冗餘
往往在CSS程式碼中,存在很多我們沒有用到的樣式,它們是冗餘的,我們需要將它們剔除,並壓縮剩餘的CSS樣式,以減少CSS檔案體積。
在命令列執行npm i glob optimize-css-assets-webpack-plugin purifycss-webpack purify-css -D
修改webpack.prod.js
// 新增以下引入
const glob = require('glob'); // 匹配所需檔案
const PurifyCssWebpack = require('purifycss-webpack'); // 去除冗餘CSS
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); // 壓縮CSS
// 新增以下外掛
new PurifyCssWebpack({
paths: glob.sync(pathResolve('src/*.html')) // 同步掃描所有html檔案中所引用的css,並去除冗餘樣式
})
// 新增以下優化
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({}) // 壓縮CSS
]
}
複製程式碼
執行npm run build
CSS
,並壓縮至一行。接下來,我們需要壓縮JS
程式碼。
由於我們使用的是uglifyjs-webpack-plugin
,它需要ES6的支援,所以我們先讓工程支援ES6的語法。 Babel 是一個 JavaScript 編譯器。它能把下一代 JavaScript 語法轉譯成ES5,以適配多種執行環境。
@babel/core
提供了babel的轉譯API,如babel.transform
等,用於對程式碼進行轉譯。像webpack
的babel-loader
就是呼叫這些API來完成轉譯過程的。
@babel/preset-env
可以根據配置的目標瀏覽器或者執行環境來自動將ES2015+
的程式碼轉換為ES5
。
先在命令列執行npm i @babel/core @babel/preset-env babel-loader @babel/plugin-syntax-dynamic-import -D
新建.babelrc
檔案
{
"presets": [ // 配置預設環境
["@babel/preset-env", {
"modules": false
}]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import" // 處理src/index.js中動態載入
]
}
複製程式碼
修改webpack.base.js
// 新增js的解析規則
{
test: /\.(js|jsx)$/,
use: 'babel-loader',
exclude: /node_modules/
},
複製程式碼
然後命令列執行npm i uglifyjs-webpack-plugin -D
修改webpack.prod.js
// 新增以下引入
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
// 新增以下優化
optimization: {
minimizer: [
+ new UglifyJsPlugin({ // 壓縮JS
cache: true,
parallel: true,
sourceMap: true
})
]
}
複製程式碼
執行npm run build
,可以看到打包的檔案體積大大減少,大功告成,JS
也被壓縮了。
以index.html
為例,我們可以開啟Chrome的開發者工具,選擇More tools,點選Coverage皮膚,可以看到JS、CSS等檔案的使用率,配合我們定製的webpack配置進行極致優化。
多頁面
有時候,我們需要同時構建多個頁面,藉助html-webpack-plugin
,只需在plugins
中新增新頁面的配置項。
新增src/main.html
<!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>main page</title>
</head>
<body>
<h1>I am Main Page</h1>
</body>
</html>
複製程式碼
修改webpack.base.js
// 修改以下內容
plugins: [
new htmlWebpackPlugin({ // 配置index.html
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
filename: pathResolve('dist/index.html'),
template: pathResolve('src/index.html'),
chunks: ['manifest', 'vendor', 'index', ] // 配置index.html需要用的chunk塊,即載入哪些JS檔案,manifest模組管理的核心,必須第一個進行載入,不然會報錯
}),
new htmlWebpackPlugin({ // 配置main.html
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
filename: pathResolve('dist/main.html'),
template: pathResolve('src/main.html'),
chunks: ['manifest', 'shake'] // 配置index.html需要用的chunk塊,載入manifest.js,shake.js
}),
],
複製程式碼
執行npm run build
,成功構建了index.html
,main.html
。
結語
至此,我們擺脫了第三方腳手架的的禁錮,循序漸進的搭建了屬於自己的前端流程工具,做到了即改即用,功能俱全,快速便捷,複用性強的特點。希望小夥伴能親自動手,別老是紙上談webpack
,要理解它的構建、優化原理,得心應手得融入到自己的工程專案中,拒絕再用以前繁瑣,不規範的開發流程,不做“CV工程師”,建立屬於自己的知識體系、工作流程,提高前端的開發效率。
最後,本專案原始碼已部署在Github
上,並增加了許多額外優化(less
的支援,ESLint
檢測,針對圖片格式的壓縮...),讓大家可以直接下載體驗,並協助專案開發,日後也會持續維護,希望小夥伴們可以互相學習,提提建議。
GitHub地址:easy-frontend 一個快速,簡單,易用的前端開發效率提升工具
Star該專案,就是你們對我最大的的鼓勵!!
前端路上,不忘初心,祝大家早日發財!!