使用 Ionic Angular 构建用户管理应用
本教程演示如何构建一个基本的用户管理应用。该应用对用户进行身份验证和识别,将他们的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料详细信息以及上传个人资料照片。该应用使用
- Supabase 数据库 - 一个用于存储用户数据的 Postgres 数据库,以及 行级别安全,以保护数据并确保用户只能访问他们自己的信息。
- Supabase Auth - 允许用户注册和登录。
- Supabase Storage - 允许用户上传个人资料照片。

如果在阅读本指南时遇到问题,请参考 GitHub 上的完整示例。
项目设置#
在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。
创建项目#
- 在 Supabase 控制面板中创建一个新项目。
- 输入您的项目详细信息。
- 等待新的数据库启动。
设置数据库 schema#
现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。
获取 API 详细信息#
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。
为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。
API 密钥的更改
Supabase 正在更改密钥的工作方式,以提高项目安全性和开发人员体验。您可以 阅读完整的公告,但在过渡期间,您可以使用当前的 anon 和 service_role 密钥以及新的可发布密钥,格式为 sb_publishable_xxx,它将取代旧的密钥。
在大多数情况下,您可以从 项目的 Connect 对话框中获取正确的密钥,但如果您需要特定的密钥,可以在 项目设置页面的 API 密钥部分中找到所有密钥
- 对于旧版密钥,从 旧版 API 密钥 选项卡中复制
anon密钥用于客户端操作,并复制service_role密钥用于服务器端操作。 - 对于新密钥,打开 API 密钥 选项卡,如果您还没有可发布密钥,请单击 创建新的 API 密钥,并复制 可发布密钥 部分中的值。
阅读 API 密钥文档 以全面了解所有密钥类型及其用途。
构建应用#
让我们从头开始构建 Angular 应用。
初始化 Ionic Angular 应用#
我们可以使用 Ionic CLI 初始化一个名为 supabase-ionic-angular 的应用
1npm install -g @ionic/cli2ionic start supabase-ionic-angular blank --type angular3cd supabase-ionic-angular然后让我们安装唯一的附加依赖项:supabase-js
1npm install @supabase/supabase-js最后,我们希望将环境变量保存在 src/environments/environment.ts 文件中。 我们只需要您之前复制的 API URL 和密钥 此处。 这些变量将在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全。
1export const = {2 : false,3 : 'YOUR_SUPABASE_URL',4 : 'YOUR_SUPABASE_KEY',5}现在我们已经准备好了 API 凭据,让我们使用 ionic g s supabase 创建一个 SupabaseService 来初始化 Supabase 客户端并实现与 Supabase API 通信的功能。
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'56export interface Profile {7 username: string8 website: string9 avatar_url: string10}1112@Injectable({13 providedIn: 'root',14})15export class SupabaseService {16 private supabase: SupabaseClient1718 constructor(19 private loadingCtrl: LoadingController,20 private toastCtrl: ToastController21 ) {22 this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)23 }2425 get user() {26 return this.supabase.auth.getUser().then(({ data }) => data?.user)27 }2829 get session() {30 return this.supabase.auth.getSession().then(({ data }) => data?.session)31 }3233 get profile() {34 return this.user35 .then((user) => user?.id)36 .then((id) =>37 this.supabase.from('profiles').select(`username, website, avatar_url`).eq('id', id).single()38 )39 }4041 authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {42 return this.supabase.auth.onAuthStateChange(callback)43 }4445 signIn(email: string) {46 return this.supabase.auth.signInWithOtp({ email })47 }4849 signOut() {50 return this.supabase.auth.signOut()51 }5253 async updateProfile(profile: Profile) {54 const user = await this.user55 const update = {56 ...profile,57 id: user?.id,58 updated_at: new Date(),59 }6061 return this.supabase.from('profiles').upsert(update)62 }6364 downLoadImage(path: string) {65 return this.supabase.storage.from('avatars').download(path)66 }6768 uploadAvatar(filePath: string, file: File) {69 return this.supabase.storage.from('avatars').upload(filePath, file)70 }7172 async createNotice(message: string) {73 const toast = await this.toastCtrl.create({ message, duration: 5000 })74 await toast.present()75 }7677 createLoader() {78 return this.loadingCtrl.create()79 }80}设置登录路由#
让我们设置一个路由来管理登录和注册。 我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。 使用 ionic g page login Ionic CLI 命令创建一个 LoginPage。
本指南将内联显示模板,但示例应用将具有 templateUrl。
1import { Component, OnInit } from '@angular/core'2import { SupabaseService } from '../supabase.service'34@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>1213 <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 {34 email = ''3536 constructor(private readonly supabase: SupabaseService) {}3738 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 error46 }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。
1import { Component, OnInit } from '@angular/core'2import { Router } from '@angular/router'3import { Profile, SupabaseService } from '../supabase.service'45@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>1314 <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>2021 <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>2526 <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>3435 <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 }4849 email = ''5051 constructor(52 private readonly supabase: SupabaseService,53 private router: Router54 ) {}55 ngOnInit() {56 this.getEmail()57 this.getProfile()58 }5960 async getEmail() {61 this.email = await this.supabase.user.then((user) => user?.email || '')62 }6364 async getProfile() {65 try {66 const { data: profile, error, status } = await this.supabase.profile67 if (error && status !== 406) {68 throw error69 }70 if (profile) {71 this.profile = profile72 }73 } catch (error: any) {74 alert(error.message)75 }76 }7778 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 error85 }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 }9394 async signOut() {95 console.log('testing?')96 await this.supabase.signOut()97 this.router.navigate(['/'], { replaceUrl: true })98 }99}启动!#
现在我们已经准备好所有组件,让我们更新 AppComponent
1import { Component } from '@angular/core'2import { Router } from '@angular/router'3import { SupabaseService } from './supabase.service'45@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(16 private supabase: SupabaseService,17 private router: Router18 ) {19 this.supabase.authChanges((_, session) => {20 console.log(session)21 if (session?.user) {22 this.router.navigate(['/account'])23 }24 })25 }26}然后更新 AppRoutingModule
1import { NgModule } from '@angular/core'2import { PreloadAllModules, RouterModule, Routes } from '@angular/router'34const 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]1415@NgModule({16 imports: [17 RouterModule.forRoot(routes, {18 preloadingStrategy: PreloadAllModules,19 }),20 ],21 exports: [RouterModule],22})23export class AppRoutingModule {}完成此操作后,在终端窗口中运行以下命令
1ionic serve浏览器将自动打开以显示该应用。

