首页 > 其他分享 >前端使用StreamSaver.js流式下载大文件

前端使用StreamSaver.js流式下载大文件

时间:2024-03-22 15:26:32浏览次数:14  
标签:const StreamSaver js return let 流式 iframe new channel

最近有个需求,要求批量下载腾讯云cos文件,并打包压缩。

1. 方案一

起初用的方案,文件数据一直是以 blob 方式传递的,小文件可以成功下载,但是遇到大文件(比如几个G)一直等待且不加遮罩层loading的情况下体验效果很差。

import { saveAs } from 'file-saver';
import JSZip from 'jszip';

// 处理下载
const handleBatchDownload = (files, cosFileHerfs, downName) => {
    const zip = new JSZip();
    const cache = {};
    const promises = [];
    cosFileHerfs.forEach((cosFileHerf) => {
        files.forEach((file) => {
            if (cosFileHerf.includes(encodeURIComponent(file.cosFileName))) {
                const promise = downloadFile(cosFileHerf).then((data) => {
                    // 获取文件名
                    const cosFileName = file.cosFileName;
                    console.log('cosFileName:', cosFileName);
                    zip.file(cosFileName, data, { binary: true }); // 逐个添加文件
                    cache[cosFileName] = data;
                });
                promises.push(promise);
            }
        });
    });
    Promise.all(promises).then(() => {
        zip.generateAsync({ type: 'blob' }).then((content) => {
            // 生成二进制流
            saveAs.saveAs(content, downName); // 利用file-saver保存文件
        });
    });
};

下载请求方法:

// 下载请求
export function downloadFile(href) {
    return request({
        url: href,
        method: 'get',
        responseType: 'arraybuffer',
        timeout: 3600000,
    });
}

2. 方案二:StreamSaver.js

流式下载:能够一边在下载,一边把下载的东西写到本地。

可以下载依赖去使用,由于作者源码使用的是立即执行函数表达式(IIFE)的形式,不好直接导出 streamSaver 对象,这里我把源文件StreamSaver.js放在目录上,index.html全局引入了。zip-stream.js源码加个默认导出,就可以引入使用。

// 放在index.html
<script src="/src/plugins/StreamSaver.js"></script>

源码目录:用到 streamSave.js 和 zip-stream.js 这两个文件:

组件里面使用:

import ZIP from '@/plugins/zip-stream';  // zip-stream.js 作者提供的 zip 工具, 源码加个默认导出,就可以引入使用。 

// cosFileNameUrls是文件名和文件链接数组; downName压缩包名字 
    const handleBatchDownload = async (cosFileNameUrls, downName) => {
	const fileStream = streamSaver.createWriteStream(downName);
	const readableZipStream = new ZIP({
		async pull(ctrl) {
			for (let i = 0; i < cosFileNameUrls.length; i++) {
				const res = await fetch(cosFileNameUrls[i].url); // url是文件链接 
				const stream = () => res.body;   // 这个是 ReadableStream 类型 
				const name = cosFileNameUrls[i].cosFileName;  // 每一个文件名 
				ctrl.enqueue({ name, stream }); // 不断接收要下载的文件
			}
			ctrl.close();
		},
	});
	if (window.WritableStream && readableZipStream.pipeTo) {
		return readableZipStream.pipeTo(fileStream).then(() => console.log('压缩包下载完成'));
	}
};

3. 附上源码文件

streamSave.js 

查看代码

/*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */

/* global chrome location ReadableStream define MessageChannel TransformStream */

