GitHub

如果你在阅读指南时遇到困难,请参考此版本

构建应用程序

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

初始化一个Angular应用程序

我们可以使用[Angular CLI](https://angular.io/cli)来初始化 一个名为`supabase-angular’的应用程序:

  npx ng new supabase-angular --routing false --style css
cd supabase-angular
  

然后让我们安装唯一的额外依赖:supabase-js

  npm install @supabase/supabase-js
  

最后我们要在environment.ts文件中保存环境变量。 我们所需要的是API URL和你[早些时候]复制的anon密钥(#get-theapi-keys)。 这些变量将暴露在浏览器上,这完全没有问题,因为我们在数据库上启用了行级安全

  export const environment = {
  production: false,
  supabaseUrl: 'YOUR_SUPABASE_URL',
  supabaseKey: 'YOUR_SUPABASE_KEY',
}
  

现在我们已经有了API凭证,让我们用ng g s supabase创建一个SupabaseService,以初始化Supabase客户端并实现与Supabase API通信的功能。

  import { Injectable } from '@angular/core'
import {
  AuthChangeEvent,
  AuthSession,
  createClient,
  Session,
  SupabaseClient,
  User,
} from '@supabase/supabase-js'
import { environment } from 'src/environments/environment'
import { Database } from 'src/schema'

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,所以用户可以用他们的电子邮件登录,而不需要使用密码。 用ng g c auth Angular CLI命令创建一个AuthComponent

  import { Component, OnInit } 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 implements OnInit {
  loading = false

  signInForm = this.formBuilder.group({
    email: '',
  })

  constructor(
    private readonly supabase: SupabaseService,
    private readonly formBuilder: FormBuilder
  ) {}

  ngOnInit(): void {}

  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('Check your email for the login link!')
    } catch (error) {
      if (error instanceof Error) {
        alert(error.message)
      }
    } finally {
      this.signInForm.reset()
      this.loading = false
    }
  }
}
  
  <div class="row flex-center flex">
  <div class="col-6 form-widget" aria-live="polite">
    <h1 class="header">Supabase + Angular</h1>
    <p class="description">Sign in via magic link with your email below</p>
    <form [formGroup]="signInForm" (ngSubmit)="onSubmit()" class="form-widget">
      <div>
        <label for="email">Email</label>
        <input
          id="email"
          formControlName="email"
          class="inputField"
          type="email"
          placeholder="Your email"
        />
      </div>
      <div>
        <button
          type="submit"
          class="button block"
          [disabled]="loading"
        >
          {{ loading ? 'Loading' : 'Send magic link' }}
        </button>
      </div>
    </form>
  </div>
</div
  

账号页面

用户还需要一种方法来编辑他们的个人资料细节,并在登录后管理他们的账户。 用ng g c accountAngular CLI命令创建一个AccountComponent

  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
      let { 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()
  }
}
  
  <form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
  <div>
    <label for="email">Email</label>
    <input id="email" type="text" [value]="session.user.email" disabled />
  </div>
  <div>
    <label for="username">Name</label>
    <input formControlName="username" id="username" type="text" />
  </div>
  <div>
    <label for="website">Website</label>
    <input formControlName="website" id="website" type="url" />
  </div>

  <div>
    <button type="submit" class="button primary block" [disabled]="loading">
      {{ loading ? 'Loading ...' : 'Update' }}
    </button>
  </div>

  <div>
    <button class="button block" (click)="signOut()">Sign Out</button>
  </div>
</form>
  

启动

现在我们已经有了所有的组件,让我们来更新AppComponent

  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))
  }
}
  
  <div class="container" style="padding: 50px 0 100px 0">
  <app-account *ngIf="session; else auth" [session]="session"></app-account>
  <ng-template #auth>
    <app-auth></app-auth>
  </ng-template>
</div>
  

app.module.ts also needs to be modified to include the ReactiveFormsModule from the @angular/forms package.

  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 {}
  

一旦完成,在终端窗口运行这个程序。

  npm run start
  

然后打开浏览器到localhost:4200,你应该看到完成的应用程序。

简介照片

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

创建一个上传小部件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。 用ng g c avatarAngular CLI命令创建一个AvatarComponent

  import { Component, EventEmitter, Input, OnInit, 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 implements OnInit {
  _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) {}

  ngOnInit(): void {}

  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 downloading image: ', error.message)
      }
    }
  }

  async uploadAvatar(event: any) {
    try {
      this.uploading = true
      if (!event.target.files || event.target.files.length === 0) {
        throw new Error('You must select an image to upload.')
      }

      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
    }
  }
}
  
  <div>
  <img
    *ngIf="_avatarUrl"
    [src]="_avatarUrl"
    alt="Avatar"
    class="avatar image"
    style="height: 150px; width: 150px"
  />
</div>
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
<div style="width: 150px">
  <label class="button primary block" for="single">
    {{ uploading ? 'Uploading ...' : 'Upload' }}
  </label>
  <input
    style="visibility: hidden;position: absolute"
    type="file"
    id="single"
    accept="image/*"
    (change)="uploadAvatar($event)"
    [disabled]="uploading"
  />
</div>
  

添加新的小组件

然后我们可以在账户组件的html模板上面添加小部件。

  <form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
  <app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
  <!-- input fields -->
</form>
  

并在AccountComponent typescript文件中加入updateAvatar函数和avatarUrl获取器。

  @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()
  }
  // ...
}
  

下一步

在这个阶段,你已经有了一个功能完备的应用程序!