Safari 剪切板写入的难题

Oct 29, 2024 · 8min

    自从玩了原神之后,我会在米游社刷一些帖子,我发现他们的表情包很有趣但是遗憾的是不能直接在外部使用。 我希望能够在其它社交平台使用他们的表情包,所以,最近做了一个项目 MiHoYo Sticker。他的功能很简单,点击表情包后会将图片写入剪切板中。这就利用到了系统剪切板 API navigator.clipboard.write()

    在很早之前,随着Web应用程序越来越复杂,浏览器开始实施更严格的安全策略,以防止网页滥用用户权限。为了保护用户的隐私和安全,确保敏感操作需要用户的明确同意。这个特性在现代浏览器中逐渐得到加强。在 2018 年左右,对于剪贴板的访问和其他敏感 API 的调用,浏览器开始引入更严格的安全限制,通常要求在事件处理函数内调用这些 API e.g.

    function App() {
      function onCopyHandler() {
        navigator.clipboard.write('hello world')
      }
    
      return (
        <Button onClick={onCopyHandler}>Copy</Button>
      )
    }

    以上代码工作的很好,哪怕是在 Safari。但是,我需要复制图片呢?在 Safari 这就变得非常糟糕,复制图片我们通常需要将图片转为 Blob 后写入剪切板中。

    function writeBlob(blob: Blob, type: string = blob.type) {
      const data = [new ClipboardItem({ [type]: blob })]
      return navigator.clipboard.write(data)
    }
    
    async function onCopy(event: MouseEvent<HTMLImageElement>, sticker: Sticker) {
      const img = event.currentTarget
    
      const canvas = document.createElement('canvas')
      canvas.width = img.naturalWidth
      canvas.height = img.naturalHeight
    
      const ctx = canvas.getContext('2d')
      ctx?.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight)
    
      canvas.toBlob(async (blob) => {
        try {
          if (!blob)
            return
          await writeBlob(blob)
    
          toast(`You copied 「${sticker.name}`, {
            position: 'top-right',
            icon: <BellIcon />,
          })
        }
        catch (e) {
          console.error(e)
        }
      }, 'image/png')
    }

    这段代码在 Google Chrome 中一切正常,在 Safari 中却出现了这样的错误

    The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

    起初我以为是我的操作系统禁止了 Safari 剪切板的读写行为,但是我找不到这个权限应该在哪里设置。我查看了 WebKit ,可是这一切看起来都很好。在我的尝试过程中,我发现 Safari clipboard.write 不能在事件处理函数中间接调用。例如上面的代码是 toBlob - invoke -> write ,这不被允许。而 Chrome 在用户点击后整个调用栈都能使用。

    知道问题后,我需要自己实现 canvas to blob 以确保 write 执行之前的代码都是同步的。

    function toBlob(canvas: HTMLCanvasElement) {
      const dataURL = canvas.toDataURL()
    
      const byteString = atob(dataURL.split(',')[1])
      const mimeString = dataURL.match(/^data:([^;]+);base64,/)?.[1]
    
      const buffer = new ArrayBuffer(byteString.length)
      const intArray = new Uint8Array(buffer)
    
      for (let i = 0; i < byteString.length; i++) {
        intArray[i] = byteString.charCodeAt(i)
      }
    
      return new Blob([intArray.buffer], { type: mimeString })
    }

    以上代码使用 toDataURL 将 canvas 转为 url,大概长这样 data:image/png;base64,... 。再通过 atob 函数将 Base64 转为原始的二进制字符串。charCodeAt 获取每一个二进制字符串的字节码。最后将这些字节码转为 Blob

    我不知道它和 canvas 提供的 toBlob 对比效率如何,但是起码它好用 (:

    async function onCopy(event: React.MouseEvent<HTMLImageElement>, sticker: Sticker) {
      const img = event.currentTarget
    
      const canvas = imgToConvas(img)
      if (!canvas)
        return
    
      const blob = toBlob(canvas)
      if (!blob)
        return
    
      const clipboardItem = [new ClipboardItem({ [blob.type]: blob })]
      await navigator.clipboard.write(clipboardItem)
    }

    在使用我封装的 toBlob 后,一切都顺利进行

    最后,

    很感谢你看到这里,亲爱的朋友!

    欢迎大家使用我的项目 MiHoYo Sticker

    同样的,它也被开源在了 GitHub

    >

    cd ..
    CC BY-NC-SA 4.0 2024-PRESENT © Clover You