深入剖析前端Services服务设计与实战优化技巧
先上代码,别管那么多
我写 Angular 项目的时候,最开始根本没搞懂 Services 到底是干啥的。一开始把所有逻辑都塞进组件里,结果一个组件动不动就五六百行,改个接口要翻半天。后来被同事骂了一顿,才老老实实抽成 Service。
直接看最基础的用法:
// 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 是自动完成的,不需要手动取消订阅。但如果你用了
interval、timer或者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))
);
}
}
>loading$
<p>最后在 app.module.ts 里注册拦截器。这样,任何通过 HttpClient 发出的请求都会自动触发 loading 状态,组件里只需要订阅 就行。不过注意:如果有多个并行请求,这个简单实现会提前关闭 loading。更严谨的做法是计数(请求+1,完成-1,为0才关),但大多数项目没那么复杂,我就先这么凑合着用。</p>
<h2>别迷信“纯 Service”,有时候组合更香</h2>
<p>有次要做一个实时聊天功能,消息列表、未读数、输入框状态全要同步。一开始想全塞进一个 ChatService,结果逻辑乱成一锅粥。后来拆成三个 Service:</p>
<ul>
<li>ChatMessageService:负责消息收发、历史记录</li>UnreadCountService
<li>:维护未读数量,监听消息事件</li>ChatInputService`:管理输入内容、快捷回复等
<li>
它们之间通过事件或共享状态通信。虽然文件多了点,但每个职责清晰,测试也方便。Angular 的依赖注入系统支持这种细粒度拆分,别怕 Service 多,怕的是一个 Service 承担太多责任。
结尾碎碎念
Services 看似简单,但用得好能让项目结构清爽不少。我现在的项目里,90% 的业务逻辑都在 Service 层,组件基本只剩模板和少量交互逻辑。这样不仅好维护,单元测试覆盖率也容易提上去。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如和 NgRx 结合、用 InjectionToken 做配置等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——毕竟谁还没被烂代码折磨过呢?

暂无评论