首页
/ Electron webContents 完全指南:掌控页面渲染、导航与窗口交互

Electron webContents 完全指南:掌控页面渲染、导航与窗口交互

2026-09-06 18:16:15作者:魏献源Searcher

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。例如 postMessagesendsendToFrame 被转发到主帧对象(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.hshell/browser/api/electron_api_web_contents.cc(共 6000+ 行):真正对接 Chromium 的 content::WebContentsWebContentsObserverWebContentsDelegate,是全部事件(如导航回调、渲染进程异常)与底层行为的发源地。

模块级方法:全局查找 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 使用

其中 fromIdfromFramefromDevToolsTargetIdgetAllWebContents 在 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 跳转到另一个页面(区别于页内导航)时,会按顺序触发以下事件:

  1. did-start-navigation
  2. will-frame-navigate
  3. will-navigate(仅主帧导航时触发)
  4. will-redirect(导航发生重定向时触发)
  5. did-redirect-navigation(重定向后触发,不可拦截)
  6. did-frame-navigate
  7. did-navigate(仅主帧导航完成时触发)

其中 will-frame-navigatewill-navigatewill-redirect 等可取消事件一旦调用了 event.preventDefault(),后续事件便不再触发。

In-page Navigation(页内导航)

点击锚点、修改 window.location.hashpushState/replaceState 及同页历史前进/后退都属于页内导航,不会触发 reload,也不可取消。事件顺序固定为:

  • did-start-navigation
  • did-navigate-in-page

Frame Navigation(帧级导航)

will-navigatedid-navigate 在主帧(mainFrame)导航时触发。若要同时监听 <iframe> 及嵌套 iframe 的导航,应改用 will-frame-navigatedid-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" 分隔的额外请求头;
  • postDataUploadRawData[] | 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(querysearchhash)会传给 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.navigationHistoryNavigationHistory 类型见 docs/api/navigation-history.md)上的同名方法:

  • contents.navigationHistory.canGoBack() / canGoForward() / canGoToOffset(offset)
  • contents.navigationHistory.goBack() / goForward() / goToIndex(index) / goToOffset(offset)
  • contents.navigationHistory.clear()
  • 另有 lengthgetActiveIndex()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' }:允许创建;还可附带 overrideBrowserWindowOptionscreateWindowoutlivesOpener 选项实现深度定制;
  • 返回 null / undefined / 无合法 action 的对象:会打印 console 错误并等效于 deny

handler 收到的 details 对象包含:url(被 resolve 过的最终 URL,例如对 window.open('foo') 会得到类似 https://the-origin/the/current/path/foo)、frameNamefeatureswindow.open 传入的逗号分隔特性串)、disposition(见下文枚举)、referrerpostBody(仅表单 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 提供了 urlframeNameoptions(合并优先级从低到高为:window.open() features 串解析结果 → 继承自父窗口的安全相关 webPreferences → setWindowOpenHandler 给出的选项,未识别的选项不会被过滤)、referrerpostBodydisposition。两个 API 的配合用法详见 docs/api/window-open.md

关键导航与页面状态事件详解

导航事件(主帧 / 任意帧 / 页内)

现代版本中,导航类事件统一携带 details 对象(旧式平铺参数已标记弃用),字段包括:

  • url:帧正在导航到的 URL;
  • isSameDocument:是否未替换文档(锚点跳转、pushState/replaceState、同页历史导航为 true);
  • isMainFrame:是否主帧;
  • frameWebFrameMain | null,正在导航的帧(帧已导航或销毁后可能为 null);
  • initiator:发起导航的帧(父帧、子帧或 null)。

will-navigate 表示用户或页面要开始主帧导航(修改 window.location、点击页面内链接等);程序化导航(loadURLback)不触发,页内导航也不触发。调用 event.preventDefault() 可阻止该次导航。若要在 iframe 级别也得到"将导航"通知,监听 will-frame-navigate

will-redirect(服务端重定向前,如 302)与 did-redirect-navigation(重定向发生后)成对出现:前者可 preventDefault()(会阻止整个导航而不仅是重定向),后者不可拦截。

did-navigate主帧导航完成时触发,携带 urlhttpResponseCode(非 HTTP 导航为 -1)、httpStatusText(非 HTTP 导航为空串);did-frame-navigate 则对任意帧导航完成触发,并额外携带 isMainFrameframeProcessIdframeRoutingId。两者都不覆盖页内导航。

加载进度与失败事件

  • did-start-navigation:任意帧(含主帧)开始导航;
  • did-start-loading / did-stop-loading:对应标签页 loading spinner 开始/停止转动的时间点;
  • dom-ready:顶层 frame 的 document 加载完成;
  • did-finish-load:导航完成、spinner 停止、onload 已派发;
  • did-frame-finish-load:某一帧完成导航(含 isMainFrameframeProcessIdframeRoutingId);
  • did-fail-load:加载失败(类似 did-finish-load 的失败版)。参数含 errorCodeerrorDescriptionvalidatedURLisMainFrame 等,完整错误码清单对应 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:导航期间页面标题被设置时触发。explicitSetfalse 表示标题由 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 取值覆盖 pointerhandtexte-resizegrabcustomnot-allowedzoom-in 等常见形态;当 type === 'custom' 时,imageNativeImage)、scalesizehotspot 会描述自定义光标。

页面渲染进程的稳定性管理

  • 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 行为类似,但在隔离上下文中执行。worldId0 为默认 world,999 是 Electron contextIsolation 特性使用的 world,也可传任意整数;scriptsWebSource[](结构见 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.pngdocs/images/web-contents-text-selection-after.png):

调用 adjustSelection({ start: 1, end: 5 }) 之前的文本选区状态

调用 adjustSelection({ start: 1, end: 5 }) 之后的文本选区状态

