微信wap頁生成分享海報(bào)功能踩坑經(jīng)驗(yàn)

本文適用人群

  1. 需要在微信wap頁開發(fā)分享海報(bào)功能的前端程序員們
  2. 想要了解html2canvas庫的吃瓜群眾
  3. 掙扎在html2canvas庫中的開發(fā)者們

背景

產(chǎn)品大大的需求: 做一個(gè)生成海報(bào)的功能。在微信wap頁使用姜盈。把html元素生成一張宣傳海報(bào),方便用戶分享出去。


image

注意點(diǎn):

  1. 在使用海報(bào)的地方啤覆,并不會(huì)顯示海報(bào)來源的html郑什,只顯示海報(bào)的圖片。 所以要求了html節(jié)點(diǎn)是隱藏的脖卖。
  2. 海報(bào)的數(shù)據(jù)來源于當(dāng)前登錄用戶乒省、當(dāng)前課程有關(guān)(根據(jù)這些生成二維碼),課程圖片也是動(dòng)態(tài)的畦木。 所以圖片不能寫死保存在文件夾下袖扛,而是放在nos上。

思路

首先梳理需求十籍。所謂的生成海報(bào)蛆封,簡化后其實(shí)就是: 根據(jù)html元素生成圖片。其中妓雾,html元素包括圖片(背景圖娶吞、課程圖片、二維碼)和文字(推廣語械姻、課程名)。

那么第一個(gè)問題就是机断,由前端實(shí)現(xiàn)還是后端實(shí)現(xiàn)楷拳?

首先看后端實(shí)現(xiàn):云課堂工程的后端使用的語言是java,java是有比較成熟的庫來實(shí)現(xiàn)html轉(zhuǎn)成image這個(gè)功能的吏奸,并且我們之前也實(shí)現(xiàn)過一個(gè)類似的功能。但是后端實(shí)現(xiàn)這個(gè)功能有一些不足: 1. 比較慢,平均生成一張圖需要2-10s 固蚤,2. 不適合需要實(shí)時(shí)的場景联贩。而我們的需求明顯是不能接受等待這么久的。3. 會(huì)產(chǎn)生白圖或者黑圖或者圖片不完整的情況泊碑。

如果由前端實(shí)現(xiàn)呢坤按,我們很容易想到用canvas,而在wap頁馒过,瀏覽器對(duì)canvas的支持還是比較好的臭脓。另外,這么有挑戰(zhàn)性的任務(wù)腹忽,身為一個(gè)前端来累,怎么能不試一試呢?

所以經(jīng)過權(quán)衡窘奏,最終的決定是由前端來實(shí)現(xiàn)嘹锁。

下一個(gè)問題,怎么實(shí)現(xiàn)着裹?

實(shí)現(xiàn)

于是開始我調(diào)研用canvas實(shí)現(xiàn)html轉(zhuǎn)圖片领猾。

方案一

在張鑫旭大大的博客里發(fā)現(xiàn)了SVG <foreignObject>簡介與截圖等應(yīng)用 這篇文章。用svg的foreignObject實(shí)現(xiàn)截圖功能。文章的主要思路是:

  1. 先寫一個(gè)svg瘤运,用foreignObject包圍要寫的html元素窍霞。

  2. 用canvas的drawImage方法把svg轉(zhuǎn)成canvas。

  3. 用canvas的toDataURL方法把canvas轉(zhuǎn)成圖片拯坟。

簡單說來但金,就是: html-->svg-->canvas-->img。

嗯郁季,看起來很簡單冷溃。看完DEMO后梦裂,年幼無知的我一頭就扎進(jìn)了實(shí)現(xiàn)業(yè)務(wù)邏輯的大坑里似枕,開始按照這個(gè)思路實(shí)現(xiàn)第一版的生成海報(bào)功能。

第一版是簡化版年柠,復(fù)制了下zxx的DEMO凿歼,圖片用本地圖片代替,寫完后在本地用Chrome嘗試沒有問題冗恨。
然鵝答憔,事情怎么會(huì)這么簡單……當(dāng)把圖片換成nos圖片后——


chrome報(bào)錯(cuò).jpg

