深入剖析前端Services服务设计与实战优化技巧

程序猿洋辰 框架 阅读 1,553
赞 10 收藏
二维码
手机扫码查看
反馈

先上代码,别管那么多

我写 Angular 项目的时候,最开始根本没搞懂 Services 到底是干啥的。一开始把所有逻辑都塞进组件里,结果一个组件动不动就五六百行,改个接口要翻半天。后来被同事骂了一顿,才老老实实抽成 Service。

深入剖析前端Services服务设计与实战优化技巧

直接看最基础的用法:

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://jztheme.com/api/users';

  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get(this.apiUrl);
  }

  createUser(user: any) {
    return this.http.post(this.apiUrl, user);
  }
}

然后在组件里这么用:

// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: 
    <div *ngFor="let user of users">
      {{ user.name }}
    </div>
  
})
export class UserListComponent implements OnInit {
  users: any[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe(data => {
      this.users = data;
    });
  }
}

看到没?就这么简单。但问题来了——很多人以为 Service 就是放 HTTP 请求的地方,其实远远不止。

这个场景最好用:状态共享

有一次做后台管理系统,左边菜单栏要根据用户权限动态显示,顶部导航也要显示用户名。两个组件不挨着,甚至不在同一个父组件下。这时候如果还在组件里各自调 API,那页面一刷新就得请求两次,用户体验差还浪费带宽。

我的做法是:在 Service 里缓存一次数据,后续直接读。

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private _currentUser: any = null;
  private currentUserSubject = new BehaviorSubject<any>(null);

  get currentUser$() {
    return this.currentUserSubject.asObservable();
  }

  login(credentials: any) {
    return this.http.post('https://jztheme.com/api/login', credentials).pipe(
      tap(user => {
        this._currentUser = user;
        this.currentUserSubject.next(user);
      })
    );
  }

  getCurrentUser() {
    if (this._currentUser) {
      return of(this._currentUser); // 已登录,直接返回
    }
    return this.http.get('https://jztheme.com/api/me').pipe(
      tap(user => {
        this._currentUser = user;
        this.currentUserSubject.next(user);
      })
    );
  }
}

这样,任意组件只要注入 AuthService,就能通过 currentUser$ 订阅用户状态变化,或者调用 getCurrentUser() 获取(带缓存)。亲测有效,再也不用担心重复请求了。

踩坑提醒:这三点一定注意

  • 别在 Service 里直接操作 DOM:我见过有人在 Service 里写 document.getElementById,说是为了“复用”。结果 SSR 直接报错,因为 Node 环境没有 document。Service 应该只处理数据和逻辑,DOM 操作留给组件。
  • 记得 unsubscribe,但别太 paranoid:HTTP 请求返回的 Observable 是自动完成的,不需要手动取消订阅。但如果你用了 intervaltimer 或者 BehaviorSubject,那就得小心内存泄漏。我在一个表格筛选功能里忘了退订,切换路由后定时器还在跑,CPU 占用直接飙到 30%……后来统一用 takeUntil 或者 async 管道解决。
  • providedIn: ‘root’ 不代表全局单例万能:虽然 @Injectable({ providedIn: 'root' }) 默认是单例,但如果某个模块 lazy load,并且你在那个模块的 providers 里又声明了一次这个 Service,那就会创建新实例!我之前在一个子模块里不小心加了 providers,导致用户信息对不上,查了半天才发现是两个 AuthService 实例。

高级技巧:拦截器 + Service 联动

有时候光靠 Service 还不够。比如全局 loading 状态、错误统一处理,这些更适合放在 HttpInterceptor 里。但怎么和 Service 联动呢?

我的方案是:在 Service 外再包一层“状态管理”,用 RxJS 的 Subject 广播 loading 状态。

// loading.service.ts
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private loadingSubject = new BehaviorSubject<boolean>(false);
  public loading$ = this.loadingSubject.asObservable();

  setLoading(loading: boolean) {
    this.loadingSubject.next(loading);
  }
}

然后写个拦截器:

typescript
// loading.interceptor.ts
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {}

intercept(req: HttpRequest<any>, next: HttpHandler) {
this.loadingService.setLoading(true);
return next.handle(req).pipe(
finalize(() => this.loadingService.setLoading(false))
);
}
}
>
<p>最后在 app.module.ts 里注册拦截器。这样,任何通过 HttpClient 发出的请求都会自动触发 loading 状态,组件里只需要订阅
loading$ 就行。不过注意:如果有多个并行请求,这个简单实现会提前关闭 loading。更严谨的做法是计数(请求+1,完成-1,为0才关),但大多数项目没那么复杂,我就先这么凑合着用。</p>

<h2>别迷信“纯 Service”,有时候组合更香</h2>
<p>有次要做一个实时聊天功能,消息列表、未读数、输入框状态全要同步。一开始想全塞进一个 ChatService,结果逻辑乱成一锅粥。后来拆成三个 Service:</p>
<ul>
<li>
ChatMessageService:负责消息收发、历史记录</li>
<li>
UnreadCountService:维护未读数量,监听消息事件</li>
<li>
ChatInputService`:管理输入内容、快捷回复等

它们之间通过事件或共享状态通信。虽然文件多了点,但每个职责清晰,测试也方便。Angular 的依赖注入系统支持这种细粒度拆分,别怕 Service 多,怕的是一个 Service 承担太多责任。

结尾碎碎念

Services 看似简单,但用得好能让项目结构清爽不少。我现在的项目里,90% 的业务逻辑都在 Service 层,组件基本只剩模板和少量交互逻辑。这样不仅好维护,单元测试覆盖率也容易提上去。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如和 NgRx 结合、用 InjectionToken 做配置等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——毕竟谁还没被烂代码折磨过呢?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论