作为一名独立开发者,我维护着一个名为「每日一诗」的微信小程序。这是一个非常简单的应用——每天为用户推荐一首古诗词,支持打卡和徽章功能。
就在最近,我面临一个棘手的问题:微信云开发的环境即将到期。
经过调研,我决定将后端从微信云开发迁移到 Supabase。整个迁移过程耗时约一周,踩了不少坑,也收获了很多经验。今天这篇文章,我想把这段经历分享出来,希望能为有类似需求的朋友提供一些参考。
为什么放弃微信云开发?
微信云开发是一个优秀的方案,它让小程序开发者可以快速搭建后端服务,无需运维,开箱即用。但对于我的场景,它存在几个难以接受的限制:
1. 环境到期且不支持迁移
微信云开发的环境存在有效期,到期后数据会怎样?官方文档语焉不详。更关键的是,即使我重新购买环境,也无法平滑迁移数据。这意味着我要么接受数据丢失的风险,要么在到期前手动导出所有数据——对于一个懒癌晚期开发者来说,这太痛苦了。
2. 费用逐年上涨
云开发的免费额度在逐年缩减,而我的小程序虽然用户量不大,但访问量却在缓慢增长。去年还能白嫖的功能,今年可能就要开始付费了。虽然费用不高,但作为一个没有任何商业化的小工具,我不想每年为它支付任何费用。
3. 微信生态的绑定焦虑
把所有用户数据、业务逻辑都绑定在微信生态里,让我隐隐感到不安。如果有一天微信调整了政策,或者我想要开发其他平台的应用(比如 iOS、H5),这些数据和服务将很难迁移出来。
基于以上考虑,我开始寻找一个开源、可移植、免费的替代方案。
为什么选择 Supabase?
在调研了多个后端即服务(BaaS)平台后,我最终锁定了 Supabase。选择它的理由很简单:
开源 + 免费 + 全球化
Supabase 是 Firebase 的开源替代品,基于 PostgreSQL 数据库,提供认证、存储、Edge Functions 等功能。它的免费额度对于我的小程序来说完全够用——每月 500MB 数据库、50MB 文件存储、50 万次 Edge Function 调用。更重要的是,它是开源的,即使哪天 Supabase 公司倒闭了,我也可以自行部署,不存在被「绑架」的风险。
技术栈匹配度高
Supabase 使用 Deno 编写 Edge Functions,对于前端开发者来说非常友好。我可以用 TypeScript 快速编写后端逻辑,无需学习新的语言和框架。
社区活跃,文档完善
Supabase 的文档写得非常清晰,社区也很活跃,遇到问题很容易找到解决方案。相比之下,微信云开发的文档虽然也不错,但很多高级用法需要自己摸索。
迁移方案概述
整个迁移过程可以分为三个阶段:
第一阶段:数据库设计
我把原来的微信云数据库(MongoDB 风格)的数据迁移到了 Supabase 的 PostgreSQL。主要涉及三张表:
users:用户信息,包括 openid、昵称、连续打卡天数、徽章等check_ins:打卡记录,关联用户,存储诗词内容badges:徽章定义,包括名称、描述、获取条件等
在 Supabase 中,我开启了 Row Level Security(RLS)策略,确保用户只能访问自己的数据,安全性得到了保障。
第二阶段:云函数重构
原来使用微信云函数实现的业务逻辑,现在迁移到 Supabase Edge Functions:
| 功能 | 原微信云函数 | 新 Edge Function |
|---|---|---|
| 用户登录 | user | wechat-auth |
| 打卡 | checkIn | check-in |
| 获取用户数据 | getUserData | get-user-data |
| 邮箱登录 | - | email-login |
第三阶段:小程序端适配
小程序的修改相对简单,只需要把请求地址从微信云函数改成 Supabase Edge Function,同时调整一下请求参数和返回数据的解析逻辑。核心业务逻辑完全不需要改变。
核心代码实现
这里展示几个关键功能的精简版实现,希望能为有类似需求的朋友提供参考。
1. 微信静默登录(Edge Function)
// supabase/functions/wechat-auth/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
const { code } = await req.json()
const { data: { env } } = await supabase.auth.getEnv()
// 调用微信 API 获取 openid
const wxRes = await fetch(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${Deno.env.get('APPID')}&secret=${Deno.env.get('SECRET')}&code=${code}&grant_type=authorization_code`
)
const tokenData = await wxRes.json()
if (tokenData.errcode) {
return new Response(JSON.stringify({ error: tokenData.errmsg }), {
status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
const { openid } = tokenData
// 查询或创建用户
const { data: user, error } = await supabase
.from('users')
.select('*')
.eq('openid', openid)
.single()
if (!user && !error) {
// 创建新用户
const { data: newUser } = await supabase
.from('users')
.insert({ openid, nickname: '诗词爱好者', badges: [] })
.select()
.single()
return new Response(JSON.stringify({ success: true, user: newUser }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
return new Response(JSON.stringify({ success: true, user }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
})
2. 打卡逻辑(Edge Function)
// supabase/functions/check-in/index.ts
Deno.serve(async (req) => {
const { openid, poem_content, poem_title, poem_author, poem_dynasty } = await req.json()
// 获取用户
const { data: user } = await supabase.from('users').select('*').eq('openid', openid).single()
const today = new Date().toISOString().split('T')[0]
// 检查今日是否已打卡
if (user.last_check_in_date === today) {
return new Response(JSON.stringify({ success: false, message: '今日已打卡' }))
}
// 计算连续打卡天数
let continuousDays = 1
if (user.last_check_in_date) {
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
if (user.last_check_in_date === yesterday) {
continuousDays = user.continuous_check_in_days + 1
}
}
// 记录打卡
await supabase.from('check_ins').insert({
user_id: user.id, poem_content, poem_title, poem_author, poem_dynasty, check_in_date: today
})
// 更新用户数据
await supabase.from('users').update({
continuous_check_in_days: continuousDays,
last_check_in_date: today,
updated_at: new Date().toISOString()
}).eq('id', user.id)
return new Response(JSON.stringify({ success: true, continuous_days: continuousDays }))
})
3. 小程序端调用
// pages/index/index.js
const SUPABASE_URL = 'https://xxx.supabase.co'
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
function callSupabaseFunction(functionName, data) {
return new Promise((resolve, reject) => {
wx.request({
url: `${SUPABASE_URL}/functions/v1/${functionName}`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
},
data,
success: (res) => res.data.success ? resolve(res.data) : reject(new Error(res.data.error)),
fail: reject
})
})
}
// 打卡
async function handleCheckIn() {
const res = await callSupabaseFunction('check-in', {
openid: this.data.openid,
poem_content: this.data.poemData.content,
poem_title: this.data.poemData.title
})
if (res.success) {
wx.showToast({ title: '打卡成功!', icon: 'success' })
}
}
踩坑记录与解决方案
迁移过程中遇到了几个有意思的问题,这里分享出来,希望读者能避开这些坑。
问题一:跨域请求失败
本地开发时,Edge Function 部署到 Supabase 服务器上,请求时报了 CORS 错误。解决方案是在 Edge Function 中添加正确的 CORS 头:
const corsHeaders = {
'Access-Control-Allow-Origin': '*', // 生产环境建议指定具体域名
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
问题二:时区处理
PostgreSQL 默认存储的是 UTC 时间,而中国时区是 UTC+8。在计算「今日是否已打卡」时,我一开始直接用 new Date() 获取当前时间,导致打卡判断出现偏差。解决方案是统一将日期字符串转为 UTC 后再比较,或者在数据库查询时使用 timezone('Asia/Shanghai', now())。
问题三:徽章并发问题
用户快速点击打卡按钮时,可能触发多次打卡请求,导致徽章被重复发放。解决方案是在 Edge Function 中使用数据库事务,或者在用户表上添加唯一索引,确保同一用户的打卡操作串行执行。
问题四:部署后环境变量不生效
在本地测试时,我把微信 AppSecret 写死在代码里,上传 Edge Function 后忘记在 Supabase Dashboard 中配置环境变量,导致所有请求都失败了。教训是:敏感信息一定要通过环境变量注入,不要心存侥幸。
迁移后的效果
迁移完成后,我对比了前后两个版本的体验:
| 指标 | 微信云开发 | Supabase |
|---|---|---|
| 响应时间 | ~200ms | ~300ms |
| 费用 | 免费(即将收费) | 免费 |
| 数据可控性 | 绑定微信生态 | 开源可移植 |
| 开发体验 | 一般 | 良好 |
响应时间略有增加,但用户基本感知不到。费用从「即将付费」变成了「永久免费」,数据也从被微信「绑架」变成了完全由自己掌控。
总结与建议
如果你也在维护一个小程序,并且对微信云开发的限制感到困扰,我建议可以考虑 Supabase 作为替代方案。它的免费额度足够支撑大多数小型项目,开源的特性也让你在未来有更多选择。
当然,迁移是有成本的。如果你现在的小程序用户量很小,迁移的收益可能不足以抵消投入的精力。但如果你对数据可控性有追求,或者想要在未来支持多平台,Supabase 绝对值得一试。
最后,分享三点建议:
- 迁移前做好数据备份:无论是导出 JSON 还是写脚本迁移,都要确保数据不丢失。
- 保留旧版本一段时间:上线新版本后,观察一段时间再下线旧版本,给自己留条后路。
- 文档要写清楚:代码注释、部署流程、配置说明,都要写得清清楚楚,方便以后维护。
祝各位开发顺利,代码无 Bug!