快照-網頁保存爲圖片

1. 背景

將網頁保存爲圖片(如下簡稱爲快照),是用戶記錄和分享頁面信息的有效手段,在各類興趣測試和營銷推廣等形式的活動頁面中尤其常見。css

快照環節一般處於頁面交互流程的末端,彙總了用戶最終的參與結果,直接影響到用戶對於活動的完總體驗。所以,生成高質量的頁面快照,對於活動的傳播和品牌的轉化具備十分重要的意義。html

本文基於雲音樂往期優質活動的相關實踐(例如「關於你的畫」、「權力的遊戲」和「你的使用說明書」等),從快照的內容完整性清晰度轉換效率等多個方面,討論將網頁轉換爲高質量圖片的實踐探索。前端

2. 適用場景

  • 適用於將頁面轉爲圖片,特別是對實時性要求較高的場景。
  • 但願在快照中展現跨域圖片資源的場景。
  • 針對生成圖片內容不完整、模糊或者轉換過程緩慢等問題,尋求有效解決方案的場景。

3. 原理簡析

3.1 方案選型

依據圖片是否由設備本地生成,快照可分爲前端處理和後端處理兩種方式。node

因爲後端生成的方案依賴於網絡通訊,不可避免地存在通訊開銷和等待時延,同時對於模板和數據結構變動也有必定的維護成本。webpack

所以,出於實時性靈活性等綜合考慮,咱們優先選用前端處理的方式。web

3.2 基本原理

前端側對於快照的處理過程,實質上是將 DOM 節點包含的視圖信息轉換爲圖片信息的過程。這個過程能夠藉助 canvas 的原生 API 實現,這也是方案可行性的基礎。canvas

theory後端

具體來講,轉換過程是將目標 DOM 節點繪製到 canvas 畫布,而後 canvas 畫布以圖片形式導出。可簡單標記爲繪製階段和導出階段兩個步驟:api

  • 繪製階段:選擇但願繪製的 DOM 節點,根據nodeType調用 canvas 對象的對應 API,將目標 DOM 節點繪製到 canvas 畫布(例如對於<img>的繪製使用 drawImage 方法)。
  • 導出階段:經過 canvas 的 toDataURL 或 getImageData 等對外接口,最終實現畫布內容的導出。

3.3 原生示例

具體地,對於單個<img>元素可按以下方式生成自身的快照:跨域

HTML:

<img id="target" src="./music-icon.png" />

JavaScript:

// 獲取目標元素const target = document.getElementById('target');// 新建canvas畫布const canvas = document.createElement('canvas');canvas.width = 100;canvas.height = 100;const ctx = canvas.getContext("2d");// 導出階段:從canvas導出新的圖片const exportNewImage = (canvas) => {    const exportImage = document.createElement('img');    exportImage.src = canvas.toDataURL();    document.body.appendChild(exportImage);}// 繪製階段:待圖片內容加載完畢後繪製畫布target.onload = () => {    // 將圖片內容繪入畫布    ctx.drawImage(target, 0, 0, 100, 100);    // 將畫布內容導出爲新的圖片    exportNewImage(canvas);}

其中,drawImage是 canvas 上下文對象的實例方法,提供多種方式將 CanvasImageSource 源繪製到 canvas 畫布上。exportNewImage用於將 canvas 中的視圖信息導出爲包含圖片展現的 data URI。

4. 基礎方案

在上一部分中,咱們能夠看到基於 canvas 提供的相關基礎 API,爲前端側的頁面快照處理提供了可能。

然而,具體的業務應用每每更加複雜,上面的「低配版」實例顯然未能覆蓋多數的實際場景,例如:

  • canvas 的drawImage方法只接受 CanvasImageSource,而CanvasImageSource並不包括文本節點、普通的div等,將非<img>的元素繪製到 canvas 須要特定處理。
  • 當有多個 DOM 元素須要繪製時,層級優先級處理較爲複雜。
  • 須要關注floatz-indexposition等佈局定位的處理。
  • 樣式合成繪製計算較爲繁瑣。

所以,基於對綜合業務場景的考慮,咱們採用社區中承認度較高的方案:html2canvascanvas2image做爲實現快照功能的基礎庫。

4.1 html2canvas

提供將 DOM 繪製到 canvas 的能力

