当前位置:首页 > 技术分析 > 正文内容

「前端纯干货」原来TinyPNG可以这样玩

ruisui883周前 (04-07)技术分析18

作者:Chess

转发链接:
https://mp.weixin.qq.com/s/aRyEzYcJ7CvOaN6ll5C10Q

前言

前端er,又称为切图仔,平时经常需要用PSD导出PNG或JPG,但是导出来的的图片一般比较大,往往需要用一些其他工具压缩后再发布到生产环境。

以前常用的做法是,使用image-webpack-loader,在webpack打包项目时自动压缩图片。但是这loader是基于imagemin的,他的压缩导致画质损失比较严重,而且压缩率也不是很高。经常出现一种情况就是,在本地开发的时候,图片明明很清晰的,但是一旦发布到生产环境,因为经过了img-webapck-loader压缩,导致画质严重损失。也因为这个原因,美术小姐姐在验收项目的时候,常常会把我叫过去,说:哎呀,我的PSD明明是很清晰的呀,为什么你放到网站上去就那么模糊了呢。我说因为压缩过了呀。然后她说这个压缩工具不给力呀,就推荐了一个在线压缩工具给我:TinyPNG在线地址。

初体验tinypng真是把我惊呆了,一般的PNG压缩率竟然可以高达50%-70%,并且肉眼看不出来任何的画质损失!

TinyPNG的原理是将PNG24位真彩色图片压缩成PNG8位索引图片,从而做到基本不损失画质和观感。至于具体算法怎么实现的也没有深入研究守,有兴趣的可以自行查阅相关资料。

tinypg除了可以通过网页端上传图片,也提供了api的方式,所以可以使用node或其他脚本语言进行上传的,可以参考这里:
https://tinify.cn/developers/reference。

然而,这么好用的压缩工具有一个很明显缺陷的,就是每天只能正常压缩20张图片。超过20张之后,就会经常出来Too many files uploaded at once。如下红色的部分:

当然,这是针对免费版用户而言的。如果想要避免这个限制,你需要花25美金买一年的会员。当然,我们程序员大多数都是很吝啬的,就连到外面吃饭要不要加个卤蛋都要纠结半个小时的,更别说给25美金他了。

但是,程序员的特点是,都爱折腾!所以我决定要尝试着绕过这20张图片的上传限制。一般来说,这种免登录就可试用的系统,都是通过用户IP来限制用户的操作次数的,所以就决定从修改IP的方式来绕开这个限制。

最简单的作法是使用动态的代理IP,比如这里https://ip.ihuan.me/就有一些免费代理IP,但是,这些IP都是国外的,很慢的,我们的目的是要上传并压缩图片,使用这么慢的代理显然是不可行的。

所以就只能寻找其他的办法。就在陷入迷茫的时候,突然想到了一个事实:目前的web架构大多数都是通过nginx作为反向代理的,如下:

从上图可以看出,客户端并不是直接请求应用服务的,而是通过统一接入层(往往是nginx)来转发请求的。那么问题来,应用服务是如何获取到客户端的IP的呢?

玩过nginx的同学应该都知道,这个通用的解决方案就是X-Forwarded-For请求头。

简单的来说,就是所有的反向代理都实现一个统一的约定,在转发请求给下游服务之前,把请求代理的IP地址写入到X-Forwarded-For头中,形成了一个IP地址列:

X-Forwarded-For: client, proxy1, proxy2

这个方案虽然不是正式的HTTP协议,但已经成为了一个事实标准,基本上所有的反向代理服务都实现了这个功能,以确保下游的服务可以感知到经过的反向代理,并从中获取到用户的IP地址。

好了,既然应用服务是通过X-Forwarded-For头部来获取客户端IP的,那么我们能不能在客户端伪造这个头部,每次上传图片的时候都设置一个随机的IP呢?所以就决定尝试一下。结果发现,该方法确实可行!竟然可以欺骗tinypng的服务器,从而绕开了上传次数的限制。

完整代码如下:

const fs = require("fs");
const path = require("path");
const https = require("https");
const { URL } = require("url");

const cwd = process.cwd();

const root = cwd;
const exts = [".jpg", ".png"];
const max = 5200000; // 5MB == 5242848.754299136

const options = {
    method: "POST",
    hostname: "tinypng.com",
    path: "/web/shrink",
    headers: {
        rejectUnauthorized: false,
        "Postman-Token": Date.now(),
        "Cache-Control": "no-cache",
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
    }
};

fileList(root);

// 生成随机IP,赋值给X-Forwarded-For
function getRandomIP() {
    return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join(".");
}

// 获取文件列表
function fileList(folder) {
    fs.readdir(folder, (err, files) => {
        if (err) console.error(err);
        files.forEach(file => {
            fileFilter(path.join(folder, file));
        });
    });
}

