{T}

剪贴板

Electron 的 clipboard 模块提供了读写系统剪贴板的能力,支持文本、图片、HTML 等多种格式。剪贴板是用户与应用之间数据交互的重要桥梁。

系统架构

code
┌─────────────────────────────────────────────────────────┐
│                   Clipboard 模块                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │   文本操作    │  │   图片操作    │  │  高级格式    │ │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤ │
│  │ readText     │  │ readImage    │  │ read         │ │
│  │ writeText    │  │ writeImage   │  │ write        │ │
│  │ readHTML     │  │ has          │  │ readBookmark │ │
│  │ writeHTML    │  │ clear        │  │ writeBookmark│ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                   系统剪贴板                             │
│  macOS: NSPasteboard                                  │
│  Windows: Clipboard API                               │
│  Linux: X11 Selection / Wayland Clipboard             │
└─────────────────────────────────────────────────────────┘

Clipboard API 完整说明

核心方法

方法参数返回值说明
readText(type)StringString读取剪贴板中的文本
writeText(text, type)String, Stringvoid写入文本到剪贴板
readHTML(type)StringString读取剪贴板中的 HTML
writeHTML(markup, type)String, Stringvoid写入 HTML 到剪贴板
readImage(type)StringNativeImage读取剪贴板中的图片
writeImage(image, type)NativeImage, Stringvoid写入图片到剪贴板
readRTF(type)StringString读取 RTF 格式内容
writeRTF(text, type)String, Stringvoid写入 RTF 格式内容
readBookmark()-Object读取书签(macOS)
writeBookmark(title, url, type)String, String, Stringvoid写入书签(macOS)
readFindText()-String读取查找文本(macOS)
writeFindText(text)Stringvoid写入查找文本(macOS)
clear(type)Stringvoid清空剪贴板
has(format, type)String, StringBoolean检查是否有指定格式
read(format)StringString读取指定格式数据
write(data, type)Object, Stringvoid写入多种格式数据
availableFormats(type)StringString[]获取可用格式列表

type 参数说明

type 参数指定剪贴板类型(仅 macOS 支持):

说明
selection选区剪贴板(鼠标选中的文本)
clipboard系统剪贴板(默认值)
javascript
// macOS 可以访问选区剪贴板
if (process.platform === 'darwin') {
  // 读取当前选中的文本
  const selectedText = clipboard.readText('selection')
  console.log('选中的文本:', selectedText)
  
  // 写入选区剪贴板
  clipboard.writeText('新选中的文本', 'selection')
}

基本用法

文本操作

javascript
const { clipboard } = require('electron')

// 写入文本
clipboard.writeText('Hello Electron!')

// 读取文本
const text = clipboard.readText()
console.log(text)  // 'Hello Electron!'

// 清空剪贴板
clipboard.clear()

// 检查剪贴板是否有文本
if (clipboard.has('text/plain')) {
  console.log('剪贴板包含文本')
}

// 获取剪贴板中的所有格式
const formats = clipboard.availableFormats()
console.log('可用格式:', formats)  // ['text/plain', 'text/html', ...]

图片操作

javascript
const { clipboard, nativeImage } = require('electron')
const fs = require('fs')

// 从文件创建图片并写入剪贴板
const image = nativeImage.createFromPath('/path/to/image.png')
clipboard.writeImage(image)

// 读取剪贴板图片
const clipImage = clipboard.readImage()

// 检查图片是否为空
if (!clipImage.isEmpty()) {
  // 保存到文件
  const pngBuffer = clipImage.toPNG()
  fs.writeFileSync('clipboard.png', pngBuffer)
  
  // 获取图片尺寸
  const size = clipImage.getSize()
  console.log(`图片尺寸: ${size.width}x${size.height}`)
  
  // 转换为 Data URL
  const dataUrl = clipImage.toDataURL()
  console.log('Data URL:', dataUrl.substring(0, 50) + '...')
}

// 判断剪贴板是否有图片
const hasImage = clipboard.has('image/png') || clipboard.has('image/jpeg')
console.log('剪贴板是否有图片:', hasImage)

HTML 操作

javascript
// 写入 HTML(同时写入纯文本版本)
clipboard.writeHTML('<h1 style="color: red;">Hello World</h1>')

// 读取 HTML
const html = clipboard.readHTML()
console.log(html)

