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

前言

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

若常常關注性能優化的話題,可能會發現不管怎樣對代碼作最好的優化也不及對一張圖片作一次壓縮好。因此壓縮圖片成了性能優化裏最多見的操做,無論是手動壓縮圖片仍是自動壓縮圖片,在項目開發過程當中必須得有。html

自動壓縮圖片一般在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小姐姐抓個正着,怎麼和設計圖對比起來不清晰啊!node

工具

圖片壓縮工具

此時可能有些同窗已轉戰到手動壓縮圖片了。比較好用的圖片壓縮工具無非就是如下幾個,如有更好用的工具麻煩在評論裏補充喔!同時筆者也整理出它們的區別,供各位同窗參考。webpack

工具集合

工具 開源 收費 API 免費體驗
QuickPicture ✖️ ✔️ ✖️ 可壓縮類型較多,壓縮質感較好,有體積限制,有數量限制
ShrinkMe ✖️ ✖️ ✖️ 可壓縮類型較多,壓縮質感通常,無數量限制,有體積限制
Squoosh ✔️ ✖️ ✔️ 可壓縮類型較少,壓縮質感通常,無數量限制,有體積限制
TinyJpg ✖️ ✔️ ✔️ 可壓縮類型較少,壓縮質感很好,有數量限制,有體積限制
TinyPng ✖️ ✔️ ✔️ 可壓縮類型較少,壓縮質感很好,有數量限制,有體積限制
Zhitu ✖️ ✖️ ✖️ 可壓縮類型通常,壓縮質感通常,有數量限制,有體積限制

從上述表格對比可看出,免費體驗都會存在體積限制,這個可理解,即便收費也同樣,畢竟每一個人都上傳單張10多M的圖片,哪一個服務器能受得了。再來就是數量限制,一次只能上傳20張,好像有個規律,壓縮質感好就限制數量,不然就不限制數量,固然收費後就沒有限制了。再來就是可壓縮類型,圖片類型通常是jpgpnggifsvgwebpgif壓縮後通常都會失真,svg一般用在矢量圖標上不多用在場景圖片上,webp因爲兼容性問題不多被使用,故能壓縮jpgpng就足夠了。固然壓縮質感是最優考慮,綜上所述,大部分同窗都會選擇TinyJpgTinyPng,其實它倆就是兄弟,出自同一廠商。git

在筆者公衆號的微信討論羣裏發起了一個簡單的投票,最終仍是TinyJpgTinyPng勝出。es6

工具投票

TinyJpg/TinyPng存在問題
  • 上傳下載全靠手動
  • 只能壓縮jpgpng
  • 每次只能壓縮20
  • 每張體積最大不能超過5M
  • 可視化處理信息不是特別齊全
TinyJpg/TinyPng壓縮原理

TinyJpg/TinyPng使用智能有損壓縮技術將圖片體積下降,選擇性地減小圖片中類似顏色,只需不多字節就能保存數據。對視覺影響幾乎不可見,可是在文件體積上就有很大的差異。而使用到智能有損壓縮技術被稱爲量化github

TinyJpg/TinyPng在壓縮png文件時效果更顯著。掃描圖片中類似顏色並將其合併,經過減小顏色數量將24位png文件轉換成體積更小的8位png文件,丟棄全部沒必要要的元數據。web

大部分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 create pkg-master c 建立模塊 生成模塊的基礎文件
pkg-master publish pkg-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上,根據自身需求定製功能。

相關文章
相關標籤/搜索