作者:JowayYoung
倉(cāng)庫(kù):Github、CodePen
博客:掘金、思否、知乎、簡(jiǎn)書(shū)、頭條、CSDN
公眾號(hào):IQ前端
聯(lián)系我:關(guān)注公眾號(hào)后有我的微信喲
特別聲明:原創(chuàng)不易,未經(jīng)授權(quán)不得對(duì)此文章進(jìn)行轉(zhuǎn)載或抄襲,否則按侵權(quán)處理,如需轉(zhuǎn)載或開(kāi)通公眾號(hào)白名單可聯(lián)系我,希望各位尊重原創(chuàng)的知識(shí)產(chǎn)權(quán)
前言
曾經(jīng)發(fā)表過(guò)一篇性能優(yōu)化的文章《前端性能優(yōu)化指南》,筆者總結(jié)了一些在項(xiàng)目開(kāi)發(fā)過(guò)程中使用過(guò)的性能優(yōu)化經(jīng)驗(yàn)。說(shuō)句真話,性能優(yōu)化可能在面試過(guò)程中會(huì)有用,實(shí)際在項(xiàng)目開(kāi)發(fā)過(guò)程中可能沒(méi)幾個(gè)同學(xué)會(huì)注意這些性能優(yōu)化的細(xì)節(jié)。
若經(jīng)常關(guān)注性能優(yōu)化的話題,可能會(huì)發(fā)現(xiàn)無(wú)論怎樣對(duì)代碼做最好的優(yōu)化也不及對(duì)一張圖片做一次壓縮好。所以壓縮圖片成了性能優(yōu)化里最常見(jiàn)的操作,不管是手動(dòng)壓縮圖片還是自動(dòng)壓縮圖片,在項(xiàng)目開(kāi)發(fā)過(guò)程中必須得有。
自動(dòng)壓縮圖片通常在webpack構(gòu)建項(xiàng)目時(shí)接入一些第三方Loader&Plugin來(lái)處理。打開(kāi)Github,搜素webpack image等關(guān)鍵字,Star最多還是image-webpack-loader和imagemin-webpack-plugin這兩個(gè)Loader&Plugin。很多同學(xué)可能都會(huì)選擇它們,方便快捷,簡(jiǎn)單易用,無(wú)腦接入。
可是,這兩個(gè)Loader&Plugin存在一些特別問(wèn)題,它們都是基于imagemin開(kāi)發(fā)的。imagemin的某些依賴托管在國(guó)外服務(wù)器,在npm i xxx安裝它們時(shí)會(huì)默認(rèn)走GitHub Releases的托管地址,若不是規(guī)范上網(wǎng),你們是不可能安裝得上的,即使規(guī)范上網(wǎng)也不一定安裝得上。所以筆者又刨根到底發(fā)表了一篇關(guān)于NPM鏡像處理的文章《聊聊NPM鏡像那些險(xiǎn)象環(huán)生的坑》,專門(mén)解決這些因?yàn)榫W(wǎng)絡(luò)環(huán)境而導(dǎo)致安裝失敗的問(wèn)題。除了這個(gè)安裝問(wèn)題,imagemin還存在另一個(gè)大問(wèn)題,就是壓縮質(zhì)感損失得比較嚴(yán)重,圖片體積越大越明顯,壓縮出來(lái)的圖片總有幾張是失真的,而且總體壓縮率不是很高。這樣在交付項(xiàng)目時(shí)有可能被細(xì)心的QA小姐姐抓個(gè)正著,怎么和設(shè)計(jì)圖對(duì)比起來(lái)不清晰啊!
工具
圖片壓縮工具
此時(shí)可能有些同學(xué)已轉(zhuǎn)戰(zhàn)到手動(dòng)壓縮圖片了。比較好用的圖片壓縮工具無(wú)非就是以下幾個(gè),若有更好用的工具麻煩在評(píng)論里補(bǔ)充喔!同時(shí)筆者也整理出它們的區(qū)別,供各位同學(xué)參考。

