前言
很多时候,在业务中,我们可能要把整个网页,或者网页中的一部分转化成图片,从而达到一些业务需求,或者营销目的。在以往,我们通常会用html2canvas来完成这个操作。虽然可以满足大部分的需求,但是还是有些不足,比如 计算精度缺失,box-shadow的支持不好 等。
为了尝试解决这些问题,我们可以尝试使用svg来绘制html。对于svg是一种可扩展标记语言,在转化的过程中,就需要使用到 <foreignObject>
这个svg元素。 <foreignObject>
允许包含不同的XML命名空间,在浏览器的上下文中,很可能是XHTML或者HTML。
<foreignObject>
介绍
关于 <foreignObject>
的简单用法,可以这样简单描述一下
const html = document.createElement('html');
const str = `
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#box {
height: 200px;
width: 200px;
background: red;
}
</style>
</head>
<body>
<div id="div">
<div id="box"></div>
</div>
</body>
`;
html.innerHTML = str;
// 序列化html片段,转化成 foreignObject 可以使用的格式
const xhtml = new XMLSerializer().serializeToString(html);
return (
`
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<foreignObject x="0" y="0" width="300" height="300">
${xhtml}
</foreignObject>
</svg>
`
)
复制代码
生成的svg图片如下
准备工作
现实场景中的页面肯定没有上面的那么简单,写个稍微复杂的页面,来用作示例。
[html文件]
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://juejin.cn/post/main.css">
<style>
* {
padding: 0;
margin: 0;
}
img {
vertical-align: middle;
}
#container {
width: 300px;
height: 500px;
margin: 48px 0 0 48px;
box-shadow: 0 0 16px rgba(0, 0, 0, .4);
border-radius: 6px;
padding: 24px 16px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="container">
<div class="img-header">
<img
width="100"
height="100"
src="https://huijian-mini-oss.zhidianfan.com/img/logo-alt.png"
>
</div>
<div class="content">
<div class="name">
懒狗小前端
</div>
<div class="section">
阿宾的高中成绩并不理想,但是毕竟也给他考上了台北附近的一所私立专校。开学之前,
他考虑到每天通车恐怕太过于辛苦,于是就在学校旁边租了间学生房,
只有周末假日,才回家看看妈妈。
</div>
</div>
</div>
<div style="margin-top: 16px;text-align: center;width: 300px;padding-left: 48px;">
<button id="btn">我要截图啦</button>
</div>
<script src="https://juejin.cn/post/main.js"></script>
</body>
</html>
复制代码
[main.css]
.img-header {
text-align: center;
}
.name {
text-align: center;
margin-top: 24px;
font-size: 20px;
line-height: 28px;
font-weight: bold;
color: #333;
transform: rotate(-30deg);
}
.section {
font-size: 14px;
line-height: 22px;
margin-top: 24px;
text-indent: 2rem;
background: #00a0e9;
padding: 12px;
border-radius: 6px;
color: #fff;
}
复制代码
[main.js]
const div = document.querySelector('.section');
let txt = div.innerText;
txt += `巴啦啦小魔仙变身了哇。`
div.innerText = txt;
复制代码
页面展示如下:
开始
一、
首先,和html2canvas一样,如果图片是外链,我们需要转化成base64才能在svg中正常展示。
const replaceImgSrc = function (doc) {
return new Promise(resolve => {
// 获取所有的图片
const imgs = Array.from(doc.getElementsByTagName('img'));
const length = imgs.filter(v => v.src).length;
// 完成的img计数器
let num = 0;
// 使用canvas把img转化成base64
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 把img转为base64,替换img的src
const replaceSrc = function (img) {
const image = new Image();
image.setAttribute('crossOrigin', 'anonymous');
image.src = img.src;
image.onload = function () {
canvas.width = this.width;
canvas.height = this.height
ctx.drawImage(this, 0, 0);
const data = canvas.toDataURL('image/png');
img.src = data;
num++;
checkReplace();
}
}
// 检查是否所有图片加载完成,替换完成。
const checkReplace = function () {
if (num === length) {
resolve(doc);
}
}
// 遍历imgs
for (let img of imgs) {
replaceSrc(img);
}
})
}
复制代码
二、
然后,<foreignObject>
中也不支持外链的css,我们需要把外链的css转换成inline的style。
// 读取外链的css
const getOuterLink = function (doc) {
// 找到外链
const head = doc.querySelector('head');
const links = Array.from(doc.getElementsByTagName('link'));
// 读取单个css文件
const getCss = function (l) {
const url = l.href;
return fetch(url)
.then(res => {
head.removeChild(l);
return res.text();
});
}
// 读取全部的css文件
const getAllCss = function () {
const arr = [];
for (let l of links) {
arr.push(getCss(l));
}
return Promise.all(arr)
.then(res => {
for (let s of res) {
const style = doc.createElement('style');
style.innerHTML = s;
head.appendChild(style);
}
});
}
return getAllCss();
}
复制代码
三、
因为在修改的同时,我们不能修改原文档中的内容,所以在修改src和css之前,我们得新创建一个document,用来进行这些操作,之所以放在这,因为写在第一步,也没啥意义,因为写最前面也不知道要用来干啥
// 创建一个document副本来进行css img的替换操作
const createDocument = function () {
// 获取html中的内容
const html = document.getElementsByTagName('html')[0];
const htmlFragment = html.innerHTML;
// 创建一个新的document,注入html片段
const newDocument = document.implementation.createHTMLDocument('photo');
newDocument.documentElement.innerHTML = htmlFragment;
return newDocument;
}
复制代码
四、
最后以后一步,就是生成svg,然后转换成图片,在页面中显示出来。
// 把svg转换成blob或者base64
const getUrlForSvg = function (svg, useBlob) {
if (useBlob) {
return URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }))
}
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
}
// 绘制图片
const render = function (html) {
const xhtml = new XMLSerializer().serializeToString(html);
const str = (
`
<svg viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
<foreignObject x="0" y="0" width="600" height="600">
${xhtml}
</foreignObject>
</svg>
`
);
const src = getUrlForSvg(str);
const img = document.getElementById('img');
img.width = 600;
img.height = 600;
img.src = src;
};
复制代码
五
整个运行流程如下
const takePhoto = function () {
const doc = createDocument();
const html = doc.getElementsByTagName('html')[0];
replaceImgSrc(doc)
.then(_ => {
return getOuterLink(doc)
})
.then(_ => {
render(html);
})
.catch(err => {
console.log('err', err)
});
};
btn.onclick = takePhoto;
复制代码
六
生成的图片和我们页面中相比,不能说是非常相似,可以说是一模一样。
结语
写到这里已经是凌晨2点多了,边写边看欧洲杯,看到德国被淘汰也是有点郁闷。看英国赢球了跟看雄鹿赢球了是一个感觉,吃了屎一样难受。
言归正传,首先很感谢 玄魂 大佬,这篇文章也是看了大佬的公众号文章,才边摸索,边写出来的。原文链接贴一贴
[mp.weixin.qq.com/s/ugrBaCIWY…]
大家可以关注下大佬的公众号,都是好东西。而且原文中的也比较详细,我这里的代码,没有对css文件做更深层的处理,因为外链的css中也会有@import,也会有外链,我是水平没到,大家看看原文应该能了解得更透彻一点。
其次,写完这篇也挺有成就感的,一年前就欠下的技术债,终于还上了,说明我还是在努力进步的,也不是很混。文章可能还有些瑕疵,我也有些不懂的地方,还要再请教下大佬,如果有问题,后续也会更改,我自己也是一知半解,只能尽量不误人子弟哇。两天混了2篇,准备直接休息一礼拜继续看我的flutter去了。
3点的比赛也没啥好看的,整把炉石,然后迎接新的一天~