简介:本项目是一个使用Electron框架和Vue.js 3构建的跨平台Windows桌面应用程序,推测为一款功能完整的笔记应用(iNote)。项目融合现代Web技术与桌面系统能力,通过Electron整合Chromium与Node.js,实现前端界面与原生系统交互的无缝结合。采用Vue 3的响应式机制、组件化架构及Composition API,提升代码可维护性与开发效率。项目结构包含主进程控制、渲染进程UI展示、静态资源管理及构建配置,使用JavaScript(ECMAScript)作为核心开发语言,支持文件系统操作、窗口管理和本地部署。适用于希望掌握桌面端全栈开发的技术人员,是前端技术向桌面领域延伸的典型实践。
Electron + Vue 3 桌面应用开发全栈指南
你有没有遇到过这样的情况?好不容易用 Electron 做了个跨平台桌面软件,结果一发布就被用户的杀毒软件当成病毒拦截;或者在 Windows 上运行得好好的功能,到了 macOS 就莫名其妙崩溃了。🤯 更别提那些因为 IPC 通信不当导致的安全漏洞,简直是开发者噩梦。
其实这些问题背后,往往不是技术选型的问题——Electron 结合 Vue 3 的这套组合拳,在现代桌面开发中依然是王炸配置。真正决定成败的,是我们如何构建整个架构体系。今天咱们就来彻底拆解一下这个“从零到上线”的完整链路,不讲虚的,全是实战中踩过坑、流过血换来的经验。
架构基石:主进程与渲染进程的共生关系
先问一个问题:你知道为什么 Electron 要搞出两个独立进程吗?🤔 很多人可能脱口而出“为了安全”,但这只是表面答案。真正的设计哲学在于 职责分离 + 安全边界控制 。
想象一下,如果你把所有代码都塞在一个进程里,一旦某个页面因为加载恶意脚本而崩溃,整个应用都会跟着挂掉。这就像一栋大楼只有一个电源开关一样危险。而 Electron 的做法是:
- 主进程 (Main Process)负责掌管全局资源调度、系统级操作(比如文件读写、窗口管理),相当于“操作系统内核”;
- 渲染进程 (Renderer Process)则专注于 UI 展示和用户交互,每个窗口都是一个独立的 Chromium 实例,互不影响。
graph TD
A[main.js] -->|app.on('ready')| B[创建BrowserWindow]
B --> C[加载index.html]
C --> D[启动渲染进程]
D --> E[运行Vue应用]
B --> F[监听IPC消息]
看到没?这就是典型的“生产者-消费者”模型。主进程负责创建窗口(生产),渲染进程消费 HTML/CSS/JS 来呈现界面。两者之间不能直接对话,必须通过 IPC(Inter-Process Communication)机制传递消息。
💡 这里有个容易被忽视的关键点:默认情况下,渲染进程是无法访问 Node.js API 的!也就是说你在
console.log(require)会发现它是 undefined。这是 Electron 主动设置的安全隔离墙。
那怎么才能让前端代码调用本地文件系统呢?这就引出了我们最核心的桥梁—— preload 脚本。
Preload 脚本:打通前后端的信任通道
你可以把 preload.js 看作是一个“外交官”。它运行在一个特殊的上下文中,既能访问 Node.js 的能力,又能向渲染进程暴露受控接口。
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
const validChannels = ['fromMain']
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
注意这里用了 contextBridge.exposeInMainWorld 而不是直接挂载到 window 上。这是因为在开启了 contextIsolation: true 后,普通的变量赋值是无效的。只有通过 contextBridge 注入的内容才会被正确映射。
而且我还加了个白名单校验( validChannels ),防止任意频道被滥用。这种“最小权限原则”是构建安全应用的基础思维。
Vue 3 登场:不只是个前端框架那么简单
现在很多人还在用 Vue 2 开发 Electron 应用,但说实话,当你真正开始做复杂项目时,Options API 的局限性就会暴露无遗。尤其是面对频繁的 IPC 通信、状态同步这些场景,逻辑碎片化问题会让你怀疑人生。
举个例子,假设你要实现一个“点击按钮 → 打开文件选择器 → 读取内容 → 更新 UI”的流程。用 Options API 写出来可能是这样:
export default {
data() {
return {
filePath: '',
content: ''
}
},
methods: {
openFile() {
ipcRenderer.send('open-file')
},
readFile(path) {
fs.readFile(path, 'utf8', (err, data) => {
if (!err) this.content = data
})
}
},
mounted() {
ipcRenderer.on('file-selected', (e, path) => {
this.filePath = path
this.readFile(path)
})
}
}
看起来好像没问题?但如果这个组件还要处理自动保存、版本对比、编码检测等功能呢?很快你的 methods 和 mounted 里就会堆满各种回调,根本看不出完整的业务流。
而 Composition API 的出现,彻底改变了这一点。
Composition API:按逻辑单元组织代码的新范式
让我们用同样的需求,但这次用 setup() 函数来重构:
import { ref, onMounted } from 'vue'
export function useFileDialog() {
const selectedFilePath = ref('')
const fileContent = ref('')
const openFile = () => {
ipcRenderer.send('open-file-dialog')
}
const loadFileContent = (event, path, content) => {
selectedFilePath.value = path
fileContent.value = content
}
onMounted(() => {
ipcRenderer.on('file-selected', loadFileContent)
})
return {
selectedFilePath,
fileContent,
openFile
}
}
看到了吗?所有关于“文件选择”的逻辑都被封装在了一个函数里。无论你在多少个组件中使用它,都能保持一致的行为。而且调试起来也方便多了——我只需要在 useFileDialog 里打断点就行,不用满屏找 mounted 钩子。
更重要的是,这种模式天然支持 TypeScript。我们可以轻松定义类型:
interface FileDialogResult {
path: string | null
content: string | null
}
export function useFileDialog(): Ref<FileDialogResult>
编译器能自动推导出返回值类型,避免了很多低级错误。
Proxy 带来的响应式革命
Vue 3 最底层的革新其实是它的响应式系统。相比 Vue 2 使用 Object.defineProperty 对属性进行劫持的方式,Vue 3 改用了 ES6 的 Proxy ,带来了质的飞跃。
// Vue 2 的限制
obj.newProp = 'hello' // ❌ 不会被监听!
// Vue 3 的突破
const reactiveObj = new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
return true
}
})
这意味着什么?意味着你现在可以动态添加属性而不用担心视图不更新。对于 Electron 应用来说特别实用——比如你从主进程收到一个深层嵌套的配置对象,以前你还得手动 this.$set() ,现在完全不用操心。
不过也要注意性能问题。如果对象太大(比如几十万行日志数据),建议使用 shallowRef 或 markRaw 来优化:
import { shallowRef, markRaw } from 'vue'
// 只监控根引用变化,内部不做代理
const bigData = shallowRef(largeObject)
// 标记为非响应式(适合第三方库实例)
const chartInstance = markRaw(new Chart(canvas))
Teleport 和 Suspense:解决真实世界难题
除了逻辑组织上的改进,Vue 3 还新增了两个非常实用的内置组件。
Teleport:弹窗不再受父容器束缚
在 Electron 应用中,经常要做模态框、右键菜单之类的功能。它们通常需要脱离当前组件层级,直接挂载到 <body> 下,否则会被 overflow: hidden 截断。
<template>
<div class="editor" style="height: 300px; overflow: hidden;">
<button @click="showModal = true">打开设置</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<h3>编辑器设置</h3>
<SettingsForm />
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</div>
</template>
你看, .editor 明明设置了 overflow: hidden ,但 .modal 依然能覆盖整个屏幕。这就是 Teleport 的魔力。
Suspense:优雅处理异步加载
Electron 应用常需加载远程插件或延迟渲染耗时组件。过去我们只能靠 v-if + loading state 来处理,代码很啰嗦。
<template>
<Suspense>
<template #default>
<AsyncPluginPanel />
</template>
<template #fallback>
<LoadingSpinner message="正在加载插件..." />
</template>
</Suspense>
</template>
<script setup>
const AsyncPluginPanel = defineAsyncComponent(() =>
import('./panels/PluginPanel.vue')
)
</script>
是不是清爽多了?而且它还支持错误捕获、超时处理等高级特性,完全可以胜任企业级应用的需求。
IPC 通信:不只是 send 和 on 那么简单
说到 Electron 开发,绕不开的就是 IPC 通信。但很多人只知道 ipcRenderer.send() 和 ipcMain.on() ,殊不知这套机制已经进化了好几个版本。
弃用 sync,拥抱 async
你见过下面这种写法吗?
// ⚠️ 千万别这么干!
const result = ipcRenderer.sendSync('get-config-sync')
虽然它确实能立刻拿到结果,但它会 完全阻塞渲染进程的主线程 !如果你的操作涉及磁盘 IO 或网络请求,用户界面就会卡住几秒钟甚至更久,体验极差。
所以官方早就标记 sendSync 为废弃,并推荐使用基于 Promise 的新 API:
// ✅ 推荐做法
// preload.js
window.electronAPI = {
getConfig: () => ipcRenderer.invoke('config:get')
}
// main.js
ipcMain.handle('config:get', async () => {
return await fs.promises.readFile(configPath, 'utf8')
})
// 组件中使用
const config = await window.electronAPI.getConfig()
invoke/handle 是异步的,不会冻结 UI,还能配合 try/catch 做错误处理,完美符合现代 JS 编程习惯。
构建类型安全的通信层
大型项目中最怕的就是拼错事件名。为此我建议建立统一的事件中心:
// shared/ipcEvents.js
const IpcEvents = {
DIALOG_OPEN: 'dialog:open',
FILE_OPENED: 'file:opened',
WINDOW_MINIMIZE: 'window:minimize',
APP_READY: 'app:ready'
}
module.exports = IpcEvents
再配合 TypeScript 类型声明:
// types/ipc.d.ts
interface IpcResponse<T> {
success: boolean
data?: T
error?: string
}
declare global {
interface Window {
electronAPI: {
openFile(): Promise<string | null>
minimizeWindow(): void
onAppReady(callback: () => void): void
}
}
}
这样一来,IDE 就能提供智能提示,编译器也能提前发现问题。团队协作效率直接起飞 🚀。
防御性编程:应对恶意输入
别忘了,渲染进程是可以被 XSS 攻击的。攻击者可能通过注入脚本发送伪造的 IPC 消息,试图修改原型链或执行危险操作。
// 恶意 payload 示例
{
"__proto__": {
"isAdmin": true
}
}
如何防御?我的做法是三层防护:
- 白名单过滤 :只允许特定频道通信
- Schema 验证 :对所有输入做结构校验
- CSP 策略 :阻止内联脚本执行
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
flowchart LR
A[用户输入] --> B{是否来自 IPC?}
B -->|是| C[执行 Schema 验证]
C --> D[拒绝非法字段]
C --> E[允许合法数据进入业务逻辑]
B -->|否| F[正常处理]
style D fill:#f66,color:#fff
style E fill:#6f6,color:#fff
记住一句话:永远不要信任客户端传来的任何数据。
模块化设计:打造可复用的组合函数体系
随着项目变大,你会发现有很多通用功能可以在不同地方复用,比如文件操作、剪贴板访问、窗口控制等等。与其到处复制粘贴,不如把这些逻辑抽象成“组合函数”(Composable Functions)。
自定义 Hook:你的专属工具箱
// composables/useWindowControls.ts
import { ref } from 'vue'
export function useWindowControls() {
const isMaximized = ref(false)
ipcRenderer.on('window-maximized', () => {
isMaximized.value = true
})
ipcRenderer.on('window-unmaximized', () => {
isMaximized.value = false
})
const maximize = () => ipcRenderer.send('maximize-window')
const unmaximize = () => ipcRenderer.send('unmaximize-window')
const minimize = () => ipcRenderer.send('minimize-window')
const close = () => ipcRenderer.send('close-window')
return {
isMaximized,
maximize,
unmaximize,
minimize,
close
}
}
然后在任意组件中导入使用:
<script setup lang="ts">
import { useWindowControls } from '@/composables/useWindowControls'
const { isMaximized, minimize, maximize, unmaximize, close } = useWindowControls()
</script>
你会发现,原本分散在各个组件里的窗口控制逻辑,现在变得高度一致且易于维护。
全局状态管理:轻量级替代 Vuex
对于一些共享状态(如当前主题、最近打开的文件列表),没必要上 Vuex/Pinia,直接用 Composition API 就够了:
// stores/useAppStore.ts
import { reactive } from 'vue'
export const useAppStore = () => {
const state = reactive({
theme: 'light',
recentFiles: [] as string[]
})
const addRecentFile = (path: string) => {
if (!state.recentFiles.includes(path)) {
state.recentFiles.unshift(path)
if (state.recentFiles.length > 10) {
state.recentFiles.pop()
}
}
}
const toggleTheme = () => {
state.theme = state.theme === 'light' ? 'dark' : 'light'
}
return {
state,
addRecentFile,
toggleTheme
}
}
关键是要加上 readonly() 包装,防止外部直接修改:
return {
state: readonly(state),
// mutation methods...
}
这样别人只能通过提供的方法来变更状态,保证了数据一致性。
打包发布:让用户第一印象就满分
终于到了发布阶段!但你以为 npm run build 就完事了吗?Too young too simple 😏
electron-builder:不只是生成 exe 文件
很多人用 electron-packager ,但我强烈推荐 electron-builder ,因为它内置了太多实用功能:
- 自动生成 NSIS/MSI 安装包
- 支持代码签名
- 差分更新(Delta Updates)
- 多平台一键构建
来看一个生产级配置:
appId: com.mycompany.awesomeapp
productName: Awesome Desktop App
copyright: Copyright © 2025 MyCompany Inc.
directories:
output: dist
buildResources: build-resources
win:
target:
- target: nsis
arch:
- x64
- ia32
publisherName: MyCompany Inc.
certificateFile: ./certificates/code-sign.pfx
certificatePassword: ${CERT_PASSWORD}
nsis:
oneClick: false
allowElevation: true
allowToChangeInstallationDirectory: true
createDesktopShortcut: always
有几个关键点要注意:
- 图标必须用 .ico 格式 ,包含多个尺寸(16×16 到 256×256)
- publisherName 必须和证书一致 ,否则 SmartScreen 会报警
- 不要开启 oneClick 安装 ,让用户有机会选择安装路径更友好
构建命令也很简单:
npx electron-builder --win --x64 --config builder.config.yaml
数字签名:绕不过去的信任门槛
没有签名的应用,在 Windows 上基本活不过三秒。SmartScreen 直接给你标红:“未知发布者,此应用可能危害你的设备”。
解决办法只有一个:花钱买正规 CA 签发的代码签名证书(DigiCert、Sectigo 等)。价格大概每年几百到上千美元不等。
拿到 .pfx 文件后,记得加进 .gitignore ,密码通过环境变量注入:
CERT_PASSWORD=your-secret-password npx electron-builder ...
构建完成后一定要验证签名是否有效:
signtool verify /pa /all /v dist\Awesome Desktop App Setup 1.0.0.exe
预期输出要有:
Verified: Signed and verified.
差分更新:用户体验的分水岭
全量更新每次都要下载几百 MB,谁受得了?而启用差分更新后,95% 以上的更新只需下载几 MB 补丁包。
原理很简单: electron-builder 会为每个版本生成 blockmap 文件,记录文件块的哈希值。下次更新时只对比差异部分。
win:
differentialPackage: true
publish:
provider: github
owner: myorg
repo: awesome-app
配合 electron-updater 客户端库,就能实现静默热更新:
import { autoUpdater } from 'electron-updater'
autoUpdater.checkForUpdatesAndNotify()
用户几乎感觉不到更新过程,体验丝滑到飞起~
sequenceDiagram
participant Client as 客户端
participant Server as GitHub Releases
Client->>Server: 获取最新版本信息
alt 版本不同
Client->>Server: 下载 .blockmap
Client->>Client: 对比本地文件块
Client->>Server: 请求差异块
Server-->>Client: 返回增量补丁
Client->>Client: 应用补丁并重启
else 版本相同
Client->>Client: 跳过更新
end
CI/CD 流水线:自动化才是终极生产力
手动打包不仅容易出错,还浪费时间。我们需要一套全自动的构建发布系统。
GitHub Actions 实战配置
name: Build and Release
on:
push:
tags:
- 'v*.*.*'
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build renderer (Vue)
run: npm run build:renderer
- name: Build installer
env:
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx electron-builder --win --x64 --config builder.config.yaml
- name: Create Release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
- name: Upload Assets
uses: svenstaro/upload-release-action@v2
with:
file: dist/*.exe
repo_token: ${{ secrets.GITHUB_TOKEN }}
这套流程做到了:
- 推送
v1.2.0标签自动触发 - 自动安装依赖、构建前端、打包安装包
- 自动创建 GitHub Release 并上传产物
- 团队成员收到 Slack 通知
整个过程无需人工干预,真正实现了“提交即发布”。
项目结构:高扩展性的目录设计
最后聊聊项目结构。一个好的目录组织能让新人快速上手,也能支撑长期迭代。
project-root/
├── main/ # 主进程代码
│ ├── index.js # 入口
│ ├── ipcHandlers/ # IPC处理器
│ └── services/ # 系统服务
├── renderer/ # 渲染进程(Vue)
│ ├── src/
│ │ ├── components/
│ │ ├── composables/
│ │ └── views/
├── shared/ # 跨进程共享
│ ├── constants/
│ └── types/
├── preload/ # 预加载脚本
├── config/ # 多环境配置
└── build/ # 构建配置
重点是把主进程、渲染进程、共享模块明确分开。特别是 shared/constants/ipcChannels.js 这种文件,统一管理所有通信频道名称,避免硬编码。
还有个小技巧:利用 __dirname 动态解析路径:
const { join } = require('path')
const configPath = join(__dirname, '..', 'config', `${env}.json`)
这样不管在哪台机器上跑,路径都不会出错。
总结:从开发者到架构师的跃迁
回头看看,我们聊的已经远远超出“怎么用 Electron + Vue”这个层面了。真正的挑战从来都不是技术本身,而是如何构建一个 安全、稳定、可维护、易扩展 的系统。
- 用 Composition API 解决逻辑碎片化
- 用 contextBridge + preload 实现安全通信
- 用 electron-builder + CI/CD 实现一键发布
- 用 模块化设计 提升代码复用率
当你能把这些点串联起来,形成自己的工程方法论时,你就不再是单纯的“开发者”,而是真正意义上的“架构师”了。
而这,正是打造高质量桌面产品的起点 🎯。
简介:本项目是一个使用Electron框架和Vue.js 3构建的跨平台Windows桌面应用程序,推测为一款功能完整的笔记应用(iNote)。项目融合现代Web技术与桌面系统能力,通过Electron整合Chromium与Node.js,实现前端界面与原生系统交互的无缝结合。采用Vue 3的响应式机制、组件化架构及Composition API,提升代码可维护性与开发效率。项目结构包含主进程控制、渲染进程UI展示、静态资源管理及构建配置,使用JavaScript(ECMAScript)作为核心开发语言,支持文件系统操作、窗口管理和本地部署。适用于希望掌握桌面端全栈开发的技术人员,是前端技术向桌面领域延伸的典型实践。

1286

被折叠的 条评论
为什么被折叠?