((name, definition) => {
	typeof module !== 'undefined' ? (module.exports = definition()) : typeof define === 'function' && typeof define.amd === 'object' ? define(definition) : (this[name] = definition());
})('streamSaver', () => {
	'use strict';

	const global = typeof window === 'object' ? window : this;
	if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread');

	let mitmTransporter = null;
	let supportsTransferable = false;
	const test = (fn) => {
		try {
			fn();
		} catch (e) {}
	};
	const ponyfill = global.WebStreamsPolyfill || {};
	const isSecureContext = global.isSecureContext;
	// TODO: Must come up with a real detection test (#69)
	let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint;
	const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style ? 'iframe' : 'navigate';

	const streamSaver = {
		createWriteStream,
		WritableStream: global.WritableStream || ponyfill.WritableStream,
		supported: true,
		version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },
		mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0',
	};

	/**
	 * create a hidden iframe and append it to the DOM (body)
	 *
	 * @param  {string} src page to load
	 * @return {HTMLIFrameElement} page to load
	 */
	function makeIframe(src) {
		if (!src) throw new Error('meh');
		const iframe = document.createElement('iframe');
		iframe.hidden = true;
		iframe.src = src;
		iframe.loaded = false;
		iframe.name = 'iframe';
		iframe.isIframe = true;
		iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args);
		iframe.addEventListener(
			'load',
			() => {
				iframe.loaded = true;
			},
			{ once: true },
		);
		document.body.appendChild(iframe);
		return iframe;
	}

	/**
	 * create a popup that simulates the basic things
	 * of what a iframe can do
	 *
	 * @param  {string} src page to load
	 * @return {object}     iframe like object
	 */
	function makePopup(src) {
		const options = 'width=200,height=100';
		const delegate = document.createDocumentFragment();
		const popup = {
			frame: global.open(src, 'popup', options),
			loaded: false,
			isIframe: false,
			isPopup: true,
			remove() {
				popup.frame.close();
			},
			addEventListener(...args) {
				delegate.addEventListener(...args);
			},
			dispatchEvent(...args) {
				delegate.dispatchEvent(...args);
			},
			removeEventListener(...args) {
				delegate.removeEventListener(...args);
			},
			postMessage(...args) {
				popup.frame.postMessage(...args);
			},
		};

		const onReady = (evt) => {
			if (evt.source === popup.frame) {
				popup.loaded = true;
				global.removeEventListener('message', onReady);
				popup.dispatchEvent(new Event('load'));
			}
		};

		global.addEventListener('message', onReady);

		return popup;
	}

	try {
		// We can't look for service worker since it may still work on http
		new Response(new ReadableStream());
		if (isSecureContext && !('serviceWorker' in navigator)) {
			useBlobFallback = true;
		}
	} catch (err) {
		useBlobFallback = true;
	}

	test(() => {
		// Transferable stream was first enabled in chrome v73 behind a flag
		const { readable } = new TransformStream();
		const mc = new MessageChannel();
		mc.port1.postMessage(readable, [readable]);
		mc.port1.close();
		mc.port2.close();
		supportsTransferable = true;
		// Freeze TransformStream object (can only work with native)
		Object.defineProperty(streamSaver, 'TransformStream', {
			configurable: false,
			writable: false,
			value: TransformStream,
		});
	});

	function loadTransporter() {
		if (!mitmTransporter) {
			mitmTransporter = isSecureContext ? makeIframe(streamSaver.mitm) : makePopup(streamSaver.mitm);
		}
	}

	/**
	 * @param  {string} filename filename that should be used
	 * @param  {object} options  [description]
	 * @param  {number} size     deprecated
	 * @return {WritableStream<Uint8Array>}
	 */
	function createWriteStream(filename, options, size) {
		let opts = {
			size: null,
			pathname: null,
			writableStrategy: undefined,
			readableStrategy: undefined,
		};

		let bytesWritten = 0; // by StreamSaver.js (not the service worker)
		let downloadUrl = null;
		let channel = null;
		let ts = null;

		// normalize arguments
		if (Number.isFinite(options)) {
			[size, options] = [options, size];
			console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream');
			opts.size = size;
			opts.writableStrategy = options;
		} else if (options && options.highWaterMark) {
			console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream');
			opts.size = size;
			opts.writableStrategy = options;
		} else {
			opts = options || {};
		}
		if (!useBlobFallback) {
			loadTransporter();

			channel = new MessageChannel();

			// Make filename RFC5987 compatible
			filename = encodeURIComponent(filename.replace(/\//g, ':')).replace(/['()]/g, escape).replace(/\*/g, '%2A');

			const response = {
				transferringReadable: supportsTransferable,
				pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
				headers: {
					'Content-Type': 'application/octet-stream; charset=utf-8',
					'Content-Disposition': "attachment; filename*=UTF-8''" + filename,
				},
			};

			if (opts.size) {
				response.headers['Content-Length'] = opts.size;
			}

			const args = [response, '*', [channel.port2]];

			if (supportsTransferable) {
				const transformer =
					downloadStrategy === 'iframe'
						? undefined
						: {
								// This transformer & flush method is only used by insecure context.
								transform(chunk, controller) {
									if (!(chunk instanceof Uint8Array)) {
										throw new TypeError('Can only write Uint8Arrays');
									}
									bytesWritten += chunk.length;
									controller.enqueue(chunk);

									if (downloadUrl) {
										location.href = downloadUrl;
										downloadUrl = null;
									}
								},
								flush() {
									if (downloadUrl) {
										location.href = downloadUrl;
									}
								},
						  };
				ts = new streamSaver.TransformStream(transformer, opts.writableStrategy, opts.readableStrategy);
				const readableStream = ts.readable;

				channel.port1.postMessage({ readableStream }, [readableStream]);
			}

			channel.port1.onmessage = (evt) => {
				// Service worker sent us a link that we should open.
				if (evt.data.download) {
					// Special treatment for popup...
					if (downloadStrategy === 'navigate') {
						mitmTransporter.remove();
						mitmTransporter = null;
						if (bytesWritten) {
							location.href = evt.data.download;
						} else {
							downloadUrl = evt.data.download;
						}
					} else {
						if (mitmTransporter.isPopup) {
							mitmTransporter.remove();
							mitmTransporter = null;
							// Special case for firefox, they can keep sw alive with fetch
							if (downloadStrategy === 'iframe') {
								makeIframe(streamSaver.mitm);
							}
						}

						// We never remove this iframes b/c it can interrupt saving
						makeIframe(evt.data.download);
					}
				} else if (evt.data.abort) {
					chunks = [];
					channel.port1.postMessage('abort'); //send back so controller is aborted
					channel.port1.onmessage = null;
					channel.port1.close();
					channel.port2.close();
					channel = null;
				}
			};

			if (mitmTransporter.loaded) {
				mitmTransporter.postMessage(...args);
			} else {
				mitmTransporter.addEventListener(
					'load',
					() => {
						mitmTransporter.postMessage(...args);
					},
					{ once: true },
				);
			}
		}

		let chunks = [];

		return (
			(!useBlobFallback && ts && ts.writable) ||
			new streamSaver.WritableStream(
				{
					write(chunk) {
						if (!(chunk instanceof Uint8Array)) {
							throw new TypeError('Can only write Uint8Arrays');
						}
						if (useBlobFallback) {
							// Safari... The new IE6
							// https://github.com/jimmywarting/StreamSaver.js/issues/69
							//
							// even though it has everything it fails to download anything
							// that comes from the service worker..!
							chunks.push(chunk);
							return;
						}

						// is called when a new chunk of data is ready to be written
						// to the underlying sink. It can return a promise to signal
						// success or failure of the write operation. The stream
						// implementation guarantees that this method will be called
						// only after previous writes have succeeded, and never after
						// close or abort is called.

						// TODO: Kind of important that service worker respond back when
						// it has been written. Otherwise we can't handle backpressure
						// EDIT: Transferable streams solves this...
						channel.port1.postMessage(chunk);
						bytesWritten += chunk.length;

						if (downloadUrl) {
							location.href = downloadUrl;
							downloadUrl = null;
						}
					},
					close() {
						if (useBlobFallback) {
							const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' });
							const link = document.createElement('a');
							link.href = URL.createObjectURL(blob);
							link.download = filename;
							link.click();
						} else {
							channel.port1.postMessage('end');
						}
					},
					abort() {
						chunks = [];
						channel.port1.postMessage('abort');
						channel.port1.onmessage = null;
						channel.port1.close();
						channel.port2.close();
						channel = null;
					},
				},
				opts.writableStrategy,
			)
		);
	}

	return streamSaver;
});

 

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)));
		},
	});
}

