技术方案的关键词:极简
「碎片」的架构可以概括为一句话:一个云函数 + 一个注册页面 + 一个边缘数据库 + 四个自定义组件。但这套极简架构背后,解决了不少实际问题。
app.json 里只注册了一个页面 pages/home/home,另外两个视图(时间线、设置)全是 CSS Overlay。
一、从多页到单页:消除白屏闪烁
最初「碎片」和普通小程序一样,每个视图一个独立页面。但微信小程序的页面切换在 webview 模式下有白屏闪烁——哪怕用了动画过渡,切换时还是会闪一下。
解决方案:把所有视图放在一个页面里,用 CSS 控制显示隐藏。app.json 只注册一个页面:
// app.json
{
"pages": ["pages/home/home"],
"renderer": "webview"
}
时间线和设置面板是 CSS overlay 浮层,从右侧滑入:
<view class="overlay-mask" bind:tap="closeTimeline"></view>
<view class="overlay-panel panel-slide-in" wx:if="{{showTimeline}}">
<timeline-view></timeline-view>
</view>
CSS:
.overlay-panel {
position: fixed;
right: 0;
top: 0;
width: 85%;
height: 100vh;
z-index: 100;
transform: translateX(100%);
transition: transform 280ms ease-out;
}
.panel-slide-in { transform: translateX(0); }
微信小程序原生有个 page-container 组件专门做这种 overlay 效果,但它只在「客户端渲染」模式下生效。在 webview 模式下(为了用 Turso 等非微信云服务),page-container 完全不工作。纯 CSS overlay 是唯一解法。
二、Turso 边缘数据库 + 云函数代理
数据库选的是 Turso——一个基于 libSQL(SQLite 的 fork)的边缘数据库。每个查询路由到离用户最近的只读副本,延迟很低。
但微信小程序不能直连 Turso(不支持 libSQL 的 HTTP2 协议),所以中间加了一层 腾讯云函数 做代理。
整个后端只有一个云函数 tursoProxy/index.js(357 行),负责所有操作:
| 路由 | 功能 |
|---|---|
POST /auth/wechat | JWT 登录(HMAC-SHA256,30 天有效期) |
GET /api/cards | 获取所有卡片 |
PUT /api/cards | 更新单张卡片 |
DELETE /api/cards | 删除卡片 |
GET /api/records | 获取时间线记录 |
POST /api/records | 新建记录 |
POST /api/weight-history | 新增体重记录 |
POST /api/wish-items/:cardId | 新增愿望项 |
POST /api/user/sync | 用户信息同步 |
POST /api/danger/delete-my-data | 一键注销(含确认流程) |
云函数通过 cloud.getWXContext() 获取用户的 openid,零配置实现用户身份识别。
三、缓存优先策略
为了让用户打开页面「秒看内容」,实现了双层缓存:
读取: 内存 → wx.getStorageSync → 显示(立即)
写入: 内存 + wx.setStorageSync(同时)
网络: 后台静默拉取最新数据,更新缓存
三个缓存桶:
__fg_cache_cards__— 卡片列表__fg_cache_records__— 时间线记录__fg_cache_weight__— 体重历史
效果:即使手机刚开机第一次打开小程序,也是立即看到上次的数据,1-2 秒后如果网络数据更新了会自动替换。
四、JWT Token 失效自动恢复
登录用 JWT(30 天有效期),但总有过期的时候。在 API 请求中,如果捕获到 Token 错误,自动调用 auth.login() 刷新 Token,然后重试失败的请求。调用方完全无感知。
这个机制在 home.js 的 loadWeightHistory() 中实现:
if (err.code === 'TOKEN_EXPIRED') {
await auth.login()
return await api.records() // 自动重试
}
五、styleIsolation 陷阱
自定义组件默认样式隔离(styleIsolation: isolated),但时间线组件需要继承全局的 .paper-sheet、.chip-glass 等样式类。解决方案是显式声明:
{
"component": true,
"styleIsolation": "apply-shared"
}
这样组件既能使用全局样式,又能覆盖自己特有的样式。
六、数据库:5 张表 + 7 个索引
Turso 上建了 5 张表:
users— 用户信息(openid 为 PK)cards— 卡片数据(type, title, content, colors, data JSON 等)records— 时间线记录(关联 card_id)weight_history— 体重历史(始终存 kg,前端显示时转换斤/公斤)wish_items— 愿望列表(关联 card_id,级联删除)
七、免费方案总结
| 服务 | 方案 | 费用 |
|---|---|---|
| 数据库 | Turso 免费计划(边缘 SQLite) | 免费 |
| 后端 | 微信云函数(tursoProxy 代理) | 免费额度内 |
| 前端 | 微信小程序(不上架) | 免费 |
| AI 编码 | AI 对话生成代码 | 按量付费 |
这个项目让我学到最多的是「架构取舍」——用一个云函数替代整个后端、用 CSS overlay 替代官方组件、用缓存优先替代实时数据。每一条都是因为真实遇到的问题才做的选择,不是为了炫技。
两个项目的 5 篇日志写完了,希望对想做 vibe coding 的人有一点参考价值。