注意:本文Demo请点击文末“阅读原文”方便查看。

前言 

现在前端富交互能力越来越强,也有很多产品基于前端技术进行离线应用开发或在线应用体验增强;这其中剪切板操作也是一个经常会亮相客串的一个基础能力。

今天这篇文章我们就一起整理一下关于剪切板读写操作的一些实践。会包含浏览器端、Electron客户端两种不同的业务场景下,一些具体的需求实现示例。

实现一个“复制”按钮,方便内容分享传播

在网页上一段文本后添加一个“复制”按钮并不是啥新鲜的事情,比如“复制分享网址”、“帮我砍一刀”...,早先FLASH还红火的时候,也有很多是基于FLASH实现的。

实际基于浏览器自身JS能力实现也非常方便:

<textarea id="txt">这里是将要被复制的文案</textarea>
<button id="btn" type="button">点我复制</button>
<script>
const iptEl = document.getElementById('txt');
const btnEl = document.getElementById('btn');
btn.onclick = function() {
iptEl.select();
document.execCommand('copy');
alert('复制完毕。');
};
</script>

点这里在线试验一下效果。[1]

上面代码中复制文本框中的内容到剪切板,主要使用了 document.execCommand('copy'),我们 通过 Can I use 查看 目前各浏览器兼容都挺好,而且也非常方便,很多富文本编辑器厂商也还是在依赖这个API。

不过需要注意,在相关标准及浏览器厂商的WEB API文档中已经警告开发者这是个“已废弃”的API了。

在新的标准和JS API实现中,已经为我们提供了专门用于剪切板操作的Clipboard API。[2]

当然我们也就可以基于这个新的API,来实现上面相同的功能。只需要将onclick响应中的代码改成下面这样:

btnEl.onclick = () => {
navigator.clipboard.writeText(iptEl.value).then(() => {
alert('复制完毕。');
});
};

点这里在线试验一下效果。[3]

修改后,主要是使用了 clipboard.writeText将文本写入剪切板。同样我们也可以去  Can I use[4]查看一下在各浏览器的兼容性 ;

和前一个方案比较,兼容性及浏览器覆盖率目前还是差一些;不过就API能力来说,clipboard可要比之前的 execCommand 更明确、更强大的多了。

修改将要写入剪切板的文本内容

前面介绍了怎么在网页里添加一个“复制”按钮,来让用户触发复制一段文案的操作。

不妨就这个功能再稍微发散考虑一下:

我们知道触发复制的行为当然不会只有这一种方式(比如也可以“Command+C || Ctrl + C”这种快捷键的形式),那么当用户复制一段文本内容的时候,我们能不能进行二次加工呢?

比如,你可能早就留意到,在某一些网页中复制一段选中文本,粘贴的时发现除了选择内容,后面还会有"原文出处、版权信息"等,是怎么做到的呢?

这里要修改将要写入剪切板的内容,可以将功能逻辑分成3部分:

  1. 监听文本复制的操作行为,阻止浏览器默认行为。

  2. 获取用户选择的目标文本内容,并追加内容。

  3. 将文本内容写入剪切板。

第1步,监听document或目标元素的copy事件就可以。

第2步,要获取用户选中的内容,需要用到另外一个能力 Selection API[5] ;下面示例代码中考虑兼容性实现了一个工具函数 getSelectionTxt。

第3步,我们在前面的示例中,已经实践过2种方案,这里我们再换一个方案,使用事件对象暴露的 clipboardData.setData[6]

function getSelectionTxt() {
if (document.selection) {
return document.selection.createRange().text;
} else {
return String(window.getSelection());
}
}
// 监听 copy 事件
document.addEventListener('copy', (evt) => {
const { clipboardData } = evt;
if (!clipboardData) return;
// 获取选中文本
let txt = getSelectionTxt();
if (!txt) return;
// 有要操作的目标内容,阻止浏览器默认行为
evt.preventDefault();
// 追加内容
txt += '\n\n 【原文地址:https://blog.pyzy.net/post/clipboard.html 】';
// 写入剪切板
clipboardData.setData('text/plain', txt);
});

