使用Ionic Angular构建用户管理应用
本教程展示如何构建一个基本的用户管理应用程序。该应用程序对用户进行身份验证和识别,将其个人资料信息存储在数据库中,并允许用户登录、更新个人资料详细信息以及上传个人资料照片。该应用程序使用:
- Supabase数据库 - 一个Postgres数据库,用于存储用户数据,并通过行级安全性来保护数据,确保用户只能访问自己的信息。
- Supabase身份验证 - 允许用户注册和登录。
- Supabase存储 - 用户可以上传个人资料照片。
如果在学习本指南时遇到问题,请参考 GitHub上的完整示例。
项目设置
在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。
创建项目
- 在 Supabase 仪表板中创建一个新项目。
- 输入项目详细信息。
- 等待新数据库启动。
设置数据库模式
现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。
获取 API 密钥
既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。
我们只需要从 API 设置中获取项目 URL 和 anon
密钥。
- 前往仪表板中的 API 设置 页面。
- 在该页面找到您的项目
URL
、anon
和service_role
密钥。
构建应用
让我们从头开始构建这个Angular应用。
初始化 Ionic Angular 应用
我们可以使用 Ionic CLI 来初始化一个名为 supabase-ionic-angular
的应用:
123npm install -g @ionic/cliionic start supabase-ionic-angular blank --type angularcd supabase-ionic-angular
然后安装唯一的额外依赖:supabase-js
1npm install @supabase/supabase-js
最后,我们需要将环境变量保存在 src/environments/environment.ts
文件中。只需要之前复制的 API URL 和 anon
密钥(见前文)。这些变量会在浏览器中暴露,这完全没问题,因为我们的数据库已启用行级安全。
12345export const = { : false, : 'YOUR_SUPABASE_URL', : 'YOUR_SUPABASE_KEY',}
现在我们已经配置好 API 凭证,让我们用 ionic g s supabase
创建一个 SupabaseService
来初始化 Supabase 客户端,并实现与 Supabase API 通信的函数。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980import { 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
。
本指南将内联显示模板,但示例应用会使用 templateUrl
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import { 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
。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899import { 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
:
1234567891011121314151617181920212223242526import { 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
:
1234567891011121314151617181920212223import { 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 {}
完成后,在终端窗口中运行以下命令:
1ionic serve
浏览器将自动打开显示应用程序。
额外功能:个人资料照片
每个 Supabase 项目都配置了存储功能,用于管理照片和视频等大型文件。
创建上传组件
让我们为用户创建一个头像组件,使他们能够上传个人资料照片。
首先安装两个用于与用户相机交互的包:
1npm install @ionic/pwa-elements @capacitor/camera
Capacitor 是 Ionic 提供的跨平台原生运行时,允许 Web 应用通过应用商店部署并提供对原生设备 API 的访问。
Ionic PWA Elements 是一个配套包,将为没有用户界面的浏览器 API 提供自定义 Ionic UI 的 polyfill。
安装这些包后,我们可以更新 main.ts
文件以包含对 Ionic PWA Elements 的额外初始化调用。
123456789101112131415import { 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
:
1ionic g component avatar --module=/src/app/account/account.module.ts --create-module
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108import { 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 模板顶部添加该组件:
123456789101112131415template: `<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><!-- 输入字段 -->`
至此,您已经拥有了一个功能完整的应用程序!