假设你的销售月底需要对出差产生的住宿/打车进行报销,于是你打开了某打车平台查看行程单进行截图,这时你发现报销的页面上传图片只能选择一个本地的图片,于是你把行程单通过办公软件发送给自己,然后再保存到本地,最后通过系统选择本地文件,终于完成了上传。
可是如果你本月需要上传的行程单很多呢?你需要一遍一遍的重复这个过程,即崩溃又浪费时间。
我们常见的上传组件主要就两种交互形式:拖拽上传 和 本地文件选择,如何在这基础之上实现截图直接上传呢?
其实使用截图工具截图本质上和 ctrl+c 是一样的,截取的图片同样存放在剪切板内,下面我们通过两种不同的方式来获取剪切板内图片实现两种不同交互的截图上传。
一、past事件
当用户通过浏览器界面发起一个"粘贴"动作时会触发 paste 事件;我们很容易可以通过剪切板对象clipboardData中的items,判断粘贴内容是否是"图片"类型并执行getAsFile()将图片转换为二进制file。
这种方式比较像我们经常使用 ctrl+c / ctrl+v
// 代码实现
const pastContentEle = document.querySelector('#past-content')
pastContentEle.addEventListener('paste', function (event) {
const items = event.clipboardData && event.clipboardData.items
if (items && items.length) {
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile()
// 第一步:FormData对象转换(用于上传)
const formData = getFileFormData(file)
// 第二步:上传
uploadImg('https://mytest.com/uploadFile', formData)
}
}
}
})
// 第一步:FormData对象转换(用于上传)
function getFileFormData(file) {
const formData = new FormData()
formData.append('file', file, file.name)
return formData
}
// 第二步:上传
function uploadImg(url, formData) {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log('上传成功')
};
xhr.open('POST', url, true);
xhr.send(formData)
}
二、Clipboard API
相比于监听用户粘贴事件, Clipboard API 可以更加灵活的对系统剪切板进行读/写操作,例如可以通过点击一个按钮读取剪切板内容上传
navigator.clipboard
2.1 使用限制
由于剪切板中可能包含用户的隐私数据(如账户、密码等),所以这个API的限制也会比较多;在Chrome下只有HTTPS协议页面才可以使用,并且在首次调用API时还会提示用户授权。
navigator.clipboard 返回 undefined 证明当前页面无法使用这个API。
2.2 读取剪切板内容
Clipboard 提供两种读取方法,两种读取方式均返回 Promise 对象:
-
Clipboard.readText()用于读取剪切板里的文本数据; -
Clipboard.read()用于读取文本或二进制数据(如图片);(这里注意:虽然我们不涉及到本地图片复制/粘贴上传,但是Clipboard无法获取到本地复制的图片,只能获取到图片名称)async function getClipboardValue() { const text = await navigator.clipboard.readText(); console.log('text: ', text) }
在开发者工具中运行代码会报错?
这是因为在开发者工具运行代码时,当前窗口指的是"开发者工具"而不是实际页面,开发者工具窗口不存在Clipboard API依赖的DOM接口。
解决办法:将代码放到setTimeout延迟运行,在函数调用前快速点击浏览页面窗口
setTimeout(() => getClipboardValue(), 3000)
2.3 剪切板图片上传
具体实现逻辑很简单,直接上代码:
async function onPasteUpload() {
// 第一步:读取剪切板内容
const blobArr = await getClipboardImgBlob()
if (blobArr.length) {
for (let i=0,len=blobArr.length; i<len; i++) {
// 第二步:FormData对象转换(用于上传)
const { blob, type } = blobArr[i]
const formData = getFileFormData(blob, type)
// 第三步:上传
uploadImg('https://my.test.com/uploadFile', formData)
}
}
}
// 第一步:读取剪切板内容
async function getClipboardImgBlob() {
try {
const blobArr = []
const clipboardItems = await navigator.clipboard.read()
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
if (type.indexOf('image') !== -1) {
// 转换成blob
const blob = await clipboardItem.getType(type)
blobArr.push({ blob, type })
}
}
}
return blobArr
} catch(e) {
return []
}
}
// 第二步:FormData对象转换(用于上传)
function getFileFormData(blob) {
const file = new window.File([blob], '234sdfzxdftyg.png', { type })
const formData = new FormData()
formData.append('file', file, file.name)
return formData
}
// 第三步:上传
function uploadImg(url, formData) {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log('上传成功')
};
xhr.open('POST', url, true);
xhr.send(formData)
}
2.3 iframe内使用(补充)
在iframe里使用 Clipboard API 会报下面的错误
The Clipboard API has been blocked because of a permissions policy applied to the current document
这是因为iframe的安全机制导致,需要通过增加 allow="clipboard-read; clipboard-write" 明确告知iframe所需要的权限
<iframe frameborder="0" :src="priceUrl" width="100%" height="600px" allow="clipboard-read; clipboard-write" />