快速入门: Expo
介绍
这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。
- 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 Native应用。
初始化一个React Native应用#
我们可以使用expo
来初始化
一个名为 "expo-user-management "的应用程序。
npx create-expo-app -t expo-template-blank-typescript expo-user-management cd expo-user-management
然后让我们安装额外的依赖项。supabase-js
npm install @supabase/supabase-js npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
现在让我们创建一个辅助文件来初始化Supabase客户端。
我们需要API URL和你[早些时候]复制的anon
密钥(#get-the-api-keys)。
这些变量将被暴露在浏览器上,这完全没有问题,因为我们有
行级安全在我们的数据库上启用。
lib/supabase.ts1import AsyncStorage from '@react-native-async-storage/async-storage'
2import { createClient } from '@supabase/supabase-js'
3
4const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
5const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
6
7export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
8 auth: {
9 storage: AsyncStorage as any,
10 autoRefreshToken: true,
11 persistSession: true,
12 detectSessionInUrl: false,
13 },
14})
设置一个登录组件
让我们建立一个React Native组件来管理登录和注册。 用户将能够用他们的电子邮件和密码登录。
components/Auth.tsx1import React, { useState } from 'react'
2import { Alert, StyleSheet, View } from 'react-native'
3import { supabase } from '../lib/supabase'
4import { Button, Input } from 'react-native-elements'
5
6export default function Auth() {
7 const [email, setEmail] = useState('')
8 const [password, setPassword] = useState('')
9 const [loading, setLoading] = useState(false)
10
11 async function signInWithEmail() {
12 setLoading(true)
13 const { error } = await supabase.auth.signInWithPassword({
14 email: email,
15 password: password,
16 })
17
18 if (error) Alert.alert(error.message)
19 setLoading(false)
20 }
21
22 async function signUpWithEmail() {
23 setLoading(true)
24 const { error } = await supabase.auth.signUp({
25 email: email,
26 password: password,
27 })
28
29 if (error) Alert.alert(error.message)
30 setLoading(false)
31 }
32
33 return (
34 <View>
35 <View style={[styles.verticallySpaced, styles.mt20]}>
36 <Input
37 label="Email"
38 leftIcon={{ type: 'font-awesome', name: 'envelope' }}
39 onChangeText={(text) => setEmail(text)}
40 value={email}
41 placeholder="email@address.com"
42 autoCapitalize={'none'}
43 />
44 </View>
45 <View style={styles.verticallySpaced}>
46 <Input
47 label="Password"
48 leftIcon={{ type: 'font-awesome', name: 'lock' }}
49 onChangeText={(text) => setPassword(text)}
50 value={password}
51 secureTextEntry={true}
52 placeholder="Password"
53 autoCapitalize={'none'}
54 />
55 </View>
56 <View style={[styles.verticallySpaced, styles.mt20]}>
57 <Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
58 </View>
59 <View style={styles.verticallySpaced}>
60 <Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
61 </View>
62 </View>
63 )
64}
65
66const styles = StyleSheet.create({
67 container: {
68 marginTop: 40,
69 padding: 12,
70 },
71 verticallySpaced: {
72 paddingTop: 4,
73 paddingBottom: 4,
74 alignSelf: 'stretch',
75 },
76 mt20: {
77 marginTop: 20,
78 },
79})
账户页面
在用户登录后,我们可以允许他们编辑他们的个人资料细节和管理他们的账户。
让我们为此创建一个新的组件,叫做Account.tsx
。
components/Account.tsx1import { useState, useEffect } from 'react'
2import { supabase } from '../lib/supabase'
3import { StyleSheet, View, Alert } from 'react-native'
4import { Button, Input } from 'react-native-elements'
5import { Session } from '@supabase/supabase-js'
6
7export default function Account({ session }: { session: Session }) {
8 const [loading, setLoading] = useState(true)
9 const [username, setUsername] = useState('')
10 const [website, setWebsite] = useState('')
11 const [avatarUrl, setAvatarUrl] = useState('')
12
13 useEffect(() => {
14 if (session) getProfile()
15 }, [session])
16
17 async function getProfile() {
18 try {
19 setLoading(true)
20 if (!session?.user) throw new Error('No user on the session!')
21
22 let { data, error, status } = await supabase
23 .from('profiles')
24 .select(`username, website, avatar_url`)
25 .eq('id', session?.user.id)
26 .single()
27 if (error && status !== 406) {
28 throw error
29 }
30
31 if (data) {
32 setUsername(data.username)
33 setWebsite(data.website)
34 setAvatarUrl(data.avatar_url)
35 }
36 } catch (error) {
37 if (error instanceof Error) {
38 Alert.alert(error.message)
39 }
40 } finally {
41 setLoading(false)
42 }
43 }
44
45 async function updateProfile({
46 username,
47 website,
48 avatar_url,
49 }: {
50 username: string
51 website: string
52 avatar_url: string
53 }) {
54 try {
55 setLoading(true)
56 if (!session?.user) throw new Error('No user on the session!')
57
58 const updates = {
59 id: session?.user.id,
60 username,
61 website,
62 avatar_url,
63 updated_at: new Date(),
64 }
65
66 let { error } = await supabase.from('profiles').upsert(updates)
67
68 if (error) {
69 throw error
70 }
71 } catch (error) {
72 if (error instanceof Error) {
73 Alert.alert(error.message)
74 }
75 } finally {
76 setLoading(false)
77 }
78 }
79
80 return (
81 <View>
82 <View style={[styles.verticallySpaced, styles.mt20]}>
83 <Input label="Email" value={session?.user?.email} disabled />
84 </View>
85 <View style={styles.verticallySpaced}>
86 <Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
87 </View>
88 <View style={styles.verticallySpaced}>
89 <Input label="Website" value={website || ''} onChangeText={(text) => setWebsite(text)} />
90 </View>
91
92 <View style={[styles.verticallySpaced, styles.mt20]}>
93 <Button
94 title={loading ? 'Loading ...' : 'Update'}
95 onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
96 disabled={loading}
97 />
98 </View>
99
100 <View style={styles.verticallySpaced}>
101 <Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
102 </View>
103 </View>
104 )
105}
106
107const styles = StyleSheet.create({
108 container: {
109 marginTop: 40,
110 padding: 12,
111 },
112 verticallySpaced: {
113 paddingTop: 4,
114 paddingBottom: 4,
115 alignSelf: 'stretch',
116 },
117 mt20: {
118 marginTop: 20,
119 },
120})
启动
现在我们有了所有的组件,让我们更新App.tsx
。
App.tsx1import 'react-native-url-polyfill/auto' 2import { useState, useEffect } from 'react' 3import { supabase } from './lib/supabase' 4import Auth from './components/Auth' 5import Account from './components/Account' 6import { View } from 'react-native' 7import { Session } from '@supabase/supabase-js' 8 9export default function App() { 10 const [session, setSession] = useState<Session | null>(null) 11 12 useEffect(() => { 13 supabase.auth.getSession().then(({ data: { session } }) => { 14 setSession(session) 15 }) 16 17 supabase.auth.onAuthStateChange((_event, session) => { 18 setSession(session) 19 }) 20 }, []) 21 22 return ( 23 <View> 24 {session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />} 25 </View> 26 ) 27}
一旦完成,在终端窗口运行这个程序。
npm start
然后为你想测试的环境按下相应的键,你应该看到完成的应用程序。
简介照片
每个Supabase项目都配置了存储,用于管理大型文件,如 照片和视频。
额外的依赖安装
你将需要一个在你将建立项目的环境下工作的文件选取器,在这个例子中我们将使用 react-native-document-picker。
expo install react-native-document-picker
创建一个上传小组件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。 我们可以从创建一个新的组件开始。
components/Avatar.tsx1import { useState, useEffect } from 'react'
2import { supabase } from '../lib/supabase'
3import { StyleSheet, View, Alert, Image, Button } from 'react-native'
4import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
5
6interface Props {
7 size: number
8 url: string | null
9 onUpload: (filePath: string) => void
10}
11
12export default function Avatar({ url, size = 150, onUpload }: Props) {
13 const [uploading, setUploading] = useState(false)
14 const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
15 const avatarSize = { height: size, width: size }
16
17 useEffect(() => {
18 if (url) downloadImage(url)
19 }, [url])
20
21 async function downloadImage(path: string) {
22 try {
23 const { data, error } = await supabase.storage.from('avatars').download(path)
24
25 if (error) {
26 throw error
27 }
28
29 const fr = new FileReader()
30 fr.readAsDataURL(data)
31 fr.onload = () => {
32 setAvatarUrl(fr.result as string)
33 }
34 } catch (error) {
35 if (error instanceof Error) {
36 console.log('Error downloading image: ', error.message)
37 }
38 }
39 }
40
41 async function uploadAvatar() {
42 try {
43 setUploading(true)
44
45 const file = await DocumentPicker.pickSingle({
46 presentationStyle: 'fullScreen',
47 copyTo: 'cachesDirectory',
48 type: types.images,
49 mode: 'open',
50 })
51
52 const photo = {
53 uri: file.fileCopyUri,
54 type: file.type,
55 name: file.name,
56 }
57
58 const formData = new FormData()
59 formData.append('file', photo)
60
61 const fileExt = file.name.split('.').pop()
62 const filePath = `${Math.random()}.${fileExt}`
63
64 let { error } = await supabase.storage.from('avatars').upload(filePath, formData)
65
66 if (error) {
67 throw error
68 }
69
70 onUpload(filePath)
71 } catch (error) {
72 if (isCancel(error)) {
73 console.warn('cancelled')
74 // User cancelled the picker, exit any dialogs or menus and move on
75 } else if (isInProgress(error)) {
76 console.warn('multiple pickers were opened, only the last will be considered')
77 } else if (error instanceof Error) {
78 Alert.alert(error.message)
79 } else {
80 throw error
81 }
82 } finally {
83 setUploading(false)
84 }
85 }
86
87 return (
88 <View>
89 {avatarUrl ? (
90 <Image
91 source={{ uri: avatarUrl }}
92 accessibilityLabel="Avatar"
93 style={[avatarSize, styles.avatar, styles.image]}
94 />
95 ) : (
96 <View style={[avatarSize, styles.avatar, styles.noImage]} />
97 )}
98 <View>
99 <Button
100 title={uploading ? 'Uploading ...' : 'Upload'}
101 onPress={uploadAvatar}
102 disabled={uploading}
103 />
104 </View>
105 </View>
106 )
107}
108
109const styles = StyleSheet.create({
110 avatar: {
111 borderRadius: 5,
112 overflow: 'hidden',
113 maxWidth: '100%',
114 },
115 image: {
116 objectFit: 'cover',
117 paddingTop: 0,
118 },
119 noImage: {
120 backgroundColor: '#333',
121 border: '1px solid rgb(200, 200, 200)',
122 borderRadius: 5,
123 },
124})
添加新的小组件
然后我们就可以把小部件添加到账户页面。
components/Account.tsx1// Import the new component 2import Avatar from './Avatar' 3 4// ... 5 return ( 6 <View> 7 {/* Add to the body */} 8 <View> 9 <Avatar 10 size={200} 11 url={avatarUrl} 12 onUpload={(url: string) => { 13 setAvatarUrl(url) 14 updateProfile({ username, website, avatar_url: url }) 15 }} 16 /> 17 </View> 18 {/* ... */} 19 </View> 20 ) 21} 22// ...
现在你需要运行预编译命令,使应用程序在你选择的平台上工作。
expo prebuild
下一步
在这个阶段,你已经有了一个功能完备的应用程序!
- 有问题吗?在此提问
- 请登录MemFire Cloud