// 同时写入 HTML 和纯文本
clipboard.write({
  text: 'Hello World',
  html: '<b style="color: blue;">Hello World</b>'
})

RTF 操作

javascript
// 写入 RTF 格式
const rtfContent = '{\\rtf1\\ansi\\b Hello\\b0 World}'
clipboard.writeRTF(rtfContent)

// 读取 RTF 格式
const rtf = clipboard.readRTF()
console.log(rtf)

高级用法

写入多种格式

javascript
const { clipboard, nativeImage } = require('electron')

// 同时写入多种格式
clipboard.write({
  text: 'Hello World',
  html: '<b>Hello World</b>',
  bookmark: 'My Bookmark',
  image: nativeImage.createFromPath('/path/to/image.png')
})

// 验证写入结果
console.log('文本:', clipboard.readText())
console.log('HTML:', clipboard.readHTML())

读取特定格式

javascript
// 检查是否有特定格式
const formats = ['text/plain', 'text/html', 'image/png']

formats.forEach(format => {
  if (clipboard.has(format)) {
    console.log(`✓ 剪贴板包含 ${format}`)
  } else {
    console.log(`✗ 剪贴板不包含 ${format}`)
  }
})

// 读取特定格式(使用 MIME 类型)
const customData = clipboard.read('public.utf8-plain-text')
console.log('自定义格式数据:', customData)

// 获取所有可用格式
const allFormats = clipboard.availableFormats()
console.log('所有可用格式:', allFormats)

书签操作(macOS)

javascript
const { clipboard } = require('electron')

if (process.platform === 'darwin') {
  // 写入书签
  clipboard.writeBookmark('Electron 官网', 'https://electronjs.org')
  
  // 读取书签
  const bookmark = clipboard.readBookmark()
  console.log('标题:', bookmark.title)   // 'Electron 官网'
  console.log('URL:', bookmark.url)     // 'https://electronjs.org'
  
  // 同时写入书签和文本
  clipboard.write({
    text: 'https://electronjs.org',
    bookmark: 'Electron 官网'
  })
}

查找文本操作(macOS)

javascript
const { clipboard } = require('electron')

if (process.platform === 'darwin') {
  // 写入查找文本(用于应用内的查找功能)
  clipboard.writeFindText('搜索关键词')
  
  // 读取查找文本
  const findText = clipboard.readFindText()
  console.log('查找文本:', findText)
}

在渲染进程中使用

通过 preload 脚本安全地暴露剪贴板功能。

主进程

javascript
// main.js
const { clipboard, nativeImage, ipcMain } = require('electron')

// 读取文本
ipcMain.handle('clipboard:readText', () => {
  return clipboard.readText()
})

// 写入文本
ipcMain.handle('clipboard:writeText', (event, text) => {
  clipboard.writeText(text)
  return true
})

// 读取图片
ipcMain.handle('clipboard:readImage', async () => {
  const image = clipboard.readImage()
  if (image.isEmpty()) return null
  return image.toDataURL()
})

// 写入图片
ipcMain.handle('clipboard:writeImage', (event, dataUrl) => {
  const image = nativeImage.createFromDataURL(dataUrl)
  clipboard.writeImage(image)
  return true
})

// 检查剪贴板是否有文本
ipcMain.handle('clipboard:hasText', () => {
  return clipboard.has('text/plain')
})

// 检查剪贴板是否有图片
ipcMain.handle('clipboard:hasImage', () => {
  return clipboard.has('image/png') || clipboard.has('image/jpeg')
})

// 清空剪贴板
ipcMain.handle('clipboard:clear', () => {
  clipboard.clear()
  return true
})

// 获取可用格式
ipcMain.handle('clipboard:availableFormats', () => {
  return clipboard.availableFormats()
})

Preload 脚本

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('clipboardAPI', {
  // 文本操作
  readText: () => ipcRenderer.invoke('clipboard:readText'),
  writeText: (text) => ipcRenderer.invoke('clipboard:writeText', text),
  
  // 图片操作
  readImage: () => ipcRenderer.invoke('clipboard:readImage'),
  writeImage: (dataUrl) => ipcRenderer.invoke('clipboard:writeImage', dataUrl),
  
  // 检查
  hasText: () => ipcRenderer.invoke('clipboard:hasText'),
  hasImage: () => ipcRenderer.invoke('clipboard:hasImage'),
  
  // 其他
  clear: () => ipcRenderer.invoke('clipboard:clear'),
  availableFormats: () => ipcRenderer.invoke('clipboard:availableFormats')
})

