本例提供了构建简单用户管理应用程序的步骤(从头开始!)使用Supabase和Ionic Vue。它包括:
-
Supabase数据库:用于存储用户数据的Postgres数据库。
-
Supabase Auth:用户可以使用魔法链接登录(没有密码,只有电子邮件)。
-
Supabase存储:用户可以上传照片。
-
行级安全:数据受到保护,因此个人只能访问自己的数据。
-
即时API:API将在创建数据库表时自动生成。
在本指南结束时,您将拥有一个应用程序,允许用户登录并更新一些基本配置文件详细信息:

单击此按钮,应用程序将:
-
在Supabase中启动并准备Postgres数据库。
-
在Vercel中启动应用程序。
-
将该示例转移到您自己的GitHub帐户中。
-
使用所有必要的环境变量准备部署的应用程序。
如果你想自己动手,让我们开始吧!
GitHub
每当你在任何时候陷入困境,都要看看这一回购协议。
项目设置
在开始构建之前,我们将设置数据库和API。这就像在Supabase中启动一个新项目,然后在数据库中创建一个“模式”一样简单。
创建项目
1.请访问app.supabase.com。
2.点击“新建项目”。
3.输入项目详细信息。
4.等待新数据库启动。
设置数据库架构
现在我们将设置数据库模式。我们可以在SQL编辑器中使用“User Management Starter”快速启动,也可以从下面复制/粘贴SQL并自己运行。
-- Create a table for public "profiles"
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Realtime!
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
1. Go to the "SQL" section.
2. Click "User Management Starter".
3. Click "Run".
获取API密钥
现在已经创建了一些数据库表,可以使用自动生成的API插入数据了。我们只需要从API设置中获取URL和anon键。
1. Go to the "Settings" section.
2. Click "API" in the sidebar.
3. Find your API URL in this page.
4. Find your "anon" and "service_role" keys on this page.
构建应用程序
让我们从头开始构建Ionic Vue应用程序。
初始化Ionic Vue应用程序
我们可以使用Ionic CLI初始化名为supabase Ionic vue的应用程序:
npm install -g @ionic/cli
ionic start supabase-ionic-vue blank --type vue
cd supabase-ionic-vue
然后,让我们安装唯一的附加依赖项:supabase js
npm install @supabase/supabase-js
最后,我们想把环境变量保存在一个.env中。我们只需要API URL和您之前复制的anon密钥。
.env
VUE_APP_SUPABASE_URL=YOUR_SUPABASE_URL
VUE_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
现在我们已经有了API凭据,让我们创建一个帮助文件来初始化Supabase客户端。这些变量将在浏览器上公开,这很好,因为我们在数据库上启用了行级安全性。
src/supabase.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.VUE_APP_SUPABASE_URL as string;
const supabaseAnonKey = process.env.VUE_APP_SUPABASE_ANON_KEY as string;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
设置登录路径
让我们设置一个Vue组件来管理登录和注册。我们将使用魔法链接,这样用户就可以不用密码就用他们的电子邮件登录。
/src/views/Login.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<h1>Supabase + Ionic Vue</h1>
<p>Sign in via magic link with your email below</p>
</div>
<ion-list inset="true">
<form @submit.prevent="handleLogin">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input
v-model="email"
name="email"
autocomplete
type="email"
></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button type="submit" fill="clear">Login</ion-button>
</div>
</form>
</ion-list>
<p>{{email}}</p>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { supabase } from '../supabase';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonList,
IonItem,
IonLabel,
IonInput,
IonButton,
toastController,
loadingController,
} from '@ionic/vue';
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'LoginPage',
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonList,
IonItem,
IonLabel,
IonInput,
IonButton,
},
setup() {
const email = ref('');
const handleLogin = async () => {
const loader = await loadingController.create({});
const toast = await toastController.create({ duration: 5000 });
try {
await loader.present();
const { error } = await supabase.auth.signIn({ email: email.value });
if (error) throw error;
toast.message = 'Check your email for the login link!';
await toast.present();
} catch (error: any) {
toast.message = error.error_description || error.message;
await toast.present();
} finally {
await loader.dismiss();
}
};
return { handleLogin, email };
},
});
</script>
账户页面
用户登录后,我们可以允许他们编辑其个人资料详细信息并管理其帐户。
让我们为Account.vue创建一个新组件。
src/views/Account.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form @submit.prevent="updateProfile">
<ion-item>
<ion-label>
<p>Email</p>
<p>{{ session?.user?.email }}</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input
type="text"
name="username"
v-model="profile.username"
></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Website</ion-label>
<ion-input
type="url"
name="website"
v-model="profile.website"
></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button fill="clear" type="submit">Update Profile</ion-button>
</div>
</form>
<div class="ion-text-center">
<ion-button fill="clear" @click="signOut">Log Out</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { store } from '@/store';
import { supabase } from '@/supabase';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
toastController,
loadingController,
IonInput,
IonItem,
IonButton,
IonLabel,
} from '@ionic/vue';
import { User } from '@supabase/supabase-js';
import { defineComponent, onMounted, ref } from 'vue';
export default defineComponent({
name: 'AccountPage',
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonInput,
IonItem,
IonButton,
IonLabel,
},
setup() {
const session = ref(supabase.auth.session());
const profile = ref({
username: '',
website: '',
avatar_url: '',
});
const user = store.user as User;
async function getProfile() {
const loader = await loadingController.create({});
const toast = await toastController.create({ duration: 5000 });
await loader.present();
try {
let { 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) {
console.log(data)
profile.value = {
username: data.username,
website: data.website,
avatar_url: data.avatar_url,
};
}
} catch (error: any) {
toast.message = error.message;
await toast.present();
} finally {
await loader.dismiss();
}
}
const updateProfile = async () => {
const loader = await loadingController.create({});
const toast = await toastController.create({ duration: 5000 });
try {
await loader.present();
const updates = {
id: user.id,
...profile.value,
updated_at: new Date(),
};
//
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
});
//
if (error) throw error;
} catch (error: any) {
toast.message = error.message;
await toast.present();
} finally {
await loader.dismiss();
}
};
async function signOut() {
const loader = await loadingController.create({});
const toast = await toastController.create({ duration: 5000 });
await loader.present();
try {
let { error } = await supabase.auth.signOut();
if (error) throw error;
} catch (error: any) {
toast.message = error.message;
await toast.present();
} finally {
await loader.dismiss();
}
}
onMounted(() => {
getProfile();
});
return { signOut, profile, session, updateProfile };
},
});
</script>
登录
现在我们已经准备好了所有组件,让我们更新应用程序.vue和我们的路线:
src/router.index.ts
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import LoginPage from '../views/Login.vue';
import AccountPage from '../views/Account.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Login',
component: LoginPage
},
{
path: '/account',
name: 'Account',
component: AccountPage
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
src/App.vue
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
<script lang="ts">
import { IonApp, IonRouterOutlet, useIonRouter } from '@ionic/vue';
import { defineComponent } from 'vue';
import { store } from './store';
import { supabase } from './supabase';
export default defineComponent({
name: 'App',
components: {
IonApp,
IonRouterOutlet
},
setup(){
const router = useIonRouter();
store.user = supabase.auth.user() ?? {};
supabase.auth.onAuthStateChange((_, session) => {
store.user = session?.user ?? {}
if(session?.user) {
router.replace('/account');
}
})
}
});
</script>
完成后,在终端窗口中运行此操作:
ionic serve
然后打开浏览器到localhost:3000,你就会看到完整的应用程序。

额外所得:个人资料照片
每个Supabase项目都配置了用于管理照片和视频等大文件的存储。
创建上载小部件
首先,安装两个软件包,以便与用户的摄像机进行交互。
npm install @ionic/pwa-elements @capacitor/camera
CapacitorJS是爱奥尼亚的跨平台本机运行时,可通过应用商店部署web应用,并提供对本机设备API的访问。
Ionic PWA elements是一个配套软件包,它将聚合某些浏览器API,这些API不提供自定义Ionic UI的用户界面。
安装了这些软件包后,我们可以更新我们的主。ts包括对Ionic PWA元件的额外自举调用。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router';
import { IonicVue } from '@ionic/vue';
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/ionic.bundle.css';
/* Theme variables */
import './theme/variables.css';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
defineCustomElements(window);
const app = createApp(App)
.use(IonicVue)
.use(router);
router.isReady().then(() => {
app.mount('#app');
});
然后创建一个AvatarComponent。
src/components/Avatar.vue
<template>
<div class="avatar">
<div class="avatar_wrapper" @click="uploadAvatar">
<img v-if="avatarUrl" :src="avatarUrl" />
<ion-icon v-else name="person" class="no-avatar"></ion-icon>
</div>
</div>
</template>
<script lang="ts">
import { ref, toRefs, watch, defineComponent } from 'vue';
import { supabase } from '../supabase';
import { Camera, CameraResultType } from '@capacitor/camera';
import { IonIcon } from '@ionic/vue';
import { person } from 'ionicons/icons';
export default defineComponent({
name: 'AppAvatar',
props: { path: String },
emits: ['upload', 'update:path'],
components: { IonIcon },
setup(prop, { emit }) {
const { path } = toRefs(prop);
const avatarUrl = ref('');
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path.value);
if (error) throw error;
avatarUrl.value = URL.createObjectURL(data!);
} catch (error: any) {
console.error('Error downloading image: ', error.message);
}
};
const uploadAvatar = async () => {
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
});
if (photo.dataUrl) {
const file = await fetch(photo.dataUrl)
.then((res) => res.blob())
.then(
(blob) =>
new File([blob], 'my-file', { type: `image/${photo.format}` })
);
const fileName = `${Math.random()}-${new Date().getTime()}.${
photo.format
}`;
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, file);
if (uploadError) {
throw uploadError;
}
emit('update:path', fileName);
emit('upload');
}
} catch (error) {
console.log(error);
}
};
watch(path, () => {
if (path.value) downloadImage();
});
return { avatarUrl, uploadAvatar, person };
},
});
</script>
<style>
.avatar {
display: block;
margin: auto;
min-height: 150px;
}
.avatar .avatar_wrapper {
margin: 16px auto 16px;
border-radius: 50%;
overflow: hidden;
height: 150px;
aspect-ratio: 1;
background: var(--ion-color-step-50);
border: thick solid var(--ion-color-step-200);
}
.avatar .avatar_wrapper:hover {
cursor: pointer;
}
.avatar .avatar_wrapper ion-icon.no-avatar {
width: 100%;
height: 115%;
}
.avatar img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
</style>
添加新的小部件
然后我们可以将小部件添加到帐户页面:
src/views/Account.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<avatar v-model:path="profile.avatar_url" @upload="updateProfile"></avatar>
...
</template>
下一步
在这个阶段,您有一个功能齐全的应用程序!
-
有问题吗?在这里提问。
-
登录:app.supabase.com
原文标题:Supabase and Ionic Vue Quickstart Guide
原文作者:Rory Wilding
原文链接:https://dzone.com/articles/supabase-and-ionic-vue-quickstart-guide




