废话不多说,直接上完整可用代码。
1、使用npm安装 streamsaver 库,这里我用的是 2.0.4 版本,
npm install streamsaver --s
2、官方提供了打包zip并下载的demo 查看,会发现demo中对于zip引入了一个 zip-stream.js 的文件,这个文件其实在安装的 streamsaver 库中存在,node_modules/streamsaver/examples/zip-stream.js,我们将这个文件复制出来在入口函数前加个export default,完整代码如下
class Crc32 {
constructor() {
this.crc = -1
}
append(data) {
var crc = this.crc | 0;
var table = this.table
for (var offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
}
this.crc = crc
}
get() {
return ~this.crc
}
}
Crc32.prototype.table = (() => {
var i;
var j;
var t;
var table = []
for (i = 0; i < 256; i++) {
t = i
for (j = 0; j < 8; j++) {
t = (t & 1)
? (t >>> 1) ^ 0xEDB88320
: t >>> 1
}
table[i] = t
}
return table
})()
const getDataHelper = byteLength => {
var uint8 = new Uint8Array(byteLength)
return {
array: uint8,
view: new DataView(uint8.buffer)
}
}
const pump = zipObj => zipObj.reader.read().then(chunk => {
if (chunk.done) return zipObj.writeFooter()
const outputData = chunk.value
zipObj.crc.append(outputData)
zipObj.uncompressedLength += outputData.length
zipObj.compressedLength += outputData.length
zipObj.ctrl.enqueue(outputData)
})
/**
* [createWriter description]
* @param {Object} underlyingSource [description]
* @return {Boolean} [description]
*/
export default function createWriter(underlyingSource) {
const files = Object.create(null)
const filenames = []
const encoder = new TextEncoder()
let offset = 0
let activeZipIndex = 0
let ctrl
let activeZipObject, closed
function next() {
activeZipIndex++
activeZipObject = files[filenames[activeZipIndex]]
if (activeZipObject) processNextChunk()
else if (closed) closeZip()
}
var zipWriter = {
enqueue(fileLike) {
if (closed) throw new TypeError('Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed')
let name = fileLike.name.trim()
const date = new Date(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified)
if (fileLike.directory && !name.endsWith('/')) name += '/'
if (files[name]) throw new Error('File already exists.')
const nameBuf = encoder.encode(name)
filenames.push(name)
const zipObject = files[name] = {
level: 0,
ctrl,
directory: !!fileLike.directory,
nameBuf,
comment: encoder.encode(fileLike.comment || ''),
compressedLength: 0,
uncompressedLength: 0,
writeHeader() {
var header = getDataHelper(26)
var data = getDataHelper(30 + nameBuf.length)
zipObject.offset = offset
zipObject.header = header
if (zipObject.level !== 0 && !zipObject.directory) {
header.view.setUint16(4, 0x0800)
}
header.view.setUint32(0, 0x14000808)
header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true)
header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true)
header.view.setUint16(22, nameBuf.length, true)
data.view.setUint32(0, 0x504b0304)
data.array.set(header.array, 4)
data.array.set(nameBuf, 30)
offset += data.array.length
ctrl.enqueue(data.array)
},
writeFooter() {
var footer = getDataHelper(16)
footer.view.setUint32(0, 0x504b0708)
if (zipObject.crc) {
zipObject.header.view.setUint32(10, zipObject.crc.get(), true)
zipObject.header.view.setUint32(14, zipObject.compressedLength, true)
zipObject.header.view.setUint32(18, zipObject.uncompressedLength, true)
footer.view.setUint32(4, zipObject.crc.get(), true)
footer.view.setUint32(8, zipObject.compressedLength, true)
footer.view.setUint32(12, zipObject.uncompressedLength, true)
}
ctrl.enqueue(footer.array)
offset += zipObject.compressedLength + 16
next()
},
fileLike
}
if (!activeZipObject) {
activeZipObject = zipObject
processNextChunk()
}
},
close() {
if (closed) throw new TypeError('Cannot close a readable stream that has already been requested to be closed')
if (!activeZipObject) closeZip()
closed = true
}
}
function closeZip() {
var length = 0
var index = 0
var indexFilename, file
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
length += 46 + file.nameBuf.length + file.comment.length
}
const data = getDataHelper(length + 22)
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
data.view.setUint32(index, 0x504b0102)
data.view.setUint16(index + 4, 0x1400)
data.array.set(file.header.array, index + 6)
data.view.setUint16(index + 32, file.comment.length, true)
if (file.directory) {
data.view.setUint8(index + 38, 0x10)
}
data.view.setUint32(index + 42, file.offset, true)
data.array.set(file.nameBuf, index + 46)
data.array.set(file.comment, index + 46 + file.nameBuf.length)
index += 46 + file.nameBuf.length + file.comment.length
}
data.view.setUint32(index, 0x504b0506)
data.view.setUint16(index + 8, filenames.length, true)
data.view.setUint16(index + 10, filenames.length, true)
data.view.setUint32(index + 12, length, true)
data.view.setUint32(index + 16, offset, true)
ctrl.enqueue(data.array)
ctrl.close()
}
function processNextChunk() {
if (!activeZipObject) return
if (activeZipObject.directory) return activeZipObject.writeFooter(activeZipObject.writeHeader())
if (activeZipObject.reader) return pump(activeZipObject)
if (activeZipObject.fileLike.stream) {
activeZipObject.crc = new Crc32()
activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
activeZipObject.writeHeader()
} else next()
}
return new ReadableStream({
start: c => {
ctrl = c
underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter))
},
pull() {
return processNextChunk() || (
underlyingSource.pull &&
Promise.resolve(underlyingSource.pull(zipWriter))
)
}
})
}
3、接下来根据官方下载zip的demo可以知道 streamsaver 的使用方式,这里我们简化下,简单封装个函数
// 导入streamsaver
import StreamSaver from "streamsaver";
// 导入上面我们修改过的zip-stream
import ZIP from "./zip-stream";
/**
* 将多个文件写进压缩包
* @param zipName zip文件名,不带文件后缀名
* @param fileOptions 文件配置集合,数据格式 [{name: '文件名,可以带路径', content: '文件内容'}]
* @param streamCreator stream创建器,第一个参数是当前的fileOption配置,第二个参数是默认的stream创建方式
* @returns 返回promise对象
*/
export function writerAsZip(zipName, fileOptions, streamCreator = (fileOption, def) => def) {
return new Promise((resolve, reject) => {
const fileStream = StreamSaver.createWriteStream(`${zipName}.zip`);
const files = fileOptions.map(fileOption => {
return {
name: `${fileOption.name}`,
stream: streamCreator(fileOption, () => new Blob([fileOption.content]).stream())
};
});
const readableZipStream = new ZIP({
start(ctrl) {
files.forEach(file => ctrl.enqueue(file));
ctrl.close()
}
});
if (window.WritableStream && readableZipStream.pipeTo) {
return readableZipStream.pipeTo(fileStream)
.then(resolve, reject);
}
reject();
});
}
4、上面的函数只是将文本型文件写进zip,接下来我们只要使用第三个参数 streamCreator 自定义下对图片的stream创建方式即可,可以看出,streamsaver 是从 Blob 对象获取的stream,因此我们只需要获取到图片的 Blob 对象即可,因此需要下面这个函数
/**
* 获取图片的Blob对象
* @param src 图片地址
* @returns 返回promise对象
*/
export function imageToBlob(src) {
return new Promise(function (resolve, reject) {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let img = new Image;
img.crossOrigin = 'Anonymous';
img.onload = () => {
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img, 0, 0);
canvas.toBlob(resolve);
canvas = null;
};
img.src = src;
});
}
5、最后结合2个函数来实现下我们的需求
// 这里我使用当前前端项目下的一个图片
import DownloadImg from '../../../assets/download.jpg'
// 关键代码
export default {
methods: {
downloadZip() {
let fileOptions = [
{
name: 'files/file1.txt',
content: '第一个文件'
},
{
name: 'files/file2.txt',
content: '第二个文件'
}
];
// 获取图片的 Blob ,这里演示的是单张图片,由于该函数返回promise因此多张图片可以使用Promise.all来实现
imageToBlob(DownloadImg)
.then(data => {
fileOptions.push(
{
name: 'files/图片.jpg',
// 将图片的 Blob 对象放在 content 属性上
content: data
}
);
writerAsZip('压缩包', fileOptions, (fileOption, def) => {
if (fileOption.name === 'files/图片.jpg') {
// 自定义下图片的stream创建方式
return () => fileOption.content.stream()
}
// 其余还是使用默认的stream创建方式
return def;
}).then(() => {
// TODO 下载成功 继续做你想做的事情吧
}).catch(err => {
// TODO 下载失败
});
})
.catch(err => {
// TODO 获取图片 Blob 失败
});
}
}
}
最后给vue开发的小伙伴推荐一款表单设计器:JForm表单设计器