暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Supabase和Ionic Vue快速入门指南

原创 eternity 2022-07-25
1314

本例提供了构建简单用户管理应用程序的步骤(从头开始!)使用Supabase和Ionic Vue。它包括:

  • Supabase数据库:用于存储用户数据的Postgres数据库。

  • Supabase Auth:用户可以使用魔法链接登录(没有密码,只有电子邮件)。

  • Supabase存储:用户可以上传照片。

  • 行级安全:数据受到保护,因此个人只能访问自己的数据。

  • 即时API:API将在创建数据库表时自动生成。

在本指南结束时,您将拥有一个应用程序,允许用户登录并更新一些基本配置文件详细信息:
微信图片_20220722152828.png

单击此按钮,应用程序将:

  • 在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,你就会看到完整的应用程序。
微信图片_20220722153730.png

额外所得:个人资料照片

每个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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论