lob對象介紹
一個Blob對象表示一個不可變的,原始數據的類似文件對象。Blob表示的數據不一定是一個JavaScript原生格式blob對象本質上是js中的一個對象,里面可以儲存大量的二進制編碼格式的數據。
創建blob對象
創建blob對象本質上和創建一個其他對象的方式是一樣的,都是使用Blob()的構造函數來進行創建。構造函數接受兩個參數:
第一個參數為一個數據序列,可以是任意格式的值。
第二個參數是一個包含兩個屬性的對象{type:MIME的類型,endings:決定第一個參數的數據格式,可以取值為"transparent"或者"native"(transparent的話不變,是默認值,native的話按操作系統轉換)。}
Blob()構造函數允許使用其他對象創建一個Blob對象,比如用字符串構建一個blob
既然是對象,那么blob也擁有自己的屬性以及方法
屬性
布爾值,指示Blob.close()是否在該對象上調用過。關閉的blob對象不可讀。
Blob對象中所包含數據的大小(字節)。
一個字符串,表明該Blob對象所包含數據的MIME類型。如果類型未知,則該值為空字符串。
方法
關閉Blob對象,以便能釋放底層資源。
返回一個新的Blob對象,包含了源Blob對象中指定范圍內的數據。其實就是對這個blob中的數據進行切割,我們在對文件進行分片上傳的時候需要使用到這個方法。
看到上面這些方法和屬性,使用過HTML5提供的File接口的應該都很熟悉,這些屬性和方法在File接口中也都有。其實File接口就是基于Blob,繼承blob功能并將其擴展為支持用戶系統上的文件,也就是說:
File接口中的Flie對象就是繼承與Blob對象。
blob對象的使用
上面說了很多關于Blob對象的一些概念性的東西,下面我們來看看實際用途。
分片上傳
首先說說分片上傳,我們在進行文件上傳的時候,因為服務器的限制,會限制每一次上傳到服務器的文件大小不會很大,這個時候我們就需要把一個需要上傳的文件進行切割,然后分別進行上傳到服務器。
假如需要做到這一步,我們需要解決兩個問題:
首先怎么切割的問題上面已經有過說明,因為File文件對象是繼承與Blob對象的,因此File文件對象也擁有slice這個方法,我們可以使用這個方法將任何一個File文件進行切割。
代碼如下:
通過上面的方法。我們就得到了一個切割之后的File對象組成的數組blobs;
接下來要做的時候就是講這些文件分別上傳到服務器。
在HTTP1.1以上的協議中,有Transfer-Encoding這個編碼協議,用以和服務器通信,來得知當前分片傳遞的文件進程。
這樣解決了這兩個問題,我們不僅可以對文件進行分片上傳,并且能夠得到文件上傳的進度。
粘貼圖片
blob還有一個應用場景,就是獲取剪切板上的數據來進行粘貼的操作。例如通過QQ截圖后,需要在網頁上進行粘貼操作。
粘貼圖片我們需要解決下面幾個問題
首先我們可以通過paste事件來監聽用戶的粘貼操作:
然后通過事件對象中的clipboardData對象來獲取圖片的文件數據。
clipboardData對象介紹
介紹一下clipboardData對象,它實際上是一個DataTransfer類型的對象,DataTransfer是拖動產生的一個對象,但實際上粘貼事件也是它。
clipboardData的屬性介紹
items介紹
items是一個DataTransferItemList對象,自然里面都是DataTransferItem類型的數據了。
屬性
items的DataTransferItem有兩個屬性kind和type
方法
在原型上還有一些其他方法,不過在處理剪切板操作的時候一般用不到了。
type介紹
一般types中常見的值有text/plain、text/html、Files。
有了上面這些方法,我們可以解決第二個問題即獲取到剪切板上的數據。
最后我們需要將獲取到的數據渲染到網頁上。
其實這個本質上就是一個類似于上傳圖片本地瀏覽的問題。我們可以直接通過HTML5的File接口將獲取到的文件上傳到服務器然后通過講服務器返回的url地址來對圖片進行渲染。也可以使用fileRender對象來進行圖片本地瀏覽。
fileRender對象簡介
從Blob中讀取內容的唯一方法是使用FileReader。
FileReader接口有4個方法,其中3個用來讀取文件,另一個用來中斷讀取。無論讀取成功或失敗,方法并不會返回讀取結果,這一結果存儲在result屬性中。
FileReader接口包含了一套完整的事件模型,用于捕獲讀取文件時的狀態。
通過上面的方法以及事件,我們可以發現,通過readAsDataURL方法及onload事件就可以拿到一個可本地瀏覽圖片的DataURL。
最終代碼如下:
這樣我們就可以監聽到用戶的粘貼操作,并且將用戶粘貼的圖片文件實時的渲染到網頁之中了。
總結
以上是我對Blob對象的一些學習分享,希望在實際應用上能對大家有所幫助。也希望大家多多支持小編。
之前在做 html 內容導出為 pdf、圖片時,先是用 html2canvas 生成截屏,再進一步轉換為 pdf 文件,感興趣的同學可以看下這篇一文搞定前端 html 內容轉圖片、pdf 和 word 等文件,截圖得到的圖片內容、質量都沒有什么問題。
不過最近有個同事反應,他導出的圖片有 bug,這我倒挺好奇的,因為這個導出功能已經用了很久,并沒有人反饋過有問題(除了那個 pdf 翻頁內容被截斷的問題,求助 jym :前端有好的解決方法嗎?),于是我要了他的文檔,果不其然,出現了下面紅框所示的問題。
檢查一下它的 DOM 結構,發現是下面這樣,猜測是就是這個原因導致的。
為了驗證自己的猜想,淺淺調試一下 html2canvas 的源碼,看下 html2canvas 是怎樣一個流程,它是如何將 html內轉成 canvas 的。
在 html2canvas 執行的地方打個斷點,開始調試代碼:
進入 html2canvas 內部,可以看到內部執行的是 renderElement 方法:
咱們直接進入到 renderElement 方法內部,看下它的執行流程:
這里主要判斷節點,快速跳過,繼續執行 。
將用戶傳入的 options 與默認的 options 合并
構建配置項,將傳入的 opts 與默認配置合并,同時初始化一個 context 上下文對象(緩存、日志等):
其中 cache 為緩存對象,主要是避免資源重復加載的問題。
原理如下:
如果遇到圖片鏈接為 blob,在加載完成后,會添加到緩存 _cache 中:
下次使用直接通過 this._cache[src] 從緩存中獲取,不用再發送請求:
同時,cache 中控制圖片的加載和處理,包括使用 proxy 代理和使用 cors 跨域資源共享這兩種情況資源的處理。
繼續往下執行
使用 DocumentCloner 方法克隆原始 DOM,避免修改原始 DOM。
使用 clonedReferenceElement 將原始 DOM 進行克隆,并調用 toIFrame 將克隆到的 DOM 繪制到 iframe 中進行渲染,此時在 DOM 樹中會出現 class 為 html2canvas-container 的 iframe 節點,通過 window.getComputedStyle 就可以拿到要克隆的目標節點上所有的樣式了。
前面幾步很簡單,主要是對傳入的 DOM 元素進行解析,獲取目標節點的樣式和內容。重點是 toCanvas 即將 DOM 渲染為 canvas 的過程,html2canvas 提供了兩種繪制 canvas 的方式:
咱們接著執行,當代碼執行到這里時判斷是否使用 foreignObject 的方式生成 canvas:
首先了解下 foreignObject 是什么?
弄懂 foreignObject 后,我們嘗試將 foreignObjectRendering 設置為 true,看看它是如何生成 canvas 的:
js復制代碼Html2canvas(warp, {
useCORS: true,
foreignObjectRendering: true,
})
在此處打個斷點:
進入 ForeignObjectRenderer 類中
這里通過 ForeignObjectRenderer 實例化一個 renderer 渲染器實例,在 ForeignObjectRenderer 構造方法中初始化 this.canvas 對象及其上下文 this.ctx
調用 render 生成 canvas,進入到 render 方法:
render 方法執行很簡單,首先通過 createForeignObjectSVG 將 DOM 內容包裝到<foreignObject>中生成 svg:
生成的 svg 如下所示:
接著通過。loadSerializedSVG 將上面的 SVG 序列化成 img 的 src(SVG 直接內聯),調用this.ctx.drawImage(img, ...); 將圖片繪制到 this.canvas 上,返回生成好的 canvas 即可。
接著點擊下一步,直到回到最開始的斷點處,將生成好的 canvas 掛在到 DOM 上,如下:
js
復制代碼document.body.appendChild(canvas)
這就解決了???收工!!!
NoNoNo,為什么使用純 canvas 繪制就有問題呢? 作為 bug 終結者,問題必須找出來,干就完了 。
而且使用 foreignObject 渲染還有其他問題,我們后面再說。
要想使用純 canvas 方式繪制,那么就需要將 DOM 樹轉換為 canvas 可以識別的數據類型,html2canvas 使用 parseTree 方法來實現轉換,我們來看下它的執行過程。
直接在調用 parseTree 方法處打斷點,進入到 parseTree 方法內:
parseTree 的作用是將克隆 DOM 轉換為 ElementContainer 樹。
首先將根節點轉換為 ElementContainer 對象,接著再調用 parseNodeTree 遍歷根節點下的每一個節點,轉換為 ElementContainer 對象。
ElementContainer 對象主要包含 DOM 元素的信息:
ts復制代碼type TextContainer = {
// 文本內容
text: string;
// 位置和大小信息
textBounds: TextBounds[];
}
export class ElementContainer {
// 樣式數據
readonly styles: CSSParsedDeclaration;
// 當前節點下的文本節點
readonly textNodes: TextContainer[] = [];
// 除文本節點外的子元素
readonly elements: ElementContainer[] = [];
// 位置大小信息(寬/高、橫/縱坐標)
bounds: Bounds;
// 標志位,用來決定如何渲染的標志
flags = 0;
...
}
ElementContainer 對象是一顆樹狀結構,層層遞歸,每個節點都包含以上字段,形成一顆 ElementContainer 樹,如下:
繼續下一步
通過 CanvasRenderer 創建一個渲染器 renderer,創建 this.canvas和this.ctx上下文對象與 ForeignObjectRenderer 類似
得到渲染器后,調用 render 方法將 parseTree 生成的 ElementContainer 樹渲染成 canvas,在這里就與 ForeignObjectRenderer 的 render 方法產生差別了。
概念不懂就看 MDN:層疊上下文
首先我們都知道 CSS 是流式布局,也就是在沒有浮動(float)和定位(position)的影響下,是不會發生重疊的,從上到下、由外到內按照 DOM 樹去布局。
而浮動和定位的元素會脫離文檔流,形成一個層疊上下文,所以如果想正常渲染,就需要得到它們的層疊信息。
可以想象一下:在我們的視線與網頁之間有一條看不見的 z 軸,層疊上下文就是一塊塊薄層,而這些薄層中有很多 DOM 元素,這些薄層根據層疊信息在這個 z 軸上排列,最終形成了我們看到的絢麗多彩的頁面。
畫個圖好像更形象些:
白色為正常元素,黃色為 float 元素,藍色為 position 元素
更多詳細資料請閱讀:深入理解 CSS 中的層疊上下文和層疊順序
canvas 在繪制節點時需要先計算出整個目標節點里子節點渲染時所展現的不同層級,因為 Canvas 繪圖需要根據樣式計算哪些元素應該繪制在上層,哪些在下層。元素在瀏覽器中渲染時,根據 W3C 的標準,所有的節點層級布局,需要遵循層疊上下文和層疊順序的標準。
調用 parseStackingContexts 方法將 parseTree 生成的 ElementContainer 樹轉為層疊上下文。
ElementContainer 樹中的每一個 ElementContainer 節點都會產生一個 ElementPaint 對象,最終生成層疊上下文的 StackingContext 如下:
數據結構如下:
ts復制代碼// ElementPaint 數據結構如下
ElementPaint: {
// 當前元素的container
container: ElementContainer
// 當前元素的border信息
curves: BoundCurves
}
// StackingContext 數據結構如下
{
element: ElementPaint;
// z-index為負的元素行測會給你的層疊上下文
negativeZIndex: StackingContext[];
// z-index為零或auto、transform或者opacity元素形成的層疊上下文
zeroOrAutoZIndexOrTransformedOrOpacity: StackingContext[];
// 定位或z-index大于等于1的元素形成的層疊上下文
positiveZIndex: StackingContext[];
// 非定位的浮動元素形成的層疊上下文
nonPositionedFloats: StackingContext[];
// 內聯的非定位元素形成的層疊上下文
nonPositionedInlineLevel: StackingContext[];
// 內聯元素
inlineLevel: ElementPaint[];
// 非內聯元素
nonInlineLevel: ElementPaint[];
}
渲染層疊內容時會根據 StackingContext 來決定渲染的順序。
繼續下一步,調用 renderStack 方法,renderStack 執行 renderStackContent 方法,咱們直接進入 renderStackContent 內:
canvas 繪制時遵循 w3c 規定的渲染規則 painting-order,renderStackContent 方法就是對此規則的一個代碼實現,步驟如下:
此處的步驟 1-7 對應上圖代碼中的 1-7:
可以看到遍歷時會對形成層疊上下文的子元素遞歸調用 renderStack,最終達到對整個層疊上下文樹進行遞歸的目的:
而對于未形成層疊上下文的子元素,就直接調用 renderNode 或 renderNodeContent 這兩個方法,兩者對比,renderNode 多了一層渲染節點的背景色和邊框的方法(renderNode 函數內部調用 renderNodeBackgroundAndBorders 和 renderNodeContent 方法)。
renderNodeContent 用于渲染一個元素節點里面的內容,分為八種類型:純文本、圖片、canvas、svg、iframe、checkbox 和 radio、input、li 和 ol。
除了 iframe 的繪制比較特殊:重新生成渲染器實例,調用 render 方法重新繪制,其他的繪制都是調用 canvas 的一些 API 來實現,比如繪制文字主要用 fillText 方法、繪制圖片、canvas、svg 都是調用 drawImage 方法進行繪制。
所有可能用到的 API
最終繪制到 this.canvas 上返回,至此,html2canvas 的調試就結束了。
ok,當調試了一遍 html2canvas 的流程之后,再回到我們的問題上,很顯然就是 canvas 渲染的時候的問題,也就是 renderNodeContent 方法,那我們直接在這里打個斷點進行調試(為了方便我只輸入一行文字進行調試),只有當是文本節點時會進入到此斷點,等到 mark 標簽中對應的元素進入斷點時,查看:
可以看到此時 width 和 height 已經是父節點的寬高,果真如此 。
既然已經知道了問題所在,那么我們開始解決問題,有以下兩種解決方案可供參考:
在 html2canvas 配置中設置 foreignObjectRendering 為 true,此問題就可以解決嗎?
然而現實并沒有這么簡單,這樣又會引出新的問題:導出的圖片內容丟失
這是為什么呢?
通過 W3C 對SVG 的介紹可知:SVG 不允許連接外部的資源,比如 HTML 中圖片鏈接、CSS link 方式的資源鏈接等,在 SVG 中都會有限制。
解決方法:需要將圖片資源轉為 base64,然后再去生成截圖,foreighnObject 這種方法更適合截取內容為文字內容居多的場景。
在對內聯元素進行截斷前,如何確定 p 標簽中的 mark 標簽有沒有換行? 因為我們沒必要對所有內聯標簽做處理。
如果 mark 標簽的高度超過 p 標簽的一半時,就說明已經換行了,然后將 <mark>要求一</mark> 替換為 <mark>要</mark><mark>求</mark><mark>一</mark> 即可,代碼如下:
ts復制代碼const handleMarkTag = (ele: HTMLElement) => {
const markElements = ele.querySelectorAll('mark')
for (let sel of markElements) {
const { height } = sel.getBoundingClientRect()
let parentElement = sel.parentElement
while (parentElement?.tagName !== 'P') {
parentElement = parentElement?.parentElement!
}
const { height: parentHeight } = (
parentElement as unknown as HTMLElement
).getBoundingClientRect()
// mark的高度沒有超過p標簽的一半時 則沒有換行
if (height < parentHeight / 2) continue
// 超過一半時說明換行了
const innerText = sel.innerText
const outHtml = sel.outerHTML
let newHtml = ''
innerText.split('')?.forEach((text) => {
newHtml += outHtml.replace(innerText, text)
})
sel.outerHTML = newHtml
}
}
ok,再次嘗試一下,完美解決,這下可以收工了。
通過對一個不是 bug 的 bug 的分析,嘗試調試了一遍 html2canvas 的代碼,弄懂了瀏覽器截圖的原理及 html2canvas 的核心流程,并從中學到了幾點新知識:
發現 canvas 真是一個有趣的東西,什么都能畫,像我現在用于畫圖的工具excalidraw、圖表庫g6、g2、echarts都是用的 canvas 搞的,看來得抽時間學習一下 canvas,不要等到“書到用時方恨少“。
以上就是本文的全部內容,希望這篇文章對你有所幫助,歡迎點贊和收藏 ,如果發現有什么錯誤或者更好的解決方案及建議,歡迎隨時聯系。
作者:翔子丶 鏈接:https://juejin.cn/post/7277045020423798840 來源:稀土掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
、Blob(Binary Large Object)定義:二進制類型的大對象數據,在 JavaScript 中 Blob 對象表示不可變的原始數據。
2、語法:
var aBlob = new Blob(blobParts, options);
其中:blobParts是一個由 ArrayBuffer、Blob、DOMString 等對象構成的數組;options是一個可選項,由type和endings組成,type代表了被放入到 blob 中的內容的 MIME 類型,endings用于指定包含行結束符 \n 的字符串如何被表示(native表示行結束符\n被更改為適合宿主操作系統的換行符,transparent會保持 blob 中保存的行結束符不變)。
定義Blob
3、Blob屬性和方法:兩個只讀屬性size和type,其中size屬性用于表示數據的大小(以字節為單位),type 屬性為MIME 類型的字符串。slice([start[, end[, contentType]]])返回一個源指定范圍內的Blob 對象;stream()返回一個讀取 blob 內容的ReadableStream;text()返回一個 Promise 對象且包含 blob 所有內容的 UTF-8 格式的 USVString;arrayBuffer()返回一個 Promise 對象且包含 blob 所有內容的二進制格式的 ArrayBuffer。
Blob屬性和方法
4、Blob URL/Object URL 是一種偽協議,允許 Blob作為鏈接的URL源,如a.href、img.src等。
創建 Blob URL:url=URL.createObjectURL(Blob),覽器器為 URL.createObjectURL 生成的 URL 存儲了一個 URL → Blob 映射,此類 URL 較短,例如
blob:http://domain/b3ad7623-60bb-4eff-9b9d-f925438b97c7
Blob 本身仍駐留在內存中,在不需要時,可以調用URL.revokeObjectURL(url)來刪除引用。
5、base64也可以作為<img src= />的源,格式為
data:[<mediatype>][;base64],<data>
其中mediatype 是個MIME 類型的字符串,如 image/png,默認值為 text/plain;charset=US-ASCII,例如:
<img src="data:image/png;base64,R0lGODlheABaAPf/AAC...">
*請認真填寫需求信息,我們會在24小時內與您取得聯系。