剪贴板
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) | String | String | 读取剪贴板中的文本 |
writeText(text, type) | String, String | void | 写入文本到剪贴板 |
readHTML(type) | String | String | 读取剪贴板中的 HTML |
writeHTML(markup, type) | String, String | void | 写入 HTML 到剪贴板 |
readImage(type) | String | NativeImage | 读取剪贴板中的图片 |
writeImage(image, type) | NativeImage, String | void | 写入图片到剪贴板 |
readRTF(type) | String | String | 读取 RTF 格式内容 |
writeRTF(text, type) | String, String | void | 写入 RTF 格式内容 |
readBookmark() | - | Object | 读取书签(macOS) |
writeBookmark(title, url, type) | String, String, String | void | 写入书签(macOS) |
readFindText() | - | String | 读取查找文本(macOS) |
writeFindText(text) | String | void | 写入查找文本(macOS) |
clear(type) | String | void | 清空剪贴板 |
has(format, type) | String, String | Boolean | 检查是否有指定格式 |
read(format) | String | String | 读取指定格式数据 |
write(data, type) | Object, String | void | 写入多种格式数据 |
availableFormats(type) | String | String[] | 获取可用格式列表 |
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()
})格式支持
平台差异对照表
| 格式 | macOS | Windows | Linux | 说明 |
|---|---|---|---|---|
text/plain | ✅ | ✅ | ✅ | 纯文本 |
text/html | ✅ | ✅ | ✅ | HTML 内容 |
image/png | ✅ | ✅ | ✅ | PNG 图片 |
image/jpeg | ✅ | ✅ | ✅ | JPEG 图片 |
image/gif | ✅ | ✅ | ✅ | GIF 图片 |
image/bmp | ⚠️ | ✅ | ⚠️ | BMP 图片 |
text/uri-list | ✅ | ✅ | ✅ | URL 列表 |
text/rtf | ✅ | ✅ | ⚠️ | RTF 格式 |
public.bookmark | ✅ | ❌ | ❌ | 书签(仅 macOS) |
public.utf8-plain-text | ✅ | ✅ | ✅ | UTF-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 = clean3. 限制剪贴板内容大小
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
}