這款來自社區的神器,爲開發者簡化了將逐個 DOM 繪製到 canvas 的過程。簡單來講,其基本原理爲:

  • 遞歸遍歷目標節點及其子節點,收集節點的樣式信息;
  • 計算節點自己的層級關係,根據必定優先級策略將節點逐一繪製到 canvas 畫布中;
  • 重複這一過程,最終實現目標節點內容的所有繪製。

在使用方面,html2canvas對外暴露了一個可執行函數,它的第一個參數用於接收待繪製的目標節點(必選);第二個參數是可選的配置項,用於設置涉及 canvas 導出的各個參數:

// element 爲目標繪製節點,options爲可選參數html2canvas(element[,options]);

簡易調用示例以下:

import html2canvas from 'html2canvas';const options = {};// 輸入body節點,返回包含body視圖內容的canvas對象html2canvas(document.body, options).then(function(canvas) {    document.body.appendChild(canvas);});

4.2 canvas2image

提供由 canvas 導出圖片信息的多種方法

相比於html2canvas承擔的複雜繪製流程,canvas2image 所要作的事情簡單的多。

canvas2image僅用於將輸入的 canvas 對象按特定格式轉換和存儲操做,其中這兩類操做均支持 PNG,JPEG,GIF,BMP 四種圖片類型:

// 格式轉換Canvas2Image.convertToPNG(canvasObj, width, height);Canvas2Image.convertToJPEG(canvasObj, width, height);Canvas2Image.convertToGIF(canvasObj, width, height);Canvas2Image.convertToBMP(canvasObj, width, height);// 另存爲指定格式圖片Canvas2Image.saveAsPNG(canvasObj, width, height);Canvas2Image.saveAsJPEG(canvasObj, width, height);Canvas2Image.saveAsGIF(canvasObj, width, height);Canvas2Image.saveAsBMP(canvasObj, width, height);

實質上,canvas2image只是提供了針對 canvas 基礎 API 的二次封裝(例如 getImageData、toDataURL),而自己並不依賴html2canvas

在使用方面,因爲目前做者並未提供 ES6 版本的canvas2image(v1.0.5),暫時不能直接以 import 方式引入該模塊。

對於支持現代化構建的工程中(例如 webpack),開發者能夠自助 clone 源碼並手動添加 export 得到 ESM 支持:

支持 ESM 導出

// canvas2Image.jsconst Canvas2Image = function () {    ...}();// 如下爲定製添加的內容export default Canvas2Image;

調用示例

import Canvas2Image from './canvas2Image.js';// 其中,canvas表明傳入的canvas對象,width, height分別爲導出圖片的寬高數值Canvas2Image.convertToPNG(canvas, width, height)

4.3 組合技

接下來,咱們基於以上兩個工具庫,實現一個基礎版的快照生成方案。一樣是分爲兩個階段,對應 3.2 節的基本原理:

  • 第一步,經過html2canvas實現 DOM 節點繪製到 canvas 對象中;
  • 第二步,將上一步返回的 canvas 對象傳入canvas2image,進而按需導出快照圖片信息。

具體地,咱們封裝一個convertToImage的函數,用於輸入目標節點以及配置項參數,輸出快照圖片信息。

JavaScript

// convertToImage.jsimport html2canvas from 'html2canvas';import Canvas2Image from './canvas2Image.js';/** * 基礎版快照方案 * @param {HTMLElement} container * @param {object} options html2canvas相關配置 */function convertToImage(container, options = {}) {    return html2canvas(container, options).then(canvas => {        const imageEl = Canvas2Image.convertToPNG(canvas, canvas.width, canvas.height);        return imageEl;    });}

5. 進階優化

經過上一節的實例,咱們基於html2canvascanvas2image,實現了相比原生方案通用性更佳的基礎頁面快照方案。然而面對實際複雜的應用場景,以上基礎方案生成的快照效果每每不盡如人意。

快照效果的差別性,一方面是因爲html2canvas導出的視圖信息是經過各類 DOM 和 canvas 的 API 複合計算二次繪製的結果(並不是一鍵柵格化)。所以不一樣宿主環境的相關 API 實現差別,可能致使生成的圖片效果存在多端不一致性或者顯示異常的狀況。

另外一方面,業務層面的因素,例如對於開發者html2canvas的配置有誤或者是頁面佈局不當等緣由,也會對生成快照的結果帶來誤差。

