主进程
职责
- 应用生命周期管理:控制应用启动、退出,监听
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> 标签默认禁用
- 安全风险:防止滥用特权
APINode.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: 允许执行JavaScriptallow-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 队列
apiprocess.nextTick(callback)
- 优先级最高:在每个
libuv阶段结束后立即执行 - 完全由
Node.js管理:与libuv无关,是Node.js自身的任务调度机制 - 潜在风险:递归调用
process.nextTick()可能导致事件循环饥饿(主线程无法进入下一阶段)
microTask队列
apiPromise.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所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Visual Studio:子进程调试相关07/15
- ♥ 【Javascript】第二部分05/14
- ♥ Chromium:鼠标事件的生成与处理07/19
- ♥ 【Javascript】应用一:图片,视频,编码相关04/29
- ♥ 【Javascript】对象构造05/20
- ♥ Base_hash05/25