如何利用webpack來提升前端開發效率(二)?
通過 如何利用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']) ], 複製程式碼
隨後在命令列執行 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
檢測,針對圖片格式的壓縮...),讓大家可以直接下載體驗,並協助專案開發,日後也會持續維護,希望小夥伴們可以互相學習,提提建議。
easy-frontend 一個快速,簡單,易用的前端開發效率提升工具