缩放控制: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.zoomModegetZoomMode())定义四种模式:

  • default:按 origin 自动处理缩放,同源的多个 webContents 共享缩放级别;
  • isolated:按 webContents 自动处理缩放,互相不影响;
  • manual:关闭自动缩放,zoom-changed 事件仍会派发但页面不会真的缩放,由应用自行管理级别;
  • disabled:完全禁用缩放,webContents 回退到默认级别并忽略一切缩放请求。

isolatedmanual 模式会跨导航持久化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 对齐:typekeyUp/keyDown)、keycodeisAutoRepeatisComposingshift/control/alt/metalocationmodifiers

若只想拦截菜单快捷键而保留页面按键,用 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-eventinput-event

before-mouse-event 在页面派发鼠标事件之前触发,event.preventDefault() 会阻止页面鼠标事件。mouseMouseInputEvent,含 typebuttonx/yglobalX/globalYclickCountmovementX/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、后续跟随请求为 falsefound-in-page 事件携带 resultrequestIdactiveMatchOrdinal(当前高亮匹配序号)、matches(总匹配数)、selectionAreafinalUpdate(是否最后一次回调)。

stopFindInPage(action) 结束查找,actionclearSelection(清除选中)、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。saveTypeHTMLOnly(仅 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])

截取页面 rectRectangle)区域快照,省略 rect 则截取整个可见页面。返回 Promise<NativeImage>。当窗口隐藏时页面仍被视为"可见"(只要捕获计数非零);若希望页面保持隐藏,设置 stayHidden: truestayAwake: true 可防止系统休眠。isBeingCaptured() 在捕获计数大于 0 时返回 true

打印:getPrintersAsync / print / printToPDF

getPrintersAsync() 返回系统打印机列表 Promise<PrinterInfo[]>print([options], [callback]) 打印当前页面。常用选项包括:silent(不弹打印设置,默认 false)、printBackground(打印背景色与背景图)、deviceName(必须是系统名而非友好名,如 'Brother_QL_820NWB')、landscapepageRanges(0 起始的 { from, to },macOS 只认一个区间)、duplexModesimplex/shortEdge/longEdge)、copiescollatedpiheader/footerpageSizeA0~A6LegalLetterTabloid{ height, width } 对象,以微米为单位且单边不得小于 353 微米)。usePrinterDefaultPageSizepageSize 互斥。可强制分页的 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.tsprint 做了完整的参数清洗:把 pageSize 字符串映射为微米制 mediaSizeLetter 215900×279400µm、A4 210000×297000µm 等)、校验自定义尺寸合法性、拒绝 usePrinterDefaultPageSizepageSize 混用。打印功能由 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,开发者控制力有限;此方法可改用 BrowserWindowWebContentsView(见 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-openeddevtools-closeddevtools-focuseddevtools-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 属性取得(此时是原生 DOM MessagePort):
// 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-syncipcRenderer.sendSync() 同步消息)。除了传统事件监听,新属性 contents.ipc(只读)提供作用域限定在本 WebContentsIpcMain 风格接口。消息派发顺序:

  1. contents.on('ipc-message')
  2. contents.mainFrame.on(channel)
  3. contents.ipc.on(channel)
  4. ipcMain.on(channel)

invoke 型处理器按以下顺序查找,取第一个已定义者执行、其余忽略:

  1. contents.mainFrame.handle(channel)
  2. contents.handle(channel)
  3. 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 的回调参数中 dirtyRectRectangle(仅脏区域被传入 buffer),image 为整帧的 NativeImage。当 webPreferences.offscreen.useSharedTexturetrue 时,details.textureOffscreenSharedTexture,实验性)携带 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()(订阅帧呈现事件与捕获帧,onlyDirtytrueimage 仅含重绘区域)也是配套能力。仅当 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 流的标识,配合 getUserMediachromeMediaSource: 'tab' 使用;标识绑定到指定 requestWebContents仅 10 秒有效
  • 媒体事件与静音:media-started-playing / media-paused / audio-state-changedaudible 布尔值)。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):开启设备模拟,参数含 screenPositiondesktop/mobile)、screenSizeviewPosition(默认 { x: 0, y: 0 })、deviceScaleFactor(默认 0 表示沿用原始值)、viewSizescale(默认 1);disableDeviceEmulation() 关闭。
  • getOSProcessId():返回承载此 WebContents 的渲染进程的操作系统 pidgetProcessId() 返回 Chromium 内部 pid,可与帧级导航事件的 frameProcessId 对照。
  • getType():返回类型,可为 backgroundPagewindowbrowserViewremotewebviewoffscreen
  • getOrCreateDevToolsTargetId():返回关联的 CDP TargetIDfromDevToolsTargetId 的反向操作,若尚不存在则新建 DevTools agent)。
  • takeHeapSnapshot(filePath):抓取 V8 堆快照写入文件,返回 Promise。
  • clone():创建当前 WebContents 的副本,保留 WebPreferences、复用同一 SiteInstance(同源页复用渲染进程、跨源才新开进程,与 Chromium window.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-redirectpreventDefault(),对 iframe 级诉求改监听 will-frame-navigate
  • before-input-event + setIgnoreMenuShortcuts 精细控制菜单快捷键与页面快捷键的冲突,而不是粗暴禁用全部输入;
  • 页面加载与状态机统一以 did-finish-load / did-fail-loadloadURL 返回的 Promise 为信号,避免手工 setTimeout 竞态;
  • 渲染进程崩溃/无响应恢复使用 render-process-goneforcefullyCrashRenderer() + 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 装配等细节,后者是理解导航事件与缩放模式等底层行为的最佳入口。

登录后查看全文
热门项目推荐
相关项目推荐