作者:vivo 網際網路大前端團隊 - Tian Yuhan
本篇文章主要聚焦海報圖分享這個形式,探討純前端在H5&小程式內,合成海報到下載到本地、分享至社交平臺整個流程中可能遇到的問題,以及如何解決。
一、引言
絕大多數的電商平臺都會設計分享裂變的功能,激勵使用者進行分享,這是一種拉新促活的常見措施。提到分享裂變,就免不了需要生成使用者專屬的分享連結或者專屬海報。當然分享推廣的形式多種多樣,有文字連結、網頁連結、圖片邀請碼、小程式、音影片等等。
本篇文章主要聚焦海報圖分享這個形式,探討純前端在H5&小程式內,合成海報到下載到本地、分享至社交平臺整個流程中可能遇到的問題,以及如何解決。
二、選型參考
2.1 實現方式
根據海報圖生成渠道的差異,可以將海報圖生成分為:客戶端生成、前端生成和服務端生成,以下是從技術實現、相容性、生成速度和功能受限四個維度進行的對比:
-
對於排版複雜的場景: 建議使用服務端Node.js渲染的方式【如Puppeteer圖片生成】;
-
對於排版簡單,但使用者請求併發大的場景:建議使用前端或客戶端生成海報圖;
-
對於需多端投放,樣式動態更新的場景:建議使用前端生成海報圖;
-
對於疊加使用者互動的場景:建議使用客戶端原生繪製的方式生成。
本篇文章著重聚焦前端H5 & 小程式內生成海報圖的技術實現,如果大家對服務端、客戶端生成海報圖的方案感興趣,文末有相關連結可供參考。
2.2 H5 生成海報圖技術選型
H5 生成海報圖常推薦的方案有三種:
(1)html2canvas:基於頁面中存在且可用的DOM結構、樣式,構建“截圖”,繪製到canvas畫布中。
下面是html2canvas的技術解析,詳細講解可以參考文末連結。
(2)dom-to-image:將DOM 節點序列化為XML,包裝到SVG中,再轉為圖片。
下面是dom-to-image的技術解析,詳細講解可以參考文末連結。
(3)canvas 純原生繪製
以下是示例程式碼,這裡不做贅述
html2canvas和dom-to-image都是用於在canvas的基礎上封裝的庫,由於底層實現方式的不同,它們各有優缺點,以下是從幾個維度進行的優缺點對比。
那麼如何快速的確認當前的專案應該用哪個元件庫呢,有沒有必要逐個去驗證,這裡給到幾點建議:
-
如果需要處理的頁面比較複雜或者需要支援SVG圖片渲染,在html2canvas中可能更好一些。
-
如果需要穩定的文字、圖片渲染能力或者處理結構化資料的能力,在dom-to-image中可能更好一些。
-
如果頁面基本是圖片資源,那麼使用原生canvas效能是最好的。
2.3 小程式生成海報圖技術選型
與H5類似,小程式生成海報圖在專案中常用的也有三種方案:
(1)wxml-to-canvas:利用微信小程式提供的canvas元件,將指定的wxml轉化成canvas繪圖,然後再將其匯出為圖片
詳見:微信小程式wxml-to-canvas官方文件
(2)Painter:利用JSX語法,將繪圖的操作轉化成繪製命令,並生成對應的canvas繪圖
下圖來源於Painter 2.0官方介紹
(3)Canvas/Canvas2D
以下是小程式2D Canvas 官方示例程式碼,詳見微信小程式基礎能力/畫布
-
從技術實現上看,wxml-to-canvas 是基於2d型別的Canvas元件能力,對基礎庫有要求,需在2.7.0以上。在canvas2d的基礎上,封裝了繪製text、image和view的能力,提供wxml繪製到canvas和匯出圖片的方法。Painter不僅支援2d型別的canvas,同時相容了低版本canvas元件。同時支援更多複雜樣式的繪製,對於網路素材實現了一套LRU儲存機制,無需重複下載圖片,並增加了容錯機制。
-
從使用方法上看,wxml-to-canvas使用相對較簡單,只需要在小程式中引入wxml-to-canvas庫,然後傳入需要生成海報圖的wxml程式碼和畫布的寬高即可。而Painter需要在小程式中使用類JSX的語法繪製所有圖形,覆蓋小程式頁面中相應的canvas元件。
針對兩種不同庫,wxml-to-canvas更適合將已有的wxml結構轉換為圖片,對於複雜的定製化設計需要在wxml中拼接、排版的海報來說,並不是最佳選擇。Painter則更適合在小程式中進行復雜繪製操作。
但從另一個角度看,如果對繪製效能要求比較高的業務來說,wxml-to-canvas 相比Painter更符合訴求。當然,如果僅涉及圖片間的快速組合,也可以使用canvas原生繪製的能力,只是要關注影像繪製和canvas匯出的實際問題。
前面兩個章節,主要解決在繪製選型上的疑慮,下一步面對的就是如何解決實現過程中出現的問題。
三、H5 常見問題
3.1 設計還原效果
設計還原除了關注最基礎的字型樣式、排版,同時也要兼顧深色模式適配、字號大小適配、頁面縮放、瀏覽器相容性。這些因素無疑給前端生成海報圖帶來巨大的壓力,下面我也將從提到的這些方面進行展開,也可以作為大家選型參考或者解決方案的參考。
當業務需求涉及複雜的文字排版,那麼建議直接放棄canvas原生繪製,難度大風險大耗時長。第三方庫處理邏輯大同小異,本質上都是基於DOM 轉為 canvas,但並不是真正的瀏覽器截圖,都會存在一些共性問題。
3.1.1 字型樣式
如下圖所示,當前裝置生效的是特殊字型,那麼生成海報圖得到的有可能是特殊字型,出現與設計不符,不同使用者生成的海報圖效果不一致的情況。
如何規避這個問題呢?複製DOM並隱藏,對複製的DOM嵌入網路字型包,保證設計效果。如果是固定文案,也可以採用嵌入圖片的方式。
3.1.2 排版
排版常遇到的問題是文字換行、超長字元打點,通常也會疊加系統字號大小、頁面縮放一起出現。
問題1: 文字換行打點怎麼辦?
-
第三方庫中一般會有文字換行相關的處理:
類似html-to-canvas它的處理基於"CanvasRenderingContext2D.measureText()",在繪製時測量每個字元的寬度,進行渲染位置的更新。基於這個處理方案,超長字元打點的問題是很難解決的。
而dom-to-canvas是基於svg轉圖片的方式,天然的具有文字排版處理的能力,這也是這個庫的優勢所在。
問題2: 那如果疊加頁面縮放呢?
一般h5都具有頁面縮放適配的能力,複製DOM也會繼承該處理,不會有太大的影響。
問題3: 如果頁面有設定大字號模式,DOM在不同字號大小下展示樣式有區別,但是需要匯出圖片樣式保持一致呢?
那麼就需要在複製DOM時,針對字號大小進行逆向處理,比如系統字型將字號放大1.25倍,在該場景下,複製DOM時就需要對字號進行縮小1.25倍。
3.1.3 適配
深色模式適配,分為兩種:
-
一種是系統級別的反色:系統級別的反色不會侵入到DOM,複製的DOM仍然與淺色模式下一致,匯出的圖片也不會受到影響。
-
一種是自定義樣式的反色:自定義的反色對DOM樣式有侵入,複製DOM及其樣式,匯出的圖片可能受影響,如下圖所示。只需要在複製DOM時將侵入的樣式或者控制深色模式的判斷條件去除即可。
相容性適配,與canvas和foreignObject的相容性相關,一般來說,不建議在Android低版本中使用,效能較差。
3.2 跨域問題
只要使用canvas繪製圖片,必然繞不過的就是跨域的問題,解決跨域問題有以下幾種思路:
-
代理伺服器:透過配置伺服器代理,將原本跨域的圖片請求轉為同域的圖片請求。從根源上解決跨域問題。針對圖片來源單一可控的場景、具備代理配置的能力的業務場景,建議採用該方案。
-
解決資源跨域:前提需要有許可權訪問被請求的跨域伺服器,可以透過設定伺服器上的 CORS頭資訊“Access-Control-Allow-Origin: * 或者特定域名”來實現,然後再解決資源的跨域問題。下面就聚焦這個場景,展開介紹下:
3.2.1 crossOrigin
在請求圖片時,需要增加屬性 img.crossOrigin = 'anonymous' ,告知跨域伺服器,本次請求可以匿名訪問,不需要提供憑證。程式碼示例如下圖所示:
設定這個就可以一勞永逸了嗎,並不是,還可能出現圖片快取引起的請求跨域問題:在同一個頁面中,如果canvas需要繪製的圖片已經載入過了,瀏覽器會預設快取下來,當canvas使用這張圖片時,瀏覽器會直接返回快取的圖片。這時被快取後的圖片就不展示CORS請求響應頭攜帶Access-Control-Allow-Origin的要求了,就會導致報錯。
解決方案最簡單的是給跨域請求追加時間戳,但這個方案會造成資源重複請求,也可能會有快取擊穿的風險。
3.2.2 ajax+Blob/base64
透過額外發一次請求,將圖片轉為blob或者直接用base64的圖片格式,避免單張圖片頻繁請求。但如果涉及到的圖片比較多,也會帶來記憶體壓力問題,需要注意資源及時釋放。程式碼示例如下圖所示:
3.3 生成效能
-
控制複製DOM的數量 原因:海報圖的生成耗時與複製dom的數量成正比。
-
如果採用的是iframe啟用的方式,減少iframe啟動次數。
-
一般元件庫是基於一個DOM生成一張海報圖,如果一次需要生成多張海報圖,可以合併DOM,啟用一次iframe,批次生成多張圖片。
-
分割操作,將文字相關耗時的操作前置,最後簡單的圖片+圖片的合併,使用canvas原生繪製,也是一種提升效能的方案。
四、小程式常見問題
4.1 元素繪製
不論是使用哪種方案,在小程式繪製海報圖時,需要提前設定好繪製的尺寸,甚至wxml-to-canvas要求為每個元素設定固定的寬度和高度,否則會出現佈局錯誤。所以自適應高度在小程式端是不存在的,對於排版樣式就進行了初步限定。
同時,元素之間是存在層級關係的,與書寫的結構強關聯,這個要尤為注意。
對於元素及其樣式的支援是有限的,不同庫的支援程度也是不一致的,可根據業務進行選擇,這裡簡單舉例對比下:
4.2 儲存問題
以下是wxml-to-canvas的原始碼,在繪製圖片時,往往是繪製網路圖片,需要從遠端拉取圖片,轉為本地臨時檔案儲存【wx.downloadFile】,canvas繪製取臨時檔案地址【tempFilePath】,然後進行繪製。儲存圖片時再將canvas匯出為臨時檔案【wx.canvasToTempFilePath】。
若在小程式內觸發儲存圖片到本地【wx.saveImageToPhotosAlbum】,需要進行許可權校驗,這部分需要業務自行處理,如下所示。
如果選擇使用Painter庫繪製海報圖,且為了提升效能開啟了LRU儲存機制,這時就要關注快取檔案的有效性,官方也在小程式 LRU 儲存設計中進行了重點講解。
4.3 生成效能
研究過Painter原始碼的同學可能關注到在canvas匯出圖片的函式中,存在一個300ms的延遲,這個是為什麼呢?300ms這個延遲可以去掉嗎?
答案是:這個邏輯下不可以省略!否則可能出現圖片缺失的問題。
Painter提供了canvas2d 介面和canvas舊介面兩套邏輯,而延遲300ms出現在舊介面邏輯體系中。這是因為舊介面使用的是downloadFile將網路圖片快取到本地後再進行drawImage,而整個繪製-匯出過程中沒有監聽圖片載入完成的步驟,因此有機率出現圖片未載入完成就直接繪製的情況,進而出現內容缺失。雖然加入了定時器延長時間,但不同的機器不同的圖片繪製時間不同,設定單一的延遲等待並不能從根本上解決問題,反而影響生成圖片的效能。
相應的,在canvas2d的體系中則可以透過 Canvas.createImage 建立圖片物件,並監聽圖片的load事件,在回撥之後執行ctx.drawImage繪製到canvas中,這樣就可以有效的解決上述問題。
針對這種場景,如果有其他比較好的解決方案,歡迎讀者在評論區留言。
ps: 微信官方正在推出Skyline/snapshot的截圖元件,也可以實現wxml/wxss匯出圖片的功能,詳見微信小程式Skyline /snapshot
五、思考總結
本文主要聚焦前端生成海報圖的技術選型,不同選型可能出現的問題及其解決思路。期望透過本篇文章,能夠讓讀者在接收到相關需求時,對於一些容易踩坑的點做好前期評估。如果當前正在開發相關的需求,遇到相似的問題能夠提供一些解決思路。
同時如果大家有更好的解決方案,歡迎在評論區留言。
當然,隨著AI技術的發展,生成海報圖可能不再需要透過程式碼邏輯實現。可以透過資料模型訓練,更加智慧化的快速生成營銷海報圖,那麼以上的問題將不再是問題。我們也將持續關注在ToC業務中相關的實現方案,與大家一起探討。
參考連線:
-
html2canvas實現瀏覽器截圖的原理(包含原始碼分析的通用方法)
-
前端關於html2canvas截圖的問題?
-
使用html2canvas在前端生成圖片