自己通过COS/CDN实现的Precompression

2023-02-23 12:25:34 浏览数 (2)

想把很多文本形式的数据放在COS上通过CDN发布。CDN自带有gzip/brotli压缩功能,确实省下了很多不必要的流量。

但是通过cdn的压缩功能来发布,要求cos上保存压缩前的数据,cdn在回源后再就地压缩。这样一方面给COS带来了很多不必要的存储(不过COS存储并不贵,这倒不是大问题);另一方面数据要用原始形态上传到COS,这样多传了好几倍的数据,上传时间也就延长了好几倍,这就有点讨厌了。

CDN的工作基本类似于Nginx,在Nginx上这个问题有很简单的解决方案是Gzip-Precompression,也就是直接把原始数据gzip压缩上传到服务器,服务器在收到支持gzip的http请求后检查到已经存在了对应的gz文件就可以直接把预压缩的数据吐出去。而CDN没有实现这个功能。那就只能自力更生了。

step 1 把数据gzip压缩后保存到COS上:

代码语言:javascript复制
const fs = require("fs"),
	zlib = require('zlib'),
	key = require('./key'),
	COS = require('cos-nodejs-sdk-v5'),
	cos = new COS(key.Secret);
async function zipFile2Cos(filePath, cosPath) {
	return new Promise((res, rej) => {
		cos.putObject({
			Bucket: key.cosInfo.Bucket,
			Region: key.cosInfo.Region,
			Key: cosPath,
			StorageClass: 'STANDARD',
			Body: fs.createReadStream(filePath).pipe(zlib.createGzip())
		}, function(err, data) {
			if (err) {
				rej(err);
			} else {
				res(data)
			}
		});
	})
}

cosPath建议使用.gz后缀,比较规范。这里因为本地已经有了文件,就直接吧文件流pipe给zlib变成压缩流然后交给COS的SDK上传,用stream方式这样处理数据可以节省大量的内存。一样的道理,如果要在数据生产程序里面上传的话可以自己包装一个可读流来做。在处理大块的数据上吃过内存溢出苦头的人都懂。

2 网页端通过cdn下载到预压缩的数据以后,用fflate来做前端解压。

代码语言:javascript复制
<script src="https://cdn.jsdelivr.net/npm/fflate@0.7.4/umd/index.js"></script>
<script>
async function unzip(path){
		return await fetch(path).then(res=>res.arrayBuffer()).then(arrayBuffer => {
		const decompress = fflate.decompressSync(new Uint8Array(arrayBuffer));
		return fflate.strFromU8(decompress);
	})
}
unzip(gzFilePath).then(data=>console.log("Unzip:",data.length))
</script>

一样的,处理大块的数据的时候用流式的方式处理更快并且更省内存,不过fetch的流( getReader.read() )读取到最后会得到一个 undefined 的 chunk,而fflate的解压流 ( fflate.Decompress )不支持push undefinde进去,要转换成空串:

代码语言:javascript复制
async function streamUnzip(path){
	let result = [];
	const utfDecode = new fflate.DecodeUTF8((data, final) => {
	  result.push(data);
	});
	const dcmpStrm = new fflate.Decompress((chunk, final) => {
	  utfDecode.push(chunk, final);
	});
	await fetch(path).then(res=>res.body.getReader()).then(async reader=>{
		while(true){
			let {done, value} = await reader.read();
			dcmpStrm.push(value || "",done);
			if(done) break;
		}
	})
	return result.join("")
}
streamUnzip(gzFilePath).then(data=>console.log("streamUnzip:",data.length))

0 人点赞