1. 项目概述一个能“养”在桌面的数字伙伴如果你和我一样每天大部分时间都对着电脑屏幕那么一个能活跃在桌面角落的“小宠物”或许能带来不少乐趣。emilie-desktop-pet这个项目就是一个用代码实现的桌面电子宠物。它不是那种需要你喂食、洗澡的复杂模拟游戏而是一个轻量级、高度可定制的桌面伴侣可以安静地待在你的屏幕一角偶尔动一动或者根据你的鼠标、键盘操作做出一些有趣的反应。这个项目的核心价值在于它用相对简单的技术实现了一个充满趣味和互动性的桌面应用。对于开发者来说它是一个绝佳的练手项目涵盖了桌面应用开发、图形渲染、事件交互、跨平台兼容性等多个基础但重要的知识点。对于普通用户它则是一个可以轻松“领养”、自由装扮的个性化桌面装饰。项目名称中的emilie可能是一个预设的角色名但它的灵魂在于其开源和可塑性——你可以把它变成任何你想象中的样子。2. 核心思路与技术选型解析2.1 为什么选择这些技术栈要创建一个始终悬浮在所有窗口之上的桌面宠物技术选型是关键。emilie-desktop-pet项目通常需要解决几个核心问题窗口无边框且置顶、图形动画渲染、系统事件监听以及跨平台运行。从常见的实现路径来看一个高效的选择是使用Electron或Tauri这类框架。它们允许你使用 Web 技术HTML, CSS, JavaScript/TypeScript来构建桌面应用并能通过底层绑定实现原生窗口控制。Electron 成熟、生态丰富但打包体积较大Tauri 则更轻量使用 Rust 构建后端安全性更高最终产物可以小到几兆字节这对于一个桌面宠物应用来说是巨大的优势。因此我推测并推荐采用Tauri TypeScript 某个前端框架如 Vue 3 或 Svelte的组合。Tauri 能完美创建无边框、透明背景、可点击穿透且始终置顶的窗口这正是桌面宠物所需的“画布”。图形渲染方面为了实现流畅的精灵动画我们不会用传统的 DOM 操作去移动一个img标签。更专业的做法是使用Canvas 2D或WebGL。对于2D精灵动画Canvas 2D API 完全够用且更简单。我们将宠物的每一帧动画制作成精灵图Sprite Sheet然后通过 Canvas 在每一帧绘制特定的区域就能实现行走、跳跃、 idle 待机等动画效果。交互逻辑是宠物的“大脑”。我们需要监听鼠标移动、点击、键盘按键等事件。当鼠标靠近时宠物可以看向鼠标或跑开双击宠物可以触发特殊动作甚至可以让它根据系统时间比如整点做一些提醒动作。这部分逻辑完全由 JavaScript/TypeScript 驱动是项目趣味性的核心。2.2 架构设计轻量、模块化与可扩展一个良好的架构能让开发和后续定制变得轻松。整个项目可以清晰地分为几个模块渲染核心模块负责创建和管理 Canvas 画布加载精灵图资源执行动画帧循环requestAnimationFrame根据当前状态绘制正确的宠物图像。状态与行为模块定义宠物的各种状态如“空闲”、“行走”、“高兴”、“睡觉”以及状态之间的转换条件。这是一个简单的状态机。例如当“空闲”状态持续时间超过30秒有20%的概率切换到“伸懒腰”状态。交互监听模块封装对鼠标、键盘事件的监听并将这些事件转化为对宠物状态模块的调用。例如监听鼠标在窗口内的移动计算宠物与鼠标的距离和角度让宠物实现“注视鼠标”或“逃离鼠标”的行为。配置与数据模块管理用户配置比如宠物类型、动画速度、互动灵敏度、是否开启声音等。这些配置可以保存到本地文件下次启动时自动加载。窗口控制模块由 Tauri 后端处理实现窗口的创建、置顶、无边框、透明、拖动等底层系统交互。这是宠物能“粘”在桌面上的基础。这种模块化设计意味着如果你想换一个宠物形象基本上只需要替换精灵图资源文件并微调状态机的参数即可。如果你想增加一个“根据天气API改变宠物着装”的复杂功能也只需在行为模块中添加新的状态和触发逻辑不会影响其他部分。3. 从零开始开发环境搭建与项目初始化3.1 环境准备与工具链首先确保你的开发环境就绪。你需要安装Node.js(版本 18 或以上)这是运行 JavaScript/TypeScript 和包管理器的基础。Rust工具链因为 Tauri 的后端是用 Rust 写的。安装最简单的方法是使用rustup它能管理多个 Rust 版本。一个代码编辑器VS Code 是绝佳选择并且建议安装Tauri和rust-analyzer扩展插件。打开终端让我们一步步创建项目。这里我选择使用 Tauri 官方推荐的与Vite和Vanilla(原生 TypeScript) 的模板以保持最精简的结构更清晰地理解原理。当然你也可以选择 Vue 或 React 模板。# 使用 npm 创建 Tauri 应用 npm create tauri-applatest在交互式命令行中依次选择Project name:emilie-desktop-petTemplate type:Vanilla(选择这个是为了避免框架的学习成本专注于核心逻辑)UI recipe:TypeScriptPackage manager:npm(或你习惯的 yarn/pnpm)创建完成后进入项目目录并安装依赖cd emilie-desktop-pet npm install3.2 初始窗口配置打造宠物“栖息地”项目创建后首要任务是配置主窗口让它变成适合宠物生活的样子。关键的配置文件是src-tauri/tauri.conf.json。我们需要修改windows配置项{ windows: [ { title: Emilie, width: 200, height: 200, resizable: false, // 不可调整大小宠物窗口大小固定 fullscreen: false, decorations: false, // 无边框这是关键 always_on_top: true, // 始终置顶宠物不会被其他窗口盖住 transparent: true, // 背景透明只显示宠物本身 skip_taskbar: true // 不在任务栏显示让它更像一个“后台”宠物 } ] }注意transparent: true要求前端页面的背景也是透明的。我们稍后会在 CSS 中设置。另一个重要配置是allowlist。为了能让我们的宠物窗口被拖动否则一个无边框窗口就卡死在屏幕上了我们需要启用window的start_dragging功能。{ tauri: { allowlist: { window: { all: false, start_dragging: true // 允许前端调用窗口拖动API } } } }完成配置后可以先运行npm run tauri dev看看效果。你应该会看到一个无边框、透明、置顶的小窗口。这就是我们宠物的“家”。4. 核心实现绘制一个会动的精灵4.1 准备精灵图资源宠物的灵魂在于它的形象和动作。你需要准备一张或多张精灵图。所谓精灵图就是把一个角色的所有动画帧按顺序排列在一张大图上。例如一个 4x4 的网格可以存放 16 帧动画用来表现一个四方向的行走动画每个方向4帧。你可以用 Aseprite、Photoshop 甚至在线工具制作。这里为了演示假设我们有一个简单的宠物只有 idle待机动画共 4 帧水平排列在一张 400x100 的图片上每帧 100x100。将图片命名为pet_sprite.png放在项目的public或assets目录下。4.2 创建 Canvas 渲染引擎现在我们来编写前端的核心渲染代码。修改src/main.ts文件// src/main.ts import { invoke } from tauri-apps/api/tauri; // 获取 Canvas 元素和上下文 const canvas document.getElementById(pet-canvas) as HTMLCanvasElement; const ctx canvas.getContext(2d)!; canvas.width 100; // 画布大小与单帧精灵大小一致 canvas.height 100; // 精灵图相关变量 const spriteSheet new Image(); spriteSheet.src ./pet_sprite.png; // 图片路径 let currentFrame 0; const totalFrames 4; const frameWidth 100; const frameHeight 100; let lastUpdateTime 0; const frameInterval 200; // 每200毫秒切换一帧控制动画速度 // 动画循环 function animate(timestamp: number) { requestAnimationFrame(animate); // 控制帧率 if (timestamp - lastUpdateTime frameInterval) { lastUpdateTime timestamp; currentFrame (currentFrame 1) % totalFrames; // 循环播放 // 清空画布因为是透明背景实际上我们也可以选择不清空取决于动画需求 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制当前帧 ctx.drawImage( spriteSheet, currentFrame * frameWidth, // 源图像 x 坐标 0, // 源图像 y 坐标 frameWidth, frameHeight, 0, // 画布 x 坐标 0, // 画布 y 坐标 frameWidth, frameHeight ); } } // 等待图片加载完成后启动动画循环 spriteSheet.onload () { requestAnimationFrame(animate); console.log(精灵图加载完毕动画开始); }; // 窗口拖动功能监听 Canvas 的鼠标按下事件调用 Tauri 后端拖动窗口 canvas.addEventListener(mousedown, (event) { // 只在鼠标左键按下时触发拖动 if (event.button 0) { invoke(start_window_drag).catch(console.error); } });同时我们需要修改src/index.html确保 Canvas 居中且背景透明!DOCTYPE html html langen head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleEmilie Desktop Pet/title style body { margin: 0; padding: 0; overflow: hidden; /* 隐藏滚动条 */ background: transparent !important; /* 关键完全透明背景 */ display: flex; justify-content: center; align-items: center; height: 100vh; } #pet-canvas { /* 可以添加一点阴影让宠物更有立体感但核心是背景透明 */ filter: drop-shadow(2px 2px 3px rgba(0, 0, 0, 0.2)); } /style /head body canvas idpet-canvas/canvas script typemodule src/src/main.ts/script /body /html4.3 实现窗口拖动的后端逻辑前端调用了invoke(start_window_drag)我们需要在 Tauri 后端实现这个命令。打开src-tauri/src/main.rs文件// src-tauri/src/main.rs #[tauri::command] fn start_window_drag(window: tauri::Window) { // 这行代码会启动原生的窗口拖动 let _ window.start_dragging(); } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![start_window_drag]) // 注册命令 .run(tauri::generate_context!()) .expect(error while running tauri application); }现在运行npm run tauri dev你应该能看到一个在屏幕中央、播放着 idle 动画的宠物。你可以用鼠标按住它并拖动到屏幕的任何位置。一个桌面宠物的雏形已经诞生了5. 注入灵魂实现交互与智能行为一个只会循环播放 idle 动画的宠物是枯燥的。接下来我们为它添加一些简单的交互逻辑让它“活”起来。5.1 状态机管理宠物的情绪和行为我们定义一个简单的状态机。宠物可以处于以下几种状态idle空闲、moving移动、sleeping睡觉、reacting对事件做出反应。我们修改src/main.ts引入状态管理// src/main.ts (部分新增) type PetState idle | moving | sleeping | reacting; class DesktopPet { private state: PetState idle; private stateTimer: number | null null; private x: number 0; // 宠物在画布内的位置未来可用于移动 private y: number 0; constructor() { this.enterState(idle); this.setupInteractions(); } private enterState(newState: PetState) { if (this.stateTimer) { clearTimeout(this.stateTimer); } this.state newState; console.log(宠物进入状态: ${newState}); // 根据状态执行不同逻辑 switch (newState) { case idle: // idle 状态持续一段时间后可能切换到其他状态 this.stateTimer window.setTimeout(() { const rand Math.random(); if (rand 0.3) { this.enterState(sleeping); } else if (rand 0.6) { // 触发一个反应比如打哈欠 this.triggerReaction(yawn); } // 否则继续 idle }, 5000 Math.random() * 10000); // 5-15秒后评估 break; case sleeping: // 睡觉状态可以播放睡觉动画 // 睡觉一段时间后醒来 this.stateTimer window.setTimeout(() { this.enterState(idle); }, 10000 Math.random() * 20000); break; case reacting: // 反应状态通常是短暂的播放完特定动画后回到 idle this.stateTimer window.setTimeout(() { this.enterState(idle); }, 2000); // 反应持续2秒 break; } } private triggerReaction(reactionType: string) { this.enterState(reacting); console.log(宠物反应: ${reactionType}); // 这里可以关联到不同的动画帧或效果 // 例如临时切换精灵图到“打哈欠”的序列 } private setupInteractions() { const canvas document.getElementById(pet-canvas)!; // 鼠标悬停互动鼠标靠近时宠物看向鼠标或做出反应 canvas.addEventListener(mousemove, (event) { const rect canvas.getBoundingClientRect(); const mouseX event.clientX - rect.left; const mouseY event.clientY - rect.top; // 计算鼠标与宠物中心点的距离 const distance Math.sqrt(Math.pow(mouseX - 50, 2) Math.pow(mouseY - 50, 2)); if (distance 30 this.state idle) { // 鼠标很近触发一个“好奇”的反应 this.triggerReaction(curious); } }); // 双击互动双击宠物触发一个高兴的动画 let lastClickTime 0; canvas.addEventListener(click, (event) { const currentTime new Date().getTime(); if (currentTime - lastClickTime 300) { // 300毫秒内算双击 this.triggerReaction(happy); event.preventDefault(); // 防止可能的文本选择 } lastClickTime currentTime; }); } } // 在图片加载后初始化宠物实例 spriteSheet.onload () { requestAnimationFrame(animate); new DesktopPet(); // 初始化宠物行为逻辑 console.log(精灵图加载完毕动画开始); };5.2 让宠物“看”向鼠标实现注视效果一个高级的互动是让宠物的眼睛或身体朝向鼠标。这需要一点三角学知识。我们假设宠物精灵图是正面朝向的。我们可以不切换精灵图而是通过计算鼠标相对于宠物中心的角度在绘制时给出一个视觉暗示比如在宠物旁边画一个代表视线的小点。更复杂的实现需要多方向精灵图。这里我们先实现一个简单的视线指示器// 在 animate 函数内部绘制宠物之后 // 计算鼠标相对角度需要获取全局鼠标位置这里需要额外记录 let globalMouseX 0; let globalMouseY 0; window.addEventListener(mousemove, (e) { globalMouseX e.clientX; globalMouseY e.clientY; }); function animate(timestamp: number) { // ... 原有的清空和绘制精灵图代码 ... // 绘制视线指示器一个小圆点 const rect canvas.getBoundingClientRect(); const petCenterX rect.left canvas.width / 2; const petCenterY rect.top canvas.height / 2; const angle Math.atan2(globalMouseY - petCenterY, globalMouseX - petCenterX); const lookDistance 15; const lookX 50 Math.cos(angle) * lookDistance; // 50是画布中心 const lookY 50 Math.sin(angle) * lookDistance; ctx.fillStyle rgba(255, 0, 0, 0.6); ctx.beginPath(); ctx.arc(lookX, lookY, 3, 0, Math.PI * 2); ctx.fill(); // ... 继续 requestAnimationFrame ... }这个红色小圆点会始终指向鼠标模拟了宠物的“视线”。你可以把它替换成更复杂的图形或者根据角度切换成不同方向的精灵图帧。6. 进阶功能与个性化定制6.1 多状态动画与精灵图管理一个完整的宠物应该有多种动画。我们需要一个更强大的动画管理器。我们可以定义一个Animation类并管理多个动画序列。// src/animation.ts export interface AnimationFrame { x: number; // 精灵图上的 x 坐标 y: number; // 精灵图上的 y 坐标 width: number; height: number; duration: number; // 这一帧显示的毫秒数 } export class Animation { private frames: AnimationFrame[]; private currentFrameIndex: number; private elapsedTime: number; private isPlaying: boolean; public name: string; constructor(name: string, frames: AnimationFrame[]) { this.name name; this.frames frames; this.currentFrameIndex 0; this.elapsedTime 0; this.isPlaying true; } update(deltaTime: number): AnimationFrame { if (!this.isPlaying) return this.frames[this.currentFrameIndex]; this.elapsedTime deltaTime; const currentFrame this.frames[this.currentFrameIndex]; if (this.elapsedTime currentFrame.duration) { this.elapsedTime 0; this.currentFrameIndex (this.currentFrameIndex 1) % this.frames.length; return this.frames[this.currentFrameIndex]; } return currentFrame; } play() { this.isPlaying true; } pause() { this.isPlaying false; } reset() { this.currentFrameIndex 0; this.elapsedTime 0; } } // 在 main.ts 中定义动画 import { Animation } from ./animation; const idleAnimation new Animation(idle, [ { x: 0, y: 0, width: 100, height: 100, duration: 200 }, { x: 100, y: 0, width: 100, height: 100, duration: 200 }, { x: 200, y: 0, width: 100, height: 100, duration: 200 }, { x: 300, y: 0, width: 100, height: 100, duration: 200 }, ]); const sleepAnimation new Animation(sleep, [ { x: 0, y: 100, width: 100, height: 100, duration: 500 }, { x: 100, y: 100, width: 100, height: 100, duration: 500 }, ]); // ... 加载更多动画然后在主渲染循环中根据DesktopPet的当前状态选择对应的Animation实例进行更新和绘制。6.2 配置系统与数据持久化为了让用户能自定义宠物我们需要一个配置系统。Tauri 提供了方便的fs和pathAPI 来读写本地文件。我们可以将配置保存为 JSON 文件。首先在前端创建一个设置界面可以是一个隐藏的、通过快捷键唤出的面板让用户调整参数如动画速度、互动开关、宠物类型等。当设置改变时调用 Tauri 后端命令保存到文件。// src-tauri/src/commands/config.rs (新建文件) use serde::{Deserialize, Serialize}; use std::fs; use tauri::PathResolver; #[derive(Serialize, Deserialize, Default)] pub struct PetConfig { pub pet_name: String, pub animation_speed: f64, pub interaction_enabled: bool, // ... 其他配置 } #[tauri::command] pub fn load_config(path_resolver: tauri::StatePathResolver) - ResultPetConfig, String { let config_path path_resolver .app_config_dir() .expect(failed to resolve app config dir) .join(config.json); if config_path.exists() { let content fs::read_to_string(config_path).map_err(|e| e.to_string())?; serde_json::from_str(content).map_err(|e| e.to_string()) } else { Ok(PetConfig::default()) // 返回默认配置 } } #[tauri::command] pub fn save_config(config: PetConfig, path_resolver: tauri::StatePathResolver) - Result(), String { let config_dir path_resolver .app_config_dir() .expect(failed to resolve app config dir); // 确保配置目录存在 fs::create_dir_all(config_dir).map_err(|e| e.to_string())?; let config_path config_dir.join(config.json); let content serde_json::to_string_pretty(config).map_err(|e| e.to_string())?; fs::write(config_path, content).map_err(|e| e.to_string())?; Ok(()) }然后在main.rs中注册这些命令。前端就可以通过invoke来加载和保存配置了。6.3 系统托盘与后台运行一个真正的桌面宠物应该在关闭窗口后仍能留在系统托盘并且可以右键退出或显示设置。Tauri 提供了强大的系统托盘支持。// src-tauri/src/main.rs use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem}; fn main() { let show CustomMenuItem::new(show.to_string(), 显示宠物); let hide CustomMenuItem::new(hide.to_string(), 隐藏宠物); let quit CustomMenuItem::new(quit.to_string(), 退出); let tray_menu SystemTrayMenu::new() .add_item(show) .add_item(hide) .add_native_item(SystemTrayMenuItem::Separator) .add_item(quit); let system_tray SystemTray::new().with_menu(tray_menu); tauri::Builder::default() .system_tray(system_tray) .on_system_tray_event(|app, event| match event { tauri::SystemTrayEvent::MenuItemClick { id, .. } { match id.as_str() { show { let window app.get_window(main).unwrap(); window.show().unwrap(); } hide { let window app.get_window(main).unwrap(); window.hide().unwrap(); } quit { app.exit(0); } _ {} } } _ {} }) // ... 其他配置和命令注册 .run(tauri::generate_context!()) .expect(error while running tauri application); }这样你的应用就有了一个托盘图标。点击“隐藏宠物”会关闭主窗口但应用仍在后台运行点击“显示宠物”会重新打开窗口。7. 打包发布与性能优化7.1 为不同平台构建应用开发完成后使用 Tauri 的构建命令可以轻松生成各平台安装包npm run tauri build这条命令会根据你的操作系统生成对应的包Windows 上是.msi安装包和.exe可执行文件macOS 上是.dmg和.appLinux 上是.deb或.AppImage。你可以在src-tauri/target/release目录下找到生成的文件。实操心得在构建前务必在tauri.conf.json中配置好bundle相关的信息如应用标识符、图标、版权信息等。图标需要多种尺寸如 32x32, 128x128, 256x256 等并放在src-tauri/icons目录下。Tauri 的文档有详细的图标生成指南。7.2 性能优化要点桌面宠物虽然小巧但常年运行性能优化很重要动画帧率限制不是所有宠物都需要 60FPS。对于简单的循环动画30FPS 甚至更低就足够了。可以使用setTimeout或基于时间的差值来控制渲染频率减少 CPU/GPU 占用。const targetFPS 30; const frameDuration 1000 / targetFPS; let lastFrameTime 0; function animate(currentTime) { if (currentTime - lastFrameTime frameDuration) { requestAnimationFrame(animate); return; } lastFrameTime currentTime; // ... 渲染逻辑 ... requestAnimationFrame(animate); }事件监听防抖像mousemove这样高频率的事件不要在其回调中执行复杂计算或状态更新。可以使用防抖debounce或节流throttle技术。function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle true; setTimeout(() inThrottle false, limit); } }; } canvas.addEventListener(mousemove, throttle((e) { // 更新鼠标位置计算距离等 }, 100)); // 每100毫秒最多执行一次资源预加载与缓存确保精灵图等资源在初始化时完全加载完毕避免运行时卡顿。对于多组精灵图可以提前加载并缓存 Image 对象。非活动状态休眠当用户长时间没有与电脑交互可以通过检测系统空闲时间或宠物窗口被其他全屏窗口完全覆盖时可以让宠物进入“低功耗”模式比如停止动画循环或切换到极低帧率的静态画面。这需要与系统API交互Tauri 可以通过插件或调用原生API实现。8. 常见问题与调试技巧8.1 窗口透明与点击穿透问题问题设置了transparent: true和decorations: false但窗口背景是黑色或者无法点击穿透到后面的应用。排查检查前端 CSS 的body和html背景色是否设置为transparent。检查 Canvas 背景。如果你在每一帧都clearRect确保清除的颜色是透明色rgba(0,0,0,0)。在 Tauri 配置中确认windows的transparent属性为true。点击穿透默认情况下透明窗口区域是可以点击穿透的。如果你希望宠物本身可拖动但其他透明区域可穿透只需要像我们之前做的那样在 Canvas 上监听拖动事件即可。如果你希望整个窗口包括透明区域都能拖动需要在 Tauri 中启用整个窗口的拖动或者设置一个全窗口大小的透明拖拽区域。8.2 动画卡顿或闪烁问题宠物动画不流畅或者绘制时出现闪烁。排查与解决双缓冲Canvas 绘制本身是单缓冲的在复杂动画中可能会闪烁。一个简单的解决方案是使用两个 Canvas一个用于离屏渲染一个用于显示。或者确保你的绘制操作在requestAnimationFrame的一次回调内完成不要分散到多个异步操作中。精灵图尺寸确保精灵图的尺寸是 2 的幂次方如 128, 256, 512并且图片格式经过优化使用pngquant或tinypng压缩加载更快。硬件加速检查 Canvas 是否使用了 GPU 加速。可以在浏览器开发者工具的“渲染”面板中勾选“图层边框”查看。通常 Canvas 默认会使用硬件加速。8.3 跨平台兼容性差异问题在 Windows 上运行良好但在 macOS 或 Linux 上位置不对、行为异常。排查DPI/缩放不同操作系统和不同显示器的缩放设置会影响 Canvas 的实际像素尺寸。可以使用window.devicePixelRatio来调整 Canvas 的绘制比例确保图像清晰。const dpr window.devicePixelRatio || 1; canvas.width 100 * dpr; canvas.height 100 * dpr; ctx.scale(dpr, dpr); // 之后的绘制逻辑都基于逻辑像素100x100系统托盘不同平台的系统托盘 API 和行为有细微差别。Tauri 已经做了封装但图标格式和菜单行为最好在各平台真机上测试。文件路径保存配置、日志等文件时使用 Tauri 提供的PathResolverAPI (app_config_dir,app_data_dir等)不要硬编码路径以保证跨平台兼容。8.4 内存泄漏排查问题长时间运行后应用占用内存缓慢增长。排查事件监听器确保在窗口关闭或组件销毁时移除了全局的事件监听器如window.addEventListener。否则这些监听器会一直持有对 DOM 元素或对象的引用导致无法垃圾回收。定时器清除所有setTimeout和setInterval。在我们的状态机代码中每次进入新状态前都清除了旧的定时器这是好习惯。使用开发者工具在开发时可以利用浏览器或 WebView的开发者工具中的“内存”面板定期拍摄堆快照对比查看哪些对象在持续增加。开发这样一个桌面宠物项目最大的乐趣在于看着一堆代码逐渐变成一个有个性、有反应的数字生命。从技术上看它串联了前端渲染、桌面应用开发、系统交互和状态逻辑是一个综合性很强的练手项目。你可以不断为它添加新功能比如让它读取系统资源使用情况并做出“疲惫”的表情或者接入简单的语音识别让它对你的声音有反应。开源社区的魅力也在于此emilie-desktop-pet只是一个起点你可以 fork 它创造出属于你自己的独一无二的桌面伙伴。