在 kbone 中實現小程序 svg 渲染
In 未分類 on 2019年11月27日 by view: 4,770
0

背景

2019 年底,微信小程序已經推出了近三個年頭,我身邊的前端開發者基本都做過至少一次小程序了。很多友商曾打算推動小程序進入 W3C 標準,而微信并不為所動,個人認為,小程序本身在框架設計上稱不上「標準」,微信也并沒打算做一個「標準的平臺」。

小程序更注重產品形態和交互,注重對開發者能力的制約,盡可能減少對用戶的干擾;因此,也許小程序從設計之初就沒有過多考慮開發層面的「優雅」,而是以方便上手、容易學習為主。最典型的例子就是 App()、Page() 這一類直接注入到模塊內的工廠方法,你不知道、也不需要知道它從何處來,來無影去無蹤,是與現在 JS 生態中早已普及的模塊化開發有點相悖的。

在架構上,小程序選擇了將邏輯層與視圖層分離的方式來組織業務代碼。小程序的源碼提交上傳時,JS 會被打包成邏輯層代碼(app-service.js),在運行時與邏輯層基礎庫 WAService.js 相結合,在邏輯層 Webview(或 JSCore)中執行;WXML/WXSS 將會編譯成 JS 并拼接成 page-frame.html,在運行時與視圖層基礎庫 WAWebview.js 相結合,在視圖層堆棧的 Webview 中執行?;A庫負責利用客戶端提供的通信管道,相互建立聯系,對小程序和頁面的生命周期、頁面上虛擬 DOM 的渲染等進行管理,并在必要時使用客戶端提供的原生能力。

熟悉小程序的開發者都知道,這樣的架構最主要的目的就是禁止業務代碼操作 DOM,迫使開發者使用數據驅動的開發方式,同時在小程序推出初期可以避免良莠不齊的 HTML 項目快速攻占小程序平臺,后期則可以緩解小程序平臺上的優質產品流失。

kbone 是什么

從 2017 年初小程序推出開始,業界最關心的就是小程序能否轉為普通的 Web 開發。最初我們只能簡單的用 Babel 進行 JS 的轉換;后來小程序推出了 web-view 組件,開發者則開始想辦法讓 Web 頁面使用小程序能力;在知道了 web-view 中的消息不能實時傳到小程序邏輯層后,大家則開始選擇妥協,改用語法樹轉換的方式來實現。很多小程序開發框架都是在這一個階段產生的,如 Wepy、Labrador、mpvue 和 Taro。

語法樹轉換終究是不可靠的——在 Wepy 和 Taro 的使用中,我們常常會碰到很多語法無法識別的坑,坑的數量與代碼量成正比。因此,這些框架更適用于從零開始寫,而不適合將一個大型項目移植到小程序。

kbone 是微信團隊開源的微信小程序同構框架,與基于語法樹轉換的 Wepy、Taro 等傳統框架不同,kbone 的思路是在邏輯層用類似 SSR 的方式模擬出 DOM 和 BOM 結構,讓邏輯層的 HTML5 代碼正常運行;而 kbone 會負責將邏輯層中的虛擬 DOM 以 setData 的形式傳遞給視圖層,讓視圖層利用小程序組件遞歸渲染的能力,產生出真實的 DOM 結構。

使用 kbone 之后,我們可以將小程序頁面理解為一個獨立的 html 文檔(而不是 SPA 中的一個 router page)。在每個頁面的 JS 中初始化 kbone,為邏輯層提供虛擬 DOM 和 BOM 的環境,然后就可以像 H5 一樣加載各種主流前端框架和業務代碼,kbone 會負責邏輯層和視圖層之間的 DOM 和事件同步。

讓 kbone 支持 HTML5 inline SVG

在 HTML 中,SVG 的引入有很多種不同的方式,可以像圖片一樣使用 <img> 標簽、background-image 屬性,也可以直接在 HTML 中插入 <svg> 標簽,另外還有 <object>、<embed> 等不太常見的方式。

在一些大型 web-view 項目遷移到 kbone 的過程中,常常會遇到 HTML inline SVG(在 HTML 中直接插入 SVG 標簽)這種情況;有的頁面還會異步加載一個含有很多小圖標(<symbol>)的大 SVG、在頁面上用 <use xlink:href="#symbol-id"> 的方式,實現 SVG 的 Sprite 化。

本文針對單個頁面上出現大量 HTML inline SVG 的實戰場景,通過識別并轉換成 background-image,來實現小程序 kbone 對 SVG 的支持。

構造用例

首先我們以 kbone 官方示例 為基礎,導入該項目后,在項目根目錄新建 kbone-svg.js,然后進入 /pages/index/index.js,在 onLoad() 的結尾先寫出調用方式和示例:

本例中,結合 <defs> <symbol><use>文檔,給出了三種示例,分別用來代表普通 SVG 的渲染、跨 SVG 引用 Symbol(類似于雪碧圖)的渲染、以及 SVG 內引用當前文檔中的 Symbol 的渲染情況。

分析和實現

上述示例中,我們模擬 H5 條件下最一般的情況,直接在 body 下添加 HTML。如何支持這樣的情況?首先我們打開 kbone 的代碼 /miniprogram_npm/miniprogram-render/node/element.js,觀察 innerHTML 的 setter:

可以看到,innerHTML 被轉化成 $_generateDomTree 的調用,生成新的子節點,并替換掉所有舊的子節點。而在 $_generateDomTree 中,最終將會調用 this.ownerDocument.$$createElement。

