入门

使用 Ionic React 构建用户管理应用


本教程演示如何构建一个基本的用户管理应用。该应用对用户进行身份验证和识别,将他们的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料详细信息以及上传个人资料照片。该应用使用

Supabase User Management example

项目设置#

在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。

创建项目#

  1. 在 Supabase 控制面板中创建一个新项目
  2. 输入您的项目详细信息。
  3. 等待新的数据库启动。

设置数据库 schema#

现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。

  1. 转到控制面板中的 SQL 编辑器 页面。
  2. 点击 社区 > 快速入门 选项卡下的 用户管理 Starter
  3. 点击 运行

获取 API 详细信息#

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。

为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。

阅读 API 密钥文档 以全面了解所有密钥类型及其用途。

构建应用#

让我们从头开始构建 React 应用。

初始化 Ionic React 应用#

我们可以使用 Ionic CLI 初始化一个名为 supabase-ionic-react 的应用

1
npm install -g @ionic/cli
2
ionic start supabase-ionic-react blank --type react
3
cd supabase-ionic-react

然后让我们安装唯一的附加依赖项:supabase-js

1
npm install @supabase/supabase-js

最后,我们想将环境变量保存在一个 .env 文件中。我们只需要您之前复制的 API URL 和密钥 此处

1
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
2
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY

现在我们已经准备好了 API 凭据,让我们创建一个辅助文件来初始化 Supabase 客户端。这些变量将在浏览器中公开,这完全没问题,因为我们在数据库上启用了 行级别安全

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || ''
4
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || ''
5
6
export const supabase = createClient(supabaseUrl, supabasePublishableKey)

设置登录路由#

让我们设置一个 React 组件来管理登录和注册。我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。

1
import { useState } from 'react';
2
import {
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';
16
17
import {supabase} from '../supabaseClient'
18
19
export function LoginPage() {
20
const [email, setEmail] = useState('');
21
22
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": email
31
});
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>
46
47
<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
<IonInput
57
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
Login
66
</IonButton>
67
</div>
68
</form>
69
</IonList>
70
</IonContent>
71
</IonPage>
72
);
73
}

账户页面#

用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。

让我们创建一个名为 Account.tsx 的新组件。

1
import {
2
IonButton,
3
IonContent,
4
IonHeader,
5
IonInput,
6
IonItem,
7
IonLabel,
8
IonPage,
9
IonTitle,
10
IonToolbar,
11
useIonLoading,
12
useIonToast,
13
useIonRouter
14
} from '@ionic/react';
15
import { useEffect, useState } from 'react';
16
import { supabase } from '../supabaseClient';
17
import { Session } from '@supabase/supabase-js';
18
19
export 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
});
29
30
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
}, [])
39
40
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 supabase
49
.from('profiles')
50
.select(`username, website, avatar_url`)
51
.eq('id', user!.data.user?.id)
52
.single();
53
54
if (error && status !== 406) {
55
throw error;
56
}
57
58
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();
77
78
console.log('update ');
79
await showLoading();
80
81
try {
82
const user = await supabase.auth.getUser();
83
84
const updates = {
85
id: user!.data.user?.id,
86
...profile,
87
avatar_url: avatar_url,
88
updated_at: new Date(),
89
};
90
91
const { error } = await supabase.from('profiles').upsert(updates);
92
93
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>
109
110
<IonContent>
111
<form onSubmit={updateProfile}>
112
<IonItem>
113
<IonLabel>
114
<p>Email</p>
115
<p>{session?.user?.email}</p>
116
</IonLabel>
117
</IonItem>
118
119
<IonItem>
120
<IonLabel position="stacked">Name</IonLabel>
121
<IonInput
122
type="text"
123
name="username"
124
value={profile.username}
125
onIonChange={(e) =>
126
setProfile({ ...profile, username: e.detail.value ?? '' })
127
}
128
></IonInput>
129
</IonItem>
130
131
<IonItem>
132
<IonLabel position="stacked">Website</IonLabel>
133
<IonInput
134
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 Profile
145
</IonButton>
146
</div>
147
</form>
148
149
<div className="ion-text-center">
150
<IonButton fill="clear" onClick={signOut}>
151
Log Out
152
</IonButton>
153
</div>
154
</IonContent>
155
</IonPage>
156
);
157
}

启动!#

现在我们已经准备好了所有组件,让我们更新 App.tsx

1
import { Redirect, Route } from 'react-router-dom'
2
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'
3
import { IonReactRouter } from '@ionic/react-router'
4
import { supabase } from './supabaseClient'
5
6
import '@ionic/react/css/ionic.bundle.css'
7
8
/* Theme variables */
9
import './theme/variables.css'
10
import { LoginPage } from './pages/Login'
11
import { AccountPage } from './pages/Account'
12
import { useEffect, useState } from 'react'
13
import { Session } from '@supabase/supabase-js'
14
15
setupIonicReact()
16
17
const 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
<Route
33
exact
34
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
}
47
48
export default App

完成此操作后,在终端窗口中运行以下命令

1
ionic serve

然后在浏览器中打开 localhost:3000,您应该会看到完成的应用。

Supabase Ionic React

奖励:个人资料照片#

每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。

创建一个上传小部件#

首先安装两个包,以便与用户的摄像头交互。

1
npm install @ionic/pwa-elements @capacitor/camera

Capacitor 是 Ionic 的一个跨平台原生运行时,它使 Web 应用能够通过应用商店部署并提供对原生设备 API 的访问。

Ionic PWA 元素是一个配套包,它将为没有用户界面的某些浏览器 API 提供自定义的 Ionic UI 进行填充。

安装了这些包后,我们可以更新 index.tsx 以包含 Ionic PWA 元素的附加引导调用。

1
import React from 'react'
2
import ReactDOM from 'react-dom'
3
import App from './App'
4
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
5
import reportWebVitals from './reportWebVitals'
6
7
import { defineCustomElements } from '@ionic/pwa-elements/loader'
8
defineCustomElements(window)
9
10
ReactDOM.render(
11
<React.StrictMode>
12
<App />
13
</React.StrictMode>,
14
document.getElementById('root')
15
)
16
17
serviceWorkerRegistration.unregister()
18
reportWebVitals()

然后创建一个 AvatarComponent

1
import { IonIcon } from '@ionic/react';
2
import { person } from 'ionicons/icons';
3
import { Camera, CameraResultType } from '@capacitor/camera';
4
import { useEffect, useState } from 'react';
5
import { supabase } from '../supabaseClient';
6
import './Avatar.css'
7
export 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>();
15
16
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
});
26
27
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
);
33
34
const fileName = `${Math.random()}-${new Date().getTime()}.${
35
photo.format
36
}`;
37
const { error: uploadError } = await supabase.storage
38
.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
};
48
49
const downloadImage = async (path: string) => {
50
try {
51
const { data, error } = await supabase.storage
52
.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
};
63
64
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>
73
74
</div>
75
);
76
}

添加新的小部件#

然后我们可以将小部件添加到 Account 页面

1
// Import the new component
2
3
import { Avatar } from '../components/Avatar';
4
5
// ...
6
return (
7
<IonPage>
8
<IonHeader>
9
<IonToolbar>
10
<IonTitle>Account</IonTitle>
11
</IonToolbar>
12
</IonHeader>
13
14
<IonContent>
15
<Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>

此时,您已经拥有一个功能齐全的应用!