快速入门: Ionic React
介绍
这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。
- MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
- MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
- MemFire Cloud 存储:用户可以上传照片。
- 行级安全:数据受到保护,个人只能访问自己的数据。
- 即时APIs。当你创建你的数据库表时,API将自动生成。
在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。
GitHub#
如果你在阅读指南时遇到困难,请参考此版本。
项目设置
在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。
创建一个项目
- 进入MemFire Cloud。
- 点击 "新项目"。
- 输入你的项目细节。
- 等待新数据库的启动。
设置数据库模式
现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。
- 进入仪表版中的SQL编辑器页面。
- 点击 用户管理的模板。
- 点击 运行。
获取API密钥#
现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。
我们只需要从API设置中获得URL和anon
密钥。
- 进入仪表板中的设置页面。
- 单击侧边栏中的API。
- 在这个页面上找到你的API
URL
、anon
和service_role
键。
构建应用程序
让我们开始从头开始构建React应用程序。
初始化一个Ionic React应用程序#
我们可以使用Ionic CLI来初始化
一个名为 supabase-ionic-react
的应用程序。
npm install -g @ionic/cli ionic start supabase-ionic-react blank --type react cd supabase-ionic-react
然后让我们安装唯一的额外依赖:supabase-js
npm install @supabase/supabase-js
最后,我们要把环境变量保存在.env
中。
我们所需要的是API URL和你[早些时候]复制的anon
密钥(#get-theapi-keys)。
.envREACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
现在我们已经有了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量将被暴露在 在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全。
src/supabaseClient.js1import { createClient } from '@supabase/supabase-js' 2 3const supabaseUrl = process.env.REACT_APP_SUPABASE_URL 4const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY 5 6export const supabase = createClient(supabaseUrl, supabaseAnonKey)
设置一个登录路线
让我们设置一个React组件来管理登录和注册。我们将使用Magic Links,所以用户可以用他们的电子邮件登录,而不使用密码。
/src/pages/Login.tsx1import { 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';
16import { supabase } from '../supabaseClient';
17
18export function LoginPage() {
19 const [email, setEmail] = useState('');
20
21 const [showLoading, hideLoading] = useIonLoading();
22 const [showToast ] = useIonToast();
23 const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
24 console.log()
25 e.preventDefault();
26 await showLoading();
27 try {
28 await supabase.auth.signIn({ email });
29 await showToast({ message: 'Check your email for the login link!' });
30 } catch (e: any) {
31 await showToast({ message: e.error_description || e.message , duration: 5000});
32 } finally {
33 await hideLoading();
34 }
35 };
36 return (
37 <IonPage>
38 <IonHeader>
39 <IonToolbar>
40 <IonTitle>Login</IonTitle>
41 </IonToolbar>
42 </IonHeader>
43
44 <IonContent>
45 <div className="ion-padding">
46 <h1>Supabase + Ionic React</h1>
47 <p>Sign in via magic link with your email below</p>
48 </div>
49 <IonList inset={true}>
50 <form onSubmit={handleLogin}>
51 <IonItem>
52 <IonLabel position="stacked">Email</IonLabel>
53 <IonInput
54 value={email}
55 name="email"
56 onIonChange={(e) => setEmail(e.detail.value ?? '')}
57 type="email"
58 ></IonInput>
59 </IonItem>
60 <div className="ion-text-center">
61 <IonButton type="submit" fill="clear">
62 Login
63 </IonButton>
64 </div>
65 </form>
66 </IonList>
67 </IonContent>
68 </IonPage>
69 );
70}
账号页面
在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。
让我们为它创建一个新的组件,叫做Account.tsx
。
src/pages/Account.tsx1import {
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';
15import { useEffect, useState } from 'react';
16import { supabase } from '../supabaseClient';
17
18export function AccountPage() {
19 const [showLoading, hideLoading] = useIonLoading();
20 const [showToast] = useIonToast();
21 const [session] = useState(() => supabase.auth.session());
22 const router = useIonRouter();
23 const [profile, setProfile] = useState({
24 username: '',
25 website: '',
26 avatar_url: '',
27 });
28 useEffect(() => {
29 getProfile();
30 }, [session]);
31 const getProfile = async () => {
32 console.log('get');
33 await showLoading();
34 try {
35 const user = supabase.auth.user();
36 let { data, error, status } = await supabase
37 .from('profiles')
38 .select(`username, website, avatar_url`)
39 .eq('id', user!.id)
40 .single();
41
42 if (error && status !== 406) {
43 throw error;
44 }
45
46 if (data) {
47 setProfile({
48 username: data.username,
49 website: data.website,
50 avatar_url: data.avatar_url,
51 });
52 }
53 } catch (error: any) {
54 showToast({ message: error.message, duration: 5000 });
55 } finally {
56 await hideLoading();
57 }
58 };
59 const signOut = async () => {
60 await supabase.auth.signOut();
61 router.push('/', 'forward', 'replace');
62 }
63 const updateProfile = async (e?: any, avatar_url: string = '') => {
64 e?.preventDefault();
65
66 console.log('update ');
67 await showLoading();
68
69 try {
70 const user = supabase.auth.user();
71
72 const updates = {
73 id: user!.id,
74 ...profile,
75 avatar_url: avatar_url,
76 updated_at: new Date(),
77 };
78
79 let { error } = await supabase.from('profiles').upsert(updates, {
80 returning: 'minimal', // Don't return the value after inserting
81 });
82
83 if (error) {
84 throw error;
85 }
86 } catch (error: any) {
87 showToast({ message: error.message, duration: 5000 });
88 } finally {
89 await hideLoading();
90 }
91 };
92 return (
93 <IonPage>
94 <IonHeader>
95 <IonToolbar>
96 <IonTitle>Account</IonTitle>
97 </IonToolbar>
98 </IonHeader>
99
100 <IonContent>
101 <form onSubmit={updateProfile}>
102 <IonItem>
103 <IonLabel>
104 <p>Email</p>
105 <p>{session?.user?.email}</p>
106 </IonLabel>
107 </IonItem>
108
109 <IonItem>
110 <IonLabel position="stacked">Name</IonLabel>
111 <IonInput
112 type="text"
113 name="username"
114 value={profile.username}
115 onIonChange={(e) =>
116 setProfile({ ...profile, username: e.detail.value ?? '' })
117 }
118 ></IonInput>
119 </IonItem>
120
121 <IonItem>
122 <IonLabel position="stacked">Website</IonLabel>
123 <IonInput
124 type="url"
125 name="website"
126 value={profile.website}
127 onIonChange={(e) =>
128 setProfile({ ...profile, website: e.detail.value ?? '' })
129 }
130 ></IonInput>
131 </IonItem>
132 <div className="ion-text-center">
133 <IonButton fill="clear" type="submit">
134 Update Profile
135 </IonButton>
136 </div>
137 </form>
138
139 <div className="ion-text-center">
140 <IonButton fill="clear" onClick={signOut}>
141 Log Out
142 </IonButton>
143 </div>
144 </IonContent>
145 </IonPage>
146 );
147}
启动
现在我们有了所有的组件,让我们更新App.tsx
。
src/App.tsx1import { Redirect, Route } from 'react-router-dom'
2import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'
3import { IonReactRouter } from '@ionic/react-router'
4import { supabase } from './supabaseClient'
5
6import '@ionic/react/css/ionic.bundle.css'
7
8/* 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'
14
15setupIonicReact()
16
17const App: React.FC = () => {
18 const [session, setSession] = useState < Session > null
19 useEffect(() => {
20 setSession(supabase.auth.session())
21 supabase.auth.onAuthStateChange((_event, session) => {
22 setSession(session)
23 })
24 }, [])
25 return (
26 <IonApp>
27 <IonReactRouter>
28 <IonRouterOutlet>
29 <Route
30 exact
31 path="/"
32 render={() => {
33 return session ? <Redirect to="/account" /> : <LoginPage />
34 }}
35 />
36 <Route exact path="/account">
37 <AccountPage />
38 </Route>
39 </IonRouterOutlet>
40 </IonReactRouter>
41 </IonApp>
42 )
43}
44
45export default App
一旦完成,在终端窗口运行这个程序。
ionic serve
然后打开浏览器到localhost:3000,你应该看到完成的应用程序。
个人照片
每个Supabase项目都配置了存储,用于管理照片和视频等大文件。
创建一个上传小组件
首先安装两个软件包,以便与用户的相机互动。
npm install @ionic/pwa-elements @capacitor/camera
CapacitorJS是Ionic的一个跨平台原生运行时间,它使网络应用通过应用商店部署,并提供对原生deavice API的访问。
Ionic PWA元素是一个配套的软件包,它将把某些不提供用户界面的浏览器API用自定义的Ionic UI进行聚填。
安装了这些包后,我们可以更新我们的index.tsx
,以包括对Ionic PWA元素的额外引导调用。
src/index.tsx1import React from 'react' 2import ReactDOM from 'react-dom' 3import App from './App' 4import * as serviceWorkerRegistration from './serviceWorkerRegistration' 5import reportWebVitals from './reportWebVitals' 6 7import { defineCustomElements } from '@ionic/pwa-elements/loader' 8defineCustomElements(window) 9 10ReactDOM.render( 11 <React.StrictMode> 12 <App /> 13 </React.StrictMode>, 14 document.getElementById('root') 15) 16 17serviceWorkerRegistration.unregister() 18reportWebVitals()
Then create an AvatarComponent.
src/components/Avatar.tsx1import { 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>();
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 let { 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}
添加新的小组件
然后我们就可以把这个小部件添加到账号页面:
src/pages/Account.tsx1// Import the new component 2 3import { Avatar } from '../components/Avatar'; 4 5// ... 6return ( 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>
下一步
在这个阶段,你已经有了一个功能完备的应用程序!
- 有问题吗?在此提问.
- 请登录MemFire Cloud