在线试验一下[7]

认识剪切板中的内容

上面我们都是基于纯文本的实践示例,实际剪切板里不止是可以用于文本内容的复制粘贴。

如果选中网页中一段带格式的富文本内容,在一个富文本编辑器粘贴,会连带格式粘贴过去。

如果我们使用微信等IM的截图功能、或者使用MacOS系统中的 Command+Shift+4+Control截图到剪切板,那么就可以在任意支持图片粘贴的地方粘贴一个图片过去,也可以在网页中粘贴实现上传。

另外在使用一些IM工具时候,通常也允许我们复制磁盘中的任意文件或文件夹,在会话交流界面粘贴发送出去。

更有代表意义的是:在Excel等Office编辑器中复制一段内容,下面也通过具体代码示例来逐步了解一下。

跟前面的示例监听 copy 类似地,可以通过监听 paste 粘贴事件,来读取剪切板中数据内容:

document.addEventListener('paste', (evt) => {
const { clipboardData } = evt;
console.log('clipboardData:', clipboardData);
});

可以将以上代码在网页中执行。

之后打开一个 Excel 表格,任意选中几个单元格,并“复制”内容。

再回到刚才执行paste事件监听的页面,Command+V(如果你是Windows系统需要Ctrl+V)。

可以看到控制台打印出的clipboardData中,types字段值数组长度为3,另外还有items、files等字段。

我们接下来对代码稍加改造,打印看一下types、items字段值中具体是什么内容:

document.addEventListener('paste', (evt) => {
const { types = [], items=[] } = evt.clipboardData || {};
console.log('clipboardData.types:', [...types]);
console.log(
'clipboardData.items:',
[...items].map(
({ kind, type }) => ({ kind, type })
)
);
});

神奇的事情发生了,一次复制行为,原来是可能会产生多种不同类型可用于粘贴的数据的:

其中types字段的内容分别为 ["text/plain", "text/html", "Files"],对应到 items字段中的数据,我们发现这里的Files类型是一个type为image/png的文件对象。

我们再改造一下示例代码,实现一个可以将"text/plain", "text/html", "image/png"这3种类型内容都分别打印到页面上的功能,看看剪切板里具体内容到底是什么东东:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>剪切板内容读取试验</title>
<style>
h2 {border-bottom: solid 1px #eee;font-size: 16px;padding: 10px;
}
#container {border: solid 1px #eee;padding: 10px;background: #f9fdff;
}
#container:empty:after {content: '请执行复制操作后,在当前页面尝试Command+V或Ctrl+V。';color: #aaa;font-size: 14px;
}
h3 {border-bottom: solid 1px #eee;font-size: 16px;padding: 0 0 10px;margin: 10px 0;
}
textarea {display: block;width: 95%;height: 200px;padding: 8px 10px;box-shadow: 1px 1px 3px rgb(0 0 0 / 20%) inset;border-radius: 4px;
}
img {display: block;width: 100%;
}
</style>
</head>
<body><h2>剪切板内容:</h2><div id="container"></div>
<script>
const containerEl = document.getElementById('container');
document.addEventListener('paste', (evt) => {
containerEl.innerHTML = '';
const items = Array.from((evt.clipboardData || {}).items || []);
items.forEach((item, i) => {
const { kind, type } = item;
const hdEl = document.createElement('h3');
hdEl.innerText = `序号:${i};kind:${kind}; type:${type}`;
containerEl.appendChild(hdEl);
const isFile = kind === 'file';
const previewEl = document.createElement(
isFile ? 'img' : 'textarea'
);
if (isFile) {
const file = item.getAsFile();
previewEl.src = URL.createObjectURL(file);
} else {
item.getAsString((str) => {
previewEl.value = str;
});
}
containerEl.appendChild(previewEl);
});
});
</script>
</body>
</html>

以上代码和之前一样,也可以在线试验一下[8]

将刚刚复制到剪切板的Execl单元格内容在这个页面中粘贴,可以看到如下效果:

