使用 Ionic React 构建用户管理应用
本教程演示如何构建一个基本的用户管理应用。该应用对用户进行身份验证和识别,将他们的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料详细信息以及上传个人资料照片。该应用使用
- Supabase 数据库 - 一个用于存储用户数据的 Postgres 数据库,以及 行级别安全,以保护数据并确保用户只能访问他们自己的信息。
- Supabase Auth - 允许用户注册和登录。
- Supabase Storage - 允许用户上传个人资料照片。

如果在阅读本指南时遇到问题,请参考 GitHub 上的完整示例。
项目设置#
在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。
创建项目#
- 在 Supabase 控制面板中创建一个新项目。
- 输入您的项目详细信息。
- 等待新的数据库启动。
设置数据库 schema#
现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。
获取 API 详细信息#
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。
为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。
API 密钥的更改
Supabase 正在更改密钥的工作方式,以提高项目安全性和开发人员体验。您可以 阅读完整的公告,但在过渡期间,您可以使用当前的 anon 和 service_role 密钥以及新的可发布密钥,格式为 sb_publishable_xxx,它将取代旧的密钥。
在大多数情况下,您可以从 项目的 Connect 对话框中获取正确的密钥,但如果您需要特定的密钥,可以在 项目设置页面的 API 密钥部分中找到所有密钥
- 对于旧版密钥,从 旧版 API 密钥 选项卡中复制
anon密钥用于客户端操作,并复制service_role密钥用于服务器端操作。 - 对于新密钥,打开 API 密钥 选项卡,如果您还没有可发布密钥,请单击 创建新的 API 密钥,并复制 可发布密钥 部分中的值。
阅读 API 密钥文档 以全面了解所有密钥类型及其用途。
构建应用#
让我们从头开始构建 React 应用。
初始化 Ionic React 应用#
我们可以使用 Ionic CLI 初始化一个名为 supabase-ionic-react 的应用
1npm install -g @ionic/cli2ionic start supabase-ionic-react blank --type react3cd supabase-ionic-react然后让我们安装唯一的附加依赖项:supabase-js
1npm install @supabase/supabase-js最后,我们想将环境变量保存在一个 .env 文件中。我们只需要您之前复制的 API URL 和密钥 此处。
1VITE_SUPABASE_URL=YOUR_SUPABASE_URL2VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY现在我们已经准备好了 API 凭据,让我们创建一个辅助文件来初始化 Supabase 客户端。这些变量将在浏览器中公开,这完全没问题,因为我们在数据库上启用了 行级别安全。
1import { createClient } from '@supabase/supabase-js'23const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || ''4const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || ''56export const supabase = createClient(supabaseUrl, supabasePublishableKey)设置登录路由#
让我们设置一个 React 组件来管理登录和注册。我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。
1import { useState } from 'react';2import {3 IonButton,4 IonContent,5 IonHeader,6 IonInput,7 IonItem,8 IonLabel,9 IonList,10 IonPage,11 IonTitle,12 IonToolbar,13 useIonToast,14 useIonLoading,15} from '@ionic/react';1617import {supabase} from '../supabaseClient'1819export function LoginPage() {20 const [email, setEmail] = useState('');2122 const [showLoading, hideLoading] = useIonLoading();23 const [showToast ] = useIonToast();24 const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {25 console.log()26 e.preventDefault();27 await showLoading();28 try {29 await supabase.auth.signInWithOtp({30 "email": email31 });32 await showToast({ message: 'Check your email for the login link!' });33 } catch (e: any) {34 await showToast({ message: e.error_description || e.message , duration: 5000});35 } finally {36 await hideLoading();37 }38 };39 return (40 <IonPage>41 <IonHeader>42 <IonToolbar>43 <IonTitle>Login</IonTitle>44 </IonToolbar>45 </IonHeader>4647 <IonContent>48 <div className="ion-padding">49 <h1>Supabase + Ionic React</h1>50 <p>Sign in via magic link with your email below</p>51 </div>52 <IonList inset={true}>53 <form onSubmit={handleLogin}>54 <IonItem>55 <IonLabel position="stacked">Email</IonLabel>56 <IonInput57 value={email}58 name="email"59 onIonChange={(e) => setEmail(e.detail.value ?? '')}60 type="email"61 ></IonInput>62 </IonItem>63 <div className="ion-text-center">64 <IonButton type="submit" fill="clear">65 Login66 </IonButton>67 </div>68 </form>69 </IonList>70 </IonContent>71 </IonPage>72 );73}账户页面#
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。
让我们创建一个名为 Account.tsx 的新组件。
1import {2 IonButton,3 IonContent,4 IonHeader,5 IonInput,6 IonItem,7 IonLabel,8 IonPage,9 IonTitle,10 IonToolbar,11 useIonLoading,12 useIonToast,13 useIonRouter14} from '@ionic/react';15import { useEffect, useState } from 'react';16import { supabase } from '../supabaseClient';17import { Session } from '@supabase/supabase-js';1819export function AccountPage() {20 const [showLoading, hideLoading] = useIonLoading();21 const [showToast] = useIonToast();22 const [session, setSession] = useState<Session | null>(null)23 const router = useIonRouter();24 const [profile, setProfile] = useState({25 username: '',26 website: '',27 avatar_url: '',28 });2930 useEffect(() => {31 const getSession = async () => {32 setSession(await supabase.auth.getSession().then((res) => res.data.session))33 }34 getSession()35 supabase.auth.onAuthStateChange((_event, session) => {36 setSession(session)37 })38 }, [])3940 useEffect(() => {41 getProfile();42 }, [session]);43 const getProfile = async () => {44 console.log('get');45 await showLoading();46 try {47 const user = await supabase.auth.getUser();48 const { data, error, status } = await supabase49 .from('profiles')50 .select(`username, website, avatar_url`)51 .eq('id', user!.data.user?.id)52 .single();5354 if (error && status !== 406) {55 throw error;56 }5758 if (data) {59 setProfile({60 username: data.username,61 website: data.website,62 avatar_url: data.avatar_url,63 });64 }65 } catch (error: any) {66 showToast({ message: error.message, duration: 5000 });67 } finally {68 await hideLoading();69 }70 };71 const signOut = async () => {72 await supabase.auth.signOut();73 router.push('/', 'forward', 'replace');74 }75 const updateProfile = async (e?: any, avatar_url: string = '') => {76 e?.preventDefault();7778 console.log('update ');79 await showLoading();8081 try {82 const user = await supabase.auth.getUser();8384 const updates = {85 id: user!.data.user?.id,86 ...profile,87 avatar_url: avatar_url,88 updated_at: new Date(),89 };9091 const { error } = await supabase.from('profiles').upsert(updates);9293 if (error) {94 throw error;95 }96 } catch (error: any) {97 showToast({ message: error.message, duration: 5000 });98 } finally {99 await hideLoading();100 }101 };102 return (103 <IonPage>104 <IonHeader>105 <IonToolbar>106 <IonTitle>Account</IonTitle>107 </IonToolbar>108 </IonHeader>109110 <IonContent>111 <form onSubmit={updateProfile}>112 <IonItem>113 <IonLabel>114 <p>Email</p>115 <p>{session?.user?.email}</p>116 </IonLabel>117 </IonItem>118119 <IonItem>120 <IonLabel position="stacked">Name</IonLabel>121 <IonInput122 type="text"123 name="username"124 value={profile.username}125 onIonChange={(e) =>126 setProfile({ ...profile, username: e.detail.value ?? '' })127 }128 ></IonInput>129 </IonItem>130131 <IonItem>132 <IonLabel position="stacked">Website</IonLabel>133 <IonInput134 type="url"135 name="website"136 value={profile.website}137 onIonChange={(e) =>138 setProfile({ ...profile, website: e.detail.value ?? '' })139 }140 ></IonInput>141 </IonItem>142 <div className="ion-text-center">143 <IonButton fill="clear" type="submit">144 Update Profile145 </IonButton>146 </div>147 </form>148149 <div className="ion-text-center">150 <IonButton fill="clear" onClick={signOut}>151 Log Out152 </IonButton>153 </div>154 </IonContent>155 </IonPage>156 );157}启动!#
现在我们已经准备好了所有组件,让我们更新 App.tsx
1import { Redirect, Route } from 'react-router-dom'2import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'3import { IonReactRouter } from '@ionic/react-router'4import { supabase } from './supabaseClient'56import '@ionic/react/css/ionic.bundle.css'78/* Theme variables */9import './theme/variables.css'10import { LoginPage } from './pages/Login'11import { AccountPage } from './pages/Account'12import { useEffect, useState } from 'react'13import { Session } from '@supabase/supabase-js'1415setupIonicReact()1617const App: React.FC = () => {18 const [session, setSession] = useState<Session | null>(null)19 useEffect(() => {20 const getSession = async () => {21 setSession(await supabase.auth.getSession().then((res) => res.data.session))22 }23 getSession()24 supabase.auth.onAuthStateChange((_event, session) => {25 setSession(session)26 })27 }, [])28 return (29 <IonApp>30 <IonReactRouter>31 <IonRouterOutlet>32 <Route33 exact34 path="/"35 render={() => {36 return session ? <Redirect to="/account" /> : <LoginPage />37 }}38 />39 <Route exact path="/account">40 <AccountPage />41 </Route>42 </IonRouterOutlet>43 </IonReactRouter>44 </IonApp>45 )46}4748export default App完成此操作后,在终端窗口中运行以下命令
1ionic serve然后在浏览器中打开 localhost:3000,您应该会看到完成的应用。

