Quickstart: Ionic Angular

介绍

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

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

构建应用程序

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

初始化一个Ionic Angular应用程序#

我们可以使用Ionic CLI来初始化 一个名为supabase-ionic-angular的应用程序。

npm install -g @ionic/cli
ionic start supabase-ionic-angular blank --type angular
cd supabase-ionic-angular

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

npm install @supabase/supabase-js

最后我们要在environment.ts文件中保存环境变量。 我们所需要的是API URL和你[早些时候]复制的anon密钥(#get-theapi-keys)。 这些变量将暴露在浏览器上,这完全没有问题,因为我们在数据库上启用了行级安全

environment.ts
1export const environment = {
2  production: false,
3  supabaseUrl: 'YOUR_SUPABASE_URL',
4  supabaseKey: 'YOUR_SUPABASE_KEY',
5}

现在我们有了API凭证,让我们用ionic g s supabase创建一个SupabaseService,以初始化Supabase客户端,并实现与Supabase API通信的功能。

src/app/supabase.service.ts
1import { Injectable } from '@angular/core'
2import { LoadingController, ToastController } from '@ionic/angular'
3import { AuthChangeEvent, createClient, Session, SupabaseClient } from '@supabase/supabase-js'
4import { environment } from '../environments/environment'
5
6export interface Profile {
7  username: string
8  website: string
9  avatar_url: string
10}
11
12@Injectable({
13  providedIn: 'root',
14})
15export class SupabaseService {
16  private supabase: SupabaseClient
17
18  constructor(private loadingCtrl: LoadingController, private toastCtrl: ToastController) {
19    this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
20  }
21
22  get user() {
23    return this.supabase.auth.user()
24  }
25
26  get session() {
27    return this.supabase.auth.session()
28  }
29
30  get profile() {
31    return this.supabase
32      .from('profiles')
33      .select(`username, website, avatar_url`)
34      .eq('id', this.user?.id)
35      .single()
36  }
37
38  authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
39    return this.supabase.auth.onAuthStateChange(callback)
40  }
41
42  signIn(email: string) {
43    return this.supabase.auth.signIn({ email })
44  }
45
46  signOut() {
47    return this.supabase.auth.signOut()
48  }
49
50  updateProfile(profile: Profile) {
51    const update = {
52      ...profile,
53      id: this.user?.id,
54      updated_at: new Date(),
55    }
56
57    return this.supabase.from('profiles').upsert(update, {
58      returning: 'minimal', // Don't return the value after inserting
59    })
60  }
61
62  downLoadImage(path: string) {
63    return this.supabase.storage.from('avatars').download(path)
64  }
65
66  uploadAvatar(filePath: string, file: File) {
67    return this.supabase.storage.from('avatars').upload(filePath, file)
68  }
69
70  async createNotice(message: string) {
71    const toast = await this.toastCtrl.create({ message, duration: 5000 })
72    await toast.present()
73  }
74
75  createLoader() {
76    return this.loadingCtrl.create()
77  }
78}

设置一个登录路由

让我们建立一个路由来管理登录和注册。我们将使用Magic Links,所以用户可以用他们的电子邮件登录,而不需要使用密码。 用ionic g page login Ionic CLI命令创建一个LoginPage

本指南将显示模板的内联,但示例应用程序将有templateUrls

src/app/login/login.page.ts
1import { Component, OnInit } from '@angular/core'
2import { SupabaseService } from '../supabase.service'
3
4@Component({
5  selector: 'app-login',
6  template: `
7    <ion-header>
8      <ion-toolbar>
9        <ion-title>Login</ion-title>
10      </ion-toolbar>
11    </ion-header>
12
13    <ion-content>
14      <div class="ion-padding">
15        <h1>Supabase + Ionic Angular</h1>
16        <p>Sign in via magic link with your email below</p>
17      </div>
18      <ion-list inset="true">
19        <form (ngSubmit)="handleLogin($event)">
20          <ion-item>
21            <ion-label position="stacked">Email</ion-label>
22            <ion-input [(ngModel)]="email" name="email" autocomplete type="email"></ion-input>
23          </ion-item>
24          <div class="ion-text-center">
25            <ion-button type="submit" fill="clear">Login</ion-button>
26          </div>
27        </form>
28      </ion-list>
29    </ion-content>
30  `,
31  styleUrls: ['./login.page.scss'],
32})
33export class LoginPage implements OnInit {
34  email = ''
35  constructor(private readonly supabase: SupabaseService) {}
36
37  ngOnInit() {}
38  async handleLogin(event: any) {
39    event.preventDefault()
40    const loader = await this.supabase.createLoader()
41    await loader.present()
42    try {
43      await this.supabase.signIn(this.email)
44      await loader.dismiss()
45      await this.supabase.createNotice('Check your email for the login link!')
46    } catch (error) {
47      await loader.dismiss()
48      await this.supabase.createNotice(error.error_description || error.message)
49    }
50  }
51}

