Comment créer une application Angular ? L'article suivant vous présentera comment utiliser MemFire Cloud pour créer des applications angulaires. J'espère qu'il vous sera utile !
【Recommandation de didacticiel connexe : "Tutoriel angulaire"】
MemFire Cloud est une base de données cloud qui offre aux utilisateurs la possibilité de créer des bases de données cloud, de gérer la base de données et de sauvegarder la base de données. Il fournit également un backend en tant que service. Les utilisateurs peuvent créer une nouvelle application en 1 minute, utiliser des API et des SDK générés automatiquement et accéder aux bases de données cloud, au stockage d'objets, à l'authentification et à l'autorisation des utilisateurs, ainsi qu'à d'autres fonctions. coder et accélérer le développement d’applications WEB ou APP.
Adresse de la vidéo d'apprentissage : www.bilibili.com/video/BV1wt…
Cet exemple fournit les étapes pour créer une application de gestion d'utilisateurs simple (à partir de zéro) en utilisant MemFire Cloud et angulaire. Cela inclut :
À la fin de ce guide, vous disposerez d'une application qui permet aux utilisateurs de se connecter et de mettre à jour certains détails de base de leur profil :
Objectif : Notre application est créée ici Obtenir une série de ressources tels que les bases de données et le stockage cloud, et obtenez le lien d'accès API exclusif et la clé d'accès de l'application. Les utilisateurs peuvent facilement interagir avec les ressources ci-dessus.
Connexioncloud.memfiredb.com/auth/loginCréer une application
Cliquez sur l'application pour créer visuellement la table de données
1 Créer la table des profils ;
Sur les données. page de la table, cliquez sur "Nouvelle table de données", la configuration de la page est la suivante :
où l'identifiant du champ de la table des profils est lié à la clé étrangère du champ id (type uuid) dans la table auth.users.
2. Activez les règles d'accès de sécurité aux données RLS pour les profils ;
Sélectionnez la table Profils créée, cliquez sur la barre d'autorisation de la table, comme indiqué dans la figure ci-dessous, cliquez sur le bouton "Activer RLS"
3. chaque utilisateur pour afficher les informations personnelles publiques ;
Cliquez sur le bouton « Nouvelle règle », dans la boîte de dialogue contextuelle, sélectionnez « Activer l'accès pour tous les utilisateurs », saisissez le nom de la politique, sélectionnez l'opération « SÉLECTIONNER (requête) » et cliquez sur le bouton « Créer une politique », comme indiqué ci-dessous.
4. Autorisez uniquement les utilisateurs à ajouter, supprimer, modifier et vérifier leurs informations de profil personnel ;
Cliquez sur le bouton "Nouvelle règle", dans la boîte contextuelle, sélectionnez "Activer les autorisations d'accès pour les utilisateurs en fonction de l'utilisateur". ID" et entrez le nom de la stratégie. Sélectionnez l'opération "TOUS" et cliquez sur le bouton "Créer une stratégie", comme indiqué ci-dessous.
Créez un bucket de stockage cloud pour stocker l'image de l'avatar de l'utilisateur. Les opérations impliquées incluent :
1. Créez un bucket d'avatars
Accédez au stockage cloud de la colonne de l'application, cliquez sur. le bouton "Nouveau bucket" pour créer des avatars de bucket.
2. Autorisez chaque utilisateur à afficher les avatars du bucket
Sélectionnez les avatars du bucket, passez à la colonne Paramètres d'autorisation, cliquez sur le bouton "Nouvelle règle", une boîte de dialogue contextuelle d'édition de politique apparaîtra, sélectionnez "Personnalisé", comme le montre l'image ci-dessous :
Sélectionnez l'opération SELECT, entrez le nom de la stratégie et cliquez sur le bouton « Générer une stratégie », comme indiqué dans la figure ci-dessous.
3. Autorisez les utilisateurs à télécharger des avatars de bucket ;
Sélectionnez les avatars de bucket, passez à la colonne Paramètres d'autorisation, cliquez sur le bouton "Nouvelle règle", une boîte de dialogue d'édition de politique apparaîtra, sélectionnez "Personnalisé", comme indiqué ci-dessous Comme indiqué :
Sélectionnez l'opération INSÉRER, entrez le nom de la stratégie et cliquez sur le bouton "Générer une stratégie", comme indiqué dans la figure ci-dessous.
Afficher les résultats
Toutes les tables de données et SQL de RLS (le nom de la stratégie est remplacé par l'anglais)
-- Create a table for public "profiles" create table profiles ( id uuid references auth.users not null, updated_at timestamp with time zone, username text unique, avatar_url text, website text, primary key (id), unique(username), ); alter table profiles enable row level security; create policy "Public profiles are viewable by everyone." on profiles for select using ( true ); create policy "Users can insert their own profile." on profiles for insert with check ( auth.uid() = id ); create policy "Users can update own profile." on profiles for update using ( auth.uid() = id ); -- Set up Storage! insert into storage.buckets (id, name) values ('avatars', 'avatars'); create policy "Avatar images are publicly accessible." on storage.objects for select using ( bucket_id = 'avatars' ); create policy "Anyone can upload an avatar." on storage.objects for insert with check ( bucket_id = 'avatars' );
Maintenant que vous avez créé des tables de base de données, vous pouvez utiliser la API générée pour insérer des données. Nous avons juste besoin d'obtenir l'URL et la clé d'anon à partir des paramètres de l'API.
Sur la page Application->Résumé, obtenez l'adresse du service et les informations sur le jeton.
La clé Anon (publique) est la clé API du client. Il permet un « accès anonyme » à votre base de données jusqu'à ce que l'utilisateur se connecte. Après la connexion, la clé est remplacée par le propre jeton de connexion de l'utilisateur. Cela permettra la sécurité au niveau des lignes pour les données.
REMARQUE : La clé service_role (secret) permet un accès complet à vos données, en contournant toute politique de sécurité. Cette clé doit être gardée secrète et utilisée dans un environnement serveur, jamais sur le client ou le navigateur. Dans l’exemple de code suivant, supabaseUrl et supabaseKey doivent être fournis.
Lorsque l'utilisateur clique sur le lien magique dans l'e-mail pour se connecter, il doit accéder à l'interface de connexion de notre application. Ici, vous devez configurer la redirection d'URL appropriée dans les paramètres d'authentification.
Parce que notre application finale sera démarrée sur le port local 4200 (ou d'autres ports), nous définissons donc ici temporairement l'url sur http://localhost:4200
De plus, cette interface peut également être personnalisée. Utilisez notre propre smtp serveur.
Créons une application Angular à partir de zéro.
Nous pouvons utiliser Angular CLI pour initialiser un projet nommé memfiredb-angular
: memfiredb-angular
:
Angular 需要 Node.js (>=14.15
npm install -g @angular/cli npx ng new memfiredb-angular --routing false --style css cd memfiredb-angular
然后让我们安装唯一的附加依赖项:supabase-js
npm install @supabase/supabase-js
最后,我们要将环境变量保存在environment.ts, 我们需要的是 API URL 和您上面anon
复制的密钥。
src/environments/environment.ts文件
export const environment = { production: false, supabaseUrl: "YOUR_SUPABASE_URL", supabaseKey: "YOUR_SUPABASE_KEY" };
现在我们已经有了 API 凭证,通过ng g s supabase
创建一个SupabaseService来初始化 Supabase 客户端并实现与 Supabase API 通信的函数。
src/app/supabase.service.ts
import { Injectable } from '@angular/core'; 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() { this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey); } get user() { return this.supabase.auth.user(); } get session() { return this.supabase.auth.session(); } get profile() { return this.supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', this.user?.id) .single(); } authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) { return this.supabase.auth.onAuthStateChange(callback); } signIn(email: string) { return this.supabase.auth.signIn({email}); } signOut() { return this.supabase.auth.signOut(); } updateProfile(profile: Profile) { const update = { ...profile, id: this.user?.id, updated_at: new Date() } return this.supabase.from('profiles').upsert(update, { returning: 'minimal', // Don't return the value after inserting }); } 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
文件。
html, body { --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; --custom-bg-color: #101010; --custom-panel-color: #222; --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8); --custom-color: #fff; --custom-color-brand: #24b47e; --custom-color-secondary: #666; --custom-border: 1px solid #333; --custom-border-radius: 5px; --custom-spacing: 5px; padding: 0; margin: 0; font-family: var(--custom-font-family); background-color: var(--custom-bg-color); } * { color: var(--custom-color); font-family: var(--custom-font-family); box-sizing: border-box; } html, body, #__next { height: 100vh; width: 100vw; overflow-x: hidden; } /* Grid */ .container { width: 90%; margin-left: auto; margin-right: auto; } .row { position: relative; width: 100%; } .row [class^='col'] { float: left; margin: 0.5rem 2%; min-height: 0.125rem; } .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12 { width: 96%; } .col-1-sm { width: 4.33%; } .col-2-sm { width: 12.66%; } .col-3-sm { width: 21%; } .col-4-sm { width: 29.33%; } .col-5-sm { width: 37.66%; } .col-6-sm { width: 46%; } .col-7-sm { width: 54.33%; } .col-8-sm { width: 62.66%; } .col-9-sm { width: 71%; } .col-10-sm { width: 79.33%; } .col-11-sm { width: 87.66%; } .col-12-sm { width: 96%; } .row::after { content: ''; display: table; clear: both; } .hidden-sm { display: none; } @media only screen and (min-width: 33.75em) { /* 540px */ .container { width: 80%; } } @media only screen and (min-width: 45em) { /* 720px */ .col-1 { width: 4.33%; } .col-2 { width: 12.66%; } .col-3 { width: 21%; } .col-4 { width: 29.33%; } .col-5 { width: 37.66%; } .col-6 { width: 46%; } .col-7 { width: 54.33%; } .col-8 { width: 62.66%; } .col-9 { width: 71%; } .col-10 { width: 79.33%; } .col-11 { width: 87.66%; } .col-12 { width: 96%; } .hidden-sm { display: block; } } @media only screen and (min-width: 60em) { /* 960px */ .container { width: 75%; max-width: 60rem; } } /* Forms */ label { display: block; margin: 5px 0; color: var(--custom-color-secondary); font-size: 0.8rem; text-transform: uppercase; } input { width: 100%; border-radius: 5px; border: var(--custom-border); padding: 8px; font-size: 0.9rem; background-color: var(--custom-bg-color); color: var(--custom-color); } input[disabled] { color: var(--custom-color-secondary); } /* Utils */ .block { display: block; width: 100%; } .inline-block { display: inline-block; width: 100%; } .flex { display: flex; } .flex.column { flex-direction: column; } .flex.row { flex-direction: row; } .flex.flex-1 { flex: 1 1 0; } .flex-end { justify-content: flex-end; } .flex-center { justify-content: center; } .items-center { align-items: center; } .text-sm { font-size: 0.8rem; font-weight: 300; } .text-right { text-align: right; } .font-light { font-weight: 300; } .opacity-half { opacity: 50%; } /* Button */ button, .button { color: var(--custom-color); border: var(--custom-border); background-color: var(--custom-bg-color); display: inline-block; text-align: center; border-radius: var(--custom-border-radius); padding: 0.5rem 1rem; cursor: pointer; text-align: center; font-size: 0.9rem; text-transform: uppercase; } button.primary, .button.primary { background-color: var(--custom-color-brand); border: 1px solid var(--custom-color-brand); } /* Widgets */ .card { width: 100%; display: block; border: var(--custom-border); border-radius: var(--custom-border-radius); padding: var(--custom-spacing); } .avatar { border-radius: var(--custom-border-radius); overflow: hidden; max-width: 100%; } .avatar.image { object-fit: cover; } .avatar.no-image { background-color: #333; border: 1px solid rgb(200, 200, 200); border-radius: 5px; } .footer { position: absolute; max-width: 100%; bottom: 0; left: 0; right: 0; display: flex; flex-flow: row; border-top: var(--custom-border); background-color: var(--custom-bg-color); } .footer div { padding: var(--custom-spacing); display: flex; align-items: center; width: 100%; } .footer div > img { height: 20px; margin-left: 10px; } .footer > div:first-child { display: none; } .footer > div:nth-child(2) { justify-content: left; } @media only screen and (min-width: 60em) { /* 960px */ .footer > div:first-child { display: flex; } .footer > div:nth-child(2) { justify-content: center; } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .mainHeader { width: 100%; font-size: 1.3rem; margin-bottom: 20px; } .avatarPlaceholder { border: var(--custom-border); border-radius: var(--custom-border-radius); width: 35px; height: 35px; background-color: rgba(255, 255, 255, 0.2); display: flex; align-items: center; justify-content: center; } /* Auth */ .auth-widget { display: flex; flex-direction: column; gap: 20px; } .auth-widget > .button { display: flex; align-items: center; justify-content: center; border: none; background-color: #444444; text-transform: none !important; transition: all 0.2s ease; } .auth-widget .button:hover { background-color: #2a2a2a; } .auth-widget .button > .loader { width: 17px; animation: spin 1s linear infinite; filter: invert(1); } /* Account */ .account { display: flex; flex-direction: column; gap: 20px; } .account > * > .avatarField { display: flex; align-items: center; margin-bottom: 30px; } .account > * > .avatarField > .avatarContainer { margin-right: 20px; } /* Profile Card */ .profileCard { border-radius: 5px; display: flex; border: var(--custom-border); background-color: var(--custom-panel-color); padding: 20px 20px; margin-bottom: 20px; } .profileCard:last-child { margin-bottom: 0px; } .profileCard > .userInfo { margin-left: 20px; font-weight: 300; display: flex; flex-direction: column; justify-content: center; } .profileCard > .userInfo > p { margin: 0; } .profileCard > .userInfo > .username { font-size: 1.3rem; font-weight: 500; margin-bottom: 5px; } .profileCard > .userInfo > .website { font-size: 0.9rem; color: var(--custom-color-brand); margin-bottom: 10px; text-decoration: none; }
让我们设置一个 Angular 组件来管理登录和注册。我们将使用 Magic Links,因此用户无需使用密码即可使用电子邮件登录。使用Angular CLI 命令创建一个AuthComponent 。 ng g c auth
src/app/auth/auth.component.ts
import { Component } from '@angular/core'; import {SupabaseService} from "../supabase.service"; @Component({ selector: 'app-auth', template: ` <div> <form> <h1>Memfiredb + Angular</h1> <p>使用下面的电子邮件通过魔术链接登录</p> <div> <input> </div> <div> <button> {{loading ? 'Loading' : 'Send magic link'}} </button> </div> </form> </div> `, }) export class AuthComponent { loading = false; constructor(private readonly supabase: SupabaseService) { } async handleLogin(input: string) { try { this.loading = true; await this.supabase.signIn(input); alert('请检查您的电子邮件以获取登录链接!'); } catch (error:any) { alert(error.error_description || error.message) } finally { this.loading = false; } } }
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。使用Angular CLI 命令创建一个AccountComponent 。 ng g c account
import {Component, Input, OnInit} from '@angular/core'; import {Profile, SupabaseService} from "../supabase.service"; import {Session} from "@supabase/supabase-js"; @Component({ selector: 'app-account', template: ` <div> <div> <label>邮箱</label> <input> </div> <div> <label>名称</label> <input> </div> <div> <label>网址</label> <input> </div> <div> <button> {{loading ? 'Loading ...' : 'Update'}} </button> </div> <div> <button> 退出登录 </button> </div> </div> ` }) export class AccountComponent implements OnInit { loading = false; profile: Profile | undefined; @Input() session: Session | undefined; constructor(private readonly supabase: SupabaseService) { } ngOnInit() { this.getProfile(); } async getProfile() { try { this.loading = true; let {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) } finally { this.loading = false; } } async updateProfile(username: string, website: string, avatar_url: string = '') { try { this.loading = true; await this.supabase.updateProfile({username, website, avatar_url}); } catch (error:any) { alert(error.message); } finally { this.loading = false; } } async signOut() { await this.supabase.signOut(); } }
import {Component, OnInit} from '@angular/core'; import {SupabaseService} from "./supabase.service"; @Component({ selector: 'app-root', template: ` <div> <app-account></app-account> <ng-template> <app-auth></app-auth> </ng-template> </div> ` }) export class AppComponent implements OnInit { session = this.supabase.session; constructor(private readonly supabase: SupabaseService) { } ngOnInit() { this.supabase.authChanges((_, session) => this.session = session); } }
anon
. fichier src/environments/environment.tsnpm run start
ng g s supabase
pour initialiser le client Supabase et implémenter des fonctions pour communiquer avec la Supabase API. 🎜🎜src/app/supabase.service.ts🎜import {Component, EventEmitter, Input, Output} from '@angular/core'; import {SupabaseService} from "../supabase.service"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; @Component({ selector: 'app-avatar', template: ` <div> <img alt="Parlons de la façon de créer des applications angulaires à l'aide de MemFire Cloud" > </div> <div></div> <div> <label> {{uploading ? 'Uploading ...' : 'Upload'}} </label> <input> </div> `, }) 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 ) { } 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:any) { 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 fileName = `${Math.random()}.${fileExt}`; const filePath = `${fileName}`; await this.supabase.uploadAvatar(filePath, file); this.upload.emit(filePath); this.downloadImage(filePath) } catch (error:any) { alert(error.message); } finally { this.uploading = false; } } }</string>
src/styles.css
. 🎜import {Component, Input, OnInit} from '@angular/core'; import {Profile, SupabaseService} from "../supabase.service"; import {Session} from "@supabase/supabase-js"; @Component({ selector: 'app-account', template: ` <app-avatar> </app-avatar> <div> <div> <label>邮箱</label> <input> </div> <div> <label>名称</label> <input> </div> <div> <label>网址</label> <input> </div> <div> <button> {{loading ? 'Loading ...' : 'Update'}} </button> </div> <div> <button> 退出登录 </button> </div> </div> ` }) export class AccountComponent implements OnInit { loading = false; profile: Profile | undefined; @Input() session: Session | undefined; constructor(private readonly supabase: SupabaseService) { } ngOnInit() { this.getProfile(); } async getProfile() { try { this.loading = true; let {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) } finally { this.loading = false; } } async updateProfile(username: string, website: string, avatar_url: string = '') { try { this.loading = true; await this.supabase.updateProfile({username, website, avatar_url}); alert("修改成功") } catch (error:any) { alert(error.message); } finally { this.loading = false; } } async signOut() { await this.supabase.signOut(); } }
ng g c auth
🎜🎜src/app/auth/auth.component.ts🎜rrreeeng g c account
🎜🎜src/app/account/account.component.ts🎜rrreee🎜🎜Modifiez le fichier d'entrée du projet🎜🎜🎜Maintenant que tous les composants sont prêts, mettons à jour 🎜AppComponent🎜 :🎜src/app/app.component.ts
import {Component, OnInit} from '@angular/core'; import {SupabaseService} from "./supabase.service"; @Component({ selector: 'app-root', template: ` <div> <app-account></app-account> <ng-template> <app-auth></app-auth> </ng-template> </div> ` }) export class AppComponent implements OnInit { session = this.supabase.session; constructor(private readonly supabase: SupabaseService) { } ngOnInit() { this.supabase.authChanges((_, session) => this.session = session); } }
完成后,在终端窗口中运行它:
npm run start
然后打开浏览器到 http://localhost:4200,你应该会看到完整的应用程序。
每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。
让我们为用户创建一个头像,以便他们可以上传个人资料照片。使用Angular CLI 命令创建AvatarComponent 。 ng g c avatar
src/app/avatar/avatar.component.ts
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {SupabaseService} from "../supabase.service"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; @Component({ selector: 'app-avatar', template: ` <div> <img alt="Parlons de la façon de créer des applications angulaires à l'aide de MemFire Cloud" > </div> <div></div> <div> <label> {{uploading ? 'Uploading ...' : 'Upload'}} </label> <input> </div> `, }) 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 ) { } 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:any) { 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 fileName = `${Math.random()}.${fileExt}`; const filePath = `${fileName}`; await this.supabase.uploadAvatar(filePath, file); this.upload.emit(filePath); this.downloadImage(filePath) } catch (error:any) { alert(error.message); } finally { this.uploading = false; } } }</string>
然后我们可以在AccountComponent html 模板的顶部添加小部件:
src/app/account/account.component.ts
import {Component, Input, OnInit} from '@angular/core'; import {Profile, SupabaseService} from "../supabase.service"; import {Session} from "@supabase/supabase-js"; @Component({ selector: 'app-account', template: ` <app-avatar> </app-avatar> <div> <div> <label>邮箱</label> <input> </div> <div> <label>名称</label> <input> </div> <div> <label>网址</label> <input> </div> <div> <button> {{loading ? 'Loading ...' : 'Update'}} </button> </div> <div> <button> 退出登录 </button> </div> </div> ` }) export class AccountComponent implements OnInit { loading = false; profile: Profile | undefined; @Input() session: Session | undefined; constructor(private readonly supabase: SupabaseService) { } ngOnInit() { this.getProfile(); } async getProfile() { try { this.loading = true; let {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) } finally { this.loading = false; } } async updateProfile(username: string, website: string, avatar_url: string = '') { try { this.loading = true; await this.supabase.updateProfile({username, website, avatar_url}); alert("修改成功") } catch (error:any) { alert(error.message); } finally { this.loading = false; } } async signOut() { await this.supabase.signOut(); } }
恭喜!在这个阶段,您拥有一个功能齐全的应用程序!
更多编程相关知识,请访问:编程视频!!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!