本文已开启[新人创作礼]活动,一起开启掘金创作之路。
前言
两周前,我的研发领导告诉我说:小波,咱们产品需要添加一个新功能,对于咱们这种建模的产品(使用鼠标拖拽在画布区域创建svg元素),BOSS需要能够直接选中图形之后,通过复制之后,可以直接在word文档中粘贴出来,这两天实现一下。
我:好的没问题,马上安排!🤓
思路
分解问题
要实现这种需求,就需要对需求做出分解:
- 如何将选中的svg元素单独抽离出来制作成图片?
- 如何才能调用系统剪切板,将在剪切板中写入图片的数据?
实现
- 首先获取到选中的
svg元素,如果没有选中直接将画布中的svg元素全部获取出来 - 获取到的svg元素通过
XMLSerializer对象将DOM树转化为xml字符串 - 将转化出来的xml字符串通过原生API
btoa编码为Base64编码的ASCII 字符串。 - 将编码好的图片通过二进制的数据写入到系统剪切板中,就完成了从浏览器复制svg元素到浏览器的操作啦
好啦,思路有了,开始实现~;
具体实现
这里通过写一个demo,最简短的代码实现这个需求
我们公司中建模工具使用的是mxgraph这个插件,我们就以此来实现
首先新建一个文件夹,进入文件夹中下载mxgraph
npm install mxgraph
新建一个html文件,写一个mxgraph的hello,world!
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>hello, world!</title>
<style>
* {
margin: 0;
padding: 0;
}
#image-con {
width: 500px;
height: 500px;
}
</style>
</head>
<body onload='main(document.getElementById("graphContainer"))'>
<div id='graphContainer'
style="position:relative;overflow:hidden;width:300px;height:300px;background:#eee;cursor:default;">
</div>
<button id='copyBtn'>
复制
</button>
<div id='image-con'>
</div>
<script>
mxBasePath = './node_modules/mxgraph/javascript/src';
</script>
<script src='./node_modules/mxgraph/javascript/mxClient.js'></script>
<script>
function main(container) {
if (!mxClient.isBrowserSupported()) {
mxUtils.error('Browser is not supported!', 200, false);
} else {
const graph = new mxGraph(container);
const parent = graph.getDefaultParent();
graph.getModel().beginUpdate();
try {
const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
graph.insertEdge(parent, null, '', v1, v2);
} finally {
graph.getModel().endUpdate();
}
}
}
</script>
</body>
</html>
显示界面如图所示, 通过点击复制按钮触发操作
接下来开始重点部分,通过实例化出来的graph中可以获取到当前灰色区域中的两个块和连接线,首先通过graph获取内容的区域大小
let { width, height, x, y } = graph.getGraphBounds();
结构出来区域的x,y的位置,如果直接靠结构出来的想,y的值去生成图片的话,由于生成出来的图片是以左上角未为顶点去生成的,就可能会造成图片内容显示不全,那么就需要对位置做出优化,我们可以通过平移灰色区域的内容的位置来优化
// 通过减去界面的偏移量来让内容区域回到0,0点
x -= graph.view.translate.x;
y -= graph.view.translate.y;
graph.view.setTranslate(-x, -y);
获取到svg元素,然后通过XMLSerializer转码为xml字符串, 通过btoa将xml字符串转为base64的字符串;
const divContent = graph.container.firstElementChild;
const xmlString = new XMLSerializer().serializeToString(divContent);
const base64 = btoa(unescape(encodeURIComponent(xmlString)));
注意看,通过btoa编码的字符串通过了encodeURIComponent和unescape, 由于btoa无法将中文转换为对应的base64的ASCLL字符串,就需要通过encodeURIComponent将字符串加密为转义序列,中文会转换为·%xx,之后通过unescape将含有%xx十六进制形式编码的字符都用ASCLL字符集中等价的字符代替,简单来说就是将其中的中文或特殊字符通过这两个方法编码为ASCLL字符。
base64编码完成之后就完成一半了,然后新实例化image,将转换好的base64拼接为image可以识别的字符串
const image = new Image();
image.width = width;
image.height = height;
image.src = 'data:image/svg+xml;base64,' + base64;
等待image加载完毕之后将image加载到创建的canvas中,通过canvas转为blob数据
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, width, height)
document.getElementById('image-con').appendChild(image);
canvas.toBlob(blob => {
writeImage(blob)
})
};
明明也可以使用svg+xml也可以转化为blob,这里需要特别说明为什么需要使用canvas,由于系统剪切板目前只能写入png的二进制图片数据,canvas转blob之后默认类型就是png
⭐️将blob二进制数据写入到剪切板,调用navigator的clipboard.write方法,写入一个数组,数组中实例化ClipboardItem写入数据之后就可以将图片写入系统剪切板
async function writeImage(blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
}
全量代码
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>hello, world!</title>
<style>
* {
margin: 0;
padding: 0;
}
#image-con {
width: 500px;
height: 500px;
}
</style>
</head>
<body onload='main(document.getElementById("graphContainer"))'>
<div id='graphContainer'
style="position:relative;overflow:hidden;width:300px;height:300px;background:#eee;cursor:default;">
</div>
<button id='copyBtn'>
复制
</button>
<div id='image-con'>
</div>
<script>
mxBasePath = './node_modules/mxgraph/javascript/src';
</script>
<script src='./node_modules/mxgraph/javascript/mxClient.js'></script>
<script>
function main(container) {
if (!mxClient.isBrowserSupported()) {
mxUtils.error('Browser is not supported!', 200, false);
} else {
const graph = new mxGraph(container);
const parent = graph.getDefaultParent();
graph.getModel().beginUpdate();
try {
const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
graph.insertEdge(parent, null, '', v1, v2);
} finally {
graph.getModel().endUpdate();
}
const btn = document.getElementById('copyBtn');
btn.onclick = function() {
let { width, height, x, y } = graph.getGraphBounds();
x -= graph.view.translate.x;
y -= graph.view.translate.y;
graph.view.setTranslate(-x, -y);
const divContent = graph.container.firstElementChild;
const xmlString = new XMLSerializer().serializeToString(divContent);
const base64 = btoa(unescape(encodeURIComponent(xmlString)));
const image = new Image();
image.width = width;
image.height = height;
image.src = 'data:image/svg+xml;base64,' + base64;
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, width, height)
document.getElementById('image-con').appendChild(image);
canvas.toBlob(blob => {
writeImage(blob)
})
};
};
}
}
async function writeImage(blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
}
</script>
</body>
</html>
注意
首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。
其次使用
clipboard要注意兼容性,比如火狐的read和write方法需要再浏览器中开启特定的开关,需要被用户须知的一个操作等
总结
svg元素转为图片写入剪切板需要4步,其中的难点就是在于将svg元素转码为base64,其中需要注意如果有中文或是特殊字符会转义失败或是显示乱码,其次就是剪切板的使用的一些特定条件必须是安全环境https或是localhost才能使用
参考
最後
以上,便是本次分享~ 感谢大家能够看到这里,谢谢各位的支持~