渲染进程

javascript
// renderer.js

// 复制文本
async function copyText(text) {
  const success = await window.clipboardAPI.writeText(text)
  if (success) {
    console.log('文本已复制到剪贴板')
  }
}

// 粘贴文本
async function pasteText() {
  const text = await window.clipboardAPI.readText()
  console.log('剪贴板文本:', text)
  return text
}

// 复制图片
async function copyImage() {
  // 从 Canvas 获取图片
  const canvas = document.getElementById('myCanvas')
  const dataUrl = canvas.toDataURL('image/png')
  
  const success = await window.clipboardAPI.writeImage(dataUrl)
  if (success) {
    console.log('图片已复制到剪贴板')
  }
}

// 粘贴图片
async function pasteImage() {
  const dataUrl = await window.clipboardAPI.readImage()
  if (dataUrl) {
    const img = document.getElementById('targetImage')
    img.src = dataUrl
  }
}

// 检查剪贴板内容
async function checkClipboard() {
  const hasText = await window.clipboardAPI.hasText()
  const hasImage = await window.clipboardAPI.hasImage()
  
  console.log('是否有文本:', hasText)
  console.log('是否有图片:', hasImage)
}

实用示例

复制按钮组件

javascript
// 主进程
const { clipboard, ipcMain } = require('electron')

ipcMain.handle('clipboard:copy', async (event, { type, content }) => {
  try {
    switch (type) {
      case 'text':
        clipboard.writeText(content)
        break
      case 'html':
        clipboard.writeHTML(content)
        break
      case 'image':
        const image = nativeImage.createFromDataURL(content)
        clipboard.writeImage(image)
        break
      default:
        throw new Error('不支持的类型')
    }
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 渲染进程
async function copyToClipboard(type, content) {
  const result = await window.clipboardAPI.copy({ type, content })
  
  if (result.success) {
    showToast('已复制到剪贴板')
  } else {
    showToast('复制失败: ' + result.error)
  }
}

监听剪贴板变化

javascript
const { clipboard, BrowserWindow } = require('electron')

class ClipboardWatcher {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.lastContent = {
      text: '',
      image: null
    }
    this.timer = null
  }
  
  start(interval = 500) {
    this.timer = setInterval(() => {
      this.checkChanges()
    }, interval)
  }
  
  stop() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }
  
  checkChanges() {
    // 检查文本变化
    const currentText = clipboard.readText()
    if (currentText !== this.lastContent.text) {
      this.lastContent.text = currentText
      this.onTextChange(currentText)
    }
    
    // 检查图片变化
    const currentImage = clipboard.readImage()
    if (!currentImage.isEmpty()) {
      const dataUrl = currentImage.toDataURL()
      if (dataUrl !== this.lastContent.image) {
        this.lastContent.image = dataUrl
        this.onImageChange(dataUrl)
      }
    }
  }
  
  onTextChange(text) {
    console.log('剪贴板文本变化:', text.substring(0, 50))
    this.mainWindow.webContents.send('clipboard:text-changed', text)
  }
  
  onImageChange(dataUrl) {
    console.log('剪贴板图片变化')
    this.mainWindow.webContents.send('clipboard:image-changed', dataUrl)
  }
}

// 使用
const watcher = new ClipboardWatcher(mainWindow)
watcher.start()

// 应用退出时停止监听
app.on('before-quit', () => {
  watcher.stop()
})

剪贴板历史记录管理器

javascript
const { clipboard } = require('electron')

class ClipboardHistory {
  constructor(maxSize = 100) {
    this.history = []
    this.maxSize = maxSize
    this.lastContent = {
      text: '',
      timestamp: 0
    }
    this.timer = null
  }
  
  start(interval = 500) {
    this.timer = setInterval(() => {
      const content = clipboard.readText()
      const timestamp = Date.now()
      
      if (content && content !== this.lastContent.text) {
        this.add(content, timestamp)
        this.lastContent = { text: content, timestamp }
      }
    }, interval)
  }
  
  stop() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }
  
  add(content, timestamp) {
    // 检查是否已存在
    const existing = this.history.findIndex(
      item => item.content === content
    )
    
    if (existing !== -1) {
      // 移动到最前面
      this.history.splice(existing, 1)
    }
    
    // 添加到历史记录
    this.history.unshift({
      content,
      timestamp,
      preview: this.getPreview(content)
    })
    
    // 限制大小
    if (this.history.length > this.maxSize) {
      this.history.pop()
    }
  }
  
  getPreview(content, maxLength = 100) {
    if (content.length <= maxLength) {
      return content
    }
    return content.substring(0, maxLength) + '...'
  }
  
  getHistory() {
    return this.history
  }
  
  restore(index) {
    if (this.history[index]) {
      clipboard.writeText(this.history[index].content)
      return true
    }
    return false
  }
  
  clear() {
    this.history = []
  }
  
  remove(index) {
    if (index >= 0 && index < this.history.length) {
      this.history.splice(index, 1)
      return true
    }
    return false
  }
}

