Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • shell

  • tool

  • client

    • electron
      • 一、Electron 是什么
      • 二、Main Process(主进程)
      • 三、Renderer Process(渲染进程)
      • 四、为什么 Renderer 不能直接访问 Node.js
      • 五、Preload + contextBridge(安全桥梁)
        • 标准写法
        • contextBridge 为什么重要
        • 实际项目示例:expression-trainer 的 analyzeText 完整流转
        • 项目技术栈速览
        • 完整调用链路
        • 逐层代码对照
        • 第 1 层:渲染进程调用
        • 第 2 层:Preload 桥接
        • 第 3 层:主进程 IPC 注册
        • 第 4 层:业务逻辑(纯规则,无 AI)
        • 对照表:每层在做什么
        • 这个例子体现的 Electron 设计原则
        • 同项目的其他 IPC 通道(共 16 个)
      • 六、最重要的安全配置
      • 七、IPC 也是安全边界
      • 八、不要把 Secret 放在 Renderer
      • 九、加载第三方网页的风险
      • 十、推荐的生产级目录结构
      • 十一、总结:一张图记住
      • 十二、下一步学习重点
  • 网络

  • compute_base

  • blog

  • growth

  • java

  • C&C++

  • ai

  • secure

  • cms

  • english

  • 生活

  • 金融学

  • more

  • other
  • client
Jacky
2026-08-15
目录

electron

# Electron 核心知识

用 Web 技术(HTML/CSS/JS)开发跨平台桌面应用,底层运行时 = Chromium + Node.js。

首次接触 Electron,最该关注的不是"怎么写 UI",而是 Main / Renderer / Preload / IPC 之间的安全边界。

# 一、Electron 是什么

Electron App
│
├── Chromium      → 渲染进程(Renderer Process),跑 HTML/CSS/JS/React/Vue
├── Node.js       → 主进程(Main Process),跑文件、网络、系统 API、窗口管理
└── Electron API  → IPC 通信,连接上面两者
1
2
3
4
5

一个最小 Electron 项目的文件结构:

my-app/
├── main.js        # 主进程入口
├── preload.js     # 预加载脚本(安全桥梁)
├── index.html     # 渲染进程页面
└── renderer.js    # 渲染进程逻辑
1
2
3
4
5

三者关系:

          ┌─────────────────────┐
          │   Electron 运行时    │
          └──────────┬──────────┘
                     │
        ┌────────────┴────────────┐
        │                         │
   Main Process            Renderer Process
     Node.js                Chromium
        │                         │
        └──────── IPC ────────────┘
                   │
               Preload(安全桥梁)
1
2
3
4
5
6
7
8
9
10
11
12

关键类比

把 Electron 类比成 Android:

  • Main Process ≈ Application / 系统服务层
  • Renderer ≈ Activity / WebView UI
  • Preload ≈ Bridge / Binder 接口层
  • IPC ≈ Binder / Intent
  • contextIsolation ≈ 进程权限隔离

# 二、Main Process(主进程)

应用启动后第一个运行的进程,有且只有一个。拥有完整 Node.js 权限。

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

app.whenReady().then(() => {
  const win = new BrowserWindow({ width: 1200, height: 800 });
  win.loadFile('index.html');
});
1
2
3
4
5
6

主进程职责:

  • 创建和管理窗口(BrowserWindow)
  • 应用生命周期(app.whenReady / window-all-closed)
  • 菜单、托盘、系统通知
  • 文件系统、网络请求、子进程
  • 所有系统级原生 API
  • 注册 IPC 处理器(ipcMain.handle)

定位

主进程 = 桌面应用的系统服务层 / 后端,高权限。


# 三、Renderer Process(渲染进程)

每个 BrowserWindow 对应一个渲染进程,本质就是一个 Chromium 标签页。

// renderer.js —— 和写前端一模一样
document.querySelector('#btn').addEventListener('click', () => {
  console.log('hello');
});
1
2
3
4

渲染进程能做:

  • HTML / CSS / JavaScript
  • React / Vue / TypeScript
  • DOM 操作、Web API

渲染进程默认不能做(现代 Electron 安全配置下):

  • ❌ require('fs')
  • ❌ require('child_process')
  • ❌ 任何 Node.js API

核心原则

Renderer ≠ Node.js。渲染进程必须被当作"不可信的 Web 页面"来对待。


