本项目旨在创建一个功能类似于 Joplin 或 Simplenote 的跨平台离线优先云同步的笔记应用。
技术栈核心:
- 前端框架: Next.js (使用 App Router)
- 桌面应用: Electron
- 后端 & 部署: Vercel
- 数据库: Vercel Postgres (云端) & Dexie.js (本地)
- 对象存储: Vercel Blob (用于图片附件)
- 样式: Tailwind CSS
第一章:项目创建与基础设置
1. 创建 Next.js 项目
首先,我们使用 create-next-app 初始化项目。
npx create-next-app@latest my-cloud-note
在交互式提示中,请确保选择 TypeScript 和 Tailwind CSS。
2. 目录结构概览
创建完成后,我们整理并规划了项目的核心目录结构:
- F:. (项目根目录)
package.json: 项目的心脏。定义了项目名称版本依赖脚本等。next.config.js: Next.js 的配置文件。tsconfig.json: TypeScript 的配置文件,定义了编译规则。prisma/schema.prisma: [核心] Prisma 的数据模型文件,定义了云端数据库的表结构,是所有数据结构的“单一事实来源”。.env.development.local: [核心] 本地开发环境变量。next dev和vercel dev会自动读取此文件,用于存放数据库连接字符串和各种 API 密钥。极其重要,切勿提交到 Git。electron/:main.js: Electron 的主进程入口。负责创建和管理桌面窗口,并加载我们的 Next.js 应用。
public/: 存放静态资源,如manifest.json(用于 PWA) 和图标。src/: 应用源码核心。app/: Next.js App Router 的根目录。layout.tsx: 全局根布局。page.tsx: 应用的首页。api/: 后端 API 路由。所有与云端交互的逻辑都在这里,它们被部署为 Vercel Serverless Functions。sync/route.ts: 负责笔记数据的云同步。upload/route.ts: 负责将图片上传到 Vercel Blob。
notebook/[notebookId]/: 动态路由,构成了应用的核心界面。
components/: React 组件库。存放可复用的 UI 组件,如Sidebar.tsxNoteList.tsxSyncButton.tsx等。lib/: 核心逻辑库。db.ts: 本地数据库定义。使用 Dexie.js 定义了notebooks和notes表的结构。sync.ts: 前端同步逻辑。封装了调用/api/sync的fetch请求,实现了本地与云端的同步。
第二章:核心功能实现与问题排查
1. 本地数据库 (离线优先)
我们使用 Dexie.js 作为浏览器端的 IndexedDB 封装库,在 src/lib/db.ts 中定义了数据表,并明确了主键为 string 类型 (Table<Notebook string>),以确保与 TypeScript 和后续的同步逻辑完美兼容,实现了应用的离线存储能力。
2. 云端数据库 (数据同步)
我们在 Vercel 上创建了 Postgres 数据库 (由 Neon 提供技术支持),并通过 prisma/schema.prisma 文件定义了与本地 DexieDB 对应的 Notebook 和 Note 模型。在 src/app/api/sync/route.ts 中,我们编写了接收本地数据通过 Prisma upsert 到 Postgres并返回云端最新数据的完整同步逻辑。
遇到的问题:
-
构建时无法连接数据库: Vercel 的构建环境无法访问数据库,导致在 "collecting page data" 阶段
new PrismaClient()初始化失败。- 最终解决方案: 将
new PrismaClient()的实例化操作,从 API 文件的顶层移动到POST等请求处理函数内部。这确保了只在处理真实请求时才尝试连接数据库,完美地解决了构建时错误。
- 最终解决方案: 将
-
Prisma 引擎与 Edge 运行时冲突: 当尝试将 API 部署到 Vercel Edge Functions (
runtime = 'edge') 时,频繁遇到Invalid client engine type错误。- 最终解决方案: 为了追求稳定性和最佳兼容性,我们将所有 API 路由的运行时统一设置为
runtime = "nodejs",并移除了prisma/schema.prisma中的engineType = "wasm"配置。这使得 API 运行在一个标准的 Node.js 环境中,Prisma 的binary引擎可以完美工作。
- 最终解决方案: 为了追求稳定性和最佳兼容性,我们将所有 API 路由的运行时统一设置为
-
多窗口数据不同步: 在一个浏览器窗口修改或删除数据后,在另一个新窗口中无法看到最新的更改。
- 诊断: 确认是 Vercel 的数据缓存 (Data Cache) 导致
/api/sync返回了旧的数据。 - 最终解决方案: 在
/api/sync/route.ts文件顶部添加export const revalidate = 0。这个指令强力禁用了该路由的任何数据缓存,确保每次请求都命中数据库,返回最新鲜的数据。
- 诊断: 确认是 Vercel 的数据缓存 (Data Cache) 导致
3. 图片上传 (集成 Vercel Blob)
我们在 src/app/api/upload/route.ts 中使用 @vercel/blob SDK 实现了图片上传功能,并在前端编辑器组件中集成了粘贴拖拽和点击上传。
遇到的问题:
-
环境变量问题: 本地能上传,线上 Vercel 不行。报错
Vercel Blob: No token found。- 解决方案: 认识到环境变量需要通过 Vercel 管理。登录 Vercel 项目的 Storage 标签页,创建并连接 Blob 存储。Vercel 随即自动生成并注入了
BLOB_READ_WRITE_TOKEN环境变量。最后,通过npx vercel env pull将此 token 同步到本地.env.development.local文件,解决了本地和云端的所有认证问题。
- 解决方案: 认识到环境变量需要通过 Vercel 管理。登录 Vercel 项目的 Storage 标签页,创建并连接 Blob 存储。Vercel 随即自动生成并注入了
-
前后端
body格式不匹配: 前端直接发送File对象,而后端尝试用request.formData()解析,导致Could not parse content as FormData错误。- 最终解决方案: 统一了前后端的行为。前端使用
new FormData().append('file' file)来包装文件后端则保持await request.formData()来解析,这是最标准兼容性最好的文件上传方式。
- 最终解决方案: 统一了前后端的行为。前端使用
第三章:桌面端集成 (Electron)
为了实现跨平台,我们将 Next.js 应用打包进 Electron。
- 安装 Electron 相关依赖:
npm install electron electron-builder electron-is-dev concurrently wait-on --save-dev - 编写主进程
electron/main.js: 这个文件负责创建浏览器窗口。在开发模式下,它加载http://localhost:3000在生产模式下,我们实现了一个智能的内置静态服务器,它能够正确处理 Next.js App Router 的动态路由和静态资源,通过index.html回退机制确保了客户端路由的正常工作。 - 配置
package.json脚本: 我们添加了electron:dev和electron:build脚本。electron:build会先执行vercel build --prod来生成一个生产环境的.vercel/output目录,然后再由electron-builder进行打包。
第四章:部署与发布
1. Web 端部署
Web 端的部署与 Vercel 的 Git 工作流深度集成。
-
本地构建与预览:
# 在本地模拟 Vercel 的生产构建 npx vercel build --prod # 在本地启动一个完全模拟 Vercel 线上环境的服务器 npx vercel dev这个流程是部署前进行最终测试的利器,我们正是用它发现了所有本地
next build无法暴露的环境差异问题。 -
发布: 将代码推送到已连接到 Vercel 项目的 Git 仓库主分支,即可自动触发构建和部署。
2. 桌面端发布
我们使用 Git Tag 来管理和触发桌面端的发布流程。
- 打标签:
git tag -a v1.0.0 -m "Release version 1.0.0" - 推送标签:
这可以触发 GitHub Actions (如果配置了) 运行git push origin v1.0.0npm run electron:build,生成各平台的安装包并上传到 Releases 页面。
MyCloudNote AppImage 解压并运行步骤
- 构建 Electron 应用:
npm run electron:build - 解压 AppImage 文件:
./MyCloudNote-1.0.0.AppImage --appimage-extract - 进入解压目录:
cd squashfs-root/ - 以无沙箱模式运行应用:
./my-cloud-note --no-sandbox
⚡ 小贴士:
- 若 AppImage 无执行权限,先运行
chmod +x MyCloudNote-1.0.0.AppImage。 - 使用
npm install -g asar后,可以用asar list app.asar查看打包内容。
结语
通过这个项目,我们不仅构建了一个实用的应用程序,更重要的是,我们亲身经历并解决了一系列在现代 Web 和桌面开发中极具代表性的问题。从前端的状态同步到后端的环境配置,从本地调试到云端部署,每一步都充满了挑战与收获。**最终我们确定了以 Vercel Serverless Functions (nodejs 运行时) 作为后端,配合“全量覆盖”同步策略和 Dexie.js 本地数据库,构建了一套简单可靠且高效的系统。
评论区(0 条)
发表评论⏳ 加载编辑器…