Quickstart: Ionic Angular
介绍
这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。
- MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
- MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
- MemFire Cloud 存储:用户可以上传照片。
- 行级安全:数据受到保护,个人只能访问自己的数据。
- 即时APIs。当你创建你的数据库表时,API将自动生成。
在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。
GitHub#
如果你在阅读指南时遇到困难,请参考此版本
项目设置
在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。
创建一个项目
- 进入MemFire Cloud。
- 点击 "新项目"。
- 输入你的项目细节。
- 等待新数据库的启动。
设置数据库模式
现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。
- 进入仪表版中的SQL编辑器页面。
- 点击 用户管理的模板。
- 点击 运行。
获取API密钥#
现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。
我们只需要从API设置中获得URL和anon
密钥。
- 进入仪表板中的设置页面。
- 单击侧边栏中的API。
- 在这个页面上找到你的API
URL
、anon
和service_role
键。
构建应用程序
让我们开始从头开始构建Angular应用程序。
初始化一个Ionic Angular应用程序#
我们可以使用Ionic CLI来初始化
一个名为supabase-ionic-angular
的应用程序。
npm install -g @ionic/cli ionic start supabase-ionic-angular blank --type angular cd supabase-ionic-angular
然后让我们安装唯一的额外依赖:supabase-js
npm install @supabase/supabase-js
最后我们要在environment.ts
文件中保存环境变量。
我们所需要的是API URL和你[早些时候]复制的anon
密钥(#get-theapi-keys)。
这些变量将暴露在浏览器上,这完全没有问题,因为我们在数据库上启用了行级安全。
environment.ts1export const environment = {
2 production: false,
3 supabaseUrl: 'YOUR_SUPABASE_URL',
4 supabaseKey: 'YOUR_SUPABASE_KEY',
5}
现在我们有了API凭证,让我们用ionic g s supabase
创建一个SupabaseService,以初始化Supabase客户端,并实现与Supabase API通信的功能。
src/app/supabase.service.ts1import { Injectable } from '@angular/core'
2import { LoadingController, ToastController } from '@ionic/angular'
3import { AuthChangeEvent, createClient, Session, SupabaseClient } from '@supabase/supabase-js'
4import { environment } from '../environments/environment'
5
6export interface Profile {
7 username: string
8 website: string
9 avatar_url: string
10}
11
12@Injectable({
13 providedIn: 'root',
14})
15export class SupabaseService {
16 private supabase: SupabaseClient
17
18 constructor(private loadingCtrl: LoadingController, private toastCtrl: ToastController) {
19 this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
20 }
21
22 get user() {
23 return this.supabase.auth.user()
24 }
25
26 get session() {
27 return this.supabase.auth.session()
28 }
29
30 get profile() {
31 return this.supabase
32 .from('profiles')
33 .select(`username, website, avatar_url`)
34 .eq('id', this.user?.id)
35 .single()
36 }
37
38 authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
39 return this.supabase.auth.onAuthStateChange(callback)
40 }
41
42 signIn(email: string) {
43 return this.supabase.auth.signIn({ email })
44 }
45
46 signOut() {
47 return this.supabase.auth.signOut()
48 }
49
50 updateProfile(profile: Profile) {
51 const update = {
52 ...profile,
53 id: this.user?.id,
54 updated_at: new Date(),
55 }
56
57 return this.supabase.from('profiles').upsert(update, {
58 returning: 'minimal', // Don't return the value after inserting
59 })
60 }
61
62 downLoadImage(path: string) {
63 return this.supabase.storage.from('avatars').download(path)
64 }
65
66 uploadAvatar(filePath: string, file: File) {
67 return this.supabase.storage.from('avatars').upload(filePath, file)
68 }
69
70 async createNotice(message: string) {
71 const toast = await this.toastCtrl.create({ message, duration: 5000 })
72 await toast.present()
73 }
74
75 createLoader() {
76 return this.loadingCtrl.create()
77 }
78}
设置一个登录路由
让我们建立一个路由来管理登录和注册。我们将使用Magic Links,所以用户可以用他们的电子邮件登录,而不需要使用密码。
用ionic g page login
Ionic CLI命令创建一个LoginPage。
本指南将显示模板的内联,但示例应用程序将有templateUrls
src/app/login/login.page.ts1import { Component, OnInit } from '@angular/core'
2import { 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})
33export class LoginPage implements OnInit {
34 email = ''
35 constructor(private readonly supabase: SupabaseService) {}
36
37 ngOnInit() {}
38 async handleLogin(event: any) {
39 event.preventDefault()
40 const loader = await this.supabase.createLoader()
41 await loader.present()
42 try {
43 await this.supabase.signIn(this.email)
44 await loader.dismiss()
45 await this.supabase.createNotice('Check your email for the login link!')
46 } catch (error) {
47 await loader.dismiss()
48 await this.supabase.createNotice(error.error_description || error.message)
49 }
50 }
51}
账号页面
在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。
用ionic g page account
Ionic CLI命令创建一个AccountComponent。
src/app/account.component.ts1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { 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" [value]="session?.user?.email"></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})
42export class AccountPage implements OnInit {
43 profile: Profile = {
44 username: '',
45 avatar_url: '',
46 website: '',
47 }
48
49 session = this.supabase.session
50
51 constructor(private readonly supabase: SupabaseService, private router: Router) {}
52 ngOnInit() {
53 this.getProfile()
54 }
55
56 async getProfile() {
57 try {
58 let { data: profile, error, status } = await this.supabase.profile
59 if (error && status !== 406) {
60 throw error
61 }
62 if (profile) {
63 this.profile = profile
64 }
65 } catch (error) {
66 alert(error.message)
67 }
68 }
69
70 async updateProfile(avatar_url: string = '') {
71 const loader = await this.supabase.createLoader()
72 await loader.present()
73 try {
74 await this.supabase.updateProfile({ ...this.profile, avatar_url })
75 await loader.dismiss()
76 await this.supabase.createNotice('Profile updated!')
77 } catch (error) {
78 await this.supabase.createNotice(error.message)
79 }
80 }
81
82 async signOut() {
83 console.log('testing?')
84 await this.supabase.signOut()
85 this.router.navigate(['/'], { replaceUrl: true })
86 }
87}
启动
现在我们已经有了所有的组件,让我们来更新AppComponent。
src/app/app.component.ts1import { Component } from '@angular/core'
2import { Router } from '@angular/router'
3import { 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})
14export class AppComponent {
15 constructor(private supabase: SupabaseService, private router: Router) {
16 this.supabase.authChanges((_, session) => {
17 console.log(session)
18 if (session?.user) {
19 this.router.navigate(['/account'])
20 }
21 })
22 }
23}
然后更新AppRoutingModule
src/app/app.ts"1import { NgModule } from '@angular/core'
2import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
3
4const 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})
23export class AppRoutingModule {}
一旦完成,在终端窗口运行这个程序。
ionic serve
浏览器将自动打开,显示该应用程序。
个人资料照片
每个Supabase项目都配置了存储,用于管理照片和视频等大文件。
创建一个上传小部件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。
首先安装两个包,以便与用户的相机互动。
npm install @ionic/pwa-elements @capacitor/camera
CapacitorJS是Ionic的一个跨平台原生运行时间,它使网络应用通过应用商店部署,并提供对原生deavice API的访问。
Ionic PWA元素是一个配套的软件包,它将对某些不提供用户界面的浏览器API进行聚填,并提供自定义的Ionic UI。
安装了这些包后,我们可以更新我们的main.ts
,以包括对Ionic PWA元素的额外引导调用。
src/main.ts1import { enableProdMode } from '@angular/core'
2import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
3
4import { AppModule } from './app/app.module'
5import { environment } from './environments/environment'
6
7import { defineCustomElements } from '@ionic/pwa-elements/loader'
8defineCustomElements(window)
9
10if (environment.production) {
11 enableProdMode()
12}
13platformBrowserDynamic()
14 .bootstrapModule(AppModule)
15 .catch((err) => console.log(err))
然后用这个Ionic CLI命令创建一个AvatarComponent。
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
src/app/avatar.component.ts1import { 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'
5@Component({
6 selector: 'app-avatar',
7 template: `
8 <div class="avatar_wrapper" (click)="uploadAvatar()">
9 <img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" />
10 <ng-template #noAvatar>
11 <ion-icon name="person" class="no-avatar"></ion-icon>
12 </ng-template>
13 </div>
14 `,
15 style: [
16 `
17 :host {
18 display: block;
19 margin: auto;
20 min-height: 150px;
21 }
22 :host .avatar_wrapper {
23 margin: 16px auto 16px;
24 border-radius: 50%;
25 overflow: hidden;
26 height: 150px;
27 aspect-ratio: 1;
28 background: var(--ion-color-step-50);
29 border: thick solid var(--ion-color-step-200);
30 }
31 :host .avatar_wrapper:hover {
32 cursor: pointer;
33 }
34 :host .avatar_wrapper ion-icon.no-avatar {
35 width: 100%;
36 height: 115%;
37 }
38 :host img {
39 display: block;
40 object-fit: cover;
41 width: 100%;
42 height: 100%;
43 }
44 `,
45 ],
46})
47export class AvatarComponent implements OnInit {
48 _avatarUrl: SafeResourceUrl | undefined
49 uploading = false
50
51 @Input()
52 set avatarUrl(url: string | undefined) {
53 if (url) {
54 this.downloadImage(url)
55 }
56 }
57
58 @Output() upload = new EventEmitter<string>()
59
60 constructor(private readonly supabase: SupabaseService, private readonly dom: DomSanitizer) {}
61
62 ngOnInit() {}
63
64 async downloadImage(path: string) {
65 try {
66 const { data } = await this.supabase.downLoadImage(path)
67 this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
68 } catch (error) {
69 console.error('Error downloading image: ', error.message)
70 }
71 }
72
73 async uploadAvatar() {
74 const loader = await this.supabase.createLoader()
75 try {
76 const photo = await Camera.getPhoto({
77 resultType: CameraResultType.DataUrl,
78 })
79
80 const file = await fetch(photo.dataUrl)
81 .then((res) => res.blob())
82 .then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))
83
84 const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`
85
86 await loader.present()
87 await this.supabase.uploadAvatar(fileName, file)
88
89 this.upload.emit(fileName)
90 } catch (error) {
91 this.supabase.createNotice(error.message)
92 } finally {
93 loader.dismiss()
94 }
95 }
96}
添加新的小组件
然后我们可以在账户组件的html模板上面添加小部件。
src/app/account.component.ts1template: ` 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`
下一步
在这个阶段,你已经有了一个功能完备的应用程序!
- 有问题吗?在此提问.
- 请登录MemFire Cloud