| 工具 | 開(kāi)源 | 收費(fèi) | API | 免費(fèi)體驗(yàn) |
|---|---|---|---|---|
| QuickPicture | ?? | ?? | ?? | 可壓縮類(lèi)型較多,壓縮質(zhì)感較好,有體積限制,有數(shù)量限制 |
| ShrinkMe | ?? | ?? | ?? | 可壓縮類(lèi)型較多,壓縮質(zhì)感一般,無(wú)數(shù)量限制,有體積限制 |
| Squoosh | ?? | ?? | ?? | 可壓縮類(lèi)型較少,壓縮質(zhì)感一般,無(wú)數(shù)量限制,有體積限制 |
| TinyJpg | ?? | ?? | ?? | 可壓縮類(lèi)型較少,壓縮質(zhì)感很好,有數(shù)量限制,有體積限制 |
| TinyPng | ?? | ?? | ?? | 可壓縮類(lèi)型較少,壓縮質(zhì)感很好,有數(shù)量限制,有體積限制 |
| Zhitu | ?? | ?? | ?? | 可壓縮類(lèi)型一般,壓縮質(zhì)感一般,有數(shù)量限制,有體積限制 |
從上述表格對(duì)比可看出,免費(fèi)體驗(yàn)都會(huì)存在體積限制,這個(gè)可理解,即使收費(fèi)也一樣,畢竟每個(gè)人都上傳單張10多M的圖片,哪個(gè)服務(wù)器能受得了。再來(lái)就是數(shù)量限制,一次只能上傳20張,好像有個(gè)規(guī)律,壓縮質(zhì)感好就限制數(shù)量,否則就不限制數(shù)量,當(dāng)然收費(fèi)后就沒(méi)有限制了。再來(lái)就是可壓縮類(lèi)型,圖片類(lèi)型一般是jpg、png、gif、svg和webp,gif壓縮后一般都會(huì)失真,svg通常用在矢量圖標(biāo)上很少用在場(chǎng)景圖片上,webp由于兼容性問(wèn)題很少被使用,故能壓縮jpg和png就足夠了。當(dāng)然壓縮質(zhì)感是最優(yōu)考慮,綜上所述,大部分同學(xué)都會(huì)選擇TinyJpg和TinyPng,其實(shí)它倆就是兄弟,出自同一廠商。
在筆者公眾號(hào)的微信討論群里發(fā)起了一個(gè)簡(jiǎn)單的投票,最終還是TinyJpg和TinyPng勝出。

TinyJpg/TinyPng存在問(wèn)題
- 上傳下載全靠
手動(dòng) - 只能壓縮
jpg和png - 每次只能壓縮
20張 - 每張?bào)w積最大不能超過(guò)
5M - 可視化處理信息不是特別齊全
TinyJpg/TinyPng壓縮原理
TinyJpg/TinyPng使用智能有損壓縮技術(shù)將圖片體積降低,選擇性地減少圖片中相似顏色,只需很少字節(jié)就能保存數(shù)據(jù)。對(duì)視覺(jué)影響幾乎不可見(jiàn),但是在文件體積上就有很大的差別。而使用到智能有損壓縮技術(shù)被稱為量化。
TinyJpg/TinyPng在壓縮png文件時(shí)效果更顯著。掃描圖片中相似顏色并將其合并,通過(guò)減少顏色數(shù)量將24位png文件轉(zhuǎn)換成體積更小的8位png文件,丟棄所有不必要的元數(shù)據(jù)。
大部分png文件都有50%~70%的壓縮率,即使視力再好也很難區(qū)分出來(lái)。使用優(yōu)化過(guò)的圖片可減少帶寬流量和加載時(shí)間,整個(gè)網(wǎng)站使用到的圖片經(jīng)TinyJpg/TinyPng壓縮一遍,其成效是再多的代碼優(yōu)化也無(wú)法追趕得上的。

TinyJpg/TinyPng開(kāi)發(fā)API
查閱相關(guān)資料,發(fā)現(xiàn)TinyJpg/TinyPng暫時(shí)還未開(kāi)源其壓縮算法,不過(guò)提供了適合開(kāi)發(fā)者使用的API。有興趣的同學(xué)可到其開(kāi)發(fā)API文檔瞧瞧。
在Node方面,TinyJpg/TinyPng官方提供了tinify作為壓縮圖片的核心JS庫(kù),使用很簡(jiǎn)單,看文檔吧??墒菗Q成開(kāi)發(fā)API還是逃不過(guò)收費(fèi),你是想包月呢還是免費(fèi)呢,想免費(fèi)的話就繼續(xù)往下看,土豪隨意!

