起因
由于 iframe
的特殊性很适合作为一个沙箱环境来使用,除了内嵌网页外,还可以用于预览动态生成 HTML 片段,由于 HTML 片段可能来自用户输入,放在 iframe 是一种比较安的全处理方式。
实际上我们也是这样用的,最近在进行在线网页编辑器的开发,需要所见即所得响应用户的操作,iframe 用于页面实时预览。最终我们会生成一份完整的页面 HTML 文件与一张预览图。这次问题也就是出在这个预览图上。
用户保存时我们需要实时生成页面的预览图,前端出图是个老生常谈的问题了,趁手的工具库不多,这里推荐一个出图库 html-to-image,也是这次故事的主角。
html-to-image
的实现是将 HTML 片段嵌入 svg 的 <foreignObject>
中生成一张 svg 图片后绘制到 canvas 实现的,内嵌 HTML 片段时还需要处理各种依赖样式与静态资源,需要将图片字体一类的资源转成 base64 的内联形式。
在使用 html-to-image
遇到了一个问题,针对 iframe 中 HTML 片段进行前端出图时图片元素是空白的,但父级窗口的图片元素导出又是正常的。所以肯定是 iframe 哪些差异引起的......
instanceof
这里也不卖关子了,排查完发现是 html-to-image
使用了 instanceof
做 node 节点检查导致的,很隐蔽的一个 BUG,分享下 instanceof
在 iframe 中使用的一些注意点。
先来复习下 instanceof,MDN 上的描述如下:
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
很好理解:
function Test() {}
const test = new Test();
console.log(test instanceof Test);
// expected output: true
console.log(test instanceof Object);
// expected output: true
复制代码
使用 instanceof
我们可以判断一个元素是否是个 node 节点。
const node = document.createElement('div');
node.innerText = '扶桑若木';
console.log(node instanceof HTMLDivElement); // true
console.log(node instanceof HTMLElement); // true
console.log(node instanceof Element); // true
// node.childNodes[0] 是个 Text 不是一个 Element 节点
console.log(node.childNodes[0] instanceof Element); // false
复制代码
通过这种方式我们可以区分出普通节点与文本节点,当然这里更推荐使用 nodeType
来区分各种 node 节点,使用 nodeType
就没这一堆问题了~
html-to-image
使用 instanceof
来判断 node 节点类型:
咋一看没啥问题,但在 iframe 就会有问题,看个示例:
<!DOCTYPE html>
<html lang="en">
<body>
<div id="a">A</div>
<iframe></iframe>
<script>
const iframe = document.querySelector('iframe');
iframe.srcdoc = `<body><div id="b">B</div></body>`;
iframe.addEventListener('load', () => {
const node = document.createElement('div');
node.setAttribute('id', 'c');
node.innerText = 'C';
iframe.contentDocument.body.appendChild(node);
});
</script>
</body>
</html>
复制代码
- A 是父级容器节点
- B 是 iframe 中节点
- C 是由父级容器创建插入到 iframe 中的节点
问下面的结果是?
const aNode = document.querySelector('#a');
const bNode = iframe.contentDocument.querySelector('#b');
const cNode = iframe.contentDocument.querySelector('#c');
console.log('aNode instanceof HTMLDivElement', aNode instanceof HTMLDivElement);
console.log('bNode instanceof HTMLDivElement', bNode instanceof HTMLDivElement);
console.log('cNode instanceof HTMLDivElement', cNode instanceof HTMLDivElement);
复制代码
Emmmm.... why ?
很好理解,iframe 和父级窗口环境是独立的,其创建的对象自然是不一样的。
而 C 节点是由父级容器创建的,所以其 HTMLDivElement
还是属于父级容器的。
如果想获取正确的结果则需要用 node 节点自身的宿主环境判断才行。
bNode instanceof bNode.ownerDocument.defaultView.HTMLDivElement
复制代码
避坑指南
- 在 iframe 使用
instanceof
之类的 api 需要关注当前宿主,需要使用 node 所属的宿主属性进行判断。 - 在 iframe 创建资源使用 iframe 自己的 api 进行操作,使用父级容器的 api 会有原型链指向混乱的问题。
- 核心就是 iframe 和父级容器对象是完全独立的,除了传递数据之外不要进行引用数据比较。
over~