報(bào)錯(cuò)"Tainted canvases may not be exported",這是因?yàn)椋?當(dāng)引用外域的圖片掀抹,并且該圖片并沒有CORS認(rèn)證時(shí)虐拓,canvas被“污染了”,而被污染的canvas不能使用toBlob(), toDataURL(), getImageData()方法傲武。見CORS enabled image

Although you can use images without CORS approval in your canvas, doing so taints the canvas. Once a canvas has been tainted, you can no longer pull data back out of the canvas. For example, you can no longer use the canvas toBlob(), toDataURL(), or getImageData() methods; doing so will throw a security error.

解決方法很簡單蓉驹,給img設(shè)置crossOrigin屬性為anonymous

<img crossOrigin="anonymous" >

或者在js中指定

if (dom.tagName.toLowerCase() == 'img') {
  dom.crossOrigin = "anonymous";
}

同時(shí)img的服務(wù)器也要有正確的Access-Control-Allow-Origin 響應(yīng)頭即可。

到此Chrome的問題解決了揪利,然而在Safari下……

safari報(bào)錯(cuò)

一切就是這么殘忍态兴。果斷給我報(bào)錯(cuò)了。問題出在最后一步: canvas-->img土童,報(bào)錯(cuò)的是canvas.toDataURL方法诗茎。這是我遇到的第一個(gè)坑:

svg的foreignObject里面有外域的圖片時(shí),盡管指定了crossOrigin献汗,在safari中敢订,canvas.toDataURL方法仍然會(huì)有安全性問題。

這個(gè)坑應(yīng)該和CORS有關(guān)罢吃,但是搜索了一番楚午,沒有找到更深層的原因和解決方案。(有人知道的話歡迎告訴我)

報(bào)錯(cuò)導(dǎo)致畫不出來圖尿招,這也就意味著矾柜,這個(gè)方案拒絕了Safari阱驾。而拒絕了Safari,就等于拒絕了所有的蘋果手機(jī)……如果你去跟策劃小哥哥說:我們能不能不兼容iphone怪蔑,相信我里覆,他們一定會(huì)提著刀來見你的。

本方案缆瓣,卒喧枷。

方案二

看來foreignObject的路子行不通了,我只好繼續(xù)尋覓~

就在這時(shí)弓坞,我發(fā)現(xiàn)了一個(gè)js庫: html2canvas隧甚。

那么下面給大家介紹一下html2canvas這個(gè)庫。

html2canvas庫

一個(gè)用js進(jìn)行“截屏”操作的庫渡冻。因?yàn)槭腔赿om元素的戚扳,并不是真的截屏,所以可能會(huì)有一些不準(zhǔn)確族吻。

This script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.

使用方式:

html2canvas(document.body, {
  onrendered: function(canvas) {
    document.body.appendChild(canvas);
  }
});

文檔地址: https://html2canvas.hertzen.com/documentation.html

原理

html2canvas的基本原理是帽借,把dom樹拉出來,挨個(gè)畫到canvas上超歌。比如div就取backgroud-color等宜雀,畫一個(gè)長方形。最后返回這個(gè)畫布握础。

The script renders the current page as a canvas image, by reading the DOM and the different styles applied to the elements.

所以我們基本可以猜想到html2canvas畫圖的整個(gè)流程:

  1. 遞歸處理每個(gè)節(jié)點(diǎn),記錄這個(gè)節(jié)點(diǎn)應(yīng)該怎么畫悴品。(比如div就畫邊框和背景禀综,文字就畫文字等等)
  2. 考慮節(jié)點(diǎn)的層級(jí)問題。比如z-index苔严,float, position等樣式的影響定枷。
  3. 從低層級(jí)開始畫到canvas上,逐漸向上畫届氢。層級(jí)高的覆蓋層級(jí)低的欠窒。

試了一下DEMO,基本可行退子。于是岖妄,就是你了!(畢竟調(diào)研+開發(fā)只有兩天時(shí)間寂祥,我不想再尋找了)

踩坑經(jīng)驗(yàn)及解決方案

決定了使用html2canvas后荐虐,還要再?zèng)Q定一個(gè)問題: 用哪個(gè)版本的?

目前html2canvas有最新版5.0beta4和正式版4.1丸凭。5.0beta版使用了promise等新技術(shù)福扬;4.1作為正式版腕铸,社區(qū)里有更多的解決方案。而我铛碑,兩個(gè)版本都試了……至于為什么狠裹,后面會(huì)告訴你們……