奖励:个人资料照片#
每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。
创建一个上传小部件#
让我们为用户创建一个头像,以便他们可以上传个人资料照片。
首先,安装两个包以与用户的摄像头交互。
1npm install @ionic/pwa-elements @capacitor/cameraCapacitor 是 Ionic 的一个跨平台原生运行时,它使 Web 应用能够通过应用商店部署并提供对原生设备 API 的访问。
Ionic PWA 元素是一个配套包,它将为没有用户界面的某些浏览器 API 提供自定义的 Ionic UI 进行填充。
安装了这些包后,我们可以更新我们的 main.ts 以包含一个额外的引导调用,用于 Ionic PWA Elements。
1import { enableProdMode } from '@angular/core'2import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'34import { AppModule } from './app/app.module'5import { environment } from './environments/environment'67import { defineCustomElements } from '@ionic/pwa-elements/loader'8defineCustomElements(window)910if (environment.production) {11 enableProdMode()12}13platformBrowserDynamic()14 .bootstrapModule(AppModule)15 .catch((err) => console.log(err))然后使用此 Ionic CLI 命令创建一个 AvatarComponent
1ionic g component avatar --module=/src/app/account/account.module.ts --create-module1import { 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'5import { addIcons } from 'ionicons'6import { 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})49export class AvatarComponent {50 _avatarUrl: SafeResourceUrl | undefined51 uploading = false5253 @Input()54 set avatarUrl(url: string | undefined) {55 if (url) {56 this.downloadImage(url)57 }58 }5960 @Output() upload = new EventEmitter<string>()6162 constructor(63 private readonly supabase: SupabaseService,64 private readonly dom: DomSanitizer65 ) {66 addIcons({ person })67 }6869 async downloadImage(path: string) {70 try {71 const { data, error } = await this.supabase.downLoadImage(path)72 if (error) {73 throw error74 }75 this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data!))76 } catch (error: any) {77 console.error('Error downloading image: ', error.message)78 }79 }8081 async uploadAvatar() {82 const loader = await this.supabase.createLoader()83 try {84 const photo = await Camera.getPhoto({85 resultType: CameraResultType.DataUrl,86 })8788 const file = await fetch(photo.dataUrl!)89 .then((res) => res.blob())90 .then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))9192 const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`9394 await loader.present()95 const { error } = await this.supabase.uploadAvatar(fileName, file)9697 if (error) {98 throw error99 }100101 this.upload.emit(fileName)102 } catch (error: any) {103 this.supabase.createNotice(error.message)104 } finally {105 loader.dismiss()106 }107 }108}添加新的小部件#
然后,我们可以在 AccountComponent HTML 模板之上添加小部件
1template: `2<ion-header>3 <ion-toolbar>4 <ion-title>Account</ion-title>5 </ion-toolbar>6</ion-header>78<ion-content>9 <app-avatar10 [avatarUrl]="this.profile?.avatar_url"11 (upload)="updateProfile($event)"12 ></app-avatar>1314<!-- input fields -->15`到此为止,您已经拥有一个功能齐全的应用程序!