微信H5頁儲存當前頁面為圖片踩坑

顧重發表於2017-11-27

歡迎關注@丁香園F2E檢視更多技術好文

1. 需求

最近產品大大又又又搞事情,說什麼想要在微信H5專案中做一個醫生邀請提問的功能,可以將醫生的二維碼名片分享出去,之前圖片由後端生成並且會快取三天導致資訊更新不及時;

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

  • 完善卡片資訊,分享出去時候資訊更加立體
  • 編輯個人資料入口
  • 儲存圖片入口
  • 可解決醫生名片快取時間問題
  • 長下面這樣 ⬇

image
image

分析下來就兩點

  • html展示實時使用者資訊
  • 點選儲存將當前頁面儲存成圖片至本地,並且不包含功能按鈕

2. 方案

因為之前已經聽說過有個庫能將HTML轉為canvas,然後又聽說canvas能轉為圖片,然後又聽說圖片能下載....(開發基本靠聽說,這是廢話)

那我的基本方案就是:
html -> canvas -> image -> a[download]

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

3. 開發

既然方案定下來那麼說幹就幹,下面請開始我的踩坑表演,?

3.1 html2canvas.js

官方是這樣介紹的:

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

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

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

3.1.1 原理

該庫的原理是將選中的dom及所有子節點全部跑一邊,將每個節點用對應的方法挨個繪製到canvas中。

所以基本可以猜到整個工作流程應該是:

  1. 遞迴處理每個節點,記錄這個節點應該怎麼畫。(比如div就畫邊框和背景,文字就畫文字等等)
  2. 考慮節點的層級問題。比如z-index,float, position等樣式的影響。
  3. 從低層級開始畫到canvas上,逐漸向上畫。層級高的覆蓋層級低的。

3.1.2 坑

目前官方提供的版本有很多,正式版本是v0.4.1 - 7.9.2013,最新版本是v0.5.0-beta4,那對於我們開發來說如果不是玩新特性什麼的一般還是會選擇正式版,結果第一個坑就掉進去爬了半天。。

3.1.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});
}複製程式碼

手機端截圖,和html展示效果一致,基本看不出來差別

image
image

3.1.2.2 微信H5頁儲存當前頁面為圖片踩坑圖片畫出來怎麼不見了

PC端截圖

image
image

可能有多種原因,最主要的原因是圖片跨域並且伺服器未設定允許跨域??這裡有解釋

看完發現我們好像需要做兩件事:

  1. 給img元素設定crossOrigin屬性,值為anonymous
  2. 圖片服務端設定允許跨域

第一件事其實忽略,因為html2canvas支援配置useCORS: true

但是第二件事有點難辦,因為我們的圖片一般都是上傳到CDN上,而CDN為了更快的響應,會快取圖片的返回值,而快取的值是不帶跨域的頭的。因為沒有跨域的頭,所以js請求會被攔截。但這是cdn不在我們手裡的情況,如果在了那不就是後端改個配置的事情麼,哈哈哈哈。

PC端: 完美。

微信環境下如果使用0.5.0也沒有問題,但是使用0.4.1時繪製canvas的還是會導致圖片丟失。。只能猜測是在html2canvas在預載圖片和繪製圖片時少了不可描述的東西。

因為一開始使用0.4.1所以這個坑我沒有繞過去,強行解決:

// 請求圖片的事自己來做;將圖片轉為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.1.2.3 倒角

border-radius 只能是≤短邊長度的一半,並且是具體數值,否則可能會出現奇妙的效果。

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

0.4.1版本中需要做圓形圖片只能置為背景圖,img不支援繪製border-radius0.5中無此限制

3.1.2.4 虛線

使用border-style: dashed/dotted無效,還是大實線,切圖在PC端有效,微信中需轉為base64並且依賴環境,可能無效。

3.2 toDataUrl()

只要canvas中沒有跨域圖片可以隨便轉

But 在微信中就算沒有也可能失敗,在嘗試使用切圖渲染虛線時微信中還是會報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
  • 轉為base64
  • 替換htmlimgsrc為base64
  • 完成頁面到圖片的轉換,微信中使用者可長按頁面調起actionSheet識別或儲存圖片
  • 在回頭看我們的需求
    • html展示實時使用者資訊
    • 點選儲存將當前頁面儲存成圖片至本地

其實只實現了第一點,第二點僅實現了一半,儲存功能還是需要使用者調起微信內建API來完成,而不是我們幫使用者完成;微信他不給這個介面我也很絕望啊/(ㄒoㄒ)/~~

希望以上內容能夠對大家以後的開發有所幫助,謝謝。

完。

相關文章