好,到這里方向是確定了汽烦,但是道路是艱難的涛菠,下面我分享一下在使用過程中遇到的問題們。

問題一刹缝,怎么畫出不顯示的元素

從文章最開始的需求背景碗暗,大家應(yīng)該就知道了,我們的html元素是隱藏的梢夯,頁面上并不會(huì)顯示言疗,只需要顯示根據(jù)html元素畫出來的圖片。然而颂砸,html2canvas本質(zhì)上是一個(gè)“截屏”工具噪奄,屏幕上有什么,它就畫什么人乓,而隱藏的元素勤篮,它不會(huì)畫出來。

怎么解決呢色罚?別急碰缔,本寶寶分別告訴大家5.0版的和4.1版的

5.0版

5.0版本,傳入的options里面戳护,有一個(gè)onclone參數(shù)金抡,這個(gè)參數(shù)是做什么的呢?看一下源碼腌且,在這個(gè)版本中梗肝,會(huì)先復(fù)制傳入的dom元素,然后再畫出復(fù)制后的dom元素铺董,onclone就是復(fù)制之后的回調(diào)巫击。所以我們?cè)赾lone dom后,給clone的dom節(jié)點(diǎn)加上display:block精续,就可以解決畫不出display:none的問題了坝锰。

html2canvas(p, {
  useCORS: true,
  onrendered: function(canvas) {
    $img.src = canvas.toDataURL('image/png');
  },
  onclone: function(doc){
    hiddenDiv = doc.getElementById('parent');
    hiddenDiv.style.display = 'block'; //  這里,設(shè)置display為block
  }
});

4.1版

4.1版本的沒有onclone回調(diào)了驻右,稍微有點(diǎn)麻煩什黑,因?yàn)槲覀冎荒芨脑创a了。

源碼中有一個(gè)isElementVisible方法堪夭,是判斷元素是否顯示的愕把。那么我們修改這個(gè)方法為:

function isElementVisible(element) {
    return (getCSS(element, 'display') !== "none");
    // return (getCSS(element, 'display') !== "none" && getCSS(element, 'visibility') !== "hidden" && !element.hasAttribute("data-html2canvas-ignore"));
  }

配合父元素的類:

.parent{
    visibility: hidden;
    position:fixed;
    z-index: -1;
    top:0;    
}

這樣就可以達(dá)到顯示隱藏的元素的目的了拣凹。

問題二,圖片模糊怎么辦

最初恨豁,我們發(fā)現(xiàn)嚣镜,生成的圖片在Mac上看總是糊的。如下圖:

blur.jpg

canvas模糊的話橘蜜,很容易想到像素點(diǎn)的原因菊匿。

于是有思路:我們嘗試把canvas的width和height放大。

給canvas的寬高比canvas樣式的寬高大计福,比如把200x200的畫縮放到100x100跌捆,這樣畫出來的圖點(diǎn)就更多,清晰度就更好象颖。

放大多少呢佩厚?——根據(jù)window.devicePixelRatio來。

5.0

