大家好,我是牧云。
前段时间,我告别了朝九晚五的打工生涯,决定换一种活法,试一试 独立开发者 (Indie Hacker) 这条路。
2026 年伊始,我正式开启了我的独立开发之旅。 这是我转型后的第一个“练手”项目:Life Grid(人生进度条)。
为什么做这个?
作为一个理科生,我习惯量化一切。人的一生按80岁算,大约只有 4160 周。我希望能有一个极简的工具,把这些时间可视化出来,警醒自己 "Make it count" 。
技术约束
作为独立开发的第一战,我给自己定下了几个硬性技术约束:
- No Server: 不买云服务器,不写一行 Spring Boot/Node.js 后端代码。
- No Build: 不用 Webpack/Vite,回归最纯粹的 Vanilla JS (原生 HTML/CSS/JS) 。
- Local-First: 必须支持离线使用,数据优先存本地,登录后自动同步云端。
最终,我选择了 Supabase 作为 BaaS (Backend as a Service) 解决方案,配合 PostgreSQL 的 RLS (行级安全策略) ,仅用 400行代码 就实现了全套的鉴权、存储和多端同步。
本文将分享在这个项目中的架构思考,特别是**“离线优先”的数据同步策略**。
🏗️ 架构设计:Backendless 的艺术
1. 数据库设计 (PostgreSQL)
既然没有后端 API 层,数据库的设计必须足够“扁平”且“安全”。
我在 Supabase 上建了一张极简的表 milestones:
SQL
create table public.test_milestones (
id bigint generated by default as identity not null,
created_at timestamp with time zone not null default now(),
user_id uuid not null default auth.uid(), -- 核心:绑定当前登录用户
week_index integer not null, -- 第几周 (0-4159)
content text null, -- 笔记内容
constraint test_milestones_pkey primary key (id),
constraint test_milestones_user_id_week_index_key unique (user_id, week_index) -- 联合唯一索引
) tablespace pg_default;
2. 安全策略 (RLS - Row Level Security)
作为习惯了后端开发思维的人,最大的心理障碍是:“直接在前端连数据库,安全吗?”
答案是:只要配置好 RLS,比你自己写的 API 还安全。
Supabase 允许我们在数据库层面定义访问策略。我开启了 RLS,并配置了如下 Policy:
-
SELECT / INSERT / UPDATE / DELETE:
- 条件:
auth.uid() = user_id
- 条件:
这就意味着,虽然前端代码是公开的,但数据库引擎会在执行 SQL 时自动校验: “你只能操作 user_id 等于你当前 Token ID 的数据” 。这从根源上解决了越权访问问题。
🧩 核心难点:Local-First 同步策略
这是本项目最烧脑的部分。
为了极致的用户体验,我设计了如下交互逻辑:
- 游客模式 (Guest): 用户打开即用,数据存
localStorage,不强迫登录。 - 登录模式 (User): 用户输入邮箱(Magic Link)登录,数据上云。
问题来了: 当用户在“游客模式”写了笔记,然后登录了账号,如何保证本地的笔记不丢,且能正确合并到云端?
我设计了一个 “Server-Wins 但兼顾本地新增” 的合并策略。
状态机逻辑:
-
Init: 读取
localStorage渲染,此时是离线数据。 -
Login: 检测到
onAuthStateChange事件。 -
Sync (关键步骤):
- Pull: 拉取云端数据 (
CloudData)。 - Diff: 遍历本地数据 (
LocalData)。如果发现 “本地有记录,但云端该位置为空” ,判定为新增数据。 - Push: 将这些新增数据
Upsert到云端。 - Merge: 重新拉取/合并数据,若有冲突,以云端数据为准 (Server Wins)。
- Cache: 将最终结果回写到
localStorage,保持一致性。
- Pull: 拉取云端数据 (
核心代码实现 (Vanilla JS)
JavaScript
// 核心同步函数
async function syncAndLoadData() {
try {
// 1. 获取本地“私房钱” (未登录时的数据)
const localRaw = localStorage.getItem('lifeGridMilestones');
const localData = localRaw ? JSON.parse(localRaw) : {};
// 2. 拉取云端数据
const { data: cloudData, error } = await supabaseClient
.from('test_milestones')
.select('week_index, content');
if (error) throw error;
// 3. 构建索引 Map
const cloudMap = new Map();
cloudData.forEach(row => cloudMap.set(row.week_index, row.content));
// 4. 【关键】找出本地新增的数据 (Local has, Cloud miss)
const updatesToUpload = [];
for (const [key, content] of Object.entries(localData)) {
const weekIdx = parseInt(key);
// 只有云端没记录时,才上传本地数据,防止覆盖云端已有的历史
if (!cloudMap.has(weekIdx) && content) {
updatesToUpload.push({
user_id: currentUser.id,
week_index: weekIdx,
content: content
});
}
}
// 5. 批量上传新增数据
if (updatesToUpload.length > 0) {
console.log("🚀 Syncing local notes to cloud...", updatesToUpload);
await supabaseClient
.from('test_milestones')
.upsert(updatesToUpload, { onConflict: 'user_id, week_index' });
// 乐观更新:直接把上传的数据补到 cloudMap 里,避免二次请求
updatesToUpload.forEach(item => cloudMap.set(item.week_index, item.content));
}
// 6. 最终合并与渲染
milestones = {};
for (const [weekIdx, content] of cloudMap.entries()) {
milestones[weekIdx] = content;
}
// 双写缓存
localStorage.setItem('lifeGridMilestones', JSON.stringify(milestones));
renderGrid();
} catch (err) {
console.error("Sync Error:", err);
// 降级处理:云端挂了就读本地
loadLocalOnly();
}
}
🎨 UI/UX:极简主义与磨砂质感
虽然是练手项目,但 UI 不能凑合。
我没有引入 Tailwind CSS,而是手写了不到 100 行 CSS。
核心交互是一个模态弹窗 (Modal) ,为了营造“原生 App”的高级感,我使用了 backdrop-filter: blur(5px) 来实现磨砂玻璃效果,并配合 transform: scale() 做了微动效。
CSS
/* 磨砂玻璃弹窗 */
.modal-overlay {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); /* 兼容 Safari */
transition: opacity 0.3s ease;
}
.modal-card {
background: var(--bg-color);
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
/* 弹窗弹起时的回弹效果 */
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
🚀 总结与开源
从庞大的微服务架构,回归到极简的 index.html + Supabase,这种掌控感久违了。
对于独立开发者来说,Tech Stack 越轻,跑得越快。
这个项目是我的起点。目前 V1.0 版本已完全开源,欢迎大家 Star 或 Fork,打造属于你自己的“人生进度条”。
GitHub 仓库地址:
体验地址:
[muyunlee2025.github.io/life-grid/]
如果你对 独立开发、Supabase 实战、AI 应用 感兴趣,欢迎关注我的掘金账号。
下一篇,我准备研究一下 Animated Drawings,试试能不能用 AI 复活我儿子的涂鸦画作。
Peace & Code.