社區中也能夠常見到一些對於生成快照質量的討論,例如:

  • 爲何有些內容顯示不完整、殘缺、白屏或黑屏?
  • 明明原頁面清晰可辨,爲何生成的圖片模糊如毛玻璃?
  • 將頁面轉換爲圖片的過程十分緩慢,影響後續相關操做,有什麼好辦法麼?
  • ...

下面咱們從內容完整性清晰度優化轉換效率,進一步探究高質量的快照解決方案。

5.1 內容完整性

首要問題:保證目標節點視圖信息完整導出

因爲真機環境的兼容性和業務實現方式的不一樣,在一些使用html2canvas過程當中常會出現快照內容與原視圖不一致的狀況。內容不完整的常見自檢checklist以下:

  • 跨域問題:存在跨域圖片污染 canvas 畫布。
  • 資源加載:生成快照時,相關資源還未加載完畢。
  • 滾動問題:頁面中滾動元素存在偏移量,致使生成的快照頂部出現空白。

5.1.1 跨域問題

常見於引入的圖片素材相對於部署工程跨域的場景。例如部署在https://st.music.163.com/上面的頁面中引入了來源爲https://p1.music.126.net的圖片,這類圖片便是屬於跨域的圖片資源。

因爲 canvas 對於圖片資源的同源限制,若是畫布中包含跨域的圖片資源則會污染畫布( Tainted canvases ),形成生成圖片內容混亂或者html2canvas方法不執行等異常問題。

對於跨域圖片資源處理,能夠從如下幾方面着手:

(1)useCORS 配置

開啓html2canvasuseCORS配置項,示例以下:

// doc: http://html2canvas.hertzen.com/configuration/const opts = {    useCORS   : true,   // 容許使用跨域圖片    allowTaint: false   // 不容許跨域圖片污染畫布};html2canvas(element, opts);

html2canvas的源碼中對於useCORS配置項置爲true的處理,實質上是將目標節點中的<img>標籤注入 crossOrigin 爲anonymous的屬性,從而容許載入符合 CORS 規範的圖片資源。

其中,allowTaint默認爲false,也能夠不做顯式設置。即便該項置爲true,也不能繞過 canvas 對於跨域圖片的限制,由於在調用 canvas 的toDataURL時依然會被瀏覽器禁止。

(2)CORS 配置

上一步的useCORS的配置,只是容許<img>接收跨域的圖片資源,而對於解鎖跨域圖片在 canvas 上的繪製並導出,須要圖片資源自己須要提供 CORS 支持。

這裏介紹下跨域圖片使用 CDN 資源時的注意事項:

驗證圖片資源是否支持 CORS 跨域,經過 Chrome 開發者工具能夠看到圖片請求響應頭中應含有Access-Control-Allow-Origin的字段,即坊間常提到的跨域頭。

例如,某個來自 CDN 圖片資源的響應頭示例:

// Response Headersaccess-control-allow-credentials: trueaccess-control-allow-headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Typeaccess-control-allow-methods: GET,POST,OPTIONSaccess-control-allow-origin: *

不一樣的 CDN 服務商配置資源跨域頭的方式不一樣,具體應諮詢 CDN 服務商。

特殊狀況下,部分 CDN 提供方可能會存在圖片緩存不含 CORS 跨域頭的狀況。爲保證快照顯示正常,建議優先聯繫 CDN 尋求技術支持,不推薦經過圖片連接後綴時間戳等方式強制回源,避免影響源站性能和 CDN 計費。

(3)服務端轉發

在微信等第三方 APP 中,平臺的用戶頭像等圖片資源是不直接提供 CORS 支持的。此時須要藉助服務端做代理轉發,從而繞過跨域限制。

即經過服務端代爲請求平臺用戶的頭像地址並轉發給客戶端(瀏覽器),固然這個服務端接口自己要與頁面同源或者支持 CORS。

爲簡潔表述,假設前端與後端針對跨域圖片轉發做以下約定,且該接口與前端工程部署在相同域名下:

請求地址

請求方式

傳入參數

返回信息

/api/redirect/image

GET

redirect,表示原圖地址

Content-Typeimage/png的圖片資源

頁面中的<img>經過拼接/api/redirect/image與表明原圖地址的查詢參數redirect,發出一個 GET 請求圖片資源。因爲接口與頁面同源,所以不會觸發跨域限制:

