前言
曾經發表過一篇效能優化的文章《前端效能優化指南》,筆者總結了一些在專案開發過程中使用過的效能優化經驗。說句真話,效能優化可能在面試過程中會有用,實際在專案開發過程中可能沒幾個同學會注意這些效能優化的細節。
若經常關注效能優化的話題,可能會發現無論怎樣對程式碼做最好的優化也不及對一張圖片做一次壓縮好
。所以壓縮圖片成了效能優化裡最常見的操作,不管是手動壓縮圖片還是自動壓縮圖片,在專案開發過程中必須得有。
自動壓縮圖片通常在webpack
構建專案時接入一些第三方Loader&Plugin
來處理。開啟Github
,搜素webpack image
等關鍵字,Star最多還是image-webpack-loader
和imagemin-webpack-plugin
這兩個Loader&Plugin
。很多同學可能都會選擇它們,方便快捷,簡單易用,無腦接入。
可是,這兩個Loader&Plugin
存在一些特別問題,它們都是基於imagemin
開發的。imagemin
的某些依賴託管在國外伺服器,在npm i xxx
安裝它們時會預設走GitHub Releases
的託管地址,若不是規範上網,你們是不可能安裝得上的,即使規範上網也不一定安裝得上。所以筆者又刨根到底發表了一篇關於NPM映象處理的文章《聊聊NPM映象那些險象環生的坑》,專門解決這些因為網路環境而導致安裝失敗的問題。除了這個安裝問題,imagemin
還存在另一個大問題,就是壓縮質感損失得比較嚴重,圖片體積越大越明顯,壓縮出來的圖片總有幾張是失真的,而且總體壓縮率不是很高。這樣在交付專案時有可能被細心的QA小姐姐抓個正著,怎麼和設計圖對比起來不清晰啊!
工具
圖片壓縮工具
此時可能有些同學已轉戰到手動壓縮圖片了。比較好用的圖片壓縮工具無非就是以下幾個,若有更好用的工具麻煩在評論裡補充喔!同時筆者也整理出它們的區別,供各位同學參考。
工具 | 開源 | 收費 | API | 免費體驗 |
---|---|---|---|---|
QuickPicture | ✖️ | ✔️ | ✖️ | 可壓縮型別較多,壓縮質感較好,有體積限制,有數量限制 |
ShrinkMe | ✖️ | ✖️ | ✖️ | 可壓縮型別較多,壓縮質感一般,無數量限制,有體積限制 |
Squoosh | ✔️ | ✖️ | ✔️ | 可壓縮型別較少,壓縮質感一般,無數量限制,有體積限制 |
TinyJpg | ✖️ | ✔️ | ✔️ | 可壓縮型別較少,壓縮質感很好,有數量限制,有體積限制 |
TinyPng | ✖️ | ✔️ | ✔️ | 可壓縮型別較少,壓縮質感很好,有數量限制,有體積限制 |
Zhitu | ✖️ | ✖️ | ✖️ | 可壓縮型別一般,壓縮質感一般,有數量限制,有體積限制 |
從上述表格對比可看出,免費體驗都會存在體積限制
,這個可理解,即使收費也一樣,畢竟每個人都上傳單張10多M的圖片,哪個伺服器能受得了。再來就是數量限制
,一次只能上傳20張,好像有個規律,壓縮質感好就限制數量,否則就不限制數量,當然收費後就沒有限制了。再來就是可壓縮型別
,圖片型別一般是jpg
、png
、gif
、svg
和webp
,gif
壓縮後一般都會失真,svg
通常用在向量圖示上很少用在場景圖片上,webp
由於相容性問題很少被使用,故能壓縮jpg
和png
就足夠了。當然壓縮質感
是最優考慮,綜上所述,大部分同學都會選擇TinyJpg和TinyPng,其實它倆就是兄弟,出自同一廠商。
在筆者公眾號的微信討論群裡發起了一個簡單的投票,最終還是TinyJpg和TinyPng勝出。
TinyJpg/TinyPng存在問題
- 上傳下載全靠
手動
- 只能壓縮
jpg
和png
- 每次只能壓縮
20
張 - 每張體積最大不能超過
5M
- 視覺化處理資訊不是特別齊全
TinyJpg/TinyPng壓縮原理
TinyJpg/TinyPng使用智慧有失真壓縮技術將圖片體積降低,選擇性地減少圖片中相似顏色,只需很少位元組就能儲存資料。對視覺影響幾乎不可見,但是在檔案體積上就有很大的差別。而使用到智慧有失真壓縮技術
被稱為量化。
TinyJpg/TinyPng在壓縮png檔案
時效果更顯著。掃描圖片中相似顏色並將其合併,通過減少顏色數量將24位png檔案
轉換成體積更小的8位png檔案
,丟棄所有不必要的後設資料。
大部分png檔案
都有50%~70%
的壓縮率,即使視力再好也很難區分出來。使用優化過的圖片可減少頻寬流量和載入時間,整個網站使用到的圖片經TinyJpg/TinyPng壓縮一遍,其成效是再多的程式碼優化也無法追趕得上的。
TinyJpg/TinyPng開發API
查閱相關資料,發現TinyJpg/TinyPng暫時還未開源其壓縮演算法,不過提供了適合開發者使用的API。有興趣的同學可到其開發API文件瞧瞧。
在Node
方面,TinyJpg/TinyPng官方提供了tinify作為壓縮圖片的核心JS庫,使用很簡單,看文件吧。可是換成開發API還是逃不過收費,你是想包月呢還是免費呢,想免費的話就繼續往下看,土豪隨意!
實現
筆者也是經常使用TinyJpg/TinyPng的程式猿,收費,那是不可能的?。尋找突破口,解決問題,是作為一位程式猿最基本的素養。我們需明確什麼問題,需解決什麼問題
。
分析
從上述得知,只需對TinyJpg/TinyPng原有功能改造成以下功能。
- 上傳下載全自動
- 可壓縮
jpg
和png
- 沒有數量限制
- 存在體積限制,最大體積不能超過
5M
- 壓縮成功與否輸出詳細資訊
自動處理
對於前端開發者來說,這種無腦的上傳下載操作必須得自動化,省事省心省力。但是這個操作得結合webpack
來處理,到底是開發成Loader
還是Plugin
,後面再分析。不過細心的同學看標題就知道用什麼方式處理了。
壓縮型別
gif
壓縮後一般都會失真,svg
通常用在向量圖示上很少用在場景圖片上,webp
由於相容性問題很少被使用,故能壓縮jpg
和png
就足夠了。在過濾圖片時,使用path模組
判斷檔案型別是否為jpg
和png
,是則繼續處理,否則不處理。
數量限制
數量限制當然是不能存在的,萬一專案裡超過20張圖片,那不是得分批處理,這個不能有。對於這種無需登入狀態就能處理一些使用者檔案的網站,通常都會通過IP來限制使用者的操作次數。有些同學可能會說,重新整理頁面不就行了嗎,每次壓縮20張圖片,再重新整理再壓縮,萬一有500張圖片呢,你就重新整理25次嗎,這樣很好玩是吧!
由於大多數Web架構很少會將應用伺服器直接對外提供服務,一般都會設定一層Nginx
作為代理和負載均衡,有的甚至可能有多層代理。鑑於大多數Web架構都是使用Nginx
作為反向代理,使用者請求不是直接請求應用伺服器的,而是通過Nginx設定的統一接入層將使用者請求轉發到伺服器的,所以可通過設定HTTP請求頭欄位X-Forwarded-For
來偽造IP。
X-Forwarded-For指用來識別通過代理
或負載均衡
的方式連線到Web伺服器的客戶端最原始的IP地址的HTTP請求頭欄位。當然,這個IP也不是一成不變的,每次請求都需隨機更換IP,騙過應用伺服器。若應用伺服器增加了偽造IP識別,那可能就無法繼續使用隨機IP了。
體積限制
體積限制這個能理解,也沒必要搞一張那麼大的圖片,多浪費頻寬流量和載入時間啊。在上傳圖片時,使用fs模組
判斷檔案體積是否超過5M
,是則不上傳,否則繼續上傳。當然,交給TinyJpg/TinyPng介面判斷也行。
輸出資訊
壓縮成功與否得讓別人知道,輸出原始大小、壓縮大小、壓縮率和錯誤提示等,讓別人清楚這些處理資訊。
編碼
通過上述抽絲剝繭的分析,那麼就開始著手編碼了。
隨機生成HTTP請求頭
既然可通過X-Forwarded-For
來偽造IP,那麼得有一個隨機生成HTTP請求頭欄位的函式,每次請求介面時都隨機生成相關的請求頭欄位。開啟tinyjpg.com或tinypng.com上傳一張圖片,通過Chrome DevTools
分析Network
發現其請求介面是web/shrink
。另外每次請求也不要集中在單一的hostname
上,隨機派發到tinyjpg.com
或tinypng.com
上會更好。通過封裝RandomHeader
函式隨機生成請求頭資訊,後續使用https模組
以RandomHeader()
生成的配置作為入參進行請求。
trample
是筆者開發的一個Web/Node
通用函式工具庫,包含常規的工具函式,助你少寫更多通用程式碼。詳情請檢視文件,順便給一個Star以作鼓勵。
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
封裝上傳圖片
和下載圖片
的函式,方便後續使用Async/Await
同步化非同步程式碼。以下函式的具體斷點除錯就不說了,有興趣的同學自行除錯函式的入參和出參哈!
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();
});
}
壓縮圖片
通過上傳圖片
函式獲取壓縮後的圖片資訊,再依據圖片資訊通過下載圖片
函式生成本地檔案。
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);
}
}
壓縮目標圖片
完成上述步驟對應的函式後,就能自由壓縮圖片了,以下使用一張圖片作為演示。
const Ora = require("ora");
(async() => {
const spinner = Ora("Image is compressing......").start();
const res = await CompressImg("src/pig.png");
spinner.stop();
console.log(res);
})();
你看,壓縮完後笨豬都變帥豬了,能電眼的豬都是好豬。原始碼請檢視compress-img。
若壓縮指定資料夾裡符合條件的所有圖片,可通過fs模組
獲取圖片並使用map()
將單個圖片路徑對映為CompressImg(path)
,再通過Promise.all()
操作即可。在這裡就不貼程式碼了,當作思考題,自行完成。
將上述壓縮圖片的功能封裝成Loader
還是Plugin
呢?接下來會一步一步分析。
Loader&Plugin
webpack
是一個前端資源打包工具,它根據模組依賴關係進行靜態分析,然後將這些模組按照指定規則生成對應的靜態資源。
網上一大堆webpack
教程,筆者就不再花大篇幅囉嗦了,相信各位同學都是一位標準的Webpack配置工程師
。以下簡單回顧一次webpack
的組成、構建機制和構建流程,相信也能從這些知識點中定位出Loader
和Plugin
在Webpack構建流程
中是處於一個什麼樣的角色地位。
本文所說的webpack都是基於webpack v4
組成
- Entry:入口
- Output:輸出
- Loader:轉換器
- Plugin:擴充套件器
- Mode:模式
- Module:模組
- Target:目標
構建機制
- 通過Babel轉換程式碼並生成單個檔案依賴
- 從入口檔案開始遞迴分析並生成依賴圖譜
- 將各個引用模組打包成一個立即執行函式
- 將最終bundle檔案寫入
bundle.js
中
構建流程
初始
初始引數
:合併命令列和配置檔案的引數
編譯
執行編譯
:依據引數初始Compiler物件
,載入所有Plugin
,執行run()
確定入口
:依據配置檔案找出所有入口檔案編譯模組
:依據入口檔案找出所有依賴模組關係,呼叫所有Loader
進行轉換生成圖譜
:得到每個模組轉換後的內容及其之間的依賴關係
輸出
輸出資源
:依據依賴關係將模組組裝成塊再組裝成包(module → chunk → bundle
)生成檔案
:依據配置檔案將確認輸出的內容寫入檔案系統
Loader
Loader用於轉換模組原始碼,筆者將其翻譯為轉換器
。Loader
可將所有型別檔案轉換為webpack
能夠處理的有效模組,然後利用webpack
的打包能力對它們進行二次處理。
Loader
具有以下特點:
- 單一職責原則(
只完成一種轉換
) - 轉換接收內容
- 返回轉換結果
- 支援鏈式呼叫
Loader
將所有型別檔案轉換為應用程式的依賴圖譜可直接引用的模組,所以Loader
可用於編譯一些檔案,例如pug → html
、sass → css
、less → css
、es5 → es6
、ts → js
等。
處理一個檔案可使用多個Loader
,Loader
的執行順序和配置順序是相反的,即末尾Loader
最先執行,開頭Loader
最後執行。最先執行的Loader
接收原始檔內容作為引數,其它Loader
接收前一個執行的Loader
的返回值作為引數,最後執行的Loader
會返回該檔案的轉換結果。一句話概括:富土康流水線廠工。
Loader
開發思路總結如下:
- 通過
module.exports
匯出一個函式
- 函式第一預設引數為
source
(原始檔內容) - 在函式體中處理資源(可引入第三方模組擴充套件功能)
- 通過
return
返回最終轉換結果(字串形式)
編寫Loader時要遵循單一職責原則,每個Loader只做一種轉換工作
Plugin
Plugin用於擴充套件執行範圍更廣的任務,筆者將其翻譯為擴充套件器
。Plugin
的範圍很廣,在Webpack構建流程
裡從開始到結束都能找到時機作為插入點,只要你想不到沒有你做不到。所以筆者認為Plugin
的功能比Loader
更加強大。
Plugin
具有以下特點:
- 監聽
webpack
執行生命週期中廣播的事件 - 在合適時機通過
webpack
提供的API改變輸出結果 webpack
的Tapable事件流機制保證Plugin的有序性
在webpack
執行生命週期中會廣播出許多事件,Plugin
可監聽這些事件並在合適時機通過webpack
提供的API改變輸出結果。在webpack
啟動後,在讀取配置過程中執行new MyPlugin(opts)
初始化自定義Plugin
獲取其例項,在初始化Compiler物件
後,通過compiler.hooks.event.tap(PLUGIN_NAME, callback)
監聽webpack
廣播事件,當捕抓到指定事件後,會通過Compilation物件
操作相關業務邏輯。一句話概括:自己看著辦。
Plugin
開發思路總結如下:
- 通過
module.exports
匯出一個函式或類
- 在
函式原型或類
上繫結apply()
訪問Compiler物件
- 在
apply()
中指定一個繫結到webpack
自身的事件鉤子 - 在事件鉤子中通過
webpack
提供的API處理資源(可引入第三方模組擴充套件功能) - 通過
webpack
提供的方法返回該資源
傳給每個Plugin的Compiler和Compilation都是同一個引用,若修改它們身上的屬性會影響後面的Plugin,所以需謹慎操作
Loader/Plugin區別
本質
Loader
本質是一個函式,轉換接收內容,返回轉換結果Plugin
本質是一個類,監聽webpack
執行生命週期中廣播的事件,在合適時機通過webpack
提供的API改變輸出結果
配置
Loader
在module.rule
中配置,型別是陣列,每一項對應一個模組解析規則Plugin
在plugin
中配置,型別是陣列,每一項對應一個擴充套件器例項,引數通過建構函式傳入
封裝
分析
從上述可知Loader
和Plugin
在角色定位和執行機制上有很多不一樣,到底如何選擇呢?各有各好,當然還是需分析後進行選擇。
Loader
在webpack
中扮演著轉換器的角色,用於轉換模組原始碼,簡單理解就是將檔案轉換成另外形式的檔案,而本文主題是壓縮圖片
,jpg
壓縮後還是jpg
,png
壓縮後還是png
,在檔案型別上來說還是沒有變化。Loader
的轉換過程是附屬在整個Webpack構建流程
中的,意味著打包時間包含了壓縮圖片的時間成本,對於追求webpack
效能優化來說實屬有點違背原則。而Plugin
恰好是監聽webpack
執行生命週期中廣播的事件,在合適時機通過webpack
提供的API改變輸出結果,所以可在整個Webpack構建流程
完成後(全部打包檔案輸出完成後)插入壓縮圖片的操作。換句話說,打包時間不再包含壓縮圖片的時間成本,打包完成後該幹嘛就幹嘛,還能幹嘛,壓縮圖片啊。
所以依據需求情況,Plugin
作為首選。
編碼
依據上述Plugin
開發思路,那麼就開始著手編碼了。
筆者把這個壓縮圖片的Plugin
命名為tinyimg-webpack-plugin,tinyimg
意味著TinyJpg和TinyPng合體。
新建專案,目錄結構如下。
tinyimg-webpack-plugin
├─ src
│ ├─ index.js
│ ├─ schema.json
├─ util
│ ├─ getting.js
│ ├─ setting.js
├─ .gitignore
├─ .npmignore
├─ license
├─ package.json
├─ readme.md
主要檔案如下。
src
- index.js:入口函式
- schema.json:引數校驗
util
- getting.js:常量集合
- setting.js:函式集合
安裝專案所需模組,和上述compress-img的依賴一致,額外安裝schema-utils
用於校驗Plugin
引數是否符合規定。
npm i chalk figures ora schema-utils trample
封裝常量集合和函式集合
將上述compress-img
的TINYIMG_URL
和RandomHeader()
封裝到工具集合中,其中常量集合增加IMG_REGEXP
和PLUGIN_NAME
兩個常量。
// 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
};
通過module.exports
匯出一個函式或類
// index.js
module.exports = class TinyimgWebpackPlugin {};
在函式原型或類
上繫結apply()
訪問Compiler物件
// index.js
module.exports = class TinyimgWebpackPlugin {
apply(compiler) {
// Do Something
}
};
在apply()
中指定一個繫結到webpack
自身的事件鉤子
從上述分析中可知,在全部打包檔案輸出完成後插入壓縮圖片的操作,所以應該選擇該時機對應的事件鉤子。從Webpack Compiler Hooks API文件中可發現,emit
正是這個Plugin
所需的事件鉤子。emit
在生成資源到輸出目錄前執行,此刻可獲取所有圖片檔案的資料和輸出路徑。
為了方便在特定條件下啟用功能
和列印日誌
,所以設定相關配置。
- enabled:是否啟用功能
- logged:是否列印日誌
在apply()
中處理相關業務邏輯,可能使用到Plugin
的入參,那麼就得對引數進行校驗。定義一個Plugin
的Schema
,通過schema-utils
來校驗Plugin
的入參。
// 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
在整合過程中會有一些小修改,各位同學可對比看看哪些細節發生了變化。
// 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();
});
}
};
在事件鉤子中通過webpack
提供的API處理資源
通過compilation.assets
獲取全部打包檔案的物件,篩選出jpg
和png
,使用map()
將單個圖片資料對映為this.compressImg(file)
,再通過Promise.all()
操作即可。
整個業務邏輯結合了Promise
和Async/Await
兩個ES6常用特性,它倆組合起來玩非同步程式設計極其有趣,關於它倆更多細節可檢視筆者這篇4000點贊量和14萬閱讀量的文章《1.5萬字概括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) { ... }
};
通過webpack
提供的方法返回該資源
由於壓縮圖片的操作是在整個Webpack構建流程
完成後,所以沒有什麼可返回了,故不作處理。
控制webpack
依賴版本
由於tinyimg-webpack-plugin
基於webpack v4
,所以需在package.json
中新增peerDependencies
,用來告知安裝該Plugin
的模組必須存在peerDependencies
裡的依賴。
{
"peerDependencies": {
"webpack": ">= 4.0.0",
"webpack-cli": ">= 3.0.0"
}
}
總結
按照上述總結的開發思路一步一步來完成編碼,其實是挺簡單的。若需開發一些跟自己專案相關的Plugin
,還是需多多熟悉Webpack Compiler Hooks API文件,相信各位同學都能手戳一個完美的Plugin
出來。
tinyimg-webpack-plugin
原始碼請戳這裡檢視,Star一個如何,嘻嘻。
測試
整個Plugin
開發完成,接下來需走一遍測試流程,看能不能把這個壓縮圖片的擴充套件器
跑通。相信各位同學都是一位標準的Webpack配置工程師
,可自行編寫測試Demo驗證你們的Plugin
。
在根目錄下建立test資料夾
,並按照以下目錄結構加入檔案。
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
安裝測試Demo所需的webpack
相關配置模組。
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
程式碼,程式碼量有點多,直接貼連結好了,請戳這裡。
最後在package.json
中的scripts
插入以下npm scripts
,然後執行npm run test
除錯測試Demo。
{
"scripts": {
"test": "webpack --config test/webpack.config.js"
}
}
釋出
釋出到NPM倉庫
上非常簡單,僅需幾行命令。若還沒註冊,趕緊去NPM
上註冊一個賬號。若當前映象為淘寶映象
,需執行npm config set registry https://registry.npmjs.org/
切換回源映象。
接下來一波操作就可完成釋出了。
- 進入目錄:
cd my-plugin
- 登入賬號:
npm login
- 校驗狀態:
npm whoami
- 釋出模組:
npm publish
- 退出賬號:
npm logout
若不想牢記這麼多命令,可用筆者開發的pkg-master
一鍵釋出,若存在某些錯誤會立馬中斷髮布並提示錯誤資訊,是一個非常好用的整合建立和釋出的NPM模組管理工具。詳情請檢視文件,順便給一個Star以作鼓勵。
安裝
npm i -g pkg-master
使用
命令 | 縮寫 | 功能 | 描述 |
---|---|---|---|
pkg-master create | pkg-master c | 建立模組 | 生成模組的基礎檔案 |
pkg-master publish | pkg-master p | 釋出模組 | 檢測NPM的執行環境 和賬號狀態 ,通過則自動釋出模組 |
接入
安裝
npm i tinyimg-webpack-plugin
使用
配置 | 功能 | 格式 | 描述 |
---|---|---|---|
enabled | 是否啟用功能 | true/false | 建議只在生產環境下開啟 |
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環境中使用
import TinyimgPlugin from "tinyimg-webpack-plugin";
export default {
plugins: [
new TinyimgPlugin({
enabled: process.env.NODE_ENV === "production",
logged: true
})
]
};
推薦一個零配置開箱即用的React/Vue應用自動化構建腳手架
bruce-cli
是一個React/Vue應用自動化構建腳手架,其零配置開箱即用的優點非常適合入門級、初中級、快速開發專案的前端同學使用,還可通過建立brucerc.js
檔案來覆蓋其預設配置,只需專注業務程式碼的編寫無需關注構建程式碼的編寫,讓專案結構更簡潔。使用時記得檢視文件喲,喜歡的話給個Star。
當然,筆者已將tinyimg-webpack-plugin
整合到bruce-cli
中,零配置開箱即用走起。
總結
總體來說開發一個Webpack Plugin
不難,只需好好分析需求,瞭解webpack
執行生命週期中廣播的事件,編寫自定義Plugin
在合適時機通過webpack
提供的API改變輸出結果。
若覺得tinyimg-webpack-plugin
對你有幫助,可在Issue上提出你的寶貴建議
,筆者會認真閱讀並整合你的建議。喜歡tinyimg-webpack-plugin
的請給一個Star,或Fork本專案到自己的Github
上,根據自身需求定製功能。