Angular+Supabase项目执行ng serve后无限加载问题求助
Angular + Supabase 登录功能无限加载问题排查与解决
问题现象
使用Angular和Supabase开发登录功能,拆分了Supabase客户端初始化服务和认证逻辑服务。执行ng serve后控制台显示打包完成,可访问http://localhost:4200/,但页面始终无限加载。尝试将Supabase服务移至Angular Zone外(使用runOutsideAngular),问题仍未解决。
相关服务代码
Supabase客户端服务
import { Injectable } from '@angular/core'; import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root' }) export class SupabaseService { public client: SupabaseClient; constructor() { this.client = createClient(environment.supabaseUrl, environment.supabaseKey); } }
认证服务
import { Injectable } from '@angular/core'; import { RealtimeChannel, User } from '@supabase/supabase-js'; import { BehaviorSubject, first, Observable, skipWhile } from 'rxjs'; import { SupabaseService } from './supabase-service.service'; export interface Profile { id: string; email: string; } @Injectable({ providedIn: 'root', }) export class AuthService { // Supabase user state private _$user = new BehaviorSubject<User | null | undefined>(undefined); $user = this._$user.pipe(skipWhile(_ => typeof _ === 'undefined')) as Observable<User | null>; private id?: string; // Profile state private _$profile = new BehaviorSubject<Profile | null | undefined>(undefined); $profile = this._$profile.pipe(skipWhile(_ => typeof _ === 'undefined')) as Observable<Profile | null>; private profile_subscription?: RealtimeChannel; constructor(private supabase: SupabaseService) { // Initialize Supabase user // Get initial user from the current session, if it exists this.supabase.client.auth.getUser().then(({ data, error }) => { this._$user.next(data && data.user && !error ? data.user : null); // After the initial value is set, listen for auth state changes (sign in/sign out) this.supabase.client.auth.onAuthStateChange((event, session) => { this._$user.next(session?.user ?? null); }); }); // Initialize the user's profile // The state of the user's profile is dependent on there being a user. If no user is set, there shouldn't be a profile. this.$user.subscribe(user => { if (user) { // We only make changes if the user is different if (user.id !== this.id) { const id = user.id; this.id = id; // One-time API call to Supabase to get the user's profile this.supabase.client.from('profiles').select('*').match({ id }).single().then(res => { //Update our profile Behavior Subject with the current value this._$profile.next(res.data ?? null); //Listen to any changes to our user's profile using Supabase Realtime this.profile_subscription = this.supabase .client .channel('public:profiles') .on('postgres_changes', { event: '*', schema: 'public', table: 'profiles', filter: 'id=eq.' + user.id}, (payload: any) => { //Update our profile Behavior Subject with the newest value this._$profile.next(payload.new); }) .subscribe() }) } } else { // If there is no user, update the profile BehaviorSubject, delete the user's id (id), and unsubscribe from the Supabase Realtime this._$profile.next(null); delete this.id; if (this.profile_subscription) { this.supabase.client.removeChannel(this.profile_subscription).then(res => { console.log('Removed profile channel subscription with status: ', res); }); } } }) //Since our _$profile BehaviorSubject is dependent on the $user Observable, // when we sign out our user, the _$profile is automatically updated. // We can now use the $profile Observable everywhere in our app. //End of constructor } //Registering account registerUser(email: string, password: string) { return new Promise<void>((resolve, reject) => { this.supabase.client.auth.signUp({ email, password }) .then(({ data, error }) => { if (error != null || data == null) return reject(new Error('User couldn\'t be registered')); this.$profile.pipe(first()).subscribe(() => resolve()); }) .catch(error => reject(error)); }); } //Sign In signIn(email: string, password: string) { return new Promise<void>((resolve, reject) => { // Set _$profile back to undefined. This will mean that $profile will wait to emit a value this._$profile.next(undefined); this.supabase.client.auth.signInWithPassword({ email, password }) .then(({ data, error }) => { if (error || !data) return reject(new Error('Invalid email/password combination')); // Wait for $profile to be set again. // We don't want to proceed until our API request for the user's profile has completed this.$profile.pipe(first()).subscribe(() => { resolve(); }); }) }) } //Sign Out signOut() { return this.supabase.client.auth.signOut(); } }
已尝试的修改
import { inject, Injectable, NgZone } from '@angular/core'; import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root' }) export class SupabaseService { public client: SupabaseClient; private readonly ngZone = inject(NgZone); constructor() { this.client = this.ngZone.runOutsideAngular(() => createClient(environment.supabase.url, environment.supabase.key) ); } }
问题排查与解决方案
1. 修复Profile请求的错误处理
当用户已登录但profiles表中无对应数据时,single()查询会抛出"找不到数据"的错误,而原代码未处理该错误,导致_$profile一直处于undefined状态。如果页面依赖$profile的发射值(比如路由守卫等待加载完成),就会引发无限加载。
修复代码:
this.supabase.client.from('profiles').select('*').match({ id }).single() .then(res => { this._$profile.next(res.data ?? null); // Realtime订阅逻辑保留 }) .catch(err => { console.error('获取Profile失败:', err); this._$profile.next(null); // 确保即使出错也能发射值,解除阻塞 });
2. 确保状态更新进入Angular Zone
即使Supabase客户端在Zone外运行,状态变化的回调需要手动进入Angular Zone,否则Angular无法检测到变化,导致视图或加载状态无法更新。
修复代码:
在AuthService中注入NgZone,并在所有状态更新的回调中使用ngZone.run()包裹:
import { Injectable, NgZone } from '@angular/core'; // 其他导入保留 @Injectable({ providedIn: 'root' }) export class AuthService { constructor(private supabase: SupabaseService, private ngZone: NgZone) { // 初始化用户部分 this.supabase.client.auth.getUser().then(({ data, error }) => { this.ngZone.run(() => { this._$user.next(data && data.user && !error ? data.user : null); }); this.supabase.client.auth.onAuthStateChange((event, session) => { this.ngZone.run(() => { this._$user.next(session?.user ?? null); }); }); }); // Profile订阅部分 this.$user.subscribe(user => { // 现有逻辑保留 this.supabase.client.from('profiles').select('*').match({ id }).single() .then(res => { this.ngZone.run(() => { this._$profile.next(res.data ?? null); }); // Realtime订阅 this.profile_subscription = this.supabase.client .channel('public:profiles') .on('postgres_changes', { /* 配置保留 */ }, (payload: any) => { this.ngZone.run(() => { this._$profile.next(payload.new); }); }) .subscribe(); }) .catch(err => { console.error('获取Profile失败:', err); this.ngZone.run(() => { this._$profile.next(null); }); }); // 其他逻辑保留 }); } }
3. 确保Observable始终发射值
在$profile的管道中添加defaultIfEmpty(null),确保即使出现异常也不会阻塞:
import { defaultIfEmpty } from 'rxjs'; // 修改$profile定义 $profile = this._$profile.pipe( skipWhile(_ => typeof _ === 'undefined'), defaultIfEmpty(null) ) as Observable<Profile | null>;
内容的提问来源于stack exchange,提问作者BraedenTurner22
相关产品推荐
相关产品推荐

