Typescript實現一鍵複製文字進剪貼簿

OceanZH發表於2022-07-06

Typescript實現一鍵複製文字進剪貼簿

場景

在搭建一些展示程式碼的頁面時,一個常見的需求是點選按鈕可以把頁面的程式碼複製進剪貼簿。

目前 @vueuse/core 這個 Vue 的組合式 API 工具庫提供了 useClipboard 方法來支援複製剪貼簿功能,使用瀏覽器 Clipboard API 實現。
核心程式碼是 await navigator!.clipboard.writeText(value)

我的應用場景是在使用 Vitepress + Typescript 搭建元件庫文件站的過程中,應用了 @vueuse/core 實現點選按鈕複製元件程式碼。在後續的測試中發現,在開發環境中點選按鈕複製程式碼的功能正常,但是在進行打包部署至生產環境後,點選按鈕會提示覆制失敗,兩個環境使用的是同一版本的 Chrome 瀏覽器。

核心程式碼
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'

const vm = getCurrentInstance()!

const props = defineProps<{
  rawSource: string
}>()

const { copy, isSupported } = useClipboard({
  source: decodeURIComponent(props.rawSource),
  read: false,
})

const copyCode = async () => {
    // $message來自element-plus 
  const { $message } = vm.appContext.config.globalProperties;
  if (!isSupported) {
    $message.error('複製失敗')
  }
  try {
    await copy()
    $message.success('複製成功')
  } catch (e: any) {
    $message.error(e.message)
  }
}

</script>

通過閱讀 @vueuse/core 的原始碼,可以發現其isSupported 判斷功能使用 Permissions API

核心的判斷方法
permissionStatus = await navigator!.permissions.query('clipboard-write')

用於判斷使用者是否有對剪貼簿的寫入許可權,而在生產環境中,isSupported 判斷的結果是不支援,而在開發環境中則是支援。

經過分析,發現跑打包後程式碼的瀏覽器 F12 中 'clipboard' in navigator === false

回頭查閱 Clipboard API 的MDN文件有一項提示

Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers.

以及 stackoverflow 上的問題討論

This requires a secure origin — either HTTPS or localhost (or disabled by running Chrome with a flag). Just like for ServiceWorker, this state is indicated by the presence or absence of the property on the navigator object.

結論是 Clipboard API 僅支援在 安全上下文(Secure contexts) 中使用,在這裡指的是基於 https 協議或者 localhost/127.0.0.1 本地環境訪問的服務。

然而實際場景中確實存在需要部署在普通 http 環境中的服務,尤其是一些在企業內部的專案,需要尋找 Clipboard API 的替代方案。

方案

Clipboard API 出現之前,主流的剪下板操作使用 document.execCommand 來實現;

相容思路是,判斷是否支援 clipboard,不支援則退回 document.execCommand

document.execCommand 實現一鍵點選複製的流程

  • 記錄當前頁面中 focus/select 的內容
  • 新建一個 textarea
  • 將要複製的文字放入 textarea.value
  • 將 textarea 插入頁面 document,並且設定樣式使其不影響現有頁面的展示
  • 選中 textarea 的文字
  • document.execCommand 複製進剪貼簿
  • 移除 textarea
  • 從記錄中還原頁面中原選中內容
實現程式碼 copy-code.ts
export async function copyToClipboard(text: string) {
  try {
    return await navigator.clipboard.writeText(text)
  } catch {
    const element = document.createElement('textarea')
    const previouslyFocusedElement = document.activeElement

    element.value = text

    // Prevent keyboard from showing on mobile
    element.setAttribute('readonly', '')

    element.style.contain = 'strict'
    element.style.position = 'absolute'
    element.style.left = '-9999px'
    element.style.fontSize = '12pt' // Prevent zooming on iOS

    const selection = document.getSelection()
    const originalRange = selection
      ? selection.rangeCount > 0 && selection.getRangeAt(0)
      : null

    document.body.appendChild(element)
    element.select()

    // Explicit selection workaround for iOS
    element.selectionStart = 0
    element.selectionEnd = text.length

    document.execCommand('copy')
    document.body.removeChild(element)

    if (originalRange) {
      selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy
      selection!.addRange(originalRange)
    }

    // Get the focus back on the previously focused element, if any
    if (previouslyFocusedElement) {
      ;(previouslyFocusedElement as HTMLElement).focus()
    }
  }
}
使用
<script setup lang="ts">
import { copyToClipboard } from './copy-code';

const vm = getCurrentInstance()!

const props = defineProps<{
  rawSource: string
}>()

const copyCode = async () => {
    // $message來自element-plus 
  const { $message } = vm.appContext.config.globalProperties;
  try {
    await copyToClipboard(decodeURIComponent(props.rawSource))
    $message.success('複製成功')
  } catch (e: any) {
    $message.error(e.message)
  }
}

</script>

參考

相關文章