<img src="/api/redirect/image?redirect=thirdwx.qlogo.cn/somebody/avatar" alt="user-pic" class="avatar" crossorigin="anonymous">

對於服務端接口的實現,這裏基於 koa 提供了一則簡易示例:

const Koa = require('koa');const router = require('koa-router')();const querystring = require('querystring');const app = new Koa();/** * 圖片轉發接口 * - 接收 redirect 入參,即須要代爲請求的圖片URL * - 返回圖片資源 */router.get('/api/redirect/image', async function(ctx) {    const querys = ctx.querystring;    if (!querys) return;    const { redirect } = querystring.parse(querys);    const res = await proxyFetchImage(redirect);    ctx.set('Content-Type', 'image/png');    ctx.set('Cache-Control', 'max-age=2592000');    ctx.response.body = res;})/** * 請求並返回圖片資源 * @param {String} url 圖片地址 */async function proxyFetchImage(url) {    const res = await fetch(url);    return res.body;}const res = await proxyFetchImage(redirect);app.use(router.routes());

在瀏覽器看來,頁面請求的圖片資源還是相同域名下的資源,轉發過程對前端透明。建議在需求開發前瞭解圖片資源的來源狀況,明確是否須要服務端支持。

在雲音樂早期的活動「權力的遊戲」中,使用了同類方案,實現了微信平臺中用戶頭像的完整繪製和快照導出。

5.1.2 資源加載

資源加載不全,是形成快照不完整的一個常見因素。在生成快照時,若是部分資源沒有加載完畢,那麼生成的內容天然也談不上完整。

除了設置必定的延遲外,若是要確保資源加載完畢,能夠基於 Promise.all 實現。

加載圖片

const preloadImg = (src) => {    return new Promise((resolve, reject) => {        const img = new Image();        img.onload = () => {            resolve();        }        img.src = src;    });}

確保在所有加載後生成快照

