快速入门: Nuxt 3

介绍

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

  • 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键。

构建应用程序

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

初始化一个Nuxt 3应用程序#

我们可以使用nuxi init来创建一个名为nuxt-user-management的应用程序。

npx nuxi init nuxt-user-management

cd nuxt-user-management

然后让我们安装唯一的额外依赖:NuxtSupabase。我们只需要将NuxtSupabase作为一个开发依赖项导入。

npm install @nuxtjs/supabase --save-dev

最后,我们要把环境变量保存在.env中。 我们所需要的是API URL和你[早些时候]复制的anon密钥(#get-theapi-keys)。

.env
SUPABASE_URL="YOUR_SUPABASE_URL"
SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY"

这些变量将暴露在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全。 关于NuxtSupabase的神奇之处在于,为了开始使用Supabase,我们只需要设置环境变量即可。 不需要初始化Supabase。该库将自动处理它。

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

nuxt.config.ts
1import { defineNuxtConfig } from 'nuxt'
2
3// https://v3.nuxtjs.org/api/configuration/nuxt.config
4export default defineNuxtConfig({
5  modules: ['@nuxtjs/supabase'],
6  css: ['@/assets/main.css'],
7})

设置Auth组件#

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

/components/Auth.vue
1<template>
2  <form class="row flex-center flex" @submit.prevent="handleLogin">
3    <div class="col-6 form-widget">
4      <h1 class="header">Supabase + Nuxt 3</h1>
5      <p class="description">Sign in via magic link with your email below</p>
6      <div>
7        <input class="inputField" type="email" placeholder="Your email" v-model="email" />
8      </div>
9      <div>
10        <input
11          type="submit"
12          class="button block"
13          :value="loading ? 'Loading' : 'Send magic link'"
14          :disabled="loading"
15        />
16      </div>
17    </div>
18  </form>
19</template>
20
21<script setup>
22  const supabase = useSupabaseClient()
23
24  const loading = ref(false)
25  const email = ref('')
26  const handleLogin = async () => {
27    try {
28      loading.value = true
29      const { error } = await supabase.auth.signInWithOtp({ email: email.value })
30      if (error) throw error
31      alert('Check your email for the login link!')
32    } catch (error) {
33      alert(error.error_description || error.message)
34    } finally {
35      loading.value = false
36    }
37  }
38</script>

用户状态

要访问用户信息,请使用Supabase Nuxt模块提供的可组合的useSupabaseUser

账号组件

在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。 让我们为其创建一个新的组件,名为Account.vue

components/Account.vue
1<template>
2  <form class="form-widget" @submit.prevent="updateProfile">
3    <div>
4      <label for="email">Email</label>
5      <input id="email" type="text" :value="user.email" disabled />
6    </div>
7    <div>
8      <label for="username">Username</label>
9      <input id="username" type="text" v-model="username" />
10    </div>
11    <div>
12      <label for="website">Website</label>
13      <input id="website" type="website" v-model="website" />
14    </div>
15
16    <div>
17      <input
18        type="submit"
19        class="button primary block"
20        :value="loading ? 'Loading ...' : 'Update'"
21        :disabled="loading"
22      />
23    </div>
24
25    <div>
26      <button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
27    </div>
28  </form>
29</template>
30
31<script setup>
32  const supabase = useSupabaseClient()
33
34  const loading = ref(true)
35  const username = ref('')
36  const website = ref('')
37  const avatar_path = ref('')
38
39
40  loading.value = true
41  const user = useSupabaseUser();
42  let { data } = await supabase
43      .from('profiles')
44      .select(`username, website, avatar_url`)
45      .eq('id', user.value.id)
46      .single()
47  if (data) {
48      username.value = data.username
49      website.value = data.website
50      avatar_path.value = data.avatar_url
51  }
52  loading.value = false
53
54  async function updateProfile() {
55      try {
56          loading.value = true
57          const user = useSupabaseUser();
58          const updates = {
59              id: user.value.id,
60              username: username.value,
61              website: website.value,
62              avatar_url: avatar_path.value,
63              updated_at: new Date(),
64          }
65          let { error } = await supabase.from('profiles').upsert(updates, {
66              returning: 'minimal', // Don't return the value after inserting
67          })
68          if (error) throw error
69      } catch (error) {
70          alert(error.message)
71      } finally {
72          loading.value = false
73      }
74  }
75
76  async function signOut() {
77      try {
78          loading.value = true
79          let { error } = await supabase.auth.signOut()
80          if (error) throw error
81          user.value = null
82      } catch (error) {
83          alert(error.message)
84      } finally {
85          loading.value = false
86      }
87  }
88</script>

启动

现在我们已经有了所有的组件,让我们来更新app.vue

app.vue
1<template>
2  <div class="container" style="padding: 50px 0 100px 0">
3    <Account v-if="user" />
4    <Auth v-else />
5  </div>
6</template>
7
8<script setup>
9  const user = useSupabaseUser()
10</script>

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

npm run dev

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

Supabase Nuxt 3

个人照片

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

创建一个上传小组件

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

components/Avatar.vue
1<template>
2  <div>
3    <img
4      v-if="src"
5      :src="src"
6      alt="Avatar"
7      class="avatar image"
8      style="width: 10em; height: 10em;"
9    />
10    <div v-else class="avatar no-image" :style="{ height: size, width: size }" />
11
12    <div style="width: 10em; position: relative;">
13      <label class="button primary block" for="single">
14        {{ uploading ? "Uploading ..." : "Upload" }}
15      </label>
16      <input
17        style="position: absolute; visibility: hidden;"
18        type="file"
19        id="single"
20        accept="image/*"
21        @change="uploadAvatar"
22        :disabled="uploading"
23      />
24    </div>
25  </div>
26</template>
27
28<script setup>
29  const props = defineProps(['path'])
30  const { path } = toRefs(props)
31
32  const emit = defineEmits(['update:path', 'upload'])
33
34  const supabase = useSupabaseClient()
35
36  const uploading = ref(false)
37  const src = ref('')
38  const files = ref()
39  const downloadImage = async () => {
40    try {
41      const { data, error } = await supabase.storage.from('avatars').download(path.value)
42      if (error) throw error
43      src.value = URL.createObjectURL(data)
44    } catch (error) {
45      console.error('Error downloading image: ', error.message)
46    }
47  }
48
49  const uploadAvatar = async (evt) => {
50    files.value = evt.target.files
51    try {
52      uploading.value = true
53      if (!files.value || files.value.length === 0) {
54        throw new Error('You must select an image to upload.')
55      }
56      const file = files.value[0]
57      const fileExt = file.name.split('.').pop()
58      const fileName = `${Math.random()}.${fileExt}`
59      const filePath = `${fileName}`
60      let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
61      if (uploadError) throw uploadError
62      emit('update:path', filePath)
63      emit('upload')
64    } catch (error) {
65      alert(error.message)
66    } finally {
67      uploading.value = false
68    }
69  }
70
71  downloadImage()
72
73  watch(path, () => {
74    if (path.value) {
75      downloadImage()
76    }
77  })
78</script>

添加新的小组件

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

components/Account.vue
1<template>
2  <form class="form-widget" @submit.prevent="updateProfile">
3    <Avatar v-model:path="avatar_path" @upload="updateProfile" />
4    <div>
5      <label for="email">Email</label>
6      <input id="email" type="text" :value="user.email" disabled />
7    </div>
8    <div>
9      <label for="username">Name</label>
10      <input id="username" type="text" v-model="username" />
11    </div>
12    <div>
13      <label for="website">Website</label>
14      <input id="website" type="website" v-model="website" />
15    </div>
16
17    <div>
18      <input
19        type="submit"
20        class="button primary block"
21        :value="loading ? 'Loading ...' : 'Update'"
22        :disabled="loading"
23      />
24    </div>
25
26    <div>
27      <button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
28    </div>
29  </form>
30</template>
31
32<script setup>
33  const supabase = useSupabaseClient()
34
35  const loading = ref(true)
36  const username = ref('')
37  const website = ref('')
38  const avatar_path = ref('')
39
40
41  loading.value = true
42  const user = useSupabaseUser();
43  let { data } = await supabase
44      .from('profiles')
45      .select(`username, website, avatar_url`)
46      .eq('id', user.value.id)
47      .single()
48  if (data) {
49      username.value = data.username
50      website.value = data.website
51      avatar_path.value = data.avatar_url
52  }
53  loading.value = false
54
55  async function updateProfile() {
56      try {
57          loading.value = true
58          const user = useSupabaseUser();
59          const updates = {
60              id: user.value.id,
61              username: username.value,
62              website: website.value,
63              avatar_url: avatar_path.value,
64              updated_at: new Date(),
65          }
66          let { error } = await supabase.from('profiles').upsert(updates, {
67              returning: 'minimal', // Don't return the value after inserting
68          })
69          if (error) throw error
70      } catch (error) {
71          alert(error.message)
72      } finally {
73          loading.value = false
74      }
75  }
76
77  async function signOut() {
78      try {
79          loading.value = true
80          let { error } = await supabase.auth.signOut()
81          if (error) throw error
82      } catch (error) {
83          alert(error.message)
84      } finally {
85          loading.value = false
86      }
87  }
88</script>

你现在应该可以向Supabase Storage上传一张个人照片。

下一步

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