# 四、为什么 Renderer 不能直接访问 Node.js

假设开启了 nodeIntegration: true:

// 渲染进程里的恶意脚本(XSS 注入)
require('child_process').exec('rm -rf ~/*');   // 删光用户文件
require('fs').readFileSync('~/.ssh/id_rsa');    // 偷私钥
1
2
3

攻击链路:

远程网页 XSS
    ↓
Renderer JS(有 Node 权限)
    ↓
文件系统 / Shell
    ↓
操作系统被接管
1
2
3
4
5
6
7

一个普通的 Web XSS 直接升级为远程代码执行(RCE)。这就是为什么现代 Electron 默认关闭 nodeIntegration。


# 五、Preload + contextBridge(安全桥梁)

Preload 是渲染进程加载页面之前执行的脚本,运行在独立上下文中,可以使用 Node.js,但通过 contextBridge 只暴露有限 API 给页面。

# 标准写法

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  getUserInfo: () => ipcRenderer.invoke('get-user-info'),
  saveConfig: (config) => ipcRenderer.invoke('save-config', config)
});
1
2
3
4
5
6
7
// renderer.js —— 只能拿到 preload 暴露的方法
const user = await window.api.getUserInfo();
1
2
// main.js —— 主进程注册对应处理器
ipcMain.handle('get-user-info', async () => {
  return { name: 'Jack' };
});
1
2
3
4

完整调用链:

Renderer (window.api.getUserInfo)
    ↓ contextBridge 包装
Preload (ipcRenderer.invoke)
    ↓ IPC 跨进程
Main (ipcMain.handle)
    ↓
Node.js / 系统能力
1
2
3
4
5
6
7

# contextBridge 为什么重要

错误写法(等于把万能钥匙交出去):

contextBridge.exposeInMainWorld('api', {
  ipcRenderer  // ❌ 暴露完整 ipcRenderer,可发任意 IPC 消息
});
1
2
3

正确写法(白名单,只暴露具体方法):

contextBridge.exposeInMainWorld('api', {
  getUserInfo: () => ipcRenderer.invoke('get-user-info'),
  saveConfig: (config) => ipcRenderer.invoke('save-config', config)
  // ✅ 每个方法都是一个受控的 IPC 通道
});
1
2
3
4
5

核心思想

不要给 Renderer 一把万能钥匙,而是提供有限的白名单 API。

# 实际项目示例:expression-trainer 的 analyzeText 完整流转

项目地址:https://github.com/fxy2311-youyou/expression-trainer (opens new window) \ https://github.com/jacky1234/expression-trainer (opens new window) 以"词库分析"功能为例,追踪一次 window.api.analyzeText(text) 调用从渲染进程到主进程再返回的完整路径。

# 项目技术栈速览

层 技术 文件
主进程 Electron 33 + Node.js main.js
Preload contextBridge 白名单 preload.js
渲染进程 原生 HTML/CSS/JS(无框架) src/index.html + src/app.js
业务逻辑 纯 JS 模块 lib/lexicon.js、lib/asr.js、lib/ai-feedback.js
语音识别 sherpa-onnx-node(离线 ASR) models/ 目录

# 完整调用链路

渲染进程 (src/app.js)
    │
    │  ① 实时识别每句话 / 粘贴文本逐句
    │  const analysis = await window.api.analyzeText(text)
    ▼
Preload (preload.js:27)
    │
    │  ② analyzeText: (text) => ipcRenderer.invoke('analyze-text', text)
    │     函数调用 → 序列化为 IPC 消息
    ▼
IPC 通道(Electron 内部,跨进程传递)
    │
    │  channel = 'analyze-text', payload = text
    ▼
主进程 (main.js:305-307)
    │
    │  ③ ipcMain.handle('analyze-text', (event, text) => {
    │       return analyzeText(text);
    │     })
    │     匹配 channel → 调用 lib/lexicon.js
    ▼
业务逻辑 (lib/lexicon.js:107-170)
    │
    │  ④ segmentText(text)              → 中文分词(最大正向匹配 2~6 字)
    │  ⑤ 遍历分词结果,查表检测四类词:
    │     - FILLER_WORDS     → 填充词(嗯、啊、那个、就是...)
    │     - HEDGE_WORDS      → 犹豫词(可能、大概、我觉得...)
    │     - VAGUE_TO_PRECISE → 笼统词(做、说、好,带替换建议)
    │     - lexiconData      → 情绪词(27000 词库)
    │  ⑥ 表达密度 = (总词数 - 填充词 - 犹豫词) / 总词数
    │  ⑦ generateSuggestions() → 生成文字建议
    │  ⑧ return { totalWords, fillers, hedges, vagueWords, emotionWords, density, suggestions }
    ▼
