在重构金山文档的剪贴板模块时,对浏览器的操作剪贴板的方式有稍许研究,一般就两种形式,一种是同步剪贴板,另一种是异步剪贴板。

同步剪贴板

在HTML5的Cliboard API规范出来之前,访问或操作系统剪贴板是通过监听copypaste事件,然后拿到事件对象里的clipboardData对象

// 写入剪贴板
document.addEvenentListener('copy', (e) => {
  // 默认会写入选中的数据,如果想覆盖默认写入的数据就得阻止默认行为
  e.preventDefault()
  const oClipboardData = e.clipboardData
  oClipboardData.setData('text/plain', '纯文本类型')
  oClipboardData.setData('text/html', '<div>html类型</div>')
  oClipboardData.setData('customtype/string', '自定义类型')
})

// 读取剪贴板
document.addEventListener('paste', (e) => {
  const oClipboardData = e.clipboardData
  const items = oClipboardData.items
  // 不同类型的数据保存在items数组里,通过循环取出所有数据
  for (let i = 0; i < items.length; i++) {
    const item = items[i]
    const data = oClipboardData.getData(item.type)
  }
})

如果想主动操作剪贴板,还可以通过document.execCommand('copy')document.execCommand('paste')手动触发copy和paste事件

<body>
  <input id="textInput" type="text">
  <button id="copy">复制</button>
  <button id="paste">粘贴</button>
  <script>
    copy.addEventListener('click', () => {
      textInput.select()
      document.execCommand('copy')
    })
    paste.addEventListener('click', () => {
      textInput.focus()
      console.log(document.execCommand('paste'))
    })
  <script>
</body>

因为安全问题,Chrome、Firefox 不支持document.execCommand('paste'),safari上会出现一个系统的粘贴菜单按钮,点击后才触发粘贴。由于execCommand一旦执行就会立即写入或读取剪贴板,我称之为同步剪贴板。

同步剪贴板支持写入的数据类型有:text/plaintext/html和自定义类型,不支持写入图片;但读取的时候可以读到从本地文件中复制的文件或图片

另外documenet.execCommand API已废弃,在部分浏览器中仍然可以使用,但如果需要主动操作剪贴板,建议使用最新的Clipboard API规范

异步剪贴板

Web最新的Clipboard API规范,用来取代document.execCommand的剪贴板访问方式。它的所有操作都是异步的,返回一个Promise 对象,所以称之为异步剪贴板。

异步剪贴板需要通过Permissions API获取权限之后,才能操作剪贴板内容;如果用户没有授予权限,则不允许操作。

// 获取读写剪贴板的权限,执行后会询问用户是否授权。如果用户拒绝,则后续无法读写剪贴板
navigator.permissions.query({name: 'clipboard-write'})
navigator.permissions.query({name: 'clipboard-read'})

异步剪贴板官方宣称可以写入任意类型的数据,但经过实测,目前支持的类型有:text/plain、text/html、image/png、image/svg+xml和自定义类型

write()方法

异步剪贴板写入数据时,需要构造一个ClipboardItem对象传给剪贴板,ClipboardItem对象包含键名数据类型和值blob数据

navigator.clipboard.write(
  [new window.ClipboardItem({
    "text/plain": new Blob(['foo'], {type: 'text/plain'}),
    "text/html": new Blob(['<div>这是一段html</div>'], {type: 'text/html'}),
    // 写入图片时,需要先将图片转为blob格式
    "image/png", new Blob([imageToBlob()], {type: 'image/png'})
  })]
)

注意目前仅支持写入png和svg格式的图片

如果要写入自定义类型的数据,需要给自定义类型加个web 前缀,注意这个自定义类型的数据跟同步剪贴板的自定义类型的数据不能通用,同步剪贴板写入的自定义类型数据,只能由同步剪贴板读取;异步剪贴板也是如此。

window.navigator.clipboard.write(
  [new window.ClipboardItem({
    "web text/custom": new Blob(['foo bar'], {type: "web text/custom"})
  })]
)

read()方法

//获取权限
navigator.permissions.query({
  name: 'clipboard-read'
}).then(result => {
  if (result.state == 'granted' || result.state == 'prompt') {
    navigator.clipboard.read().then(async (clipboardItems) => {
      // 读取成功会返回一个ClipboardItem数组
      for (const clipboardItem of clipboardItems) {
        // clipboardItem.types数组包含了所有可用的数据类型
        for (const type of clipboardItem.types) {
          //通过getType方法拿到对应类型的blob数据
          const blob = await clipboardItem.getType(type);
          if (type === 'text/plain') {
            // 处理文本数据
          } else if (type === 'text/html') {
            // 处理html
            Object.assign(result, res)
          } else if (type.includes('image/')) {
            // 处理图片数据
          } else {
            // 其他类型
            result[type] = blob;
          }
        }
      }
    })
  }
})

异步剪贴板在读取html数据时会有一个怪异的行为,就是自动“优化”html结构,例如剥离script标签的代码和内嵌css,自动给table增加tbody标签,行内style颜色值由十六进制变为rgb十进制,过滤空标签等情况,通过查询API规范,可以通过unsanitize标记来处理是否需要“优化”。

navigator.clipboard.read({ unsanitized: ['text/html'] })

这个标记依赖于浏览器自身的实现,目前应该只有Chrome高版本(120版本以上)才实现了这个功能。

另外还有writeText()、readText()这两个方法,字面意思就是单纯的读写纯文本类型的数据,不做介绍

安全问题

由于可能会向剪贴板写入敏感数据,任意读取剪贴板数据就会产生安全风险,所以规定只有HTTPS协议的页面才能使用这个API。

另外因为safari的安全问题限制,使用异步剪贴板复制图片时不能在调用Clipboard API之前通过异步的方式将图片转换为Blob格式,否则会导致复制失败

总结

异步剪贴板规范会逐渐取代同步剪贴板,但现在各浏览器的实现还不是很完整,为了兼容性,同步剪贴板目前也还是需要继续使用,特别是监听copypaste事件这种方式。

虽然同步剪贴板无法复制图片,局限性比较大,但是粘贴的时候却可以拿到比较完整的数据;而异步剪贴板可以写入图片,但无法读取从本地文件系统复制的文件,包括图片,所以有时需要结合两种方式去读取剪贴板,让用户获得最佳体验。