實(shí)現(xiàn)
筆者也是經(jīng)常使用TinyJpg/TinyPng的程序猿,收費(fèi),那是不可能的??。尋找突破口,解決問(wèn)題,是作為一位程序猿最基本的素養(yǎng)。我們需明確什么問(wèn)題,需解決什么問(wèn)題。
分析
從上述得知,只需對(duì)TinyJpg/TinyPng原有功能改造成以下功能。
- 上傳下載全自動(dòng)
- 可壓縮
jpg和png - 沒(méi)有數(shù)量限制
- 存在體積限制,最大體積不能超過(guò)
5M - 壓縮成功與否輸出詳細(xì)信息
自動(dòng)處理
對(duì)于前端開(kāi)發(fā)者來(lái)說(shuō),這種無(wú)腦的上傳下載操作必須得自動(dòng)化,省事省心省力。但是這個(gè)操作得結(jié)合webpack來(lái)處理,到底是開(kāi)發(fā)成Loader還是Plugin,后面再分析。不過(guò)細(xì)心的同學(xué)看標(biāo)題就知道用什么方式處理了。
壓縮類(lèi)型
gif壓縮后一般都會(huì)失真,svg通常用在矢量圖標(biāo)上很少用在場(chǎng)景圖片上,webp由于兼容性問(wèn)題很少被使用,故能壓縮jpg和png就足夠了。在過(guò)濾圖片時(shí),使用path模塊判斷文件類(lèi)型是否為jpg和png,是則繼續(xù)處理,否則不處理。
數(shù)量限制
數(shù)量限制當(dāng)然是不能存在的,萬(wàn)一項(xiàng)目里超過(guò)20張圖片,那不是得分批處理,這個(gè)不能有。對(duì)于這種無(wú)需登錄狀態(tài)就能處理一些用戶文件的網(wǎng)站,通常都會(huì)通過(guò)IP來(lái)限制用戶的操作次數(shù)。有些同學(xué)可能會(huì)說(shuō),刷新頁(yè)面不就行了嗎,每次壓縮20張圖片,再刷新再壓縮,萬(wàn)一有500張圖片呢,你就刷新25次嗎,這樣很好玩是吧!
由于大多數(shù)Web架構(gòu)很少會(huì)將應(yīng)用服務(wù)器直接對(duì)外提供服務(wù),一般都會(huì)設(shè)置一層Nginx作為代理和負(fù)載均衡,有的甚至可能有多層代理。鑒于大多數(shù)Web架構(gòu)都是使用Nginx作為反向代理,用戶請(qǐng)求不是直接請(qǐng)求應(yīng)用服務(wù)器的,而是通過(guò)Nginx設(shè)置的統(tǒng)一接入層將用戶請(qǐng)求轉(zhuǎn)發(fā)到服務(wù)器的,所以可通過(guò)設(shè)置HTTP請(qǐng)求頭字段X-Forwarded-For來(lái)偽造IP。
X-Forwarded-For指用來(lái)識(shí)別通過(guò)代理或負(fù)載均衡的方式連接到Web服務(wù)器的客戶端最原始的IP地址的HTTP請(qǐng)求頭字段。當(dāng)然,這個(gè)IP也不是一成不變的,每次請(qǐng)求都需隨機(jī)更換IP,騙過(guò)應(yīng)用服務(wù)器。若應(yīng)用服務(wù)器增加了偽造IP識(shí)別,那可能就無(wú)法繼續(xù)使用隨機(jī)IP了。
體積限制
體積限制這個(gè)能理解,也沒(méi)必要搞一張那么大的圖片,多浪費(fèi)帶寬流量和加載時(shí)間啊。在上傳圖片時(shí),使用fs模塊判斷文件體積是否超過(guò)5M,是則不上傳,否則繼續(xù)上傳。當(dāng)然,交給TinyJpg/TinyPng接口判斷也行。
輸出信息
壓縮成功與否得讓別人知道,輸出原始大小、壓縮大小、壓縮率和錯(cuò)誤提示等,讓別人清楚這些處理信息。
編碼
通過(guò)上述抽絲剝繭的分析,那么就開(kāi)始著手編碼了。
隨機(jī)生成HTTP請(qǐng)求頭
既然可通過(guò)X-Forwarded-For來(lái)偽造IP,那么得有一個(gè)隨機(jī)生成HTTP請(qǐng)求頭字段的函數(shù),每次請(qǐng)求接口時(shí)都隨機(jī)生成相關(guān)的請(qǐng)求頭字段。打開(kāi)tinyjpg.com或tinypng.com上傳一張圖片,通過(guò)Chrome DevTools分析Network發(fā)現(xiàn)其請(qǐng)求接口是web/shrink。另外每次請(qǐng)求也不要集中在單一的hostname上,隨機(jī)派發(fā)到tinyjpg.com或tinypng.com上會(huì)更好。通過(guò)封裝RandomHeader函數(shù)隨機(jī)生成請(qǐng)求頭信息,后續(xù)使用https模塊以RandomHeader()生成的配置作為入?yún)⑦M(jìn)行請(qǐng)求。
trample是筆者開(kāi)發(fā)的一個(gè)Web/Node通用函數(shù)工具庫(kù),包含常規(guī)的工具函數(shù),助你少寫(xiě)更多通用代碼。詳情請(qǐng)查看文檔,順便給一個(gè)Star以作鼓勵(lì)。

const { RandomNum } = require("trample/node");
const TINYIMG_URL = [
"tinyjpg.com",
"tinypng.com"
];
function RandomHeader() {
const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
const index = RandomNum(0, 1);
return {
headers: {
"Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded",
"Postman-Token": Date.now(),
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"X-Forwarded-For": ip
},
hostname: TINYIMG_URL[index],
method: "POST",
path: "/web/shrink",
rejectUnauthorized: false
};
}
上傳圖片與下載圖片
使用Promise封裝上傳圖片和下載圖片的函數(shù),方便后續(xù)使用Async/Await同步化異步代碼。以下函數(shù)的具體斷點(diǎn)調(diào)試就不說(shuō)了,有興趣的同學(xué)自行調(diào)試函數(shù)的入?yún)⒑统鰠⒐?/p>
const Https = require("https");
const Url = require("url");
function UploadImg(file) {
const opts = RandomHeader();
return new Promise((resolve, reject) => {
const req = Https.request(opts, res => res.on("data", data => {
const obj = JSON.parse(data.toString());
obj.error ? reject(obj.message) : resolve(obj);
}));
req.write(file, "binary");
req.on("error", e => reject(e));
req.end();
});
}
function DownloadImg(url) {
const opts = new Url.URL(url);
return new Promise((resolve, reject) => {
const req = Https.request(opts, res => {
let file = "";
res.setEncoding("binary");
res.on("data", chunk => file += chunk);
res.on("end", () => resolve(file));
});
req.on("error", e => reject(e));
req.end();
});
}
壓縮圖片
通過(guò)上傳圖片函數(shù)獲取壓縮后的圖片信息,再依據(jù)圖片信息通過(guò)下載圖片函數(shù)生成本地文件。
const Fs = require("fs");
const Path = require("path");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");
async function CompressImg(path) {
try {
const file = Fs.readFileSync(path, "binary");
const obj = await UploadImg(file);
const data = await DownloadImg(obj.output.url);
const oldSize = Chalk.redBright(ByteSize(obj.input.size));
const newSize = Chalk.greenBright(ByteSize(obj.output.size));
const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
const dpath = Path.join("img", Path.basename(path));
const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
Fs.writeFileSync(dpath, data, "binary");
return Promise.resolve(msg);
} catch (err) {
const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
return Promise.resolve(msg);
}
}
壓縮目標(biāo)圖片
完成上述步驟對(duì)應(yīng)的函數(shù)后,就能自由壓縮圖片了,以下使用一張圖片作為演示。
const Ora = require("ora");
(async() => {
const spinner = Ora("Image is compressing......").start();
const res = await CompressImg("src/pig.png");
spinner.stop();
console.log(res);
})();
你看,壓縮完后笨豬都變帥豬了,能電眼的豬都是好豬。源碼請(qǐng)查看compress-img。

