使用React构建用户管理应用
本教程展示如何构建一个基本的用户管理应用程序。该应用程序对用户进行身份验证和识别,将其个人资料信息存储在数据库中,并允许用户登录、更新个人资料详细信息以及上传个人资料照片。该应用程序使用:
- Supabase数据库 - 一个Postgres数据库,用于存储用户数据,并通过行级安全性来保护数据,确保用户只能访问自己的信息。
- Supabase身份验证 - 允许用户注册和登录。
- Supabase存储 - 用户可以上传个人资料照片。
如果在学习本指南时遇到问题,请参考 GitHub上的完整示例。
项目设置
在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。
创建项目
- 在 Supabase 仪表板中创建一个新项目。
- 输入项目详细信息。
- 等待新数据库启动。
设置数据库模式
现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。
获取 API 密钥
既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。
我们只需要从 API 设置中获取项目 URL 和 anon
密钥。
- 前往仪表板中的 API 设置 页面。
- 在该页面找到您的项目
URL
、anon
和service_role
密钥。
构建应用
让我们从零开始构建这个React应用。
初始化React应用
我们可以使用Vite来初始化一个名为supabase-react
的应用:
12npm create vite@latest supabase-react -- --template reactcd supabase-react
然后安装唯一的额外依赖:supabase-js。
1npm install @supabase/supabase-js
最后我们需要将环境变量保存在.env.local
文件中。
只需要之前复制的API URL和anon
密钥。
12VITE_SUPABASE_URL=YOUR_SUPABASE_URLVITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
现在我们已经配置好了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量会在浏览器端暴露,这完全没问题,因为我们的数据库已经启用了行级安全。
创建并编辑src/supabaseClient.js
:
123456import { createClient } from '@supabase/supabase-js'const supabaseUrl = import.meta.env.VITE_SUPABASE_URLconst supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEYexport const supabase = createClient(supabaseUrl, supabaseAnonKey)
应用样式(可选)
可选步骤是更新 src/index.css
文件来美化应用外观。
您可以在此处找到该文件的完整内容。
设置登录组件
让我们创建一个 React 组件来管理登录和注册功能。我们将使用 Magic Links(魔法链接),这样用户无需密码即可通过电子邮件登录。
创建并编辑 src/Auth.jsx
:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import { useState } from 'react'import { supabase } from './supabaseClient'export default function Auth() { const [loading, setLoading] = useState(false) const [email, setEmail] = useState('') const handleLogin = async (event) => { event.preventDefault() setLoading(true) const { error } = await supabase.auth.signInWithOtp({ email }) if (error) { alert(error.error_description || error.message) } else { alert('请查看您的邮箱获取登录链接!') } setLoading(false) } return ( <div className="row flex flex-center"> <div className="col-6 form-widget"> <h1 className="header">Supabase + React</h1> <p className="description">通过下方邮箱接收魔法链接登录</p> <form className="form-widget" onSubmit={handleLogin}> <div> <input className="inputField" type="email" placeholder="您的邮箱" value={email} required={true} onChange={(e) => setEmail(e.target.value)} /> </div> <div> <button className={'button block'} disabled={loading}> {loading ? <span>加载中</span> : <span>发送魔法链接</span>} </button> </div> </form> </div> </div> )}
账户页面
当用户登录后,我们可以允许他们编辑个人资料信息和管理账户。
让我们为此创建一个名为 src/Account.jsx
的新组件。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import { useState, useEffect } from 'react'import { supabase } from './supabaseClient'export default function Account({ session }) { const [loading, setLoading] = useState(true) const [username, setUsername] = useState(null) const [website, setWebsite] = useState(null) const [avatar_url, setAvatarUrl] = useState(null) useEffect(() => { let ignore = false async function getProfile() { setLoading(true) const { user } = session const { data, error } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) .single() if (!ignore) { if (error) { console.warn(error) } else if (data) { setUsername(data.username) setWebsite(data.website) setAvatarUrl(data.avatar_url) } } setLoading(false) } getProfile() return () => { ignore = true } }, [session]) async function updateProfile(event, avatarUrl) { event.preventDefault() setLoading(true) const { user } = session const updates = { id: user.id, username, website, avatar_url: avatarUrl, updated_at: new Date(), } const { error } = await supabase.from('profiles').upsert(updates) if (error) { alert(error.message) } else { setAvatarUrl(avatarUrl) } setLoading(false) } return ( <form onSubmit={updateProfile} className="form-widget"> <div> <label htmlFor="email">Email</label> <input id="email" type="text" value={session.user.email} disabled /> </div> <div> <label htmlFor="username">用户名</label> <input id="username" type="text" required value={username || ''} onChange={(e) => setUsername(e.target.value)} /> </div> <div> <label htmlFor="website">网站</label> <input id="website" type="url" value={website || ''} onChange={(e) => setWebsite(e.target.value)} /> </div> <div> <button className="button block primary" type="submit" disabled={loading}> {loading ? '加载中...' : '更新'} </button> </div> <div> <button className="button block" type="button" onClick={() => supabase.auth.signOut()}> 退出登录 </button> </div> </form> )}
启动应用!
现在我们已经准备好了所有组件,让我们更新 src/App.jsx
:
123456789101112131415161718192021222324252627import './App.css'import { useState, useEffect } from 'react'import { supabase } from './supabaseClient'import Auth from './Auth'import Account from './Account'function App() { const [session, setSession] = useState(null) useEffect(() => { supabase.auth.getSession().then(({ data: { session } }) => { setSession(session) }) supabase.auth.onAuthStateChange((_event, session) => { setSession(session) }) }, []) return ( <div className="container" style={{ padding: '50px 0 100px 0' }}> {!session ? <Auth /> : <Account key={session.user.id} session={session} />} </div> )}export default App
完成后,在终端窗口中运行:
1npm run dev
然后在浏览器中打开 localhost:5173,您应该能看到完整的应用。
额外功能:个人头像
每个 Supabase 项目都配置了存储功能,用于管理照片和视频等大型文件。
创建上传组件
让我们为用户创建一个头像组件,使其能够上传个人资料照片。我们可以从创建一个新组件开始:
创建并编辑 src/Avatar.jsx
:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182import { useEffect, useState } from 'react'import { supabase } from './supabaseClient'export default function Avatar({ url, size, onUpload }) { const [avatarUrl, setAvatarUrl] = useState(null) const [uploading, setUploading] = useState(false) useEffect(() => { if (url) downloadImage(url) }, [url]) async function downloadImage(path) { try { const { data, error } = await supabase.storage.from('avatars').download(path) if (error) { throw error } const url = URL.createObjectURL(data) setAvatarUrl(url) } catch (error) { console.log('下载图片时出错: ', error.message) } } async function uploadAvatar(event) { try { setUploading(true) if (!event.target.files || event.target.files.length === 0) { throw new Error('必须选择要上传的图片。') } const file = event.target.files[0] const fileExt = file.name.split('.').pop() const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError } onUpload(event, filePath) } catch (error) { alert(error.message) } finally { setUploading(false) } } return ( <div> {avatarUrl ? ( <img src={avatarUrl} alt="头像" className="avatar image" style={{ height: size, width: size }} /> ) : ( <div className="avatar no-image" style={{ height: size, width: size }} /> )} <div style={{ width: size }}> <label className="button primary block" htmlFor="single"> {uploading ? '上传中...' : '上传'} </label> <input style={{ visibility: 'hidden', position: 'absolute', }} type="file" id="single" accept="image/*" onChange={uploadAvatar} disabled={uploading} /> </div> </div> )}
添加新组件
接下来我们可以将新组件添加到 src/Account.jsx
的账户页面:
123456789101112131415161718// 导入新组件import Avatar from './Avatar'// ...return ( <form onSubmit={updateProfile} className="form-widget"> {/* 添加到主体部分 */} <Avatar url={avatar_url} size={150} onUpload={(event, url) => { updateProfile(event, url) }} /> {/* ... */} </form>)
至此,您已经拥有了一个功能完整的应用程序!