嗯,手搓一个TinyPng压缩图片的WebpackPlugin也
作者:JowayYoung
仓库:Github、CodePen
博客:掘金、思否、知乎、简书、头条、CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信哟
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权
前言
曾经发表过一篇性能优化的文章《前端性能优化指南》,笔者总结了一些在项目开发过程中使用过的性能优化经验。说句真话,性能优化可能在面试过程中会有用,实际在项目开发过程中可能没几个同学会注意这些性能优化的细节。
若经常关注性能优化的话题,可能会发现无论怎样对代码做最好的优化也不及对一张图片做一次压缩好
。所以压缩图片成了性能优化里最常见的操作,不管是手动压缩图片还是自动压缩图片,在项目开发过程中必须得有。
自动压缩图片通常在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
上,根据自身需求定制功能。
结语
❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章
关注公众号IQ前端
,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔
- 关注后回复
关键词
免费领取视频教程 - 关注后添加
我微信
拉你进技术交流群 - 欢迎关注
IQ前端
,更多CSS/JS开发技巧只在公众号推送