账号页面

在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。 用ionic g page account Ionic CLI命令创建一个AccountComponent

src/app/account.component.ts
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { Profile, SupabaseService } from '../supabase.service'
4
5@Component({
6  selector: 'app-account',
7  template: `
8    <ion-header>
9      <ion-toolbar>
10        <ion-title>Account</ion-title>
11      </ion-toolbar>
12    </ion-header>
13
14    <ion-content>
15      <form>
16        <ion-item>
17          <ion-label position="stacked">Email</ion-label>
18          <ion-input type="email" [value]="session?.user?.email"></ion-input>
19        </ion-item>
20
21        <ion-item>
22          <ion-label position="stacked">Name</ion-label>
23          <ion-input type="text" name="username" [(ngModel)]="profile.username"></ion-input>
24        </ion-item>
25
26        <ion-item>
27          <ion-label position="stacked">Website</ion-label>
28          <ion-input type="url" name="website" [(ngModel)]="profile.website"></ion-input>
29        </ion-item>
30        <div class="ion-text-center">
31          <ion-button fill="clear" (click)="updateProfile()">Update Profile</ion-button>
32        </div>
33      </form>
34
35      <div class="ion-text-center">
36        <ion-button fill="clear" (click)="signOut()">Log Out</ion-button>
37      </div>
38    </ion-content>
39  `,
40  styleUrls: ['./account.page.scss'],
41})
42export class AccountPage implements OnInit {
43  profile: Profile = {
44    username: '',
45    avatar_url: '',
46    website: '',
47  }
48
49  session = this.supabase.session
50
51  constructor(private readonly supabase: SupabaseService, private router: Router) {}
52  ngOnInit() {
53    this.getProfile()
54  }
55
56  async getProfile() {
57    try {
58      let { data: profile, error, status } = await this.supabase.profile
59      if (error && status !== 406) {
60        throw error
61      }
62      if (profile) {
63        this.profile = profile
64      }
65    } catch (error) {
66      alert(error.message)
67    }
68  }
69
70  async updateProfile(avatar_url: string = '') {
71    const loader = await this.supabase.createLoader()
72    await loader.present()
73    try {
74      await this.supabase.updateProfile({ ...this.profile, avatar_url })
75      await loader.dismiss()
76      await this.supabase.createNotice('Profile updated!')
77    } catch (error) {
78      await this.supabase.createNotice(error.message)
79    }
80  }
81
82  async signOut() {
83    console.log('testing?')
84    await this.supabase.signOut()
85    this.router.navigate(['/'], { replaceUrl: true })
86  }
87}

启动

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

src/app/app.component.ts
1import { Component } from '@angular/core'
2import { Router } from '@angular/router'
3import { SupabaseService } from './supabase.service'
4
5@Component({
6  selector: 'app-root',
7  template: `
8    <ion-app>
9      <ion-router-outlet></ion-router-outlet>
10    </ion-app>
11  `,
12  styleUrls: ['app.component.scss'],
13})
14export class AppComponent {
15  constructor(private supabase: SupabaseService, private router: Router) {
16    this.supabase.authChanges((_, session) => {
17      console.log(session)
18      if (session?.user) {
19        this.router.navigate(['/account'])
20      }
21    })
22  }
23}

然后更新AppRoutingModule

src/app/app.ts"
1import { NgModule } from '@angular/core'
2import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
3
4const routes: Routes = [
5  {
6    path: '/',
7    loadChildren: () => import('./login/login.module').then((m) => m.LoginPageModule),
8  },
9  {
10    path: 'account',
11    loadChildren: () => import('./account/account.module').then((m) => m.AccountPageModule),
12  },
13]
14
15@NgModule({
16  imports: [
17    RouterModule.forRoot(routes, {
18      preloadingStrategy: PreloadAllModules,
19    }),
20  ],
21  exports: [RouterModule],
22})
23export class AppRoutingModule {}

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

