云计算百科
云计算领域专业知识百科平台

从零开始的故事:一个番茄钟是如何诞生的

        这是一个关于时间、代码和 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/

赞(0)
未经允许不得转载:网硕互联帮助中心 » 从零开始的故事:一个番茄钟是如何诞生的
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!