右边内容展示区中,从上往下依次是:带有定界符的纯文本内容、带有CSS样式及table代码的HTML片段、一张对选择区域截图产生的图片文件。

我们再试验一下前面说到的使用微信截个图:

也还是打开上面的剪切板粘贴试验页[9],进行粘贴试验:

哇欧,我们在网页中通过剪切板API读取到了截取的图片 ---- 并且跟前面Excel复制行为比较仅有一个截图数据。

试试在文章左边Blog作者头像上右键复制图片:

也还是回到试验页[10],Command+V 粘贴试验:

哦,原来在网页里复制的图片,可以通过剪切板拿到一端富文本HTML代码和一个图片文件对象。

那么我们从任意磁盘目录中选择并Command+C复制一个图片呢?

剪切板粘贴试验页[11]进行粘贴读取到的内容:

可以看到,我们在网页中拿到了被复制图片的一个text/plain类型的string文件名和一个image/png类型的File对象。

厉害了,通过上面的示例实现的代码能力,已经实现了剪切板资源读取、文件的File对象[12]获取、图片预览。

如果能将File直接发送给服务端,那么一个通过剪切板的复制粘贴能力来上传分享图片、发送截图的能力就可以实现了。

将File上传到服务端

完整的文件上传示例,必须依赖服务端,这里只提供纯前端伪代码的几种方案:

首先,如果你的服务端有一个独立的用于文件上传的接口,基于XMLHttpRequest(也就是熟知的AJAX)方式发送File对象给服务端即可:

const xhr = new XMLHttpRequest();
xhr.onload = () => {
console.log('上传结果:', xhr.responseText);
};
xhr.open('POST', './uploadFile', true);
xhr.send(file);

如果服务端接口在接受文件内容时,还要求有别的必填字段信息,往服务端send一个FormData对象即可:

const formData = new FormData();
formData.append('type', 'image');
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.onload = () => {
console.log('上传结果:', xhr.responseText);
};
xhr.open('POST', './uploadFile', true);
xhr.send(formData);

如果服务端接口还额外要求必须携带一些自定义请求头字段信息,也可以改成使用 Fetch API 来发送 formData 给服务端。

fetch('./uploadFile', {
method: 'POST',
body: formData,
headers: new Headers({
'Content-Type': 'application/json'
})
}) .then((res) => { ... });

当然,文件上传并不会这么简单,比如还可能会涉及到传输进度同步、用户主动取消传输、大文件分片、文件秒传等等,不符合这里的主题,就不展开讨论了。

浏览器端能否通过复制发送文件或文件夹?

当剧情发展到这里,聪明的你也许已经意识到:好像哪里不太对劲,是不是刻意隐瞒了一些问题?

又或者,聪明的你早就发现了,前面基于剪切板传递的都是纯文本字串、富文本HTML、或者静态图片啊!

我们先来复制一个GIF动图,比如下面这个:

到前面的剪切板粘贴试验页[13]粘贴一下,看看效果:

好的,类别kind: string 对应内容和前面示例中发现的规律表现一致,我们继续看。

诶呦,喂!不对啊,明明复制的是image/gif,图片内容咋也变成了和前面复制个png一样,你看它,不会动了嘿!

先暂且按下不管,我们干脆再多一些尝试、复制点别的文件试试,比如分别尝试粘贴一个CSS文件、粘贴一个JS文件、粘贴一个HTML文件、粘贴一组图片、粘贴一个文件夹、粘贴一组任意文件:

WTF!!什么烂七八糟的?拿不到文件信息?

对了(突然灵鸡一动),记得么?前面我们尝试打印clipboardData对象到控制台的时候,和items平级的有个files字段诶。

我们再在粘贴时[14]多打印一下这个files字段,看看这里面会不会就是被复制的文件集合。

先来一组任意文件:

啥也没读出来啊。再来一组图片试试:

复制了3张图片,但 Files 里只读出了1张图片。

