├── .editorconfig ├── .gitignore ├── README.md ├── bin └── wecos.js ├── index.js ├── lib ├── common.js ├── compress.js ├── config.js ├── task.js ├── upload.js └── watcher.js ├── package.json └── test ├── index.js ├── test-config-file.js ├── test-config-option.js ├── test-task.js ├── test-upload.js └── test.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | test/app-src 4 | test/app 5 | test/.tmp 6 | test/.backup 7 | test/wecos_backup 8 | test/wecos.config.json 9 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeCOS —— 微信小程序 COS 瘦身解决方案 2 | 3 | 通过WeCOS,你的小程序项目中的图片资源会自动上传到你的[腾讯云对象存储服务(COS)](https://www.qcloud.com/product/cos),且WeCOS自动替换代码中图片资源地址的引用为线上地址,移除项目目录中的图片资源,从而减小小程序包大小,为你解决包大小超过限制的烦恼。 4 | ![WeCOS](https://pic1.zhimg.com/v2-cc9a13ee4c09935127181a66ebe4a3e8_b.jpg) 5 |
6 | 7 | ## 为什么你需要 WeCOS 8 | 9 | 为了提升小程序体验流畅度,编译后的代码包大小需小于 1MB ,大于 1MB 的代码包将上传失败。 10 | 11 | 在开发小程序的过程中,图片资源通常会占用较大空间,很容易超出官方的1MB限制。这时候,使用WeCOS,可以让你在开发过程中不需要关心图片资源占用多少空间的问题,专注于自己的逻辑开发。 12 |
13 | 14 | ## 准备工作 15 | * 进入[腾讯云官网](https://www.qcloud.com),注册帐号 16 | * 登录[云对象存储服务(COS)控制台](https://console.qcloud.com/cos4),开通COS服务,创建Bucket 17 | * 安装[Node.js](https://nodejs.org)环境 18 |
19 | 20 | ## 安装 21 | 22 | ```js 23 | npm install -g wecos 24 | ``` 25 |
26 | 27 | ## 基本配置 28 | 在你的小程序目录同级下创建`wecos.config.json`文件 29 | 30 | `wecos.config.json`配置项例子: 31 | ```json 32 | { 33 | "appDir": "./app", 34 | "cos": { 35 | "secret_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 36 | "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 37 | "bucket": "wxapp-1251902136", 38 | "region": "ap-guangzhou", //创建bucket时选择的地域简称 39 | "folder": "/", //资源存放在bucket的哪个目录下 40 | } 41 | } 42 | ``` 43 | 44 | | 配置项 | 类型 | 说明 | 45 | |:-- |:-- |:-- | 46 | | appDir | **[String]** | 默认 `./app`,小程序项目目录 | 47 | | cos | **[Object]** | 必填,填写需要上传到COS对应的配置信息,部分信息可在[COS控制台](https://console.qcloud.com/cos4/secret)查看 | 48 |
49 | 50 | ## 使用 51 | 52 | 在配置文件同级目录下命令行执行 53 | ```js 54 | wecos 55 | ``` 56 | 注意,执行前需要在该目录下创建`wecos.config.json`文件 57 | 58 |
59 |
60 | 61 | ## 高级配置 62 | 63 | | 配置项 | 类型 | 说明 | 64 | |:-- |:-- |:-- | 65 | | backupDir | **[String]** | 默认 `./wecos_backup`,备份目录 | 66 | | uploadFileSuffix | **[Array]** | 默认 `[".jpg", ".png", ".gif"]`,图片上传后缀名配置 | 67 | | uploadFileBlackList | **[Array]** | 默认 `[]`,图片资源黑名单 | 68 | | replaceHost | **[String]** | 默认 `''`,把指定域名替换成 targetHost | 69 | | targetHost | **[String]** | 默认 `''`,使用自定义域名 | 70 | | compress | **[Boolean]** | 默认 `false`,是否开启压缩图片 | 71 | | watch | **[Boolean]** | 默认 `true`,是否开启实时监听项目目录 | 72 | 73 | #### 设置备份目录 74 | 75 | 由于WeCOS在运行时会自动将项目下的图片上传至COS然后删除,这样可能存在丢失源文件的风险,因此我们也提供了备份源文件的功能,每上传一张图片,会在项目同级的某个目录下备份该文件 76 | 77 | 为了方便使用,可以通过以下配置来修改备份目录名,如果不需要使用该功能,可以设置为空值 78 | ```json 79 | "backupDir": "./wecos_backup" 80 | ``` 81 |
82 | 83 | #### 设置图片后缀 84 | 85 | 有些时候,我们需要限制上传图片的格式,例如只允许`jpg`格式,可以通过WeCOS提供的图片后缀配置项来定义 86 | 87 | WeCOS默认支持`jpg,png,gif`三种格式,假如你还需要添加其他格式,例如webp,可以在该配置项中添加 88 | 89 | ```json 90 | "uploadFileSuffix": [".jpg",".png",".gif",".webp"] 91 | ``` 92 |
93 | 94 | #### 设置图片黑名单 95 | 96 | 开发过程中,某些特定的图片我们不希望被上传,可以通过WeCOS的黑名单配置来解决这个问题,配置后上传程序会自动忽略掉这些图片 97 | 98 | 黑名单配置支持目录或具体到文件名的写法 99 | ```json 100 | "uploadFileBlackList": ["./images/logo.png","./logo/"] 101 | ``` 102 |
103 | 104 | #### 自定义域名 105 | 106 | 如果希望 COS 文件链接使用自定义的域名,可以配置 targetHost 代替默认域名,可以省略:`http://`: 107 | 108 | ```json 109 | "targetHost": "http://example.com" 110 | ``` 111 | 112 | 如果代码中的图片链接想换一个域名,可以配置 replaceHost targetHost 来实现。 113 | 114 | ```json 115 | "replaceHost": "http://wx-12345678.myqcloud.com", 116 | "targetHost": "https://example.com" 117 | ``` 118 |
119 | 120 | #### 开启图片压缩 121 | 122 | 图片上传到COS之后虽然大大减轻了程序包的大小,但如果图片自身体积过大,访问速度也会影响到用户体验 123 | 124 | 令人激动的是,WeCOS在图片上云的基础功能上还额外提供了基于[腾讯云万象优图](https://www.qcloud.com/product/ci)的图片压缩功能。 125 | 126 | 首先,你需要在[万象优图控制台](https://console.qcloud.com/ci)创建 COS的同名bucket 127 | 128 | 然后,开启该选项,资源将被压缩后上传(注:如果图片已经小到一定程度,压缩后大小可能不会变化) 129 | 130 | ```json 131 | "compress": true 132 | ``` 133 |
134 | 135 | #### 设置实时监听 136 | 137 | WeCOS默认实时监听项目目录变化,自动处理图片资源,在开发过程中,如果觉得实时监听不方便,或者只需要一次性处理就停止,可以修改该配置,程序将只会执行一次后退出 138 | ```json 139 | "watch": false 140 | ``` 141 | 142 |
143 |
144 | 145 | ## 高级用法 146 | 如果你除了上述使用命令行来执行的方式外,还想使用其他的方式,例如定制化成自己的模块,我们也提供了直接引用的使用方法满足你个性化的需求 147 | 148 | ```js 149 | var wecos = require('wecos'); 150 | 151 | /** 152 | * option 可选 [String|Object] 153 | * 传入 String,指定配置文件路径 154 | * 传入 Object,指定配置项 155 | */ 156 | wecos([option]); 157 | 158 | //指定配置文件路径 159 | wecos('./wecos-config.js'); 160 | 161 | //指定配置项 162 | wecos({ 163 | appDir: './xxx', 164 | cos: { 165 | ... 166 | } 167 | }); 168 | 169 | ``` 170 | 171 |
172 |
173 | 174 | ## 相关 175 | 176 | * [WeCOS-UGC-DEMO](https://github.com/tencentyun/wecos-ugc-upload-demo)——小程序用户资源上传COS DEMO 177 | 178 | * [COS-AUTH](https://github.com/tencentyun/cos-auth)——COS鉴权服务器DEMO 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /bin/wecos.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var pkg = require('../package.json'); 5 | var hasArg = false; 6 | process.argv.slice(1).filter(function (arg) { 7 | var match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=(.*))?$/i); 8 | if (match) { 9 | arg = match[1]; 10 | } else { 11 | return arg; 12 | } 13 | 14 | switch (arg) { 15 | case 'v': 16 | case 'version': 17 | hasArg = true; 18 | console.log('wecos ' + pkg.version); 19 | break; 20 | } 21 | }); 22 | 23 | if (!hasArg) { 24 | var watchTask = require('../lib/watcher'); 25 | watchTask(); 26 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var watchTask = require('./lib/watcher'); 4 | 5 | module.exports = watchTask; -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs'); 4 | var globalConfig = require('./config'); 5 | var pkg = require('../package'); 6 | var COS = require('cos-nodejs-sdk-v5'); 7 | 8 | var tool = { 9 | uploadFile: uploadFile 10 | }; 11 | 12 | var cos = new COS({ 13 | UserAgent: 'cos-wecos-' + pkg.version, 14 | }); 15 | function uploadFile(_config, fromPath, toPath, cb) { 16 | cos.options.SecretId = globalConfig.cos.secret_id; 17 | cos.options.SecretKey = globalConfig.cos.secret_key; 18 | 19 | // 桶里的文件夹 20 | var cosFolder = globalConfig.cos.folder || '/'; 21 | if (cosFolder.substr(-1, 1) !== '/') { 22 | cosFolder = cosFolder + '/' 23 | } 24 | 25 | var opt = { 26 | Bucket: globalConfig.cos.bucket || (globalConfig.cos.bucketname + '-' + globalConfig.cos.appid), 27 | Region: globalConfig.cos.region, 28 | }; 29 | toPath=toPath.replace(/\\/g,"/"); 30 | opt.Key = cosFolder + toPath; 31 | opt.Body = fs.createReadStream(fromPath); 32 | opt.ContentLength = fs.statSync(fromPath).size; 33 | cos.putObject(opt, function (err, data) { 34 | opt.Sign = false; 35 | var url = cos.getObjectUrl(opt); 36 | cb && cb(err, url); 37 | }); 38 | } 39 | 40 | module.exports = tool; 41 | -------------------------------------------------------------------------------- /lib/compress.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var request = require('request'); 4 | var pathLib = require('path'); 5 | var fs = require('fs'); 6 | var tool = require('./common'); 7 | 8 | var dns = require('dns'); 9 | 10 | var isOA = false; 11 | 12 | dns.resolve('www.oa.com', function(err, address) { 13 | if(!err){ 14 | var innerIPReg = /(^10\.|^172\.|^192.)/; 15 | if (innerIPReg.test(address[0])) { 16 | isOA = true; 17 | } 18 | } 19 | }); 20 | 21 | 22 | function compress(config, filePath, toPath, cb) { 23 | 24 | var isExists = fs.existsSync('./.tmp'); 25 | 26 | if(!isExists) { 27 | fs.mkdirSync('./.tmp'); 28 | } 29 | 30 | tool.uploadFile(config, filePath, toPath, function(err, cosFilepath) { 31 | var destPath = '.tmp/'+pathLib.basename(filePath); 32 | var folder = config.folder; 33 | 34 | if(folder && folder.indexOf('/') != 0) { 35 | folder = '/' + folder; 36 | } 37 | 38 | var req; 39 | var picURL = 'http://' + config.bucket + '.pic.' + config.region + '.myqcloud.com' + folder + toPath + '?imageView2/q/70'; 40 | 41 | if(!isOA) { 42 | req = request(picURL); 43 | }else { 44 | var _req = request.defaults({'proxy':'http://dev-proxy.oa.com:8080'}); 45 | req = _req(picURL); 46 | } 47 | 48 | req.on('response', function() { 49 | var ws = fs.createWriteStream(destPath); 50 | req.pipe(ws); 51 | ws.on('close', function() { 52 | cb(destPath); 53 | }); 54 | }).on('error', function(err) { 55 | cb(err); 56 | }); 57 | }); 58 | } 59 | 60 | module.exports = compress; 61 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | var pathLib = require('path'); 6 | var CWD = process.cwd(); 7 | 8 | /** 9 | * 全局配置 10 | * @type {Object} 11 | * appDir [String] 小程序项目目录,默认./app 12 | * uploadFileSuffix [Array] 上传的图片后缀名,默认jpg, png, gif 13 | * uploadFileBlackList [Path or File] 指定不上传的文件或者路径,不支持正则 14 | * watch [Boolean] 是否实时监听,默认true 15 | * compress [Boolean] 上传的图片是否使用优图的压缩功能,默认false 16 | * cos [Object] 查看https://www.qcloud.com/document/product/436/6066 17 | */ 18 | 19 | var _defaultConfig = { 20 | "appDir": "./app", 21 | "backupDir": "./wecos_backup", 22 | "uploadFileSuffix": [".jpg", ".png", ".gif"], 23 | "uploadFileBlackList": [], 24 | "replaceHost": "", 25 | "targetHost": "", 26 | "compress": false, 27 | "watch": true, 28 | "cos": { 29 | "secret_id": "", 30 | "secret_key": "", 31 | "appid": "", 32 | "bucketname": "", 33 | "bucket": "", 34 | "region": "", 35 | "folder": "", 36 | } 37 | }; 38 | 39 | function initByOption(option) { 40 | _.assignIn(config, option) 41 | } 42 | 43 | function initByFile(configPath) { 44 | var exists = fs.existsSync(configPath); 45 | if (!exists) { 46 | throw('need file wecos.config.json'); 47 | } 48 | var content = fs.readFileSync(configPath).toString(); 49 | try { 50 | var userConfig = (new Function('return (' + content + ')'))(); 51 | } catch (e) { 52 | throw('wecos.config.json is not JSON format!'); 53 | } 54 | _.assignIn(config, userConfig); 55 | var appDir = pathLib.join(CWD, config.appDir); 56 | var appDirExists = fs.existsSync(appDir); 57 | if (!appDirExists) { 58 | throw('appDir is not exist'); 59 | } 60 | if (!userConfig.cos) { 61 | throw('option "cos" need in config!'); 62 | } 63 | if (userConfig.replaceHost && !userConfig.targetHost) { 64 | throw('option "targetHost" need in config!'); 65 | } 66 | var cosOptionNeed = ['region', 'secret_key', 'secret_id']; 67 | for (var i = 0; i < cosOptionNeed.length; i++) { 68 | if (!userConfig.cos[cosOptionNeed[i]]) { 69 | throw('option "cos.' + cosOptionNeed[i] + '" need in config!'); 70 | } 71 | } 72 | if (!(userConfig.cos.bucket || (userConfig.cos.bucketname && userConfig.cos.appid))) { 73 | throw('option "cos.bucket" need in config! format as "test-1251902136"'); 74 | } 75 | } 76 | 77 | var config = _.assign(_defaultConfig); 78 | 79 | config.init = function(option){ 80 | if (!option) { 81 | option = pathLib.join(CWD, 'wecos.config.json'); 82 | } 83 | if(_.isString(option)){ 84 | initByFile(option) 85 | }else if(_.isObject(option)) { 86 | initByOption(option) 87 | } 88 | if (!config.cos.bucket && config.cos.bucket && config.cos.appid) { 89 | config.cos.bucket = config.cos.bucketname + '-' + config.cos.appid; 90 | } 91 | if (config.cos.bucket && !(config.cos.bucket || config.cos.appid)) { 92 | var lastIndex = config.cos.bucket.lastIndexOf('') 93 | config.cos.bucketname = config.cos.bucket.substr(0, lastIndex) 94 | config.cos.appid = config.cos.bucket.substr(lastIndex + 1) 95 | } 96 | } 97 | 98 | module.exports = config; 99 | 100 | 101 | -------------------------------------------------------------------------------- /lib/task.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var _ = require('lodash'); 3 | var glob = require("glob"); 4 | var pathLib = require('path'); 5 | var globalConfig = require('./config'); 6 | var upload = require('./upload'); 7 | 8 | var CWD = process.cwd(); 9 | var SUFFIX = ['wxml', 'wxss']; 10 | var REG_PROTOCOL = /^([a-zA-Z]{2,}:)\/\//; 11 | var REG_IMAGE_TAG = /(]*)? src *= *['|"])(.*?)(['|"])/g; 12 | var REG_COVER_IMAGE_TAG = /(]*)? src *= *['|"])(.*?)(['|"])/g; 13 | var REG_IMAGE_STYLE = /(background(-image)? *:[^;]*url *\( *['|"]?)(.*?)(['|"]? *\))/g; 14 | var APP; 15 | 16 | 17 | // 多层创建目录 18 | var deepMkdir = function(dirpath, callback) { 19 | var exists = fs.existsSync(dirpath); 20 | if(!exists) { 21 | deepMkdir(pathLib.dirname(dirpath), callback); 22 | fs.mkdirSync(dirpath, callback); 23 | } 24 | }; 25 | 26 | // 备份源文件 27 | var backupFile = function (fromPath, callback) { 28 | if (globalConfig.backupDir === null || globalConfig.backupDir === '') return; 29 | var toPath = pathLib.resolve(globalConfig.backupDir, pathLib.relative(APP, fromPath)); 30 | var toDir = pathLib.dirname(toPath); 31 | var cb = function (err) { 32 | callback && callback(err); 33 | }; 34 | var backup = function () { 35 | fs.readFile(fromPath, function(err, data) { 36 | if (err) { 37 | cb(err); 38 | } else { 39 | fs.writeFile(toPath, data, function(err){ 40 | if (err) { 41 | cb(err); 42 | } else { 43 | if (fs.existsSync(fromPath)) { 44 | fs.unlink(fromPath, cb); 45 | } 46 | } 47 | }); 48 | } 49 | }); 50 | }; 51 | if (fs.existsSync(toDir)) { 52 | backup(); 53 | } else { 54 | deepMkdir(toDir, backup); 55 | } 56 | }; 57 | 58 | // 格式化 url 59 | var formatUrl = function (url, defaultProtocol) { 60 | defaultProtocol = defaultProtocol || 'http:'; 61 | if (url.substr(0, 2) == '//') { 62 | url = defaultProtocol + url; 63 | } 64 | if (!REG_PROTOCOL.test(url)) { 65 | url = defaultProtocol + '//' + url; 66 | } 67 | return url; 68 | }; 69 | 70 | // 替换 url 里的 host 71 | var replaceUrlHost = function (url, fromHost, toHost) { 72 | if (!fromHost || !fromHost.length || !toHost || !REG_PROTOCOL.test(url)) return url; 73 | fromHost.forEach(function (host) { 74 | var match = url.match(REG_PROTOCOL); 75 | var protocol = match ? match[1] : 'http:'; 76 | var formattedFromHost = formatUrl(host, protocol); 77 | var formattedToHost = formatUrl(toHost, protocol); 78 | if (url.substr(0, formattedFromHost.length) == formattedFromHost) { 79 | var pathname = url.substr(formattedFromHost.length); 80 | var sep = pathname.substr(0, 1) == '/' || formattedToHost.slice(-1) == '/' ? '/' : ''; 81 | formattedToHost = formattedToHost.replace(/\/+$/, ''); 82 | pathname = pathname.replace(/^\/+/, ''); 83 | sep = !sep && pathname ? '/' : sep; 84 | url = formattedToHost + sep + pathname; 85 | } 86 | }); 87 | return url; 88 | }; 89 | 90 | // 替换文件内的资源文件 91 | var onlineLinks = function (currentPath, resourcePath, url) { 92 | var content = fs.readFileSync(currentPath).toString(); 93 | var oldContent = content; 94 | var replaceList = []; 95 | // 先替换 url 96 | var match = url.match(/^([a-zA-Z]{2,}:)\/\/[^/]+/); 97 | if (!match) return replaceList; 98 | if (globalConfig.targetHost) { 99 | url = replaceUrlHost(url, _.concat([], globalConfig.replaceHost, match && match[0]), globalConfig.targetHost); 100 | } 101 | // 找出资源链接,并替换 102 | var replaceHandler = function (s, m1, m2, oldUrl, m4) { 103 | var findResourcePath = oldUrl.substr(0, 1) === '/' ? pathLib.join(APP, oldUrl) : 104 | pathLib.resolve(pathLib.dirname(currentPath), oldUrl); 105 | if (pathLib.resolve(findResourcePath) == pathLib.resolve(resourcePath)) { 106 | replaceList.push({ 107 | file: pathLib.relative(APP, currentPath), 108 | from: oldUrl, 109 | to: url 110 | }); 111 | return m1 + url + m4; 112 | } else { 113 | return s; 114 | } 115 | }; 116 | content = content.replace(REG_IMAGE_TAG, replaceHandler); 117 | content = content.replace(REG_COVER_IMAGE_TAG, replaceHandler); 118 | // 如果文件内容有改变,写入文件 119 | if (oldContent !== content) { 120 | fs.writeFileSync(currentPath, content); 121 | } 122 | return replaceList; 123 | }; 124 | 125 | // 替换文件内的链接为新的 Host 126 | var replaceLinks = function (currentPath) { 127 | var replaceList = []; 128 | var fromHost = _.isArray(globalConfig.replaceHost) ? globalConfig.replaceHost : [globalConfig.replaceHost]; 129 | var toHost = globalConfig.targetHost; 130 | var content = fs.readFileSync(currentPath).toString(); 131 | var oldContent = content; 132 | var _replaceM3 = function (s, m1, m2, oldUrl, m4) { 133 | var url = replaceUrlHost(oldUrl, fromHost, toHost); 134 | if (oldUrl != url) { 135 | replaceList.push({ 136 | file: pathLib.relative(APP, currentPath), 137 | from: oldUrl, 138 | to: url 139 | }); 140 | } 141 | return m1 + url + m4; 142 | }; 143 | content = content.replace(REG_IMAGE_TAG, _replaceM3); 144 | content = content.replace(REG_COVER_IMAGE_TAG, _replaceM3); 145 | content = content.replace(REG_IMAGE_STYLE, _replaceM3); 146 | // 如果文件内容有改变,写入文件 147 | if (oldContent !== content) { 148 | fs.writeFileSync(currentPath, content); 149 | } 150 | return replaceList; 151 | }; 152 | 153 | // 上传文件 154 | var uploadFile = (function () { 155 | var count = 0; 156 | var taskQueue = []; 157 | // 最多 5 个并发上传任务 158 | var next = function () { 159 | if (count > 5 || !taskQueue.length) return; 160 | ++count; 161 | var task = taskQueue.shift(); 162 | var localPath = pathLib.relative(CWD, task.filepath); 163 | var remotePath = pathLib.relative(APP, task.filepath).replace(pathLib.sep, '/'); 164 | upload(localPath, remotePath, function (err, url) { 165 | if (!err) backupFile(task.filepath); 166 | task.callback && task.callback(err, url); 167 | --count; 168 | next(); 169 | }); 170 | }; 171 | 172 | return function (filepath, callback) { 173 | taskQueue.push({ 174 | filepath: filepath, 175 | callback: callback 176 | }); 177 | next(); 178 | }; 179 | })(); 180 | 181 | // 扫描所有文件,替换成新的链接 182 | var scanFiles = function (iterator, complete) { 183 | glob(pathLib.join(APP, '/**/*') + '.{' + SUFFIX.join(',') + '}', function (er, files) { 184 | var replaceList = []; 185 | files.forEach( function (filepath){ 186 | if (SUFFIX.indexOf(filepath.split('.').pop()) > -1) { 187 | var rList = iterator(filepath); 188 | replaceList = _.concat(replaceList, rList); 189 | } 190 | }); 191 | complete && complete(null, replaceList); 192 | }); 193 | }; 194 | 195 | // 上传资源文件并替换线上链接 196 | exports.resourceToCloud = function (resourcePath, callback) { 197 | APP = pathLib.join(CWD, globalConfig.appDir); 198 | fs.lstat(resourcePath, function (err, stat) { 199 | if (stat.size > 0) { 200 | uploadFile(resourcePath, function (err, url) { 201 | if (err) { 202 | callback(err); 203 | } else { 204 | scanFiles(function (currentPath) { 205 | return onlineLinks(currentPath, resourcePath, url); 206 | }, callback); 207 | } 208 | }); 209 | } else { 210 | callback && callback('FILE EMPTY'); 211 | } 212 | }); 213 | }; 214 | 215 | // 替换资源文件的域名 216 | exports.resourceReplaceHost = function (callback) { 217 | APP = pathLib.join(CWD, globalConfig.appDir); 218 | if (globalConfig.replaceHost && globalConfig.targetHost) { 219 | scanFiles(replaceLinks, callback); 220 | } 221 | }; 222 | 223 | if (!module.parent) { 224 | exports.resourceReplaceHost(); 225 | } -------------------------------------------------------------------------------- /lib/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //鉴权 4 | //上传 5 | //压缩 6 | var fs = require('fs'); 7 | var pathLib = require('path'); 8 | var _ = require('lodash'); 9 | var globalConfig = require('./config'); 10 | var compress = require('./compress'); 11 | var tool = require('./common'); 12 | 13 | //上传 14 | function upload(fromPath,toPath,cb) { 15 | 16 | var isExists = fs.existsSync(fromPath); 17 | 18 | if(!isExists) { 19 | cb('FILE NOT EXISTS'); 20 | return; 21 | } 22 | 23 | if(globalConfig.compress) { 24 | compress(globalConfig.cos, fromPath, toPath, function(destPath) { 25 | if(!_.isString(destPath)) { 26 | var err = destPath; 27 | cb(err); 28 | } else { 29 | tool.uploadFile(globalConfig.cos, destPath, toPath, cb); 30 | } 31 | }); 32 | }else { 33 | tool.uploadFile(globalConfig.cos, fromPath, toPath, cb); 34 | } 35 | 36 | } 37 | 38 | module.exports = upload; 39 | 40 | -------------------------------------------------------------------------------- /lib/watcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global require */ 4 | var fs = require('fs'); 5 | var pathLib = require('path'); 6 | var watch = require('watch'); 7 | var _ = require('lodash'); 8 | var ora = require('ora'); 9 | var glob = require('glob'); 10 | var globalConfig = require('./config'); 11 | var task = require('./task'); 12 | var CWD = process.cwd(); 13 | 14 | 15 | function normalizePath(path) { 16 | path = pathLib.normalize(path); 17 | 18 | return path; 19 | } 20 | 21 | function joinPath(path) { 22 | var path = pathLib.join(CWD, path); 23 | 24 | return path; 25 | } 26 | 27 | function relativePath(from, to) { 28 | var path = pathLib.relative(from, to); 29 | 30 | return path; 31 | } 32 | 33 | function resolvePath(path) { 34 | var path = pathLib.resolve(path); 35 | 36 | return path; 37 | } 38 | 39 | function fixPathToUnix(path) { 40 | 41 | path = path.replace(/\//g, '\\'); 42 | 43 | return path; 44 | } 45 | 46 | function showReplaceList(replaceList) { 47 | replaceList.forEach(function (item) { 48 | console.log('[file:' + item.file + ']', 'replace host from', '"' + item.from + '"', 'to', '"' + item.to + '"'); 49 | }); 50 | } 51 | 52 | var taskTimer = {}; 53 | function runFileTask(filepath, event, stat) { 54 | 55 | // taskTimer[filepath] && clearTimeout(taskTimer[filepath]); 56 | // taskTimer[filepath] = setTimeout(function () { 57 | // taskTimer[filepath] = null; 58 | 59 | if( pathLib.extname(filepath) 60 | && _.includes(globalConfig['uploadFileSuffix'], pathLib.extname(filepath)) 61 | ) { 62 | 63 | var isBlackList = _.some(globalConfig.uploadFileBlackList, function(item) { 64 | 65 | if(resolvePath(item) === resolvePath(filepath)) { 66 | return true 67 | } 68 | if(_.startsWith(resolvePath(filepath), resolvePath(item))) { 69 | return true 70 | } 71 | }) 72 | if(isBlackList) return; 73 | 74 | if (!fs.existsSync(filepath)) return; 75 | 76 | 77 | filepath = relativePath(CWD, filepath); 78 | 79 | var spinner = ora('upload: ' + filepath).start(); 80 | task.resourceToCloud(filepath, function(err) { 81 | if(err) { 82 | spinner.fail(); 83 | console.log('Error:', err); 84 | return 85 | } 86 | // console.log(filepath + ' 已传上cos'); 87 | spinner.succeed(); 88 | 89 | // console.log('event', event, 'filepath', filepath + ' 已传上cos'); 90 | 91 | }); 92 | 93 | } 94 | // }, 300); 95 | 96 | } 97 | 98 | function watchFile(option) { 99 | 100 | globalConfig.init(option); 101 | 102 | var appDir = relativePath(CWD, resolvePath(globalConfig.appDir)); 103 | var uploadSuffix = globalConfig.uploadFileSuffix; 104 | uploadSuffix = _.isArray(uploadSuffix) ? uploadSuffix : [uploadSuffix]; 105 | var watchList = _.map(uploadSuffix, function(item) { 106 | return pathLib.join(appDir, '**/*' + item); 107 | }); 108 | var uploadBlackList = globalConfig.uploadFileBlackList; 109 | uploadBlackList = _.isArray(uploadBlackList) ? uploadBlackList : [uploadBlackList]; 110 | uploadBlackList = _.map(uploadBlackList, function(item) { 111 | if(fs.lstatSync(resolvePath(item)).isFile()) { 112 | return item 113 | }else if(fs.lstatSync(resolvePath(item)).isDirectory()) { 114 | return pathLib.join(item, '**'); 115 | } 116 | }); 117 | 118 | task.resourceReplaceHost(function (err, replaceList) { 119 | if (!err) { 120 | replaceList.length && console.log('replace host finish:'); 121 | showReplaceList(replaceList); 122 | } 123 | }); 124 | 125 | console.log('watchList', watchList); 126 | console.log('uploadBlackList', uploadBlackList); 127 | 128 | if (globalConfig.watch) { 129 | console.log('/***** 【微信 COS 瘦身工具】 start watching *****/'); 130 | console.log('/***** 【Listening App path】 ' + appDir + ' *****/'); 131 | 132 | glob('{' + watchList.join(',') + '}', { 133 | ignore: uploadBlackList 134 | },function (err, files) { 135 | files.forEach(function (filepath) { 136 | runFileTask(filepath); 137 | }); 138 | }); 139 | watch.createMonitor(appDir, { 140 | interval: 1 141 | },function (monitor) { 142 | 143 | monitor.on("created", function(f, stat) { 144 | runFileTask(f, 'created', stat) 145 | }) 146 | monitor.on("changed", function(f, stat) { 147 | runFileTask(f, 'changed', stat) 148 | }) 149 | 150 | }); 151 | } else { 152 | console.log('/***** 【微信 COS 瘦身工具】 start *****/'); 153 | console.log('/***** 【App Path】 ' + appDir + ' *****/'); 154 | glob('{' + watchList.join(',') + '}', { 155 | ignore: uploadBlackList 156 | },function (err, files) { 157 | files.forEach(function (filepath) { 158 | runFileTask(filepath); 159 | }); 160 | }); 161 | } 162 | 163 | } 164 | 165 | module.exports = watchFile; 166 | if (!module.parent) { 167 | watchFile(); 168 | console.log('watching!'); 169 | } 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wecos", 3 | "version": "2.0.3", 4 | "description": "Tencent Qcloud COS about node utilities for wxapp minify.", 5 | "keywords": [ 6 | "cos", 7 | "wxapp", 8 | "minify" 9 | ], 10 | "main": "index.js", 11 | "scripts": { 12 | "test": "cd test && node index.js", 13 | "test-config-file": "cd test && node test-config-file.js", 14 | "test-config-option": "cd test && node test-config-option.js" 15 | }, 16 | "bin": { 17 | "wecos": "./bin/wecos.js" 18 | }, 19 | "homepage": "https://github.com/tencentyun/wecos", 20 | "bugs": "https://github.com/tencentyun/wecos/issues", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/tencentyun/wecos.git" 24 | }, 25 | "author": { 26 | "name": "carsonxu" 27 | }, 28 | "maintainers": [ 29 | { 30 | "name": "carsonxu" 31 | }, 32 | { 33 | "name": "galen-yip", 34 | "email": "s275497985@gmail.com" 35 | }, 36 | { 37 | "name": "yinshawnrao" 38 | } 39 | ], 40 | "engines": { 41 | "node": ">=0.9" 42 | }, 43 | "license": "MIT", 44 | "dependencies": { 45 | "cos-nodejs-sdk-v5": "^2.4.9", 46 | "glob": "^7.1.1", 47 | "lodash": "^4.17.4", 48 | "ora": "^0.4.0", 49 | "request": "^2.79.0", 50 | "watch": "^1.0.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../bin/wecos.js'); -------------------------------------------------------------------------------- /test/test-config-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var wecos = require('../lib/watcher.js'); 4 | wecos('./wecos.config.json'); -------------------------------------------------------------------------------- /test/test-config-option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var config = require('./wecos.config.js'); 3 | var wecos = require('../lib/watcher.js'); 4 | wecos(config); -------------------------------------------------------------------------------- /test/test-task.js: -------------------------------------------------------------------------------- 1 | var resourceToCloud = require('../task'); 2 | 3 | var CWD = __dirname; 4 | resourceToCloud([ 5 | path.join(CWD, 'app/images/logo.png'), 6 | path.join(CWD, 'app/images/camera.png'), 7 | path.join(CWD, 'app/images/qr.png') 8 | ], function () { 9 | console.log('cloud finished!'); 10 | }); -------------------------------------------------------------------------------- /test/test-upload.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var upload = require('../lib/upload'); 3 | 4 | upload(path.join(__dirname, 'test.png'), '/test.jpg', function(res) { 5 | console.log(res); 6 | }); 7 | -------------------------------------------------------------------------------- /test/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencentyun/wecos/bc3537e9ececd7bf047255aaee86e04614f1790f/test/test.jpg --------------------------------------------------------------------------------