入门

使用 Svelte 构建用户管理应用


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

Supabase User Management example

项目设置#

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

创建项目#

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

设置数据库 schema#

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

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

获取 API 详细信息#

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

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

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

构建应用#

开始从头构建 Svelte 应用。

初始化 Svelte 应用#

您可以使用 Vite Svelte TypeScript 模板来初始化一个名为 supabase-svelte 的应用

1
npm create vite@latest supabase-svelte -- --template svelte-ts
2
cd supabase-svelte
3
npm install

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

1
npm install @supabase/supabase-js

最后,将环境变量保存在一个 .env 文件中。您只需要之前复制的 API URL 和密钥 即可

1
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
2
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY

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

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

应用样式 (可选)#

可选地,更新 CSS 文件 src/app.css 以使应用看起来更美观。您可以在 GitHub 上找到此文件的完整内容。

设置登录组件#

设置一个 Svelte 组件来管理登录和注册。它使用 Magic Links,因此用户无需使用密码即可通过电子邮件登录。

src/lib/Auth.svelte
1
<script lang="ts">
2
import { supabase } from "../supabaseClient";
3
4
let loading = $state(false);
5
let email = $state("");
6
7
const handleLogin = async () => {
8
try {
9
loading = true;
10
const { error } = await supabase.auth.signInWithOtp({ email });
11
if (error) throw error;
12
alert("Check your email for login link!");
13
} catch (error) {
14
if (error instanceof Error) {
15
alert(error.message);
16
}
17
} finally {
18
loading = false;
19
}
20
};
21
</script>
22
23
<div class="row flex-center flex">
24
<div class="col-6 form-widget" aria-live="polite">
25
<h1 class="header">Supabase + Svelte</h1>
26
<p class="description">Sign in via magic link with your email below</p>
27
<form class="form-widget" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
28
<div>
29
<label for="email">Email</label>
30
<input
31
id="email"
32
class="inputField"
33
type="email"
34
placeholder="Your email"
35
bind:value={email}
36
/>
37
</div>
38
<div>
39
<button
40
type="submit"
41
class="button block"
42
aria-live="polite"
43
disabled={loading}
44
>
45
<span>{loading ? "Loading" : "Send magic link"}</span>
46
</button>
47
</div>
48
</form>
49
</div>
50
</div>
查看源代码

账户页面#

用户登录后,允许他们编辑个人资料详细信息并管理他们的帐户。为此创建一个名为 Account.svelte 的新组件。

1
<script lang="ts">
2
import { onMount } from "svelte";
3
import type { AuthSession } from "@supabase/supabase-js";
4
import { supabase } from "../supabaseClient";
5
6
// ...
7
8
interface Props {
9
session: AuthSession;
10
}
11
12
let { session }: Props = $props();
13
14
// ...
15
16
let username = $state<string | null>(null);
17
let website = $state<string | null>(null);
18
let avatarUrl = $state<string | null>(null);
19
20
onMount(() => {
21
getProfile();
22
});
23
24
const getProfile = async () => {
25
try {
26
loading = true;
27
const { user } = session;
28
29
const { data, error, status } = await supabase
30
.from("profiles")
31
.select("username, website, avatar_url")
32
.eq("id", user.id)
33
.single();
34
35
if (error && status !== 406) throw error;
36
37
// ...
38
39
40
if (data) {
41
username = data.username;
42
website = data.website;
43
avatarUrl = data.avatar_url;
44
}
45
} catch (error) {
46
if (error instanceof Error) {
47
alert(error.message);
48
}
49
} finally {
50
loading = false;
51
}
52
};
53
54
const updateProfile = async () => {
55
try {
56
loading = true;
57
const { user } = session;
58
59
60
// ...
61
62
id: user.id,
63
username,
64
website,
65
avatar_url: avatarUrl,
66
updated_at: new Date().toISOString(),
67
};
68
69
const { error } = await supabase.from("profiles").upsert(updates);
70
71
if (error) {
72
throw error;
73
}
74
} catch (error) {
75
if (error instanceof Error) {
76
alert(error.message);
77
}
78
} finally {
79
loading = false;
80
}
81
82
// ...
83
84
</script>
85
86
<form onsubmit={(e) => { e.preventDefault(); updateProfile(); }} class="form-widget">
87
<div>Email: {session.user.email}</div>
88
<div>
89
<Avatar bind:url={avatarUrl} size={150} onupload={updateProfile} />
90
<label for="username">Name</label>
91
<input id="username" type="text" bind:value={username} />
92
</div>
93
<div>
94
<label for="website">Website</label>
95
<input id="website" type="text" bind:value={website} />
96
</div>
97
<div>
98
<button type="submit" class="button primary block" disabled={loading}>
99
{loading ? "Saving ..." : "Update profile"}
100
</button>
101
</div>
102
<button
103
type="button"
104
class="button block"
105
onclick={() => supabase.auth.signOut()}
106
>
107
Sign Out
108
</button>
109
</form>
查看源代码

