快速入门: SvelteKit

介绍

这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。

  • MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
  • MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
  • MemFire Cloud 存储:用户可以上传照片。
  • 行级安全:数据受到保护,个人只能访问自己的数据。
  • 即时APIs。当你创建你的数据库表时,API将自动生成。

在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。

Supabase用户管理实例

GitHub#

如果你在阅读指南时遇到困难,请参考此版本

项目设置

在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。

创建一个项目

  1. 进入MemFire Cloud
  2. 点击 "新项目"。
  3. 输入你的项目细节。
  4. 等待新数据库的启动。

设置数据库模式

现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。

  1. 进入仪表版中的SQL编辑器页面。
  2. 点击 用户管理的模板。
  3. 点击 运行

获取API密钥#

现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。 我们只需要从API设置中获得URL和anon密钥。

  1. 进入仪表板中的设置页面。
  2. 单击侧边栏中的API
  3. 在这个页面上找到你的APIURLanonservice_role键。

构建应用程序

让我们从头开始构建Svelte应用程序。

初始化一个Svelte应用程序#

我们可以使用SvelteKit骨架项目来初始化 一个名为 supabase-sveltekit的应用程序(本教程中你不需要TypeScript、ESLint、Prettier或Playwright)。

npm init svelte@next supabase-sveltekit
cd supabase-sveltekit
npm install

然后让我们安装唯一的额外依赖:supabase-js

npm install @supabase/supabase-js

最后,我们要把环境变量保存在.env中。 我们所需要的是SUPABASE_URL和你[早些时候]复制的SUPABASE_KEY键(#get-the-api-keys)。

.env
PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"

现在我们已经有了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量将被暴露在 在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全

src/lib/supabaseClient.ts
1import { createClient } from '@supabase/auth-helpers-sveltekit'
2import { env } from '$env/dynamic/public'
3
4export const supabase = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)

还有一个可选的步骤是更新CSS文件public/global.css以使应用程序看起来漂亮。 你可以找到这个文件的全部内容这里

Supabase 认证帮助程序#

SvelteKit是一个高度通用的框架,在构建时提供预渲染(SSG),在请求时提供服务器端渲染(SSR),API路由等。

在所有这些不同的环境中对你的用户进行认证是很有挑战性的,这就是为什么我们创建了Supabase Auth Helpers来使SvelteKit内的用户管理和数据获取尽可能简单。

安装SvelteKit的Auth助手。

npm install @supabase/auth-helpers-sveltekit

更新你的src/routes/+layout.svelte

src/routes/+layout.svelte
1<script lang="ts">
2  import { supabase } from '$lib/supabaseClient'
3  import { invalidate } from '$app/navigation'
4  import { onMount } from 'svelte'
5  import './styles.css'
6
7  onMount(() => {
8    const {
9      data: { subscription },
10    } = supabase.auth.onAuthStateChange(() => {
11      invalidate('supabase:auth')
12    })
13
14    return () => {
15      subscription.unsubscribe()
16    }
17  })
18</script>
19
20<div class="container" style="padding: 50px 0 100px 0">
21  <slot />
22</div>

创建一个新的src/routes/+layout.ts文件,在客户端处理会话。

src/routes/+layout.ts
1import type { LayoutLoad } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3
4export const load: LayoutLoad = async (event) => {
5  const { session } = await getSupabase(event)
6  return { session }
7}

创建一个新的src/routes/+layout.server.ts文件,在服务器端处理会话。

src/routes/+layout.server.ts
1import type { LayoutServerLoad } from './$types'
2import { getServerSession } from '@supabase/auth-helpers-sveltekit'
3
4export const load: LayoutServerLoad = async (event) => {
5  return {
6    session: await getServerSession(event),
7  }
8}

请确保创建src/hooks.client.tssrc/hooks.server.ts,以便在客户端和服务器端启动auth帮助器。

src/hooks.client.ts
1import '$lib/supabaseClient'
src/hooks.server.ts
1import '$lib/supabaseClient'

设置一个登录组件

让我们建立一个Svelte组件来管理登录和注册。我们将使用Magic Links,这样用户就可以用他们的电子邮件登录,而无需使用密码。

src/routes/Auth.svelte
1<script lang="ts">
2  import { supabase } from '$lib/supabaseClient'
3
4  let loading = false
5  let email: string
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 the 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<form class="row flex-center flex" on:submit|preventDefault="{handleLogin}">
24  <div class="col-6 form-widget">
25    <h1 class="header">Supabase + SvelteKit</h1>
26    <p class="description">Sign in via magic link with your email below</p>
27    <div>
28      <input class="inputField" type="email" placeholder="Your email" bind:value="{email}" />
29    </div>
30    <div>
31      <input type="submit" class="button block" value={loading ? 'Loading' : 'Send magic link'}
32      disabled={loading} />
33    </div>
34  </div>
35</form>

账户组件

在用户登录后,他们需要能够编辑他们的个人资料细节和管理他们的账户。 创建一个新的Account.svelte组件来处理这个功能。

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

启动

现在我们有了所有的组件,让我们更新src/routes/+page.svelte

src/routes/+page.svelte
1<script>
2  import { page } from '$app/stores'
3  import Account from './Account.svelte'
4  import Auth from './Auth.svelte'
5</script>
6
7<svelte:head>
8  <title>Supabase + SvelteKit</title>
9  <meta name="description" content="SvelteKit using supabase-js v2" />
10</svelte:head>
11
12{#if !$page.data.session}
13<Auth />
14{:else}
15<Account session="{$page.data.session}" />
16{/if}

一旦完成,在终端窗口运行这个程序。

npm run dev

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

Supabase Svelte

个人照片

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

创建一个上传小组件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。我们可以从创建一个新的组件开始。

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

添加新的小组件

然后我们就可以把这个小部件添加到账号页面:

src/routes/Account.svelte
1<script>
2  // Import the new component
3  import Avatar from './Avatar.svelte'
4</script>
5
6<form use:getProfile class="form-widget" on:submit|preventDefault="{updateProfile}">
7  <!-- Add to body -->
8  <Avatar bind:url="{avatarUrl}" size="{10}" on:upload="{updateProfile}" />
9
10  <!-- Other form elements -->
11</form>

下一步

在这个阶段,你已经有了一个功能完备的应用程序!