嗯,手搓一个TinyPng压缩图片的WebpackPlugin也SoEasy啦

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

前言

曾经发表过一篇性能优化的文章《前端性能优化指南》,笔者总结了一些在项目开发过程中使用过的性能优化经验。说句真话,性能优化可能在面试过程中会有用,实际在项目开发过程中可能没几个同学会注意这些性能优化的细节。

若经常关注性能优化的话题,可能会发现无论怎样对代码做最好的优化也不及对一张图片做一次压缩好。所以压缩图片成了性能优化里最常见的操作,不管是手动压缩图片还是自动压缩图片,在项目开发过程中必须得有。

自动压缩图片通常在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 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上,根据自身需求定制功能。

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送
image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339