const preloadList = [    './pic-1.png',    './pic-2.png',    './pic-3.png',];Promise.all(preloadList.map(src => preloadImg(src))).then(async () => {    convertToImage(container).then(canvas => {        // ...    })});

實際上,以上方法只是解決頁面圖片的顯示問題。在真實場景中,即便頁面上的圖片顯示完整,保存快照後依然可能出現內容空白的狀況。緣由是 html2canvas 庫內部處理時,對圖片資源仍會作一次加載請求;若是此時加載失敗,那麼該部分保存快照後便是空白的。

下面介紹圖片資源轉 Blob 的方案,保證圖片的地址來自本地,避免在快照轉化時加載失敗的狀況。這裏提到的 Blob 對象表示一個不可變、表明二進制原始數據的類文件對象,在特定的使用場景會使用到。

圖片資源轉 Blob:

// 返回圖片Blob地址const toBlobURL = (function () {    const urlMap = {};    // @param {string} url 傳入圖片資源地址    return function (url) {        // 過濾重複值        if (urlMap[url]) return Promise.resolve(urlMap[url]);        return new Promise((resolve, reject) => {            const canvas = document.createElement('canvas');            const ctx = canvas.getContext('2d');            const img = document.createElement('img');            img.src = url;            img.onload = () => {                canvas.width = img.width;                canvas.height = img.height;                ctx.drawImage(img, 0, 0);                // 關鍵👇                canvas.toBlob((blob) => {                    const blobURL = URL.createObjectURL(blob);                    resolve(blobURL);                });            };            img.onerror = (e) => {                reject(e);            };        });    };}());

以上toBlobURL方法實現將加載<img>的資源連接轉爲 blobURL。

進一步地,經過convertToBlobImage方法,實現對於傳入的目標節點中的<img>批量處理爲Blob格式。

// 批量處理function convertToBlobImage(targetNode, timeout) {    if (!targetNode) return Promise.resolve();    let nodeList = targetNode;    if (targetNode instanceof Element) {        if (targetNode.tagName.toLowerCase() === 'img') {            nodeList = [targetNode];        } else {            nodeList = targetNode.getElementsByTagName('img');        }    } else if (!(nodeList instanceof Array) && !(nodeList instanceof NodeList)) {        throw new Error('[convertToBlobImage] 必須是Element或NodeList類型');    }    if (nodeList.length === 0) return Promise.resolve();    // 僅考慮<img>    return new Promise((resolve) => {        let resolved = false;        // 超時處理        if (timeout) {            setTimeout(() => {                if (!resolved) resolve();                resolved = true;            }, timeout);        }        let count = 0;        // 逐一替換<img>資源地址        for (let i = 0, len = nodeList.length; i < len; ++i) {            const v = nodeList[i];            let p = Promise.resolve();            if (v.tagName.toLowerCase() === 'img') {                p = toBlobURL(v.src).then((blob) => {                    v.src = blob;                });            }            p.finally(() => {                if (++count === nodeList.length && !resolved) resolve();            });        }    });}export default convertToBlobImage;

使用方面,convertToBlobImage應在調用生成快照convertToImage方法前執行。

5.1.3 滾動問題

  • 典型特徵:生成快照的頂部存在空白區域。
  • 緣由:通常是保存長圖(超過一屏),而且滾動條不在頂部時致使(常見於 SPA 類應用)。
  • 解決辦法:在調用convertToImage以前,先記錄此時的scrollTop,而後調用window.scroll(0, 0)將頁面移動至頂部。待快照生成後,再調用window.scroll(0, scrollTop)恢復原有縱向偏移量。

示例

// 待保存的目標節點(按實際修改👇)const container = document.body;// 實際的滾動元素(按實際修改👇)const scrollElement = document.documentElement;// 記錄滾動元素縱向偏移量const scrollTop = scrollElement.scrollTop;// 針對滾動元素是 body 先做置頂window.scroll(0, 0);convertToImage(container)    .then(() => {        // ...    }).catch(() => {        // ...    }).finally(() => {        // 恢復偏移量        window.scroll(0, scrollTop);    });

特別地,對於存在局部滾動佈局的狀況,也能夠操做對應滾動元素置頂避免容器頂部空白的狀況。

5.2 清晰度優化

清晰度是快照質量的分水嶺

下圖取自「權力的遊戲」中兩張優化先後的結果頁快照對比。能夠看到優化前的左圖,不管是在文字邊緣仍是圖像細節上,相較優化後的清晰度存在明顯可辨的差距。

clear

最終生成快照的清晰度,源頭上取決於第一步中 DOM 轉換成的 canvas 的清晰度。

如下介紹 5 種行之有效的清晰度優化方法。

5.2.1 使用px單位

爲了給到html2canvas明確的整數計算值,避免因小數舍入而致使的拉伸模糊,建議將佈局中使用中使用%vwvhrem等單位的元素樣式,統一改成使用px

good:

<div style="width: 100px;"></div>

bad:

<div style="width: 30%;"></div>

5.2.2 優先使用 img 標籤展現圖片

不少狀況下,導出圖片模糊是由原視圖中的圖片是以 css 中 background 的方式顯示的。由於 background-size 並不會反饋一個具體的寬高數值,而是經過枚舉值如 contain、cover 等表明圖片縮放的類型;相對於<img>標籤, background 方式最終生成的圖片會較爲模糊。將 background 改成<img>方式呈現,對於圖片清晰度會有必定的改觀。對於必需要使用 background 的場景,參見 5.25 節的解決方案。

good:

<img class="u-image" src="./music.png" alt="icon">

bad:

<div class="u-image" style="background: url(./music.png);"></div>

5.2.3  配置高倍的 canvas 畫布

對於高分辨率的屏幕,canvas 可經過將 css 像素與高分屏的物理像素對齊,實現必定程度的清晰度提高(這裏對兩類像素有詳細描述和討論)。

在具體操做中,建立由 devicePixelRatio 放大的圖像,而後使用 css 將其縮小相同的倍數,有效地提升繪製到 canvas 中的圖像清晰度表現。

在使用html2canvas時,咱們能夠配置一個放縮後的 canvas 畫布用於導入節點的繪製。

// convertToImage.jsimport html2canvas from 'html2canvas';// 建立用於繪製的基礎canvas畫布function createBaseCanvas(scale) {    const canvas = document.createElement("canvas");    canvas.width = width * scale;    canvas.height = height * scale;    canvas.getContext("2d").scale(scale, scale);    return canvas;}// 生成快照function convertToImage(container, options = {}) {    // 設置放大倍數    const scale = window.devicePixelRatio;    // 建立用於繪製的基礎canvas畫布    const canvas = createBaseCanvas(scale);    // 傳入節點原始寬高    const width = container.offsetWidth;    const height = container.offsetHeight;    // html2canvas配置項    const ops = {        scale,        width,        height,        canvas,        useCORS: true,        allowTaint: false,        ...options    };    return html2canvas(container, ops).then(canvas => {        const imageEl = Canvas2Image.convertToPNG(canvas, canvas.width, canvas.height);        return imageEl;    });}

5.2.4  關閉抗鋸齒

imageSmoothingEnabled 是 Canvas 2D API 用來設置圖片是否平滑的屬性,true表示圖片平滑(默認值),false表示關閉 canvas 抗鋸齒。

默認狀況下,canvas 的抗鋸齒是開啓的,能夠經過關閉抗鋸齒來實現必定程度上的圖像銳化,提升線條邊緣的清晰度。

據此,咱們將以上createBaseCanvas方法升級爲:

// 建立用於繪製的基礎canvas畫布function createBaseCanvas(scale) {    const canvas = document.createElement("canvas");    canvas.width = width * scale;    canvas.height = height * scale;    const context = canvas.getContext("2d");    // 關閉抗鋸齒    context.mozImageSmoothingEnabled = false;    context.webkitImageSmoothingEnabled = false;    context.msImageSmoothingEnabled = false;    context.imageSmoothingEnabled = false;    context.scale(scale, scale);    return canvas;}

5.2.5  銳化特定元素

受到 canvas 畫布放縮的啓發,咱們對特定的 DOM 元素也能夠採用相似的優化操做,即設置待優化元素寬高設置爲 2 倍或devicePixelRatio倍,而後經過 css 縮放的方式控制其展現大小不變。

scale

例如,對於必須用背景圖background的元素,採用如下方式可明顯提升快照的清晰度:

.box {    background: url(/path/to/image) no-repeat;    width: 100px;    height: 100px;    transform: scale(0.5);    transform-origin: 0 0;}

其中,widthheight爲實際顯示寬高的 2 倍值,經過transform: scale(0.5)實現了元素大小的縮放,transform-origin根據實際狀況設置。

5.3 轉換效率

快照的轉換效率直接關係到用戶的等待時長。咱們能夠在目標節點傳入階段和快照導出兩個階段對其進行必定優化。

5.3.1 傳入階段

傳入節點的視圖信息越精簡,生成快照處理的計算量就越小

如下方式適用於傳入視圖信息「瘦身」:

  • 減小 DOM 規模,下降html2canvas遞歸遍歷的計算量。
  • 壓縮圖片素材自己的體積,使用 tinypng 或 ImageOptim 等工具壓縮素材。
  • 若是使用了自定義字體,請使用 fontmin 工具對文字進行按需裁剪,避免動輒數兆的無效資源引入。
  • 傳入合適的scale值以縮放 canvas 畫布(5.2.3節)。一般狀況下 2~3 倍就已經知足通常的場景,沒必要要傳入過大的放大倍數。
  • 在 5.1.2 節中提到的圖片資源轉 blob,可將圖片資源本地化,避免了生成快照時 html2canvas 的二次圖片加載處理,同時所生成的資源連接具有 URL 長度較短等優點。

5.3.2 導出優化

canvas2image提供了多個 API 用於導出圖片信息,上文已有介紹。包括:

  • convertToPNG
  • convertToJPEG
  • convertToGIF
  • convertToBMP

不一樣的導出格式,對於生成快照的文件體積存在較大的影響。一般對於沒有透明度展現要求的圖片素材,可使用jpeg格式的導出。在咱們的相關實踐中,jpeg相比於png甚至可以節約 80% 以上的文件體積。

實際場景中的的圖片導出格式,按業務需求選用便可。

6. 小結

本文基於html2canvascanvas2image,從快照的內容完整性、清晰度和轉換效率等多個方面,介紹了前端頁面生成高質量快照的解決方案。

因爲實際應用的複雜性,以上方案可能沒法覆蓋到每一處具體場景,歡迎你們交流和探討。

7. 參考連接

  • 基於html2canvas實現網頁保存爲圖片及圖片清晰度優化
  • 微信wap頁生成分享海報功能踩坑經驗
  • H5 實現保存圖片的採坑記錄
  • 實現微信H5實現網頁長按保存圖片及識別二維碼
  • MDN: Allowing cross-origin use of images and canvas
相關文章
相關標籤/搜索