Electron 30 的 WebContentsView-替代 BrowserView 之后多视图应用怎么写

7 阅读8分钟

Electron 30 把用了多年的 BrowserView 退役了,替代品是 WebContentsView + BaseWindow。不是革新性新能力,是回归 Chromium Views API 标准的工程清理。本文给 4 方对比 + 4 处迁移 diff + 3 个真实迁移坑。

我最近在重写一个老 Electron 应用——它原来用了七八个 BrowserView 拼出"多 tab + 侧边栏 + 浮窗"的界面。结果一升级到 Electron 30,到处都是 deprecated 警告:BrowserView 这个用了多年的 API 终于被换掉了。

替代品是 WebContentsView + BaseWindow 这套新组合。Electron 官方有迁移指南,但偏简略,我自己边写边踩了几个坑——这篇把新模型的设计意图、迁移细节、容易踩的地方一次讲完,省得后来者再走一遍。

为什么要换:BrowserView 的历史包袱

BrowserView 是 Electron 早期为了"在 BrowserWindow 里嵌入子页面"做的一个变通方案。从 Chromium 的视角看它一直是个怪胎——既不是标准的 <iframe>,也不是 Chromium Views API 的标准产物,而是 Electron 自己实现的一层胶水。

具体表现:

  • BrowserView 不在 DOM 树里,但它的 webContents 又是完整的 Chromium 渲染管线产物
  • BrowserView 的定位、尺寸、层级,全靠主进程显式调用 setBounds 管理——和 BrowserWindow 的关系是"挂载"而不是"组合"
  • 多个 BrowserView 叠加时层级管理混乱,需要手动 setTopBrowserView

这套设计支撑 Electron 跑了好几年,但维护成本越来越高——尤其当 Chromium 自己在演进 Views API 时,BrowserView 这个非标实现总是要单独适配。Electron 团队决定推倒重来:让"嵌入式视图"这件事回归 Chromium Views API 标准路径。

新模型:BaseWindow + WebContentsView

WebContentsView 是 Chromium Views API 的一等公民。它做的事和 BrowserView 类似——在主进程里持有一个 web 渲染单元,由主进程显式控制位置和尺寸——但底层实现完全对接 Chromium Views,未来跟着 Chromium 升级而不用单独适配。

配套的 BaseWindow 是新的窗口基类。BrowserWindow 现在实际上是 BaseWindow + 内置主 WebContentsView 的语法糖;如果你要做更复杂的多视图组合,直接用 BaseWindow 然后手动添加 N 个 WebContentsView

代码示意:

const { app, BaseWindow, WebContentsView } = require('electron');

app.whenReady().then(() => {
  const win = new BaseWindow({ width: 1200, height: 800 });

  // 主视图
  const mainView = new WebContentsView();
  win.contentView.addChildView(mainView);
  mainView.setBounds({ x: 0, y: 0, width: 800, height: 800 });
  mainView.webContents.loadURL('https://chenguangliang.com');

  // 侧边栏视图
  const sidebarView = new WebContentsView();
  win.contentView.addChildView(sidebarView);
  sidebarView.setBounds({ x: 800, y: 0, width: 400, height: 800 });
  sidebarView.webContents.loadFile('sidebar.html');

  // 浮动小窗
  const popupView = new WebContentsView();
  win.contentView.addChildView(popupView);
  popupView.setBounds({ x: 500, y: 600, width: 300, height: 150 });
  popupView.webContents.loadFile('popup.html');
});

三个独立的 WebContentsView 组合在一个 BaseWindow 里,每个有自己独立的 webContents——这意味着独立的 V8 上下文、独立的 cookie 域(如果你配置不同的 partition)、独立的 DevTools。

这种"主进程组合多个视图"的能力,原来要靠多个 BrowserView 凑出来,现在是 first-class API。

与 iframe / webview / BrowserView 的真实差异

新人最容易混淆的是这几个方案的边界。我整理一下:

方案在 DOM 中?由谁控制进程隔离适用场景
<iframe>renderer同进程(同域)/ 跨进程(跨域 + site-isolation)嵌入站内或可信第三方页面
<webview>renderer独立进程嵌入完全不可信的第三方页面
BrowserView(旧)主进程独立进程主进程拼多个原生视图
WebContentsView(新)主进程独立进程主进程拼多个原生视图,BrowserView 的现代替代

WebContentsView 接替了 BrowserView 的位置——iframe / webview 那两个没变。如果你应用里用的是 iframe 或 webview,不需要迁移。

什么时候选 WebContentsView 而不是 iframe?我的判断:

  • 要嵌入完全独立的 web 页面(比如自家 SaaS web 端 + 桌面壳)→ WebContentsView
  • 要做多 tab / 多面板的 IDE 类应用(VS Code、Cursor 这种)→ WebContentsView
  • 要给嵌入页面单独的 cookie / session / 扩展集合 → WebContentsView(用不同 partition)
  • 要嵌入 trusted 站内内容(设置页、模态层) → iframe 简单够用
  • 要嵌入完全不可信第三方 → webview 沙箱更彻底

迁移代码:从 BrowserView 改到 WebContentsView

实际迁移大约 80% 是机械替换,剩下 20% 是几个 API 改动。

