
这是一个关于时间、代码和 Electron 的故事。我们将跟随一个番茄钟应用Tomato Clock从一行命令到桌面窗口的完整旅程逐层剖析它的源码架构。解读源码链接序一切从一个命令开始深夜你敲下npm run electron:dev屏幕亮起一个 400×500 的桌面窗口缓缓浮现——深蓝色背景白色数字25:00四个彩色按钮静静等待点击。这背后发生了什么让我们把时间倒回从头讲起。第一项目的基因图谱--package.json每一个 Node.js 项目的故事都从package.json开始。它就像这个应用的出生证明和能力清单。{ name: tomato-vite-electron, version: 1.0.0, type: module, main: electron/main.js }关键解读字段含义故事中的角色type: module使用 ES Module 语法告诉 Node.js我用import/export不是requiremain: electron/main.jsElectron 入口这是整个桌面应用的大脑所在scripts 脚本——项目的咒语{ dev: vite, build: vite build, preview: vite preview, electron:dev: vite --mode electron, electron:build: vite build electron-builder }当你输入npm run electron:dev时Vite 启动开发服务器同时vite-plugin-electron插件会启动 Electron 主进程两者通过环境变量VITE_DEV_SERVER_URL建立连接。生产环境只需要 Vue——因为最终打包后Electron 加载的是静态 HTML 文件不需要 Vite 或 Electron 本身。第二Vite 的魔法--vite.config.jsVite 是这个项目的魔法工坊。配置文件决定了代码如何被编译、如何被 Electron 加载。import { defineConfig } from vite import vue from vitejs/plugin-vue import electron from vite-plugin-electron import renderer from vite-plugin-electron-renderer import { fileURLToPath } from url export default defineConfig({ plugins: [ vue(), // ① 让 Vite 理解 .vue 单文件组件 electron([ // ② Electron 主进程插件 { entry: electron/main.js, onstart(options) { options.startup() // 主进程变化时自动重启 Electron }, vite: { build: { sourcemap: true, outDir: dist/electron, }, }, }, ]), renderer(), // ③ 让渲染进程也能用 Node.js API虽然本项目未使用 ], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } } })三把钥匙的解读vue() → 编译 .vue 文件支持 SFC 热更新 electron() → 监听主进程代码变化时重启 Electron renderer() → 为渲染进程注入 Node.js 能力路径别名 让你可以写import Timer from /components/Timer.vue而不是../../../components/Timer.vue——在大型项目中这是救命的设计。第三样式的炼金术--Tailwind 三剑客tailwind.config.js/** type {import(tailwindcss).Config} */ export default { content: [ ./index.html, ./src/**/*.{vue,js,ts,jsx,tsx}, ], theme: { extend: {}, }, plugins: [], }content 数组告诉 Tailwind去这些文件里扫描类名没用到的就删掉。这就是 Tailwind 能做到生产环境 CSS 只有几KB的秘密——Tree Shaking。postcss.config.jsexport default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }PostCSS 是CSS 的 Babel。它让 Tailwind 的指令tailwind base;被编译成真实 CSS同时autoprefixer自动添加-webkit-、-moz-等浏览器前缀。src/style.csstailwind base; tailwind components; tailwind utilities; body { margin: 0; }三行指令引入 Tailwind 的全部力量base浏览器重置样式类似 normalize.csscomponents可复用组件样式本项目未自定义utilities所有原子类flex、text-7xl、bg-[#1a1a2e]等第四Vue 的入口--src/main.jsimport { createApp } from vue import ./style.css import App from ./App.vue createApp(App).mount(#app)四行代码完成三件事1. 创建 Vue 应用实例 2. 导入全局样式Tailwind 在此生效 3. 将 App 组件挂载到 index.html 中 idapp 的 DOM 节点第五HTML 的骨架--index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title番茄钟/title /head body div idapp/div script typemodule src/src/main.js/script /body /html这是整个应用的空画布。div idapp是 Vue 的挂载点script typemodule让浏览器以 ES Module 方式加载入口。Electron 视角Electron 的BrowserWindow最终loadURL或loadFile加载的就是这个 HTML。第六计时器的心脏--Timer.vue这是整个项目最核心的组件。它不关心 UI 长什么样只关心时间怎么走。模板层template div classtext-7xl my-8 {{ timeStr }} /div /template只做一件事显示格式化后的时间字符串。逻辑层--状态机const props defineProps([initSeconds]) // 从父组件接收初始秒数 const emit defineEmits([finish]) // 向父组件发射完成事件 const totalSeconds ref(props.initSeconds) // 当前剩余秒数 let timerId null // setInterval 的句柄 const isRunning ref(false) // 运行状态锁核心算法const timeStr computed(() { const s totalSeconds.value const m Math.floor(s / 60) const sec s % 60 return String(m).padStart(2, 0) : String(sec).padStart(2, 0) })这是一个计算属性依赖totalSeconds。每当totalSeconds变化timeStr自动重新计算。输出格式MM:SS。心跳函数function tick() { totalSeconds.value-- if (totalSeconds.value 0) { clearInterval(timerId) isRunning.value false emit(finish) // 时间到通知父组件 } }对外 APIdefineExposedefineExpose({ start, // 开始计时 pause, // 暂停计时 reset // 重置计时 })设计亮点defineExpose让父组件可以通过ref直接调用这些方法。这是一种命令式 API 设计——父组件说开始Timer 就开始而不是通过 props 驱动。防重复启动const start () { if (isRunning.value) return // 已经在跑了忽略 isRunning.value true timerId setInterval(tick, 1000) }响应式重置watchwatch(() props.initSeconds, (val) { clearInterval(timerId) isRunning.value false totalSeconds.value val })当父组件切换工作/休息模式时initSeconds变化Timer 自动停止并重置——不需要父组件手动调用 reset()。第七指挥家--App.vueApp.vue 是整个应用的指挥官它编排 Timer 组件管理应用状态。模板结构template div classbg-[#1a1a2e] text-white h-screen flex items-center justify-center font-sans div classtext-center h1 classtext-2xl mb-5{{ modeText }}/h1 Timer :init-secondscurrentSeconds finishonTimerFinish reftimerRef / div classgrid grid-cols-2 gap-3 mt-8 button clickstartTimer class... bg-[#2ecc71]开始/button button clickpauseTimer class... bg-[#f39c12]暂停/button button clickresetTimer class... bg-[#e74c3c]重置/button button clickswitchMode class... bg-[#3498db]切换休息/button /div /div /div /template布局解读┌─────────────────────────────────┐ │ bg-[#1a1a2e] │ ← 深蓝背景全屏 │ │ │ 专注时间 / 休息时间 │ ← h1 标题 │ │ │ 25:00 │ ← Timer 组件 │ │ │ ┌──────┐ ┌──────┐ │ │ │ 开始 │ │ 暂停 │ │ ← 2×2 按钮网格 │ └──────┘ └──────┘ │ │ ┌──────┐ ┌──────┐ │ │ │ 重置 │ │切换 │ │ │ └──────┘ └──────┘ │ └─────────────────────────────────┘状态管理const isWorkMode ref(true) // 当前模式 const currentSeconds ref(25 * 60) // 当前倒计时秒数 const timerRef ref(null) // Timer 组件的引用模式切换逻辑function switchMode() { isWorkMode.value !isWorkMode.value currentSeconds.value isWorkMode.value ? 25 * 60 : 5 * 60 }切换模式时isWorkMode翻转currentSeconds变为 25 分钟或 5 分钟由于watch(() props.initSeconds)的存在Timer 自动重置完成回调function onTimerFinish() { alert(时间到) }当前使用alert()弹窗。在生产级应用中可以替换为系统通知Notification API或 Electron 的Tray提醒。第八桌面壳--electron/main.jsElectron 的主进程是这个应用的操作系统接口。它创建窗口、管理应用生命周期。import { app, BrowserWindow } from electron import path from path import { fileURLToPath } from url const __filename fileURLToPath(import.meta.url) const __dirname path.dirname(__filename)ESM 兼容处理Electron 主进程使用 ESMtype: module所以不能用__dirname和__filename。通过fileURLToPath手动计算。窗口创建function createWindow() { const win new BrowserWindow({ width: 400, height: 500, resizable: false, // 固定大小不可拖拽缩放 webPreferences: { nodeIntegration: false, // 安全渲染进程不能直接用 Node.js contextIsolation: true // 安全上下文隔离 } })安全配置解读配置项值为什么nodeIntegrationfalse防止渲染进程执行require(child_process)等危险操作contextIsolationtrue防止网页脚本访问 Electron 内部 APIresizablefalse番茄钟是极简工具固定尺寸更专注加载策略if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(process.env.VITE_DEV_SERVER_URL) // 开发模式加载 Vite 服务器 } else { win.loadFile(path.join(__dirname, ../dist/index.html)) // 生产模式加载打包后的 HTML }两种模式的切换开发模式Electron → http://localhost:5173 → Vite HMR 热更新 生产模式Electron → dist/index.html → 静态文件应用生命周期app.whenReady().then(createWindow) app.on(window-all-closed, () { if (process.platform ! darwin) app.quit() })macOS 上关闭所有窗口后应用仍然驻留macOS 惯例其他平台直接退出。尾声一个番茄钟的启示这个项目虽然只有 ~300 行核心代码但它展示了一个现代桌面应用开发的完整范式Vue 3 → 声明式 UI 响应式状态 Vite → 极速构建 HMR Electron → Web 技术 → 桌面应用 Tailwind → 原子化样式零心智负担番茄钟的精髓不在于代码而在于专注。愿这个应用能帮你把时间切成一块块番茄每一块都饱满而充实。附录命令背后的故事命令发生了什么npm install安装所有依赖到node_modules生成package-lock.jsonnpm run dev启动 Vite 开发服务器浏览器可访问localhost:5173npm run electron:devVite Electron 同时启动桌面窗口出现支持热更新npm run electron:buildVite 打包 Vue →dist/Electron Builder 打包为 exe →dist_electron/