5.0版本支持自定義canvas并傳進(jìn)去说订。所以我們?cè)谡{(diào)用html2canvas的時(shí)候抄瓦,先創(chuàng)建好一個(gè)尺寸合適的canvas,作為參數(shù)傳進(jìn)去陶冷。

var p = document.getElementById(domId);
var scaleBy = backingScale();
var box = window.getComputedStyle(p);
var w = parsePixelValue(box.width, 10);
var h = parsePixelValue(box.height, 10);
var canvas = document.createElement('canvas');

function backingScale () {
    if (window.devicePixelRatio && window.devicePixelRatio > 1) {
        return window.devicePixelRatio;
    }
    return 1;
};
function parsePixelValue(value) {
    return parseInt(value, 10);
};
// 就是這里了
canvas.width = w * scaleBy;
canvas.height = h * scaleBy;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';

var context = canvas.getContext('2d');
context.scale(scaleBy, scaleBy);

html2canvas(p, {
    useCORS: true,
    canvas: canvas,  // 把canvas傳進(jìn)去
    onrendered: function(canvas) {
        cb(canvas.toDataURL('image/png', 1));
    },
    logging: true,
    onclone: function(doc) {
        hiddenDiv = doc.getElementById(domId);
        hiddenDiv.style.display = 'block';
    }
});

4.1

4.1版本盡管也支持自定義傳canvas進(jìn)去钙姊,但是在最后畫圖的時(shí)候,會(huì)改寫canvas的width和height埂伦,

return function(parsedData, options, document, queue, _html2canvas) {
...
canvas.width = canvas.style.width =  options.width || zStack.ctx.width;
canvas.height = canvas.style.height = options.height || zStack.ctx.height;
}

所以想要像用5.0版本一樣傳canvas參數(shù)進(jìn)去的話煞额,就要失望了,還是會(huì)一樣的糊(廢話沾谜,寬高都被改了立镶,我還傳進(jìn)去干啥)。

所以类早,我們又要改源碼了……翻到源碼最后,首先加一個(gè)backingScale方法嗜逻,根據(jù)window.devicePixelRatio計(jì)算縮放倍數(shù)涩僻。然后重寫canvas的width和style.width

function backingScale () {
      if (window.devicePixelRatio && window.devicePixelRatio > 1) {
          return window.devicePixelRatio;
      }
  };
return function(parsedData, options, document, queue, _html2canvas) {
...
     // 改成下面的
    canvas.width = Math.ceil(options.width ||zStack.ctx.width)*scaleBy;
    canvas.height = Math.ceil(options.height || zStack.ctx.height)*scaleBy;
    canvas.style.width =  options.width || zStack.ctx.width;
    canvas.style.height = options.height || zStack.ctx.height;
...
if (options.elements.length === 1) {
      if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
        // 如果傳入的element是一個(gè)dom元素的話,把圖片里面的這個(gè)dom元素切出來栈顷,否則可能會(huì)有白邊逆日。
        bounds = _html2canvas.Util.Bounds(options.elements[0]);
        newCanvas = document.createElement('canvas');
        // 這兩句是原來的,注釋掉
        // newCanvas.width = Math.ceil(bounds.width);
        // newCanvas.height = Math.ceil(bounds.height);
        // 改成下面的
        newCanvas.width = bounds.width*scaleBy;
        newCanvas.height = bounds.height*scaleBy;
        newCanvas.style.width = bounds.width+ 'px';
        newCanvas.style.height = bounds.height+'px';

        newctx = newCanvas.getContext("2d");
         // 原來的
         // newctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
        // 同樣改成下面的
        newctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, newCanvas.width, newCanvas.height);        
       
        // newctx.scale(4, 4);
        canvas = null;
        return newCanvas;
      }
    }
}

這樣之后萄凤,圖片顯然清晰了許多:

qingxide.jpg

問題三室抽,圖片跨域和CDN緩存導(dǎo)致報(bào)錯(cuò)

首先,圖片跨域的問題靡努,可以通過useCORS這個(gè)參數(shù)來解決坪圾。原理就是上面說過的crossOrigin晓折。

html2canvas(p, {
  useCORS: true,}) //先寫這個(gè)參數(shù)

而關(guān)于CND緩存,是這樣的:因?yàn)槲覀兊膱D片一般都是上傳到CDN上兽泄,而CDN為了更快的響應(yīng)漓概,會(huì)緩存圖片的返回值,而緩存的值是不帶跨域的頭的病梢。因?yàn)闆]有跨域的頭胃珍,所以js請(qǐng)求會(huì)被攔截。而html2canvas中蜓陌,畫圖片之前會(huì)先preload所有的圖片觅彰,這就導(dǎo)致了js報(bào)錯(cuò):圖片跨域(此處沒有圖,相信前端小司機(jī)應(yīng)該見過很多次這個(gè)報(bào)錯(cuò))

解決的思路是這樣的:js中钮热,請(qǐng)求圖片的時(shí)候填抬,給請(qǐng)求的圖片鏈接加上個(gè)時(shí)間戳參數(shù),這樣CDN就映射不到緩存了霉旗,會(huì)回源痴奏,回源到 NOS,而NOS的圖片是帶跨域的頭的厌秒,這樣返回就不會(huì)再報(bào)錯(cuò)读拆。

5.0