ionic serve

浏览器将自动打开,显示该应用程序。

Supabase Angular

个人资料照片

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

创建一个上传小部件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。

首先安装两个包,以便与用户的相机互动。

npm install @ionic/pwa-elements @capacitor/camera

CapacitorJS是Ionic的一个跨平台原生运行时间,它使网络应用通过应用商店部署,并提供对原生deavice API的访问。

Ionic PWA元素是一个配套的软件包,它将对某些不提供用户界面的浏览器API进行聚填,并提供自定义的Ionic UI。

安装了这些包后,我们可以更新我们的main.ts,以包括对Ionic PWA元素的额外引导调用。

src/main.ts
1import { enableProdMode } from '@angular/core'
2import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
3
4import { AppModule } from './app/app.module'
5import { environment } from './environments/environment'
6
7import { defineCustomElements } from '@ionic/pwa-elements/loader'
8defineCustomElements(window)
9
10if (environment.production) {
11  enableProdMode()
12}
13platformBrowserDynamic()
14  .bootstrapModule(AppModule)
15  .catch((err) => console.log(err))

然后用这个Ionic CLI命令创建一个AvatarComponent

 ionic g component avatar --module=/src/app/account/account.module.ts --create-module
src/app/avatar.component.ts
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
3import { SupabaseService } from '../supabase.service'
4import { Camera, CameraResultType } from '@capacitor/camera'
5@Component({
6  selector: 'app-avatar',
7  template: `
8    <div class="avatar_wrapper" (click)="uploadAvatar()">
9      <img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" />
10      <ng-template #noAvatar>
11        <ion-icon name="person" class="no-avatar"></ion-icon>
12      </ng-template>
13    </div>
14  `,
15  style: [
16    `
17    :host {
18       display: block;
19       margin: auto;
20       min-height: 150px;
21    }
22     :host .avatar_wrapper {
23       margin: 16px auto 16px;
24       border-radius: 50%;
25       overflow: hidden;
26       height: 150px;
27       aspect-ratio: 1;
28       background: var(--ion-color-step-50);
29       border: thick solid var(--ion-color-step-200);
30    }
31     :host .avatar_wrapper:hover {
32       cursor: pointer;
33    }
34     :host .avatar_wrapper ion-icon.no-avatar {
35       width: 100%;
36       height: 115%;
37    }
38     :host img {
39       display: block;
40       object-fit: cover;
41       width: 100%;
42       height: 100%;
43    }
44  `,
45  ],
46})
47export class AvatarComponent implements OnInit {
48  _avatarUrl: SafeResourceUrl | undefined
49  uploading = false
50
51  @Input()
52  set avatarUrl(url: string | undefined) {
53    if (url) {
54      this.downloadImage(url)
55    }
56  }
57
58  @Output() upload = new EventEmitter<string>()
59
60  constructor(private readonly supabase: SupabaseService, private readonly dom: DomSanitizer) {}
61
62  ngOnInit() {}
63
64  async downloadImage(path: string) {
65    try {
66      const { data } = await this.supabase.downLoadImage(path)
67      this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
68    } catch (error) {
69      console.error('Error downloading image: ', error.message)
70    }
71  }
72
73  async uploadAvatar() {
74    const loader = await this.supabase.createLoader()
75    try {
76      const photo = await Camera.getPhoto({
77        resultType: CameraResultType.DataUrl,
78      })
79
80      const file = await fetch(photo.dataUrl)
81        .then((res) => res.blob())
82        .then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))
83
84      const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`
85
86      await loader.present()
87      await this.supabase.uploadAvatar(fileName, file)
88
89      this.upload.emit(fileName)
90    } catch (error) {
91      this.supabase.createNotice(error.message)
92    } finally {
93      loader.dismiss()
94    }
95  }
96}

添加新的小组件

然后我们可以在账户组件的html模板上面添加小部件。

src/app/account.component.ts
1template: `
2<ion-header>
3  <ion-toolbar>
4    <ion-title>Account</ion-title>
5  </ion-toolbar>
6</ion-header>
7
8<ion-content>
9  <app-avatar
10    [avatarUrl]="this.profile?.avatar_url"
11    (upload)="updateProfile($event)"
12  ></app-avatar>
13
14<!-- input fields -->
15`

下一步

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

资源