快速入门: Nuxt 3
介绍
这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。
- MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
- MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
- MemFire Cloud 存储:用户可以上传照片。
- 行级安全:数据受到保护,个人只能访问自己的数据。
- 即时APIs。当你创建你的数据库表时,API将自动生成。
在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。
Github#
如果你在阅读指南时遇到困难,请参考此版本。
项目设置
在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。
创建一个项目
- 进入MemFire Cloud。
- 点击 "新项目"。
- 输入你的项目细节。
- 等待新数据库的启动。
设置数据库模式
现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。
- 进入仪表版中的SQL编辑器页面。
- 点击 用户管理的模板。
- 点击 运行。
获取API密钥#
现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。
我们只需要从API设置中获得URL和anon
密钥。
- 进入仪表板中的设置页面。
- 单击侧边栏中的API。
- 在这个页面上找到你的API
URL
、anon
和service_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)。
.envSUPABASE_URL="YOUR_SUPABASE_URL" SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY"
这些变量将暴露在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全。 关于NuxtSupabase的神奇之处在于,为了开始使用Supabase,我们只需要设置环境变量即可。 不需要初始化Supabase。该库将自动处理它。
还有一个可选的步骤是更新CSS文件assets/main.css
以使应用程序看起来漂亮。
你可以找到这个文件的全部内容这里。
nuxt.config.ts1import { 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.vue1<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.vue1<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.vue1<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项目都配置了存储,用于管理照片和视频等大文件。
创建一个上传小组件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。我们可以从创建一个新的组件开始。
components/Avatar.vue1<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.vue1<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上传一张个人照片。
下一步
在这个阶段,你已经有了一个功能完备的应用程序!
- 有问题吗?在此提问.
- 请登录MemFire Cloud