若壓縮指定文件夾里符合條件的所有圖片,可通過(guò)fs模塊獲取圖片并使用map()將單個(gè)圖片路徑映射為CompressImg(path),再通過(guò)Promise.all()操作即可。在這里就不貼代碼了,當(dāng)作思考題,自行完成。
將上述壓縮圖片的功能封裝成Loader還是Plugin呢?接下來(lái)會(huì)一步一步分析。
Loader&Plugin
webpack是一個(gè)前端資源打包工具,它根據(jù)模塊依賴關(guān)系進(jìn)行靜態(tài)分析,然后將這些模塊按照指定規(guī)則生成對(duì)應(yīng)的靜態(tài)資源。
網(wǎng)上一大堆webpack教程,筆者就不再花大篇幅啰嗦了,相信各位同學(xué)都是一位標(biāo)準(zhǔn)的Webpack配置工程師。以下簡(jiǎn)單回顧一次webpack的組成、構(gòu)建機(jī)制和構(gòu)建流程,相信也能從這些知識(shí)點(diǎn)中定位出Loader和Plugin在Webpack構(gòu)建流程中是處于一個(gè)什么樣的角色地位。
本文所說(shuō)的webpack都是基于webpack v4
組成
- Entry:入口
- Output:輸出
- Loader:轉(zhuǎn)換器
- Plugin:擴(kuò)展器
- Mode:模式
- Module:模塊
- Target:目標(biāo)
構(gòu)建機(jī)制
- 通過(guò)Babel轉(zhuǎn)換代碼并生成單個(gè)文件依賴
- 從入口文件開(kāi)始遞歸分析并生成依賴圖譜
- 將各個(gè)引用模塊打包成一個(gè)立即執(zhí)行函數(shù)
- 將最終bundle文件寫(xiě)入
bundle.js中
構(gòu)建流程
-
初始
-
初始參數(shù):合并命令行和配置文件的參數(shù)
-
-
編譯
-
執(zhí)行編譯:依據(jù)參數(shù)初始Compiler對(duì)象,加載所有Plugin,執(zhí)行run() -
確定入口:依據(jù)配置文件找出所有入口文件 -
編譯模塊:依據(jù)入口文件找出所有依賴模塊關(guān)系,調(diào)用所有Loader進(jìn)行轉(zhuǎn)換 -
生成圖譜:得到每個(gè)模塊轉(zhuǎn)換后的內(nèi)容及其之間的依賴關(guān)系
-
-
輸出
-
輸出資源:依據(jù)依賴關(guān)系將模塊組裝成塊再組裝成包(module → chunk → bundle) -
生成文件:依據(jù)配置文件將確認(rèn)輸出的內(nèi)容寫(xiě)入文件系統(tǒng)
-
Loader
Loader用于轉(zhuǎn)換模塊源碼,筆者將其翻譯為轉(zhuǎn)換器。Loader可將所有類(lèi)型文件轉(zhuǎn)換為webpack能夠處理的有效模塊,然后利用webpack的打包能力對(duì)它們進(jìn)行二次處理。
Loader具有以下特點(diǎn):
- 單一職責(zé)原則(
只完成一種轉(zhuǎn)換) - 轉(zhuǎn)換接收內(nèi)容
- 返回轉(zhuǎn)換結(jié)果
- 支持鏈?zhǔn)秸{(diào)用
Loader將所有類(lèi)型文件轉(zhuǎn)換為應(yīng)用程序的依賴圖譜可直接引用的模塊,所以Loader可用于編譯一些文件,例如pug → html、sass → css、less → css、es5 → es6、ts → js等。
處理一個(gè)文件可使用多個(gè)Loader,Loader的執(zhí)行順序和配置順序是相反的,即末尾Loader最先執(zhí)行,開(kāi)頭Loader最后執(zhí)行。最先執(zhí)行的Loader接收源文件內(nèi)容作為參數(shù),其它Loader接收前一個(gè)執(zhí)行的Loader的返回值作為參數(shù),最后執(zhí)行的Loader會(huì)返回該文件的轉(zhuǎn)換結(jié)果。一句話概括:富土康流水線廠工。
Loader開(kāi)發(fā)思路總結(jié)如下:
- 通過(guò)
module.exports導(dǎo)出一個(gè)函數(shù) - 函數(shù)第一默認(rèn)參數(shù)為
source(源文件內(nèi)容) - 在函數(shù)體中處理資源(可引入第三方模塊擴(kuò)展功能)
- 通過(guò)
return返回最終轉(zhuǎn)換結(jié)果(字符串形式)
編寫(xiě)Loader時(shí)要遵循單一職責(zé)原則,每個(gè)Loader只做一種轉(zhuǎn)換工作
Plugin
Plugin用于擴(kuò)展執(zhí)行范圍更廣的任務(wù),筆者將其翻譯為擴(kuò)展器。Plugin的范圍很廣,在Webpack構(gòu)建流程里從開(kāi)始到結(jié)束都能找到時(shí)機(jī)作為插入點(diǎn),只要你想不到?jīng)]有你做不到。所以筆者認(rèn)為Plugin的功能比Loader更加強(qiáng)大。
Plugin具有以下特點(diǎn):
- 監(jiān)聽(tīng)
webpack運(yùn)行生命周期中廣播的事件 - 在合適時(shí)機(jī)通過(guò)
webpack提供的API改變輸出結(jié)果 -
webpack的Tapable事件流機(jī)制保證Plugin的有序性
在webpack運(yùn)行生命周期中會(huì)廣播出許多事件,Plugin可監(jiān)聽(tīng)這些事件并在合適時(shí)機(jī)通過(guò)webpack提供的API改變輸出結(jié)果。在webpack啟動(dòng)后,在讀取配置過(guò)程中執(zhí)行new MyPlugin(opts)初始化自定義Plugin獲取其實(shí)例,在初始化Compiler對(duì)象后,通過(guò)compiler.hooks.event.tap(PLUGIN_NAME, callback)監(jiān)聽(tīng)webpack廣播事件,當(dāng)捕抓到指定事件后,會(huì)通過(guò)Compilation對(duì)象操作相關(guān)業(yè)務(wù)邏輯。一句話概括:自己看著辦。
Plugin開(kāi)發(fā)思路總結(jié)如下:
- 通過(guò)
module.exports導(dǎo)出一個(gè)函數(shù)或類(lèi) - 在
函數(shù)原型或類(lèi)上綁定apply()訪問(wèn)Compiler對(duì)象 - 在
apply()中指定一個(gè)綁定到webpack自身的事件鉤子 - 在事件鉤子中通過(guò)
webpack提供的API處理資源(可引入第三方模塊擴(kuò)展功能) - 通過(guò)
webpack提供的方法返回該資源
傳給每個(gè)Plugin的Compiler和Compilation都是同一個(gè)引用,若修改它們身上的屬性會(huì)影響后面的Plugin,所以需謹(jǐn)慎操作
Loader/Plugin區(qū)別
- 本質(zhì)
-
Loader本質(zhì)是一個(gè)函數(shù),轉(zhuǎn)換接收內(nèi)容,返回轉(zhuǎn)換結(jié)果 -
Plugin本質(zhì)是一個(gè)類(lèi),監(jiān)聽(tīng)webpack運(yùn)行生命周期中廣播的事件,在合適時(shí)機(jī)通過(guò)webpack提供的API改變輸出結(jié)果
-
- 配置
-
Loader在module.rule中配置,類(lèi)型是數(shù)組,每一項(xiàng)對(duì)應(yīng)一個(gè)模塊解析規(guī)則 -
Plugin在plugin中配置,類(lèi)型是數(shù)組,每一項(xiàng)對(duì)應(yīng)一個(gè)擴(kuò)展器實(shí)例,參數(shù)通過(guò)構(gòu)造函數(shù)傳入
-
封裝
分析
從上述可知Loader和Plugin在角色定位和執(zhí)行機(jī)制上有很多不一樣,到底如何選擇呢?各有各好,當(dāng)然還是需分析后進(jìn)行選擇。
Loader在webpack中扮演著轉(zhuǎn)換器的角色,用于轉(zhuǎn)換模塊源碼,簡(jiǎn)單理解就是將文件轉(zhuǎn)換成另外形式的文件,而本文主題是壓縮圖片,jpg壓縮后還是jpg,png壓縮后還是png,在文件類(lèi)型上來(lái)說(shuō)還是沒(méi)有變化。Loader的轉(zhuǎn)換過(guò)程是附屬在整個(gè)Webpack構(gòu)建流程中的,意味著打包時(shí)間包含了壓縮圖片的時(shí)間成本,對(duì)于追求webpack性能優(yōu)化來(lái)說(shuō)實(shí)屬有點(diǎn)違背原則。而Plugin恰好是監(jiān)聽(tīng)webpack運(yùn)行生命周期中廣播的事件,在合適時(shí)機(jī)通過(guò)webpack提供的API改變輸出結(jié)果,所以可在整個(gè)Webpack構(gòu)建流程完成后(全部打包文件輸出完成后)插入壓縮圖片的操作。換句話說(shuō),打包時(shí)間不再包含壓縮圖片的時(shí)間成本,打包完成后該干嘛就干嘛,還能干嘛,壓縮圖片啊。
所以依據(jù)需求情況,Plugin作為首選。
編碼
依據(jù)上述Plugin開(kāi)發(fā)思路,那么就開(kāi)始著手編碼了。
筆者把這個(gè)壓縮圖片的Plugin命名為tinyimg-webpack-plugin,tinyimg意味著TinyJpg和TinyPng合體。
新建項(xiàng)目,目錄結(jié)構(gòu)如下。
tinyimg-webpack-plugin
├─ src
│ ├─ index.js
│ ├─ schema.json
├─ util
│ ├─ getting.js
│ ├─ setting.js
├─ .gitignore
├─ .npmignore
├─ license
├─ package.json
├─ readme.md
主要文件如下。
-
src
- index.js:入口函數(shù)
- schema.json:參數(shù)校驗(yàn)
-
util
- getting.js:常量集合
- setting.js:函數(shù)集合
安裝項(xiàng)目所需模塊,和上述compress-img的依賴一致,額外安裝schema-utils用于校驗(yàn)Plugin參數(shù)是否符合規(guī)定。
npm i chalk figures ora schema-utils trample
封裝常量集合和函數(shù)集合
將上述compress-img的TINYIMG_URL和RandomHeader()封裝到工具集合中,其中常量集合增加IMG_REGEXP和PLUGIN_NAME兩個(gè)常量。
// getting.js
const IMG_REGEXP = /\.(jpe?g|png)$/;
const PLUGIN_NAME = "tinyimg-webpack-plugin";
const TINYIMG_URL = [
"tinyjpg.com",
"tinypng.com"
];
module.exports = {
IMG_REGEXP,
PLUGIN_NAME,
TINYIMG_URL
};
// setting.js
const { RandomNum } = require("trample/node");
const { TINYIMG_URL } = require("./getting");
function RandomHeader() {
const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
const index = RandomNum(0, 1);
return {
headers: {
"Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded",
"Postman-Token": Date.now(),
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"X-Forwarded-For": ip
},
hostname: TINYIMG_URL[index],
method: "POST",
path: "/web/shrink",
rejectUnauthorized: false
};
}
module.exports = {
RandomHeader
};
通過(guò)
module.exports導(dǎo)出一個(gè)函數(shù)或類(lèi)
// index.js
module.exports = class TinyimgWebpackPlugin {};
在
函數(shù)原型或類(lèi)上綁定apply()訪問(wèn)Compiler對(duì)象
// index.js
module.exports = class TinyimgWebpackPlugin {
apply(compiler) {
// Do Something
}
};
在
apply()中指定一個(gè)綁定到webpack自身的事件鉤子
從上述分析中可知,在全部打包文件輸出完成后插入壓縮圖片的操作,所以應(yīng)該選擇該時(shí)機(jī)對(duì)應(yīng)的事件鉤子。從Webpack Compiler Hooks API文檔中可發(fā)現(xiàn),emit正是這個(gè)Plugin所需的事件鉤子。emit在生成資源到輸出目錄前執(zhí)行,此刻可獲取所有圖片文件的數(shù)據(jù)和輸出路徑。
為了方便在特定條件下啟用功能和打印日志,所以設(shè)置相關(guān)配置。
- enabled:是否啟用功能
- logged:是否打印日志
在apply()中處理相關(guān)業(yè)務(wù)邏輯,可能使用到Plugin的入?yún)?,那么就得?duì)參數(shù)進(jìn)行校驗(yàn)。定義一個(gè)Plugin的Schema,通過(guò)schema-utils來(lái)校驗(yàn)Plugin的入?yún)ⅰ?/p>
// schema.json
{
"type": "object",
"properties": {
"enabled": {
"description": "start plugin",
"type": "boolean"
},
"logged": {
"description": "print log",
"type": "boolean"
}
},
"additionalProperties": false
}
// index.js
const SchemaUtils = require("schema-utils");
const { PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");
module.exports = class TinyimgWebpackPlugin {
constructor(opts) {
this.opts = opts;
}
apply(compiler) {
const { enabled } = this.opts;
SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
// Do Something
});
}
};
整合
compress-img到Plugin
在整合過(guò)程中會(huì)有一些小修改,各位同學(xué)可對(duì)比看看哪些細(xì)節(jié)發(fā)生了變化。
// index.js
const Fs = require("fs");
const Https = require("https");
const Url = require("url");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");
const { RandomHeader } = require("../util/setting");
module.exports = class TinyimgWebpackPlugin {
constructor(opts) { ... }
apply(compiler) { ... }
async compressImg(assets, path) {
try {
const file = assets[path].source();
const obj = await this.uploadImg(file);
const data = await this.downloadImg(obj.output.url);
const oldSize = Chalk.redBright(ByteSize(obj.input.size));
const newSize = Chalk.greenBright(ByteSize(obj.output.size));
const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
const dpath = assets[path].existsAt;
const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
Fs.writeFileSync(dpath, data, "binary");
return Promise.resolve(msg);
} catch (err) {
const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
return Promise.resolve(msg);
}
}
downloadImg(url) {
const opts = new Url.URL(url);
return new Promise((resolve, reject) => {
const req = Https.request(opts, res => {
let file = "";
res.setEncoding("binary");
res.on("data", chunk => file += chunk);
res.on("end", () => resolve(file));
});
req.on("error", e => reject(e));
req.end();
});
}
uploadImg(file) {
const opts = RandomHeader();
return new Promise((resolve, reject) => {
const req = Https.request(opts, res => res.on("data", data => {
const obj = JSON.parse(data.toString());
obj.error ? reject(obj.message) : resolve(obj);
}));
req.write(file, "binary");
req.on("error", e => reject(e));
req.end();
});
}
};
在事件鉤子中通過(guò)
webpack提供的API處理資源
通過(guò)compilation.assets獲取全部打包文件的對(duì)象,篩選出jpg和png,使用map()將單個(gè)圖片數(shù)據(jù)映射為this.compressImg(file),再通過(guò)Promise.all()操作即可。
整個(gè)業(yè)務(wù)邏輯結(jié)合了Promise和Async/Await兩個(gè)ES6常用特性,它倆組合起來(lái)玩異步編程極其有趣,關(guān)于它倆更多細(xì)節(jié)可查看筆者這篇4000點(diǎn)贊量和14萬(wàn)閱讀量的文章《1.5萬(wàn)字概括ES6全部特性》。
// index.js
const Ora = require("ora");
const SchemaUtils = require("schema-utils");
const { IMG_REGEXP, PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");
module.exports = class TinyimgWebpackPlugin {
constructor(opts) { ... }
apply(compiler) {
const { enabled, logged } = this.opts;
SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
const imgs = Object.keys(compilation.assets).filter(v => IMG_REGEXP.test(v));
if (!imgs.length) return Promise.resolve();
const promises = imgs.map(v => this.compressImg(compilation.assets, v));
const spinner = Ora("Image is compressing......").start();
return Promise.all(promises).then(res => {
spinner.stop();
logged && res.forEach(v => console.log(v));
});
});
}
async compressImg(assets, path) { ... }
downloadImg(url) { ... }
uploadImg(file) { ... }
};
通過(guò)
webpack提供的方法返回該資源
由于壓縮圖片的操作是在整個(gè)Webpack構(gòu)建流程完成后,所以沒(méi)有什么可返回了,故不作處理。
控制
webpack依賴版本
由于tinyimg-webpack-plugin基于webpack v4,所以需在package.json中添加peerDependencies,用來(lái)告知安裝該Plugin的模塊必須存在peerDependencies里的依賴。
{
"peerDependencies": {
"webpack": ">= 4.0.0",
"webpack-cli": ">= 3.0.0"
}
}
總結(jié)
按照上述總結(jié)的開(kāi)發(fā)思路一步一步來(lái)完成編碼,其實(shí)是挺簡(jiǎn)單的。若需開(kāi)發(fā)一些跟自己項(xiàng)目相關(guān)的Plugin,還是需多多熟悉Webpack Compiler Hooks API文檔,相信各位同學(xué)都能手戳一個(gè)完美的Plugin出來(lái)。
tinyimg-webpack-plugin源碼請(qǐng)戳這里查看,Star一個(gè)如何,嘻嘻。