启动!#

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

src/App.svelte
1
<script lang="ts">
2
import { onMount } from 'svelte'
3
import { supabase } from './supabaseClient'
4
import type { AuthSession } from '@supabase/supabase-js'
5
import Account from './lib/Account.svelte'
6
import Auth from './lib/Auth.svelte'
7
8
let session = $state<AuthSession | null>(null)
9
10
onMount(() => {
11
supabase.auth.getSession().then(({ data }) => {
12
session = data.session
13
})
14
15
supabase.auth.onAuthStateChange((_event, _session) => {
16
session = _session
17
})
18
})
19
</script>
20
21
<div class="container" style="padding: 50px 0 100px 0">
22
{#if !session}
23
<Auth />
24
{:else}
25
<Account {session} />
26
{/if}
27
</div>
查看源代码

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

1
npm run dev

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

Supabase Svelte

奖励:个人资料照片#

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

创建一个上传小部件#

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

src/lib/Avatar.svelte
1
<script lang="ts">
2
import { supabase } from "../supabaseClient";
3
4
interface Props {
5
size: number;
6
url?: string | null;
7
onupload?: () => void;
8
}
9
10
let { size, url = $bindable(null), onupload }: Props = $props();
11
12
let avatarUrl = $state<string | null>(null);
13
let uploading = $state(false);
14
let files = $state<FileList>();
15
16
const downloadImage = async (path: string) => {
17
try {
18
const { data, error } = await supabase.storage
19
.from("avatars")
20
.download(path);
21
22
if (error) {
23
throw error;
24
}
25
26
const url = URL.createObjectURL(data);
27
avatarUrl = url;
28
} catch (error) {
29
if (error instanceof Error) {
30
console.log("Error downloading image: ", error.message);
31
}
32
}
33
};
34
35
const uploadAvatar = async () => {
36
try {
37
uploading = true;
38
39
if (!files || files.length === 0) {
40
throw new Error("You must select an image to upload.");
41
}
42
43
const file = files[0];
44
const fileExt = file.name.split(".").pop();
45
const filePath = `${Math.random()}.${fileExt}`;
46
47
const { error } = await supabase.storage
48
.from("avatars")
49
.upload(filePath, file);
50
51
if (error) {
52
throw error;
53
}
54
55
url = filePath;
56
onupload?.();
57
} catch (error) {
58
if (error instanceof Error) {
59
alert(error.message);
60
}
61
} finally {
62
uploading = false;
63
}
64
};
65
66
$effect(() => {
67
if (url) downloadImage(url);
68
});
69
</script>
70
71
<div style="width: {size}px" aria-live="polite">
72
{#if avatarUrl}
73
<img
74
src={avatarUrl}
75
alt={avatarUrl ? "Avatar" : "No image"}
76
class="avatar image"
77
style="height: {size}px, width: {size}px"
78
/>
79
{:else}
80
<div class="avatar no-image" style="height: {size}px, width: {size}px"></div>
81
{/if}
82
<div style="width: {size}px">
83
<label class="button primary block" for="single">
84
{uploading ? "Uploading ..." : "Upload avatar"}
85
</label>
86
<span style="display:none">
87
<input
88
type="file"
89
id="single"
90
accept="image/*"
91
bind:files
92
onchange={uploadAvatar}
93
disabled={uploading}
94
/>
95
</span>
96
</div>
97
</div>
查看源代码

添加新的小部件#

然后您可以将小部件添加到 Account 页面

1
<script lang="ts">
2
3
// ...
4
5
import Avatar from "./Avatar.svelte";
6
7
// ...
8
9
} finally {
10
loading = false;
11
}
12
13
// ...
14
15
};
16
17
// ...
18
19
</div>
20
<button
21
type="button"
22
class="button block"
23
onclick={() => supabase.auth.signOut()}
24
>
25
Sign Out
26
</button>
27
</form>
查看源代码

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