这是一个关于时间、代码和 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,不是 require" |
|
main: "electron/main.js" |
Electron 入口 |
这是整个桌面应用的"大脑"所在 |
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.js
Vite 是这个项目的"魔法工坊"。配置文件决定了代码如何被编译、如何被 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.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
PostCSS 是"CSS 的 Babel"。它让 Tailwind 的指令(@tailwind base;)被编译成真实 CSS,同时 autoprefixer 自动添加 -webkit-、-moz- 等浏览器前缀。
src/style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
}
三行指令,引入 Tailwind 的全部力量:
- base:浏览器重置样式(类似 normalize.css)
- components:可复用组件样式(本项目未自定义)
- utilities:所有原子类(flex、text-7xl、bg-[#1a1a2e] 等)
第四:Vue 的入口–src/main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
四行代码,完成三件事:
1. 创建 Vue 应用实例
2. 导入全局样式(Tailwind 在此生效)
3. 将 App 组件挂载到 index.html 中 id="app" 的 DOM 节点
第五:HTML 的骨架–index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄钟</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
这是整个应用的"空画布"。<div id="app"> 是 Vue 的挂载点,<script type="module"> 让浏览器以 ES Module 方式加载入口。
Electron 视角:Electron 的 BrowserWindow 最终 loadURL 或 loadFile 加载的就是这个 HTML。
第六:计时器的心脏–Timer.vue
这是整个项目最核心的组件。它不关心 UI 长什么样,只关心时间怎么走。
模板层
<template>
<div class="text-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') // 时间到!通知父组件
}
}
对外 API(defineExpose):
defineExpose({
start, // 开始计时
pause, // 暂停计时
reset // 重置计时
})
设计亮点:defineExpose 让父组件可以通过 ref 直接调用这些方法。这是一种命令式 API 设计——父组件说"开始",Timer 就"开始",而不是通过 props 驱动。
防重复启动:
const start = () => {
if (isRunning.value) return // 已经在跑了?忽略
isRunning.value = true
timerId = setInterval(tick, 1000)
}
响应式重置(watch):
watch(() => props.initSeconds, (val) => {
clearInterval(timerId)
isRunning.value = false
totalSeconds.value = val
})
当父组件切换"工作/休息"模式时,initSeconds 变化,Timer 自动停止并重置——不需要父组件手动调用 reset()。
第七:指挥家–App.vue
App.vue 是整个应用的指挥官,它编排 Timer 组件,管理应用状态。
模板结构
<template>
<div class="bg-[#1a1a2e] text-white h-screen flex items-center justify-center font-sans">
<div class="text-center">
<h1 class="text-2xl mb-5">{{ modeText }}</h1>
<Timer :init-seconds="currentSeconds" @finish="onTimerFinish" ref="timerRef" />
<div class="grid grid-cols-2 gap-3 mt-8">
<button @click="startTimer" class="… bg-[#2ecc71]">开始</button>
<button @click="pauseTimer" class="… bg-[#f39c12]">暂停</button>
<button @click="resetTimer" class="… bg-[#e74c3c]">重置</button>
<button @click="switchMode" 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.js
Electron 的主进程是这个应用的"操作系统接口"。它创建窗口、管理应用生命周期。
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 主进程使用 ESM(type: "module"),所以不能用 __dirname 和 __filename。通过 fileURLToPath 手动计算。
窗口创建
function createWindow() {
const win = new BrowserWindow({
width: 400,
height: 500,
resizable: false, // 固定大小,不可拖拽缩放
webPreferences: {
nodeIntegration: false, // 安全:渲染进程不能直接用 Node.js
contextIsolation: true // 安全:上下文隔离
}
})
安全配置解读:
|
nodeIntegration |
false |
防止渲染进程执行 require('child_process') 等危险操作 |
|
contextIsolation |
true |
防止网页脚本访问 Electron 内部 API |
|
resizable |
false |
番茄钟是极简工具,固定尺寸更专注 |
加载策略
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.json |
|
npm run dev |
启动 Vite 开发服务器,浏览器可访问 localhost:5173 |
|
npm run electron:dev |
Vite + Electron 同时启动,桌面窗口出现,支持热更新 |
|
npm run electron:build |
Vite 打包 Vue → dist/,Electron Builder 打包为 exe → dist_electron/ |
网硕互联帮助中心






评论前必须登录!
注册