返回值(原路返回)
    │
    │  对象 → IPC 序列化 → Promise resolve
    ▼
渲染进程 (app.js:230-239)
    │
    │  ⑨ 累加 stats(fillers / hedges / vagueWords / totalWords)
    │     updateStatsDisplay() → 刷新左侧统计面板
    │     碰到笼统词 → 右侧反馈栏弹出替换建议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 逐层代码对照

# 第 1 层:渲染进程调用

src/app.js 中有两处调用:

// 场景 A:实时录制中,每识别完一句话调一次(line 228)
async analyzeCurrentSentence(text) {
  const analysis = await window.api.analyzeText(text);
  if (analysis) {
    this.stats.fillers += analysis.fillers.length;
    this.stats.hedges += analysis.hedges.length;
    this.stats.vagueWords += analysis.vagueWords.length;
    this.stats.totalWords += analysis.totalWords;
    this.updateStatsDisplay();
    // 笼统词 → 即时弹替换建议
    if (analysis.vagueWords?.length > 0) {
      analysis.vagueWords.forEach(item => {
        const alts = item.alternatives.slice(0, 3).join(' / ');
        // ... 渲染到反馈栏
      });
    }
  }
}

// 场景 B:粘贴文本模式,按句号切分后逐句循环(line 463-477)
for (const sentence of sentences) {
  const analysis = await window.api.analyzeText(sentence);
  if (analysis) {
    this.stats.fillers += analysis.fillers.length;
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 第 2 层:Preload 桥接

preload.js 只做透传,无业务逻辑:

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  // ... 其他 15 个方法
  analyzeText: (text) => ipcRenderer.invoke('analyze-text', text),  // line 27
  // ...
});
1
2
3
4
5
6
7

关键细节

ipcRenderer.invoke(channel, ...args) 是请求-响应模式,返回一个 Promise。主进程 return 的值会自动作为 Promise 的 resolve 值,不需要手动发回消息。

# 第 3 层:主进程 IPC 注册

main.js 注册 channel 处理器:

// line 5:引入业务模块
const { loadLexicon, analyzeText } = require('./lib/lexicon');

// line 305-307:注册处理器
ipcMain.handle('analyze-text', (event, text) => {
  return analyzeText(text);
});
1
2
3
4
5
6
7

handler 签名

ipcMain.handle(channel, (event, ...args) => {...}) —— 第一个参数永远是 event 对象(含 sender 等元信息),从第二个参数开始才是渲染进程传过来的实际数据。

# 第 4 层:业务逻辑(纯规则,无 AI)

lib/lexicon.js 的 analyzeText 是纯函数,不调任何大模型:

