快速开始

使用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 应用。

初始化 Angular 应用

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

1
2
npx ng new supabase-angular --routing false --style css --standalone falsecd supabase-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 凭证,让我们通过 ng 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
import { Injectable } from '@angular/core'import { AuthChangeEvent, AuthSession, createClient, Session, SupabaseClient, User,} from '@supabase/supabase-js'import { environment } from '../environments/environment'export interface Profile { id?: string username: string website: string avatar_url: string}@Injectable({ providedIn: 'root',})export class SupabaseService { private supabase: SupabaseClient _session: AuthSession | null = null constructor() { this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey) } get session() { this.supabase.auth.getSession().then(({ data }) => { this._session = data.session }) return this._session } profile(user: User) { return this.supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.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() } updateProfile(profile: Profile) { const update = { ...profile, 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) }}

可选地,更新 src/styles.css 来为应用添加样式。

设置登录组件

让我们设置一个Angular组件来管理登录和注册。我们将使用Magic Links(魔法链接),这样用户无需使用密码即可通过电子邮件登录。

使用Angular CLI命令ng g c auth创建一个AuthComponent

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
import { Component } from '@angular/core'import { FormBuilder } from '@angular/forms'import { SupabaseService } from '../supabase.service'@Component({ selector: 'app-auth', templateUrl: './auth.component.html', styleUrls: ['./auth.component.css'],})export class AuthComponent { loading = false signInForm = this.formBuilder.group({ email: '', }) constructor( private readonly supabase: SupabaseService, private readonly formBuilder: FormBuilder ) {} async onSubmit(): Promise<void> { try { this.loading = true const email = this.signInForm.value.email as string const { error } = await this.supabase.signIn(email) if (error) throw error alert('请检查您的邮箱获取登录链接!') } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.signInForm.reset() this.loading = false } }}

账户页面

用户登录后还需要能够编辑个人资料信息和管理账户。使用 Angular CLI 命令 ng g c 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
import { Component, Input, OnInit } from '@angular/core'import { FormBuilder } from '@angular/forms'import { AuthSession } from '@supabase/supabase-js'import { Profile, SupabaseService } from '../supabase.service'@Component({ selector: 'app-account', templateUrl: './account.component.html', styleUrls: ['./account.component.css'],})export class AccountComponent implements OnInit { loading = false profile!: Profile @Input() session!: AuthSession updateProfileForm = this.formBuilder.group({ username: '', website: '', avatar_url: '', }) constructor( private readonly supabase: SupabaseService, private formBuilder: FormBuilder ) {} async ngOnInit(): Promise<void> { await this.getProfile() const { username, website, avatar_url } = this.profile this.updateProfileForm.patchValue({ username, website, avatar_url, }) } async getProfile() { try { this.loading = true const { user } = this.session const { data: profile, error, status } = await this.supabase.profile(user) if (error && status !== 406) { throw error } if (profile) { this.profile = profile } } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.loading = false } } async updateProfile(): Promise<void> { try { this.loading = true const { user } = this.session const username = this.updateProfileForm.value.username as string const website = this.updateProfileForm.value.website as string const avatar_url = this.updateProfileForm.value.avatar_url as string const { error } = await this.supabase.updateProfile({ id: user.id, username, website, avatar_url, }) if (error) throw error } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.loading = false } } async signOut() { await this.supabase.signOut() }}

启动应用!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit } from '@angular/core'import { SupabaseService } from './supabase.service'@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'],})export class AppComponent implements OnInit { title = 'angular-user-management' session = this.supabase.session constructor(private readonly supabase: SupabaseService) {} ngOnInit() { this.supabase.authChanges((_, session) => (this.session = session)) }}

还需要修改 app.module.ts 来引入 @angular/forms 包中的 ReactiveFormsModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { NgModule } from '@angular/core'import { BrowserModule } from '@angular/platform-browser'import { AppComponent } from './app.component'import { AuthComponent } from './auth/auth.component'import { AccountComponent } from './account/account.component'import { ReactiveFormsModule } from '@angular/forms'import { AvatarComponent } from './avatar/avatar.component'@NgModule({ declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent], imports: [BrowserModule, ReactiveFormsModule], providers: [], bootstrap: [AppComponent],})export class AppModule {}

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

1
npm run start

然后在浏览器中打开 localhost:4200,您应该能看到完整的应用。

Supabase Angular

额外功能:个人资料照片

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

创建上传组件

让我们为用户创建一个头像组件,使他们能够上传个人资料照片。使用 Angular CLI 命令 ng g c avatar 创建一个 AvatarComponent

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
import { Component, EventEmitter, Input, Output } from '@angular/core'import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'import { SupabaseService } from '../supabase.service'@Component({ selector: 'app-avatar', templateUrl: './avatar.component.html', styleUrls: ['./avatar.component.css'],})export class AvatarComponent { _avatarUrl: SafeResourceUrl | undefined uploading = false @Input() set avatarUrl(url: string | null) { if (url) { this.downloadImage(url) } } @Output() upload = new EventEmitter<string>() constructor( private readonly supabase: SupabaseService, private readonly dom: DomSanitizer ) {} async downloadImage(path: string) { try { const { data } = await this.supabase.downLoadImage(path) if (data instanceof Blob) { this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data)) } } catch (error) { if (error instanceof Error) { console.error('下载图片错误: ', error.message) } } } async uploadAvatar(event: any) { try { this.uploading = true if (!event.target.files || event.target.files.length === 0) { throw new Error('必须选择要上传的图片') } const file = event.target.files[0] const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` await this.supabase.uploadAvatar(filePath, file) this.upload.emit(filePath) } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.uploading = false } }}

添加新组件

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

1
2
3
4
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget"> <app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar> <!-- 输入字段 --></form>

同时向 AccountComponent 的 TypeScript 文件添加 updateAvatar 函数和 avatarUrl 获取器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component({ selector: 'app-account', templateUrl: './account.component.html', styleUrls: ['./account.component.css'],})export class AccountComponent implements OnInit { // ... get avatarUrl() { return this.updateProfileForm.value.avatar_url as string } async updateAvatar(event: string): Promise<void> { this.updateProfileForm.patchValue({ avatar_url: event, }) await this.updateProfile() } // ...}

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