奖励:个人资料照片#
每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。
创建一个上传小部件#
首先安装两个包,以便与用户的摄像头交互。
1npm install @ionic/pwa-elements @capacitor/cameraCapacitor 是 Ionic 的一个跨平台原生运行时,它使 Web 应用能够通过应用商店部署并提供对原生设备 API 的访问。
Ionic PWA 元素是一个配套包,它将为没有用户界面的某些浏览器 API 提供自定义的 Ionic UI 进行填充。
安装了这些包后,我们可以更新 index.tsx 以包含 Ionic PWA 元素的附加引导调用。
1import React from 'react'2import ReactDOM from 'react-dom'3import App from './App'4import * as serviceWorkerRegistration from './serviceWorkerRegistration'5import reportWebVitals from './reportWebVitals'67import { defineCustomElements } from '@ionic/pwa-elements/loader'8defineCustomElements(window)910ReactDOM.render(11 <React.StrictMode>12 <App />13 </React.StrictMode>,14 document.getElementById('root')15)1617serviceWorkerRegistration.unregister()18reportWebVitals()然后创建一个 AvatarComponent。
1import { IonIcon } from '@ionic/react';2import { person } from 'ionicons/icons';3import { Camera, CameraResultType } from '@capacitor/camera';4import { useEffect, useState } from 'react';5import { supabase } from '../supabaseClient';6import './Avatar.css'7export function Avatar({8 url,9 onUpload,10}: {11 url: string;12 onUpload: (e: any, file: string) => Promise<void>;13}) {14 const [avatarUrl, setAvatarUrl] = useState<string | undefined>();1516 useEffect(() => {17 if (url) {18 downloadImage(url);19 }20 }, [url]);21 const uploadAvatar = async () => {22 try {23 const photo = await Camera.getPhoto({24 resultType: CameraResultType.DataUrl,25 });2627 const file = await fetch(photo.dataUrl!)28 .then((res) => res.blob())29 .then(30 (blob) =>31 new File([blob], 'my-file', { type: `image/${photo.format}` })32 );3334 const fileName = `${Math.random()}-${new Date().getTime()}.${35 photo.format36 }`;37 const { error: uploadError } = await supabase.storage38 .from('avatars')39 .upload(fileName, file);40 if (uploadError) {41 throw uploadError;42 }43 onUpload(null, fileName);44 } catch (error) {45 console.log(error);46 }47 };4849 const downloadImage = async (path: string) => {50 try {51 const { data, error } = await supabase.storage52 .from('avatars')53 .download(path);54 if (error) {55 throw error;56 }57 const url = URL.createObjectURL(data!);58 setAvatarUrl(url);59 } catch (error: any) {60 console.log('Error downloading image: ', error.message);61 }62 };6364 return (65 <div className="avatar">66 <div className="avatar_wrapper" onClick={uploadAvatar}>67 {avatarUrl ? (68 <img src={avatarUrl} />69 ) : (70 <IonIcon icon={person} className="no-avatar" />71 )}72 </div>7374 </div>75 );76}添加新的小部件#
然后我们可以将小部件添加到 Account 页面
1// Import the new component23import { Avatar } from '../components/Avatar';45// ...6return (7 <IonPage>8 <IonHeader>9 <IonToolbar>10 <IonTitle>Account</IonTitle>11 </IonToolbar>12 </IonHeader>1314 <IonContent>15 <Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>此时,您已经拥有一个功能齐全的应用!