window.ZIP = createWriter;

标签:const,StreamSaver,js,return,let,流式,iframe,new,channel
From: https://www.cnblogs.com/shyhuahua/p/18089505

相关文章

  • Nodejs常用命令及解释
    node-启动Node.js程序解释:使用该命令可以直接运行一个Node.js程序,从而执行其中的JavaScript代码。npm-Node.js包管理器命令解释:npm是Node.js的包管理器,可以用来安装、卸载和管理Node.js包。常用的命令有:npminit-初始化一个新的Node.js项目npminsta......
  • Three.js基础入门介绍——【毕业季】Three.js动态相册
    前言岁月匆匆,又是一年毕业季,这次做个动态相册展示图片,放些有意思的内容,一起回忆下校园生活吧。预期效果相册展示和点选切换,利用相机旋转和移动来实现一个点击切图平滑过渡的效果。实现流程基本流程1、搭建场景2、放置图片3、鼠标事件4、相机运动工程文件工程......
  • 如何快速上手Vue.js,Vue.js怎么学习,看这篇就够了(全网最牛)
    1、官方文档Vue.js官方文档 是最重要的学习资源,其中包含了详细的教程、示例和API参考,让你快速了解Vue.js的核心概念和用法。2、VueCLI使用VueCLI创建项目是一个快速搭建Vue.js应用的好方法,它提供了一个交互式的界面和现代的开发工具链,让您轻松生成项目结构和......
  • Json泛型化处理
    importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.TypeReference;importjava.util.List;publicclassJSONCommonBuilder{/***Json泛型化处理*/publicstatic<T>BusinessCache<T>getBusinessCache(Objectobject,Clas......
  • js 如何提取富文本里的图片路径
    在JavaScript中,要从富文本内容中提取图片路径,你可以创建一个DOM元素来作为解析富文本内容的容器,然后将富文本内容作为文本节点插入这个容器中。接着,你可以使用querySelectorAll方法和CSS选择器来选择所有的img元素,并获取它们的src属性。以下是一个简单的示例代码functionextra......
  • 【附源码】Node.js毕业设计高校后勤管理系统(Express)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在当今信息化时代,高校后勤管理作为学校日常运营的重要组成部分,承担着保障校园环境、维护学生生活和教学秩序的重要职责。随着教育体系的不断壮大,传统的人工......
  • 【附源码】Node.js毕业设计高校后勤保修系统(Express)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在当今信息化时代,高效、便捷的管理方式已经成为了各个领域追求的目标。对于高校来说,后勤保修工作是保障校园正常运行的重要环节。传统的高校后勤保修工作主......
  • JSON格式数据
    JSON简介JSON是一种轻量级的数据交换格式,全称为JavaScriptObjectNotation。它采用完全独立于编程语言的文本格式来表示数据,具有简洁、易读、易解析等特点。简洁和清晰的层次结构使得JSON成为理想的数据交换语言,易于一般人阅读和编写。 JSON的作用可在多种语言......
  • Jackson进行JSON序列化/反序列化添加Java 8的日期和时间库支持
     添加依赖包<!--Jackson进行JSON序列化/反序列化添加Java8的日期和时间库支持--> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.13.0</version> ......
  • 前端基础之原生js事件绑定案例
    原生js事件绑定开关灯案例<script><divid="d1"class="c1bg_greenbg_red"></div><buttonid="d2">变色</button><script>letbtnEle=document.getElementById('d2')letdivEle=docum......