修改html2canvas源碼的imageContainer方法,self.image.src = src + (src.split('?')[1] ? '&':'?') + (+new Date());

function ImageContainer(src, cors) {
    this.src = src;
    this.image = new Image();
    var self = this;
    this.tainted = null;
    this.promise = new Promise(function(resolve, reject) {
        self.image.onload = resolve;
        self.image.onerror = reject;
        if (cors) {
            self.image.crossOrigin = "anonymous";
        }
        // 原來是self.image.src = src,改為現(xiàn)在這句
        self.image.src = src + (src.split('?')[1] ? '&':'?') + (+new Date()); 
        if (self.image.complete === true) {
            resolve(self.image);
        }
    });
}

4.1

基本類似鸵闪,修改loadImage方法

loadImage: function( src ) {
      var img, imageObj;
      if ( src && images[src] === undefined ) {
        // 這里檐晕,加時(shí)間戳參數(shù)
        src = src + (src.split('?')[1] ? '&':'?') + (+new Date());
        img = new Image();

另外,其實(shí)并不建議用CDN的圖片蚌讼,因?yàn)檎每吹絼⒃姶ǖ奈恼拢?a target="_blank" rel="nofollow">開發(fā)富文本編輯器的一些經(jīng)驗(yàn)教訓(xùn)辟灰,CDN會(huì)導(dǎo)致回源,因此請(qǐng)求會(huì)更慢返回篡石。

但是芥喇,我上面提到的將含有跨域CDN圖片的DOM節(jié)點(diǎn)渲染成圖片的情況下,向CDN代理節(jié)點(diǎn)請(qǐng)求圖片資源反而會(huì)比我們直接向靜態(tài)資源源站點(diǎn)請(qǐng)求要來的慢凰萨,...CDN代理節(jié)點(diǎn)遇到一個(gè)自己沒有緩存的資源继控,它就會(huì)向靜態(tài)資源的源站點(diǎn)去請(qǐng)求,得到結(jié)果后再轉(zhuǎn)發(fā)給用戶胖眷,這等于說我們這個(gè)帶有時(shí)間戳的圖片URL的請(qǐng)求武通,不但沒能利用的CDN的緩存提速,反而由CDN代理節(jié)點(diǎn)充當(dāng)了一次中介珊搀,這顯然會(huì)增加資源的返回耗時(shí)

所以建議使用NOS的圖片冶忱,比如:http://nos.netease.com/edu-image/AE32703A6908FBD2A57F917F5E93A55D.jpg這種,而不要使用NOS CDN的圖片境析,比如:http://edu-image.nosdn.127.net/AE32703A6908FBD2A57F917F5E93A55D.jpg

到此為止囚枪,demo已經(jīng)沒什么大問題了派诬,剩下的就是應(yīng)用到實(shí)際工程中了。然而眶拉,在實(shí)際使用的過程中千埃,依然遇到了一些問題:

1. 畫出來的圖中,只有background-image一定會(huì)出現(xiàn)忆植,其他圖片有概率不出現(xiàn)

很令人費(fèi)解的情況放可。這個(gè)幾率不算很高,點(diǎn)個(gè)20次大概會(huì)有一次出現(xiàn)這種情況朝刊。最初以為是5.0beta版本的原因耀里,所以換成了用4.1正式版,然而換了版本之后并沒有解決這個(gè)問題拾氓。

v4.1源碼中對(duì)img標(biāo)簽和background-image的處理其實(shí)是類似的冯挎。都是先preload,然后canvas.draw咙鞍。只是background-image多了一個(gè)createPattern步驟房官,用于處理background-repeat屬性。

解決方案是把html中的圖片都寫成background-image续滋,嘗試后沒有再出現(xiàn)顯示不出img的情況翰守。

另外,因?yàn)槭褂昧藇4.1版本疲酌,所以后面的問題和解決方案都是針對(duì)v4.1蜡峰。

2. 有一定幾率截出來的圖是全白屏

經(jīng)過排查,在最后一步的canvas中朗恳,還是有所有圖片的湿颅,但是canvas->newcanvas后,圖片不見了粥诫,所以懷疑是這句產(chǎn)生了問題:

newctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, newCanvas.width, newCanvas.height);

