轉(zhuǎn)載自 那些年,被blob虐過(guò)的程序猿覺(jué)醒了
前言
本文介紹前端如何進(jìn)行圖片處理,然后穿插介紹二進(jìn)制还惠、Blob厕诡、Blob URL蝗锥、Base64、Data URL此迅、ArrayBuffer铁孵、TypedArray锭硼、DataView 和圖片壓縮相關(guān)的知識(shí)點(diǎn)。
Blob
Blob
表示二進(jìn)制類(lèi)型的大對(duì)象蜕劝,通常是影像檀头、聲音或多媒體文件轰异,在 javaScript中Blob表示一個(gè)不可變、原始數(shù)據(jù)的類(lèi)文件對(duì)象鳖擒。
其構(gòu)造函數(shù)如下:
new Blob(blobParts, options);
-
lobParts
:數(shù)組類(lèi)型溉浙,可以存放任意多個(gè)ArrayBuffer, ArrayBufferView, Blob_或者_(dá)DOMString(會(huì)編碼為UTF-8)烫止,將它們連接起來(lái)構(gòu)成Blob對(duì)象的數(shù)據(jù)蒋荚。 -
options
:可選項(xiàng),用于設(shè)置blob對(duì)象的屬性馆蠕,可以指定如下兩個(gè)屬性: - type:存放到blob中數(shù)組內(nèi)容的MIME類(lèi)型(默認(rèn)為"")期升。
- endings:用于指定包含行結(jié)束符\n的字符串如何被寫(xiě)入。值為native表示行結(jié)束符會(huì)被更改為適合宿主操作系統(tǒng)文件系統(tǒng)的換行符(默認(rèn)值為transparent表示會(huì)保持blob中保存的結(jié)束符不變)
DOMString 是一個(gè)UTF-16字符串互躬。由于JavaScript已經(jīng)使用了這樣的字符串播赁,所以DOMString直接映射到 一個(gè)String。
ArrayBuffer(二進(jìn)制數(shù)據(jù)緩沖區(qū))吼渡、ArrayBufferView(二進(jìn)制數(shù)據(jù)緩沖區(qū)的array-like視圖)
屬性
Blob對(duì)象有兩個(gè)屬性容为,參見(jiàn)下表??:
屬性名 | 描述 |
---|---|
size | Blob對(duì)象中所包含數(shù)據(jù)的大小。字節(jié)為單位寺酪。只讀坎背。 |
type | 一個(gè)字符串,表明該Blob對(duì)象所包含數(shù)據(jù)的MIME類(lèi)型寄雀。如果類(lèi)型未知得滤,則該值為空字符串。只讀盒犹。 |
方法
-
slice(start:number, end:number, contentType:DOMString)
:
類(lèi)似于數(shù)組的slice方法懂更,將原始Blob對(duì)象按照指定范圍分割成新的blob對(duì)象并返回,可以用作切片上傳- start:開(kāi)始索引,默認(rèn)為0
- end:結(jié)束索引急膀,默認(rèn)為最后一個(gè)索引
- contentType:新Blob的MIME類(lèi)型沮协,默認(rèn)情況下為空字符串
-
stream()
:返回一個(gè)能讀取blob內(nèi)容的ReadableStream。 -
text()
:返回一個(gè)Promise對(duì)象且包含blob所有內(nèi)容的UTF-8格式的 USVString卓嫂。 -
arrayBuffer()
:返回一個(gè)Promise 對(duì)象且包含blob所有內(nèi)容的二進(jìn)制格式的ArrayBuffer慷暂。
將blob(或者file)二進(jìn)制文件保存到 formData
進(jìn)行網(wǎng)絡(luò)請(qǐng)求(之后可以獲取到圖片的imageUrl可以用作圖片展示或者后續(xù)的通過(guò)websocket
發(fā)送圖片地址)
示例如下??
- 創(chuàng)建一個(gè)包含domstring對(duì)象的blob對(duì)象
const blob = new Blob(['<div>john</div>'], { type: 'text/xml' });
console.log(blob); // Blob {size: 15, type: "text/xml"}
- 創(chuàng)建一個(gè)包含arraybuffer對(duì)象的blob對(duì)象
var abf = new ArrayBuffer(8);
const blob = new Blob([abf], { type: 'text/plain' });
console.log(blob); // Blob {size: 8, type: "text/plain"}
- 創(chuàng)建一個(gè)包含arraybufferview對(duì)象的blob對(duì)象
var abf = new ArrayBuffer(8);
var abv = new Int16Array(abf);
const blob = new Blob(abv, { type: 'text/plain' });
console.log(blob); // Blob {size: 4, type: "text/plain"}
File
File
對(duì)象是一種特殊的Blob對(duì)象,繼承了所有Blob的屬性和方法,當(dāng)然同樣也可以用作formData
二進(jìn)制文件上傳
屬性
File對(duì)象屬性命黔,參見(jiàn)下表??:
屬性名 | 描述 |
---|---|
lastModified | 引用文件最后修改日期 |
name | 文件名或文件路徑 |
size | 以字節(jié)為單位返回文件的大小 |
type | 文件的 MIME 類(lèi)型 |
方法
File 對(duì)象沒(méi)有自己的實(shí)例方法呜呐,由于繼承了 Blob 對(duì)象,因此可以使用 Blob 的實(shí)例方法 slice()悍募。
File的獲饶⒓:
下面我們分別使用input和拖放方式選擇多張圖片操作??:
- input獲取本地文件
<input type="file" multiple id="f" />
<script>
var elem = document.getElementById('f');
elem.onchange = function (event) {
var files = event.target.files;
console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
var file = files[0];
console.log(file); // {name: "1.jpg",lastModified: 1594369580771,size: 22344,type: "image/jpeg"...}
console.log(file instanceof File); //true
console.log(files instanceof FileList); // true
/* File繼承Blob */
console.log(file.__proto__.__proto__); // Blob {size: 22344, type: ""}
};
</script>
為input元素添加multiple屬性,允許用戶(hù)選擇多個(gè)文件坠宴,用戶(hù)選擇的每一個(gè)文件都是一個(gè)file對(duì)象洋魂,而FileList對(duì)象則是這些file對(duì)象的列表,代表用戶(hù)選擇的所有文件,是file對(duì)象的集合副砍。
- 拖放獲取
<div id="content" ondrop="drop(event)" ondragover="allowDrop(event);" />
<script>
function allowDrop(ev) {
ev.preventDefault();
}
function drop(ev) {
ev.preventDefault();
const files = ev.dataTransfer.files;
console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
console.log(files instanceof FileList); // true
}
</script>
<style type="text/css">
#content {
width: 500px;
height: 500px;
border: 1px solid brown;
}
</style>
數(shù)據(jù)緩沖區(qū)
從
XHR
衔肢、File API
、Canvas
等等各種地方豁翎,讀取了一大串字節(jié)流角骤,如果用JS里的Array去存,又浪費(fèi)心剥,又低效邦尊。在編程中,數(shù)據(jù)緩沖區(qū)
(或簡(jiǎn)稱(chēng)為緩沖區(qū))是物理內(nèi)存中中操作二進(jìn)制數(shù)據(jù)的存儲(chǔ)區(qū)(比硬盤(pán)驅(qū)動(dòng)器訪問(wèn)快)优烧,用于在數(shù)據(jù)從一個(gè)位置移動(dòng)到另一位置時(shí)存儲(chǔ)臨時(shí)數(shù)據(jù)蝉揍,解釋器
借助存儲(chǔ)二進(jìn)制數(shù)據(jù)的內(nèi)存緩沖區(qū)讀取行。主內(nèi)存中有一個(gè)正在運(yùn)行的文件,如果解釋器必須返回文件以讀取每個(gè)位畦娄,則執(zhí)行過(guò)程將耗費(fèi)大量時(shí)間又沾。為了防止這種情況,JavaScript使用數(shù)據(jù)緩沖區(qū)熙卡,該緩沖區(qū)將一些位存儲(chǔ)在一起杖刷,然后將所有位一起發(fā)送給解釋器。這樣再膳,JavaScript解釋器就不必?fù)?dān)心從文件數(shù)據(jù)中檢索文件挺勿。這種方法節(jié)省了執(zhí)行時(shí)間并加快了應(yīng)用程序的速度。各種緩沖區(qū)類(lèi)對(duì)數(shù)據(jù)執(zhí)行有效的二進(jìn)制操作喂柒,包括File
不瓶,Blob
,ArrayBuffer
和Array
灾杰。選擇的方法決定了內(nèi)存中緩沖區(qū)的內(nèi)部結(jié)構(gòu)蚊丐。
Buffer
Buffer
是Node.js
提供的對(duì)象,前端沒(méi)有艳吠。它一般應(yīng)用于IO操作
麦备,例如接收前端請(qǐng)求數(shù)據(jù)時(shí)候,可以通過(guò)Buffer相關(guān)的API創(chuàng)建一個(gè)專(zhuān)門(mén)存放二進(jìn)制數(shù)據(jù)的緩存區(qū)對(duì)接收到的前端數(shù)據(jù)進(jìn)行整合昭娩,一個(gè)Buffer類(lèi)似于一個(gè)整數(shù)數(shù)組凛篙,但它對(duì)應(yīng)于V8
堆內(nèi)存之外的一塊原始內(nèi)存。
ArrayBuffer栏渺、ArrayBufferView
ArrayBuffer
ArrayBuffer
表示固定長(zhǎng)度
的二進(jìn)制數(shù)據(jù)的原始緩沖區(qū)呛梆,它的作用是分配一段可以存放數(shù)據(jù)的連續(xù)內(nèi)存區(qū)域,因此對(duì)于高密度的訪問(wèn)(如音頻數(shù)據(jù))操作而言它比JS中的Array速度會(huì)快很多磕诊,ArrayBuffer存在的意義就是作為數(shù)據(jù)源提前寫(xiě)入在內(nèi)存中填物,因此其長(zhǎng)度固定
先大致看下ArrayBuffer的功能:
ArrayBuffer對(duì)象的構(gòu)造函數(shù)如下(length表示ArrayBuffer的長(zhǎng)度)??:
ArrayBuffer(length)
Array和ArrayBuffer的區(qū)別??:
Array | ArrayBuffer |
---|---|
可以放數(shù)字纹腌、字符串、布爾值以及對(duì)象和數(shù)組等 | 只能存放0和1組成的二進(jìn)制數(shù)據(jù) |
數(shù)據(jù)放在堆中 | 數(shù)據(jù)放在棧中滞磺,取數(shù)據(jù)時(shí)更快 |
可以自由增減 | 只讀升薯,初始化后固定大小,無(wú)論緩沖區(qū)是否為空击困,只能借助TypedArrays涎劈、Dataview寫(xiě)入 |
屬性
ArrayBuffer對(duì)象屬性,參見(jiàn)下表??:
屬性名 | 描述 |
---|---|
byteLength | 表示ArrayBuffer的大小 |
方法
-
slice
:有兩個(gè)參數(shù)??begin表示起始沛励,end表示結(jié)束點(diǎn)责语。方法返回一個(gè)新的 ArrayBuffer ,它的內(nèi)容是這個(gè)ArrayBuffer的字節(jié)副本目派,從begin(包括),到end(不包括)胁赢。
ArrayBuffer不能直接操作企蹭,而是要通過(guò) TypedArray
或 DataView
對(duì)象來(lái)操作,它們會(huì)將緩沖區(qū)中的數(shù)據(jù)轉(zhuǎn)換為各種數(shù)據(jù)類(lèi)型的數(shù)組智末,并通過(guò)這些格式來(lái)讀寫(xiě)緩沖區(qū)的內(nèi)容谅摄。
ArrayBufferView
由于ArrayBuffer對(duì)象不提供任何直接讀寫(xiě)內(nèi)存的方法,而
ArrayBufferView
對(duì)象實(shí)際上是建立在ArrayBuffer對(duì)象基礎(chǔ)上的視圖
系馆,它指定了原始二進(jìn)制數(shù)據(jù)
的基本處理單元送漠,通過(guò)ArrayBufferView對(duì)象來(lái)讀取ArrayBuffer對(duì)象的內(nèi)容。類(lèi)型化數(shù)組(TypedArrays)和DataView是ArrayBufferView的實(shí)例由蘑。
TypedArrays
類(lèi)型化數(shù)組(TypedArrays
)是JavaScript中新出現(xiàn)的一個(gè)概念闽寡,專(zhuān)為訪問(wèn)原始的二進(jìn)制數(shù)據(jù)而生。
本質(zhì)上尼酿,類(lèi)型化數(shù)組和ArrayBuffer是一樣的爷狈,只不過(guò)是他具備讀寫(xiě)功能。
類(lèi)型數(shù)組的類(lèi)型有:??:
名稱(chēng) | 大小 (以字節(jié)為單位) | 說(shuō)明 |
---|---|---|
Int8Array | 1 | 8位有符號(hào)整數(shù) |
Uint8Array | 1 | 8位無(wú)符號(hào)整數(shù) |
Int16Array | 2 | 16位有符號(hào)整數(shù) |
Uint16Array | 2 | 16位無(wú)符號(hào)整數(shù) |
Int32Array | 4 | 32位有符號(hào)整數(shù) |
Uint32Array | 4 | 32位無(wú)符號(hào)整數(shù) |
Float32Array | 4 | 32位浮點(diǎn)數(shù) |
Float64Array | 8 | 64位浮點(diǎn)數(shù) |
類(lèi)型轉(zhuǎn)換如圖??:
舉一些代碼例子展示如何轉(zhuǎn)換??:
// 創(chuàng)建一個(gè)指向b的視圖v1裳擎,采用Int32類(lèi)型涎永,開(kāi)始于默認(rèn)的字節(jié)索引0,直到緩沖區(qū)的末尾
var v1 = new Int32Array(b); // Int32Array(2) [0, 0]
v1[0] = 1
console.log(v1); // Int32Array(2) [1, 0]
// 創(chuàng)建一個(gè)指向b的視圖v2鹿响,采用Uint8類(lèi)型羡微,開(kāi)始于字節(jié)索引2,直到緩沖區(qū)的末尾
var v2 = new Uint8Array(b, 2); // Uint8Array(6) [0, 0, 0, 0, 0, 0]
// 創(chuàng)建一個(gè)指向b的視圖v3惶我,采用Int16類(lèi)型聊记,開(kāi)始于字節(jié)索引2,長(zhǎng)度為2
var v3 = new Int16Array(b, 2, 2); // Int16Array(2) [0, 0]`
因?yàn)槠胀↗avascript數(shù)組使用的是Hash查找方式誊抛,而類(lèi)型化數(shù)組直接訪問(wèn)固定內(nèi)存猴抹,因此贬堵,速度很贊,比傳統(tǒng)數(shù)組要快结洼!同時(shí)黎做,類(lèi)型化數(shù)組天生處理二進(jìn)制數(shù)據(jù),這對(duì)于XMLHttpRequest
松忍、canvas
蒸殿、webGL
等技術(shù)有著先天的優(yōu)勢(shì)。
TypedArray的應(yīng)用如何拼接兩個(gè)音頻文件?
fetch請(qǐng)求音頻資源 -> ArrayBuffer -> TypedArray -> 拼接成一個(gè) TypedArray -> ArrayBuffer -> Blob -> Object URL
DataView
DataView
對(duì)象可以在ArrayBuffer中的任意位置讀取和存儲(chǔ)不同類(lèi)型的二進(jìn)制數(shù)據(jù)鸣峭。
創(chuàng)建DataView的語(yǔ)法如下:
var dataView = new DataView(DataView(buffer, byteOffset[可選], byteLength[可選])
屬性
DataView對(duì)象有三個(gè)屬性宏所,參見(jiàn)下表??:
屬性名 | 描述 |
---|---|
buffer | 表示ArrayBuffer |
byteOffset | 指緩沖區(qū)開(kāi)始處的偏移量 |
byteLength | 指緩沖區(qū)部分的長(zhǎng)度 |
方法
-
setint8()
:從DataView起始位置以byte為計(jì)數(shù)的指定偏移量(byteOffset)處存儲(chǔ)一個(gè)8-bit數(shù)(一個(gè)字節(jié)) -
getint8()
:從DataView起始位置以byte為計(jì)數(shù)的指定偏移量(byteOffset)處獲取一個(gè)8-bit數(shù)(一個(gè)字節(jié))
除此之外還有g(shù)etInt16, getUint16, getInt32, getUint32... 使用方法一致,這里就不一一例舉
用法如下??:
let buffer = new ArrayBuffer(32);
let dataView = new DataView(buffer,0);
dataView.setInt16(1,56);
dataView.getInt16(1); // 56
FileReader
我們無(wú)法直接訪問(wèn)Blob或者文件對(duì)象的內(nèi)容摊溶,如果想要讀取它們并轉(zhuǎn)化為其他格式的數(shù)據(jù)爬骤,可以借助
FileReader
對(duì)象的API進(jìn)行操作
-
readAsText(Blob)
:將Blob轉(zhuǎn)化為文本字符串 -
readAsArrayBuffer(Blob)
:將Blob轉(zhuǎn)為ArrayBuffer格式數(shù)據(jù) -
readAsDataURL()
: 將Blob轉(zhuǎn)化為Base64格式的DataURL
使用分別如下??:
const blob = new Blob(['<xml>foo</xml>'], { type: 'text/xml' });
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result);
};
reader.readAsText(blob); // <xml>foo</xml>
reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4` </pre>
下面我們嘗試把一個(gè)文件的內(nèi)容通過(guò)字符串的方式讀取出來(lái):
<input type="file" id='f' />
<script> document.getElementById('f').addEventListener('change', function (e) {
var file = this.files[0];
// 首先,需要?jiǎng)?chuàng)建一個(gè)FileReader的實(shí)例莫换。
const reader = new FileReader();
reader.onload = function () {
// 在加載完成時(shí)回調(diào)
const content = reader.result;
console.log(content);
}
reader.readAsText(file); // 將blob轉(zhuǎn)化為文本字符串讀取
}, false); </script>
讀取結(jié)果如下??:
BlobURL
BlobURL
(ObjectURL)是一種偽協(xié)議
霞玄,只能由瀏覽器在內(nèi)部生成,我們知道script/img/video/iframe
等標(biāo)簽的src屬性和background的url可以通過(guò)url和base64來(lái)顯示拉岁,我們同樣可以把blob或者file轉(zhuǎn)換為url生成BlobURL來(lái)展示圖像坷剧,BlobURL允許Blob和File對(duì)象用作圖像,下載二進(jìn)制數(shù)據(jù)鏈接等的URL源喊暖。
圖像展示??:
<div id="content">
<input type="file" multiple id="f" />
</div>
<script>
const elem = document.getElementById('f');
const content = document.getElementById('content');
// 根據(jù)不同瀏覽器封裝一個(gè)轉(zhuǎn)換BlobUrl的方法:file可以是File對(duì)象也可以是Blob對(duì)象
const getObjectURL = (file) => {
let url;
if (window.createObjectURL) {
url = window.createObjectURL(file);
} else if (window.URL) {
url = window.URL.createObjectURL(file);
} else if (window.webkitURL) {
url = window.webkitURL.createObjectURL(file);
}
return url;
};
elem.onchange = function (event) {
const files = event.target.files;
const file = files[0];
const img = document.createElement('img');
img.src = getObjectURL(file);
content.appendChild(img);
};
</script>
我們查看demo頁(yè)面這個(gè)mm圖片元素惫企,會(huì)發(fā)現(xiàn)其URL地址既不是傳統(tǒng)HTTP,也不是Base64 URL陵叽,而是blob:開(kāi)頭的字符串,可以通過(guò)將其放在地址欄中進(jìn)行檢查狞尔。文件下載??:
<body>
<button onclick="download()">download.txt</button>
<script> const getObjectURL = (file) => {
let url;
if (window.createObjectURL) {
url = window.createObjectURL(file);
} else if (window.URL) {
url = window.URL.createObjectURL(file);
} else if (window.webkitURL) {
url = window.webkitURL.createObjectURL(file);
}
return url;
};
function download() {
const fileName = 'download.txt';
const myBlob = new Blob(['johnYu'], { type: 'text/plain' });
downloadFun(fileName, myBlob);
}
function downloadFun(fileName, blob) {
const link = document.createElement('a');
link.href = getObjectURL(blob);
link.download = fileName;
link.click();
link.remove();
URL.revokeObjectURL(link.href);
}</script>
</body>
點(diǎn)擊按鈕下載文檔,文檔內(nèi)容為:johnYu
這里不調(diào)用revokeObjectURL
時(shí)訪問(wèn)chrome://blob-internals/
可以看到當(dāng)前內(nèi)部的blob文件列表:
不再使用的BlobUrl后續(xù)會(huì)自動(dòng)清除(關(guān)閉瀏覽器也會(huì)自動(dòng)清除)咨跌,但是最好使用URL.revokeObjectURL(url)
手動(dòng)清除它們:
URL.revokeObjectURL('blob:http://127.0.0.1:5500/d2a9a812-0dbf-41c5-a96b-b6384d33f281')
執(zhí)行后再次訪問(wèn)chrome://blob-internals/
可以看到文件已經(jīng)被清除
dataURL
dataURL
允許內(nèi)容的創(chuàng)建者將較小的文件嵌入到文檔中沪么。與常規(guī)的URL使用場(chǎng)合類(lèi)似
其語(yǔ)法格式格式如下??:
data:[<mediatype>][;base64],data
-
data
:前綴 -
mediatype
表明數(shù)據(jù)類(lèi)型,是一個(gè)MIME類(lèi)型字符串,如image/jpeg表示一個(gè)JPEG圖片文件锌半。如果省略禽车,默認(rèn)值為text/plain;charset=US-ASCII。 -
base64
:標(biāo)志位(如果是文本刊殉,則可選) -
data
:數(shù)據(jù)本身
如何獲取DataUrl
- 上面示例中使用的方法readAsDataURL()就是將Blob轉(zhuǎn)化為Base64格式的DataUrl;
- 使用原生
Web API
編碼/解碼
Javascript中有兩個(gè)函數(shù)負(fù)責(zé)編碼和解碼base64字符串殉摔,分別是atob和btoa。
兩者都只針對(duì)Data URL中的data進(jìn)行處理记焊。
-
atob(): 負(fù)責(zé)解碼已經(jīng)使用base64編碼了的字符串逸月。
btoa('hello base64') // PHhtbD5mb288L3htbD4=
-
btoa(): 將二進(jìn)制字符串轉(zhuǎn)為base64編碼的
ASCII
字符串。atob('PHhtbD5mb288L3htbD4=') // <xml>foo</xml>
- Canvas的toDataURL方法:
Canvas提供了toDataURL方法遍膜,用于獲取canvas繪制內(nèi)容碗硬,將其轉(zhuǎn)為base64格式
<body>
<canvas id="canvas" width="200" height="50"></canvas>
<textarea id="content" style="width: 200px; height: 200px"></textarea>
<script> var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
// canvas的繪制
ctx.font = 'Bold 20px Arial';
ctx.textAlign = 'left';
ctx.fillStyle = 'purple';
ctx.fillText('johnYu', 10, 30);
// 獲取 Data URL
document.getElementById('content').value = canvas.toDataURL();
}</script>
</body>
如下圖所示瓤湘,文本框中的內(nèi)容即為canvas中繪制內(nèi)容的base64格式。如果我們將前面的返回結(jié)果data:text/xml;base64,PHhtbD5mb288L3htbD4=
放在瀏覽器的地址欄中恩尾,則可以看到顯示的內(nèi)容:
DataUrl的使用
- 由于可以將其用作URL的替代弛说,因此DataURL和BlobUrl一樣可以在script/img/video/iframe等標(biāo)簽的src屬性和background的url中使用,用法與BlobUrl基本一致翰意,只需要將前面的
elem.onchange
做如下改造
<body>
<div id="content">
<input type="file" multiple id="f" />
</div>
<script>
const elem = document.getElementById('f');
const content = document.getElementById('content');
elem.onchange = function (event) {
const files = event.target.files;
const file = files[0];
const img = document.createElement('img');
- img.src = getObjectURL(file);
+ const reader = new FileReader();
+ reader.onload = function () {
+ img.src = reader.result;
+ };
+ reader.readAsDataURL(file);
content.appendChild(img);
};
</script>
</body>
由于數(shù)據(jù)本身由URL表示木人,因此可以將其保存在Cookie中傳遞給服務(wù)器。
當(dāng)圖片的體積太小冀偶,占用一個(gè)HTTP會(huì)話(huà)不是很值得時(shí)醒第。
當(dāng)訪問(wèn)外部資源很麻煩或受限時(shí)。
DataUrl不會(huì)被瀏覽器緩存进鸠,但是小部分會(huì)通過(guò)css緩存稠曼,在下面例子中,DataUrl的使用是完全符合場(chǎng)景的堤如。它避免了讓這個(gè)小小的背景圖片獨(dú)自產(chǎn)生一次HTTP請(qǐng)求蒲列,而且,這個(gè)小圖片還能同CSS文件一起被瀏覽器緩存起來(lái)搀罢,重復(fù)使 用,不會(huì)每次使用時(shí)都加載一次侥猩。只要這個(gè)圖片不是很大榔至,而且不是在CSS文件里反復(fù)使用,就可以DataUrl方法呈現(xiàn)圖片降低頁(yè)面的加載時(shí)間欺劳,改善用戶(hù)的瀏覽體驗(yàn)唧取。
background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7")
- 作為下載連接使用。
點(diǎn)擊a標(biāo)簽后后下載文本內(nèi)容為johnYu的txt文件,在下面的BlobURL同樣可以實(shí)現(xiàn)??
<script>
const createDownload = (fileName, content) => {
const blob = new Blob([content]);
const reader = new FileReader();
const link = document.createElement('a');
link.innerHTML = fileName;
link.download = fileName;
reader.onload = () => {
link.href = reader.result;
document.getElementsByTagName('body')[0].appendChild(link);
};
reader.readAsDataURL(blob);
};
createDownload('download.txt', 'johnYu');
</script>
區(qū)別
BlobURL 基本用法與 DataUrl 相同划提,都可以通過(guò)將其放在地址欄中進(jìn)行檢查也可以用作普通URL使用枫弟。
但也存在以下差異:
BlobUrl始終是唯一字符串,即時(shí)你每次傳遞相同的Blob鹏往,每次也會(huì)生成不同的BlobUrl淡诗;DataUrl值跟隨blob變化;
就BlobUrl而言伊履,它并不代表數(shù)據(jù)本身韩容,數(shù)據(jù)存儲(chǔ)在瀏覽器中,BlobUrl只是訪問(wèn)它的key唐瀑。數(shù)據(jù)會(huì)一直有效群凶,直到關(guān)閉瀏覽器或者手動(dòng)清除。而DataUrl是直接編碼的數(shù)據(jù)本身哄辣。因此即使將BlobUrl傳遞給服務(wù)器等也無(wú)法訪問(wèn)數(shù)據(jù)请梢。關(guān)閉瀏覽器后仍然可以在地址欄訪問(wèn)后DataUrl赠尾,但是訪問(wèn)不到BlobUrl
BlobUrl的長(zhǎng)度一般比較短,但DataUrl因?yàn)橹苯哟鎯?chǔ)圖片base64編碼后的數(shù)據(jù)毅弧,往往很長(zhǎng)(Base64編碼的數(shù)據(jù)體積通常會(huì)比二進(jìn)制格式的圖片體積大1/3气嫁。),因此當(dāng)顯式大圖片時(shí)形真,使用BlobUrl能獲取更好的可能性杉编,速度和內(nèi)存比DataUrl更有效
BlobUrl可以方便的使用XMLHttpRequest獲取源數(shù)據(jù)(xhr.responseType = 'blob')。對(duì)于DataUrl咆霜,并不是所有瀏覽器都支持通過(guò)XMLHttpRequest獲取源數(shù)據(jù)的
<body>
<button onclick="download1()">XMLHttpRequest下載</button>
<button onclick="download2()">fetch下載</button>
<img id="img" />
<script> var eleAppend = document.getElementById('forAppend');
const url = 'https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/9ecb4e119c26e64b8b4ec5258f159b3b~300x300.image';
const pingan = document.querySelector('#pingan');
function download1() {
const xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (this.status == 200) {
renderImage(this.response);
}
};
xhr.send(null);
}
function download2() {
fetch(url)
.then((res) => {
return res.blob();
})
.then((myBlob) => {
renderImage(myBlob);
});
}
function renderImage(blob) {
window.URL = window.URL || window.webkitURL;
var img = document.getElementById('img');
img.onload = function (e) {
window.URL.revokeObjectURL(img.src); // 清除釋放
};
img.src = window.URL.createObjectURL(blob);
}</script>
</body>
- BlobUrl除了可以用作圖片資源的網(wǎng)絡(luò)地址邓馒,BlobUrl也可以用作其他資源的網(wǎng)絡(luò)地址,例如html文件蛾坯、json文件等光酣,為了保證瀏覽器能正確的解析BlobUrl返回的文件類(lèi)型,需要在創(chuàng)建Blob對(duì)象時(shí)指定相應(yīng)的type
const createDownload = (fileName, content) => {
const blob = new Blob([content], { type: 'text/html' });
const link = document.createElement('a');
link.innerHTML = fileName;
link.download = fileName;
link.href = getObjectURL(blob);
document.getElementsByTagName('body')[0].appendChild(link);
};
createDownload('download.html', '<button>foo</button>')
-
DataUrl不會(huì)被瀏覽器緩存脉课,這意味著每次訪問(wèn)這樣頁(yè)面時(shí)都被下載一次救军。這是一個(gè)使用效率方面的問(wèn)題——尤其當(dāng)這個(gè)圖片被整個(gè)網(wǎng)站大量使用的時(shí)候。但是小部分可以通過(guò)css緩存
canvas
Canvas
對(duì)象元素負(fù)責(zé)在頁(yè)面中設(shè)定一個(gè)區(qū)域倘零,然后就可以通過(guò) JavaScript 動(dòng)態(tài)地在這個(gè)區(qū)域中繪制圖形唱遭。
方法
-
toDataURL(type, encoderOptions))
:以指定格式返回 DataUrl,該方法接收兩個(gè)可選參數(shù)- type:表示圖片格式,默認(rèn)為 image/png
- encoderOptions:表示圖片的質(zhì)量呈驶,在指定圖片格式為 image/jpeg 或 image/webp 的情況下拷泽,可以從 0 到 1 的區(qū)間內(nèi)選擇圖片的質(zhì)量。如果超出取值范圍袖瞻,將會(huì)使用默認(rèn)值 0.92司致,其他參數(shù)會(huì)被忽略。
-
toBlob(callback, type, encoderOptions)
:創(chuàng)造Blob對(duì)象聋迎, 用于展示canvas的圖片脂矫,默認(rèn)圖片類(lèi)型是image/png,分辨率是96dpi- callback: 參數(shù)是blob對(duì)象的回調(diào)函數(shù)
-
getImageData(x,y,width,height)
:返回 ImageData 對(duì)象霉晕,該對(duì)象拷貝了畫(huà)布指定矩形的像素?cái)?shù)據(jù)庭再。- x: 開(kāi)始復(fù)制的左上角位置的 x 坐標(biāo)。
- y: 開(kāi)始復(fù)制的左上角位置的 y 坐標(biāo)娄昆。
- width: 將要復(fù)制的矩形區(qū)域的寬度佩微。
- height: 將要復(fù)制的矩形區(qū)域的高度。
-
putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight)
:將圖像數(shù)據(jù)(從指定的 ImageData 對(duì)象)放回畫(huà)布上萌焰。- imgData: 規(guī)定要放回畫(huà)布的 ImageData 對(duì)象哺眯。
- x: ImageData 對(duì)象左上角的 x 坐標(biāo),以像素計(jì)扒俯。
- y: ImageData 對(duì)象左上角的 y 坐標(biāo)奶卓,以像素計(jì)一疯。
- dirtyX: 可選。水平值(x)夺姑,以像素計(jì)墩邀,在畫(huà)布上放置圖像的位置。
- dirtyY: 可選盏浙。水平值(y)眉睹,以像素計(jì),在畫(huà)布上放置圖像的位置废膘。
- dirtyWidth: 可選竹海。在畫(huà)布上繪制圖像所使用的寬度。
- dirtyHeight: 可選丐黄。在畫(huà)布上繪制圖像所使用的高度斋配。
應(yīng)用場(chǎng)景
當(dāng)我們需要獲取到canvas的內(nèi)容,可以用到toDataURL
和toBlob
屬性(可用于簽名灌闺,圖片剪裁艰争,圖片壓縮等場(chǎng)景),putImageData
桂对、getImageData
可以用于圖片灰度或者復(fù)制時(shí)使用(見(jiàn)后面的使用場(chǎng)景章節(jié)??)
獲取內(nèi)容:
<body>
<div id="content">
<button onclick="drawnImg()">繪制圖像</button>
<button onclick="getImg()">獲取圖像</button>
<canvas style="border: 1px solid black" id="drawing" width="200" height="200">A drawing of something.</canvas>
<img src="./timg.jpg" alt="" />
</div>
<script> var drawing = document.getElementById('drawing');
var quality = 0.3;
const imgType = 'image/jpeg';
var drawnImg = function () {
if (drawing.getContext) {
var context = drawing.getContext('2d');
//取得圖像的數(shù)據(jù) URI
var image = document.images[0];
context.drawImage(image, 20, 20, 100, 100);
}
};
var getImg = async function () {
const content = getContent('base64');
console.log(content);
const content1 = await getContent('file');
console.log(content1);
};
var getContent = function (type) {
switch (type) {
case 'base64':
{
const imgURL = drawing.toDataURL(imgType, quality);
return imgURL;
}
break;
case 'file':
{
// 轉(zhuǎn)為文件格式
return new Promise((resolve) => {
drawing.toBlob(
(blob) => {
resolve(blob);
},
imgType,
quality
);
});
}
break;
}
};</script>
</body>
關(guān)系及轉(zhuǎn)換
`
字符串 → Uint8Array
var str = 'ab';
console.log(Uint8Array.from(str.split(''), (e) => e.charCodeAt(0))); // Uint8Array(2) [97, 98]
Uint8Array → 字符串
var u8 = Uint8Array.of(97, 98);
console.log(Array.from(u8, (e) => String.fromCharCode(e)).join('')); // ab
字符串 → DataUrl
var str = 'ab';
console.log('data:application/octet-stream;base64,' + btoa(str)); // data:application/octet-stream;base64,YWI=
DataUrl -> 字符串
var data = 'data:application/octet-stream;base64,YWI=';
console.log(atob(data.split(',')[1])); // ab
Uint8Array -> ArrayBuffer
var u8 = Uint8Array.of(1, 2);
console.log(u8.buffer); // ArrayBuffer(2) {}
ArrayBuffer -> Uint8Array
var buffer = new ArrayBuffer(2);
console.log(new Uint8Array(buffer)); // Uint8Array(2) [0, 0]
ArrayBuffer -> DataView
var buffer = new ArrayBuffer(2);
var dataView = new DataView(buffer, 0); // DataView(2) {}
DataView -> ArrayBuffer
console.log(dataView.buffer); // ArrayBuffer(2) {}
ArrayBuffer → Blob
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]); // Blob {size: 32, type: ""}
UintXXArray → Blob
var u8 = Uint8Array.of(97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33);
var blob = new Blob([u8])
##### 字符串 → Blob
var blob = new Blob(['Hello World!'], {type: 'text/plain'});
// Blob {size: 12, type: "text/plain"}`
以上都是用new Blob()轉(zhuǎn)blob
DataUrl -> blob
var data = 'data:application/octet-stream;base64,YWI=';
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
console.log(dataURLtoBlob(data)); // Blob {size: 2, type: "application/octet-stream"}
Blob →
需要用到 FileReader 的 Api 轉(zhuǎn)換 readAsText(Blob)甩卓、readAsArrayBuffer(Blob)、readAsDataURL() 但是需要異步執(zhí)行
var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
var reader = new FileReader();
reader.readAsText(blob, 'utf-8');
reader.onload = function (e) {
console.info(reader.result); // a Hello world!
};
reader.onerror = function (e) {
console.error(reader.error);
}
可以用promise做多次轉(zhuǎn)換
var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
function read(blob) {
var fr = new FileReader();
var pr = new Promise((resolve, reject) => {
fr.onload = (eve) => {
resolve(fr.result);
};
fr.onerror = (eve) => {
reject(fr.error);
};
});
return {
arrayBuffer() {
fr.readAsArrayBuffer(blob);
return pr;
},
binaryString() {
fr.readAsBinaryString(blob);
return pr;
},
dataURL() {
fr.readAsDataURL(blob);
return pr;
},
text() {
fr.readAsText(blob);
return pr;
},
};
}
var pstr1 = read(blob).binaryString();
var pstr2 = read(blob)
.arrayBuffer()
.then((e) => Array.from(new Uint8Array(e), (e) => String.fromCharCode(e)).join(''));
Promise.all([pstr1, pstr2]).then((e) => {
console.log(e[0]); // a Hello world!
console.log(e[0] === e[1]); // true
});
應(yīng)用場(chǎng)景
圖像灰度化
這里主要用到canvas
和imageData
的轉(zhuǎn)換
<body>
<button onclick="drawngray()">黑白圖片</button>
<img src="./syz.jpg" alt="" />
<canvas id="myCanvas">canvas</canvas>
<script> var drawngray = function () {
var myCanvas = document.getElementById('myCanvas');
if (myCanvas.getContext) {
var context = myCanvas.getContext('2d');
var image = document.images[0];
// 動(dòng)態(tài)設(shè)置canvas的大小
myCanvas.width = image.width;
myCanvas.height = image.height;
var imageData, data, i, len, average, red, green, blue, alpha;
//繪制原始圖像
context.drawImage(image, 0, 0);
//取得圖像數(shù)據(jù)
imageData = context.getImageData(0, 0, image.width, image.height);
data = imageData.data;
for (i = 0, len = data.length; i < len; i += 4) {
red = data[i];
green = data[i + 1];
blue = data[i + 2];
// alpha = data[i + 3];
//求得 rgb 平均值
average = Math.floor((red + green + blue) / 3);
//設(shè)置顏色值蕉斜,透明度不變
data[i] = average;
data[i + 1] = average;
data[i + 2] = average;
}
//回寫(xiě)圖像數(shù)據(jù)并顯示結(jié)果
imageData.data = data;
context.putImageData(imageData, 0, 0);
}
};</script>
</body>
除次之外 getImageData
和putImageData
還可以用作 cavas 圖片復(fù)制: https://www.w3school.com.cn/tiy/t.asp?f=html5_canvas_getimagedata
圖片壓縮
在前端要實(shí)現(xiàn)圖片壓縮猛频,我們可以利用 Canvas 對(duì)象提供的 toDataURL()
方法
compress.js
const MAX_WIDTH = 800; // 圖片最大寬度
function compress(base64, quality, mimeType) {
let canvas = document.createElement('canvas');
let img = document.createElement('img');
img.crossOrigin = 'anonymous';
return new Promise((resolve, reject) => {
img.src = base64;
img.onload = () => {
let targetWidth, targetHeight;
if (img.width > MAX_WIDTH) {
targetWidth = MAX_WIDTH;
targetHeight = (img.height * MAX_WIDTH) / img.width;
} else {
targetWidth = img.width;
targetHeight = img.height;
}
canvas.width = targetWidth;
canvas.height = targetHeight;
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除畫(huà)布
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 通過(guò)toDataURL壓縮后的base64
let imageData = canvas.toDataURL(mimeType, quality / 100);
resolve(imageData);
};
});
}
test.html
<body>
<input type="file" accept="image/*" onchange="loadFile(event)" />
<script src="./compress.js"></script>
<script> function dataUrlToBlob(base64) {
var arr = base64.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
function uploadFile(url, blob) {
let formData = new FormData();
let request = new XMLHttpRequest();
// 封裝到FormData中進(jìn)行文件的上傳
formData.append('image', blob);
request.open('POST', url, true);
request.send(formData);
}
const loadFile = function (event) {
const reader = new FileReader();
reader.onload = async function () {
let compressedDataURL = await compress(reader.result, 90, 'image/jpeg');
// 壓縮后將base64轉(zhuǎn)為Blob 對(duì)象減少傳輸數(shù)據(jù)量
let compressedImageBlob = dataUrlToBlob(compressedDataURL);
uploadFile('https://httpbin.org/post', compressedImageBlob);
};
// 獲取用戶(hù)選取的圖片文件,通過(guò)FileReader轉(zhuǎn)化成base64
reader.readAsDataURL(event.target.files[0]);
};</script>
</body>
分片上傳
<body>
<input type="file" name="file" onchange="selfile();" />
<script> const url = 'https://httpbin.org/post';
/**
* @param file 原始文件
* @param chunkSize 默認(rèn)每次上傳分片大小
*/
async function chunkedUpload(file, chunkSize = 1024 * 1024 * 5) {
// 將文件拆分成chunkSize大小的分塊,然后每次請(qǐng)求只需要上傳這一個(gè)部分的分塊即可
for (let start = 0; start < file.size; start += chunkSize) {
// File對(duì)象繼承自Blob對(duì)象蛛勉,因此可以使用slice方法對(duì)大文件進(jìn)行切
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append('data', chunk);
await fetch(url, { method: 'post', body: fd })
.then((res) => res.text())
.then((res) => console.log(res)); // 打印上傳結(jié)果
}
}
function selfile() {
let file = document.querySelector('[name=file]').files[0];
// 自定義分片大小
const LENGTH = 1024 * 1024 * 1;
chunkedUpload(file, LENGTH);
}</script>
</body>
服務(wù)器接收到這些切片后,再將他們拼接起來(lái)就可以了睦柴,下面是PHP拼接切片的示例代碼:
$filename = './upload/' . $_POST['filename'];//確定上傳的文件名
//第一次上傳時(shí)沒(méi)有文件诽凌,就創(chuàng)建文件,此后上傳只需要把數(shù)據(jù)追加到此文件中
if(!file_exists($filename)){
move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
echo $filename;
}
測(cè)試時(shí)記得修改 nginx 的 server 配置坦敌,否則大文件可能會(huì)提示 413 Request Entity Too Large
的錯(cuò)誤侣诵。
server {
// ...
client_max_body_size 50m;
}
文件下載
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Blob 文件下載示例</title>
</head>
<body>
<button id="downloadBtn">文件下載</button>
</body>
<script>
const download = (fileName, blob) => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
link.remove();
URL.revokeObjectURL(link.href);
};
const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", (event) => {
const fileName = "blob.txt";
const myBlob = new Blob(["一文徹底掌握 Blob Web API"], { type: "text/plain" });
download(fileName, myBlob);
});
</script>
</html>