Electron webContents 完全指南:掌控页面渲染、导航与窗口交互
webContents 是 Electron 中"渲染并控制网页内容"的核心对象:每个 BrowserWindow 背后都有一个 webContents 实例负责承载页面,应用开发者通过它完成页面加载、导航拦截、脚本注入、进程治理、离屏渲染与 IPC 通信等几乎所有"浏览器能力"层面的操作。读完本文,你将系统掌握 Electron webContents 的事件体系、模块级查找 API、全部实例方法/属性的使用边界,并能直接把这些能力组合到真实的主进程代码中。
什么是 webContents:进程归属与访问方式
webContents 是一个继承自 Node.js EventEmitter 的对象,运行在**主进程(Main Process)**中,是 BrowserWindow 对象的属性,负责渲染并控制一个网页。最基础的访问方式如下:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')
const contents = win.webContents
console.log(contents)
值得注意的是:WebContents 类并不从 'electron' 模块直接导出,它只作为其他 API 的返回值存在——最常见的来源就是 BrowserWindow.webContents、<webview> 的 guest 页面,以及 WebContentsView 等基于 View 体系的容器。模块本身仍可整体引入(见下文"模块级方法")。
从仓库源码结构看,Electron 对 WebContents 的实现分两层:
- TypeScript 层 lib/browser/api/web-contents.ts(942 行):负责将 C++ 层
electron_browser_web_contents绑定对象扩展为完整的 JS API。例如postMessage、send、sendToFrame被转发到主帧对象(mainFrame.send/mainFrame.postMessage);executeJavaScript通过ipcMainUtils.invokeInWebContents把代码投递给渲染进程的webFrame执行(代码必须在页面加载完成后才运行,waitTillCanExecuteJavaScript会等待did-stop-loading);loadFile则基于url.format拼出file://协议 URL 再走loadURL。 - C++ 层 shell/browser/api/electron_api_web_contents.h 与 shell/browser/api/electron_api_web_contents.cc(共 6000+ 行):真正对接 Chromium 的
content::WebContents、WebContentsObserver、WebContentsDelegate,是全部事件(如导航回调、渲染进程异常)与底层行为的发源地。
模块级方法:全局查找 WebContents 实例
从模块可直接调用的静态方法,用于在主进程中检索已有的 WebContents:
const { webContents } = require('electron')
console.log(webContents)
| 方法 | 返回值 | 说明 |
|---|---|---|
webContents.getAllWebContents() |
WebContents[] |
返回所有 WebContents 实例数组,覆盖所有窗口、webview、已打开的 DevTools,以及 DevTools 扩展的 background page |
webContents.getFocusedWebContents() |
WebContents | null |
返回当前应用中获得焦点的 webContents,否则为 null。其实现(见 lib/browser/api/web-contents.ts)会遍历全部实例,若某个 webview 的 webContents 同时被宿主窗口报为 focused,则优先返回 webview 实例 |
webContents.fromId(id) |
WebContents | undefined |
根据数字 ID 取回实例。ID 在应用内唯一,可由实例的只读属性 contents.id 获得 |
webContents.fromFrame(frame) |
WebContents | undefined |
根据 WebFrameMain 找到其宿主 WebContents |
webContents.fromDevToolsTargetId(targetId) |
WebContents | undefined |
根据 CDP 的 TargetID 反查实例,常配合 contents.debugger 使用 |
其中 fromId、fromFrame、fromDevToolsTargetId 与 getAllWebContents 在 JS 层都只是透传 C++ 绑定(见 lib/browser/api/web-contents.ts)。fromDevToolsTargetId 的典型用法是:通过调试器拿到当前页的 TargetID 后,再换回对应的 WebContents 做后续操作:
async function lookupTargetId (browserWindow) {
const wc = browserWindow.webContents
await wc.debugger.attach('1.3')
const { targetInfo } = await wc.debugger.sendCommand('Target.getTargetInfo')
const { targetId } = targetInfo
const targetWebContents = await wc.fromDevToolsTargetId(targetId)
}
导航事件体系:精确掌握页面生命周期
导航事件是 webContents 最有价值的能力之一,可用于监控、拦截甚至取消导航。理解这套体系前,先分清三种导航类型。
Document Navigation(文档级导航)
当 webContents 跳转到另一个页面(区别于页内导航)时,会按顺序触发以下事件:
did-start-navigationwill-frame-navigatewill-navigate(仅主帧导航时触发)will-redirect(导航发生重定向时触发)did-redirect-navigation(重定向后触发,不可拦截)did-frame-navigatedid-navigate(仅主帧导航完成时触发)
其中 will-frame-navigate、will-navigate、will-redirect 等可取消事件一旦调用了 event.preventDefault(),后续事件便不再触发。
In-page Navigation(页内导航)
点击锚点、修改 window.location.hash、pushState/replaceState 及同页历史前进/后退都属于页内导航,不会触发 reload,也不可取消。事件顺序固定为:
did-start-navigationdid-navigate-in-page
Frame Navigation(帧级导航)
will-navigate 与 did-navigate 只在主帧(mainFrame)导航时触发。若要同时监听 <iframe> 及嵌套 iframe 的导航,应改用 will-frame-navigate 与 did-frame-navigate。这两组事件天然形成互补:需要"整页级"守卫时用前者,需要"任意帧级"观测时用后者。
从源码看,这些事件与 Chromium 的 WebContentsObserver 回调一一对应,声明在 shell/browser/api/electron_api_web_contents.h 中;同时 loadURL 返回的 Promise 在 JS 层也是依赖 did-finish-load / did-fail-load / did-start-navigation / did-stop-loading / did-navigate-in-page 这组事件协作完成的(见 lib/browser/api/web-contents.ts):一旦主帧开始了一次与本次 load 无关的新导航,旧 Promise 会以 ERR_ABORTED 拒绝。
加载与导航控制方法
contents.loadURL(url[, options]) 与 contents.loadFile(filePath[, options])
loadURL 在窗口中加载指定 URL,URL 必须带协议前缀(如 http://、file://)。如需绕过 HTTP 缓存,可借助 pragma 请求头。
const win = new BrowserWindow()
const options = { extraHeaders: 'pragma: no-cache\n' }
win.webContents.loadURL('https://github.com', options)
loadURL 支持的关键 options 包括:
httpReferrer:HTTP Referrer 来源;userAgent:发起请求的 UA;extraHeaders:以"\n"分隔的额外请求头;postData:UploadRawData[] | UploadFile[],构造带 POST 体的导航;baseURLForDataURL:仅当 URL 是data:且需要加载其他文件时才需要的基础路径。
两者都返回 Promise<void>:页面加载完成(即 did-finish-load 触发)时 resolve,加载失败(did-fail-load 触发)时 reject。若当前页面带有 beforeunload 处理器,did-fail-load 仍会被调用(除非 will-prevent-unload 被正确处理)。JS 层实现会在 _awaitNextLoad 内预挂一个空 rejection 处理器以避免未处理的 Promise 异常(见 lib/browser/api/web-contents.ts)。
loadFile 接收相对于应用根目录的 HTML 文件路径,是加载本地页面最稳妥的方式。对如下目录结构:
| root
| - package.json
| - src
| - main.js
| - index.html
调用 win.loadFile('src/index.html') 即可。其 options(query、search、hash)会传给 url.format(),最终生成带参数的 file:// URL。
状态查询与控制
加载过程中的"状态查询/控制"方法族:
getURL()/getTitle():当前页面 URL 与标题;isLoading()/isLoadingMainFrame()/isWaitingForResponse():资源仍在加载 / 仅主帧仍在加载 / 正等待主资源首响应;stop():停止任何待处理的导航;reload()/reloadIgnoringCache():刷新当前页(后者忽略缓存);isDestroyed()/isCrashed():判断页面是否已销毁 / 渲染进程是否崩溃;close([opts]):等效于网页自身调用window.close()。当页面关闭成功(未被页面阻止卸载,或未要求等待),WebContents 会被销毁且不可再用,随后触发destroyed事件。选项waitForBeforeUnload: true时,会先触发beforeunload;若页面阻止卸载,则不关闭,此时触发will-prevent-unload。
导航历史:NavigationHistory(旧 API 已弃用)
goBack/goForward/goToIndex/goToOffset/canGoBack/canGoForward/canGoToOffset/clearHistory 这些直接挂在 contents 上的历史导航方法均已弃用,一律改用只读属性 contents.navigationHistory(NavigationHistory 类型见 docs/api/navigation-history.md)上的同名方法:
contents.navigationHistory.canGoBack()/canGoForward()/canGoToOffset(offset)contents.navigationHistory.goBack()/goForward()/goToIndex(index)/goToOffset(offset)contents.navigationHistory.clear()- 另有
length、getActiveIndex()、getEntryAtIndex(index)、getAllEntries()、removeEntryAtIndex(index)、restore({ index, entries })
该属性在 JS 层的 _init 中完成装配,旧方法仅作为弃用警告壳转发(见 lib/browser/api/web-contents.ts)。
窗口创建治理:setWindowOpenHandler 与 did-create-window
setWindowOpenHandler(handler)
当渲染进程请求创建新窗口(window.open()、<a target="_blank">、shift+click 链接、<form target="_blank"> 提交等)时,主进程会先调用此 handler 决定放行还是拒绝:
- 返回
{ action: 'deny' }:取消新窗口创建; - 返回
{ action: 'allow' }:允许创建;还可附带overrideBrowserWindowOptions、createWindow、outlivesOpener选项实现深度定制; - 返回
null/undefined/ 无合法action的对象:会打印 console 错误并等效于deny。
handler 收到的 details 对象包含:url(被 resolve 过的最终 URL,例如对 window.open('foo') 会得到类似 https://the-origin/the/current/path/foo)、frameName、features(window.open 传入的逗号分隔特性串)、disposition(见下文枚举)、referrer、postBody(仅表单 target=_blank 时存在)。
一个非常实用的模式是把新窗口创建过程定制为挂在主窗口内的 BrowserView(对应文档 docs/api/browser-view.md):
const { BrowserView, BrowserWindow } = require('electron')
const mainWindow = new BrowserWindow()
mainWindow.webContents.setWindowOpenHandler((details) => {
return {
action: 'allow',
createWindow: (options) => {
const browserView = new BrowserView(options)
mainWindow.addBrowserView(browserView)
browserView.setBounds({ x: 0, y: 0, width: 640, height: 480 })
// 对于 background-tab(中键或 Ctrl/Cmd+点击),options.webContents 未定义,需手动加载
if (details.disposition === 'background-tab') {
browserView.webContents.loadURL(details.url)
}
return browserView.webContents
}
}
})
disposition 的取值与触发方式对应 Chromium 的 WindowOpenDisposition:
default:Chromium 认为窗口内导航合法的场景;foreground-tab:左键点击或 shift + 中键点击;background-tab:中键点击或 Ctrl/Cmd + 点击;new-window:shift + 左键点击;other:其余未被 Electron 显式处理的打开方式。
did-create-window 事件
在渲染进程通过 window.open 成功创建窗口之后触发(若被 setWindowOpenHandler 取消则不触发)。回调参数中的 details 提供了 url、frameName、options(合并优先级从低到高为:window.open() features 串解析结果 → 继承自父窗口的安全相关 webPreferences → setWindowOpenHandler 给出的选项,未识别的选项不会被过滤)、referrer、postBody、disposition。两个 API 的配合用法详见 docs/api/window-open.md。
关键导航与页面状态事件详解
导航事件(主帧 / 任意帧 / 页内)
现代版本中,导航类事件统一携带 details 对象(旧式平铺参数已标记弃用),字段包括:
url:帧正在导航到的 URL;isSameDocument:是否未替换文档(锚点跳转、pushState/replaceState、同页历史导航为true);isMainFrame:是否主帧;frame:WebFrameMain | null,正在导航的帧(帧已导航或销毁后可能为null);initiator:发起导航的帧(父帧、子帧或null)。
will-navigate 表示用户或页面要开始主帧导航(修改 window.location、点击页面内链接等);程序化导航(loadURL、back)不触发,页内导航也不触发。调用 event.preventDefault() 可阻止该次导航。若要在 iframe 级别也得到"将导航"通知,监听 will-frame-navigate。
will-redirect(服务端重定向前,如 302)与 did-redirect-navigation(重定向发生后)成对出现:前者可 preventDefault()(会阻止整个导航而不仅是重定向),后者不可拦截。
did-navigate 在主帧导航完成时触发,携带 url、httpResponseCode(非 HTTP 导航为 -1)、httpStatusText(非 HTTP 导航为空串);did-frame-navigate 则对任意帧导航完成触发,并额外携带 isMainFrame、frameProcessId、frameRoutingId。两者都不覆盖页内导航。
加载进度与失败事件
did-start-navigation:任意帧(含主帧)开始导航;did-start-loading/did-stop-loading:对应标签页 loading spinner 开始/停止转动的时间点;dom-ready:顶层 frame 的 document 加载完成;did-finish-load:导航完成、spinner 停止、onload已派发;did-frame-finish-load:某一帧完成导航(含isMainFrame、frameProcessId、frameRoutingId);did-fail-load:加载失败(类似did-finish-load的失败版)。参数含errorCode、errorDescription、validatedURL、isMainFrame等,完整错误码清单对应 Chromium 的net/base/net_error_list.h;did-fail-provisional-load:加载被取消(例如调用了window.stop())时触发;did-navigate-in-page:任意帧发生页内导航(锚点点击、hashchange触发等)。URL 改变但未离开当前页面。
卸载确认:will-prevent-unload
当页面 beforeunload 处理器试图取消卸载时触发。调用 event.preventDefault() 会忽略页面阻止并放行卸载。下面的模式可把"是否离开"的决定权交给用户:
const { BrowserWindow, dialog } = require('electron')
const win = new BrowserWindow({ width: 800, height: 600 })
win.webContents.on('will-prevent-unload', (event) => {
const choice = dialog.showMessageBoxSync(win, {
type: 'question',
buttons: ['Leave', 'Stay'],
title: 'Do you want to leave this site?',
message: 'Changes you made may not be saved.',
defaultId: 0,
cancelId: 1
})
const leave = (choice === 0)
if (leave) {
event.preventDefault()
}
})
注意:该事件对
BrowserView也会触发但不会被执行——Electron 刻意不把 BrowserView 的生命周期绑定到其宿主 BrowserWindow。
页面元信息事件
page-title-updated:导航期间页面标题被设置时触发。explicitSet为false表示标题由 file URL 合成而来;page-favicon-updated:页面收到 favicon URL 列表时触发,参数favicons: string[];did-change-theme-color:页面主题色变化时触发,颜色格式为'#rrggbb'(无主题色时为null),通常来自<meta name='theme-color' content='#ff0000'>;update-target-url:鼠标悬停或键盘焦点移到某个链接上时触发;cursor-changed:光标类型改变时触发。type取值覆盖pointer、hand、text、e-resize、grab、custom、not-allowed、zoom-in等常见形态;当type === 'custom'时,image(NativeImage)、scale、size、hotspot会描述自定义光标。
页面渲染进程的稳定性管理
render-process-gone:渲染进程意外消失(崩溃或被 kill)。details结构见 docs/api/structures/render-process-gone-details.md。JS 层会在收到后向app转发同名事件(见 lib/browser/api/web-contents.ts),便于在应用级统一处理;unresponsive:网页无响应;responsive:页面恢复响应。forcefullyCrashRenderer():强制终止承载当前webContents的渲染进程,随后会以reason=killed || crashed触发render-process-gone。注意部分 webContents 共享渲染进程,调用可能波及其他宿主。紧接调用reload()会在新进程中强制重载,常用于从unresponsive状态恢复:
const win = new BrowserWindow()
win.webContents.on('unresponsive', async () => {
const { response } = await dialog.showMessageBox({
message: 'App X has become unresponsive',
title: 'Do you want to try forcefully reloading the app?',
buttons: ['OK', 'Cancel'],
cancelId: 1
})
if (response === 0) {
win.webContents.forcefullyCrashRenderer()
win.webContents.reload()
}
})
执行脚本与样式注入
executeJavaScript(code[, userGesture])
在页面中求值一段 JS,返回 Promise<any>,代码结果为 rejected promise 时返回的 Promise 也会 reject。代码执行会挂起直到页面停止加载;某些仅能由用户手势触发的 HTML API(如 requestFullScreen)可通过 userGesture: true 绕过该限制:
const win = new BrowserWindow()
win.webContents.executeJavaScript('fetch("https://jsonplaceholder.typicode.com/users/1").then(resp => resp.json())', true)
.then((result) => {
console.log(result) // fetch 返回的 JSON 对象
})
executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture])
与 executeJavaScript 行为类似,但在隔离上下文中执行。worldId 中 0 为默认 world,999 是 Electron contextIsolation 特性使用的 world,也可传任意整数;scripts 为 WebSource[](结构见 docs/api/structures/web-source.md)。
insertCSS(css[, options]) / removeInsertedCSS(key)
向当前页面注入 CSS,返回 Promise 解析出唯一 key,可用该 key 反向移除。cssOrigin 可选 'user' 或 'author'(决定样式表的级联来源,默认 author):
const win = new BrowserWindow()
win.webContents.on('did-finish-load', async () => {
const key = await win.webContents.insertCSS('html, body { background-color: #f00; }')
win.webContents.removeInsertedCSS(key)
})
编辑、选区与缩放控制
文本编辑命令
undo()、redo()、cut()、copy()、paste()、pasteAndMatchStyle()、delete()、selectAll()、unselect()、replace(text)、replaceMisspelling(text) 在页面中执行对应编辑命令;centerSelection() 将当前文本选区在页面内居中;insertText(text) 向焦点元素插入文本(返回 Promise);copyImageAt(x, y) 把坐标处图片拷入剪贴板;对视频媒体元素调用 copyVideoFrameAt(x, y) 可复制该帧,saveVideoFrameAs(x, y) 会弹出保存对话框把该帧存盘。
adjustSelection(options) 以指定偏移调整当前选区起止点(start/end 为正则向后、为负则向前),适用于"选区微调"类 UI:
// 选区起点后移 1 个字符、终点后移 5 个字符
win.webContents.adjustSelection({ start: 1, end: 5 })
// 起点后移 2 个字符、终点前移 3 个字符
win.webContents.adjustSelection({ start: 2, end: -3 })
效果对比如下(图片源自 docs/images/web-contents-text-selection-before.png 与 docs/images/web-contents-text-selection-after.png):
缩放控制:factor / level / mode
setZoomFactor(factor):缩放因子 = 缩放百分比 ÷ 100(300% 即3.0),必须大于0.0;setZoomLevel(level):缩放级别,原始大小0,每增减 1 代表放大/缩小 20%,默认限制在 300%~50%,公式为scale := 1.2 ^ level;- 注意 Chromium 默认采用**同源(same-origin)**缩放策略:同一域名的缩放会在所有窗口实例间传播。若需按 WebContents 隔离,用
setZoomMode('isolated')。
setZoomMode(mode)(新 API,另有只读属性 contents.zoomMode 与 getZoomMode())定义四种模式:
default:按 origin 自动处理缩放,同源的多个 webContents 共享缩放级别;isolated:按 webContents 自动处理缩放,互相不影响;manual:关闭自动缩放,zoom-changed事件仍会派发但页面不会真的缩放,由应用自行管理级别;disabled:完全禁用缩放,webContents 回退到默认级别并忽略一切缩放请求。
isolated 与 manual 模式会跨导航持久化。setZoomMode 的 C++ 侧实现位于 shell/browser/api/electron_api_web_contents.cc(约 4246 行),最终落到 WebContentsZoomController 上,其控制器源码见 shell/browser/web_contents_zoom_controller.cc。对应的属性壳 zoomLevel/zoomFactor/zoomMode 定义于 JS 层 _init(见 lib/browser/api/web-contents.ts)。
捏合缩放与视觉缩放
setVisualZoomLevelLimits(minimumLevel, maximumLevel) 设置捏合(pinch-to-zoom)缩放的上下限,返回 Promise<void>。Electron 默认关闭视觉缩放,需要时显式开启:
const win = new BrowserWindow()
win.webContents.setVisualZoomLevelLimits(1, 3)
输入事件:键盘与鼠标的页面级监听
before-input-event
在页面派发 keydown/keyup 之前触发。对 input 对象调用 event.preventDefault() 会同时阻止页面按键事件和应用菜单快捷键。input 字段与 DOM KeyboardEvent 对齐:type(keyUp/keyDown)、key、code、isAutoRepeat、isComposing、shift/control/alt/meta、location、modifiers。
若只想拦截菜单快捷键而保留页面按键,用 setIgnoreMenuShortcuts(ignore):
const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 800, height: 600 })
win.webContents.on('before-input-event', (event, input) => {
// 按住 Ctrl/Cmd 时启用应用菜单键盘快捷键
win.webContents.setIgnoreMenuShortcuts(!input.control && !input.meta)
})
})
before-mouse-event 与 input-event
before-mouse-event 在页面派发鼠标事件之前触发,event.preventDefault() 会阻止页面鼠标事件。mouse 为 MouseInputEvent,含 type、button、x/y、globalX/globalY、clickCount、movementX/Y 等字段:
const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 800, height: 600 })
win.webContents.on('before-mouse-event', (event, mouse) => {
// 阻止所有 mouseDown
if (mouse.type === 'mouseDown') {
console.log(mouse)
/*
{
type: 'mouseDown',
clickCount: 1,
movementX: 0,
movementY: 0,
button: 'left',
x: 632.359375,
y: 480.6875,
globalX: 168.359375,
globalY: 193.6875
}
*/
event.preventDefault()
}
})
})
input-event 则在输入事件被送入 WebContents 时触发(参数见 InputEvent)。反向地,主进程可用 sendInputEvent(inputEvent) 向页面合成注入键盘/鼠标/滚轮事件(sendInputEvent 要求宿主 BrowserWindow 处于聚焦状态)。
页面内查找、Bluetooth、拖拽等交互能力
页面查找:findInPage / stopFindInPage
findInPage(text[, options]) 发起查找,返回该次请求的 requestId(Integer),结果通过 found-in-page 事件异步返回。options.forward(默认 true)控制方向、matchCase(默认 false)控制大小写、findNext:首次请求应为 true、后续跟随请求为 false。found-in-page 事件携带 result:requestId、activeMatchOrdinal(当前高亮匹配序号)、matches(总匹配数)、selectionArea、finalUpdate(是否最后一次回调)。
stopFindInPage(action) 结束查找,action 取 clearSelection(清除选中)、keepSelection(转为普通选区)、activateSelection(聚焦并点击选中节点):
const win = new BrowserWindow()
win.webContents.on('found-in-page', (event, result) => {
if (result.finalUpdate) win.webContents.stopFindInPage('clearSelection')
})
const requestId = win.webContents.findInPage('api')
console.log(requestId)
蓝牙设备选择:select-bluetooth-device
当页面调用 navigator.bluetooth.requestDevice 需要挑选设备时触发。必须用 deviceId 调用 callback;传空串表示取消。若没有任何监听器,所有蓝牙请求默认被取消;若监听了但未 preventDefault(),则自动选择第一个可用设备。由于蓝牙扫描耗时,该事件可能多次触发直到 callback 被调用。
const { app, BrowserWindow } = require('electron')
let win = null
app.whenReady().then(() => {
win = new BrowserWindow({ width: 800, height: 600 })
win.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
event.preventDefault()
const result = deviceList.find((device) => {
return device.deviceName === 'test'
})
if (!result) {
// 未找到目标设备:要么继续等待(如设备尚未开启),要么以空串取消
callback('')
} else {
callback(result.deviceId)
}
})
})
原生拖拽与页面保存
startDrag(item) 把当前拖拽操作的拖拽项设为指定文件:item.file 为绝对路径(可用 item.files 数组覆盖 file 字段),item.icon(macOS 上必须非空)为拖拽光标下方的图标。
savePage(fullPath, saveType) 将整页保存到磁盘,返回 Promise。saveType 取 HTMLOnly(仅 HTML)、HTMLComplete(完整页面,含资源)、MHTML(MHTML 格式):
const { BrowserWindow } = require('electron')
const win = new BrowserWindow()
win.loadURL('https://github.com')
win.webContents.on('did-finish-load', async () => {
win.webContents.savePage('/tmp/test.html', 'HTMLComplete').then(() => {
console.log('Page was saved successfully.')
}).catch(err => {
console.log(err)
})
})
截图与打印
capturePage([rect, opts])
截取页面 rect(Rectangle)区域快照,省略 rect 则截取整个可见页面。返回 Promise<NativeImage>。当窗口隐藏时页面仍被视为"可见"(只要捕获计数非零);若希望页面保持隐藏,设置 stayHidden: true;stayAwake: true 可防止系统休眠。isBeingCaptured() 在捕获计数大于 0 时返回 true。
打印:getPrintersAsync / print / printToPDF
getPrintersAsync() 返回系统打印机列表 Promise<PrinterInfo[]>。print([options], [callback]) 打印当前页面。常用选项包括:silent(不弹打印设置,默认 false)、printBackground(打印背景色与背景图)、deviceName(必须是系统名而非友好名,如 'Brother_QL_820NWB')、landscape、pageRanges(0 起始的 { from, to },macOS 只认一个区间)、duplexMode(simplex/shortEdge/longEdge)、copies、collate、dpi、header/footer、pageSize(A0~A6、Legal、Letter、Tabloid 或 { height, width } 对象,以微米为单位且单边不得小于 353 微米)。usePrinterDefaultPageSize 与 pageSize 互斥。可强制分页的 CSS 为 page-break-before: always;。打印失败回调的常见 failureReason:"Invalid printer settings"、"Print job canceled"、"Print job failed"。
const win = new BrowserWindow()
const options = {
silent: true,
deviceName: 'My-Printer',
pageRanges: [{
from: 0,
to: 1
}]
}
win.webContents.print(options, (success, errorType) => {
if (!success) console.log(errorType)
})
JS 层 lib/browser/api/web-contents.ts 对 print 做了完整的参数清洗:把 pageSize 字符串映射为微米制 mediaSize(Letter 215900×279400µm、A4 210000×297000µm 等)、校验自定义尺寸合法性、拒绝 usePrinterDefaultPageSize 与 pageSize 混用。打印功能由 lib/browser/print-to-pdf.ts 等模块承接。
printToPDF(options) 直接把页面打印为 PDF,返回 Promise<Buffer>。页面中使用 @page CSS 规则时 landscape 会被忽略:
const { app, BrowserWindow } = require('electron')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
app.whenReady().then(() => {
const win = new BrowserWindow()
win.loadURL('https://github.com')
win.webContents.on('did-finish-load', () => {
// 使用默认打印选项
const pdfPath = path.join(os.homedir(), 'Desktop', 'temp.pdf')
win.webContents.printToPDF({}).then(data => {
fs.writeFile(pdfPath, data, (error) => {
if (error) throw error
console.log(`Wrote PDF successfully to ${pdfPath}`)
})
}).catch(error => {
console.log(`Failed to write PDF to ${pdfPath}: `, error)
})
})
})
DevTools 深度控制
WebContents 提供了把 DevTools"搬进任意 WebContents"的能力:
setDevToolsWebContents(devToolsWebContents):用目标WebContents承载 DevTools UI(目标不能已发生任何导航,之后也不能挪作他用)。Electron 默认使用内部原生 view 管理 DevTools,开发者控制力有限;此方法可改用BrowserWindow或WebContentsView(见 docs/api/web-contents-view.md)。关闭 DevTools 不会销毁devToolsWebContents,需调用方自行销毁:
const { app, BrowserWindow } = require('electron')
let win = null
let devtools = null
app.whenReady().then(() => {
win = new BrowserWindow()
devtools = new BrowserWindow()
win.loadURL('https://github.com')
win.webContents.setDevToolsWebContents(devtools.webContents)
win.webContents.openDevTools({ mode: 'detach' })
})
openDevTools([options]):mode可取值left/right/bottom/undocked/detach,默认沿用上次停靠状态;undocked下可重新停靠,detach不行;activate(默认true)是否把 DevTools 窗口带到前台;title仅在undocked/detach下生效。当 contents 是<webview>时默认detach;Windows 下开启 Window Control Overlay 时也会强制detach。closeDevTools()/toggleDevTools()/isDevToolsOpened()/isDevToolsFocused()/getDevToolsTitle()/setDevToolsTitle(title):开合、切换与标题管理。inspectElement(x, y):在页面坐标处开始元素审查;inspectServiceWorker()、inspectSharedWorker()、inspectSharedWorkerById(workerId)、getAllSharedWorkers():审查各类 worker。addWorkSpace(path)/removeWorkSpace(path):增删 DevTools workspace 目录,需在 DevTools 创建之后调用(监听devtools-opened)。- 配套事件:
devtools-opened、devtools-closed、devtools-focused、devtools-reload-page(DevTools 请求重载页面时触发)、devtools-open-url(DevTools 内点击链接或右键"在新标签页打开")、devtools-search-query(选中文本并搜索时触发)。
IPC 通信:定向发消息与作用域 ipc
主进程向渲染进程发消息是单向通信的核心:
contents.send(channel, ...args):向渲染进程异步发消息。参数采用结构化克隆算法序列化,原型链不会保留;函数、Promise、Symbol、WeakMap、WeakSet 以及 DOM 对象、特殊 Electron 对象都会抛异常。渲染侧用ipcRenderer监听。实现上该方法实际委托给mainFrame.send(见 lib/browser/api/web-contents.ts)。contents.sendToFrame(frameId, channel, ...args):把消息发往特定帧。frameId传数字(主帧进程内的 routingId),若目标帧与主帧不在同一进程则传[processId, frameId]二元组。渲染进程中用webFrame.routingId自报帧号,主进程在收到 IPC 时也能从event.frameId读到来源帧。contents.postMessage(channel, message, [transfer]):发消息并可选转移零个或多个MessagePortMain的所有权。渲染端通过事件的ports属性取得(此时是原生 DOMMessagePort):
// Main process
const win = new BrowserWindow()
const { port1, port2 } = new MessageChannelMain()
win.webContents.postMessage('port', { message: 'hello' }, [port1])
// Renderer process
ipcRenderer.on('port', (e, msg) => {
const [port] = e.ports
// ...
})
对应的入站事件为 ipc-message(渲染进程 ipcRenderer.send() 异步消息)与 ipc-message-sync(ipcRenderer.sendSync() 同步消息)。除了传统事件监听,新属性 contents.ipc(只读)提供作用域限定在本 WebContents 的 IpcMain 风格接口。消息派发顺序:
contents.on('ipc-message')contents.mainFrame.on(channel)contents.ipc.on(channel)ipcMain.on(channel)
invoke 型处理器按以下顺序查找,取第一个已定义者执行、其余忽略:
contents.mainFrame.handle(channel)contents.handle(channel)ipcMain.handle(channel)
注册在 WebContents 上的处理器会收到任意帧(含子帧)的消息;默认只有主帧可发 IPC,但启用 nodeIntegrationInSubFrames 后子帧也可发送,此时应在处理器内检查事件的 senderFrame,或直接在对应帧上用 WebFrameMain.ipc(见 docs/api/web-frame-main.md)注册。完整教程见 docs/tutorial/ipc.md。
离屏渲染与帧级事件
开启离屏渲染(webPreferences.offscreen: true)后,可用 paint 事件接收新生成的帧,实现视频采集、远程桌面等"无窗口渲染"场景:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ webPreferences: { offscreen: true } })
win.webContents.on('paint', (event, dirty, image) => {
// updateBitmap(dirty, image.toBitmap())
})
win.loadURL('https://github.com')
paint 的回调参数中 dirtyRect 为 Rectangle(仅脏区域被传入 buffer),image 为整帧的 NativeImage。当 webPreferences.offscreen.useSharedTexture 为 true 时,details.texture(OffscreenSharedTexture,实验性)携带 GPU 共享纹理句柄,可免去 CPU/GPU 间拷贝直接喂给外部渲染管线——适合高性能渲染场景,但同一时间能存在的纹理数量有限,用完必须尽快 texture.release(),也可自行管理生命周期后将 texture.textureInfo 经 IPC 传给其他进程:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ webPreferences: { offscreen: { useSharedTexture: true } } })
win.webContents.on('paint', async (e, dirty, image) => {
if (e.texture) {
// 自行管理生命周期,可在异步处理器中处理,或把 e.texture.textureInfo 传给其他进程
// (注意不要传 e.texture 本身——release 函数无法经 IPC 传递)
await new Promise(resolve => setTimeout(resolve, 50))
// importTextureHandle(dirty, e.texture.textureInfo)
// 必须尽早 release,避免底层帧池被耗尽
e.texture.release()
}
})
win.loadURL('https://github.com')
更多内容见 docs/tutorial/offscreen-rendering.md;native 侧实现文档位于 shell/browser/osr/README.md。离屏相关的其他方法与属性:isOffscreen()、startPainting()、stopPainting()、isPainting()、setFrameRate(fps)(非共享纹理模式下仅接受 1~240)、getFrameRate()、invalidate()(安排整窗重绘,离屏下会生成新帧并触发 paint)。frameRate 属性与 beginFrameSubscription([onlyDirty], callback)/endFrameSubscription()(订阅帧呈现事件与捕获帧,onlyDirty 为 true 时 image 仅含重绘区域)也是配套能力。仅当 WebContents 开启离屏渲染时以上方法才适用。
WebRTC 与媒体控制
setWebRTCIPHandlingPolicy(policy):控制经 WebRTC 暴露哪些 IP。四种策略:default:暴露公网与本地 IP(默认行为,允许枚举所有网卡);default_public_interface_only:只暴露公网 IP,仅使用 HTTP 的默认路由;default_public_and_private_interfaces:暴露公网与私有 IP,使用默认路由;disable_non_proxied_udp:公网/本地 IP 均不暴露,除非代理支持 UDP,否则只经 TCP 联络。
getWebRTCUDPPortRange()/setWebRTCUDPPortRange({ min, max }):读取/限制 WebRTC 使用的 UDP 端口范围,默认{ min: 0, max: 0 }(无限制),重置为无限制同样传该值。getMediaSourceId(requestWebContents):返回某 WebContents 流的标识,配合getUserMedia的chromeMediaSource: 'tab'使用;标识绑定到指定requestWebContents且仅 10 秒有效。- 媒体事件与静音:
media-started-playing/media-paused/audio-state-changed(audible布尔值)。setAudioMuted(muted)/isAudioMuted()/isCurrentlyAudible()控制并查询静音状态,属性版为audioMuted。 - 其他策略性 API:
setCaretBrowsingEnabled(enabled)/isCaretBrowsingEnabled()(配合属性caretBrowsingEnabled,开启后页面出现可移动光标以支持纯键盘选读;<webview>guest 会继承创建时刻的值再独立跟踪,同时进程内任一 WebContents 开启都会进程级通知辅助技术);setImageAnimationPolicy(policy)(animate/animateOnce/noAnimation,只影响新图像,已有动图可通过img.src = img.src强制重算,无网络流量);setBackgroundThrottling(allowed)(控制页面后台化时是否节流动画与定时器,同时影响 Page Visibility API;注意设为false会影响宿主BrowserWindow内所有 WebContents,见 docs/api/breaking-changes.md)。
设备模拟、进程信息与克隆
enableDeviceEmulation(parameters):开启设备模拟,参数含screenPosition(desktop/mobile)、screenSize、viewPosition(默认{ x: 0, y: 0 })、deviceScaleFactor(默认 0 表示沿用原始值)、viewSize、scale(默认 1);disableDeviceEmulation()关闭。getOSProcessId():返回承载此 WebContents 的渲染进程的操作系统 pid;getProcessId()返回 Chromium 内部 pid,可与帧级导航事件的frameProcessId对照。getType():返回类型,可为backgroundPage、window、browserView、remote、webview、offscreen。getOrCreateDevToolsTargetId():返回关联的 CDPTargetID(fromDevToolsTargetId的反向操作,若尚不存在则新建 DevTools agent)。takeHeapSnapshot(filePath):抓取 V8 堆快照写入文件,返回 Promise。clone():创建当前 WebContents 的副本,保留 WebPreferences、复用同一 SiteInstance(同源页复用渲染进程、跨源才新开进程,与 Chromiumwindow.open及标签复制行为一致)、继承 opener 关系、拷贝导航历史与控制器状态;克隆体生命周期完全独立、可单独销毁,且不含任何已打开页面。适合需要"与源共享渲染进程又能独立管理生命周期"的场景,也有助于节省内存与加速加载(参考 Chromium Site Isolation 设计)。
实例属性速查
| 属性 | 类型/可写 | 说明 |
|---|---|---|
contents.ipc |
只读 | 作用域限定在本 WebContents 的 IpcMain |
contents.id |
只读 | 应用内全局唯一的 WebContents ID |
contents.session |
只读 | 该 webContents 使用的 Session |
contents.navigationHistory |
只读 | 会话历史,见 docs/api/navigation-history.md |
contents.mainFrame |
只读 | 帧层级中最顶层的 WebFrameMain |
contents.opener |
只读 | 打开本 WebContents 的帧(open() 或带 target 的链接导航),可为 null |
contents.focusedFrame |
只读 | 当前聚焦帧(顶层/内嵌 iframe),无焦点时为 null |
contents.hostWebContents |
只读 | 可能拥有本 WebContents 的宿主实例(如 webview 的 embedder),否则 null |
contents.devToolsWebContents |
只读 | 关联的 DevTools WebContents。勿长期保存——DevTools 关闭后可能变为 null |
contents.debugger |
只读 | 本 webContents 的 Debugger 实例(CDP 入口) |
contents.audioMuted |
可读写 | 页面是否静音 |
contents.userAgent |
可读写 | 当前页面的 UA |
contents.zoomLevel / zoomFactor / zoomMode |
可读写 | 缩放级别/因子/模式 |
contents.frameRate |
可读写 | 离屏渲染帧率(仅接受 1~240) |
contents.backgroundThrottling |
可读写 | 后台化时是否节流动画与定时器 |
contents.caretBrowsingEnabled |
可读写 | 是否开启光标浏览 |
安全实践与典型应用建议
由于 webContents 掌控页面加载与脚本执行,配合 Electron 安全模型(见 docs/tutorial/security.md)时建议:
- 始终开启
contextIsolation与沙箱,并通过setWindowOpenHandler+will-navigate等事件把window.open与主帧导航全部纳入白名单校验,避免不可信页面通过导航逃逸; - 对"不允许离开/重定向"的页面使用
will-navigate/will-redirect的preventDefault(),对 iframe 级诉求改监听will-frame-navigate; - 用
before-input-event+setIgnoreMenuShortcuts精细控制菜单快捷键与页面快捷键的冲突,而不是粗暴禁用全部输入; - 页面加载与状态机统一以
did-finish-load/did-fail-load或loadURL返回的 Promise 为信号,避免手工 setTimeout 竞态; - 渲染进程崩溃/无响应恢复使用
render-process-gone与forcefullyCrashRenderer()+reload()的组合,并把相关日志统一上报; - 跨进程发消息优先走
contents.ipc(仅限单实例场景)并在收到子帧消息时校验senderFrame,同时避免向渲染进程发送不可结构化克隆的对象。
仓库中对应的测试覆盖集中在 spec/api-web-contents-spec.ts,源码级实现可沿 lib/browser/api/web-contents.ts(JS 封装层)与 shell/browser/api/electron_api_web_contents.cc(Chromium 对接层)两条路径深入研读——前者能看清 loadURL 的 Promise 语义、print 的参数清洗、IPC 转发与 navigationHistory 装配等细节,后者是理解导航事件与缩放模式等底层行为的最佳入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