通过API文档[15]了解到 clipboardData 是一个 DataTransfer 对象,查一下相关标准文档[16],没找到直接与剪切板操作相关的描述。但有一句:“如果DataTransfer对象不再与拖动数据存储关联,FileList则为空”,不知道是不是可以理解为不是拖拽行为也就不会有FileList?

Electron 场景能否通过复制发送文件或文件夹?

浏览器中看来无解了,但是WEB前端的魔爪可是早就已经不止于浏览器中了,比如我们是可以借助 Electron 开发离线应用,并拓展使用一些浏览器中不具备的 Native 能力。

而且,刚好笔者涉及当前需求的产品场景就是同时支持浏览器端和Electron包壳两种场景的,那么我们不妨也调研一下。

通过 Electron 剪贴板 API[17]可以看到,当下开放的方法能力实际和浏览器端没有太大区别,文档告诉我们可以通过剪切板的读写的数据或文件资源也是比较基础的文本、富文本、特殊标记语言文本、静态Image资源数据。

不过功夫不负有心人,经过不停翻找看到在Electron社区[18]有人说:通过Mac剪切板查看器示例程序[19]发现应该能通过clipboard.read('NSFilenamesPboardType')读取到一个被复制的文件或文件夹列表的XML格式描述文本:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>/path/to/file1.ext</string>
<string>/path/to/file2.ext</string>
</array>
</plist>

该XML内容的plist节点下的信息就是描述了被复制文件或文件夹的路径。

虽然不能拿到文件对象,但这可是Electron端哦。既然能拿到文件路径,再在页面里通过路径读取到文件的buffer不也就能做很多后续文件操作能力了?

太棒了,不妨按照程序逻辑流程逐步试验一下!

首先,需要从剪切板中拿到的这个XML里提取出被复制的文件或文件夹的路径:

