背景
最近接到需求要做海报,UI给一张背景图然后内容自己填充最后生成一张图片。刚开始做没多想就拿canvas画,一些简单的还好,能够hold的住。遇到元素多的就麻烦了😹。后来发现有dom-to-image的方案,不错,解决的我的问题👍👍。话不多说,下面就来说说他是怎样实现的。
Api介绍
看了看代码,一共887行,代码量不多,每个方法都比较易懂。核心Api有以下几个。
toSvg
toPng
toJpeg
toBlob
toPixelData
顾名思义了,这里就不多说,其实这几个方法实现方式都一样的。第一个方法toSvg将dom节点转成svg,然后其他的方法都是拿到svg后处理为dataUrl再处理为cavnas再操作的。下面就拿 toPng 去展开说明。
代码解读
1. toPng
function toPng(node, options) {
return draw(node, options || {}).then(function (canvas) {
return canvas.toDataURL();
});
}
可见,是通过draw方法将dom节点转为canvas,然后通过canvas获取图片资源。
2.draw
function draw(domNode, options) {
// 将dom节点转为svg
return toSvg(domNode, options)
// 拿到的svg是image data URL,这里进一步通过svg创建图片
.then(util.makeImage)
.then(util.delay(100))
.then(function (image) {
// 通过图片创建canvas并返回
var canvas = newCanvas(domNode);
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
});
// 新建canvas节点,处理dataUrl资源,和options参数
function newCanvas(domNode) {
var canvas = document.createElement("canvas");
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
var ctx = canvas.getContext("2d");
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}
3.toSvg
function toSvg(node, options) {
options = options || {};
copyOptions(options);
return Promise.resolve(node)
.then(function (node) {
// 递归克隆dom节点
return cloneNode(node, options.filter, true);
})
// 嵌入字体,找出所有font-face样式,添加入一个新的style里面
.then(embedFonts)
// 将图片链接转换为dataUrl形式使用
.then(inlineImages)
// 将options里面的一些style放进style里面
.then(applyOptions)
.then(function (clone) {
// 创建svg,将dom节点通过 XMLSerializer().serializeToString() 序列化为字符串
// 然后用 foreignObject 包裹,就能将dom转为svg。
return makeSvgDataUri(
clone,
options.width || util.width(node),
options.height || util.height(node)
);
});
// 处理一些options的样式
function applyOptions(clone) {
if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;
if (options.width) clone.style.width = options.width + "px";
if (options.height) clone.style.height = options.height + "px";
if (options.style)
Object.keys(options.style).forEach(function (property) {
clone.style[property] = options.style[property];
});
return clone;
}
}
4. cloneNode
function cloneNode(node, filter, root) {
if (!root && filter && !filter(node)) return Promise.resolve();
return Promise.resolve(node)
.then(makeNodeCopy)
.then(function (clone) {
return cloneChildren(node, clone, filter);
})
.then(function (clone) {
return processClone(node, clone);
});
function makeNodeCopy(node) {
// 遇到canvas转为image对象
if (node instanceof HTMLCanvasElement)
return util.makeImage(node.toDataURL());
// 克隆第一层
return node.cloneNode(false);
}
// 克隆子节点
function cloneChildren(original, clone, filter) {
var children = original.childNodes;
if (children.length === 0) return Promise.resolve(clone);
return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
function () {
return clone;
}
);
// 递归克隆
function cloneChildrenInOrder(parent, children, filter) {
var done = Promise.resolve();
children.forEach(function (child) {
done = done
.then(function () {
return cloneNode(child, filter);
})
.then(function (childClone) {
if (childClone) parent.appendChild(childClone);
});
});
return done;
}
}
function processClone(original, clone) {
if (!(clone instanceof Element)) return clone;
return Promise.resolve()
.then(cloneStyle)
.then(clonePseudoElements)
.then(copyUserInput)
.then(fixSvg)
.then(function () {
return clone;
});
// 克隆节点上面所有使用的样式。
function cloneStyle() {
// 顺便提提,为什么不用style,因为如果什么样式也没有设置的话,style是光秃秃的
// 而getComputedStyle则能获取到应用在节点上面所有样式
copyStyle(window.getComputedStyle(original), clone.style);
function copyStyle(source, target) {
if (source.cssText) target.cssText = source.cssText;
else copyProperties(source, target);
function copyProperties(source, target) {
util.asArray(source).forEach(function (name) {
target.setProperty(
name,
source.getPropertyValue(name),
source.getPropertyPriority(name)
);
});
}
}
}
// 提取伪类样式,放到css
function clonePseudoElements() {
[":before", ":after"].forEach(function (element) {
clonePseudoElement(element);
});
function clonePseudoElement(element) {
var style = window.getComputedStyle(original, element);
var content = style.getPropertyValue("content");
if (content === "" || content === "none") return;
var className = util.uid();
clone.className = clone.className + " " + className;
var styleElement = document.createElement("style");
styleElement.appendChild(
formatPseudoElementStyle(className, element, style)
);
clone.appendChild(styleElement);
function formatPseudoElementStyle(className, element, style) {
var selector = "." + className + ":" + element;
var cssText = style.cssText
? formatCssText(style)
: formatCssProperties(style);
return document.createTextNode(selector + "{" + cssText + "}");
function formatCssText(style) {
var content = style.getPropertyValue("content");
return style.cssText + " content: " + content + ";";
}
function formatCssProperties(style) {
return util.asArray(style).map(formatProperty).join("; ") + ";";
function formatProperty(name) {
return (
name +
": " +
style.getPropertyValue(name) +
(style.getPropertyPriority(name) ? " !important" : "")
);
}
}
}
}
}
// 处理输入内容
function copyUserInput() {
...
}
// 处理svg,创建命名空间
function fixSvg() {
if (!(clone instanceof SVGElement)) return;
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
...
}
}
}
5.makeSvgDataUri
function makeSvgDataUri(node, width, height) {
return (
Promise.resolve(node)
.then(function (node) {
// 将dom转换为字符串
node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
return new XMLSerializer().serializeToString(node);
})
.then(util.escapeXhtml)
.then(function (xhtml) {
return (
'<foreignObject x="0" y="0" width="100%" height="100%">' +
xhtml +
"</foreignObject>"
);
})
/**
* 顺带提一提
* 不指定xmlns命名空间是不会渲染的
* xmlns="http://www.w3.org/2000/svg"
*/
.then(function (foreignObject) {
return (
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
width +
'" height="' +
height +
'">' +
foreignObject +
"</svg>"
);
})
.then(function (svg) {
return "data:image/svg+xml;charset=utf-8," + svg;
})
);
}
总结
总结下上面几个核心方法流程
递归去克隆dom节点,其中
① 遇到canvas转为image对象
② 提取元素computed样式,并且插到新建的style标签上面,对于":before,:after"这些伪元素,会提取其样式,放到新建样式名中并且插入到新建的style标签中,供所属的节点使用。
③ 处理输入内容和svg。插入字体
① 获取所有样式表并处理为数组,提取包含 rule.type === CSSRule.FONT_FACE_RULE 规则,再提取包含 src 的 rules。
② 下载资源,将资源转为dataUrl并给 src 使用。处理图片
① 图片都处理为dataUrl序列化dom节点为字符串,然后在 foreignObject 嵌入转换好的字符串,foreignObject 能够在 svg 内部嵌入XHTML,再将svg处理为dataUrl数据。
用 canvas 渲染出处理好的 dataUrl 数据。
最后,拿到canvas了,想怎样处理都行了。
参考
gitlab提取了源文件出来,并写了注释,有兴趣可以自己研究下,看看其中的工作方式。