// 使用
const clipboardHistory = new ClipboardHistory(50)
clipboardHistory.start()

// 获取历史记录
const history = clipboardHistory.getHistory()
console.log('历史记录数量:', history.length)

// 恢复历史记录
clipboardHistory.restore(0)  // 恢复第一条记录

// 应用退出时停止
app.on('before-quit', () => {
  clipboardHistory.stop()
})

格式支持

平台差异对照表

格式macOSWindowsLinux说明
text/plain纯文本
text/htmlHTML 内容
image/pngPNG 图片
image/jpegJPEG 图片
image/gifGIF 图片
image/bmp⚠️⚠️BMP 图片
text/uri-listURL 列表
text/rtf⚠️RTF 格式
public.bookmark书签(仅 macOS)
public.utf8-plain-textUTF-8 文本

格式检测与转换

javascript
const { clipboard, nativeImage } = require('electron')

// 检测剪贴板内容类型
function detectClipboardType() {
  const formats = clipboard.availableFormats()
  
  if (formats.some(f => f.startsWith('image/'))) {
    return 'image'
  }
  
  if (formats.includes('text/html')) {
    return 'html'
  }
  
  if (formats.includes('text/plain')) {
    return 'text'
  }
  
  return 'unknown'
}

// 根据类型读取内容
function readClipboardContent() {
  const type = detectClipboardType()
  
  switch (type) {
    case 'image':
      return {
        type: 'image',
        data: clipboard.readImage().toDataURL()
      }
    case 'html':
      return {
        type: 'html',
        data: clipboard.readHTML()
      }
    case 'text':
      return {
        type: 'text',
        data: clipboard.readText()
      }
    default:
      return null
  }
}

性能优化

1. 减少剪贴板访问频率

javascript
// ❌ 避免:频繁访问剪贴板
setInterval(() => {
  const text = clipboard.readText()
  console.log(text)
}, 100)  // 太频繁

// ✅ 推荐:合理的访问间隔
setInterval(() => {
  const text = clipboard.readText()
  console.log(text)
}, 500)  // 500ms 比较合适

2. 缓存剪贴板内容

javascript
// 缓存上次的剪贴板内容
let cachedText = ''
let cachedImage = null

function getCachedClipboard() {
  const currentText = clipboard.readText()
  
  if (currentText !== cachedText) {
    cachedText = currentText
    return { changed: true, text: currentText }
  }
  
  return { changed: false, text: cachedText }
}

3. 图片处理优化

javascript
// 读取大图片时使用缩略图
function readImageThumbnail(maxSize = 100) {
  const image = clipboard.readImage()
  
  if (image.isEmpty()) {
    return null
  }
  
  const size = image.getSize()
  
  // 如果图片很小,直接返回
  if (size.width <= maxSize && size.height <= maxSize) {
    return image.toDataURL()
  }
  
  // 缩放图片
  const resized = image.resize({
    width: maxSize,
    height: maxSize,
    quality: 'good'
  })
  
  return resized.toDataURL()
}

安全注意事项

1. 隐私保护

javascript
// 监听剪贴板时,注意用户隐私
class SecureClipboardWatcher {
  constructor() {
    this.enabled = false
  }
  
  start() {
    // 提示用户应用正在监听剪贴板
    const consent = askUserConsent('应用将监听剪贴板变化,是否同意?')
    
    if (consent) {
      this.enabled = true
      // 开始监听
    }
  }
  
  stop() {
    this.enabled = false
  }
  
  onClipboardChange(content) {
    // 不要记录敏感信息
    if (this.looksSensitive(content)) {
      console.log('检测到敏感内容,已忽略')
      return
    }
    
    // 处理内容
  }
  