測(cè)試
整個(gè)Plugin開(kāi)發(fā)完成,接下來(lái)需走一遍測(cè)試流程,看能不能把這個(gè)壓縮圖片的擴(kuò)展器跑通。相信各位同學(xué)都是一位標(biāo)準(zhǔn)的Webpack配置工程師,可自行編寫(xiě)測(cè)試Demo驗(yàn)證你們的Plugin。
在根目錄下創(chuàng)建test文件夾,并按照以下目錄結(jié)構(gòu)加入文件。
tinyimg-webpack-plugin
├─ test
│ ├─ src
│ │ ├─ img
│ │ │ ├─ favicon.ico
│ │ │ ├─ gz.jpg
│ │ │ ├─ pig-1.jpg
│ │ │ ├─ pig-2.jpg
│ │ │ ├─ pig-3.jpg
│ │ ├─ index.html
│ │ ├─ index.js
│ │ ├─ index.scss
│ │ ├─ reset.css
│ └─ webpack.config.js
安裝測(cè)試Demo所需的webpack相關(guān)配置模塊。
npm i -D @babel/core @babel/preset-env babel-loader clean-webpack-plugin css-loader file-loader html-webpack-plugin mini-css-extract-plugin node-sass sass sass-loader style-loader url-loader webpack webpack-cli webpackbar
安裝完成后,著手完善webpack.config.js代碼,代碼量有點(diǎn)多,直接貼鏈接好了,請(qǐng)戳這里。
最后在package.json中的scripts插入以下npm scripts,然后執(zhí)行npm run test調(diào)試測(cè)試Demo。
{
"scripts": {
"test": "webpack --config test/webpack.config.js"
}
}
發(fā)布
發(fā)布到NPM倉(cāng)庫(kù)上非常簡(jiǎn)單,僅需幾行命令。若還沒(méi)注冊(cè),趕緊去NPM上注冊(cè)一個(gè)賬號(hào)。若當(dāng)前鏡像為淘寶鏡像,需執(zhí)行npm config set registry https://registry.npmjs.org/切換回源鏡像。
接下來(lái)一波操作就可完成發(fā)布了。
- 進(jìn)入目錄:
cd my-plugin - 登錄賬號(hào):
npm login - 校驗(yàn)狀態(tài):
npm whoami - 發(fā)布模塊:
npm publish - 退出賬號(hào):
npm logout
若不想牢記這么多命令,可用筆者開(kāi)發(fā)的pkg-master一鍵發(fā)布,若存在某些錯(cuò)誤會(huì)立馬中斷發(fā)布并提示錯(cuò)誤信息,是一個(gè)非常好用的集成創(chuàng)建和發(fā)布的NPM模塊管理工具。詳情請(qǐng)查看文檔,順便給一個(gè)Star以作鼓勵(lì)。
安裝
npm i -g pkg-master
使用
| 命令 | 縮寫(xiě) | 功能 | 描述 |
|---|---|---|---|
pkg-master create |
pkg-master c |
創(chuàng)建模塊 | 生成模塊的基礎(chǔ)文件
|
pkg-master publish |
pkg-master p |
發(fā)布模塊 | 檢測(cè)NPM的運(yùn)行環(huán)境和賬號(hào)狀態(tài),通過(guò)則自動(dòng)發(fā)布模塊 |

