入门

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


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

Supabase User Management example

项目设置#

在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。

创建项目#

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

设置数据库 schema#

现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。

  1. 转到控制面板中的 SQL 编辑器 页面。
  2. 点击 社区 > 快速入门 选项卡下的 用户管理 Starter
  3. 点击 运行

获取 API 详细信息#

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。

为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。

阅读 API 密钥文档 以全面了解所有密钥类型及其用途。

构建应用#

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

初始化 Ionic Angular 应用#

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

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

然后让我们安装唯一的附加依赖项:supabase-js

1
npm install @supabase/supabase-js

最后,我们希望将环境变量保存在 src/environments/environment.ts 文件中。 我们只需要您之前复制的 API URL 和密钥 此处。 这些变量将在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全

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

现在我们已经准备好了 API 凭据,让我们使用 ionic g s supabase 创建一个 SupabaseService 来初始化 Supabase 客户端并实现与 Supabase API 通信的功能。

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

设置登录路由#

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

1
import { Component, OnInit } from '@angular/core'
2
import { 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
})
33
export class LoginPage {
34
email = ''
35
36
constructor(private readonly supabase: SupabaseService) {}
37
38
async handleLogin(event: any) {
39
event.preventDefault()
40
const loader = await this.supabase.createLoader()
41
await loader.present()
42
try {
43
const { error } = await this.supabase.signIn(this.email)
44
if (error) {
45
throw error
46
}
47
await loader.dismiss()
48
await this.supabase.createNotice('Check your email for the login link!')
49
} catch (error: any) {
50
await loader.dismiss()
51
await this.supabase.createNotice(error.error_description || error.message)
52
}
53
}
54
}

账户页面#

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

1
import { Component, OnInit } from '@angular/core'
2
import { Router } from '@angular/router'
3
import { 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" name="email" [(ngModel)]="email" readonly></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
})
42
export class AccountPage implements OnInit {
43
profile: Profile = {
44
username: '',
45
avatar_url: '',
46
website: '',
47
}
48
49
email = ''
50
51
constructor(
52
private readonly supabase: SupabaseService,
53
private router: Router
54
) {}
55
ngOnInit() {
56
this.getEmail()
57
this.getProfile()
58
}
59
60
async getEmail() {
61
this.email = await this.supabase.user.then((user) => user?.email || '')
62
}
63
64
async getProfile() {
65
try {
66
const { data: profile, error, status } = await this.supabase.profile
67
if (error && status !== 406) {
68
throw error
69
}
70
if (profile) {
71
this.profile = profile
72
}
73
} catch (error: any) {
74
alert(error.message)
75
}
76
}
77
78
async updateProfile(avatar_url: string = '') {
79
const loader = await this.supabase.createLoader()
80
await loader.present()
81
try {
82
const { error } = await this.supabase.updateProfile({ ...this.profile, avatar_url })
83
if (error) {
84
throw error
85
}
86
await loader.dismiss()
87
await this.supabase.createNotice('Profile updated!')
88
} catch (error: any) {
89
await loader.dismiss()
90
await this.supabase.createNotice(error.message)
91
}
92
}
93
94
async signOut() {
95
console.log('testing?')
96
await this.supabase.signOut()
97
this.router.navigate(['/'], { replaceUrl: true })
98
}
99
}

启动!#

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

1
import { Component } from '@angular/core'
2
import { Router } from '@angular/router'
3
import { 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
})
14
export class AppComponent {
15
constructor(
16
private supabase: SupabaseService,
17
private router: Router
18
) {
19
this.supabase.authChanges((_, session) => {
20
console.log(session)
21
if (session?.user) {
22
this.router.navigate(['/account'])
23
}
24
})
25
}
26
}

然后更新 AppRoutingModule

1
import { NgModule } from '@angular/core'
2
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
3
4
const 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
})
23
export class AppRoutingModule {}

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

1
ionic serve

浏览器将自动打开以显示该应用。

Supabase Angular

奖励:个人资料照片#

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

创建一个上传小部件#

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

首先,安装两个包以与用户的摄像头交互。

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

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

Ionic PWA 元素是一个配套包,它将为没有用户界面的某些浏览器 API 提供自定义的 Ionic UI 进行填充。

安装了这些包后,我们可以更新我们的 main.ts 以包含一个额外的引导调用,用于 Ionic PWA Elements。

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

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

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

添加新的小部件#

然后,我们可以在 AccountComponent HTML 模板之上添加小部件

1
template: `
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
`

到此为止,您已经拥有一个功能齐全的应用程序!

另请参阅#