function analyzeText(text) {
  if (!text || !text.trim()) return null;  // 空文本直接返回

  const words = segmentText(text);          // 中文分词
  const totalWords = words.length;

  // 检测填充词
  const fillers = [];
  words.forEach((word, idx) => {
    if (FILLER_WORDS.includes(word)) fillers.push({ word, position: idx });
  });

  // 检测犹豫词
  const hedges = [];
  words.forEach((word, idx) => {
    if (HEDGE_WORDS.includes(word)) hedges.push({ word, position: idx });
  });

  // 检测笼统词(带替换建议)
  const vagueWords = [];
  words.forEach((word, idx) => {
    if (VAGUE_TO_PRECISE[word]) {
      vagueWords.push({ word, position: idx, alternatives: VAGUE_TO_PRECISE[word] });
    }
  });

  // 检测情绪词(27000 词库,启动时加载到内存)
  const emotionWords = [];
  if (lexiconData?.emotions) {
    words.forEach((word, idx) => {
      if (lexiconData.emotions[word]) emotionWords.push({ word, position: idx, ...lexiconData.emotions[word] });
    });
  }

  // 表达密度
  const meaningfulWords = totalWords - fillers.length - hedges.length;
  const density = totalWords > 0 ? (meaningfulWords / totalWords) : 1;

  return {
    totalWords, fillers, hedges, vagueWords, emotionWords,
    density: Math.round(density * 100),
    suggestions: generateSuggestions(vagueWords, fillers, hedges)
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 对照表:每层在做什么

层 文件:行 代码 职责
渲染进程触发 app.js:229 / app.js:470 await window.api.analyzeText(text) 拿到识别文本后请求分析
Preload 桥接 preload.js:27 ipcRenderer.invoke('analyze-text', text) 函数调用 → IPC 消息,白名单透传
主进程注册 main.js:305-307 ipcMain.handle('analyze-text', (event, text) => analyzeText(text)) 接收 IPC,分发到业务模块
分词 lexicon.js:112 segmentText(text) 中文分词(2~6 字最大正向匹配)
四类检测 lexicon.js:116-155 查 FILLER / HEDGE / VAGUE / emotions 表 纯规则匹配,计数 + 定位
密度计算 lexicon.js:158-159 meaningfulWords / totalWords 百分比
渲染进程消费 app.js:231-239 累加 stats、刷新面板、弹建议 更新 UI

# 这个例子体现的 Electron 设计原则

  1. 渲染进程不碰业务逻辑——分词、查词库全在主进程,渲染进程只负责 UI
  2. Preload 零逻辑——只做 window.api.xxx → ipcRenderer.invoke 的映射
  3. 主进程持有重资源——27000 词库加载在主进程内存,渲染进程不持有
  4. IPC 参数即不可信输入——虽然这里没做校验(纯分析无副作用),但涉及文件/网络的 IPC 必须校验
  5. invoke 是请求-响应——不需要手动 webContents.send 回传,return 即可

# 同项目的其他 IPC 通道(共 16 个)

通道 Preload 方法 主进程处理 用途
get-settings getSettings() loadSettings() 读 AI 后端配置
save-settings saveSettings(s) saveSettings(s) 保存配置到 userData
open-settings openSettings() createSettingsWindow() 打开设置窗口
get-custom-prompt getCustomPrompt() loadCustomPrompt() 读自定义 Prompt 模板
save-custom-prompt saveCustomPrompt(d) saveCustomPrompt(d) 保存自定义 Prompt
init-asr initASR() 初始化 sherpa-onnx 启动离线语音识别
feed-audio feedAudio(samples) 喂音频流给 ASR 实时音频识别
analyze-text analyzeText(text) lib/lexicon.js 词库分析(本文示例)
get-realtime-feedback getRealtimeFeedback(t) lib/ai-feedback.js AI 实时反馈
get-final-report getFinalReport(d) lib/ai-feedback.js AI 总结报告
test-llm-connection testLLMConnection(s) 测试 API 连通性 保存设置时测通
save-file saveFile(c, f) 写文件 导出报告
close-current-window closeWindow() win.close() 关闭子窗口

# 六、最重要的安全配置

创建窗口时的 webPreferences:

const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,    // 渲染进程禁用 Node.js
    contextIsolation: true,    // 隔离 Preload 和网页 JS 上下文
    sandbox: true,             // 增强渲染进程沙箱(可选但推荐)
    preload: path.join(__dirname, 'preload.js')
  }
});
1
2
3
4
5
6
7
8
配置 推荐值 意义
nodeIntegration false Renderer 禁止直接使用 Node.js
contextIsolation true 隔离 Preload 与网页的 JS 运行环境,防止网页篡改 Preload 暴露的对象
sandbox true 增强 Renderer 沙箱隔离,Preload 也受限
preload 指定文件 提供受控 API 桥梁
CSP 配置 限制脚本来源,防 XSS
HTTPS 使用 防止网络传输被劫持

注意

contextIsolation: true 时,Preload 里直接 window.xxx = ... 网页是看不到的,必须用 contextBridge.exposeInMainWorld。


# 七、IPC 也是安全边界

很多人以为 IPC 是内部通信就放松警惕,实际上 IPC 参数必须被当作不可信输入。

危险设计:

// main.js
ipcMain.handle('execute', (event, command) => {
  return exec(command);  // ❌ 任意命令执行
});
1
2
3
4
// renderer.js
window.api.execute('rm -rf /');  // 直接删根目录
1
2

安全设计(白名单 + 校验):

ipcMain.handle('open-file', async (event, fileId) => {
  if (!ALLOWED_FILE_IDS.includes(fileId)) {
    throw new Error('permission denied');
  }
  return readFile(fileId);
});
1
2
3
4
5
6

