关于前端:嗯手搓一个TinyPng压缩图片的WebpackPlugin也SoEasy啦

44次阅读

共计 15921 个字符,预计需要花费 40 分钟才能阅读完成。

前言

已经发表过一篇性能优化的文章《前端性能优化指南》,笔者总结了一些在我的项目开发过程中应用过的性能优化教训。说句真话,性能优化可能在面试过程中会有用,理论在我的项目开发过程中可能没几个同学会留神这些性能优化的细节。

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

主动压缩图片通常在 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.com 或 tinypng.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 中,零配置开箱即用走起。

  • Github 地址:请戳这里
  • 官网文档:请戳这里
  • 掘金文档:请戳这里

总结

总体来说开发一个 Webpack Plugin 不难,只需好好剖析需要,理解 webpack 运行生命周期中播送的事件,编写 自定义 Plugin在适合机会通过 webpack 提供的 API 扭转输入后果。

若感觉 tinyimg-webpack-plugin 对你有帮忙,可在 Issue 上 提出你的贵重倡议 ,笔者会认真浏览并整合你的倡议。喜爱tinyimg-webpack-plugin 的请给一个 Star,或 Fork 本我的项目到本人的 Github 上,依据本身需要定制性能。

正文完
 0