這里稍微解釋下油航,為什么會(huì)有canvas->newcanvas這個(gè)步驟。html2canvas庫中怀浆,畫每個(gè)節(jié)點(diǎn)時(shí) 劝堪,需要定位這個(gè)節(jié)點(diǎn)從canvas的哪里開始的,即需要一個(gè)(x,y)坐標(biāo)揉稚。通過 ele.getBoundingClientRect可以得到這個(gè)元素在client(也就是窗口)的位置,然后從這個(gè)點(diǎn)開始畫出該元素熬粗。如果傳入的element不是body的話搀玖,意思就是我們只想要這個(gè)元素的canvas圖,并不關(guān)心它在窗口中的位置驻呐。所以對(duì)前一步的canvas進(jìn)行一下裁剪灌诅,重新畫到一個(gè)新的canvas上面去芳来。一圖勝千言,下面上圖:

canvas->newcanvas

紅框框表示我們傳進(jìn)去的element猜拾,左邊是canvas(最初的畫布)即舌,右邊是newcanvas(我們需要的畫布)。

回到剛剛的問題挎袜,鎖定了出問題的行之后顽聂,我們看一下原因。

第一盯仪,top,lef有問題紊搪,通過log查看,出問題時(shí)全景,top值會(huì)為-1耀石,所以修改源碼的bound方法,使top和Left始終大于等于0

_html2canvas.Util.Bounds = function (element) {
  var clientRect, bounds = {};

  if (element.getBoundingClientRect){
    clientRect = element.getBoundingClientRect();

    // bounds.top = clientRect.top;
    bounds.top = clientRect.top > 0 ? clientRect.top : 0; // 改成這個(gè)
    bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
    // bounds.left = clientRect.left;
    bounds.left = clientRect.left > 0 ? clientRect.left : 0; // 改成這個(gè)

    bounds.width = element.offsetWidth;
    bounds.height = element.offsetHeight;
  }

  return bounds;
};

第二爸黄,width和height有問題滞伟,可能超過了畫布大小,導(dǎo)致畫出來白圖炕贵。所以同樣修改源代碼(經(jīng)嘗試梆奈,safari下width和height越界會(huì)導(dǎo)致白圖,chrome不會(huì))鲁驶,用Math.ceil取寬高

//canvas.width = (options.width ||zStack.ctx.width)*scaleBy;
//canvas.height = (options.height || zStack.ctx.height)*scaleBy;
// 改成下面
canvas.width = Math.ceil(options.width ||zStack.ctx.width)*scaleBy;
canvas.height = Math.ceil(options.height || zStack.ctx.height)*scaleBy;

3. v4.1的html2canvas對(duì)background-size: contain不兼容鉴裹。

可以理解,畢竟它是解析css屬性值之后畫到canvas上的钥弯。不兼容background-size就導(dǎo)致背景圖只能顯示一部分径荔。

解決方法: 不用background-size。脆霎。总处。

但是不用background-size時(shí),寬高是rem的話睛蛛,背景圖片顯示會(huì)被切斷鹦马。

解決方案: 用px作為單位,根據(jù)背景圖的比例來忆肾,同時(shí)用nos對(duì)背景圖進(jìn)行裁剪

width: 600px;
height: 800px;
background-image: url("http://edu-image.nosdn.127.net/F361D28EEC677CFD44D7C359D24E3DC0.png?imageView&thumbnail=600y800");

這里會(huì)遇到第四個(gè)問題荸频,本來背景圖是600x800的,外面的div寬高應(yīng)該也寫600x800客冈,但是在手機(jī)端寬高大于屏幕尺寸時(shí)旭从,會(huì)導(dǎo)致截圖被切斷(也就是只能畫出來窗口內(nèi)的內(nèi)容),像這樣:
half-img

原因: 在createStack方法里,對(duì)于傳進(jìn)去的元素和悦,生成的canvas的寬高最大值取的窗口寬高退疫。canvas就這么大一點(diǎn),畫出來的內(nèi)容當(dāng)然不全了鸽素。

function createStack(element, parentStack, bounds, transform) {
h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
...
}

解決方案:
因?yàn)楦冈厥菦]有parentStack的褒繁,所以它的ctx的寬度會(huì)取document的寬度。因此把這段代碼改成如下:

