使用Svelte构建用户管理应用
本教程展示如何构建一个基本的用户管理应用程序。该应用程序对用户进行身份验证和识别,将其个人资料信息存储在数据库中,并允许用户登录、更新个人资料详细信息以及上传个人资料照片。该应用程序使用:
- Supabase数据库 - 一个Postgres数据库,用于存储用户数据,并通过行级安全性来保护数据,确保用户只能访问自己的信息。
- Supabase身份验证 - 允许用户注册和登录。
- Supabase存储 - 用户可以上传个人资料照片。
如果在学习本指南时遇到问题,请参考 GitHub上的完整示例。
项目设置
在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。
创建项目
- 在 Supabase 仪表板中创建一个新项目。
- 输入项目详细信息。
- 等待新数据库启动。
设置数据库模式
现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。
获取 API 密钥
既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。
我们只需要从 API 设置中获取项目 URL 和 anon
密钥。
- 前往仪表板中的 API 设置 页面。
- 在该页面找到您的项目
URL
、anon
和service_role
密钥。
构建应用
让我们从零开始构建这个Svelte应用。
初始化Svelte应用
我们可以使用Vite的Svelte TypeScript模板来初始化一个名为supabase-svelte
的应用:
123npm create vite@latest supabase-svelte -- --template svelte-tscd supabase-sveltenpm install
然后安装唯一的额外依赖:supabase-js
1npm install @supabase/supabase-js
最后我们需要将环境变量保存在.env
文件中。
只需要之前获取的API URL和anon
密钥。
12VITE_SUPABASE_URL=YOUR_SUPABASE_URLVITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
现在我们已经配置好了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量会在浏览器端暴露,这完全没问题,因为我们的数据库已经启用了行级安全。
123456import { createClient } from '@supabase/supabase-js'const supabaseUrl = import.meta.env.VITE_SUPABASE_URLconst supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEYexport const supabase = createClient(supabaseUrl, supabaseAnonKey)
应用样式(可选)
可选步骤是更新 CSS 文件 src/app.css
来美化应用外观。
您可以在这里找到该文件的完整内容。
设置登录组件
让我们设置一个 Svelte 组件来管理登录和注册。我们将使用 Magic Links(魔法链接),这样用户无需密码即可通过电子邮件登录。
123456789101112131415161718192021222324252627282930313233343536373839404142434445<script lang="ts"> import { supabase } from '../supabaseClient' let loading = false let email = '' const handleLogin = async () => { try { loading = true const { error } = await supabase.auth.signInWithOtp({ email }) if (error) throw error alert('请检查您的邮箱获取登录链接!') } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { loading = false } }</script><div class="row flex-center flex"> <div class="col-6 form-widget" aria-live="polite"> <h1 class="header">Supabase + Svelte</h1> <p class="description">通过下方邮箱接收魔法链接登录</p> <form class="form-widget" on:submit|preventDefault="{handleLogin}"> <div> <label for="email">邮箱</label> <input id="email" class="inputField" type="email" placeholder="您的邮箱" bind:value="{email}" /> </div> <div> <button type="submit" class="button block" aria-live="polite" disabled="{loading}"> <span>{loading ? '加载中' : '发送魔法链接'}</span> </button> </div> </form> </div></div>
账户页面
用户登录后,我们可以允许他们编辑个人资料信息和管理账户。让我们创建一个名为Account.svelte
的新组件来实现这个功能。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889<script lang="ts"> import { onMount } from 'svelte' import type { AuthSession } from '@supabase/supabase-js' import { supabase } from '../supabaseClient' export let session: AuthSession let loading = false let username: string | null = null let website: string | null = null let avatarUrl: string | null = null onMount(() => { getProfile() }) const getProfile = async () => { try { loading = true const { user } = session const { data, error, status } = await supabase .from('profiles') .select('username, website, avatar_url') .eq('id', user.id) .single() if (error && status !== 406) throw error if (data) { username = data.username website = data.website avatarUrl = data.avatar_url } } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { loading = false } } const updateProfile = async () => { try { loading = true const { user } = session const updates = { id: user.id, username, website, avatar_url: avatarUrl, updated_at: new Date().toISOString(), } const { error } = await supabase.from('profiles').upsert(updates) if (error) { throw error } } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { loading = false } }</script><form on:submit|preventDefault="{updateProfile}" class="form-widget"> <div>邮箱: {session.user.email}</div> <div> <label for="username">用户名</label> <input id="username" type="text" bind:value="{username}" /> </div> <div> <label for="website">网站</label> <input id="website" type="text" bind:value="{website}" /> </div> <div> <button type="submit" class="button primary block" disabled="{loading}"> {loading ? '保存中...' : '更新资料'} </button> </div> <button type="button" class="button block" on:click={() => supabase.auth.signOut()}> 退出登录 </button></form>
启动应用!
现在我们已经准备好了所有组件,让我们更新 App.svelte
文件:
123456789101112131415161718192021222324252627<script lang="ts"> import { onMount } from 'svelte' import { supabase } from './supabaseClient' import type { AuthSession } from '@supabase/supabase-js' import Account from './lib/Account.svelte' import Auth from './lib/Auth.svelte' let session: AuthSession | null onMount(() => { supabase.auth.getSession().then(({ data }) => { session = data.session }) supabase.auth.onAuthStateChange((_event, _session) => { session = _session }) })</script><div class="container" style="padding: 50px 0 100px 0"> {#if !session} <Auth /> {:else} <Account {session} /> {/if}</div>
完成后,在终端窗口运行以下命令:
1npm run dev
然后在浏览器中打开 localhost:5173,您应该能看到完整的应用。
Svelte 使用 Vite,默认端口是 5173
,而 Supabase 使用 3000
端口。要修改 Supabase 的重定向端口,请前往:认证 > 设置
,将 站点 URL
改为 http://localhost:5173/
额外功能:个人头像
每个 Supabase 项目都配置了存储功能,用于管理照片和视频等大型文件。
创建上传组件
让我们为用户创建一个头像上传组件,使他们能够上传个人资料照片。我们可以从创建一个新组件开始:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283<script lang="ts"> import { createEventDispatcher } from 'svelte' import { supabase } from '../supabaseClient' export let size: number export let url: string | null = null let avatarUrl: string | null = null let uploading = false let files: FileList const dispatch = createEventDispatcher() const downloadImage = async (path: string) => { try { const { data, error } = await supabase.storage.from('avatars').download(path) if (error) { throw error } const url = URL.createObjectURL(data) avatarUrl = url } catch (error) { if (error instanceof Error) { console.log('下载图片错误: ', error.message) } } } const uploadAvatar = async () => { try { uploading = true if (!files || files.length === 0) { throw new Error('请选择要上传的图片。') } const file = files[0] const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` const { error } = await supabase.storage.from('avatars').upload(filePath, file) if (error) { throw error } url = filePath dispatch('upload') } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { uploading = false } } $: if (url) downloadImage(url)</script><div style="width: {size}px" aria-live="polite"> {#if avatarUrl} <img src={avatarUrl} alt={avatarUrl ? '头像' : '无图片'} class="avatar image" style="height: {size}px, width: {size}px" /> {:else} <div class="avatar no-image" style="height: {size}px, width: {size}px" /> {/if} <div style="width: {size}px"> <label class="button primary block" for="single"> {uploading ? '上传中...' : '上传头像'} </label> <span style="display:none"> <input type="file" id="single" accept="image/*" bind:files on:change={uploadAvatar} disabled={uploading} /> </span> </div></div>
添加新组件
然后我们可以将组件添加到账户页面:
1234567891011<script lang="ts"> // 导入新组件 import Avatar from './Avatar.svelte'</script><form on:submit|preventDefault="{updateProfile}" class="form-widget"> <!-- 添加到主体 --> <Avatar bind:url="{avatarUrl}" size="{150}" on:upload="{updateProfile}" /> <!-- 其他表单元素 --></form>
至此,您已经拥有一个功能完整的应用程序了!