快速入门: Ionic React

介绍

这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。

  • MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
  • MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
  • MemFire Cloud 存储:用户可以上传照片。
  • 行级安全:数据受到保护,个人只能访问自己的数据。
  • 即时APIs。当你创建你的数据库表时,API将自动生成。

在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。

Supabase用户管理实例

GitHub#

如果你在阅读指南时遇到困难,请参考此版本

项目设置

在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。

创建一个项目

  1. 进入MemFire Cloud
  2. 点击 "新项目"。
  3. 输入你的项目细节。
  4. 等待新数据库的启动。

设置数据库模式

现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。

  1. 进入仪表版中的SQL编辑器页面。
  2. 点击 用户管理的模板。
  3. 点击 运行

获取API密钥#

现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。 我们只需要从API设置中获得URL和anon密钥。

  1. 进入仪表板中的设置页面。
  2. 单击侧边栏中的API
  3. 在这个页面上找到你的APIURLanonservice_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)。

.env
REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL
REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

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

src/supabaseClient.js
1import { 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.tsx
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';
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.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  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.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'
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 Ionic React

个人照片

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

创建一个上传小组件

首先安装两个软件包,以便与用户的相机互动。

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

CapacitorJS是Ionic的一个跨平台原生运行时间,它使网络应用通过应用商店部署,并提供对原生deavice API的访问。

Ionic PWA元素是一个配套的软件包,它将把某些不提供用户界面的浏览器API用自定义的Ionic UI进行聚填。

安装了这些包后,我们可以更新我们的index.tsx,以包括对Ionic PWA元素的额外引导调用。

src/index.tsx
1import 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.tsx
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>();
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.tsx
1// 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>

下一步

在这个阶段,你已经有了一个功能完备的应用程序!