曾經(jīng)發(fā)表過(guò)一篇性能優(yōu)化的文章《前端性能優(yōu)化指南》,筆者總結(jié)了一些在項(xiàng)目開發(fā)過(guò)程中使用過(guò)的性能優(yōu)化經(jīng)驗(yàn)。說(shuō)句真話,性能優(yōu)化可能在面試過(guò)程中會(huì)有用,實(shí)際在項(xiàng)目開發(fā)過(guò)程中可能沒幾個(gè)同學(xué)會(huì)注意這些性能優(yōu)化的細(xì)節(jié)。
若經(jīng)常關(guān)注性能優(yōu)化的話題,可能會(huì)發(fā)現(xiàn)無(wú)論怎樣對(duì)代碼做最好的優(yōu)化也不及對(duì)一張圖片做一次壓縮好
。所以壓縮圖片成了性能優(yōu)化里最常見的操作,不管是手動(dòng)壓縮圖片還是自動(dòng)壓縮圖片,在項(xiàng)目開發(fā)過(guò)程中必須得有。
自動(dòng)壓縮圖片通常在webpack
構(gòu)建項(xiàng)目時(shí)接入一些第三方Loader&Plugin
來(lá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
開發(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)生的坑》,專門解決這些因?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)不清晰?。?/p>
此時(shí)可能有些同學(xué)已轉(zhuǎn)戰(zhàn)到手動(dòng)壓縮圖片了。比較好用的圖片壓縮工具無(wú)非就是以下幾個(gè),若有更好用的工具麻煩在評(píng)論里補(bǔ)充喔!同時(shí)筆者也整理出它們的區(qū)別,供各位同學(xué)參考。
工具 | 開源 | 收費(fèi) | API | 免費(fèi)體驗(yàn) |
---|---|---|---|---|
QuickPicture | ?? | ?? | ?? | 可壓縮類型較多,壓縮質(zhì)感較好,有體積限制,有數(shù)量限制 |
ShrinkMe | ?? | ?? | ?? | 可壓縮類型較多,壓縮質(zhì)感一般,無(wú)數(shù)量限制,有體積限制 |
Squoosh | ?? | ?? | ?? | 可壓縮類型較少,壓縮質(zhì)感一般,無(wú)數(shù)量限制,有體積限制 |
TinyJpg | ?? | ?? | ?? | 可壓縮類型較少,壓縮質(zhì)感很好,有數(shù)量限制,有體積限制 |
TinyPng | ?? | ?? | ?? | 可壓縮類型較少,壓縮質(zhì)感很好,有數(shù)量限制,有體積限制 |
Zhitu | ?? | ?? | ?? | 可壓縮類型一般,壓縮質(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)后就沒有限制了。再來(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勝出。
手動(dòng)
jpg
和png
20
張5M
TinyJpg/TinyPng使用智能有損壓縮技術(shù)將圖片體積降低,選擇性地減少圖片中相似顏色,只需很少字節(jié)就能保存數(shù)據(jù)。對(duì)視覺影響幾乎不可見,但是在文件體積上就有很大的差別。而使用到智能有損壓縮技術(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ú)法追趕得上的。
查閱相關(guān)資料,發(fā)現(xiàn)TinyJpg/TinyPng暫時(shí)還未開源其壓縮算法,不過(guò)提供了適合開發(fā)者使用的API。有興趣的同學(xué)可到其開發(fā)API文檔瞧瞧。
在Node
方面,TinyJpg/TinyPng官方提供了tinify作為壓縮圖片的核心JS庫(kù),使用很簡(jiǎn)單,看文檔吧??墒菗Q成開發(fā)API還是逃不過(guò)收費(fèi),你是想包月呢還是免費(fèi)呢,想免費(fèi)的話就繼續(xù)往下看,土豪隨意!
筆者也是經(jīng)常使用TinyJpg/TinyPng的程序猿,收費(fèi),那是不可能的??。尋找突破口,解決問(wèn)題,是作為一位程序猿最基本的素養(yǎng)。我們需明確什么問(wèn)題,需解決什么問(wèn)題
。
從上述得知,只需對(duì)TinyJpg/TinyPng原有功能改造成以下功能。
jpg
和png
5M
自動(dòng)處理
對(duì)于前端開發(fā)者來(lái)說(shuō),這種無(wú)腦的上傳下載操作必須得自動(dòng)化,省事省心省力。但是這個(gè)操作得結(jié)合webpack
來(lái)處理,到底是開發(fā)成Loader
還是Plugin
,后面再分析。不過(guò)細(xì)心的同學(xué)看標(biāo)題就知道用什么方式處理了。
壓縮類型
gif
壓縮后一般都會(huì)失真,svg
通常用在矢量圖標(biāo)上很少用在場(chǎng)景圖片上,webp
由于兼容性問(wèn)題很少被使用,故能壓縮jpg
和png
就足夠了。在過(guò)濾圖片時(shí),使用path模塊
判斷文件類型是否為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è)能理解,也沒必要搞一張那么大的圖片,多浪費(fèi)帶寬流量和加載時(shí)間啊。在上傳圖片時(shí),使用fs模塊
判斷文件體積是否超過(guò)5M
,是則不上傳,否則繼續(xù)上傳。當(dāng)然,交給TinyJpg/TinyPng接口判斷也行。
輸出信息
壓縮成功與否得讓別人知道,輸出原始大小、壓縮大小、壓縮率和錯(cuò)誤提示等,讓別人清楚這些處理信息。
通過(guò)上述抽絲剝繭的分析,那么就開始著手編碼了。
隨機(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)求頭字段。打開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
是筆者開發(fā)的一個(gè)Web/Node
通用函數(shù)工具庫(kù),包含常規(guī)的工具函數(shù),助你少寫更多通用代碼。詳情請(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ì)一步一步分析。
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
組成
構(gòu)建機(jī)制
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)容寫入文件系統(tǒng)Loader用于轉(zhuǎn)換模塊源碼,筆者將其翻譯為轉(zhuǎn)換器
。Loader
可將所有類型文件轉(zhuǎn)換為webpack
能夠處理的有效模塊,然后利用webpack
的打包能力對(duì)它們進(jìn)行二次處理。
Loader
具有以下特點(diǎn):
只完成一種轉(zhuǎn)換
)Loader
將所有類型文件轉(zhuǎn)換為應(yīng)用程序的依賴圖譜可直接引用的模塊,所以Loader
可用于編譯一些文件,例如pug → html
、sass → css
、less → css
、es5 → es6
、ts → js
等。
處理一個(gè)文件可使用多個(gè)Loader
,Loader
的執(zhí)行順序和配置順序是相反的,即末尾Loader
最先執(zhí)行,開頭Loader
最后執(zhí)行。最先執(zhí)行的Loader
接收源文件內(nèi)容作為參數(shù),其它Loader
接收前一個(gè)執(zhí)行的Loader
的返回值作為參數(shù),最后執(zhí)行的Loader
會(huì)返回該文件的轉(zhuǎn)換結(jié)果。一句話概括:富土康流水線廠工。
Loader
開發(fā)思路總結(jié)如下:
module.exports
導(dǎo)出一個(gè)函數(shù)
source
(源文件內(nèi)容)return
返回最終轉(zhuǎn)換結(jié)果(字符串形式)編寫Loader時(shí)要遵循單一職責(zé)原則,每個(gè)Loader只做一種轉(zhuǎn)換工作
Plugin用于擴(kuò)展執(zhí)行范圍更廣的任務(wù),筆者將其翻譯為擴(kuò)展器
。Plugin
的范圍很廣,在Webpack構(gòu)建流程
里從開始到結(jié)束都能找到時(shí)機(jī)作為插入點(diǎn),只要你想不到?jīng)]有你做不到。所以筆者認(rèn)為Plugin
的功能比Loader
更加強(qiáng)大。
Plugin
具有以下特點(diǎn):
webpack
運(yùn)行生命周期中廣播的事件webpack
提供的API改變輸出結(jié)果webpack
的Tapable事件流機(jī)制保證Plugin的有序性在webpack
運(yùn)行生命周期中會(huì)廣播出許多事件,Plugin
可監(jiān)聽這些事件并在合適時(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)聽webpack
廣播事件,當(dāng)捕抓到指定事件后,會(huì)通過(guò)Compilation對(duì)象
操作相關(guān)業(yè)務(wù)邏輯。一句話概括:自己看著辦。
Plugin
開發(fā)思路總結(jié)如下:
module.exports
導(dǎo)出一個(gè)函數(shù)或類
函數(shù)原型或類
上綁定apply()
訪問(wèn)Compiler對(duì)象
apply()
中指定一個(gè)綁定到webpack
自身的事件鉤子webpack
提供的API處理資源(可引入第三方模塊擴(kuò)展功能)webpack
提供的方法返回該資源傳給每個(gè)Plugin的Compiler和Compilation都是同一個(gè)引用,若修改它們身上的屬性會(huì)影響后面的Plugin,所以需謹(jǐn)慎操作
本質(zhì)
Loader
本質(zhì)是一個(gè)函數(shù),轉(zhuǎn)換接收內(nèi)容,返回轉(zhuǎn)換結(jié)果Plugin
本質(zhì)是一個(gè)類,監(jiān)聽webpack
運(yùn)行生命周期中廣播的事件,在合適時(shí)機(jī)通過(guò)webpack
提供的API改變輸出結(jié)果配置
Loader
在module.rule
中配置,類型是數(shù)組,每一項(xiàng)對(duì)應(yīng)一個(gè)模塊解析規(guī)則Plugin
在plugin
中配置,類型是數(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)說(shuō)還是沒有變化。Loader
的轉(zhuǎn)換過(guò)程是附屬在整個(gè)Webpack構(gòu)建流程
中的,意味著打包時(shí)間包含了壓縮圖片的時(shí)間成本,對(duì)于追求webpack
性能優(yōu)化來(lái)說(shuō)實(shí)屬有點(diǎn)違背原則。而Plugin
恰好是監(jiān)聽webpack
運(yùn)行生命周期中廣播的事件,在合適時(shí)機(jī)通過(guò)webpack
提供的API改變輸出結(jié)果,所以可在整個(gè)Webpack構(gòu)建流程
完成后(全部打包文件輸出完成后)插入壓縮圖片的操作。換句話說(shuō),打包時(shí)間不再包含壓縮圖片的時(shí)間成本,打包完成后該干嘛就干嘛,還能干嘛,壓縮圖片啊。
所以依據(jù)需求情況,Plugin
作為首選。
依據(jù)上述Plugin
開發(fā)思路,那么就開始著手編碼了。
筆者把這個(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
util
安裝項(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.jsconst 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.jsconst { 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ù)或類
// index.jsmodule.exports = class TinyimgWebpackPlugin {};
在函數(shù)原型或類
上綁定apply()
訪問(wèn)Compiler對(duì)象
// index.jsmodule.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)配置。
在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.jsconst 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.jsconst 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.jsconst 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)建流程
完成后,所以沒有什么可返回了,故不作處理。
控制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é)的開發(fā)思路一步一步來(lái)完成編碼,其實(shí)是挺簡(jiǎn)單的。若需開發(fā)一些跟自己項(xiàng)目相關(guān)的Plugin
,還是需多多熟悉Webpack Compiler Hooks API文檔,相信各位同學(xué)都能手戳一個(gè)完美的Plugin
出來(lái)。
tinyimg-webpack-plugin
源碼請(qǐng)戳這里查看,Star一個(gè)如何,嘻嘻。
整個(gè)Plugin
開發(fā)完成,接下來(lái)需走一遍測(cè)試流程,看能不能把這個(gè)壓縮圖片的擴(kuò)展器
跑通。相信各位同學(xué)都是一位標(biāo)準(zhǔn)的Webpack配置工程師
,可自行編寫測(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ā)布到NPM倉(cāng)庫(kù)
上非常簡(jiǎn)單,僅需幾行命令。若還沒注冊(cè),趕緊去NPM
上注冊(cè)一個(gè)賬號(hào)。若當(dāng)前鏡像為淘寶鏡像
,需執(zhí)行npm config set registry https://registry.npmjs.org/
切換回源鏡像。
接下來(lái)一波操作就可完成發(fā)布了。
cd my-plugin
npm login
npm whoami
npm publish
npm logout
若不想牢記這么多命令,可用筆者開發(fā)的pkg-master
一鍵發(fā)布,若存在某些錯(cuò)誤會(huì)立馬中斷發(fā)布并提示錯(cuò)誤信息,是一個(gè)非常好用的集成創(chuàng)建和發(fā)布的NPM模塊管理工具。詳情請(qǐng)查看文檔,順便給一個(gè)Star以作鼓勵(lì)。
安裝
npm i -g pkg-master
使用
命令 | 縮寫 | 功能 | 描述 |
---|---|---|---|
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)境下開啟 |
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è)零配置開箱即用的React/Vue應(yīng)用自動(dòng)化構(gòu)建腳手架
bruce-cli
是一個(gè)React/Vue應(yīng)用自動(dòng)化構(gòu)建腳手架,其零配置開箱即用的優(yōu)點(diǎn)非常適合入門級(jí)、初中級(jí)、快速開發(fā)項(xiàng)目的前端同學(xué)使用,還可通過(guò)創(chuàng)建brucerc.js
文件來(lái)覆蓋其默認(rèn)配置,只需專注業(yè)務(wù)代碼的編寫無(wú)需關(guān)注構(gòu)建代碼的編寫,讓項(xiàng)目結(jié)構(gòu)更簡(jiǎn)潔。使用時(shí)記得查看文檔喲,喜歡的話給個(gè)Star。
當(dāng)然,筆者已將tinyimg-webpack-plugin
集成到bruce-cli
中,零配置開箱即用走起。
總體來(lái)說(shuō)開發(fā)一個(gè)Webpack Plugin
不難,只需好好分析需求,了解webpack
運(yùn)行生命周期中廣播的事件,編寫自定義Plugin
在合適時(shí)機(jī)通過(guò)webpack
提供的API改變輸出結(jié)果。
若覺得tinyimg-webpack-plugin
對(duì)你有幫助,可在Issue上提出你的寶貴建議
,筆者會(huì)認(rèn)真閱讀并整合你的建議。喜歡tinyimg-webpack-plugin
的請(qǐng)給一個(gè)Star,或Fork本項(xiàng)目到自己的Github
上,根據(jù)自身需求定制功能。
聯(lián)系客服