背景
近期在项目中遇到了一个需求,需求大致就是把文字,贴纸和背景图合成一张图片产物。这个需求最后就用到了dom-to-image。过程中也踩了一些坑,本文就分析一下这个库的原理,及这个库的一些坑。
基本原理
这个库需要做的事情就是把一段dom变为一个图片,我们来看一下最核心的源码中的一个函数makeSvgDataUri
。
function makeSvgDataUri(node, width, height) {
return Promise.resolve(node)
.then(function (node) {
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>';
})
.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;
});
}
这个函数就是一个node节点变为图片的一个过程,大致经历了以下过程
- 将html node转化为xml,设定命名空间
- 用foreignObject 包裹xml
- 把内容变为了svg
- svg变为base64的图片
大致就是利用了svg作为了dom变为图片的桥梁
图片与字体的处理
了解了基本原理之后我们知道,其实html和css 转为svg再到图片其实是没有什么问题和难度,但是有两个问题我们必须解决,一个就是dom中的图片,另一个就是字体,而这两个问题在实际项目中也都是踩了一些坑。
图片问题
先要明白为什么图片是需要单独处理的,在基本原理中我们提到了dom最后会变成base64的图片,但实际上我们知道我们的dom中如果是有图片资源的话,浏览器是需要通过请求去获取这个远程资源的。解决这个问题的方案就是提前去下载这个图片资源,并且将其转化为base64,插入到dom中。
第一步下载图片,这个库的处理就是直接get请求下载图片,在实际工程中需要主要一个点就是资源的ajax跨域下载的问题,建议如果是cdn的素材资源,可以通过工程后端做一个请求转发。
第二步就是将data变为base64,这个没有什么难度,可以看一下这个链接https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs,只要确定了mediatype即可,用字符串拼接的方式实现。
踩坑记录,dom2image的库中也确实实现了上面的过程,但是在图片的判断上有些问题,dom中可能存在的图片有,img标签,背景图和边框图,但实际上库中只在属性中遍历了前两种,border-iamge属性没有考虑到,需要自己修复一下。
字体问题
问题来源,我们目前使用的字体含iconfont,无非两种写法
- css @font定义font-family
var fontFace = new FontFace(family, source, descriptors);
第二种相对较少,而远程的字体同样有图片远程资源的问题,这个的解决方案是与图片一致的,不再赘述。我们来看一下库中如何处理字体的问题。
源码中的处理
`
function embedFonts(node) {
return fontFaces.resolveAll()
.then(function (cssText) {
var styleNode = document.createElement('style');
node.appendChild(styleNode);
styleNode.appendChild(document.createTextNode(cssText));
return node;
})
`
function readAll() {
return Promise.resolve(util.asArray(document.styleSheets))
.then(getCssRules)
.then(selectWebFontRules)
.then(function (rules) {
return rules.map(newWebFont);
});
function selectWebFontRules(cssRules) {
return cssRules
.filter(function (rule) {
return rule.type === CSSRule.FONT_FACE_RULE;
})
.filter(function (rule) {
return inliner.shouldProcess(rule.style.getPropertyValue('src'));
});
}
function getCssRules(styleSheets) {
var cssRules = [];
styleSheets.forEach(function (sheet) {
try {
util.asArray(sheet.cssRules || []).forEach(cssRules.push.bind(cssRules));
} catch (e) {
console.log('Error while reading CSS rules from ' + sheet.href, e.toString());
}
});
return cssRules;
}
function newWebFont(webFontRule) {
return {
resolve: function resolve() {
var baseUrl = (webFontRule.parentStyleSheet || {}).href;
return inliner.inlineAll(webFontRule.cssText, baseUrl);
},
src: function () {
return webFontRule.style.getPropertyValue('src');
}
};
}
}
}
难点就是去找到与字体相关的css,然后只要新建style标签插入对应的字体定义,只要保证这个style也在最后的svg里,那么字体展示就不会有问题。
主要用了CSSRule.cssText
,字体相关的css就是在CSSRule.FONT_FACE_RULE
中,这部分需要遍历styleSheets。
这里也是有一些坑的,主要是两个问题
- 用FontFace的定义的字体,并不能用上面的方法生效
- 工程项目的styleSheets较大,遍历性能较差。
问了解决这两个问题,建议改造一下库的处理,删去原有的处理,自己手动给dom2image传入需要的字体文件,或者是直接在需要转化的dom中自己插入字体style
总结
domtoimage的方案,确实是有很多的坑,但也不是完全不能在工程中使用,使用前还是要注意解决一下他自身的一些问题。最后base64的这个方案还是有些问题,来源于base64在处理大图片,大资源的时候会有一定的性能问题,这个问题目前除了避免大资源外,暂时还没有想到更好的方案。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。