前言:
说来很奇怪,现在的不少技术交流群里面存在这一些“伪程序员”,就比如说下图的这段对话,用在线的图片压缩网站要对自己的大量图片进行压缩,居然嫌麻烦都跑群里面问要怎么办?
从程序员的角度来解决这个问题:
- 上班摸鱼法: 一张一张来,干一张算一张。
- 土豪氪金法: 通过网站开放的API进行简单编程进行批量处理,当然你处理的越多就需要支付一些费用。
- 展示技术法: 适合在合理的数量内,难得的机会中复习一下你的编程知识,还能把活干好。
- 其他: 。。。
打码前的准备:
- 我们选择展示技术法来做今天的Demo,我也觉得这是一个程序员的选择(丢给美工的事我。。。);
- 一款产品的质量也是需要逐渐进行打磨优化,
tinypng
在程序员中间还是流传的较为好用的一款产品,我们依然选择tinypng
,用别人专业的工具做自己的事,漂亮!。
思路介绍:
- 递归获取本地文件夹里的文件
- 过滤文件,格式必须是.jpg .png,大小小于5MB.(文件夹递归)
- 每次只处理一个文件(可以绕过20个的数量限制)
- 处理返回数据拿到远程优化图片地址
- 取回图片更新本地图片
- 纯node实现不依赖任何其他代码片段
打码实现:
仅适用Node提供的模块:
const fs = require("fs"); const { Console } = require("console"); const path = require("path"); const https = require("https"); const URL = require("url").URL; 复制代码
通用浏览器标识,防止同一标识被服务器拦截:
const USER_AGENT = [ "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv,2.0.1) Gecko/20100101 Firefox/4.0.1", "Mozilla/5.0 (Windows NT 6.1; rv,2.0.1) Gecko/20100101 Firefox/4.0.1", "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; maxthon 2.0)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)", ]; 复制代码
定义log,支持输出日志到文件:
// 定义log,支持输出日志到文件 class Log { options = { flags: "a", // append模式 encoding: "utf8", // utf8编码 }; logger = {}; /** * 初始化打印配置 */ constructor() { this.logger = new Console({ stdout: fs.createWriteStream("./log.tinypng.stdout.log", this.options), stderr: fs.createWriteStream("./log.tinypng.stderr.log", this.options), }); } /** * log级别 * @param {*} message 输出信息 */ log(message) { if (message) { this.logger.log(message); console.log(message); } } /** * error级别 * @param {*} message 输出err信息 */ error(message) { if (message) { this.logger.error(message); console.error(message); } } } // 实例化Log对象 const Tlog = new Log(); 复制代码
定义TinyPng对象:
class TinyPng { // 配置信息: 后缀格式和最大文件大小受接收限制不允许调整 config = { files: [], entryFolder: "./", deepLoop: false, extension: [".jpg", ".png"], max: 5200000, // 5MB == 5242848.754299136 min: 100000, // 100KB }; // 成功处理计数 successCount = 0; // 失败处理计数 failCount = 0; /** * TinyPng 构造器 * @param {*} entry 入口文件 * @param {*} deep 是否递归 */ constructor(entry, deep) { console.log(USER_AGENT[Math.floor(Math.random() * 10)]); if (entry != undefined) { this.config.entryFolder = entry; } if (deep != undefined) { this.config.deepLoop = deep; } // 过滤传入入口目录中符合调整的待处理文件 this.fileFilter(this.config.entryFolder); Tlog.log(`本次执行脚本的配置:`); Object.keys(this.config).forEach((key) => { if (key !== "files") { Tlog.log(`配置${key}:${this.config[key]}`); } }); Tlog.log(`等待处理文件的数量:${this.config.files.length}`); } /** * 执行压缩 */ compress() { Tlog.log("启动图像压缩,请稍等..."); let asyncAll = []; if (this.config.files.length > 0) { this.config.files.forEach((img) => { asyncAll.push(this.fileUpload(img)); }); Promise.all(asyncAll) .then(() => { Tlog.log( `处理完毕: 成功: ${this.successCount}张, 成功率${ this.successCount / this.config.files.length }` ); }) .catch((error) => { Tlog.error(error); }); } } /** * 过滤待处理文件夹,得到待处理文件列表 * @param {*} folder 待处理文件夹 * @param {*} files 待处理文件列表 */ fileFilter(folder) { // 读取文件夹 fs.readdirSync(folder).forEach((file) => { let fullFilePath = path.join(folder, file); // 读取文件信息 let fileStat = fs.statSync(fullFilePath); // 过滤文件安全性/大小限制/后缀名 if ( fileStat.size <= this.config.max && fileStat.size >= this.config.min && fileStat.isFile() && this.config.extension.includes(path.extname(file)) ) { this.config.files.push(fullFilePath); } // 是都要深度递归处理文件夹 else if (this.config.deepLoop && fileStat.isDirectory()) { this.fileFilter(fullFilePath); } }); } /** * TinyPng 远程压缩 HTTPS 请求的配置生成方法 */ getAjaxOptions() { return { method: "POST", hostname: "tinypng.com", path: "/web/shrink", headers: { rejectUnauthorized: false, "X-Forwarded-For": Array(4) .fill(1) .map(() => parseInt(Math.random() * 254 + 1)) .join("."), "Postman-Token": Date.now(), "Cache-Control": "no-cache", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT[Math.floor(Math.random() * 10)], }, }; } /** * TinyPng 远程压缩 HTTPS 请求 * @param {string} img 待处理的文件 * @success { * "input": { "size": 887, "type": "image/png" }, * "output": { "size": 785, "type": "image/png", "width": 81, "height": 81, "ratio": 0.885, "url": "https://tinypng.com/web/output/7aztz90nq5p9545zch8gjzqg5ubdatd6" } * } * @error {"error": "Bad request", "message" : "Request is invalid"} */ fileUpload(imgPath) { return new Promise((resolve) => { let req = https.request(this.getAjaxOptions(), (res) => { res.on("data", async (buf) => { let obj = JSON.parse(buf.toString()); if (obj.error) { Tlog.log(`压缩失败!\n 当前文件:${imgPath} \n ${obj.message}`); } else { resolve(await this.fileUpdate(imgPath, obj)); } }); }); req.write(fs.readFileSync(imgPath), "binary"); req.on("error", (e) => { Tlog.log(`请求错误! \n 当前文件:${imgPath} \n, ${e}`); }); req.end(); }).catch((error) => { Tlog.log(error); }); } // 该方法被循环调用,请求图片数据 fileUpdate(entryImgPath, obj) { return new Promise((resolve) => { let options = new URL(obj.output.url); let req = https.request(options, (res) => { let body = ""; res.setEncoding("binary"); res.on("data", (data) => (body += data)); res.on("end", () => { fs.writeFile(entryImgPath, body, "binary", (err) => { if (err) { Tlog.log(err); } else { this.successCount++; let message = `压缩成功 : 优化比例: ${( (1 - obj.output.ratio) * 100 ).toFixed(2)}% ,原始大小: ${(obj.input.size / 1024).toFixed( 2 )}KB ,压缩大小: ${(obj.output.size / 1024).toFixed( 2 )}KB ,文件:${entryImgPath}`; Tlog.log(message); resolve(message); } }); }); }); req.on("error", (e) => { Tlog.log(e); }); req.end(); }).catch((error) => { Tlog.log(error); }); } } module.exports = TinyPng; 复制代码
入口脚本:
/** * 因网络原因和第三方接口防刷等技术限制导致部分图像处理失败 */ const TinyPng = require("./tinypng.compress.img"); function getEntryPath() { let i = process.argv.findIndex((i) => i === "-p"); if (process.argv[i + 1]) { return process.argv[i + 1]; } } new TinyPng(getEntryPath(), true).compress(); 复制代码
执行演示:
日志记录:
结语:
程序员还是要将重复的工作简单化,几年前就靠这份脚本将150多M的前端项目压到了20~30兆,你会想着说怎么还能有这样的项目,你们项目很大么?说实话就是不规范导致的,多年积累的文件你要一张张去处理你觉得靠谱么,你刚压缩完其他同事又提交了一堆大图片怎么办,那么最好将脚本改一下再加入到编译时的插件中,完美!