一次 H5 「保存頁面爲圖片」 的踩坑之旅

1. 需求

最近丁香醫生的產品大佬又雙叒叕搞事情,想要在 H5 中作一個醫生邀請提問的功能,能夠將醫生的二維碼名片分享出去,支持移動、PC 和微信。以前的圖片是由後端生成的,而且會緩存三天,致使信息更新不及時。由前端來作就能避免這些問題。css

我一聽,這好說,不就是個保存圖片的功能麼,簡單看看需求:html

  • 完善卡片信息,分享出去時候信息更加立體
  • 編輯我的資料入口
  • 保存圖片入口
  • 可解決醫生名片緩存時間問題
  • 長下面這樣 ⬇

image
image

分析下來就兩點前端

  • html展現實時用戶信息
  • 點擊保存將當前頁面保存成圖片至本地,而且不包含功能按鈕

2. 方案

由於以前已經據說過有個庫能將 HTML 轉爲 canvas,而後又據說 canvas 能轉爲圖片,而後又據說圖片能下載....(開發基本靠據說(搜索),這是廢話)node

那個人基本方案就是:
html -> canvas -> image -> a[download]git

  1. html2canvas.js:可將 htmldom 轉爲 canvas 元素。傳送門
  2. canvasAPI:toDataUrl() 可將 canvas 轉爲 base64 格式
  3. 建立 a[download] 標籤觸發 click 事件實現下載

3. 採坑表演

既然方案定下來了,下面就開始踩坑表演了,👏程序員

3.1. 原理

官方是這樣介紹的:github

js將遍歷加載頁面的 DOM 節點,收集全部元素的信息,而後用這些信息來呈現頁面。換句話說,實際上這個庫並非真的對頁面進行截圖,而是基於從 DOM 讀取的元素及屬性來一點點的繪製 canvas。 所以,它只能正確地呈現它理解的元素和屬性,這意味着有許多 CSS 屬性不起做用。chrome

// v0.4.1
html2canvas(element, {
    onrendered: function(canvas) {
        // 如今你已經拿到了canvas DOM元素 
    }
});

// v0.5.0
html2canvas(element, options).then(canvas => {
    // 如今你已經拿到了canvas DOM元素 
});複製代碼

因此基本能夠猜到整個工做流程應該是:canvas

  1. 遞歸處理每一個節點,記錄這個節點應該怎麼畫。(好比div就畫邊框和背景,文字就畫文字等等)
  2. 考慮節點的層級問題。好比不少佈局相關樣式屬性: z-index、float、position 等的影響。
  3. 從低層級開始畫到 canvas 上,逐漸向上畫。層級高的覆蓋層級低的(和瀏覽器自己的渲染流程很像)。

3.2 坑💀

目前官方提供的版本有不少,正式版本是v0.4.1 - 7.9.2013,最新版本是v0.5.0-beta4,那對於咱們開發來講若是不是玩新特性什麼的通常仍是會選擇正式版,結果第一個坑就掉進去爬了半天。後端

3.2.1 圖片模糊

由於開發的時候是用 chrome 模擬器生成 canvas 後沒有發現有模糊的地方,可是用 PC 代理手機請求開發資源時,發現畫面的模糊感很是明顯。

如圖:

image
image

容易想到,多是移動端像素密度計算的問題。

設備像素比 (簡稱 dpr) 定義了物理像素和設備獨立像素的對應關係,它的值能夠按以下的公式的獲得:

設備像素比 = 物理像素 / 設備獨立像素 // 在某一方向上,x方向或者y方向

知道了這個也沒用,由於文檔中根本沒有給出可以配置像素比的地方。。

然而經過研究發現,官方文檔其實仍是 0.4.1 的,從 0.5.0 版本開始,其實已經偷偷摸摸支持自定義 canvas 做爲配置項傳入了,它會根據咱們傳入的 canvas 爲基礎開始繪製。因此咱們在調用 html2canvas 的時候,能夠先建立好一個尺寸合適的 canvas,再傳進去。

話很少說,首先將庫升級到 0.5.0,而後:

/** * 根據window.devicePixelRatio獲取像素比 */
function DPR() {
    if (window.devicePixelRatio && window.devicePixelRatio > 1) {
        return window.devicePixelRatio;
    }
    return 1;
}
/** * 將傳入值轉爲整數 */
function parseValue(value) {
    return parseInt(value, 10);
};
/** * 繪製canvas */
async function drawCanvas(selector) {
    // 獲取想要轉換的 DOM 節點
    const dom = document.querySelector(selector);
    const box = window.getComputedStyle(dom);
    // DOM 節點計算後寬高
    const width = parseValue(box.width);
    const height = parseValue(box.height);
    // 獲取像素比
    const scaleBy = DPR();
    // 建立自定義 canvas 元素
    const canvas = document.createElement('canvas');

    // 設定 canvas 元素屬性寬高爲 DOM 節點寬高 * 像素比
    canvas.width = width * scaleBy;
    canvas.height = height * scaleBy;
    // 設定 canvas css寬高爲 DOM 節點寬高
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    // 獲取畫筆
    const context = canvas.getContext('2d');

    // 將全部繪製內容放大像素比倍
    context.scale(scaleBy, scaleBy);

    // 將自定義 canvas 做爲配置項傳入,開始繪製
    return await html2canvas(dom, {canvas});
}複製代碼