原则

即使请求来自"自己的 Renderer",也不能认为参数天然可信——XSS 可以伪造任何 IPC 调用。


# 八、不要把 Secret 放在 Renderer

// renderer.js ❌
const API_KEY = 'sk-xxxxxxxx';
1
2

Electron 打包后 JS 在 app.asar 里,用户可以直接解压读取,等于公开。

正确架构:

Renderer(无 Key)
    ↓ Preload / IPC
Main(持有 API_KEY)
    ↓ HTTPS
OpenAI / 自建服务
1
2
3
4
5

API Key 只存在于主进程,渲染进程通过 IPC 请求主进程代发请求。


# 九、加载第三方网页的风险

如果应用需要加载远程页面:

win.loadURL('https://third-party.com');
1

风险链路:

第三方网页(不可信)
    ↓
Renderer
    ↓
Preload API(如果暴露了 deleteFile / executeCommand)
    ↓
Main(高权限)
    ↓
本机被控制
1
2
3
4
5
6
7
8
9

对策:

  • 第三方网页用独立的 BrowserWindow / WebContents,不挂 Preload 或挂最小权限 Preload
  • 自己的 UI 页面才挂完整 Preload API
  • 开启 sandbox: true

# 十、推荐的生产级目录结构

electron-app/
├── main/                  # 主进程
│   ├── index.ts           # 入口
│   ├── ipc/               # IPC 处理器(按领域拆分)
│   │   ├── user.ts
│   │   ├── file.ts
│   │   └── system.ts
│   └── services/          # 业务逻辑
│       ├── fileService.ts
│       └── userService.ts
├── preload/
│   └── index.ts           # contextBridge 白名单
├── renderer/              # 渲染进程(前端)
│   ├── src/
│   │   ├── components/
│   │   ├── pages/
│   │   └── services/      # 调用 window.api
│   └── index.html
└── shared/                # 主进程与渲染进程共享
    ├── types/             # TypeScript 类型定义
    └── constants/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

核心原则:

renderer  →  只能调用有限 API (window.api)
preload   →  IPC 白名单转发
main      →  系统 / 网络 / 文件系统
1
2
3

# 十一、总结:一张图记住

                    Internet
                        │
                      HTTPS
                        ▼
┌──────────────────────────────────────────────┐
│                  Electron                    │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │          Renderer Process              │  │
│  │                                        │  │
│  │     React / Vue / TypeScript           │  │
│  │                                        │  │
│  │     ❌ Node.js                         │  │
│  │     ❌ fs                              │  │
│  │     ❌ child_process                   │  │
│  └────────────────┬───────────────────────┘  │
│                   │                          │
│             Limited API(白名单)             │
│                   ↓                          │
│  ┌────────────────────────────────────────┐  │
│  │              Preload                   │  │
│  │                                        │  │
│  │       contextBridge                    │  │
│  │       ipcRenderer                      │  │
│  └────────────────┬───────────────────────┘  │
│                   │ IPC                      │
│                   ↓                          │
│  ┌────────────────────────────────────────┐  │
│  │               Main                     │  │
│  │                                        │  │
│  │       Node.js                          │  │
│  │       File System                      │  │
│  │       Native API                       │  │
│  │       Network(持有 Secret)            │  │
│  └────────────────────────────────────────┘  │
│                                              │
└──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

一句话总结

把 Renderer 当成"不可信 Web 页面",把 Main 当成"高权限系统服务",Preload + IPC 是两者之间严格受控的安全边界。


# 十二、下一步学习重点

实际开发 Electron,优先搞懂这 5 个东西:

  1. contextIsolation — 为什么要隔离,不隔离会怎样
  2. contextBridge — 正确暴露白名单 API
  3. IPC — ipcRenderer.invoke / ipcMain.handle 的请求-响应模式
  4. sandbox — 渲染进程沙箱增强
  5. CSP — Content Security Policy,防 XSS

这五个基本就是 Electron 安全架构的全部核心。

#electron#web#client
上次更新: 2026/08/26, 21:22:46
mermaid 入门指南
network category

← mermaid 入门指南 network category→

最近更新
01
brew
09-08
02
位运算参考文档
09-06
03
Swift 开发最佳实践
08-26
更多文章>
Theme by Vdoing | Copyright © 2019-2026 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式