├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── index.js ├── map └── dict ├── package.json ├── src ├── stdout.js └── uploader.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esversion": 6, 7 | "forin": false, 8 | "freeze": false, 9 | "strict": true, 10 | "undef": true, 11 | "unused": true, 12 | "laxcomma": false, 13 | "asi": true, 14 | "boss": true, 15 | "expr": true, 16 | "sub": true, 17 | "quotmark": false, 18 | "loopfunc": false, 19 | "lastsemic": true, 20 | "funcscope": true, 21 | "node":true, 22 | "jquery":true, 23 | "browser":true, 24 | "devel":true 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 NBE01 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinypng-webpack-plugin 2 | 3 | a img compress plugin use with tinyPNG for webpack. 4 | 5 | ## Get TinyPNG key 6 | 7 | [link](https://tinypng.com/developers) 8 | 9 | ## Installation 10 | ```shell 11 | # for webpack 4 12 | $ npm install tinypng-webpack-plugin --save-dev 13 | 14 | # for webpack 3 & 2 & 1 15 | $ npm install tinypng-webpack-plugin@1.0.2 --save-dev 16 | ``` 17 | 18 | ## Example Webpack Config 19 | 20 | ```javascript 21 | var tinyPngWebpackPlugin = require('tinypng-webpack-plugin'); 22 | 23 | //in your webpack plugins array 24 | module.exports = { 25 | plugins: [ 26 | new tinyPngWebpackPlugin({ 27 | key:"your tinyPNG key" 28 | }) 29 | ] 30 | } 31 | ``` 32 | ## Usage 33 | ```javascript 34 | new tinyPngWebpackPlugin({ 35 | key:"your tinyPNG key",//can be Array, eg:['your key 1','your key 2'....] 36 | ext: ['png', 'jpeg', 'jpg'],//img ext name 37 | proxy:'http://user:pass@192.168.0.1:8080'//http proxy,eg:如果你来自中国,同时拥有shadowsocks,翻墙默认配置为 http:127.0.0.1:1080 即可。(注,该参数因为需要超时断开连接的原因,导致最后会延迟执行一会webpack。但相对于国内网络环境,用此参数还是非常划算的,测试原有两张图片,无此参数耗时2000ms+,有此参数耗时1000ms+节约近半。) 38 | }) 39 | ``` 40 | ### Options Description 41 | * key: Required, tinyPNG key 42 | * ext: not Required, to be compress img ext name. 43 | * proxy:not Required, a http proxy to improve the network environment.eg:http://127.0.0.1:1080 44 | 45 | ### Defaults Options 46 | ```javascript 47 | { 48 | key:'', 49 | ext: ['png', 'jpeg', 'jpg'], 50 | proxy:'' 51 | } 52 | ``` 53 | 54 | ## License 55 | http://www.opensource.org/licenses/mit-license.php 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const uploader = require('./src/uploader.js'); 5 | const stdout = require('./src/stdout.js'); 6 | 7 | class TinyPNGPlugin { 8 | constructor(options){ 9 | debugger; 10 | this.pluginName = 'tinypng-webpack-plugin'; 11 | this.options = _.assign({ 12 | key: '', 13 | ext: ['png', 'jpeg', 'jpg'], 14 | proxy:'' 15 | }, options); 16 | 17 | if (!this.options.key) { 18 | throw new Error('need tinyPNG key'); 19 | } 20 | 21 | if (_.isString(this.options.key)) { 22 | this.options.key = [this.options.key]; 23 | } 24 | 25 | if(_.isString(this.options.proxy) && this.options.proxy !== ''){ 26 | if(this.options.proxy.indexOf('http://') === -1){ 27 | throw new Error('the proxy must be HTTP proxy!') 28 | } 29 | } 30 | 31 | //正则表达式筛选图片 32 | this.reg = new RegExp("\.(" + this.options.ext.join('|') + ')$', 'i'); 33 | } 34 | apply(compiler){ 35 | compiler.hooks.emit.tapPromise(this.pluginName,(compilation)=>{ 36 | stdout.render(); 37 | return uploader(compilation, this.options).then((failList) => { 38 | stdout.stop(); 39 | stdout.renderErrorList(failList); 40 | }).catch((e) => { 41 | stdout.stop(); 42 | compiler.errors.push(e); 43 | }); 44 | }); 45 | } 46 | } 47 | 48 | 49 | module.exports = TinyPNGPlugin; 50 | -------------------------------------------------------------------------------- /map/dict: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyujilong/tinypng-webpack-plugin/3cbd16230a35c1b61b52e78240d4f2f759a31bae/map/dict -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinypng-webpack-plugin", 3 | "version": "2.0.0", 4 | "description": "a webpack plugin use tinyPNG", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "lodash": "^4.17.4", 11 | "tinify": "^1.5.0", 12 | "colors": "^1.1.2", 13 | "co": "^4.6.0", 14 | "md5":"^2.2.1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/skyujilong/tinypng-webpack-plugin.git" 19 | }, 20 | "keywords": [ 21 | "webpack", 22 | "tinyPNG", 23 | "webpack tinyPNG", 24 | "tinyPNG webpack" 25 | ], 26 | "author": "Jilong Yu", 27 | "license": "MIT", 28 | "engines": { 29 | "node": ">=4.0.0" 30 | }, 31 | "peerDependencies": { 32 | "webpack": "^4.0.0" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/skyujilong/tinypng-webpack-plugin/issues" 36 | }, 37 | "homepage": "https://github.com/skyujilong/tinypng-webpack-plugin#readme" 38 | } 39 | -------------------------------------------------------------------------------- /src/stdout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const readline = require('readline'); 4 | 5 | const stdout = process.stdout; 6 | 7 | const colors = require('colors'); 8 | 9 | const _ = require('lodash'); 10 | 11 | function clearLine() { 12 | readline.clearLine(stdout, 0); 13 | readline.cursorTo(stdout, 0, null); 14 | } 15 | 16 | let ide; 17 | 18 | module.exports = { 19 | render: function() { 20 | stdout.write('/n'); 21 | let count = 0; 22 | ide = setInterval(() => { 23 | let symbol; 24 | if (count % 3 === 0) { 25 | symbol = '.'; 26 | } else if (count % 3 === 1) { 27 | symbol = '..'; 28 | } else if (count % 3 === 2) { 29 | symbol = '...'; 30 | } 31 | clearLine(); 32 | stdout.write(colors.green('tinyPNG is compressing imgs ' + symbol)); 33 | count++; 34 | }, 300); 35 | }, 36 | stop: function() { 37 | clearInterval(ide); 38 | clearLine(); 39 | stdout.write(colors.green('tinyPNG compress imgs done ...\n')); 40 | }, 41 | renderErrorList: function(list){ 42 | 43 | let _list = []; 44 | _.each(list,(val) => { 45 | _list = _.concat(_list,val); 46 | }); 47 | 48 | if(_list.length === 0){ 49 | return; 50 | } 51 | 52 | _.each(_list,(name) => { 53 | console.log(colors.yellow('tinyPNG compress img error: ' + name)); 54 | }); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/uploader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const co = require('co'); 3 | const _ = require('lodash'); 4 | const tinify = require('tinify'); 5 | const stdout = process.stdout; 6 | const fs = require('fs'); 7 | const md5 = require('md5'); 8 | const path = require('path'); 9 | const readline = require('readline'); 10 | 11 | let dict = {}, 12 | appendDict = {}, 13 | splitCode = "$$$"; 14 | 15 | function getImgQueue(list, reg) { 16 | //对应分成三个队列,开启3个线程进行上传 17 | let queue = [ 18 | [], 19 | [], 20 | [] 21 | ]; 22 | let count = 0; 23 | _.each(list, function (val, key) { 24 | if (reg.exec(key)) { 25 | //val RawSource 对象 26 | queue[count % queue.length].push({ 27 | name: key, 28 | source: val 29 | }); 30 | count++; 31 | } 32 | }); 33 | return queue; 34 | } 35 | 36 | /** 37 | * 写操作,将压缩后的图片存储在一个固定的位置 38 | * @param {*} md5 压缩前 md5指纹 39 | * @param {*} imgBuffer 压缩后的 img buffer 40 | */ 41 | function* writeImg(imgBuffer, md5) { 42 | let filePath = yield new Promise(function (resolve, reject) { 43 | //获取md5值 44 | let filePath = path.resolve(__dirname, '../map', md5); 45 | fs.writeFile(filePath, imgBuffer, function (err) { 46 | if (err) { 47 | reject(err); 48 | } else { 49 | resolve(filePath); 50 | } 51 | }); 52 | }); 53 | return filePath; 54 | } 55 | 56 | function deImgQueue(queue, keys) { 57 | let reTryCount = 3; 58 | let uploadErrorList = []; 59 | return co(function* () { 60 | function* upload(fileInfo, reTryCount) { 61 | if (reTryCount < 0) { 62 | //超过尝试次数 63 | uploadErrorList.push(fileInfo.name); 64 | return; 65 | } 66 | 67 | // 添加缓存,防止多次走服务器 md5 68 | let fileMd5 = md5(fileInfo.source.source()); 69 | try { 70 | if (dict[fileMd5]) { 71 | //找到对应的文件流,加入到fileInfo.source._value中 72 | let compressBuffer = yield new Promise(function (resolve, reject) { 73 | fs.readFile(dict[fileMd5], function (err, buffer) { 74 | if (err) { 75 | reject(err); 76 | } else { 77 | resolve(buffer); 78 | } 79 | }) 80 | }); 81 | fileInfo.source._value = compressBuffer; 82 | return; 83 | } 84 | } catch (e) { 85 | throw e; 86 | } 87 | 88 | try { 89 | let compressImg = yield new Promise((resolve, reject) => { 90 | tinify.fromBuffer(fileInfo.source.source()).toBuffer((err, resultData) => { 91 | if (err) { 92 | reject(err); 93 | } else { 94 | resolve(resultData); 95 | } 96 | }) 97 | }); 98 | //压缩图片成功 99 | fileInfo.source._value = compressImg; 100 | // 缓存压缩后的文件 101 | let filePath = yield writeImg(compressImg, fileMd5); 102 | appendDict[fileMd5] = filePath; 103 | } catch (err) { 104 | if (err instanceof tinify.AccountError) { 105 | // Verify your API key and account limit. 106 | if (!keys) { 107 | //输出文件名 fileInfo.name 108 | uploadErrorList.push(fileInfo.name); 109 | return; 110 | } 111 | //tinify key 更换 112 | tinify.key = _.first(keys); 113 | keys = _.drop(keys); 114 | yield upload(fileInfo, reTryCount); 115 | } else { 116 | // Something else went wrong, unrelated to the Tinify API. 117 | yield upload(fileInfo, reTryCount - 1); 118 | } 119 | } 120 | } 121 | 122 | for (let fileInfo of queue) { 123 | yield upload(fileInfo, reTryCount); 124 | } 125 | 126 | return uploadErrorList; 127 | }); 128 | } 129 | 130 | /** 131 | * 初始化字典对象 132 | */ 133 | function* initDict() { 134 | let dictPath = path.resolve(__dirname, '../map/dict'); 135 | yield new Promise(function (resolve, reject) { 136 | let rl = readline.createInterface({ 137 | input: fs.createReadStream(dictPath) 138 | }); 139 | rl.on('line', function (line) { 140 | //给dict对象 添加属性与对应的值 141 | if (line && line.indexOf(splitCode) >= 0) { 142 | let list = line.split(splitCode); 143 | dict[list[0]] = list[1]; 144 | } 145 | }); 146 | rl.on('close', function () { 147 | resolve(dict); 148 | }) 149 | }); 150 | } 151 | 152 | /** 153 | * 将appendDict内容导入到dict文件中 154 | */ 155 | function* appendDictFile() { 156 | let dictPath = path.resolve(__dirname, '../map/dict'); 157 | 158 | function append(filePath, data) { 159 | return new Promise(function (resolve, reject) { 160 | fs.appendFile(filePath, data, function (err) { 161 | if (err) { 162 | reject(err); 163 | } else { 164 | resolve(resolve); 165 | } 166 | }); 167 | }); 168 | } 169 | for (let key in appendDict) { 170 | yield append(dictPath, key + splitCode + appendDict[key] + '\n'); 171 | } 172 | } 173 | 174 | /** 175 | * 进行图片上传主操作 176 | * @param {[type]} compilation [webpack 构建对象] 177 | * @param {[type]} options [选项] 178 | * @return {Promise} 179 | */ 180 | module.exports = (compilation, options) => { 181 | //过滤文件尾缀名称 182 | let reg = new RegExp("\.(" + options.ext.join('|') + ')$', 'i'); 183 | let keys = options.key; 184 | if (options.proxy) { 185 | //这里启用proxy 但是proxy因为建立scoket连接,最后需要有个超时的等待时间来关闭这个scoket 186 | tinify.proxy = options.proxy; 187 | } 188 | return co(function* () { 189 | //初始化字典 190 | yield initDict; 191 | let imgQueue = getImgQueue(compilation.assets, reg); 192 | tinify.key = _.first(keys); 193 | keys = _.drop(keys); 194 | let result = yield Promise.all([ 195 | deImgQueue(imgQueue[0], keys), 196 | deImgQueue(imgQueue[1], keys), 197 | deImgQueue(imgQueue[2], keys) 198 | ]); 199 | 200 | //将appendDict 保存到dict文件中 201 | yield appendDictFile; 202 | return result; 203 | }); 204 | }; -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | charenc@~0.0.1: 6 | version "0.0.2" 7 | resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 8 | 9 | co@^4.6.0: 10 | version "4.6.0" 11 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 12 | 13 | colors@^1.1.2: 14 | version "1.1.2" 15 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 16 | 17 | crypt@~0.0.1: 18 | version "0.0.2" 19 | resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 20 | 21 | is-buffer@~1.1.1: 22 | version "1.1.6" 23 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 24 | 25 | lodash@^4.17.4: 26 | version "4.17.4" 27 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 28 | 29 | md5@^2.2.1: 30 | version "2.2.1" 31 | resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" 32 | dependencies: 33 | charenc "~0.0.1" 34 | crypt "~0.0.1" 35 | is-buffer "~1.1.1" 36 | 37 | "promise-nodeify@>= 0.1": 38 | version "1.0.1" 39 | resolved "https://registry.yarnpkg.com/promise-nodeify/-/promise-nodeify-1.0.1.tgz#05e78cce162f93021dd285c66dadc38adaaae38c" 40 | 41 | "proxying-agent@>= 2.1": 42 | version "2.1.2" 43 | resolved "https://registry.yarnpkg.com/proxying-agent/-/proxying-agent-2.1.2.tgz#bd66b5f0f967725e31c0f1b38a594c622f55fd26" 44 | 45 | tinify@^1.5.0: 46 | version "1.5.0" 47 | resolved "https://registry.yarnpkg.com/tinify/-/tinify-1.5.0.tgz#6ba2ea7e8827c845d263de3b46f4db7bad555920" 48 | dependencies: 49 | promise-nodeify ">= 0.1" 50 | proxying-agent ">= 2.1" 51 | --------------------------------------------------------------------------------