You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 04:39:51