HTMLStream API是一種處理HTML流式資料的方法。通過HTMLStream API,您可以在邊緣節點上處理HTML流式資料,例如即時更新的股票資料或即時聊天記錄。HTMLStream API可以將HTML流式資料分塊傳輸,提高資料轉送效率。
背景介紹
在邊緣程式ER(EdgeRoutine)的情境中前端情境佔了很大一部分,由於邊緣有不少特殊的請求資料,例如UA、地理資訊和IP等,因此有不少需要在邊緣即時更改HTML的需求。常規的更改方式是使用Regex製作adhoc的解析器更改,但使用該方法容易出錯且不利用串流,使用開源JavaScript的解析器,例如parse5、htmlparser2等,對記憶體的消耗大、效能損失高。為瞭解決該問題,ER推出了邊緣的HTML流式解析器,專門用於流式的修改HTML代碼和頁面。
該功能並非Web標準,屬於ER內建的擴充功能。
舉例說明
情境描述
假設您需要將HTML頁面中所有的
<a/>標籤修改後全部指向http://www.taobao.com,在ER中您可以使用以下代碼實現。程式碼範例
async function handleRequest(request) { // 1.假設exmaple頁面返回的是待修改的HTML頁面。 const response = await fetch("http://www.example.com"); // 2.您需要設定好HTML流式解析器。流式解析器允許您設定多個不同CSS Selector // 文法的捕獲方式,然後註冊回呼函數進行改寫。 const htmlStream = new HTMLStream( response.body, // 放入需要改寫的HTML流 [[ "a", // 元素選取器,表示選擇所有的`a`標籤。 { // 註冊回呼函數對象,名為element的回呼函數會在a標籤(在DOM中為ElementNode)被調用。 // 調用時您可以傳入object來更改e的資訊。 element: function(e) { // 更改屬性href e.setAttribute("href", "http://www.taobao.com"); } } ]]); // 3.返回修改後的請求到瀏覽器。HTMLStream是個ReadableStream,所以任何能使用 // ReadableStream的地方均可使用HTMLStream。 return new Response(htmlStream); } export default { async fetch(request) { return handleRequest(request); } };結果分析
以上代碼便是簡單的使用HTMLStream即時修改HTML頁面流的方法,具體分析如下:
通過Fetch您可以得到一個對請求回複的流的表示,實際ER可能沒有從網路層讀取body,該設計可以減少緩衝引起的大量GC問題。
HTMLStream也是一個流,他的作用是TransformStream,即接受一個流,然後通過您註冊的重寫handler即時更改HTML頁面。如果您要修改一個HTML頁面,需要得到對應HTML的未經處理資料流,再把這個流當作輸入,放入到新建立的HTML流中,例如上述程式碼範例中的樣本2。
HTMLStream的第一個參數是一個流,表示HTML未經處理資料。
HTMLStream的第二個參數是個數組,表示一組重寫器。重寫器實際是一個數組,它包含一個選取器,用於選擇需要改寫的HTML及選擇一個對象,對象中的部分property會被當作回呼函數調用。例如上述例子中
["a" , {....}]用於聲明一個重寫器,實際表示一個元素為2的數組。第一個字串“a”表示元素選取器,在上述例子中表示找出文檔中的所有a標籤。第二個對象表示回調對象,對使用元素選取器而言,該對象可以包含以下三個函數:element函數,簽名為function(e),當元素選取器選擇的element被解析時調用該函數。comments函數,簽名為function(e),當element中嵌套的注釋被解析時調用該函數。text函數,簽名為function(e),當element中的文字被解析時調用該函數。該函數支援調用多次,因為串流的原因,HTMLStream可能只是正在處理一個字串片段。
HTMLStream是一個流,您可以直接回複這個流,但HTMLStream內部不會進行資料緩衝,且HTMLStream和常見的parse5、htmlparser2不同,HTMLStream不會產生DOM樹,大大減少了處理時間和記憶體消耗,確保進行HTML解析的同時,保持高吞吐和並發。
重寫器
重寫器是用於註冊重寫指示的對象,實際為包含兩個元素的數組。對數組的具體說明如下:
數組的第一個元素必須為String或null。
String:表示一個元素選取器,該選取器始終用於定位一個元素或標籤。
null:表示整個重寫器是針對整個文檔的。
說明多數情況下您不需要用文檔層級的重寫器,文檔層級的重寫器無法定位元素。
數組的第二個元素是一個JS對象,表示您註冊的回呼函數對象。
當您使用元素選取器時,其註冊對象稱為“元素選取器回調”;當您使用文檔選取器時,其註冊對象稱為“文檔選取器回調”。
同一個HTMLStream可以設定多個不同的重寫器,但只能設定一個文檔選取器以及支援設定多個元素選取器。
元素選取器文法
元素選取器是CSS選取器的文法子集,文法是子集不代表語言和CSS選取器一模一樣。元素選取器的文法如下:
*:所有元素或標籤。div:名為div的標籤。您可以以此類推其他的標籤名稱,可以是HTML標籤或定製化標籤。E#id:名為E的標籤,標籤的屬性ID是值id。E.Class:名為E的標籤,標籤的屬性Class是值Class。E[attr]:名為E的標籤,標籤的屬性中含有名稱attr。元素屬性選擇:
E[attr="a"]:選擇標籤E,標籤的屬性含有名attr,且值為a,注意區分大小寫。E[attr^="a"]:選擇標籤E,標籤的屬性含有名attr,且值為a,不區分大小寫。E[attr$="a"]:選擇標籤E,標籤的屬性含有名attr,且值以“a”結尾。E[attr^="a"]:選擇標籤E,標籤的屬性含有名attr,且值以“a”開頭。E[attr*="a"]:選擇標籤E,標籤的屬性含有名attr,且值含有“a”。E[attr|="a"]:選擇標籤E,標籤的屬性含有名attr,且值以“a-”開頭的一串英文逗號(,)分隔的列表,例如en-ch, en-us。
序列選擇:
E F:選擇標籤F,且F存在父節點元素E中。E > F:選擇標籤F,且F的直接父節點元素為E。
E:not(S):選擇元素E,S是另外一個選取器,當選取器值為false時,才能選中元素E。
元素選取器回調
元素選取器回調包含以下三個回呼函數。
回呼函數 | 說明 | 回呼函數簽名 |
element | 該屬性必須為一個非非同步函數,該屬性會在元素被解析完畢時調用。 | 回呼函數的簽名為function(e),傳入對象為Element對象。更多資訊,請參見Element。 |
comments | 該屬性必須為一個非非同步函數,該屬性會在元素選取器選中的元素中存在注釋時被調用。 | 回呼函數的簽名為function(e),傳入對象為Comments對象。更多資訊,請參見Comments。 |
text | 該屬性必須為一個非非同步函數,該屬性會在元素選取器的元素text部分被解析時被調用。 | 回呼函數的簽名為function(e),傳入對象為TextChunk對象。更多資訊,請參見TextChunk。 說明 該函數會被調用多次,當HTMLStream每次都從HTML未經處理資料流中讀一部分出來text時,每次解析完畢就會調用這個函數。如果您想看到整個text,需要自己緩衝text後拼接起來。 |
元素選取器可以不包含以上三個函數,此時對應的元素會被直接輸出。如果您只想修改某一部分內容,只需要註冊對應的回呼函數。
文檔選取器
文檔選取器主要用於選擇文檔層級的內容。文檔選取器用null表示,同一個HTMLStream中只能設定一個文檔選取器。
文檔選取器回調
文檔選取器回呼函數和元素選取器回呼函數類似,文檔選取器回調包含以下四個回呼函數。
回呼函數 | 說明 | 回呼函數簽名 |
doctype | 該屬性必須為一個非非同步函數,該屬性會在文檔的doctype被解析後被調用。 | 回呼函數的簽名為function(e),傳入對象為Doctype對象。更多資訊,請參見Doctype。 |
comments | 該屬性必須為一個非非同步函數,該屬性會在文檔層級的comments被調用。 | 回呼函數的簽名為function(e),傳入對象為Comments對象。更多資訊,請參見Comments。 |
text | 該屬性必須為一個非非同步函數,該屬性會在文檔層級非元素的text節點遇到時被調用。 | 回呼函數的簽名為function(e),傳入對象為TextChunk對象。更多資訊,請參見TextChunk。 說明 該函數會被調用多次,當HTMLStream每次都從HTML未經處理資料流中讀一部分出來text時,每次解析完畢就會調用這個函數。如果您想看到整個text,需要自己緩衝text後拼接起來。 |
docend | 該屬性必須為一個非非同步函數,該屬性會在文檔被解析完畢時調用,主要為了追加內容到文檔末端,通常可以以注釋的形式記錄一些debug資訊到HTML末尾,用於後續排查和追蹤問題。 | 回呼函數簽名為function(e),傳入對象為Docend對象。更多資訊,請參見Docend。 |
異常錯誤處理
以上任何一個回呼函數中拋出的JS異常,都會在ER運行時被捕獲,同時HTMLStream將停止處理HTML流,並且將您的異常順勢傳播到外層。
如果HTMLStream的reader.read是在JS中被觸發,您的異常會被重新拋出。
如果HTMLStream的reader.read是在ER運行時被調用,例如返回給用戶端,那麼異常將被ER運行時吞掉,用戶端會看到一個被截斷的內容,原因是HTMLStream是一個流,可能一部分已經發送至用戶端。該操作與現在的TransformStream的流式讀寫處理異常方式類似。
回調參數
每個回呼函數都會收到一個對象,表示選擇的HTML標籤或相關部分,該對象稱為回調參數。本文為您介紹Element、TextChunk和Comments等回調參數。
所有的回調參數必須在回呼函數中調用才有效,在回呼函數以外的地方調用回調參數對象的屬性或方法會導致JS異常。您可以將需要的部分儲存到其他JS對象或資料結構中。
本文中出現的所有option都表示一個對象,對象中的屬性html可以被設定為true或false,用於表示該內容是否被解釋為HTML內容或純文字內容。如果將html設定為false,HTMLStream會對其做
html encoding/esacping。
Element
定義
該對象會在Element回呼函數被調用時提供給您,表示某個被選擇的HTML標籤。
屬性
tagName(string):標籤名字。attributes(iterator):返回一個迭代器,該迭代器返回所有的屬性[name, value]。removed(bool):唯讀,表示該元素是否被刪除。刪除一個元素使用remove,多數情況下需要根據這個標籤跳過該元素,因為元素已經被刪除。namespaceURI:唯讀,表示該元素的命名空間URI,例如svg或script元素。
方法
修改屬性
getAttribute(name):查詢某個元素的屬性名稱。
setAttribute(name, value):設定和修改某個元素的屬性名稱。
hasAttribute(name):查詢某個元素的屬性名稱是否存在。
removeAttribute(name):刪除某個元素的屬性名稱。
說明屬性名稱和值要求必須是string類型。
修改內容
before(data, option):將內容插入到該元素之前,即元素的標籤之前。
after(data, option):將內容插入到該元素之後,即元素的標籤之後。
prepend(data, option):將內容插入到該元素的內容之前,即元素的開啟標籤結束的後方,例如
<div>(prepend) |aaaa|(append)</div>。append(data, option):將內容插入到該元素的內容之後,即元素的結束標籤結束的前方,例如
<div>(prepend) |aaaa|(append)</div>。replace(data, option):替換整個元素,包括標籤和嵌套的標籤都會被替換。
setInnerContent(data, option):設定元素內容,標籤和屬性不變。
remove():刪除該元素,removed屬性為true。
removeAndKeepContent():刪除元素的標籤和屬性,保留內容。
TextChunk
定義
該對象會在text回呼函數被調用時提供給您,代表某個被選擇的HTML的Text部分。
屬性
removed(bool):唯讀,表示該元素是否被刪除。刪除一個元素使用remove,多數情況下需要根據這個標籤跳過該元素,因為元素已經被刪除。text(string):唯讀,表示該text的內容。該text有可能只是一部分text,如果此字串為空白,代表這是最後一個chunk,您可以趁機把所有的text拼接起來。lastInTextNode(bool):唯讀,表示是否為最後一個text。如果為true,text屬性返回Null 字元串。
方法
修改內容的方法如下:
before(data, option):將內容插入到該元素之前,即元素的標籤之前。
after(data, option):將內容插入到該元素之後,即元素的標籤之後。
replace(data, option):替換整個元素,包括標籤和嵌套的標籤都會被替換。
remove():刪除該元素,removed屬性為true。
Comments
定義
該對象會在comments回呼函數被調用時提供給您,代表某個被選擇的HTML的注釋部分。
屬性
removed(bool):唯讀,表示該元素是否被刪除。刪除一個元素使用remove,多數情況下需要根據這個標籤跳過該元素,因為元素已經被刪除。text(string):可讀可寫,表示注釋的內容或改變注釋內容。
方法
修改內容的方法如下:
before(data, option):將內容插入到該元素之前,即元素的標籤之前。
after(data, option):將內容插入到該元素之後,即元素的標籤之後。
replace(data, option):替換整個元素,包括標籤和嵌套的標籤都會被替換。
remove():刪除該元素,removed屬性為true。
Doctype
定義
該對象會在doctype回呼函數被調用時提供給您,代表某個被選擇的HTML的Doctype部分。
屬性
name(string):唯讀,表示doctype名字。publicId(string):唯讀,如果有表示public identifier,否則返回null。systemId(string):唯讀,如果有表示system identifier,否則返回null。
Docend
定義
該對象會在docend回呼函數被調用時提供給您,代表整個HTML的末尾部分。
方法
append(string, option) :用於向HTML流末尾加入若干內容。