根據 /miniprogram_npm/miniprogram-render/document.js 中的定義,Document.prototype.$$createElement 作為我們熟知的 Document.prototype.createElement 的內部實現,因此為了監聽 <svg> 等節點的創建,需要對 $$createElement 方法進行 Hook。

在 kbone 官方文檔 DOM/BOM 擴展 API 一章中不難發現,我們可以使用 window.$$addAspect 函數對所需的方法進行 Hook:

在這里,我們監聽了 <svg> 節點的建立,并在下一個宏任務中(即等待 <svg> 節點的所有子節點掛載完成后)調用我們自己的 renderSvg() 方法。在 renderSvg() 中,我們希望進行下列一些操作:

  1. 首先分析并保存當前 SVG 文檔中的所有 Symbol,以便于當前 SVG 文檔內部或者其它 SVG 中使用;
  2. 將當前 SVG 文檔中的跨文檔 <use> 節點替換成對應 Symbol 的 HTML,如果對應的 Symbol 還沒有加載,則監聽其加載完成;
  3. 清理當前 SVG 文檔,并轉換為 data:image/svg+xml 格式的 Data URI;
  4. 將當前 SVG 標記為已渲染,清除所有子節點,并將生成的 Data URI 設置為 CSS background-image 屬性。

在并不知道 Symbol 是否可以再包含 <use> 的情況下,為了簡化問題,我們可以先假設所有的 Symbol 中不會包含 <use>,即不存在 Symbol 之間多級依賴和循環依賴的情況。經過反復修改,renderSvg() 方法實現如下:

接下來我們需要實現 resolveSymbol 方法。當遇到 Symbol 時,需要解析其 ID,保存該 Symbol 節點,并觸發所有依賴當前 Symbol 的其他 SVG 的重新渲染。

最后,我們需要定義 SVG 進行清理和渲染(轉化為 Data URI)的過程。在此之前,需要對 setAttribute 和 setAttributeNS 進行一個 polyfill,因為 kbone 不支持為節點設置任意屬性,很多屬性設置之后會丟失。

接下來即可定義 SVG 文檔轉化為 Data URI 的過程了,這里需要用到很多正則表達式。

以上是經過反復 debug 后的相對穩定的代碼。放在上文的演示項目中,效果如下圖:

image

可以看出,前兩例中已經可以渲染出圖片,第三例中,與 MDN 官方文檔的表現 不太一致,經過檢查,生成的 Data URI 直接打開并沒有問題,可能是小程序視圖層的環境對 SVG 內的尺寸換算存在問題。

在 Android 和 iOS 真機調試中,本例沒有出現無法顯示的兼容問題,這也說明了這種方案可行。

問題與總結

kbone 解決了 JS 難題,卻留下了 CSS 難題

在上述例子中可以看到,kbone 已經非常類似于 H5 的環境,但有一個很容易忽略的問題:由于實際的操作對象是 <body> 的虛擬 DOM,且小程序視圖層并不支持 <style> ,我們已經無法通過 JS 給整個頁面(而非特定元素)注入 CSS,因此也無法通過純 JS 層面的 polyfill 來為 svg 等某一類元素定義一些優先級較低的默認樣式。

例如,在解析 SVG 的過程中,我們可能希望通過獲取 SVG 元素的尺寸來設置渲染后背景圖的默認尺寸(像 <img> 那樣),同時允許來自業務代碼中的尺寸覆蓋,這在 kbone 環境下,甚至也許在小程序架構中是不可能的——除非我們利用 Webpack 的黑魔法將自己的 polyfill 編譯到 WXSS 中去,或者如果你有超人的膽量和氣魄,也可以給你遷移過來的業務代碼中要覆蓋你的樣式批量加上 !important。

同理,可以肯定的是,我們也無法在 JS 中控制諸如媒體查詢、字體定義、動畫定義、以及 ::before、::after 偽元素的展示行為等,這些都是只能通過靜態 WXSS 編譯到小程序包內,而無法通過小程序 JS 動態加載的。

數據量消耗

另外,雖然在 HTML5 環境中十分推崇 SVG 格式,但放在 kbone 的特定環境下,把 SVG 轉換成 CSS background-image 反而是一種不甚考究的方案,因為這將會占用 setData()(小程序基礎庫中稱為 vdSyncBatch)的數據量,降低數據層和視圖層之間通信的效率,不過好在每個 SVG 圖片只會被傳輸一次。

在寫這個項目的同時,我也嘗試將經過清理后生成的 SVG 利用小程序接口保存到本地文件,然后將文件的虛擬 URL 交給視圖層,結果并不樂觀。視圖層在向微信 JSSDK 請求該 SVG 文件的過程中,也許因為沒有收到 Content-Type 或者收到的 Content-Type 不對,導致 SVG 文件無法被正確解析展示出來。這可能是小程序的 Bug,或者也許是小程序并沒有打算支持的灰色地帶。

小結

盡管依然存在諸多問題,通過一個 polyfill 來為項目遷移過程中遇到的 SVG 提供一個臨時展示方案仍然是有必要的——這讓我們可以先擱置圖片格式的問題,將更重要的問題處理完之后,再回來批量轉換格式、或改用 Canvas 來繪制。

文中完成的 kbone SVG polyfill 只有一個 JS 文件,托管在我個人的 GitHub,同時為了方便使用也發布到 NPM。本文存在很多主觀推測和評論,如有謬誤,歡迎留言指正。

原創文章轉載請注明:

轉載自AlloyTeam:http://www.ecomenagepro.com/2019/11/14073/

發表評論