嗯,是的,我回来填坑了。既然要打广告,就要对得起各位金主,总是水文是不行的,还是要输出干货。
广告继续。本博客即日起开始常年招商,欢迎各位想推广产品的老板投广告,目前定价 4800/年,亦可增加评测文章、教学文章配合,详情可与我联系。具体详情更新在 本站广告 2023 年招商。
Supabase 用户体系入门
Supabase 提供 Serverless 数据库,自带用户体系,开发用户系统非常简单。而且 SDK 很完善,适配各种框架,大部分功能开箱即用;官方教程方面做得也很好。所以,首先请大家先认真阅读这两篇官方文档、教程:
第二篇教程文章详细的介绍了使用 Supabae 开发用户管理系统的过程,配合上面的文档,相信大家都可以轻松入门。唯一的难点(对部分同学来说)可能是英文,我就先不复述。
接下来的文章主要介绍官方没有细说的最佳实践和各种实用细节。
邮件登录与邮件模版
虽然官方提供了很多方案,但是我想,对于大部分同学来说,邮件+密码登录才是最常用的,最多再加上第三方比如 Gmail 登录。
Supabase 提供了发邮件的功能,方便用户校验邮箱、找回密码。但是,正如很多邮件服务器那样,它的到达率并不理想。我猜测有些用户即使没有滥用 Supabase 的邮件服务,也会因为邮件模版过于简单而被误杀。所以大家第一步应该先配置邮件模版,尽量多体现自己的产品相关信息,尽量跟大家的通用模版区分开,减少误报误杀的可能。
Supabase 默认要求用户校验邮箱之后才能登录,如果你暂时不关心这一点,可以考虑在 Auth ProviderS > Email关掉这个功能。此后,用户完成注册后就可以登录,不过他们的用户状态在数据里仍然是未验证,我们也可以借此对用户进行一些差异化的控制。
注册与登录,自动注册
有一点我不太喜欢。Supabase 提供两个方法:signUp
和 signInWithPassword
,分别用来注册和登录。但是通常情况下,我并不希望区分这两个方法。我认为我们的网站对用户来说,只是一个小工具,我觉得他们不会在意是否在这里注册过,他们只希望做他们要做的事情,注册登录只是我们为了方便自己而做的步骤。
所以我一般仍然会通过不同的路径区分用户是想注册,还是想登录。但是我会在用户 signUp
后检查错误信息,如果是重复注册,就自动帮他们登录。
const { data: { user }, error } = await supabase.auth[method]({
email,
password,
options,
});
if (error) {
// registered, login
if (error.message === 'User already registered') {
return login(email, password);
}
throw error;
}
Nuxt3+Pinia 实现用户身份校验及额度查询
教程中的例子比较简单,通常来说我们的产品会复杂很多,比如,我们可能需要全局状态管理工具,方便不同页面不同组件中使用。在 Nuxt3 中使用 Pinia 需要用到特有的 Module:@pinia/nuxt,简单安装官方教程配置一下即可。这里主要分享我摸索的其它未提及要点。
额度查询
Supabase 在创建项目的时候已经帮我们建好了用户表,并且放在默认不对外的库里面。所以我们最好不要直接修改表,而是如教程所讲,创建一个新的表,把用户账户相关的其它数据放进去,比如在这样一款典型的 ChatGPT 产品中,就是帐户额度,然后通过关联 id 的方式访问。
这就带来一个问题:什么时候去读这个表?
对 SPA 来说,这个问题比较简单,登录之后,以及网页初始化的时候读取即可,反正所有请求都由我们全权控制。但是对于 Nuxt3 来说就值得讨论一下。因为 Nuxt3 要进行服务器端渲染,所以我们通常希望它在渲染页面的时候,这个数据已经就位了。
这里我推荐配合 Pinia Store,使用 supabase.auth.onAuthStateChange
函数。我会建立一个 userStore
,在里面完成用户注册、登录、登出等动作,同时注册 onAuthStateChange
事件,完成用户状态变更后的处理。
const useUserStore = defineStore('user', () => {
const supabase = useSupabaseClient();
const user = useSupabaseUser();
async function login(email: string, password: string, isSignUp = false): Promise<void> {}
async function loginWithGoogle(): Promise<void> {}
async function logout(): Promise<void> {}
// 注册事件处理函数
supabase.auth.onAuthStateChange((event, session) => {
switch (event) {
case 'INITIAL_SESSION':
case 'SIGNED_IN':
// 在这里处理登录后和初始化时拿到用户身份后的操作,比如加载用户账户的额度
// 这里需要注意:此时 `user.value` 尚未被填充,所以我们不能依赖它。有两个方案:1. 延迟执行 `setTimeout`;2. 直接从 `session` 参数里拿到用户信息然后处理
// 我的做法是,如果需要请求 API,那就选择方案2;否则,使用方案1,等 100ms
}
});
});
export useUserStore;
请仔细看上方代码的注释,非常重要,看起来很简单,但其实是血泪铺就的道路。
判断第三方登录,及 UI 呈现
当用户选择第三方登录,比如 Gmail 的时候,Supabase 目前只支持跳转这一种方式,不如 Auth0,可以用 popup,省很多事。
完整的第三方登录流程是:
- 跳转到第三方登录页面
- 登录
- 跳转到 Supabase 中间页
- 带着
access_token
跳回我们的网站,Supabase SDK 负责读取access_token
,验证用户身份等工作
其中涉及到两个(或更多)网站的配置,因为文档中写的有,我就不再详述,需要的同学留言我再补充。
最麻烦的是(4),跳转回来之后,从页面初始化成功到完成登录,中间可能会隔很久,短则1、2秒,长则10s 都有可能。这期间用户可以继续操作,会产生各种误会和误操作。这里我建议:
- 登录时传递
redirectTo
,要求最终跳转页面是某个登录落地页 - 初始化的时候,检查 URL 里的
#access_token=
- 如果有,说明是第三方登录返回,然后就显示登录中的状态
- 等待
user.value
变化,说明完成登录,跳转到原先应该去的页面,或者继续之前没做完的操作 - 如果没有
access_token
,直接跳转到其他落地页。
总结
上面这些内容看起来不复杂,但实际上对用户体验影响很大,并且文档、教程说得也不清楚,导致我的老板对产品一直不太满意。希望这篇文章可以节省大家的宝贵时间。
如果有关于 Supabase、Vercel、Nuxt3 的问题,欢迎评论里面留言。
系列文章
- 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇
- 【本文】使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录
欢迎吐槽,共同进步