接入
安裝
npm i tinyimg-webpack-plugin
使用
| 配置 | 功能 | 格式 | 描述 |
|---|---|---|---|
enabled |
是否啟用功能 | true/false |
建議只在生產(chǎn)環(huán)境下開(kāi)啟 |
logged |
是否打印日志 | true/false |
打印處理信息 |
在webpack.config.js或webpack配置插入以下代碼。
在CommonJS中使用
const TinyimgPlugin = require("tinyimg-webpack-plugin");
module.exports = {
plugins: [
new TinyimgPlugin({
enabled: process.env.NODE_ENV === "production",
logged: true
})
]
};
在ESM中使用
必須在babel加持下的Node環(huán)境中使用
import TinyimgPlugin from "tinyimg-webpack-plugin";
export default {
plugins: [
new TinyimgPlugin({
enabled: process.env.NODE_ENV === "production",
logged: true
})
]
};
推薦一個(gè)零配置開(kāi)箱即用的React/Vue應(yīng)用自動(dòng)化構(gòu)建腳手架
bruce-cli是一個(gè)React/Vue應(yīng)用自動(dòng)化構(gòu)建腳手架,其零配置開(kāi)箱即用的優(yōu)點(diǎn)非常適合入門(mén)級(jí)、初中級(jí)、快速開(kāi)發(fā)項(xiàng)目的前端同學(xué)使用,還可通過(guò)創(chuàng)建brucerc.js文件來(lái)覆蓋其默認(rèn)配置,只需專注業(yè)務(wù)代碼的編寫(xiě)無(wú)需關(guān)注構(gòu)建代碼的編寫(xiě),讓項(xiàng)目結(jié)構(gòu)更簡(jiǎn)潔。使用時(shí)記得查看文檔喲,喜歡的話給個(gè)Star。
當(dāng)然,筆者已將tinyimg-webpack-plugin集成到bruce-cli中,零配置開(kāi)箱即用走起。
總結(jié)
總體來(lái)說(shuō)開(kāi)發(fā)一個(gè)Webpack Plugin不難,只需好好分析需求,了解webpack運(yùn)行生命周期中廣播的事件,編寫(xiě)自定義Plugin在合適時(shí)機(jī)通過(guò)webpack提供的API改變輸出結(jié)果。
若覺(jué)得tinyimg-webpack-plugin對(duì)你有幫助,可在Issue上提出你的寶貴建議,筆者會(huì)認(rèn)真閱讀并整合你的建議。喜歡tinyimg-webpack-plugin的請(qǐng)給一個(gè)Star,或Fork本項(xiàng)目到自己的Github上,根據(jù)自身需求定制功能。
結(jié)語(yǔ)
??關(guān)注+點(diǎn)贊+收藏+評(píng)論+轉(zhuǎn)發(fā)??,原創(chuàng)不易,鼓勵(lì)筆者創(chuàng)作更好的文章
關(guān)注公眾號(hào)IQ前端,一個(gè)專注于CSS/JS開(kāi)發(fā)技巧的前端公眾號(hào),更多前端小干貨等著你喔
- 關(guān)注后回復(fù)
關(guān)鍵詞免費(fèi)領(lǐng)取視頻教程 - 關(guān)注后添加
我微信拉你進(jìn)技術(shù)交流群 - 歡迎關(guān)注
IQ前端,更多CSS/JS開(kāi)發(fā)技巧只在公眾號(hào)推送
