shell 与 clipboard:打开外部资源与剪贴板读写
本教程共 45 篇 · 第 24 篇 · 更新于 2026-08-03
24. shell 与 clipboard:打开外部链接与读写剪贴板
本节目标
- 理解
shell模块打开外部链接和文件的用法。 - 掌握
openExternal与openPath的区别与平台注意。 - 用
clipboard读写系统剪贴板里的文本与图像。 - 在沙箱渲染进程里,通过 IPC 安全调用这两个模块。
- 留意 Windows 链接长度、剪贴板来源等平台差异。
24-1 两个模块的定位
shell 与 clipboard 都属于「桌面集成」类能力。它们让应用跳出窗口,去操作用户系统里的默认程序、文件管理器和剪贴板。
shell 主要做三件事:用默认浏览器打开网址、用关联程序打开本地文件、在文件管理器里定位文件。clipboard 则是系统剪贴板的读写接口,可存取文本、HTML、图像等。
需要特别注意的是,这两个模块虽然文档上标注「主进程可用」,但也能在渲染进程调用。问题在于:一旦渲染进程开启了沙箱(sandbox: true),它们就无法在渲染进程里直接使用;而且沙箱化的 preload 可用模块子集也不含 shell 与 clipboard,所以也不能在 preload 里 require('electron/main') 直接调用它们。
因此本教程统一采用安全写法:需要触发这些能力时,由渲染进程通过 contextBridge 暴露的 API 经 IPC 调主进程完成。这样既保留能力,又不会破坏隔离模型。
24-2 用 shell.openExternal 打开外部链接
openExternal(url) 会用系统默认的关联程序打开一个外部协议链接。最常见的场景是打开 https:// 网址,系统会用默认浏览器加载它。
它返回一个 Promise<void>,成功或失败都通过 Promise 结算。下面的例子假设用户点击了页面上的「访问官网」按钮。
// 主进程 main.js
const { app, BrowserWindow, ipcMain, shell } = require('electron/main')
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// 安全默认:开启上下文隔离与沙箱,关闭 Node 集成
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: './preload.js'
}
})
win.loadFile('index.html')
// 响应渲染进程的打开链接请求
ipcMain.handle('open-external', async (event, url) => {
await shell.openExternal(url)
})
})
// 预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('desktop', {
openExternal: (url) => ipcRenderer.invoke('open-external', url)
})
// 渲染进程 renderer.js
document.getElementById('openSite').addEventListener('click', () => {
window.desktop.openExternal('https://www.electronjs.org')
})
这样写,渲染进程永远拿不到 shell 对象本身,只得到一个受限的 openExternal 方法,安全风险被降到最低。
24-3 用 shell.openPath 打开文件与文件夹
openPath(path) 用来用系统默认方式打开一个本地文件或文件夹。传入一个文件路径,系统会用关联程序打开它,比如传入一个 PDF 会调用默认阅读器。
它返回 Promise<string>:失败时解析为错误描述字符串,成功则为空字符串。可以用返回值判断打开是否出错。
// 主进程 main.js
const { ipcMain, shell } = require('electron/main')
ipcMain.handle('open-path', async (event, filePath) => {
const error = await shell.openPath(filePath)
if (error) {
console.error('打开失败:', error)
}
})
// 预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('desktop', {
openPath: (filePath) => ipcRenderer.invoke('open-path', filePath)
})
另外两个常用方法:showItemInFolder(fullPath) 会在文件管理器里选中并高亮该文件;trashItem(path) 把文件移入回收站(macOS 的废纸篓、Windows 的回收站)。删除操作建议先和用户确认,避免误删。
// 主进程 main.js
const { shell } = require('electron/main')
// 在文件管理器里定位并选中
shell.showItemInFolder('/Users/me/docs/report.pdf')
// 移入回收站(注意路径分隔符要符合平台)
shell.trashItem('/Users/me/docs/old.txt')
24-4 clipboard 文本读写
clipboard 的文本读写是最常用的一组方法。writeText(text) 写入纯文本,readText() 读出当前剪贴板文本。
在 v40 之后,渲染进程里直接调用 clipboard 已被标记为废弃;而沙箱(sandbox)下的 preload 也不允许直接 require('electron/main') 拿到 clipboard。正确做法是把调用放到主进程,渲染进程通过 contextBridge 暴露的 API 经 IPC 调主进程完成。
// 主进程 main.js
const { ipcMain, clipboard } = require('electron/main')
ipcMain.handle('clipboard-write-text', (event, text) => {
clipboard.writeText(text)
})
ipcMain.handle('clipboard-read-text', () => {
return clipboard.readText()
})
ipcMain.handle('clipboard-clear', () => {
clipboard.clear()
})
// 预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('clipboardApi', {
writeText: (text) => ipcRenderer.invoke('clipboard-write-text', text),
readText: () => ipcRenderer.invoke('clipboard-read-text'),
clear: () => ipcRenderer.invoke('clipboard-clear')
})
// 渲染进程 renderer.js
const input = document.getElementById('text')
const output = document.getElementById('output')
document.getElementById('copy').addEventListener('click', () => {
window.clipboardApi.writeText(input.value)
})
document.getElementById('paste').addEventListener('click', () => {
output.value = window.clipboardApi.readText()
})
除了纯文本,clipboard 还支持 writeHTML / readHTML 读写带格式的富文本,以及 writeRTF / readRTF 读写 RTF。一次写入多种格式可用 clipboard.write({ text, html, image }),这样粘贴到不同程序时会自动选择最合适的一种。
24-5 clipboard 图像读写
剪贴板不仅能存文字,也能存图像。readImage() 返回的是一个 NativeImage 实例(下一章会细讲),writeImage(image) 则把一个 NativeImage 写入剪贴板。
典型用途是「复制截图到剪贴板」或「把当前画布导出成图片后复制」。下面演示把一张本地图片读入并写入剪贴板。
// 主进程 main.js
const { ipcMain, clipboard, nativeImage } = require('electron/main')
ipcMain.handle('clipboard-copy-image', (event, filePath) => {
const image = nativeImage.createFromPath(filePath)
clipboard.writeImage(image)
})
ipcMain.handle('clipboard-read-image-dataurl', () => {
const image = clipboard.readImage()
return image.toDataURL()
})
// 预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('clipboardApi', {
copyImageFromFile: (filePath) => ipcRenderer.invoke('clipboard-copy-image', filePath),
readImageAsDataURL: () => ipcRenderer.invoke('clipboard-read-image-dataurl')
})
// 渲染进程 renderer.js
// 复制一张图片到剪贴板
window.clipboardApi.copyImageFromFile('/Users/me/pics/logo.png')
// 读取剪贴板里的图片并显示
const url = window.clipboardApi.readImageAsDataURL()
document.getElementById('preview').src = url
读取图像时要先判断剪贴板是否为空:image.isEmpty() 为 true 时说明当前没有图像内容,避免后续处理出错。
24-6 平台注意事项
使用这两个模块时,有几个跨平台差异容易踩坑,这里集中说明。
Windows 上 openExternal 的 url 长度上限约为 2081 个字符,超出可能被截断。需要打开很长的链接时,建议改用 openPath 打开本地文件,或缩短链接。
macOS 与 Windows 支持 readBookmark / writeBookmark,可把带标题的书签写入剪贴板,但 Windows 上 title 会被忽略。Linux 额外存在一个名为 selection 的剪贴板(中键选区),相关方法可传 'selection' 作为 type 参数。
剪贴板是全局共享资源,其他程序随时可能修改它。读取前最好用 availableFormats() 查看当前有哪些格式,再用 readText / readImage 取对应内容,这样更稳健。
24-7 一个组合小场景
把 shell 与 clipboard 组合起来,可以做出很实用的小功能。比如用户在页面里选中一段文字,点击「复制并打开反馈页」,应用先把文字写进剪贴板,再用默认浏览器打开反馈表单,用户直接粘贴即可。
这个流程完全走我们前面建立的 IPC 通道,渲染进程只负责触发和收集,真正的系统操作都在主进程完成,安全边界清晰。
// 主进程 main.js
const { ipcMain, clipboard, shell } = require('electron/main')
ipcMain.handle('feedback-copy-and-open', async (event, text) => {
clipboard.writeText(text)
await shell.openExternal('https://github.com/your-name/app/issues/new')
})
// 预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('feedback', {
copyAndOpen: (text) => ipcRenderer.invoke('feedback-copy-and-open', text)
})
日常开发里,这类「复制信息 + 打开外部页面」的需求非常多,统一收敛到主进程处理,也能避免不同页面各自实现带来的行为不一致。
再补充两个实用细节。读取剪贴板前,可用 clipboard.availableFormats() 查看当前有哪些格式,据此决定调用 readText 还是 readImage,避免取到空内容。Linux 上还存在名为 selection 的中键选区剪贴板,相关方法可传 'selection' 作为 type 参数,跨平台代码要记得为非 Linux 平台忽略这个参数。
另外,shell.openExternal 不要对用户输入的字符串直接拼接。应在调用前校验协议,仅允许 http、https、mailto 等安全协议,拒绝 javascript: 之类的危险协议,防止通过精心构造的链接执行意外行为。
常见误区
不要把 shell 或 clipboard 直接暴露给渲染进程。旧教程里常见 require('electron').shell 在渲染进程调用的写法,在沙箱模式下会失效,且违反最小权限原则。
不要在渲染进程里写 nodeIntegration: true 来「图方便」调用这些模块。正确做法始终是通过 preload 的 contextBridge 暴露少量安全方法。
shell.openExternal 不要拼接用户不可信的字符串作为 URL。恶意构造的 javascript: 等协议可能带来风险,打开前应当校验协议白名单(仅允许 http/https/mailto 等)。