针对这个场景,做为一名合格的前端工程师,应该能够有一些本身的想法,提升工做效率;使用 NODEJS
撸一个自动压缩工具,减小这些冗余的无心义的工做。前端
若是有同窗对这些模块不知道的话,能够去官网学习一下node
这个基类的做用就是底层处理,读取目标图片,而后交给 tinify
处理。tinify
提供了 fromBuffer
和 toBuffer
API 进行压缩和输出 buffer
。最后将输出的 buffer
输出到指定的目录下生成文件。json
核心代码其实就这些windows
const rs = fs.createReadStream(oldPath);
const ws = fs.createWriteStream(newPath);
rs.on('data', (chunkData) => {
tinify.fromBuffer(chunkData).toBuffer((error, chunk) => {
if (error) return reject(error.message);
// 写入目标文件
ws.end(chunk);
// 写入成功
ws.on('finish', () => resolve());
// 写入失败
ws.on('err', () => reject(err.message));
});
});
复制代码
咱们须要去 tinify 官网申请一个开发者 key 而后才可使用。tinify
一样提供了 validate
API 进行验证。前端工程师
// key 就是咱们申请获得的id
tinify.key = this.key;
// 开始验证,callback 是验证失败或成功都会调用的回调函数
tinify.validate(callback);
复制代码
完整代码以下:app
const EventEmitter = require('events');
const tinify = require('tinify');
const chalk = require('chalk');
const fs = require('fs');
class Tinify extends EventEmitter {
constructor(key) {
super();
this.key = key;
this.initTinify();
}
// 初始化验证阶段
initTinify () {
tinify.key = this.key;
tinify.validate((error) => {
if (error) {
// tinify 初始化验证失败
this.emit('error', error);
return process.exit();
}
if (this.remainingCompressions() <= 0) {
console.log(chalk.red('压缩数量已经用完'));
this.emit('close');
return process.exit();
}
// 初始化验证成功
this.emit('initial');
});
}
// 计算剩余压缩张数
remainingCompressions () {
return 500 - tinify.compressionCount;
}
// 压缩图片,并输出到指定目录
miniIMG (oldPath, newPath) {
if (this.remainingCompressions() <= 0) {
this.emit('error', Error({message: 'tinify has no remaining compressed sheets', name: 'TypeError'}));
return process.exit();
}
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(oldPath);
const ws = fs.createWriteStream(newPath);
rs.on('data', (chunkData) => {
tinify.fromBuffer(chunkData).toBuffer((error, chunk) => {
if (error) return reject(error.message);
ws.end(chunk);
ws.on('finish', () => resolve());
ws.on('err', () => reject(err.message));
});
});
});
}
}
复制代码
基于基类,实现一些复杂的场景需求。异步
const path = require('path');
const fs = require('fs');
const Tinify = require('./tinify');
class Application extends Tinify {
constructor({key, entry, output}) {
super(key);
this.config = {
key,
entry,
output,
mime: /\.(png|jpg|gif|jpeg)$/,
};
// 建立一个堆栈,防止图片被重复执行压缩
this.stack = new Set();
// 作一个防抖处理
this.fileChange = this.debounce(this.watchFileChange, 2000);
// 当输入和输出的目录都建立完成之后,触发 runing,表示能够开始压缩了
Promise.all([this.mkDir(this.config.entry), this.mkDir(this.config.output)])
.then(() => {
this.emit('running');
})
.catch((error) => {
this.emit('error', error);
process.exit();
});
}
async mkDir (dir) {
const exists = fs.existsSync(dir);
if (exists) return;
fs.mkdirSync(dir);
return;
}
debounce (fn, delay) {
let timer;
return function() {
const self = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(self, args);
}, delay);
}
}
// 监听
watch (dir) {
// recursive 标示是否监听子目录。目前只支持 macOS、windows
fs.watch(dir, {recursive: true}, (eventName) => {
if (eventName === 'change') return;
this.fileChange(dir);
});
}
watchFileChange (dir) {
// 读取该目录下的全部文件
fs.readdir(dir, (error, files) => {
if (error) {
this.emit('error', error);
return process.exit();
}
// 若是改目录是一个空目录,直接删除
if (files.length === 0 && dir !== this.config.entry) {
try {
fs.rmdirSync(dir);
return;
} catch (err) {
console.log(err);
}
}
for (let i = 0; i < files.length; i++) {
this.handleFile(dir, files[i]);
}
});
}
// 文件处理
handleFile (dir, base) {
const entryPath = path.join(dir, base);
const outputPath = path.join(this.config.output, base);
// 若是该文件还在 stack 中不会进行压缩
if (this.stack.has(entryPath)) return;
// 查看是否还有可压缩的次数
if (this.remainingCompressions() <= 0) {
this.emit('error', Error({message: 'tinify has no remaining compressed sheets', name: 'TypeError'}));
return process.exit();
}
fs.stat(entryPath, (error, stat) => {
if (error) {
this.emit('error', error);
return process.exit();
}
// 若是是一个文件目录,递归遍历该目录中的全部文件
if (stat.isDirectory()) {
this.watchFileChange(entryPath);
return;
};
// 文件格式不符合;或者是一个空的文件;执行删除
if (!this.config.mime.test(base) || stat.size <= 0) {
try {
fs.unlinkSync(entryPath);
return;
} catch (err) {
console.log(err);
}
}
// 将文件的路径做为该文件的惟一标示,存放在 stack 中;避免重复压缩;
this.stack.add(entryPath);
// 压缩图片
this.miniIMG(entryPath, outputPath)
.then(() => {
// 压缩完成
this.emit('complete', null, base);
fs.unlink(entryPath, () => this.stack.delete(entryPath));
})
.catch(() => {
// 压缩失败
this.emit('complete', true, base);
this.stack.delete(entryPath);
});
});
}
run () {
// 添加到队列的头部,当 running 触发的时候,开始监听
this.prependOnceListener('running', () => {
// 若是目录中已经有文件存在,执行压缩
this.watchFileChange(this.config.entry);
this.watch(this.config.entry);
});
}
}
复制代码
一切都是基于 events 模块,因此实现很容易async
const Application = require('./application.js');
const chalk = require('chalk');
const params = require('./package.json');
const {key, entry, output} = {...params}
const app = new Application({key, entry, output});
// 开始运行
app.run();
app.on('initial', () => {
console.log('初始化验证成功了');
});
app.on('running', () => {
console.log('应用已经开始了');
});
app.on('close', () => {
console.log('应用关闭了');
});
app.on('error', (error) => {
console.log('应用出错了', error);
});
app.on('complete', (err, filename) => {
if (err) {
console.log(chalk.red(`文件压缩失败【${filename}】`));
return;
}
console.log(chalk.green(`文件压缩成功【${filename}】`));
});
// 处理未捕获的未知错误
process.on('uncaughtExpection', function(error) {
// 打印日志
console.log(error);
// 推出程序
process.exit();
});
复制代码
程序在运行中,可能会出现未知错误,固然这多是由于我写的程序不够健壮致使的。因此咱们须要在程序中作一个 uncaughtExpection 事件的监听。函数
一个自动化的工具,咱们但愿它开始之后,就能够永久的执行下去。因此咱们单独的依赖一个进程确定不行。因此须要有一个主进程对这个工做进程进行监听,当监听到工做进程报错或是退出程序时。主进程就能够当即从新开启一个新的工做进程。确保工做不被耽误。工具
nodejs
恰好提供了咱们想要的东西。cluster
和 child_process
均可以开启一个新进程(cluster
其实就是 child_process
的一个封装)。
const cluster = require('cluster');
const child_process = require('child_process');
cluster.setupMaster({exec: './index.js'});
// 衍生一个新的进程
const worker = cluster.fork();
// 衍生一个进程
const sunProcess = child_process.fork('./index.js');
复制代码
const limit = 10;
const stack = [];
const stemp = 60000;
// 限制频繁启动;这种场景通常出如今错误的程序中
const lastStack = stack.slice(limit * -1);
if (lastStack[lastStack.length - 1] - lastStack[0] < stemp) return;
createApplication();
复制代码
上面代码的做用:就是限制程序频繁的从新启动,这种状况通常是咱们人为形成的,因此加入这段代码进行过滤。
完整代码以下:
const chalk = require('chalk');
const cluster = require('cluster');
const args = process.argv.slice(2);
const execArgv = process.execArgv;
// 这里使用的是 cluster。
cluster.setupMaster({'exec': './index.js', args, execArgv});
let worker = null;
const limit = 10;
const stack = [];
const stemp = 60000;
const createApplication = () => {
stack.push(Date.now());
// 衍生一个新的进程;相似 child_process.fork()
worker = cluster.fork();
// 监听到子进程推出
worker.on('exit', (code, signal) => {
console.log(code, signal);
console.log(chalk.red(`工做进程【${worker.process.pid}】已经推出`));
if (signal === 'SIGTERM' || signal === 'SIGHUB' || signal === 'SIGINT') return;
// 限制频繁启动;这种场景通常出如今错误的程序中
const lastStack = stack.slice(limit * -1);
if (lastStack[lastStack.length - 1] - lastStack[0] < stemp) return;
createApplication();
})
}
createApplication();
// 当主进程推出时,杀死子进程
process.on('exit', () => {
process.kill(worker.process.pid, 'SIGTERM');
})
复制代码
不要忘记在 package.json
中配置开发者 key
、文件输入和输出的目录 entry
和 output
。
运行:$ ndoe ./listener.js
打完收工。