// 过滤文件格式,返回所有jpg,png图片
function fileFilter(file) {
    fs.stat(file, (err, stats) => {
        if (err) return console.error(err);
        if (
            // 必须是文件,小于5MB,后缀 jpg||png
            stats.size <= max stats.isfile exts.includespath.extnamefile x-forwarded-for ip options.headersx-forwarded-for='getRandomIP();' fileuploadfile console.log file if stats.isdirectory filelistfile api error:bad requestmessage:request is invalid input: size: 887 type: imagepng output: size: 785 type: imagepng width: 81 height: 81 ratio: 0.885 url: https:tinypng.comweboutput7aztz90nq5p9545zch8gjzqg5ubdatd6 function fileuploadimg var req='https.request(options,' functionres res.ondata> {
            const obj = JSON.parse(buf.toString());
            if (obj.error) {
                console.log(`[${img}]:压缩失败!报错:${obj.message}`);
            } else {
                fileUpdate(img, obj);
            }
        });
    });

    req.write(fs.readFileSync(img), "binary");
    req.on("error", e => {
        console.error(e);
    });
    req.end();
}
// 该方法被循环调用,请求图片数据
function fileUpdate(imgpath, obj) {
    const outputDir = path.join(cwd, "output");
    imgpath = path.join(cwd, "output", imgpath.replace(cwd, ""));
    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir);
    }
    const options = new URL(obj.output.url);
    const req = https.request(options, res => {
        let body = "";
        res.setEncoding("binary");
        res.on("data", function(data) {
            body += data;
        });
        res.on("end", function() {
            fs.writeFile(imgpath, body, "binary", err => {
                if (err) return console.error(err);
                console.log(
                    `[${imgpath}] \n 压缩成功,原始大小-${obj.input.size},压缩大小-${
                        obj.output.size
                    },优化比例-${obj.output.ratio}`
                );
            });
        });
    });
    req.on("error", e => {
        console.error(e);
    });
    req.end();
}

这段代码是参考了nodejs全自动使用Tinypng(免费版,无需任何配置)压缩图片,只是简单的加上了动态的IP头,从而实现了不受上传次数限制。

核心的代码在这里:

// 生成随机IP,赋值给X-Forwarded-For
function getRandomIP() {
    return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join(".");
}
// ....
options.headers["X-Forwarded-For"] = getRandomIP();
// ....

代码上传到了github,有兴趣的可以点击下面链接查看并star关注喔:

super-tinypng

同时,也发布到了npm上面。只需要全局安装:

npm i -g super-tinypng

然后在你想要压缩图片的目录里面运行super-tinypng就能自动压缩图片了,并且不会有数量限制!

作者:Chess

转发链接:
https://mp.weixin.qq.com/s/aRyEzYcJ7CvOaN6ll5C10Q

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3338.html

标签: readfilesync
分享给朋友:

“「前端纯干货」原来TinyPNG可以这样玩” 的相关文章

git的几种分支模式

编写代码,是软件开发交付过程的起点,发布上线,是开发工作完成的终点。代码分支模式贯穿了开发、集成和发布的整个过程,是工程师们最亲切的小伙伴。那如何根据自身的业务特点和团队规模来选择适合的分支模式呢?本文分享几种主流 Git 分支模式的流程及特点,并给出选择建议。分支的目的是隔离,但多一个分支也意味着...

国产操作系统上Vim的详解03--安装和使用插件 | 统信 | 麒麟 | 中科方德

原文链接:国产操作系统上Vim的详解03--使用Vundle插件管理器来安装和使用插件 | 统信 | 麒麟 | 中科方德Hello,大家好啊!今天给大家带来一篇在国产操作系统上使用Vundle插件管理器来安装和使用Vim插件的详解文章。Vundle是Vim的一款强大的插件管理器,可以帮助我们轻松地安...

多项修正 尼康D4s发布最新1.10版固件

尼康公司与2014年8月27日发布了D4s的最新固件,固件版本号为C:1.10。这次固件升级,主要解决了一些BUG,并且对拍摄菜单与相机操作做了一定调整。下面是本次新固件的具体信息:尼康发布D4s最新C固件 1.10版对C固件升级到1.10版所作的修改:当选定运动VR模式并换上 AF-S 尼克尔 4...

一文让你彻底搞懂 vue-Router

路由是网络工程里面的专业术语,就是通过互联把信息从源地址传输到目的地址的活动。本质上就是一种对应关系。分为前端路由和后端路由。后端路由:URL 的请求地址与服务器上的资源对应,根据不同的请求地址返回不同的资源。前端路由:在单页面应用中,根据用户触发的事件,改变URL在不刷新页面的前提下,改变显示内容...

关于Vue页面跳转传参,参数不同, 但页面只获取参数一次的问题

#头条创作挑战赛#1.问题描述问题描述: element 展示表格(页面A),点击表格的每一行的查看详情按钮,可以携带此行的信息参数跳转到另一个页面(页面B),但是从A页面到B页面,只有第一次跳转的时候B页面可以获取到A页面的参数,返回再次A->B ,B页面无法获取到参数。2.解决办法:方法一...

Vue实战篇|使用路由管理用户权限(动态路由)

权限控制是后台管理系统比较常见的需求,如果我们需要对某些页面的添加权限控制的话,那我们可以在路由管理中的权限做一些校验,没有通过权限校验的给出相应的提示或者直接跳转到报错页面。跟着我一起来学vue实战篇路由管理权限吧!权限校验函数getCurrentAuthority()函数用于获取当前用户权限,一...