html2canvas 1.4.1 在 iOS Safari 中生成图片卡住的问题排查与修复
问题现象
在 iOS Safari / 移动端 vConsole 环境中,使用 html2canvas 1.4.1 时无法成功生成图片。
最初看到的报错类似:
text
TypeError: undefined is not a function
(near '...t.match(...)...')
同时 html2canvas 日志停在:
text
Starting document clone...
之后没有进入 resolved,也没有正常输出 canvas。
最小复现
为了判断是脚本问题还是业务 DOM 问题,先做了一个最小 DOM 测试:
js
function testHtml2canvasCore(scriptUrl) {
scriptUrl = scriptUrl || "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js";
$.getScript(scriptUrl + "?v=iframefix").done(function () {
var demo = document.createElement("div");
demo.id = "html2canvasCoreDemo";
demo.style.cssText = [
"position:fixed",
"left:10px",
"top:10px",
"width:120px",
"height:60px",
"background:#fff",
"color:#111",
"font-size:14px",
"z-index:999999"
].join(";");
demo.innerHTML = "core demo";
document.body.appendChild(demo);
html2canvas(demo, {
scale: 1,
useCORS: true,
logging: true
}).then(function (canvas) {
console.log("core demo ok", canvas.width, canvas.height);
}).catch(function (err) {
console.error("core demo error", err);
});
});
}
测试结果:
- 使用 1.4.1:最小 DOM 也卡住。
这说明问题第一层不在业务 DOM,而在 html2canvas 1.4.1 自身的 document clone / iframe 流程。
根因定位
html2canvas 1.4.1 在生成图片前会 clone 当前 document 到一个隐藏 iframe 中,然后等待 iframe 加载完成。
相关的 minified 代码原本类似:
js
In=function(B){
return new Promise(function(e,A){
var t=B.contentWindow;
if(!t)return A("No window assigned for iframe");
var r=t.document;
t.onload=B.onload=function(){
t.onload=B.onload=null;
var A=setInterval(function(){
0<r.body.childNodes.length&&"complete"===r.readyState&&(clearInterval(A),e(B))
},50)
}
})
}
关键问题在这里:
js
t.onload = B.onload = function () { ... }
原逻辑必须等 iframe 的 load 事件触发后,才开始轮询 iframe document 是否准备好。
在当前 iOS Safari 环境中,这个隐藏 iframe 的 load 事件没有稳定触发,导致 Promise 一直不 resolve,于是 html2canvas 日志停在:
text
Starting document clone...
这也是为什么业务 DOM 和极简 DOM 都失败。
修复方案
修复思路是:不要完全依赖 iframe 的 load 事件。注册 onload 的同时,立即启动一次相同的检查逻辑。
补丁后的 In 函数:
js
In=function(B){
return new Promise(function(e,A){
var t=B.contentWindow;
if(!t)return A("No window assigned for iframe");
var r=t.document,n=function(){
t.onload=B.onload=null;
var A=setInterval(function(){
r.body&&0<r.body.childNodes.length&&("complete"===r.readyState||"interactive"===r.readyState)&&(clearInterval(A),e(B))
},50)
};
t.onload=B.onload=n,setTimeout(n,0)
})
}
相比原逻辑,改动点有三个:
-
把
onload回调提取成n。 -
保留原来的
t.onload = B.onload = n。 -
增加
setTimeout(n, 0),避免 iframeload事件不触发时永久卡住。
同时轮询条件也做了兼容:
js
r.body &&
0 < r.body.childNodes.length &&
("complete" === r.readyState || "interactive" === r.readyState)
这样 iframe document 已经进入 interactive 状态时也可以继续。
验证方式
在移动端 vConsole 中验证最小 DOM:
js
testHtml2canvas141()
成功后再验证当前业务 DOM:
js
testHtml2canvasCurrentDom()
最终结果:
-
最小 DOM 可以生成 canvas。
-
当前业务 DOM 可以继续生成图片。
-
1.4.1 的样式表现优于旧 plugin 版本。
经验总结
这次问题容易被误判成业务 DOM 或 CSS 问题,因为 html2canvas 的报错栈经过压缩后很难直接读,vConsole 里显示的 t.match 也不一定是根因。
更有效的排查路径是:
-
先确认实际加载的是哪一个 html2canvas 文件。
-
用极简 DOM 做最小复现。
-
如果极简 DOM 也失败,就优先排查 html2canvas 初始化、clone、iframe、资源加载这些基础流程。
-
观察 html2canvas 日志停在哪一步。
本次的关键判断点是:极简 DOM 也卡在 Starting document clone...,所以问题不在业务 DOM,而在 1.4.1 的隐藏 iframe clone 等待逻辑。