function createStack(element, parentStack, bounds, transform) {
    //var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
    // 改成:
    var ctx = h2cRenderContext( (!parentStack) ? (bounds.width + bounds.left): bounds.width, (!parentStack) ? (bounds.height + bounds.top) : bounds.height),

加上bounds.left是因?yàn)閡til.Bounds方法會(huì)計(jì)算每個(gè)元素距離client的左上頂點(diǎn)的距離馍忽,畫canvas的時(shí)候就從這個(gè)點(diǎn)開始畫棒坏。所以canvas的實(shí)際寬度應(yīng)該是父元素寬度+父元素左邊距離窗口的偏移值。

順便說一下舵匾,這里改完了之后俊抵,canvas的模糊問題也解決了……因?yàn)樵?00x800,所以canvas大小也是600x800坐梯,而實(shí)際應(yīng)用中徽诲,在移動(dòng)端顯示的圖片style的寬高并不大,所以看起來不糊了吵血。

4. iphone手機(jī)升級(jí)IOS11后谎替,當(dāng)模板里有文字和《混排時(shí),會(huì)發(fā)生文字位置錯(cuò)亂的現(xiàn)象

話不多說蹋辅,看圖(歡迎大家掃碼買課):


image

本來的課程名應(yīng)該是:Excel從入門到忘記钱贯。這里表現(xiàn)為:文字缺失和文字重疊

經(jīng)過觀察,有以下規(guī)律:

  1. 《后面的文字會(huì)被吞掉1-2個(gè)侦另。
  2. 末尾的》始終顯示不出來秩命,
  3. 倒數(shù)第n個(gè)字會(huì)發(fā)生重疊。

最初懷疑是font-family對(duì)字符集的支持不夠完善導(dǎo)致的褒傅,但是safari下看了下弃锐,html元素的顯示是正確的,只有畫到canvas上之后才錯(cuò)亂殿托。

然后仔細(xì)觀察html模板(我們使用的是regular)霹菊,懷疑是模板渲染之后,《和{xxx}混排導(dǎo)致了這個(gè)問題支竹,于是修改模板為:

<!--原來的-->
<div class="courseName">
《{courseData.productName}》
</div>

<div class="courseName">{'《'+courseData.productName+'》'}</div>

問題解決旋廷。

探究原因,是《導(dǎo)致的嗎礼搁?于是把模板中的《換成%饶碘,無果,仍然錯(cuò)亂馒吴。所以扎运,單個(gè)《是不會(huì)有問題的卑雁,有問題的是《{xxx}》混在一起。

再看下源碼绪囱,html2canvas是怎么畫文字的呢?

取textNode莹捡,然后遍歷textNode中的每一個(gè)字鬼吵,用document.createRange方法創(chuàng)建一個(gè)range,然后設(shè)置這個(gè)range的范圍篮赢,最后用getBoundingClientRect計(jì)算出這個(gè)字符應(yīng)該占的大小齿椅,然后畫到canvas上。關(guān)鍵代碼:

var range = doc.createRange();
range.setStart(textNode, textOffset);
range.setEnd(textNode, textOffset + text.length);
return range.getBoundingClientRect();

而符號(hào)與模板中變量引用混排時(shí)启泣,會(huì)變成:


image

這其實(shí)是3個(gè)textNode涣脚。

打個(gè)log看一下在處理這3個(gè)textNode的時(shí)候,每個(gè)text的left和top值寥茫,發(fā)現(xiàn):

  1. 第一個(gè)textNode遣蚀,即內(nèi)容為《的這個(gè),位置是正確的
  2. 第二個(gè)textNode的開頭纱耻,range.getBoundingClientRect()的結(jié)果是top:0,left:0芭梯,所以沒有出現(xiàn)。導(dǎo)致文字缺失
  3. 第三個(gè)textNode弄喘,即》這個(gè)玖喘,top:0,left:0。所以同樣沒有出現(xiàn)蘑志,導(dǎo)致文字缺失

那么文字重疊是因?yàn)槭裁茨乩勰危幸唤M數(shù)據(jù):

text=w,left=270.39...,right=287.609359
text=微,left=287.59375...

