返回文章列表

「碎片」架构:单页 SPA + Turso 边缘数据库 + 云函数 + 缓存优先

技术方案的关键词:极简

「碎片」的架构可以概括为一句话:一个云函数 + 一个注册页面 + 一个边缘数据库 + 四个自定义组件。但这套极简架构背后,解决了不少实际问题。

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 不能用

微信小程序原生有个 page-container 组件专门做这种 overlay 效果,但它只在「客户端渲染」模式下生效。在 webview 模式下(为了用 Turso 等非微信云服务),page-container 完全不工作。纯 CSS overlay 是唯一解法。


二、Turso 边缘数据库 + 云函数代理

数据库选的是 Turso——一个基于 libSQL(SQLite 的 fork)的边缘数据库。每个查询路由到离用户最近的只读副本,延迟很低。

但微信小程序不能直连 Turso(不支持 libSQL 的 HTTP2 协议),所以中间加了一层 腾讯云函数 做代理。

整个后端只有一个云函数 tursoProxy/index.js(357 行),负责所有操作:

路由功能
POST /auth/wechatJWT 登录(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.jsloadWeightHistory() 中实现:

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 的人有一点参考价值。