前端 Vue 将图片添加到zip压缩文件并下载 streamsaver

废话不多说,直接上完整可用代码。

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,完整代码如下

zip-stream.js

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表单设计器

你可能感兴趣的:(vue,前端,vue,zip压缩,streamsaver,图片压缩)