快速入门: SolidJS
介绍
这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。
- 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
键。
构建应用程序
让我们开始从头开始构建SolidJS应用程序。
初始化一个SolidJS应用程序#
我们可以使用Degit来初始化一个名为supabase-solid
的应用程序。
npx degit solidjs/templates/ts supabase-solid cd supabase-solid
然后让我们安装唯一的额外依赖:supabase-js
npm install @supabase/supabase-js
最后,我们要把环境变量保存在.env
中。
我们所需要的是API URL和你[早些时候]复制的anon
密钥(#get-theapi-keys)。
.envVITE_SUPABASE_URL=YOUR_SUPABASE_URL VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
现在我们已经有了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量将被暴露在 在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全。
src/supabaseClient.jsx1import { createClient } from '@supabase/supabase-js' 2 3const supabaseUrl = import.meta.env.VITE_SUPABASE_URL 4const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY 5 6export const supabase = createClient(supabaseUrl, supabaseAnonKey)
还有一个可选的步骤是更新CSS文件src/index.css
以使应用程序看起来漂亮。
你可以找到这个文件的全部内容这里
设置一个登录组件
让我们设置一个 SolidJS 组件来管理登录和注册。我们将使用Magic Links,因此用户可以用他们的电子邮件登录,而无需使用密码。
src/Auth.tsx1import { createSignal } from 'solid-js'
2import { supabase } from './supabaseClient'
3
4export default function Auth() {
5 const [loading, setLoading] = createSignal(false)
6 const [email, setEmail] = createSignal('')
7
8 const handleLogin = async (e: SubmitEvent) => {
9 e.preventDefault()
10
11 try {
12 setLoading(true)
13 const { error } = await supabase.auth.signInWithOtp({ email: email() })
14 if (error) throw error
15 alert('Check your email for the login link!')
16 } catch (error) {
17 if (error instanceof Error) {
18 alert(error.message)
19 }
20 } finally {
21 setLoading(false)
22 }
23 }
24
25 return (
26 <div class="row flex-center flex">
27 <div class="col-6 form-widget" aria-live="polite">
28 <h1 class="header">Supabase + SolidJS</h1>
29 <p class="description">Sign in via magic link with your email below</p>
30 <form class="form-widget" onSubmit={handleLogin}>
31 <div>
32 <label for="email">Email</label>
33 <input
34 id="email"
35 class="inputField"
36 type="email"
37 placeholder="Your email"
38 value={email()}
39 onChange={(e) => setEmail(e.currentTarget.value)}
40 />
41 </div>
42 <div>
43 <button type="submit" class="button block" aria-live="polite">
44 {loading() ? <span>Loading</span> : <span>Send magic link</span>}
45 </button>
46 </div>
47 </form>
48 </div>
49 </div>
50 )
51}
账号页面
在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。
让我们为它创建一个新的组件,叫做Account.tsx
。
src/Account.tsx1import { AuthSession } from '@supabase/supabase-js'
2import { Component, createEffect, createSignal } from 'solid-js'
3import { supabase } from './supabaseClient'
4
5interface Props {
6 session: AuthSession;
7}
8
9const Account: Component<Props> = ({ session }) => {
10 const [loading, setLoading] = createSignal(true)
11 const [username, setUsername] = (createSignal < string) | (null > null)
12 const [website, setWebsite] = (createSignal < string) | (null > null)
13 const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
14
15 createEffect(() => {
16 getProfile()
17 })
18
19 const getProfile = async () => {
20 try {
21 setLoading(true)
22 const { user } = session
23
24 let { data, error, status } = await supabase
25 .from('profiles')
26 .select(`username, website, avatar_url`)
27 .eq('id', user.id)
28 .single()
29
30 if (error && status !== 406) {
31 throw error
32 }
33
34 if (data) {
35 setUsername(data.username)
36 setWebsite(data.website)
37 setAvatarUrl(data.avatar_url)
38 }
39 } catch (error) {
40 if (error instanceof Error) {
41 alert(error.message)
42 }
43 } finally {
44 setLoading(false)
45 }
46 }
47
48 const updateProfile = async (e: Event) => {
49 e.preventDefault()
50
51 try {
52 setLoading(true)
53 const { user } = session
54
55 const updates = {
56 id: user.id,
57 username: username(),
58 website: website(),
59 avatar_url: avatarUrl(),
60 updated_at: new Date().toISOString(),
61 }
62
63 let { error } = await supabase.from('profiles').upsert(updates)
64
65 if (error) {
66 throw error
67 }
68 } catch (error) {
69 if (error instanceof Error) {
70 alert(error.message)
71 }
72 } finally {
73 setLoading(false)
74 }
75 }
76
77 return (
78 <div aria-live="polite">
79 <form onSubmit={updateProfile} class="form-widget">
80 <div>Email: {session.user.email}</div>
81 <div>
82 <label for="username">Name</label>
83 <input
84 id="username"
85 type="text"
86 value={username() || ''}
87 onChange={(e) => setUsername(e.currentTarget.value)}
88 />
89 </div>
90 <div>
91 <label for="website">Website</label>
92 <input
93 id="website"
94 type="text"
95 value={website() || ''}
96 onChange={(e) => setWebsite(e.currentTarget.value)}
97 />
98 </div>
99 <div>
100 <button type="submit" class="button primary block" disabled={loading()}>
101 {loading() ? 'Saving ...' : 'Update profile'}
102 </button>
103 </div>
104 <button type="button" class="button block" onClick={() => supabase.auth.signOut()}>
105 Sign Out
106 </button>
107 </form>
108 </div>
109 )
110}
111
112export default Account
启动
现在我们有了所有的组件,让我们更新App.tsx
。
src/App.tsx1import { Component, createEffect, createSignal } from 'solid-js'
2import { supabase } from './supabaseClient'
3import { AuthSession } from '@supabase/supabase-js'
4import Account from './Account'
5import Auth from './Auth'
6
7const App: Component = () => {
8 const [session, setSession] = createSignal<AuthSession | null>(null)
9
10 createEffect(() => {
11 supabase.auth.getSession().then(({ data: { session } }) => {
12 setSession(session)
13 })
14
15 supabase.auth.onAuthStateChange((_event, session) => {
16 setSession(session)
17 })
18 })
19
20 return (
21 <div class="container" style={{ padding: '50px 0 100px 0' }}>
22 {!session() ? <Auth /> : <Account session={session()!} />}
23 </div>
24 )
25}
26
27export default App
一旦完成,在终端窗口运行这个程序。
npm start
然后打开浏览器到localhost:3000,你应该看到完成的应用程序。
个人照片
每个Supabase项目都配置了存储,用于管理照片和视频等大文件。
创建一个上传小组件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。我们可以从创建一个新的组件开始。
src/Avatar.tsx1import { Component, createEffect, createSignal, JSX } from 'solid-js'
2import { supabase } from './supabaseClient'
3
4interface Props {
5 size: number
6 url: string | null
7 onUpload: (event: Event, filePath: string) => void
8}
9
10const Avatar: Component<Props> = (props) => {
11 const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
12 const [uploading, setUploading] = createSignal(false)
13
14 createEffect(() => {
15 if (props.url) downloadImage(props.url)
16 })
17
18 const downloadImage = async (path: string) => {
19 try {
20 const { data, error } = await supabase.storage.from('avatars').download(path)
21 if (error) {
22 throw error
23 }
24 const url = URL.createObjectURL(data)
25 setAvatarUrl(url)
26 } catch (error) {
27 if (error instanceof Error) {
28 console.log('Error downloading image: ', error.message)
29 }
30 }
31 }
32
33 const uploadAvatar: JSX.EventHandler<HTMLInputElement, Event> = async (event) => {
34 try {
35 setUploading(true)
36
37 const target = event.currentTarget
38 if (!target?.files || target.files.length === 0) {
39 throw new Error('You must select an image to upload.')
40 }
41
42 const file = target.files[0]
43 const fileExt = file.name.split('.').pop()
44 const fileName = `${Math.random()}.${fileExt}`
45 const filePath = `${fileName}`
46
47 let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
48
49 if (uploadError) {
50 throw uploadError
51 }
52
53 props.onUpload(event, filePath)
54 } catch (error) {
55 if (error instanceof Error) {
56 alert(error.message)
57 }
58 } finally {
59 setUploading(false)
60 }
61 }
62
63 return (
64 <div style={{ width: props.size }} aria-live="polite">
65 {avatarUrl() ? (
66 <img
67 src={avatarUrl()!}
68 alt={avatarUrl() ? 'Avatar' : 'No image'}
69 class="avatar image"
70 style={{ height: `${props.size}px`, width: `${props.size}px` }}
71 />
72 ) : (
73 <div
74 class="avatar no-image"
75 style={{ height: `${props.size}px`, width: `${props.size}px` }}
76 />
77 )}
78 <div style={{ width: `${props.size}px` }}>
79 <label class="button primary block" for="single">
80 {uploading() ? 'Uploading ...' : 'Upload avatar'}
81 </label>
82 <span style="display:none">
83 <input
84 type="file"
85 id="single"
86 accept="image/*"
87 onChange={uploadAvatar}
88 disabled={uploading()}
89 />
90 </span>
91 </div>
92 </div>
93 )
94}
95
96export default Avatar
添加新的小组件
然后我们就可以把这个小部件添加到账号页面:
src/Account.tsx1// Import the new component 2import Avatar from './Avatar' 3 4// ... 5 6return ( 7 <form onSubmit={updateProfile} class="form-widget"> 8 {/* Add to the body */} 9 <Avatar 10 url={avatarUrl()} 11 size={150} 12 onUpload={(e: Event, url: string) => { 13 setAvatarUrl(url) 14 updateProfile(e) 15 }} 16 /> 17 {/* ... */} 18 </div> 19)
下一步
在这个阶段,你已经有了一个功能完备的应用程序!
- 有问题吗?在此提问.
- 请登录MemFire Cloud