以上代碼先獲取設備像素比,並根據比例建立尺寸更大的 canvas。如二倍屏就是二倍,三倍屏就是三倍,八倍鏡就是八倍···
手機端截圖,和html展現效果一致,基本看不出來差異。

image
image

3.2.2 圖片畫出來怎麼不見了

PC端截圖:

image
image

可能有多種緣由,排查後發現是由於 canvas 內的圖片跨域了 這裏有解釋
總而言之,就是:能夠在 canvas 中繪製跨域的圖片,但此時的 canvas 處於被 「污染」 的狀態,而污染狀態的 canvas 使用 toDataUrl() 等 API 是會出現問題的。

因此,如今咱們須要作兩件事:

  1. 給 img 元素設置 crossOrigin 屬性,值爲 anonymous
  2. 圖片服務端設置容許跨域(返回 CORS 頭)

第一件事好辦,由於 html2canvas 自己支持配置useCORS: true

可是第二件事就要分狀況。當圖片放在本身服務器時,僅僅是讓後端小哥改個配置的事兒。可是當圖片放在 CDN 上時······嗯, 爲了更快的響應,不少 CDN 會緩存圖片的返回值,而緩存的值是不帶 CORS 頭的。由於沒有 CORS 頭,因此 js 請求會被攔截。這個時候,咱們可使用服務器轉發,在轉發時帶上 CORS 頭。(前端擼一個 node 中間層來進行服務器轉發是個很好的方案,這個下回再單獨說)

OK。使用以上方案,咱們測試一下。

PC 端打開,完美。

微信端,咦,仍是不行。
後期發現,使用 html2canvas 0.5.0 版本是沒有問題的,可是開發時使用 0.4.1 繪製 canvas 仍是會致使圖片丟失。猜想是由於 html2canvas 在預載圖片和繪製圖片時多了什麼不可描述的東西。爲了解決這個問題,咱們使用了一個很是暴力的解決方案:用 js 去獲取圖片,得到其 base64,放回 img 的 src 中再進行繪製。

/** * 圖片轉base64格式 */
img2base64(url, crossOrigin) {
    return new Promise(resolve => {
        const img = new Image();

        img.onload = () => {
            const c = document.createElement('canvas');

            c.width = img.naturalWidth;
            c.height = img.naturalHeight;

            const cxt = c.getContext('2d');

            cxt.drawImage(img, 0, 0);
            // 獲得圖片的base64編碼數據
            resolve(c.toDataURL('image/png'));
        };

        crossOrigin && img.setAttribute('crossOrigin', crossOrigin);
        img.src = url;
    });
}複製代碼

這個坑總算是磕磕碰碰趟過去了。

3.2.3 倒角

border-radius 必須 ≤ 短邊長度的一半,而且是具體數值,不然可能會出現奇妙的效果。

另外使用僞元素實現 0.5px 邊框也可能會出現奇妙效果,建議直接使用 border 屬性

0.4.1 版本中須要作圓形圖片只能置爲背景圖,img 不支持繪製 border-radius,0.5.0 中則無此限制

3.2.4 虛線

前面說的, html2canvas 並不支持全部 css 屬性。使用 border-style: dashed/dotted 無效,仍是大實線。切圖在 PC 端有效,可是在微信中,嘗試使用切圖渲染虛線時有可能還會報 SecurityError, The operation is insecure. 錯誤,致使轉 base64 失敗

3.3 保存

理想:

/** * 在本地進行文件保存 * @param {String} data 要保存到本地的圖片數據 * @param {String} filename 文件名 */
saveFile(data, filename) {
    const save_link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
    save_link.href = data;
    save_link.download = filename;

    const event = document.createEvent('MouseEvents');
    event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    save_link.dispatchEvent(event);
}複製代碼

現實:

PC端: 完美。微信大佬:很差意思,你說什麼?我聽不見?!

好嘛,微信中根本沒有任何反應。查看 微信sdk 後發現:

  • downloadImage 僅支持 uploadImage 接口上傳的圖片。
  • uploadImage 接口僅支持 chooseImage 接口相冊選擇的圖片。
  • chooseImage 接口是從本地相冊選擇圖片。
  • 那麼問題來了,圖片都在相冊了還須要咱們幹啥?
  • ....

4. (在痛苦和妥協中) 交付

最終實現的方案是:

  • 用戶進入該頁面
  • 獲取當前用戶全部信息,頭像,二維碼等
  • 將全部圖片轉爲 base64
  • 渲染 html
  • 繪製 canvas
  • 將 canvas 保存爲 base64
  • 替換 htmlimgsrc爲 base64
  • 完成頁面到圖片的轉換,微信中用戶可長按頁面調起 actionSheet 識別或保存圖片

也就是說,用戶剛進入頁面時,顯示的是 html。js 執行完後,將原有 html 刪掉,替換爲圖片。

再回頭看咱們的需求:

  • html 展現實時用戶信息
  • 點擊保存將當前頁面保存成圖片至本地

其實最終只實現了第一點,而第二點實際上是實現了一半,圖片雖然生成了,但保存功能仍是須要用戶長按圖片,調起微信內置菜單來完成。在進行 H5 開發時,一旦考慮到微信,就有可能出現一些以前考慮不到的問題和限制,對此,產品經理和程序員都要儘量地多多瞭解。知道在微信中,能幹什麼,不能幹什麼,下降開發和反覆溝通的成本。

但願以上內容可以對你們之後的開發有所幫助。

做者: 丁香園 f2e - 顧重喜

相關文章
相關標籤/搜索