嗯,手搓一個TinyPng壓縮圖片的WebpackPlugin也SoEasy啦

JowayYoung發表於2020-08-10

前言

曾經發表過一篇效能優化的文章《前端效能優化指南》,筆者總結了一些在專案開發過程中使用過的效能優化經驗。說句真話,效能優化可能在面試過程中會有用,實際在專案開發過程中可能沒幾個同學會注意這些效能優化的細節。

若經常關注效能優化的話題,可能會發現無論怎樣對程式碼做最好的優化也不及對一張圖片做一次壓縮好。所以壓縮圖片成了效能優化裡最常見的操作,不管是手動壓縮圖片還是自動壓縮圖片,在專案開發過程中必須得有。

自動壓縮圖片通常在webpack構建專案時接入一些第三方Loader&Plugin來處理。開啟Github,搜素webpack image等關鍵字,Star最多還是image-webpack-loaderimagemin-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張,好像有個規律,壓縮質感好就限制數量,否則就不限制數量,當然收費後就沒有限制了。再來就是可壓縮型別,圖片型別一般是jpgpnggifsvgwebpgif壓縮後一般都會失真,svg通常用在向量圖示上很少用在場景圖片上,webp由於相容性問題很少被使用,故能壓縮jpgpng就足夠了。當然壓縮質感是最優考慮,綜上所述,大部分同學都會選擇TinyJpgTinyPng,其實它倆就是兄弟,出自同一廠商。

在筆者公眾號的微信討論群裡發起了一個簡單的投票,最終還是TinyJpgTinyPng勝出。

工具投票

TinyJpg/TinyPng存在問題
  • 上傳下載全靠手動
  • 只能壓縮jpgpng
  • 每次只能壓縮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原有功能改造成以下功能。

  • 上傳下載全自動
  • 可壓縮jpgpng
  • 沒有數量限制
  • 存在體積限制,最大體積不能超過5M
  • 壓縮成功與否輸出詳細資訊
自動處理

對於前端開發者來說,這種無腦的上傳下載操作必須得自動化,省事省心省力。但是這個操作得結合webpack來處理,到底是開發成Loader還是Plugin,後面再分析。不過細心的同學看標題就知道用什麼方式處理了。

壓縮型別

gif壓縮後一般都會失真,svg通常用在向量圖示上很少用在場景圖片上,webp由於相容性問題很少被使用,故能壓縮jpgpng就足夠了。在過濾圖片時,使用path模組判斷檔案型別是否為jpgpng,是則繼續處理,否則不處理。

數量限制

數量限制當然是不能存在的,萬一專案裡超過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.comtinypng.com上傳一張圖片,通過Chrome DevTools分析Network發現其請求介面是web/shrink。另外每次請求也不要集中在單一的hostname上,隨機派發到tinyjpg.comtinypng.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的組成、構建機制和構建流程,相信也能從這些知識點中定位出LoaderPluginWebpack構建流程中是處於一個什麼樣的角色地位。

本文所說的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 → htmlsass → cssless → csses5 → es6ts → js等。

處理一個檔案可使用多個LoaderLoader的執行順序和配置順序是相反的,即末尾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改變輸出結果
  • 配置

    • Loadermodule.rule中配置,型別是陣列,每一項對應一個模組解析規則
    • Pluginplugin中配置,型別是陣列,每一項對應一個擴充套件器例項,引數通過建構函式傳入

封裝

分析

從上述可知LoaderPlugin在角色定位和執行機制上有很多不一樣,到底如何選擇呢?各有各好,當然還是需分析後進行選擇。

Loaderwebpack中扮演著轉換器的角色,用於轉換模組原始碼,簡單理解就是將檔案轉換成另外形式的檔案,而本文主題是壓縮圖片jpg壓縮後還是jpgpng壓縮後還是png,在檔案型別上來說還是沒有變化。Loader的轉換過程是附屬在整個Webpack構建流程中的,意味著打包時間包含了壓縮圖片的時間成本,對於追求webpack效能優化來說實屬有點違背原則。而Plugin恰好是監聽webpack執行生命週期中廣播的事件,在合適時機通過webpack提供的API改變輸出結果,所以可在整個Webpack構建流程完成後(全部打包檔案輸出完成後)插入壓縮圖片的操作。換句話說,打包時間不再包含壓縮圖片的時間成本,打包完成後該幹嘛就幹嘛,還能幹嘛,壓縮圖片啊。

所以依據需求情況,Plugin作為首選。

編碼

依據上述Plugin開發思路,那麼就開始著手編碼了。

筆者把這個壓縮圖片的Plugin命名為tinyimg-webpack-plugintinyimg意味著TinyJpgTinyPng合體。

新建專案,目錄結構如下。

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-imgTINYIMG_URLRandomHeader()封裝到工具集合中,其中常量集合增加IMG_REGEXPPLUGIN_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的入參,那麼就得對引數進行校驗。定義一個PluginSchema,通過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-imgPlugin

在整合過程中會有一些小修改,各位同學可對比看看哪些細節發生了變化。

// 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獲取全部打包檔案的物件,篩選出jpgpng,使用map()將單個圖片資料對映為this.compressImg(file),再通過Promise.all()操作即可。

整個業務邏輯結合了PromiseAsync/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 createpkg-master c建立模組生成模組的基礎檔案
pkg-master publishpkg-master p釋出模組檢測NPM的執行環境賬號狀態,通過則自動釋出模組

釋出模組

接入

安裝

npm i tinyimg-webpack-plugin

使用
配置功能格式描述
enabled是否啟用功能true/false建議只在生產環境下開啟
logged是否列印日誌true/false列印處理資訊

webpack.config.jswebpack配置插入以下程式碼。

在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上,根據自身需求定製功能。

相關文章