快速开始

使用Ionic Angular构建用户管理应用


本教程展示如何构建一个基本的用户管理应用程序。该应用程序对用户进行身份验证和识别,将其个人资料信息存储在数据库中,并允许用户登录、更新个人资料详细信息以及上传个人资料照片。该应用程序使用:

Supabase 用户管理示例

项目设置

在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。

创建项目

  1. 在 Supabase 仪表板中创建一个新项目
  2. 输入项目详细信息。
  3. 等待新数据库启动。

设置数据库模式

现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。

  1. 转到仪表板中的SQL 编辑器页面。
  2. 点击 用户管理入门
  3. 点击 运行
1
2
3
4
supabase link --project-ref <project-id># 您可以从项目的仪表板 URL 获取 <project-id>:https://supabase.com/dashboard/project/<project-id>supabase db pull

获取 API 密钥

既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。 我们只需要从 API 设置中获取项目 URL 和 anon 密钥。

  1. 前往仪表板中的 API 设置 页面。
  2. 在该页面找到您的项目 URLanonservice_role 密钥。

构建应用

让我们从头开始构建这个Angular应用。

初始化 Ionic Angular 应用

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

1
2
3
npm install -g @ionic/cliionic start supabase-ionic-angular blank --type angularcd supabase-ionic-angular

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

1
npm install @supabase/supabase-js

最后,我们需要将环境变量保存在 src/environments/environment.ts 文件中。只需要之前复制的 API URL 和 anon 密钥(见前文)。这些变量会在浏览器中暴露,这完全没问题,因为我们的数据库已启用行级安全

1
2
3
4
5
export const = { : false, : 'YOUR_SUPABASE_URL', : 'YOUR_SUPABASE_KEY',}

现在我们已经配置好 API 凭证,让我们用 ionic g s supabase 创建一个 SupabaseService 来初始化 Supabase 客户端,并实现与 Supabase API 通信的函数。

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

设置登录路由

让我们设置一个路由来管理登录和注册。我们将使用 Magic Links(魔法链接),这样用户无需密码即可通过电子邮件登录。使用 Ionic CLI 命令 ionic g page login 创建一个 LoginPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { Component, OnInit } from '@angular/core'import { SupabaseService } from '../supabase.service'@Component({ selector: 'app-login', template: ` <ion-header> <ion-toolbar> <ion-title>登录</ion-title> </ion-toolbar> </ion-header> <ion-content> <div class="ion-padding"> <h1>Supabase + Ionic Angular</h1> <p>通过下方邮箱输入魔法链接登录</p> </div> <ion-list inset="true"> <form (ngSubmit)="handleLogin($event)"> <ion-item> <ion-label position="stacked">邮箱</ion-label> <ion-input [(ngModel)]="email" name="email" autocomplete type="email"></ion-input> </ion-item> <div class="ion-text-center"> <ion-button type="submit" fill="clear">登录</ion-button> </div> </form> </ion-list> </ion-content> `, styleUrls: ['./login.page.scss'],})export class LoginPage { email = '' constructor(private readonly supabase: SupabaseService) {} async handleLogin(event: any) { event.preventDefault() const loader = await this.supabase.createLoader() await loader.present() try { const { error } = await this.supabase.signIn(this.email) if (error) { throw error } await loader.dismiss() await this.supabase.createNotice('请检查您的邮箱获取登录链接!') } catch (error: any) { await loader.dismiss() await this.supabase.createNotice(error.error_description || error.message) } }}

账户页面

用户登录后,我们可以允许他们编辑个人资料信息和管理账户。使用 Ionic CLI 命令 ionic g page account 创建一个 AccountComponent

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

启动应用!

现在我们已经准备好了所有组件,让我们更新 AppComponent

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

然后更新 AppRoutingModule

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

完成后,在终端窗口中运行以下命令:

1
ionic serve

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

Supabase Angular

额外功能:个人资料照片

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

创建上传组件

让我们为用户创建一个头像组件,使他们能够上传个人资料照片。

首先安装两个用于与用户相机交互的包:

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

Capacitor 是 Ionic 提供的跨平台原生运行时,允许 Web 应用通过应用商店部署并提供对原生设备 API 的访问。

Ionic PWA Elements 是一个配套包,将为没有用户界面的浏览器 API 提供自定义 Ionic UI 的 polyfill。

安装这些包后,我们可以更新 main.ts 文件以包含对 Ionic PWA Elements 的额外初始化调用。

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

然后使用以下 Ionic CLI 命令创建 AvatarComponent

1
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'import { SupabaseService } from '../supabase.service'import { Camera, CameraResultType } from '@capacitor/camera'import { addIcons } from 'ionicons'import { person } from 'ionicons/icons'@Component({ selector: 'app-avatar', template: ` <div class="avatar_wrapper" (click)="uploadAvatar()"> <img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" /> <ng-template #noAvatar> <ion-icon name="person" class="no-avatar"></ion-icon> </ng-template> </div> `, style: [ ` :host { display: block; margin: auto; min-height: 150px; } :host .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); } :host .avatar_wrapper:hover { cursor: pointer; } :host .avatar_wrapper ion-icon.no-avatar { width: 100%; height: 115%; } :host img { display: block; object-fit: cover; width: 100%; height: 100%; } `, ],})export class AvatarComponent { _avatarUrl: SafeResourceUrl | undefined uploading = false @Input() set avatarUrl(url: string | undefined) { if (url) { this.downloadImage(url) } } @Output() upload = new EventEmitter<string>() constructor( private readonly supabase: SupabaseService, private readonly dom: DomSanitizer ) { addIcons({ person }) } async downloadImage(path: string) { try { const { data, error } = await this.supabase.downLoadImage(path) if (error) { throw error } this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data!)) } catch (error: any) { console.error('下载图片时出错: ', error.message) } } async uploadAvatar() { const loader = await this.supabase.createLoader() try { const photo = await Camera.getPhoto({ resultType: CameraResultType.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}` await loader.present() const { error } = await this.supabase.uploadAvatar(fileName, file) if (error) { throw error } this.upload.emit(fileName) } catch (error: any) { this.supabase.createNotice(error.message) } finally { loader.dismiss() } }}

添加新组件

然后,我们可以在 AccountComponent HTML 模板顶部添加该组件:

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

至此,您已经拥有了一个功能完整的应用程序!

相关阅读