图片、视频的打包下载功能实现及原理分析
引言
打包下载多媒体文件比如图片或音视频是前端项目中比较常见的需求。那么这个需求该如何实现?要用到什么npm包?都涉及到哪些知识点?如何做到把多个文件打成一个压缩包的?文件是如何下载下来的?欢迎阅读本文探索答案~
1基础知识
1.1 Blob
1.1.1 概览脑图
Blob ****对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
要从其他非blob对象和数据构造一个 Blob,请使用 Blob() 构造函数。要创建一个 blob 数据的子集 blob,请使用 slice() 方法。
1.1.2 使用示例
exportAlarmList() {
exportAlarm(this.searchForm).then((resp) => {
let blob = new Blob([resp], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
saveAsFile(blob, '报警列表')
})
},
其中saveAsFile的实现如下:
export function saveAsFile(blob, filename) {
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename)
} else {
const link = document.createElement('a')
const body = document.querySelector('body')
link.href = window.URL.createObjectURL(blob)
link.download = filename
// fix Firefox
link.style.display = 'none'
body.appendChild(link)
link.click()
body.removeChild(link)
window.URL.revokeObjectURL(link.href)
}
}
整体逻辑比较好理解,涉及到URL.createObjectURL,URL.revokeObjectURL稍后介绍。
1.1.3 Blob转ArrayBuffer
// blob数据转成ArrayBuffer
export function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = function (e) {
resolve(e.target.result)
}
reader.readAsArrayBuffer(blob)
})
}
export function blobToArrayBuffer2(blob) {
return blob.arrayBuffer()
}
聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer
1.2.ArrayBuffer,类型数组对象,DataView,Base64
1.2.1 ArrayBuffer简介
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。它是一个字节数组,通常在其他语言中称为“byte array”。你不能直接操作 ArrayBuffer 的内容,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength);
// expected output: 8
ArrayBuffer转Blob方法非常简单,使用Blob构造函数:
var buffer = new ArrayBuffer(16)
var blob = new Blob([buffer])
1.2.2 类型数组对象
一个类型化数组(TypedArray) 对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。事实上,没有名为 TypedArray 的全局属性,也没有一个名为 TypedArray 的构造函数。相反,有许多不同的全局属性,它们的值是特定元素类型的类型化数组构造函数。
Int8Array:8位有符号整数,长度1个字节。(-128~127)
Uint8Array:8位无符号整数,长度1个字节。(0~255)
Int16Array:16位有符号整数,长度2个字节。(-32768,32767)
Uint16Array:16位无符号整数,长度2个字节。(0~65535)
Int32Array:32位有符号整数,长度4个字节。(-2147483648~2147483647)
Uint32Array:32位无符号整数,长度4个字节。(0~4294967295)
Float32Array:32位浮点数,长度4个字节。
Float64Array:64位浮点数,长度8个字节。
// 下面代码是语法格式,不能直接运行,
// TypedArray 关键字需要替换为底部列出的构造函数。
new TypedArray(); // ES2017中新增
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);
// create a TypedArray with a size in bytes
const typedArray1 = new Int8Array(8);
typedArray1[0] = 32;
const typedArray2 = new Int8Array(typedArray1);
typedArray2[1] = 42;
console.log(typedArray1);
// expected output: Int8Array [32, 0, 0, 0, 0, 0, 0, 0]
console.log(typedArray2);
// expected output: Int8Array [32, 42, 0, 0, 0, 0, 0, 0]
1.2.3 DataView
DataView 视图是一个可以从 二进制ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
// create an ArrayBuffer with a size in bytes
const buffer = new ArrayBuffer(16);
// Create a couple of views
const view1 = new DataView(buffer);
const view2 = new DataView(buffer, 12, 4); //from byte 12 for the next 4 bytes
view1.setInt8(12, 42); // put 42 in slot 12
console.log(view2.getInt8(0));
// expected output: 42
类型化数组和DataView的关系: 类型化视图一般也被称为类型化数组,因为它们除了元素必须是某种特定的数据类型外,与常规的数组无异。而且它们都继承了DataView。
1.2.4 Base64
Base64 是一组相似的二进制到文本(binary-to-text)的编码规则,使得二进制数据在解释成 radix-64 的表现形式后能够用 ASCII 字符串的格式表示出来。Base64 这个词出自一种 MIME 数据传输编码。
Base64编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景。这样是为了保证数据的完整并且不用在传输过程中修改这些数据。
在 JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:
let encodedData = window.btoa("Hello, world"); // 编码
let decodedData = window.atob(encodedData); // 解码
1.3. URL
1.3.1 概览脑图
1.3.2 构造函数和属性
new URL():创建并返回一个URL对象,该URL对象引用使用绝对URL字符串,相对URL字符串和基本URL字符串指定的URL。
hash:包含'#'的USVString,后跟URL的片段标识符。
host: 一个USVString,其中包含域(即主机名),后跟(如果指定了端口)“:”和URL的端口。
hostname: 包含 URL 域名的 USVString。
origin :只读,返回一个包含协议名、域名和端口号的 USVString。
protocol :包含 URL 协议名的 USVString,末尾带 ':'。
pathname: 以 '/' 起头紧跟着 URL 文件路径的 DOMString。
search:一个USVString ,指示URL的参数字符串; 如果提供了任何参数,则此字符串包括所有参数,并以开头的“?”开头 字符。
searchParams : 只读,URLSearchParams对象,可用于访问search中找到的各个查询参数。
password:包含在域名前面指定的密码的 USVString 。
username: 包含在域名前面指定的用户名的 USVString。
const url = new URL('http://localhost:8000/api/vehicle_platform/management/information/getTypeList?category=9')
console.log(url)
const url = new URL('http://localhost:8000/#/realtime-position')
console.log(url)
const url = new URL('http://localhost:8000/api/vehicle_platform/management/information/getTypeList?category=9')
const searchParams = url.searchParams
console.log(searchParams.get('category'))
// 9
1.3.3 实例方法
const url = new URL('http://localhost:8000/#/realtime-position')
const json = url.toJSON()
console.log(json)
// http://localhost:8000/#/realtime-position
const url = new URL('http://localhost:8000/#/realtime-position')
const str = url.toString()
console.log(str)
// http://localhost:8000/#/realtime-position
1.3.4 静态方法
createObjectURL():返回一个DOMString ,包含一个唯一的blob链接(该链接协议为以blob:,后跟唯一标识浏览器中的对象的掩码)。
revokeObjectURL():销毁之前使用URL.createObjectURL()方法创建的URL实例。
URL.revokeObjectURL()方法会释放一个通过URL.createObjectURL()创建的对象URL. 当你要已经用过了这个对象URL,然后要让浏览器知道这个URL已经不再需要指向对应的文件的时候,就需要调用这个方法.
具体的意思就是说,一个对象URL,使用这个url是可以访问到指定的文件的,但是我可能只需要访问一次,一旦已经访问到了,这个对象URL就不再需要了,就被释放掉,被释放掉以后,这个对象URL就不再指向指定的文件了.
比如一张图片,我创建了一个对象URL,然后通过这个对象URL,我页面里加载了这张图.既然已经被加载,并且不需要再次加载这张图,那我就把这个对象URL释放,然后这个URL就不再指向这张图了.
示例代码:
const link = document.createElement('a')
const body = document.querySelector('body')
link.href = window.URL.createObjectURL(blob)
link.download = filename
// fix Firefox
link.style.display = 'none'
body.appendChild(link)
link.click()
body.removeChild(link)
window.URL.revokeObjectURL(link.href)
如上代码创建了a标签,并将其放到body末尾;为a标签指定的href属性时使用createObjectURL方法创建的url; 点击a标签执行下载功能;移出a标签并且使用revokeObjectURL方法销毁url实例。
2.图片和视频的本地打包下载
2.1 流程
2.2 依赖包
import FileSaver from 'file-saver'
import JSZip from 'jszip'
2.3 实现
async handleBatchDownload(selectList, name) {
const zip = new JSZip()
for (let i = 0; i < selectList.length; i++) {
const data = await getMediaBlob(selectList[i])
const buf = await blobToArrayBuffer(data)
const suffix = this.imgVideoList[i].type === 'video' ? 'mp4' : 'png'
zip.file(`文件${i}.${suffix}`, buf, { binary: true }) // 逐个添加文件
}
zip
.generateAsync({ type: 'blob' })
.then((content) => {
// 生成二进制流
FileSaver.saveAs(content, name + '等' + '.zip') // 利用file-saver保存文件
this.loading = false
})
.catch((err) => {
this.loading = false
this.$message.error('网络出现了一点小问题,请稍后重试')
})
}
其中getMediaBlob方法如下:
//获取图片或者視頻的Blob值
export function getMediaBlob(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('get', url, true)
xhr.responseType = 'blob'
xhr.onload = function () {
if (this.status == 200) {
console.log('response', this.response)
resolve(this.response)
} else {
reject('error')
}
}
xhr.send()
})
}
Blob转ArrayBuffer方法如下:
// blob数据转成ArrayBuffer
export function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = function (e) {
resolve(e.target.result)
}
reader.readAsArrayBuffer(blob)
})
}
export function blobToArrayBuffer2(blob) {
return blob.arrayBuffer()
}
3.FileSaver源码分析
FileSaver的github地址:
http://purl.eligrey.com/github/FileSaver.js
分析的文件路径为src\FileSaver.js。
3.1 获取各种环境下的全局变量
// The one and only way of getting global scope in all environments
// https://stackoverflow.com/q/3277182/1008999
var _global = typeof window === 'object' && window.window === window
? window : typeof self === 'object' && self.self === self
? self : typeof global === 'object' && global.global === global
? global
: this
如上代码为获取各种环境下的全局变量。
3.2 blob数据转换
function bom (blob, opts) {
if (typeof opts === 'undefined') opts = { autoBom: false }
else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object')
opts = { autoBom: !opts }
}
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (opts.autoBom && /^\s*(?:text/\S*|application/xml|\S*/\S*+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
}
return blob
}
如上代码直接返回参数blob或者创建Blob类型数据再返回。
3.3 通过网络请求的方式下载方法
function download (url, name, opts) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = function () {
saveAs(xhr.response, name, opts)
}
xhr.onerror = function () {
console.error('could not download file')
}
xhr.send()
}
通过创建ajax的get请求的方式下载文件,其核心在于对saveAs方法的调用。
3.4 是否允许跨域网络请求
function corsEnabled (url) {
var xhr = new XMLHttpRequest()
// use sync to avoid popup blocker
xhr.open('HEAD', url, false)
try {
xhr.send()
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299
}
使用Head请求测试是否可以发起跨域网络请求。
3.5 封装click方法
// `a.click()` doesn't work for all browsers (#465)
function click (node) {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}
createEvent用于创建新的Event对象,创建后必须初始化(例如initMouseEvent),dispatchEvent用于手动触发事件。
3.6 特殊浏览器检测
// Detect WebView inside a native macOS app by ruling out all browsers
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent)
如上代码用于检测是否为Safari浏览器。
3.7 定义saveAs方法
var saveAs = _global.saveAs || (
// probably in some web worker
(typeof window !== 'object' || window !== _global)
? function saveAs () { /* noop */ }
// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
: ('download' in HTMLAnchorElement.prototype && !isMacOSWebView)
? function saveAs (blob, name, opts) {
var URL = _global.URL || _global.webkitURL
var a = document.createElement('a')
name = name || blob.name || 'download'
a.download = name
a.rel = 'noopener' // tabnabbing
// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'
// 参数是string
if (typeof blob === 'string') {
// Support regular links
a.href = blob
// 域名不同
if (a.origin !== location.origin) {
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, a.target = '_blank')
} else {
click(a)
}
} else {
// Support blobs
// 参数是blob
a.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
setTimeout(function () { click(a) }, 0)
}
}
// Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator
? function saveAs (blob, name, opts) {
name = name || blob.name || 'download'
if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts)
} else {
var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name)
}
}
// Fallback to using FileReader and a popup
: function saveAs (blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank')
if (popup) {
popup.document.title =
popup.document.body.innerText = 'downloading...'
}
if (typeof blob === 'string') return download(blob, name, opts)
var force = blob.type === 'application/octet-stream'
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
var isChromeIOS = /CriOS/[\d]+/.test(navigator.userAgent)
if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
} else {
var URL = _global.URL || _global.webkitURL
var url = URL.createObjectURL(blob)
if (popup) popup.location = url
else location.href = url
popup = null // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
}
}
)
_global.saveAs = saveAs.saveAs = saveAs
if (typeof module !== 'undefined') {
module.exports = saveAs;
}
说明:
- HTMLAnchorElement 接口表示超链接元素,并提供一些特别的属性和方法,以用于操作这些元素的布局和显示。HTMLAnchorElement.download属性是一个DOMString ,表明链接的资源将被下载,而不是显示在浏览器中。该值表示下载文件的建议名称。如果该名称不是基础操作系统的有效文件名,浏览器将对其进行调整。
- Internet Explorer 10 的 msSaveBlob 和 msSaveOrOpenBlob 方法允许用户在客户端上保存文件,方法如同从 Internet 下载文件,这是此类文件保存到“下载”文件夹的原因。
用法:
1.msSaveBlob:只提供一个保存按钮
window.navigator.msSaveBlob(blobObject, 'msSaveBlob_testFile.txt');
2.msSaveOrOpenBlob:提供保存和打开按钮
window.navigator.msSaveOrOpenBlob(blobObject, 'msSaveBlobOrOpenBlob_testFile.txt');
- popup是指弹窗,open('', '_blank')可以打开新窗口
- Blob数据转url
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
3.8 总结
FileSaver保存文件做到了多浏览器多环境兼容,涉及到click方法的封装,download方法的封装,以及saveAs方法的定义。不同浏览器环境下api接口不尽相同,saveAs基于三目的条件运算符对环境判断然后定义不同的方法体。
总结
本文介绍vue构建的前端项目中实现打包下载图片(视频)的方法和原理。介绍具体实现方法之前先介绍了相关的基础知识,主要包括Blob、ArrayBuffer和URL。在介绍具体方法之后又对使用的FileSaver依赖的源码进行了简单的分析。
参考资料:
转载自:https://juejin.cn/post/7187274188741312570