入门

使用 SolidJS 构建用户管理应用


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

Supabase User Management example

项目设置#

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

创建项目#

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

设置数据库 schema#

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

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

获取 API 详细信息#

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

为此,您需要从 项目 连接 对话框 获取项目 URL 和密钥。

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

构建应用#

从头开始构建 SolidJS 应用。

初始化 SolidJS 应用#

您可以使用 degit 初始化一个名为 supabase-solid 的应用

1
npx degit solidjs/templates/ts supabase-solid
2
cd supabase-solid

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

1
npm install @supabase/supabase-js

最后,将环境变量保存在一个 .env 文件中,其中包含您之前复制的 API URL 和密钥

1
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
2
VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
查看源代码

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

1
import { createClient } from '@supabase/supabase-js'
2
import { Database } from './schema'
3
4
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
5
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
6
7
export const supabase = createClient(supabaseUrl, supabasePublishableKey)
查看源代码

应用样式 (可选)#

一个可选步骤是更新 CSS 文件 src/index.css 以使应用看起来更好。您可以在 此处 找到此文件的完整内容。

设置登录组件#

设置一个 SolidJS 组件来管理登录和注册,使用 Magic Links,以便用户无需使用密码即可使用他们的电子邮件登录。

1
import { Component, createSignal } from 'solid-js'
2
import { supabase } from './supabaseClient'
3
4
const Auth: Component = () => {
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
}
52
53
export default Auth
查看源代码

账户页面#

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

为此创建一个名为 Account.tsx 的新组件。

1
import { Component, createEffect, createSignal } from 'solid-js'
2
3
// ...
4
5
import { supabase } from './supabaseClient'
6
7
interface Props {
8
userId: string
9
userEmail: string | null
10
}
11
12
const Account: Component<Props> = ({ userId, userEmail }) => {
13
const [loading, setLoading] = createSignal(true)
14
const [username, setUsername] = createSignal<string | null>(null)
15
const [website, setWebsite] = createSignal<string | null>(null)
16
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
17
18
createEffect(() => {
19
getProfile()
20
})
21
22
const getProfile = async () => {
23
try {
24
setLoading(true)
25
26
let { data, error, status } = await supabase
27
.from('profiles')
28
.select(`username, website, avatar_url`)
29
.eq('id', userId)
30
.single()
31
32
if (error && status !== 406) {
33
throw error
34
}
35
36
if (data) {
37
setUsername(data.username)
38
setWebsite(data.website)
39
setAvatarUrl(data.avatar_url)
40
}
41
} catch (error) {
42
if (error instanceof Error) {
43
alert(error.message)
44
}
45
} finally {
46
setLoading(false)
47
}
48
}
49
50
const updateProfile = async (e: Event) => {
51
e.preventDefault()
52
53
try {
54
setLoading(true)
55
56
const updates = {
57
id: userId,
58
username: username(),
59
website: website(),
60
avatar_url: avatarUrl(),
61
updated_at: new Date().toISOString(),
62
}
63
64
let { error } = await supabase.from('profiles').upsert(updates)
65
66
if (error) {
67
throw error
68
}
69
} catch (error) {
70
if (error instanceof Error) {
71
alert(error.message)
72
}
73
} finally {
74
setLoading(false)
75
}
76
}
77
78
return (
79
<div aria-live="polite">
80
<form onSubmit={updateProfile} class="form-widget">
81
82
{/* ... */}
83
84
<div>Email: {userEmail}</div>
85
<div>
86
<label for="username">Name</label>
87
<input
88
id="username"
89
type="text"
90
value={username() || ''}
91
onChange={(e) => setUsername(e.currentTarget.value)}
92
/>
93
</div>
94
<div>
95
<label for="website">Website</label>
96
<input
97
id="website"
98
type="text"
99
value={website() || ''}
100
onChange={(e) => setWebsite(e.currentTarget.value)}
101
/>
102
</div>
103
<div>
104
<button type="submit" class="button primary block" disabled={loading()}>
105
{loading() ? 'Saving ...' : 'Update profile'}
106
</button>
107
</div>
108
<button type="button" class="button block" onClick={() => supabase.auth.signOut()}>
109
Sign Out
110
</button>
111
</form>
112
</div>
113
)
114
}
115
116
export default Account
查看源代码

启动!#

现在您已经准备好了所有组件,请更新 App.tsx

1
import { Component, createEffect, createSignal } from 'solid-js'
2
import { supabase } from './supabaseClient'
3
import Account from './Account'
4
import Auth from './Auth'
5
6
const App: Component = () => {
7
const [userId, setUserId] = createSignal<string | null>(null)
8
const [userEmail, setUserEmail] = createSignal<string | null>(null)
9
10
const syncClaims = async () => {
11
const { data } = await supabase.auth.getClaims()
12
setUserId((data?.claims.sub as string) ?? null)
13
setUserEmail((data?.claims.email as string) ?? null)
14
}
15
16
createEffect(() => {
17
syncClaims()
18
19
supabase.auth.onAuthStateChange(() => {
20
syncClaims()
21
})
22
})
23
24
return (
25
<div class="container" style={{ padding: '50px 0 100px 0' }}>
26
{!userId() ? <Auth /> : <Account userId={userId()!} userEmail={userEmail()} />}
27
</div>
28
)
29
}
30
31
export default App
查看源代码

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

1
npm start

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

Supabase SolidJS

奖励:个人资料照片#

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

创建一个上传小部件#

为用户创建一个头像,以便他们可以上传个人资料照片。首先创建一个新组件

1
import { Component, createEffect, createSignal, JSX } from 'solid-js'
2
import { supabase } from './supabaseClient'
3
4
interface Props {
5
size: number
6
url: string | null
7
onUpload: (event: Event, filePath: string) => void
8
}
9
10
const 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}px` }} 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
96
export default Avatar
查看源代码

添加新的小部件#

然后将小部件添加到账户页面

1
import { Component, createEffect, createSignal } from 'solid-js'
2
import Avatar from './Avatar'
3
import { supabase } from './supabaseClient'
4
5
// ...
6
7
return (
8
<div aria-live="polite">
9
<form onSubmit={updateProfile} class="form-widget">
10
<Avatar
11
url={avatarUrl()}
12
size={150}
13
onUpload={(e: Event, url: string) => {
14
setAvatarUrl(url)
15
updateProfile(e)
16
}}
17
/>
18
<div>Email: {userEmail}</div>
19
<div>
20
21
// ...
查看源代码

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