内容简介:前端项目开发完成后需要部署到服务器,为了减轻业务服务器的压力,以及为了更快的浏览器初次渲染速度,会做动静分离,也就是静态资源分离到CDN中去,动态生成的资源(主要是接口)才会部署到自己服务器上。webpack支持output.publicPath来替换打包出的资源中assets的引用路径,output.publicPath配置为http://xxx.cdn.com/,那么/static/a.jpg 的路径就会被替换为http://xxx.cdn.com/static/a.jpg。这样我们只要把静态文件上传到
前端项目开发完成后需要部署到服务器,为了减轻业务服务器的压力,以及为了更快的浏览器初次渲染速度,会做动静分离,也就是静态资源分离到CDN中去,动态生成的资源(主要是接口)才会部署到自己服务器上。
webpack支持output.publicPath来替换打包出的资源中assets的引用路径,output.publicPath配置为http://xxx.cdn.com/,那么/static/a.jpg 的路径就会被替换为http://xxx.cdn.com/static/a.jpg。这样我们只要把静态文件上传到CDN就好了。
最近的一次会议上,我们分析服务端的统计数据的时候发现服务器30%的流量都被静态资源占去了,这反映出我们急需把静态资源批量上传到CDN上,减轻业务服务器的压力。而我们正缺少这样的一个工具,于是我们就基于node开发了一个静态文件上传CDN的工具。
分析下需求,主要有这么几点:
- 能够把指定路径下指定模式(后缀名等)的文件匹配出来
- 能够批量的并发的上传,但并发数量要可控
- 多次上传能够识别出更改的部分,实现增量上传
方案设计
基于这3点需求,我们进行了调研和设计,最终方案是这样的:
实现第一点需求(匹配指定模式的文件),可以使用node-dir实现,readFiles方法支持读取一个目录下的文件,根据一些模式来过滤:
dir.readFiles(__dirname, { match: /.txt$/, exclude: /^\./ }, function(err, content, next) { if (err) throw err; console.log('content:', content); next(); }, function(err, files){ if (err) throw err; console.log('finished reading files:',files); }); 复制代码
实现第二点需求(异步上传文件)可以使用p-queue ,支持传入多个异步的promise对象,然后指定并发数concurrency。
const queue = new PQueue({ concurrency: limit }); const files = ['/static/a.jpg', '/static/b.jpg']; queue.addAll( files.map((filePath) => () => uploadFile(targetProject, { ...data, file: filePath, filename: path.relative(uploadDir, filePath).replace(/[\\]/g, '/'), }).then((rs) => { result.push(rs); }), ), ); 复制代码
进度条可以使用cli-progress 来实现,结合上面的p-queue来显示进度。
const cliProgress = require('cli-progress'); const bar = new cliProgress.Bar( { format: '上传进度 [{bar}] {percentage}% | 预计: {eta}s | {value}/{total}', }, cliProgress.Presets.rect, ); bar.start(files.length, 0); bar.increment();//每个文件上传完成时 bar.stop(); 复制代码
第三点需求(增量上传)的方案是这样的,使用node-dir匹配出文件列表之后,生成每个文件的md5,文件路径作为值,生成一个map,叫做toUploadManifest,然后上传完成后,把上传过的文件的内容md5和文件路径生成uploadedManifest。每次上传之前把toUploadManifest 中在uploadedManifest出现过的文件都去掉,这样就实现了增量的上传。
md5的生成使用node的crypto内置模块:
/** * buffer to md5 str * @param {*} buffer */ function bufferToMD5(buffer) { const md5 = crypto.createHash('md5'); md5.update(buffer); return md5.digest('base64'); } 复制代码
生成toUploadManifest:
/** * * 生成toUpload清单 * @param {*} files 待上传文件 */ function generateToUploadManifest(filePaths = []) { return Promise.all(filePaths.map(filePath => new Promise((resolve) => { fs.readFile(filePath, (err, content) => { if (err) { console.log(filePath + '读取失败'); return; } const md5 = bufferToMD5(content); resolve({ [md5]: filePath }); }); }))).then(manifestItems => manifestItems.length ? Object.assign(...manifestItems) : {}) } 复制代码
读取uploadedManifest.json:
/** * 获取uploadedManifest */ const UPLOADED_MANIFEST_PATH = path.resolve(process.cwd(), 'uploadedManifest.json'); function getUploadedManifest() { try { const uploadedManifestStr = fs.readFileSync(UPLOADED_MANIFEST_PATH); return JSON.parse(uploadedManifestStr); } catch(e) { return {} } } 复制代码
更新uploadedManifest.json:
/** * 更新uploadedManifest */ function updateUploadedManifest(filePaths) { let manifest = {}; try { const uploadedManifestStr = fs.readFileSync(UPLOADED_MANIFEST_PATH); manifest = JSON.parse(uploadedManifestStr); } catch(e) { } generateToUploadManifest(filePaths).then(uploadedManifest => { manifest = Object.assign(manifest, uploadedManifest); fs.writeFileSync(UPLOADED_MANIFEST_PATH, JSON.stringify(manifest)); }) } 复制代码
过滤掉toUploadManifest中已上传的部分:
/** * 过滤掉toUploadManifest中已上传的部分 */ function filterToUploadManifest(toUploadManifest) { console.log(); const uploadedManifest = getUploadedManifest(); Object.keys(toUploadManifest).filter(item => uploadedManifest[item]).forEach(item => { console.log(toUploadManifest[item] + ' 已上传过'); delete toUploadManifest[item] }); console.log(); return Object.values(toUploadManifest); } 复制代码
至此,实现静态文件增量上传CDN的功能就基本可以实现了。当然上传CDN的接口实现需要做一些鉴权之类的,这里因为我们后端实现了这部分功能,我们只需要调用接口就可以了,如果自己实现需要做一些鉴权。可以参看ali-oss的文档。
很多情况下上传cdn的脚本都是跑在gitlab ci的,gitlab ci使用不同的runner来执行脚本,runner可以在不同的机器上,所以想要uploadedManifest.json真正做到记录上传过的文件的功能,必须统一放到一个地方,可以结合gitlab ci的cache来实现:
image: hub.pri.xxx.com/frontend/xxx stages: - test upload: stage: test cache: paths: - node_modules - uploadedManifest.json before_script: - yarn install --slient script: - node upload.js 复制代码
总结
动静分离几乎必用的优化手段,主要有两步:webpack配置output.publicPath,然后把静态资源上传CDN。我们开发的 工具 就是实现了静态资源增量上传CDN,并且可以控制并发数。增量上传的部分可以是基于md5 + 持久化的文件来实现的,在gitlab ci的runner中运行时,要是用gitlab cache来存储清单文件。
完整代码:
// getUploadFiles.js const dir = require('node-dir'); const readline = require('readline'); function clearWrite(text) { readline.clearLine(process.stdout, 0) readline.cursorTo(process.stdout, 0) process.stdout.write(text); } /** * @description * @param {String} UploadDir, 绝对路径 * @param {Object} options { * exclude, 通过正则或数组忽略指定的文件名 * encoding, 文件编码 (默认 'utf8') * excludeDir, 通过正则或数组忽略指定的目录 * match, 通过正则或数组匹配指定的文件名 * matchDir 通过正则或数组匹配指定的目录 * } * @return {Promise} */ const getUploadFiles = (UploadDir, options) => new Promise((resolve, reject) => { let total = 0; dir.readFiles( UploadDir, options, function(err, content, next) { if (err) throw err; clearWrite(`共读取到 ${++total} 个文件`); next(); }, function(err, files) { if (err) return reject(err); return resolve(files); }, ); }); module.exports = getUploadFiles; 复制代码
//uploadFiles.js const getUploadFiles = require('./getUploadFiles.js'); const fs = require('fs'); const request = require('request'); const url = require('url'); const path = require('path'); const cliProgress = require('cli-progress'); const PQueue = require('p-queue'); const crypto = require('crypto'); const cwd = process.cwd(); const { name: projectName } = require(path.resolve(cwd, 'package.json')); const uploadUrl = 'http://xxx/xxx'; const targetHost = 'https://xxx.cdn.xxx.com/'; // 上传文件 function uploadFile (targetProject, data) { return new Promise((resolve, reject) => { request.post( { url: uploadUrl, formData: { ...data, file: fs.createReadStream(data.file), }, }, function (err, resp, body) { if (err) { return reject(err); } var result = JSON.parse(body); if (result) { const rs = { ...result, url: url.resolve(targetProject, data.filename), localPath: data.file }; return resolve(rs); } return reject(resp); }, ); }).catch((error) => { // 其他失败,导致无法继续上传,失败即退出 console.log('fail:', data.file); error && console.log('Error:', error.msg || error); return process.exit(1); }); } /** * buffer to md5 str * @param {*} buffer */ function bufferToMD5(buffer) { const md5 = crypto.createHash('md5'); md5.update(buffer); return md5.digest('base64'); } /** * * 生成toUpload清单 * @param {*} files 待上传文件 */ function generateToUploadManifest(filePaths = []) { return Promise.all(filePaths.map(filePath => new Promise((resolve) => { fs.readFile(filePath, (err, content) => { if (err) { console.log(filePath + '读取失败'); return; } const md5 = bufferToMD5(content); resolve({ [md5]: filePath }); }); }))).then(manifestItems => manifestItems.length ? Object.assign(...manifestItems) : {}) } /** * 获取uploadedManifest */ const UPLOADED_MANIFEST_PATH = path.resolve(process.cwd(), 'node_modules', 'uploadedManifest.json'); function getUploadedManifest() { try { const uploadedManifestStr = fs.readFileSync(UPLOADED_MANIFEST_PATH); console.log(uploadedManifestStr); return JSON.parse(uploadedManifestStr); } catch(e) { console.log('未找到uploadedManifest.json') return {} } } /** * 更新uploadedManifest */ function updateUploadedManifest(filePaths) { let manifest = {}; try { const uploadedManifestStr = fs.readFileSync(UPLOADED_MANIFEST_PATH); manifest = JSON.parse(uploadedManifestStr); } catch(e) { } generateToUploadManifest(filePaths).then(uploadedManifest => { manifest = Object.assign(manifest, uploadedManifest); fs.writeFileSync(UPLOADED_MANIFEST_PATH, JSON.stringify(manifest)); }) } /** * 过滤掉toUploadManifest中已上传的部分 */ function filterToUploadManifest(toUploadManifest) { console.log(); const uploadedManifest = getUploadedManifest(); Object.keys(toUploadManifest).filter(item => uploadedManifest[item]).forEach(item => { console.log(toUploadManifest[item] + ' 已上传过'); delete toUploadManifest[item] }); console.log(); return Object.values(toUploadManifest); } /** * @description * @date 2019-03-08 * @param {string} dir 本地项目目录,相对执行命令所在文件 * @param {object} { * project, 上传OSS所在目录,通常使用项目名 * limit = 5, 并发最大数 * region = 'oss-cn-hangzhou', * bucketName = 'xxx, * ...options 传递给获取文件的接口 * } * @param {function} cb * @returns Promise */ function upload ( dir, { project, limit = 5, region = 'oss-cn-hangzhou', bucketName = 'xxx', ...options }, cb, ) { const data = { region, path: project || projectName + '/', bucket_name: bucketName, filename: '', file: '', }; // 上传后的网络地址 const targetProject = url.resolve(targetHost, data.path); // 上传的本地目录 const uploadDir = path.resolve(cwd, dir); const bar = new cliProgress.Bar( { format: '上传进度 [{bar}] {percentage}% | 预计: {eta}s | {value}/{total}', }, cliProgress.Presets.rect, ); const queue = new PQueue({ concurrency: limit }); return getUploadFiles(uploadDir, options) .then((files) => { return generateToUploadManifest(files).then( toUploadManifest => { files = filterToUploadManifest(toUploadManifest); const result = []; bar.start(files.length, 0); // 添加到队列中 queue.addAll( files.map((filePath) => () => uploadFile(targetProject, { ...data, file: filePath, filename: path.relative(uploadDir, filePath).replace(/[\\]/g, '/'), }).then((rs) => { // 更新进度条 bar.increment(); result.push(rs); }), ), ); return queue.onIdle().then(() => { bar.stop(); return result; }); }) }) .then((res) => { const success = []; const fail = []; console.log(); // 全部结束 if (Array.isArray(res)) { // 更新UploadedManifest updateUploadedManifest(res.map(item => item.localPath)); // 分拣成功和失败的资源地址 res.forEach((item) => { if (item) { if (item.status) { success.push(item.url); } else { fail.push(item.url); } } }); return Promise.resolve({ success, fail, status: fail.length > 0 ? 0 : 1, // 有失败时返回 0 ,全部成功返回 1 isResolve: true, }); } return Promise.resolve(res); }) .then((rs) => { if (cb) { return rs && rs.isResolve ? cb(null, rs) : cb(rs, null); } if (rs && rs.isResolve) { return Promise.resolve(rs); } return Promise.reject(rs); }) .catch((error) => { console.log('Error:', error.msg || error); // 发生未知错误 process.exit(1); return Promise.reject(error); }); } module.exports = upload; 复制代码
//使用时: const upload = require('upload'); upload('static', { project: 'upload-test/', limit: 5, match: /\.(jpe?g|png)$/, // exclude: /\.png$/, // matchDir: ['test'] }).then((rs) => { console.log(`共成功上传${rs.success.length}个文件:\n${rs.success.join('\n')}`); if (rs.status === 1) { console.log(`已全部上传完成!`); } else { console.log(`部分文件上传失败:\n${rs.fail.join('\n')}`); } }); 复制代码
以上所述就是小编给大家介绍的《Node实现静态文件增量上传CDN》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Photoshop Anthology
Corrie Haffly / SitePoint Pty. Ltd. / 2006 / USD 39.95
The Photoshop Anthology is full-color, question-and-answer book for Web Designers who want to use Photoshop to build Websites and create better looking web graphics more effectively. The book covers: ......一起来看看 《The Photoshop Anthology》 这本书的介绍吧!