改动 1:构造和添加方式

// 旧(BrowserView)
const view = new BrowserView({ webPreferences: { ... } });
mainWindow.setBrowserView(view);            // 设为主 view
// 或
mainWindow.addBrowserView(view);            // 添加额外 view

// 新(WebContentsView)
const view = new WebContentsView({ webPreferences: { ... } });
mainWindow.contentView.addChildView(view);  // 统一用 addChildView

注意:setBrowserViewaddBrowserView 的语义差别被消除了。新 API 全部走 contentView.addChildView()——这是 Views API 标准模式。

改动 2:自动尺寸跟随

// 旧(BrowserView 的 setAutoResize)
view.setAutoResize({ width: true, height: true });

// 新(手动监听 resize 事件)
mainWindow.on('resize', () => {
  const [w, h] = mainWindow.getContentSize();
  view.setBounds({ x: 0, y: 0, width: w, height: h });
});

这是迁移里最容易漏掉的一步。setAutoResize 没了,要自己处理 resize 事件。如果你的应用有侧边栏 + 主视图 + 底栏的复杂布局,建议封装一个 LayoutManager 类管理所有 view 的 bounds——比散在各处 resize listener 好维护。

改动 3:transparent 背景

// 旧
view.setBackgroundColor('#00000000');  // 部分平台不生效

// 新
view.setBackgroundColor('#00000000');  // alpha=00 全平台生效

代码一样,但据我自己实测,新 API 在跨平台透明背景上的表现比旧 BrowserView 更稳——原来 Windows 上偶尔不生效的问题在我的项目里消失了(具体修复细节未在官方 changelog 看到明确条目,按你的实测为准)。

改动 4:webContents 访问

// 旧 + 新都一样
view.webContents.loadURL(...);
view.webContents.openDevTools();

view.webContents 这个属性没变——你之前所有 IPC 通信、监听器、preload 配置都不用动。这是迁移成本最低的一档。

几个我踩过的坑

光读官方迁移指南是不够的。我踩了三个坑,记下来给后来者:

坑 1:addChildView 的顺序就是层级

addChildView 后加的会盖在先加的上面(Z-index 顺序)。所以浮动小窗 / 模态层要最后加。如果你迁移时按"主视图 → 侧边栏 → 模态浮窗"的顺序,巧合是对的;但如果按"模态 → 主视图 → 侧边栏"的顺序加,模态会被盖到最底下。

旧的 setTopBrowserView 没了,要重排顺序得 removeChildView + addChildView 重新加。

坑 2:DevTools 现在独立窗口

旧 BrowserView 的 DevTools 默认是 docked 模式。WebContentsView 的 DevTools 默认会单独开新窗口——这不是 bug 是 feature,但用习惯了的话第一次会懵。要 docked 模式:

view.webContents.openDevTools({ mode: 'detach' });  // 独立窗口
view.webContents.openDevTools({ mode: 'bottom' }); // 底部 docked

坑 3:preload 路径一律用绝对路径

这条不是 WebContentsView 特有的——Electron 一直要求 webPreferences.preload 必须是绝对路径。但旧 BrowserView 代码里如果你侥幸用了相对路径(某些版本下有兜底解析行为),迁移时建议统一改成 path.join(__dirname, 'preload.js')。这条算迁移时顺手清理的技术债,不算 WebContentsView 引入的新问题。

性能:会变更好吗

诚实回答:单视图场景几乎没差别。WebContentsView 的实际渲染管线和 BrowserView 一样底层用 Chromium,性能差异在 1% 以内。

差异在多视图场景

  • 旧 BrowserView 多视图叠加时主进程要做大量 z-index 计算和 IPC 协调
  • 新 WebContentsView 走 Chromium Views API 标准合成路径,多视图叠加效率更高

实测我那个有 7 个视图的老应用迁移后:内存占用约降 8%,启动速度约快 200ms。样本只有一个项目,仅供参考——不是颠覆性提升,但白送的优化值得拿。

给还没迁移的人的建议

如果你现在还在 Electron < 30,迁移时机看产品节奏:

  • 明年才升 Electron 主版本:不急,先继续用 BrowserView,但写新代码尽量用 WebContentsView 避免增加迁移债
  • 本季度要升 Electron 30+:现在就把迁移做完,deprecated 警告不解决迟早会成阻塞项
  • 本来就在 Electron 30+ 但还在用 BrowserView:抓紧迁——Electron 官方未公布具体移除版本,但按 deprecated → 移除通常 2-4 个大版本的节奏看,建议 Electron 32 之前清干净

迁移本身工作量不大,一个有 5-10 个 view 的中型项目大约半天到一天能搞定。真正的成本是测试——多视图布局的边角 case 很多,建议把所有 view 切换、resize、模态弹出的场景过一遍。

一句话总结

WebContentsView 不是革新性的新能力,是 Electron 把 BrowserView 这套自家实现退役、回归 Chromium Views API 标准的工程清理。对开发者:API 更一致、未来更稳定、多视图组合性能略好;对维护者:少了一层非标胶水,跟着 Chromium 升级少踩坑。

不算性感的更新,但是把基础设施收拾干净的那种——值得迁,但不用激动。


延伸阅读

原文首发于 chenguangliang.com