主进程
职责
- 应用生命周期管理:控制应用启动、退出,监听
ready
、window-all-closed
等事件 - 窗口管理:通过
BrowserWindow
模块创建和管理窗口(如最小化、最大化、关闭) - 系统级操作:访问
Node.js API
,执行文件读写、调用系统对话框(如打开文件选择器)、与操作系统交互等 IPC
通信中枢:通过ipcMain
模块监听渲染进程的请求,处理核心逻辑并返回结果
特点
- 唯一性:整个应用仅有一个主进程
- 权限高:可直接调用
Node.js
模块(如fs
、path
),但需谨慎处理敏感操作
示例
1 2 3 4 5 6 7 8 9 10 11 12 |
const { app, BrowserWindow, ipcMain } = require('electron'); app.whenReady().then(() => { const win = new BrowserWindow({ webPreferences: { nodeIntegration: true } }); win.loadFile('index.html'); // 监听来自渲染进程的请求 ipcMain.on('read-file', (event, path) => { const data = fs.readFileSync(path); event.reply('file-data', data); }); }); |
渲染进程
职责
- 渲染界面:每个窗口对应一个渲染进程,负责加载和显示
HTML/CSS/JavaScript
页面 - 前端交互:处理用户输入、
DOM
操作、前端框架(如React/Vue
)逻辑 - 受限系统访问:默认无法直接调用
Node.js API
,除非通过配置或预加载脚本暴露特定功能
特点
- 多实例性:每个窗口独立运行一个渲染进程,崩溃不会影响其他窗口
- 默认隔离:出于安全考虑,渲染进程默认运行在浏览器沙箱环境中,与系统隔离
示例
1 2 3 4 5 6 7 8 9 10 11 |
<!-- index.html --> <button id="btn">读取文件</button> <script> const { ipcRenderer } = require('electron'); document.getElementById('btn').addEventListener('click', () => { ipcRenderer.send('read-file', '/path/to/file'); }); ipcRenderer.on('file-data', (event, data) => { console.log('文件内容:', data); }); </script> |
进程间通信
概述
- 主进程与渲染进程通过
IPC
(Inter-Process Communication
) 传递消息:
渲染进程 → 主进程
- 使用
ipcRenderer.send()
发送事件,主进程通过ipcMain.on()
监听
主进程 → 渲染进程
- 通过
event.reply()
或webContents.send()
返回数据,渲染进程通过ipcRenderer.on()
监听
安全最佳实践
禁用 Node.js
集成(渲染进程)
- 在
webPreferences
中设置nodeIntegration: false
,避免恶意代码利用Node.js
能力
启用上下文隔离(Context Isolation
)
- 隔离渲染进程的前端代码与预加载脚本,防止全局变量污染
使用预加载脚本(Preload Scripts
)
- 选择性暴露有限的
Node.js API
到渲染进程
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 主进程创建窗口时配置预加载脚本 new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true // 开启上下文隔离 } }); // preload.js:通过 contextBridge 暴露 API const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('api', { readFile: (path) => ipcRenderer.invoke('read-file', path) }); |
类比浏览器模型
主进程
≈
浏览器的主进程(管理标签页、扩展)
渲染进程
≈
浏览器的每个标签页,但额外支持Node.js
集成(需谨慎配置)
疑问与理解:其他
关于渲染进程
- 概述
- 在
Electron
应用中,每个窗口(BrowserWindow
)默认会创建一个独立的渲染进程,而窗口内的页面(无论是单页面应用还是多页面应用)都在该窗口的渲染进程中运行
- 在
- 默认行为
- 单窗口应用:整个应用只有一个渲染进程,用于加载所有页面
- 多窗口应用:每打开一个新窗口(
new BrowserWindow()
),Electron
会为其分配一个独立的渲染进程 - 标签页(如使用
<webview>
或<iframe>
):默认情况下,这些内容共享父窗口的渲染进程,但可以通过配置(如webPreferences
中的partition
)强制隔离到独立进程中
- 隔离性
- 崩溃隔离:如果某个窗口的渲染进程崩溃(例如页面
JavaScript
错误),不会影响其他窗口的渲染进程或主进程 - 内存隔离:每个渲染进程拥有独立的内存空间,页面间的数据默认不共享(除非通过
IPC
或主进程共享状态)
- 崩溃隔离:如果某个窗口的渲染进程崩溃(例如页面
- 进程与页面的关系
- 单页应用(
SPA
):一个窗口加载多个“页面”(通过前端路由切换),但始终在同一个渲染进程中运行 - 多窗口多页面:每个窗口加载不同的页面,各自在独立的渲染进程中运行
- 动态窗口:例如点击链接时通过
window.open()
创建的新窗口,默认也会生成新的渲染进程
- 单页应用(
- 进程复用与配置
Electron
允许通过配置控制进程的创建行为:- 禁用独立进程:通过
webPreferences
中的nodeIntegrationInSubFrames: true
等配置,可以让子框架(如<iframe>
)共享父窗口的渲染进程 - 强制进程隔离:通过
webPreferences.partition
参数,可以为不同窗口或<webview>
标签分配独立的进程
- 示例
- 运行后如果发现
page1.html
和page2.html
的process.pid
不同,说明它们运行在独立的渲染进程中
- 运行后如果发现
1 2 3 4 5 6 7 8 |
// 主进程:创建两个窗口 const { BrowserWindow } = require('electron'); const win1 = new BrowserWindow(); win1.loadFile('page1.html'); const win2 = new BrowserWindow(); win2.loadFile('page2.html'); |
1 2 |
// page1.html 和 page2.html 的代码 console.log("当前进程 PID:", process.pid); |
为什么<webview>
标签默认禁用
- 安全风险:防止滥用特权
API
Node.js
集成风险:若<webview>
标签启用了 Node.js 集成(通过webPreferences.nodeIntegration
),其内部加载的页面可直接访问Node.js API
,可能导致恶意代码执行或系统权限滥用- 跨站脚本攻击(
XSS
):如果<webview>
加载不受信任的第三方内容(如用户输入生成的URL
),攻击者可能通过XSS
漏洞操控渲染进程,甚至通过IPC
通信攻击主进程
- 沙箱隔离的复杂性
- 默认沙箱限制:
Electron
推荐将渲染进程运行在沙箱环境中,限制其对系统资源的访问。但<webview>
的默认配置可能绕过沙箱规则,导致隔离失效 - 进程共享问题:多个
<webview>
默认共享父窗口的渲染进程,若未正确配置partition
参数,可能导致敏感数据(如Cookie
、LocalStorage
)在不同页面间泄露
- 默认沙箱限制:
- 遵循“最小权限原则”
- 安全默认值(
Secure by Default
):Electron
的核心理念是默认关闭高风险功能,强制开发者显式启用并明确风险。类似的设计还有:
默认禁用渲染进程的Node.js
集成(nodeIntegration: false
)
默认启用上下文隔离(contextIsolation: true
) - 减少攻击面:禁用
<webview>
标签可避免开发者无意中引入漏洞,尤其是在嵌入外部内容时
- 安全默认值(
- 性能与稳定性
- 进程泄漏风险:每个
<webview>
可能创建独立的渲染进程,若未妥善管理(如动态创建/销毁),可能导致内存泄漏或进程残留。 - 资源消耗:大量
<webview>
实例会显著增加内存和CPU
占用,默认禁用可提醒开发者谨慎使用
- 进程泄漏风险:每个
- 替代方案的推荐
Electron
官方更推荐使用BrowserView
替代<webview>
,因为:- 更安全的进程控制:
BrowserView
允许直接配置webPreferences
(如独立partition
),更易实现进程隔离 - 布局灵活性:
BrowserView
可以自由调整位置和尺寸,适合复杂UI
集成 - 维护优先级:
Electron
团队对<webview>
的维护优先级较低,已知问题可能修复较慢
- 如何安全启用
<webview>
?- 若必须使用
<webview>
,需遵循以下安全实践:
- 若必须使用
1 2 3 4 5 6 7 8 9 10 11 |
// 显式启用标签 // 主进程创建窗口时配置 new BrowserWindow({ webPreferences: { webviewTag: true, // 显式启用 nodeIntegration: false, // 禁用 Node.js 集成 contextIsolation: true, // 启用上下文隔离 sandbox: true // 启用沙箱 } }); |
1 2 3 4 |
// 强制沙箱隔离 <!-- 为每个 <webview> 启用沙箱 --> <webview sandbox="allow-same-origin allow-scripts" src="page.html"></webview> |
1 2 3 4 5 |
// 使用唯一 partition 隔离进程 <!-- 不同 partition 的 webview 使用独立进程和数据存储 --> <webview partition="persist:webview1" src="https://example.com"></webview> <webview partition="persist:webview2" src="https://another-site.com"></webview> |
BrowserView
和webview
的区别
- 概述
- 在
Electron
中,BrowserView
和<webview>
都用于在窗口中嵌入子内容(如网页、第三方应用等),但它们在实现方式、安全性、控制粒度等方面有显著差异
- 在
- 实现方式与架构
特性 | BrowserView |
webview |
创建方式 | 通过主进程的 BrowserView 类创建。 |
在渲染进程的 HTML 中作为 DOM 标签使用。 |
进程归属 | 每个 BrowserView 默认拥有独立渲染进程。 |
共享父窗口的渲染进程,需显式配置独立进程。 |
生命周期控制 | 由主进程直接管理(创建、附加、销毁)。 | 通过 DOM 操作管理(如动态添加/移除标签)。 |
布局控制 | 通过坐标和尺寸精确控制位置,支持叠加和层叠。 | 作为普通 DOM 元素,依赖 CSS 布局。 |
- 进程隔离与安全性
特性 | BrowserView |
webview |
默认进程隔离 | ✅ 每个实例默认使用独立渲染进程。 | ❌ 默认共享父窗口进程,需通过 partition 隔离。 |
Node.js 集成 |
默认禁用,需在 webPreferences 中显式启用。 |
默认禁用,但父窗口启用 nodeIntegration 时可能继承。 |
沙箱模式 | 支持通过 sandbox: true 强制启用沙箱。 |
需显式设置 <webview sandbox="..."> 属性。 |
安全风险 | 隔离性强,适合加载不受信任内容。 | 配置不当易导致漏洞(如 XSS 攻击)。 |
- 使用场景对比
- 适合使用
BrowserView
的场景
需要严格进程隔离:例如嵌入第三方网页、广告等高风险内容。
复杂布局需求:多个视图叠加、动态调整位置(如IDE
的分屏编辑器)
高性能渲染:独立进程避免主页面卡顿
官方推荐场景:Electron
官方更推荐BrowserView
,因其维护更积极 - 适合使用
<webview>
的场景
简单内容嵌入:在固定位置加载受信任的子页面(如帮助文档)
快速原型开发:无需主进程介入,直接通过HTML
标签嵌入
旧项目兼容:已有代码基于<webview>
,且无严格安全要求
- 适合使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 使用 BrowserView // 主进程中创建和管理 BrowserView const { BrowserView, BrowserWindow } = require('electron'); const mainWindow = new BrowserWindow(); const view = new BrowserView({ webPreferences: { nodeIntegration: false, sandbox: true, partition: 'view-1' // 独立进程标识 } }); mainWindow.setBrowserView(view); view.setBounds({ x: 0, y: 50, width: 800, height: 600 }); view.webContents.loadURL('https://example.com'); |
1 2 3 4 5 6 7 8 9 |
// 使用 <webview> <!-- 在渲染进程的 HTML 中直接使用 --> <webview src="https://example.com" partition="persist:webview-1" sandbox="allow-same-origin allow-scripts" style="width: 800px; height: 600px;" ></webview> |
iframe
的配置只需要通过partition
参数吗
- 概述
- 在
Electron
中,通过<iframe>
实现进程隔离或存储隔离时,partition
参数是核心配置之一,但并非唯一需要关注的参数
- 在
partition
的作用- 定义存储分区:
partition
参数决定了<iframe>
的 存储隔离(如Cookie
、LocalStorage
、SessionStorage
等)和 进程隔离 - 相同
partition
值的<iframe>
共享存储和进程 - 不同
partition
值的<iframe>
使用独立的存储和进程
- 定义存储分区:
1 2 3 |
<!-- 父窗口中的不同 iframe 使用不同 partition --> <iframe src="https://site1.com" partition="persist:site1"></iframe> <iframe src="https://site2.com" partition="persist:site2"></iframe> |
- 其他关键配置参数,如下:
- 要完全控制
<iframe>
的进程隔离和安全性,需结合以下webPreferences
参数:
- 要完全控制
sandbox
- 启用沙箱模式:限制
<iframe>
的权限(如禁止执行脚本、禁止访问父页面DOM
) allow-scripts
: 允许执行JavaScript
allow-same-origin
: 允许同源访问(需谨慎使用)
- 启用沙箱模式:限制
1 2 3 4 5 |
<iframe src="https://untrusted.com" sandbox="allow-same-origin allow-scripts" partition="persist:untrusted" ></iframe> |
nodeIntegration
- 禁用
Node.js
集成:防止<iframe>
直接调用Node.js API
- 父窗口禁用
nodeIntegration
后,其所有子<iframe>
默认继承此配置
- 禁用
1 2 3 4 5 6 7 |
// 父窗口的 webPreferences 配置(主进程中) new BrowserWindow({ webPreferences: { nodeIntegration: false, // 父窗口禁用 Node.js 集成 contextIsolation: true // 启用上下文隔离 } }); |
contextIsolation
- 上下文隔离:隔离父窗口的前端代码与预加载脚本(
preload
),防止全局变量污染
- 上下文隔离:隔离父窗口的前端代码与预加载脚本(
1 2 3 4 5 6 7 |
// 父窗口配置 new BrowserWindow({ webPreferences: { contextIsolation: true, // 强制启用 preload: path.join(__dirname, 'preload.js') } }); |
webSecurity
- 同源策略:控制是否允许跨域请求
1 2 3 4 5 |
new BrowserWindow({ webPreferences: { webSecurity: true // 默认启用,阻止跨域请求 } }); |
- 完整配置示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 主进程(创建父窗口) const { BrowserWindow } = require('electron'); const win = new BrowserWindow({ webPreferences: { nodeIntegration: false, // 禁用 Node.js 集成 contextIsolation: true, // 启用上下文隔离 sandbox: true, // 启用沙箱模式 partition: 'persist:main' // 父窗口的默认分区 } }); win.loadFile('index.html'); |
1 2 3 4 5 6 7 8 9 10 11 |
// 渲染进程(index.html 中使用 iframe) <body> <!-- 隔离的 iframe --> <iframe src="https://untrusted.com" partition="persist:untrusted" sandbox="allow-scripts allow-same-origin" style="width: 100%; height: 100%;" ></iframe> </body> |
什么是同源访问
- 概述
- 同源访问(
Same-origin Access
)是浏览器实施的一项核心安全策略,称为同源策略(Same-origin Policy
) - 它的核心目的是限制不同源的网页之间的交互,防止恶意网站窃取用户数据或攻击其他网站
- 同源访问(
- 什么是“同源”?
- 两个
URL
的 协议(Protocol
)、域名(Domain
)、端口(Port
) 必须完全相同,才属于同源
- 两个
URL1 |
URL2 |
是否同源 | 原因 |
https://example.com/app |
https://example.com/api |
✅ 是 | 协议、域名、端口一致 |
http://example.com |
https://example.com |
❌ 否 | 协议不同(HTTP vs HTTPS ) |
https://example.com |
https://sub.example.com |
❌ 否 | 域名不同(主域 vs 子域) |
https://example.com:80 |
https://example.com:443 |
❌ 否 | 端口不同 |
- 同源策略的限制
- 如果两个页面不同源,浏览器会禁止以下操作:
- 访问
DOM
无法通过JavaScript
读取或修改其他源的页面DOM
示例:https://malicious.com
无法通过iframe.contentWindow.document
获取https://bank.com
的DOM
- 读取
Cookie
、LocalStorage
等存储
不同源的页面无法读取彼此的Cookie
或本地存储数据 - 发送跨域请求(
AJAX/Fetch
)
默认禁止通过XMLHttpRequest
或fetch()
向不同源的服务器发送请求(需目标服务器明确允许跨域) - 访问
JavaScript
全局对象
无法通过window.open()
或iframe
跨源访问窗口对象的属性和方法
允许跨源访问的例外情况
CORS
(跨源资源共享)- 服务器可通过设置
HTTP
响应头(如Access-Control-Allow-Origin: *
)明确允许特定源的跨域请求
- 服务器可通过设置
1 |
Access-Control-Allow-Origin: https://trusted-site.com |
JSONP
(已过时)- 通过
<script>
标签加载跨域脚本(仅限GET
请求),但存在安全风险,已逐渐被CORS
取代
- 通过
postMessage API
- 通过
window.postMessage()
在不同源的窗口间安全传递消息
- 通过
1 2 3 4 5 6 7 8 |
// 父窗口发送消息 iframe.contentWindow.postMessage('Hello', 'https://target-site.com'); // 子窗口接收消息 window.addEventListener('message', (event) => { if (event.origin !== 'https://parent-site.com') return; console.log(event.data); // 'Hello' }); |
document.domain
(仅限主域相同)- 如果两个页面主域相同(如
a.example.com
和b.example.com
),可通过设置document.domain = 'example.com'
实现跨子域访问(需双方都设置)
- 如果两个页面主域相同(如
在 Electron
中处理同源访问
- 概述
Electron
的渲染进程默认遵循浏览器的同源策略,但可通过配置绕过限制(需谨慎使用):
- 禁用同源策略(不推荐)
- 在创建
BrowserWindow
时关闭webSecurity
:
- 在创建
1 2 3 4 5 |
new BrowserWindow({ webPreferences: { webSecurity: false // 禁用同源策略,存在安全风险! } }); |
- 允许特定源的跨域请求
- 通过主进程代理或服务端配置
CORS
头部,安全地实现跨域通信
- 通过主进程代理或服务端配置
- 使用预加载脚本绕过限制
- 在预加载脚本(
preload
)中暴露安全的方法,通过IPC
与主进程通信:
- 在预加载脚本(
1 2 3 4 5 6 7 8 9 10 11 12 |
// preload.js const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('api', { fetchData: (url) => ipcRenderer.invoke('fetch-data', url) }); // 主进程处理请求 ipcMain.handle('fetch-data', async (event, url) => { const response = await fetch(url); return response.json(); }); |
ipcMain.on
- 用途:
- 监听渲染进程通过
ipcRenderer.send
或ipcRenderer.sendSync
发送的事件 - 监听一次性或持续事件
- 监听渲染进程通过
- 特点:
- 无自动响应:需手动调用
event.reply()
向渲染进程返回数据 - 持续监听:除非显式移除监听器(
removeListener
),否则会一直响应后续的同名事件 - 兼容性:适用于所有
Electron
版本
- 无自动响应:需手动调用
- 总结:
- 支持单向或双向通信(多次回复)
- 适合复杂交互(如进度更新、流式数据传输)
- 示例:
1 2 3 4 5 6 7 8 9 10 11 |
// 主进程:监听事件 ipcMain.on('request-data', (event, args) => { const result = fetchData(args); event.reply('response-data', result); // 手动回复 }); // 渲染进程:发送事件 ipcRenderer.send('request-data', { id: 1 }); ipcRenderer.on('response-data', (event, result) => { console.log(result); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 错误处理 // 主进程 ipcMain.on('get-file', (event, path) => { fs.readFile(path, (err, data) => { if (err) { event.reply('get-file-error', err.message); } else { event.reply('get-file-success', data); } }); }); // 渲染进程 ipcRenderer.send('get-file', 'file.txt'); ipcRenderer.on('get-file-success', (event, data) => { ... }); ipcRenderer.on('get-file-error', (event, error) => { ... }); |
ipcMain.handle
- 用途:
- 配合渲染进程的
ipcRenderer.invoke
使用,专门处理Promise
驱动的异步操作
- 配合渲染进程的
- 特点:
- 自动响应:返回
Promise
的结果会自动发送回渲染进程 - 错误传递:若
Promise
被拒绝(Reject
),错误会传递到渲染进程的catch
中 - 简洁性:无需手动回复,代码更简洁
- 版本要求:
Electron 7.0.0+
- 自动响应:返回
- 总结:
- 仅支持单向请求-响应(一次返回)
- 适合简单的异步操作(如数据库查询、文件读写)
- 示例:
1 2 3 4 5 6 7 8 9 |
// 主进程:处理请求 ipcMain.handle('fetch-data', async (event, args) => { const data = await fetchDataFromDB(args); // 返回 Promise return data; // 自动发送给渲染进程 }); // 渲染进程:调用并等待结果 const result = await ipcRenderer.invoke('fetch-data', { id: 1 }); console.log(result); |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 错误处理 // 主进程 ipcMain.handle('get-file', async (event, path) => { return fs.promises.readFile(path); // 自动处理成功/失败 }); // 渲染进程 try { const data = await ipcRenderer.invoke('get-file', 'file.txt'); } catch (error) { console.error(error); } |
nextTick
队列
api
process.nextTick(callback)
- 优先级最高:在每个
libuv
阶段结束后立即执行 - 完全由
Node.js
管理:与libuv
无关,是Node.js
自身的任务调度机制 - 潜在风险:递归调用
process.nextTick()
可能导致事件循环饥饿(主线程无法进入下一阶段)
microTask
队列
api
Promise.then()
、async/await
、queueMicrotask()
- 优先级次于
nextTick
:在nextTick
队列清空后执行 - 由
V8
引擎管理:遵循ECMAScript
规范,与浏览器中的微任务行为一致 - 与
libuv
无关:是JavaScript
语言层面的异步处理机制
疑问与理解:主进程和渲染进程,同步异步代码
主进程的同步、异步执行
-
主进程的主线程(
Node.js
线程)Electron
的主进程运行在Node.js
环境中,因此其主线程就是Node.js
的主线程- 执行主进程的所有同步代码
- 运行
Node.js
的事件循环(Event Loop
),处理异步操作的回调
-
同步代码
- 在主进程的主线程(即
Node.js
主线程)中直接执行
- 在主进程的主线程(即
-
异步代码
Node.js
的异步操作(如文件读写、网络请求等)会被提交到libuv
的线程池中执行- 完成后,回调函数会被放入事件循环的对应阶段队列(如
I/O callbacks
阶段),等待主线程空闲时按优先级调度执行
渲染进程的同步,异步操作
- 渲染进程的主线程(
UI
线程)- 每个渲染进程的 主线程 由
Chromium
管理,负责处理以下任务:
UI
渲染:HTML/CSS
解析、布局、绘制
JavaScript
执行:执行页面中的同步JavaScript
代码
事件循环:管理任务队列(如DOM
事件、定时器回调、Promise
微任务等)
- 每个渲染进程的 主线程 由
- 渲染进程的同步代码
- 所有同步代码(包括原生
JavaScript
代码和Node.js
同步API
)都在渲染进程的主线程中直接执行 - 如果同步代码耗时过长,会阻塞主线程,导致页面卡顿或无响应
- 所有同步代码(包括原生
1 2 3 4 5 6 7 |
// 纯前端同步代码 // 渲染进程页面脚本(例如 index.html 中的 <script>) console.log('开始同步操作'); let sum = 0; for (let i = 0; i < 1e10; i++) { sum += i; } // 耗时同步循环 console.log('同步操作结束'); |
1 2 3 4 5 6 7 |
// Node.js 同步 API(需启用 Node.js 集成) // 渲染进程页面脚本(需 nodeIntegration: true) const fs = require('fs'); console.log('开始读取文件'); const data = fs.readFileSync('large-file.txt'); // 同步读取大文件 console.log('文件读取完成'); |
- 异步代码如下:
- 未暴露
Node.js
功能的渲染进程- 如果渲染进程未启用
Node.js
集成(nodeIntegration: false
),或通过预加载脚本仅暴露有限功能,则无法直接调用Node.js
的异步API
(如fs.readFile
) - 此类异步操作必须通过
IPC
请求主进程 代为执行,此时实际执行异步操作的是主进程的libuv
线程池
- 如果渲染进程未启用
- 启用
Node.js
集成的渲染进程- 如果渲染进程启用了 Node.js 集成(
nodeIntegration: true
)且未启用上下文隔离(contextIsolation: false
),则渲染进程可以直接调用Node.js
的异步API
- 此时异步操作由渲染进程自身的
libuv
线程池处理,而非主进程的线程池。这意味着:
渲染进程的异步任务(如setTimeout
、fs.readFile
)会使用自己的事件循环
如果渲染进程崩溃,其未完成的异步操作也会终止
- 如果渲染进程启用了 Node.js 集成(
- 通过预加载脚本暴露部分功能
- 若渲染进程禁用了
Node.js
集成(nodeIntegration: false
)但通过 预加载脚本(Preload Script
) 暴露了特定Node.js
功能(如ipcRenderer
或自定义异步方法),则: - 预加载脚本中的异步操作会使用 主进程的
libuv
线程池(因为预加载脚本运行在独立上下文,但依赖主进程的Node.js
环境) - 渲染进程的前端代码(如页面脚本)仍无法直接访问
Node.js
,需通过预加载脚本代理
- 若渲染进程禁用了
libuv
相关
- 主线程启动时初始化:
- 当
Node.js
主进程启动时,会自动初始化libuv
库,并创建以下核心组件: - 事件循环(
Event Loop
):管理异步任务的生命周期,包括多个阶段(如Timers
、I/O Callbacks
等) - 线程池(
Thread Pool
):默认创建4
个线程(可通过环境变量UV_THREADPOOL_SIZE
调整),用于处理文件I/O
、DNS
解析等阻塞型操作
- 当
- 主线程的事件循环:
Node.js
事件循环=
libuv
事件循环的封装libuv
事件循环:是跨平台的异步I/O
库(C
语言实现),为Node.js
提供事件循环的基础设施Node.js
事件循环:在libuv
事件循环的基础上,Node.js
进行了扩展和封装,使其能够处理JavaScript
层的异步逻辑(如Promise
、nextTick
等)
libuv
的任务队列Timers(setTimeout/setInterval)
Pending I/O
(系统中断等)Idle/Prepare
(内部使用)Poll
(检索新I/O
事件)Check
(setImmediate
)Close
(关闭事件回调)- 也就是说,上面这
6
个阶段的任务队列是libuv
原生自有的,它们是libuv
事件循环的核心设计,与Node.js
的封装无关
node.js
自己的任务队列node.js
封装了libuv
,在对方的基础上,加了两个任务队列,nextTick
和microTask
,这是node.js
自有的nextTick
队列:由process.nextTick()
触发microTask
队列:由Promise
回调触发
- 异步任务的执行流程:
- 主线程提交任务:当调用异步 API(如
fs.readFile
)时,主线程将任务提交给libuv
线程池 - 线程池执行任务:线程池中的工作线程执行具体的阻塞操作(如读取文件)
- 任务完成通知:操作完成后,线程池将回调函数放入
Pending I/O
队列 - 主线程处理回调:事件循环进入
I/O Poll
阶段时,主线程执行这些回调
- 主线程提交任务:当调用异步 API(如
疑问与理解:框架构成
Node.js
- 基于以下三个核心组件构建的运行时环境:
组件 | 作用 | 与 Node.js 的关系 |
V8 引擎 |
由 Google 开发的 JavaScript 引擎,负责解析和执行 JS 代码 |
Node.js 使用 V8 作为 JS 代码的执行引擎(类似浏览器的 JS 引擎,如 Chrome 的 V8 ) |
libuv |
跨平台的异步 I/O 库,提供事件循环、线程池、文件系统操作等底层能力 |
Node.js 通过 libuv 实现非阻塞 I/O 和事件循环,处理异步任务(如文件读写、网络请求) |
Node.js 核心模块 |
内置的 JavaScript/C++ 模块(如 fs 、http 、path ) |
这些模块封装了 libuv 和底层系统调用,提供高层次的 API (如 fs.readFile ) |
- 运行环境
- 服务端/命令行工具
- 进程模型
- 单进程(可扩展为多进程)
Electron
Electron
在Node.js
基础上 集成了Chromium
,因此可以同时运行Node.js
主进程和Chromium
渲染进程- 但在纯
Node.js
环境中,无需Chromium
- 但在纯
- 运行环境
- 桌面应用(主进程 + 多个渲染进程)
- 进程模型
- 默认多进程(主进程管理多个渲染进程)
Node.js
的架构层级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
+-----------------------------------+ | Node.js 应用代码 | | (如: http.createServer, fs.readFile) | +-----------------------------------+ ↓ +-----------------------------------+ | Node.js 核心模块 | | (如: fs, http, net, path 等) | +-----------------------------------+ ↓ ↓ ↓ +----------------+ +----------------+ +----------------+ | V8 | | libuv | | C/C++ 绑定 | | (执行 JS 代码) | | (异步 I/O 处理) | | (桥接 JS 与 C++) | +----------------+ +----------------+ +----------------+ |
Electron
相比CEF
electron
≈ Chromium + Node.js + Web
技术栈
cef
≈ Chromium + C/C++ API
关于配置
为每个窗口配置单独的渲染进程
- 概述
- 在
Electron
中,默认情况下每个窗口(BrowserWindow
)会创建一个独立的渲染进程,但窗口内的子页面(如通过<iframe>
或<webview>
加载的内容)默认会共享父窗口的渲染进程
- 在
- 强制每个页面(包括子页面)单独创建渲染进程,可以通过以下配置实现:
- 方法
1
:webview
相关- 使用
<webview>
标签并设置partition
- 关键参数:
partition
如果多个<webview>
使用相同的partition
,它们会共享一个进程
若设为唯一值(如persist:page1
、persist:page2
),则每个<webview>
会创建独立的渲染进程 <webview>
标签默认禁用,需在父窗口的webPreferences
中启用:
- 使用
1 2 3 |
<!-- 在 HTML 文件中使用 webview --> <webview src="https://example.com/page1" partition="persist:page1"></webview> <webview src="https://example.com/page2" partition="persist:page2"></webview> |
1 2 3 4 5 6 |
// 主进程创建窗口时配置 new BrowserWindow({ webPreferences: { webviewTag: true // 启用 webview 标签 } }); |
- 方法
2
:webview
相关- 通过
BrowserView
动态附加独立进程 - 通过
BrowserView
可以更灵活地控制子内容的布局和进程隔离
- 通过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const { BrowserView, BrowserWindow } = require('electron'); const win = new BrowserWindow(); const view1 = new BrowserView({ webPreferences: { partition: 'view1' // 独立进程标识 } }); const view2 = new BrowserView({ webPreferences: { partition: 'view2' // 另一个独立进程标识 } }); win.addBrowserView(view1); win.addBrowserView(view2); view1.webContents.loadURL('https://example.com/page1'); view2.webContents.loadURL('https://example.com/page2'); |
- 方法
1
:iframe
相关- 若希望
<iframe>
中的内容运行在独立进程中,需配置父窗口的webPreferences
: sandbox: false
:禁用沙箱模式,允许子框架使用独立进程(但需谨慎,可能降低安全性)partition
:若不同<iframe>
使用不同partition
,则会创建独立进程
- 若希望
1 2 3 4 5 6 7 8 9 10 11 |
// 主进程创建窗口时配置 new BrowserWindow({ webPreferences: { nodeIntegration: true, webviewTag: true, // 强制所有子框架(iframe)使用独立进程 sandbox: false, // 或通过 partition 控制进程复用 partition: 'unique-partition-id' } }); |
- 多窗口模式下自动隔离
- 直接为每个窗口创建独立的
BrowserWindow
实例: - 每个窗口自动分配独立渲染进程,无需额外配置
- 直接为每个窗口创建独立的
1 2 3 4 5 6 7 8 9 10 |
// 主进程中创建多个窗口 const { BrowserWindow } = require('electron'); function createWindow(url) { const win = new BrowserWindow(); win.loadURL(url); } createWindow('https://example.com/page1'); createWindow('https://example.com/page2'); |
- 完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// main.js const { app, BrowserWindow } = require('electron'); app.whenReady().then(() => { // 主窗口(独立进程) const mainWin = new BrowserWindow({ webPreferences: { webviewTag: true } }); mainWin.loadFile('index.html'); // 子窗口(独立进程) const subWin = new BrowserWindow(); subWin.loadURL('https://example.com'); }); |
1 2 3 4 5 6 7 8 |
// index.html <!DOCTYPE html> <body> <!-- 通过 webview 加载独立进程的页面 --> <webview src="https://example.com/page1" partition="persist:page1"></webview> <webview src="https://example.com/page2" partition="persist:page2"></webview> </body> |
硬件加速策略
1 2 3 4 5 6 7 |
app.disableHardwareAcceleration(); // 禁用全局硬件加速 // 或按窗口配置 new BrowserWindow({ webPreferences: { enablePreferredSizeMode: true // 优化渲染性能 } }); |
内容安全策略(CSP
)
- 防止
XSS
攻击,限制资源加载源
1 |
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> |
权限控制
1 2 3 |
const { systemPreferences } = require('electron'); // 检查摄像头权限 systemPreferences.askForMediaAccess('camera'); |
自定义协议(Protocol
)
1 2 3 |
protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { standard: true, secure: true } } ]); |
代理设置
1 2 3 |
session.defaultSession.setProxy({ proxyRules: 'http://proxy.example.com:8080' }); |
自定义菜单
1 2 3 4 5 |
const { Menu } = require('electron'); Menu.setApplicationMenu(Menu.buildFromTemplate([{ label: '文件', submenu: [{ role: 'quit' }] }])); |
系统托盘
1 2 3 |
const { Tray } = require('electron'); const tray = new Tray('icon.png'); tray.setToolTip('我的应用'); |
懒加载
1 2 3 4 |
// 动态加载非关键模块 window.addEventListener('load', () => { import('./heavy-module.js').then(module => module.init()); }); |
开启 DevTools
1 2 |
const win = new BrowserWindow(); win.webContents.openDevTools(); // 开发模式开启调试工具 |
集成日志框架
1 2 |
const log = require('electron-log'); log.info('应用启动成功'); |
自动更新
1 2 |
const { autoUpdater } = require('electron-updater'); autoUpdater.checkForUpdatesAndNotify(); |
代码签名(发布必备)
- 在
package.json
中设置签名证书(Windows/macOS
)
使用 Worker
进程
1 2 |
const { Worker } = require('worker_threads'); new Worker('./task.js'); |
多窗口协作
1 2 3 4 |
// 主进程管理多个窗口的通信 ipcMain.on('sync-data', (event, data) => { allWindows.forEach(win => win.webContents.send('update-data', data)); }); |
修改 Chromium
标志
1 |
app.commandLine.appendSwitch('disable-gpu'); // 禁用 GPU 加速 |
自定义 V8
参数
1 |
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096'); // 调整内存限制 |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Base_file05/27
- ♥ WebSocket协议相关学习一03/24
- ♥ 【Javascript】回调,promise,promise链04/13
- ♥ HTTP协议相关学习一03/22
- ♥ Cef:介绍06/29
- ♥ Chromium:多线程通信机制09/03