  looksSensitive(content) {
    // 检测密码、信用卡号等敏感信息
    const patterns = [
      /^\d{16}$/,  // 可能是信用卡号
      /^password:/i,
      /^secret:/i
    ]
    
    return patterns.some(p => p.test(content))
  }
}

2. 避免注入攻击

javascript
// ❌ 危险:直接使用剪贴板 HTML
document.innerHTML = clipboard.readHTML()

// ✅ 安全:清理 HTML 内容
function sanitizeHTML(html) {
  const div = document.createElement('div')
  div.textContent = html  // 转义 HTML
  return div.innerHTML
}

// 或使用 DOMPurify 库
const clean = DOMPurify.sanitize(clipboard.readHTML())
document.innerHTML = clean

3. 限制剪贴板内容大小

javascript
const MAX_TEXT_SIZE = 1024 * 1024  // 1MB

function safeReadText() {
  const text = clipboard.readText()
  
  if (text && text.length > MAX_TEXT_SIZE) {
    console.warn('剪贴板文本过大,已截断')
    return text.substring(0, MAX_TEXT_SIZE)
  }
  
  return text
}

常见问题解答

1. 剪贴板内容为空怎么办?

javascript
const image = clipboard.readImage()

if (image.isEmpty()) {
  console.log('剪贴板没有图片')
} else {
  console.log('图片尺寸:', image.getSize())
}

2. 如何在 macOS 上访问选区剪贴板?

javascript
if (process.platform === 'darwin') {
  // 读取当前选中的文本
  const selectedText = clipboard.readText('selection')
  console.log('选中的文本:', selectedText)
}

3. 如何实现富文本复制?

javascript
// 复制富文本(同时包含 HTML 和纯文本)
clipboard.write({
  text: '这是纯文本版本',
  html: '<b style="color: red;">这是富文本版本</b>'
})

// 粘贴时应用会根据情况选择合适的格式

4. 如何复制文件路径?

javascript
// 方式1:复制为文本
clipboard.writeText('/path/to/file.txt')

// 方式2:复制为文件 URL
clipboard.write({
  text: '/path/to/file.txt',
  bookmark: 'file:///path/to/file.txt'
})

// 方式3:使用 URI 列表
clipboard.write({
  text: 'file:///path/to/file.txt',
  html: '<a href="file:///path/to/file.txt">文件链接</a>'
})

5. 剪贴板监听在不同平台上的表现如何?

平台选区剪贴板监听性能特殊行为
macOS✅ 支持优秀可以访问选区剪贴板
Windows❌ 不支持良好需要轮询检测
Linux❌ 不支持一般X11/Wayland 有差异

6. 如何处理剪贴板锁定的情况?

javascript
function safeWriteText(text, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      clipboard.writeText(text)
      return true
    } catch (error) {
      console.warn(`写入剪贴板失败,重试 ${i + 1}/${retries}`)
      // 等待一段时间后重试
      Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
    }
  }
  return false
}

最佳实践

1. 提供用户反馈

javascript
async function copyWithFeedback(text) {
  try {
    clipboard.writeText(text)
    // 显示成功提示
    showNotification('已复制到剪贴板')
  } catch (error) {
    showNotification('复制失败')
  }
}

2. 支持多种格式

javascript
function smartCopy(content) {
  if (content.type === 'image') {
    clipboard.writeImage(nativeImage.createFromDataURL(content.data))
  } else if (content.type === 'html') {
    clipboard.write({
      text: content.plainText,
      html: content.html
    })
  } else {
    clipboard.writeText(content.text)
  }
}

3. 尊重用户隐私

javascript
// 监听剪贴板前征得用户同意
if (await askUserConsent()) {
  startClipboardWatcher()
}

// 提供关闭选项
function stopClipboardWatcher() {
  watcher.stop()
}

4. 跨平台适配

javascript
function getClipboardContent() {
  const isMac = process.platform === 'darwin'
  
  const result = {
    text: clipboard.readText(),
    html: clipboard.readHTML(),
    image: null
  }
  
  const image = clipboard.readImage()
  if (!image.isEmpty()) {
    result.image = image.toDataURL()
  }
  
  // macOS 可以获取选区内容
  if (isMac) {
    result.selection = clipboard.readText('selection')
  }
  
  return result
}

参考链接