造你自己的桌面应用
Node
本来想用 jdx/mise 丝滑安装和管理 Node.js 版本,可似乎没搭配 npm,于是参考了 Install Node.js on Windows 推荐的 nvm-windows ,在 Windows 上安装长期支持的 Node 版本。
> nvm install 20
Downloading node.js version 20.18.1 (64-bit)...
Extracting node and npm...
Complete
npm v10.8.2 installed successfully.
Installation complete. If you want to use this version, type
nvm use 20.18.1
> nvm use 20.18.1
Now using node v20.18.1 (64-bit)
> npm --version
10.8.2
遇到问题直接 nvm debug
很有帮助。
Solution 1: Electron + Vue + daisyUI
在 Web 开发中,我们通常使用 HTML 和 CSS 以及 JavaScript 来开发网站前端,这些技术也可以用来开发桌面应用程序。例如 Electron 是最流行的跨平台桌面应用框架之一,Visual Studio Code、Postman、Discord 等知名桌面应用都在展示柜的前列。
方便了解核心,除了阅读官方文档,也看看大语言模型怎么说:
Electron 内嵌 Chromium(浏览器内核)与 Node.js(服务器端 JavaScript 运行时),提供两种进程:
- 主进程(Main Process):管理窗口、系统菜单等桌面级功能。
- 负责应用的生命周期管理和系统交互(如文件操作、窗口创建等)。
- 执行的文件定义在 Electron 的
package.json
的main
字段中(通常是main.js
或main.ts
)。 - 可以使用 Node.js 的全部功能,比如文件读写、启动子进程等。
- 渲染进程(Renderer Process):运行 Web 界面,类似浏览器页面。
- 每个窗口都有自己的渲染进程,负责展示和管理用户界面。
- 类似于网页中的浏览器环境,通过 Chromium 渲染 HTML/CSS/JS 内容。
- 可以通过
preload
脚本安全地向渲染进程暴露一些 Node.js 功能。
两者之间可以通过 ipcMain
和 ipcRenderer
通信。
作为后端开发小子,面对琳琅满目的 Web 前端技术栈,结合本土市场情况,决定选择认知负荷较小的 Vue.js 来控制倒计时组件状态,并且安装最流行、免费、开源的 daisyUI 作为 Tailwind CSS 插件来美化界面。
生成样板代码
通过关键词 starter/scaffold/boilerplate/template 搜了一圈,发现 electron-vite-vue 看起来不错,Vite 常用于 Vue 项目的前端构建工具(frontend build tool),于是通过项目模板 electron-vite
创建项目。
npm create electron-vite countdown-desktop
生成的目录结构如下所示:
│ .gitignore
│ electron-builder.json5
│ index.html
│ package.json
│ README.md
│ tsconfig.json
│ tsconfig.node.json
│ vite.config.ts
│
├───.vscode
│ extensions.json
│
├───electron
│ electron-env.d.ts
│ main.ts
│ preload.ts
│
├───public
│ electron-vite.animate.svg
│ electron-vite.svg
│ vite.svg
│
└───src
│ App.vue
│ main.ts
│ style.css
│ vite-env.d.ts
│
├───assets
│ vue.svg
│
└───components
HelloWorld.vue
其中 electron
目录下的 main.ts
是 Electron 主进程入口,preload.ts
是预加载脚本,src
目录下的 main.ts
是渲染进程入口,App.vue
是 Vue 根组件,HelloWorld.vue
是示例组件,index.html
是入口 HTML 文件;package.json
是项目配置文件,vite.config.ts
是 Vite 配置文件,tsconfig.json
是 TypeScript 配置文件。
npm install
npm run dev
上面的命令会安装依赖并运行示例,若要在当前平台构建桌面应用,则可直接运行 npm run build
;在 package.json
中可以看到脚本命令的定义。
{
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build && electron-builder",
"preview": "vite preview"
}
}
构建产生的软件包可以在目录 release
下找到。
并非从零开始
站在巨人的肩膀上。
npm install vue@3 @chenfengyuan/vue-countdown@2
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
npm i -D daisyui@latest
感谢 vue-countdown 的作者隐藏了倒计时 Timing Events 的复杂性,也经过了 Vue 的入门,参考了 countdown 后,定制组件模板如下所示:
<template>
<!-- Reference to the countdown component -->
<vue-countdown v-if="isReady" ref="countdown" :auto-start="false" :time="time" @progress="onProgress" @end="onEnd">
<div class="flex justify-center items-center flex-col h-screen">
<!-- Countdown Display -->
<div class="grid grid-flow-col gap-5 text-center auto-cols-max" v-show="state.running || state.paused">
<!-- Div for Days, Hours, Minutes, Seconds -->
</div>
<!-- Input Fields -->
<div class="flex mt-5 gap-1" v-show="!(state.running || state.paused)">
<!-- Input Fields for Days, Hours, Minutes, Seconds -->
</div>
<!-- Control Buttons -->
<div class="flex mt-5 gap-5">
<button class="btn btn-wide" @click="onCancel" :disabled="state.ended">Cancel</button>
<button class="btn btn-wide" @click="onSwitch">{{ state.switch }}</button>
</div>
</div>
</vue-countdown>
</template>
同时在 script
中管理状态,包括初始化“槽位”的状态,UI 响应状态变化,以及监听事件的处理等。
import { computed, onMounted, reactive, ref, nextTick } from 'vue';
const countdown = ref();
const time = ref(1);
const isReady = ref(false);
const initSlot = () => {
return {
seconds: 0,
minutes: 0,
hours: 0,
days: 0,
}
}
const slot = reactive(initSlot());
const seconds = computed(() => {
return { '--value': slot.seconds, }
});
// minutes, hours, days
function onProgress(data: any) {
slot.seconds = data.seconds;
// minutes, hours, days
}
enum Switch {
Play = 'Play',
Pause = 'Pause',
}
const initSate = () => {
return {
running: false,
paused: false,
ended: true,
switch: Switch.Play
}
}
// countdown state
const state = reactive(initSate());
let last = 0;
const daysInput = ref();
// hoursInput, minutesInput, secondsInput
function getTimeValue() {
const days = daysInput.value.value;
// hours, minutes, seconds
return last === 0 ? (seconds * 1000 + minutes * 60 * 1000 + hours * 60 * 60 * 1000 + days * 24 * 60 * 60 * 1000) + 1000 : last;
}
async function onSwitch() {
if (state.running) {
countdown.value.abort();
last = countdown.value.totalMilliseconds;
state.running = false;
state.paused = true;
state.ended = false;
state.switch = Switch.Play;
} else {
state.running = true;
state.paused = false;
state.ended = false;
state.switch = Switch.Pause;
// await for DOM updates to complete
await nextTick();
time.value = getTimeValue();
countdown.value.restart();
}
}
const NOTIFICATION_TITLE = 'countdown-desktop';
const NOTIFICATION_BODY = 'Your countdown has ended!'
function onCancel() {
reset();
countdown.value.end();
}
function reset() {
Object.assign(state, initSate());
Object.assign(slot, initSlot());
last = 0;
}
function onEnd() {
if (!state.ended) {
reset();
}
// show notification
new window.Notification(NOTIFICATION_TITLE, { body: NOTIFICATION_BODY });
}
onMounted(() => {
isReady.value = true;
});
Debug 全靠 Chrome DevTools,只需要在 electron/main.ts
创建窗口时区分环境打开开发者工具。
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL)
// Open the DevTools
win.webContents.openDevTools()
} else {
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
}
只想起一个关于托盘和计时器的问题:过一段时间打开托盘后倒计时应用中途停止了。在 GitHub 上搜索到了相似的问题 electron/issues/7079 ,人们说禁用后台页面中定时器任务的节流(throttle)可以修复类似的缺陷。
// Disable task throttling of timer tasks from background pages
win.webContents.setBackgroundThrottling(false)
最终效果和完整代码在 countdown-desktop 可以找到
Solution 2: Tauri + Vue + daisyUI
Electron 非常全面,除了未经足够优化的应用体积大、资源占用高,在某些场景下可能会成为用户体验的“痛点”。在学习 Rust 时很难不注意到 Tauri ,目标是创建轻量、快速、安全的跨平台应用程序。
AI 认为它的架构分成三部分:
- Web 前端
- 使用你熟悉的前端技术栈,比如 React、Vue、Svelte 或 Angular。前端代码被编译成静态文件(HTML、CSS、JavaScript),并嵌入到 Tauri 应用中,或者通过外部加载。
- Tauri 核心
- 使用 Rust 实现,负责处理应用生命周期管理、系统级功能和 IPC(进程间通信)。提供对操作系统功能的访问,例如文件系统、剪贴板、窗口控制等。
- WebView
- Tauri 应用运行时使用的 WebView 是操作系统本身自带的(比如 macOS 上的 WKWebView 和 Windows 的 WebView2)。
搭建一个新项目
npm create tauri-app@latest
为了方便“移植”,提示前端风味(flavor)选择 TypeScript,遇到 UI 模板还得是 Vue。
继续站在巨人的肩膀上
安装完上文的依赖包和拷贝前端文件之后,使用 Rust 在 src-tauri/src/main.rs
重写窗口和托盘的逻辑。
fn main() {
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new().add_item(quit);
tauri::Builder::default()
.system_tray(tauri::SystemTray::new().with_menu(tray_menu))
.on_window_event(|event| match event.event() {
tauri::WindowEvent::CloseRequested { api, .. } => {
let window = event.window();
window.hide().unwrap();
api.prevent_close();
}
_ => {}
})
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"quit" => {
std::process::exit(0);
}
_ => {}
},
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
看起来相对简洁,但是首次运行 npm run dev
还得等待编译 Rust 代码的漫长过程,不过运行 npm run tauri build
后的应用体积相较于 Electron 方案小得惊人,后者达到 168 MB,而前者只有 6.47 MB。但作为新的挑战者,在开发者工具、跨平台一致性、Web API 支持等方面还不完备。
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
}
}
构建结果可以在目录 src-tauri/target/release
下找到,最终效果和完整代码已更新到 cddt。
总结一下
- 介绍了使用 Electron 和 Tauri 框架开发桌面应用的基本流程。
- 以倒计时桌面应用为例,展示了如何使用 Vue.js 和 daisyUI 创建界面,以及如何使用 TypeScript 和 Rust 控制应用逻辑。
- 分析了 Electron 和 Tauri 的优缺点,以及适用场景。