可以觀察到,前一個(gè)字符的right比后一個(gè)字符的left大急但。這應(yīng)該是"w"和"微"導(dǎo)致重疊的原因澎媒。那么為什么會(huì)這樣呢?是升級(jí)后safari的bug羊始?還是createRange和getBoundingClientRect的兼容性問題旱幼?到這里我也不知道了,畢竟IOS11的safari連不上電腦突委,只能真機(jī)打LOG調(diào)試柏卤,難度太大。

總結(jié)

文章寫到這里就要結(jié)束了匀油≡蹈浚總結(jié)一下,本文從我們產(chǎn)品大大的需求開始敌蚜,分析了需求的實(shí)現(xiàn)方式和思路整理桥滨,并進(jìn)行了html轉(zhuǎn)成canvas的調(diào)研:

有兩種方案

  1. 用svg的foreignObject作為中轉(zhuǎn)。缺點(diǎn)是safari下對(duì)外域圖片有安全性報(bào)錯(cuò)。
  2. 使用html2canvas.js庫齐媒。

我們最后使用的是html2canvas庫蒲每。

然后分享了在使用html2canvas過程中,遇到了一些問題和最后的解決方案:

  1. 怎么畫出不顯示的元素
  2. 圖片模糊怎么辦
  3. 圖片跨域和CDN緩存導(dǎo)致報(bào)錯(cuò)

以及脫離了demo環(huán)境喻括,在實(shí)際工程中使用時(shí)候遇到的問題和解決方案:

  1. 畫出來的圖中邀杏,只有background-image一定會(huì)出現(xiàn),其他圖片有概率不出現(xiàn)
  2. 有一定幾率截出來的圖是全白屏
  3. v4.1的html2canvas對(duì)background-size: contain不兼容
  4. iphone手機(jī)升級(jí)IOS11后唬血,當(dāng)模板里有文字和《混排時(shí)望蜡,會(huì)發(fā)生文字位置錯(cuò)亂的現(xiàn)象

希望能對(duì)需要做wap頁生成海報(bào)功能的各位小伙伴,以及因?yàn)楦鞣N原因需要使用html2canvas.js庫并且在踩坑的小司機(jī)們有所幫助拷恨。

(產(chǎn)品大大們脖律,這些坑就是這個(gè)功能周五上線失敗、周六上線失敗腕侄、最后周日才上線的原因……希望你們滿意這個(gè)解釋小泉。嗯。)

參考

CORS enabled image
SVG <foreignObject>簡介與截圖等應(yīng)用
開發(fā)富文本編輯器的一些經(jīng)驗(yàn)教訓(xùn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兜挨,一起剝皮案震驚了整個(gè)濱河市膏孟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拌汇,老刑警劉巖柒桑,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異噪舀,居然都是意外死亡魁淳,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門与倡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來界逛,“玉大人,你說我怎么就攤上這事纺座∠荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵净响,是天一觀的道長少欺。 經(jīng)常有香客問我,道長馋贤,這世上最難降的妖魔是什么赞别? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮配乓,結(jié)果婚禮上仿滔,老公的妹妹穿的比我還像新娘惠毁。我一直安慰自己,他們只是感情好崎页,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布鞠绰。 她就那樣靜靜地躺著,像睡著了一般飒焦。 火紅的嫁衣襯著肌膚如雪洞豁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天荒给,我揣著相機(jī)與錄音,去河邊找鬼刁卜。 笑死志电,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛔趴。 我是一名探鬼主播挑辆,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼孝情!你這毒婦竟也來了鱼蝉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤箫荡,失蹤者是張志新(化名)和其女友劉穎魁亦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羔挡,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洁奈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绞灼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片利术。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖低矮,靈堂內(nèi)的尸體忽然破棺而出印叁,到底是詐尸還是另有隱情,我是刑警寧澤军掂,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布轮蜕,位于F島的核電站,受9級(jí)特大地震影響良姆,放射性物質(zhì)發(fā)生泄漏肠虽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一玛追、第九天 我趴在偏房一處隱蔽的房頂上張望税课。 院中可真熱鬧闲延,春花似錦、人聲如沸韩玩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽找颓。三九已至合愈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間击狮,已是汗流浹背佛析。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彪蓬,地道東北人寸莫。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像档冬,于是被迫代替她去往敵國和親膘茎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容