介绍:以往的前端下载基本流程都是**(服务端 -> 客户端 -> 再触发客户端下载)** ,这样当文件过大 时候,会导致浏览器崩溃 (原因是下载的文件缓存在浏览器)。要想解决这个问题,第一种方案: 则只能通过 <a src="" download></a> 标签去下载(因为a标签是直接存到本地的),但是a标签会有很多限制,比如请求头之类的一系列配置,不同源无法触发下载等。下面我介绍一下第二种方案: 借助StreamSaver.js 实现管道下载,且实时获取下载进度.
文件目录:
一共两个文件 (downloadProgress.js 和 streamsaver.js)
downloadProgress.js
javascript
import './streamsaver'
/**
* 使用 StreamSaver.js 进行流式下载,并实时监控进度
* @param {object} options - 配置对象
* @param {string} options.url - 要下载的文件URL
* @param {string} options.fileName - 保存时建议的文件名
* @param {string} options.method - 请求方法
* @param {object} options.headers - 请求头
* @param {object} options.data - 请求体
* @param {function} options.onProgress - 进度回调函数 (percent) => {}
* @param {function} options.onSuccess - 成功回调函数 () => {}
* @param {function} options.onError - 错误回调函数 (error) => {}
*/
// MIME 类型到文件后缀的映射表
const MIME_TYPE_MAP = {
'video/mp4': 'mp4',
'video/webm': 'webm',
'video/ogg': 'ogv',
'video/quicktime': 'mov',
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'application/pdf': 'pdf',
'application/zip': 'zip',
'text/plain': 'txt',
};
let extension = '';
const isValidFilename = (filename) => {
const pattern = /^[^\\/:*?"<>|]+\.[a-zA-Z0-9]+$/;
return pattern.test(filename);
}
const streamHttp = async ({ url, fileName = 'download-file', method = 'GET', headers = {}, data = {}, onProgress, onSuccess, onError }) => {
// 1. 检查浏览器支持
if (typeof streamSaver === 'undefined') {
const error = new Error('您没有引入StreamSaver.js \n\n<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.6/StreamSaver.min.js"></script>\n');
return onError(error);
}
if (!streamSaver.supported) {
const error = new Error('你的浏览器不支持流式下载,请升级浏览器。');
return onError(error);
}
try {
// 1. 启动 fetch 请求
const config = {
method,
headers,
body: JSON.stringify(data),
}
// 如果是get请求,则不需要body
if (method?.toLocaleUpperCase() === 'GET') {
delete config.body;
}
const response = await fetch(url, config);
const contentType = '';
if (contentType && MIME_TYPE_MAP[contentType]) {
// 如果有精确匹配,优先使用
extension = MIME_TYPE_MAP[contentType];
}
// 如果 contentType 不存在,并且 fileName不是一个正常带后缀的文件名,则给出下载不了的提示
if (!contentType && !isValidFilename(fileName)) {
return onError(new Error('无法确定文件类型,请提供正确的文件名。'));
}
// 如果用户传入的 fileName 没有后缀,则自动添加
const finalFileName = fileName.includes('.') ? fileName : `${fileName}.${extension}`;
// 2. 创建文件流,这会立即触发浏览器的下载对话框
const fileStream = streamSaver.createWriteStream(finalFileName);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 3. 获取文件总大小,用于计算进度
const contentLength = response.headers.get('Content-Length');
let loaded = 0;
// 4. 创建一个转换流,用于在数据流过时计算进度
const progressStream = new TransformStream({
transform(chunk, controller) {
loaded += chunk.length;
// 如果有总大小,计算并更新进度
if (contentLength) {
const percent = (loaded / contentLength) * 100;
onProgress({
percent,
});
} else {
// 如果没有 Content-Length,只显示已下载的字节数
onProgress({
percent: null, // 表示无法计算百分比
});
}
// 将数据块传递给下一个流
controller.enqueue(chunk);
}
});
// 5. 连接管道:fetch响应 -> 进度监控流 -> 文件写入流
// pipeTo 返回一个 Promise,它会在下载完成时 resolve
await response.body.pipeThrough(progressStream).pipeTo(fileStream);
// 6. 如果 pipeTo 成功,下载完成
onSuccess({ status: 200, message: '下载完成' });
} catch (error) {
onError(error);
}
};
export default streamHttp;
streamsaver.js
javascript
/**
* Minified by jsDelivr using Terser v5.37.0.
* Original file: /npm/streamsaver@2.0.6/StreamSaver.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
( (e, t) => {
"undefined" != typeof module ? module.exports = t() : "function" == typeof define && "object" == typeof define.amd ? define(t) : window.streamSaver = t()
}
)(0, ( () => {
"use strict";
const e = window;
e.HTMLElement || console.warn("streamsaver is meant to run on browsers main thread");
let t = null
, a = !1;
const r = e.WebStreamsPolyfill || {}
, n = e.isSecureContext;
let o = /constructor/i.test(e.HTMLElement) || !!e.safari || !!e.WebKitPoint;
const s = n || "MozAppearance"in document.documentElement.style ? "iframe" : "navigate"
, i = {
createWriteStream: function(r, m, c) {
let d = {
size: null,
pathname: null,
writableStrategy: void 0,
readableStrategy: void 0
}
, p = 0
, u = null
, f = null
, g = null;
Number.isFinite(m) ? ([c,m] = [m, c],
console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),
d.size = c,
d.writableStrategy = m) : m && m.highWaterMark ? (console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),
d.size = c,
d.writableStrategy = m) : d = m || {};
if (!o) {
t || (t = n ? l(i.mitm) : function(t) {
const a = "width=200,height=100"
, r = document.createDocumentFragment()
, n = {
frame: e.open(t, "popup", a),
loaded: !1,
isIframe: !1,
isPopup: !0,
remove() {
n.frame.close()
},
addEventListener(...e) {
r.addEventListener(...e)
},
dispatchEvent(...e) {
r.dispatchEvent(...e)
},
removeEventListener(...e) {
r.removeEventListener(...e)
},
postMessage(...e) {
n.frame.postMessage(...e)
}
}
, o = t => {
t.source === n.frame && (n.loaded = !0,
e.removeEventListener("message", o),
n.dispatchEvent(new Event("load")))
}
;
return e.addEventListener("message", o),
n
}(i.mitm)),
f = new MessageChannel,
r = encodeURIComponent(r.replace(/\//g, ":")).replace(/['()]/g, escape).replace(/\*/g, "%2A");
const o = {
transferringReadable: a,
pathname: d.pathname || Math.random().toString().slice(-6) + "/" + r,
headers: {
"Content-Type": "application/octet-stream; charset=utf-8",
"Content-Disposition": "attachment; filename*=UTF-8''" + r
}
};
d.size && (o.headers["Content-Length"] = d.size);
const m = [o, "*", [f.port2]];
if (a) {
const e = "iframe" === s ? void 0 : {
transform(e, t) {
if (!(e instanceof Uint8Array))
throw new TypeError("Can only write Uint8Arrays");
p += e.length,
t.enqueue(e),
u && (location.href = u,
u = null)
},
flush() {
u && (location.href = u)
}
};
g = new i.TransformStream(e,d.writableStrategy,d.readableStrategy);
const t = g.readable;
f.port1.postMessage({
readableStream: t
}, [t])
}
f.port1.onmessage = e => {
e.data.download ? "navigate" === s ? (t.remove(),
t = null,
p ? location.href = e.data.download : u = e.data.download) : (t.isPopup && (t.remove(),
t = null,
"iframe" === s && l(i.mitm)),
l(e.data.download)) : e.data.abort && (h = [],
f.port1.postMessage("abort"),
f.port1.onmessage = null,
f.port1.close(),
f.port2.close(),
f = null)
}
,
t.loaded ? t.postMessage(...m) : t.addEventListener("load", ( () => {
t.postMessage(...m)
}
), {
once: !0
})
}
let h = [];
return !o && g && g.writable || new i.WritableStream({
write(e) {
if (!(e instanceof Uint8Array))
throw new TypeError("Can only write Uint8Arrays");
o ? h.push(e) : (f.port1.postMessage(e),
p += e.length,
u && (location.href = u,
u = null))
},
close() {
if (o) {
const e = new Blob(h,{
type: "application/octet-stream; charset=utf-8"
})
, t = document.createElement("a");
t.href = URL.createObjectURL(e),
t.download = r,
t.click()
} else
f.port1.postMessage("end")
},
abort() {
h = [],
f.port1.postMessage("abort"),
f.port1.onmessage = null,
f.port1.close(),
f.port2.close(),
f = null
}
},d.writableStrategy)
},
WritableStream: e.WritableStream || r.WritableStream,
supported: !0,
version: {
full: "2.0.5",
major: 2,
minor: 0,
dot: 5
},
mitm: "https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0"
};
function l(e) {
if (!e)
throw new Error("meh");
const t = document.createElement("iframe");
return t.hidden = !0,
t.src = e,
t.loaded = !1,
t.name = "iframe",
t.isIframe = !0,
t.postMessage = (...e) => t.contentWindow.postMessage(...e),
t.addEventListener("load", ( () => {
t.loaded = !0
}
), {
once: !0
}),
document.body.appendChild(t),
t
}
try {
new Response(new ReadableStream),
n && !("serviceWorker"in navigator) && (o = !0)
} catch (e) {
o = !0
}
return (e => {
try {
e()
} catch (e) {}
}
)(( () => {
const {readable: e} = new TransformStream
, t = new MessageChannel;
t.port1.postMessage(e, [e]),
t.port1.close(),
t.port2.close(),
a = !0,
Object.defineProperty(i, "TransformStream", {
configurable: !1,
writable: !1,
value: TransformStream
})
}
)),
i
}
));
//# sourceMappingURL=/sm/a47e90c9e77f79e22405531b2deda4d299dc4ffb8262b6b5b3af3580ec770db0.map
使用方式:
javascript
<template>
<el-button @click="download">下载</el-button>
</template>
<script setup>
import http from './utils/downloadProgress'
const download = () => {
http({
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
method: 'get',
header: {},
data: {},
onProgress: (progress) => {
console.log(progress)
},
onSuccess: (res) => {
console.log(res)
},
onError: (err) => {
console.log(err)
}
})
}
</script>