const { clipboard } = require('electron');
function getClipboardPaths() {
const filePathsXML = clipboard.read('NSFilenamesPboardType') || '';
return (filePathsXML.match(/<string>([^<]*)<\/string>/gim) || []).map(
(filePath) => {
return filePath
.replace(/(^<string>|<\/string>$)/g, '')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
);
}

如果我们复制了2个文件和一个文件夹,上面的代码可以帮我们读取到如下数组:

[
'/path/to/folder1',
'/path/to/file1.ext',
'/path/to/file2.ext',
]

上面的 /path/to/folder1 是一个文件夹,如果我们的目的是要复制粘贴文件,那么递归遍历展开文件夹也是必须的:

const fs = require('fs');
const path = require('path');
async function flatPaths(filePathsArr) {
const filePaths = [];
await Promise.all(
filePathsArr.map((filePath) => {
return new Promise((resolve) => {
fs.stat(filePath, (err, stats) => {
if (!err) {
if (stats.isDirectory()) {
fs.readdir(filePath, async (err, files) => {
if (!err && files) {
const filesPaths = files.map((fileName) => {
return path.join(filePath, fileName);
});
const flatFilesPaths = await flatPaths(filesPaths);
filePaths.push(...flatFilesPaths);
}
resolve();
});
return;
} else if (stats.isFile()) {
filePaths.push(filePath);
}
}
resolve();
});
});
})
);
return filePaths;
}

接下来是重点了,我们先简单粗暴一些,直接循环将前面加工拿到的所有文件绝对路径通过fs.readFile读取出文件的buffer数据。

const fs = require('fs');
const path = require('path');
async function getClipboardFiles(fileAbsPathsArr) {
const files = [];
await Promise.all(
fileAbsPathsArr.map((filePath) => {
return new Promise((resolve) => {
fs.readFile(filePath, (err, data) => {
if (!err) {
files.push({
filePath,
fileName: path.basename(filePath),
data, // 可用于在Web端JS new File([data], fileName)
});
}
resolve();
});
});
})
);
return files;
}

这次经过getClipboardFiles之后我们拿到的是像下面这样,带有文件绝对路径、文件名、文件buffer数据的数组:

[
{
filePath: '/path/to/folder1/fileN.ext',
fileName: 'fileN.ext',
data: [...Uint8Array...],
},
{
filePath: '/path/to/file1.ext',
fileName: 'file1.ext',
data: [...Uint8Array...],
},
...,
]

现在可以回到我们熟悉的 Web 页面中JS交互逻辑里了。

document.addEventListener('paste', (evt) => {
const clipboardPaths = await flatPaths(getClipboardPaths());
if (clipboardPaths.length) {
evt.stopPropagation();
evt.preventDefault();
} else {
return; // any ...
}
// 得到剪切板中文件数据们
const clipboardFiles = await getClipboardFiles(clipboardPaths);
// 使用 new File 生成可用于Web端的 File 对象
const fileList = clipboardFiles.map(({ fileName, data }) => {
return new File([data], fileName);
});
/* 文件预览、发送.... sendFiles(fileList); */
});

到这里,我们在Electron端发送文件的效果也就实现了。

拿到文件列表就可以后面的折腾了,比如用户发送前,给一个文件预览,二次确认的列表:

另外,Electron场景的NSFilenamesPboardType还有一个非常值得令人振奋的地方,不但可以读取剪切板文件列表,也可以写任意文件列表到剪切板了。

const { clipboard } = require('electron');
clipboard.writeBuffer(
'NSFilenamesPboardType',
Buffer.from(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>/path/to/file1.ext</string>
<string>/path/to/file2.ext</string>
</array>
</plist>
`)
)

写在最后

首先,请读者注意:以上所有示例代码更侧重在思路示意,甚至可以说是伪代码,如果要在正式业务场景应用,请一定酌情调整、增加严谨性、健壮性、兼容性相关考量。

另外,相关标准委员会、浏览器厂商(比如:Chrome)、Electron官方也都还在不断迭代扩展Clipboard API,本文的方案只是基于时下的形式考虑,如果发现不符欢迎随时指正,以免误导。

最后,郑重感谢您花时间在本文中,如果有哪些点描述不准确或需要讨论的,也欢迎评论留言。

祝大家新春快乐,牛气冲天~

参考链接及Demo地址

[1]

https://code.h5jun.com/zetex/3/edit?html,js,output

[2]

https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard

[3]

https://code.h5jun.com/qila/1/edit?html,js,output

[4]

https://caniuse.com/?search=clipboard.writeText

[5]

https://developer.mozilla.org/zh-CN/docs/Web/API/Selection

[6]

https://developer.mozilla.org/zh-CN/docs/Web/API/ClipboardEvent/clipboardData

[7]

https://code.h5jun.com/mefuv/2/edit?html,js,output

[8]

https://code.h5jun.com/jucef/edit?js,output

[9]

https://code.h5jun.com/jucef/edit?js,output

[10]

https://code.h5jun.com/jucef/edit?js,output

[11]

https://code.h5jun.com/jucef/edit?js,output

[12]

https://developer.mozilla.org/zh-CN/docs/Web/API/File

[13]

https://code.h5jun.com/jucef/edit?js,output

[14]

https://code.h5jun.com/qehim/edit?js,console,output

[15]

https://developer.mozilla.org/zh-CN/docs/Web/API/ClipboardEvent/clipboardData

[16]

https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-files

[17]

https://www.electronjs.org/docs/api/clipboard

[18]

https://github.com/electron/electron/issues/9035#issuecomment-359160710

[19]

https://developer.apple.com/library/archive/samplecode/ClipboardViewer/Introduction/Intro.html

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

​Web前端剪切板文本分享到文件发送相关推荐

  1. Web前端开发入门学习分享

    Web前端开发入门学习分享 1:如何开始学习Web前端 首先你需要学习html的各个标签,掌握其用法和规范,明白其作用. 开始学习css的使用,你先学习在html页面中为标签增加css样式,其次是将c ...

  2. Autohotkey全选复制并保存剪切板文本至以时间命名的文本文件

    AutoHotKey实现全选复制并保存剪切板文本至以时间命名的文本文件 一键保存当前页面的文字为文本文件 一键保存当前页面的文字为文本文件 借用AutoHotKey软件编写的一小段代码实现 按组合快捷 ...

  3. 231个web前端的javascript特效分享(仅供本人学习,非教程类型)

    2019独角兽企业重金招聘Python工程师标准>>> 1.文本框焦点问题 onBlur:当失去输入焦点后产生该事件 onFocus:当输入获得焦点后,产生该文件 Onchange: ...

  4. web前端介绍_html-超文本标记语言

    web 前端简介 1. web1.0 时代的网页制作 Web应用程序是一种可以通过Web访问的应用程序,程序的最大好处是用户很容易访问应用程序,用户只需要有浏览器即可,不需要再安装其他软件应用程序有两 ...

  5. 浏览器 Web 访问剪切板图片

    前言 有时候,我们希望能访问用户的剪切板,来实现一些方便用户的功能:但是另一方面,剪切板里的数据对用户来说又是非常隐私的,所以浏览器在获取信息方面有安全限制,同时也提供访问接口. 前段时间由于业务功能 ...

  6. windows剪切板文本和文件的获取设置

    介绍 windows剪切板的内容包含很多不同的格式,例如:CF_TEXT.CF_BITMAP.CF_METAFILEPICT.CF_SYLK.CF_DIF.CF_TIFF.CF_OEMTEXT.CF_ ...

  7. web前端自学之路分享

    前言: 2018年随着编程的火热和自身对这岗位的热爱,这一年毅然决然的开始了自己的自学之路,这一阶段的学习过程中有很多感触,随着工作逐步稳定想要写下来和正在学习或者正在犹豫要不要学习的人们一起分享.也 ...

  8. Web前端开发学习资料分享

    2019独角兽企业重金招聘Python工程师标准>>> Web前端开发教程: (1)Web开发必备手册大集合 (2)Web前端开发人员和设计师必读文章推荐(系列一~系列八) (3)[ ...

  9. java webpack web项目_零基础如何学习web前端,入门教程分享

    前端作为互联网时代直接触达用户的窗口,大到我们每天浏览到的网站,小到一次点击按钮的页面,前端无处不在.并且在产品的众多开发环节之中,最能让用户直观感受到的就是前端开发.因而前端行业的广阔发展前景也吸引 ...

最新文章

  1. 运维7年,对Linux的经验总结
  2. python相关概念
  3. ptrace和wait的理解 (ptrace监控进程)
  4. shell:判断某个变量是否包含字符串/变量的方法
  5. flask 自定义错误页面
  6. opp原则_OPP--面向对象知识点
  7. go Mutex (互斥锁)和RWMutex(读写锁)
  8. Java从入门到精通+第三版.pdf
  9. 第五部分 家庭创业奔小康6.开家畅销书专送店
  10. python制作日历_利用Python自动化生成明星定制日历!
  11. css强制一行显示超出的部分显示点点点
  12. STM32——FLASH擦除/写入失败的踩坑笔记。(WRPERR)
  13. bind dlz mysql ptr_Bind+DLZ+MySQL智能DNS的正向解析和反向解析实现方法
  14. 如何为一个kafka集群选择topics/partitions的数量
  15. [UE4渲染]LightPass中加入ramp图
  16. 知识点索引:幂函数性质
  17. QueryRunner中query方法
  18. 百度、高德离线地图SDK开发工具,局域网内离线地图开发环境
  19. 一边学计算机一边上班累的说说,上班累了的心情说说_上班的心情说说精选
  20. 推荐几款音频转文字软件给你

热门文章

  1. sharding-jdbc系列之按月动态分表(十二)
  2. 台灯显色指数多少合适?专家教你护眼灯怎么选
  3. LTspice基础教程-008.LTspice PWL设置
  4. 【OpenSource】开源管理平台BlackDuck简介
  5. 替代A4988的微型打印机驱动TMI8421国产电机驱动芯片
  6. systemverilog中rand机制的 $urandom_range()函数
  7. 【Zookeeper】分布式集群(详细图文)
  8. 轩迅汇如何做好个人定位?定位越早,受益越多
  9. Lucas定理——推导及证明
  10. Matlab合并文本或excel文件数据并绘图