26、屏幕共享Angular组件:Service封装、依赖注入、ChangeDetection优化
这一章,我们来聊聊Angular里的屏幕共享组件怎么写才够“专业”。
说实话,很多同学写Angular组件,功能是能跑,但一遇到复杂场景——比如屏幕共享这种高频触发变更的——就开始卡顿、内存泄漏、甚至直接崩溃。我早期就踩过这个坑,后来才慢慢摸索出一套比较稳的写法。
核心就三件事:Service封装、依赖注入、ChangeDetection优化。咱们一个一个拆开讲。
为什么要把屏幕共享逻辑封装到Service里?
你想想看,屏幕共享的逻辑其实挺重的:获取媒体流、监听设备变化、处理权限、管理生命周期……如果全写在组件里,那组件会变得又臭又硬,难以测试,更难以复用。
我个人习惯是:组件只负责UI展示和用户交互,所有业务逻辑都交给Service。这样组件就变成了一个“瘦壳”,清爽很多。
Service封装:把屏幕共享变成可注入的服务
我们先写一个基础的 ScreenShareService。它负责获取屏幕流、停止共享、以及暴露流的状态。
// screen-share.service.ts
import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ScreenShareService {
private streamSubject = new BehaviorSubject<MediaStream | null>(null);
public stream$: Observable<MediaStream | null> = this.streamSubject.asObservable();
private isSharingSubject = new BehaviorSubject<boolean>(false);
public isSharing$: Observable<boolean> = this.isSharingSubject.asObservable();
constructor(private ngZone: NgZone) {}
async startScreenShare(): Promise<MediaStream> {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true
});
// 监听用户停止共享(浏览器自带按钮)
stream.getVideoTracks()[0].onended = () => {
this.stopScreenShare();
};
this.ngZone.run(() => {
this.streamSubject.next(stream);
this.isSharingSubject.next(true);
});
return stream;
} catch (err) {
console.error('屏幕共享启动失败:', err);
throw err;
}
}
stopScreenShare(): void {
const stream = this.streamSubject.getValue();
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
this.ngZone.run(() => {
this.streamSubject.next(null);
this.isSharingSubject.next(false);
});
}
}
这里有个细节:onended 回调是在浏览器事件循环里触发的,不在Angular的Zone内。所以必须用 ngZone.run() 把状态更新包进去,否则视图不会更新。嗯,这个坑我当年调了一下午才找到原因。
依赖注入:让组件和Service解耦
有了Service,组件里只需要注入它,然后调用方法即可。依赖注入的好处是:你可以随时替换实现,比如在测试时注入一个Mock Service。
// screen-share.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ScreenShareService } from './screen-share.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-screen-share',
templateUrl: './screen-share.component.html',
styleUrls: ['./screen-share.component.css']
})
export class ScreenShareComponent implements OnInit, OnDestroy {
localStream: MediaStream | null = null;
isSharing = false;
private subs: Subscription[] = [];
constructor(private screenShareService: ScreenShareService) {}
ngOnInit(): void {
this.subs.push(
this.screenShareService.stream$.subscribe(stream => {
this.localStream = stream;
})
);
this.subs.push(
this.screenShareService.isSharing$.subscribe(sharing => {
this.isSharing = sharing;
})
);
}
async onStartShare(): Promise<void> {
try {
await this.screenShareService.startScreenShare();
} catch (err) {
alert('屏幕共享启动失败,请检查权限设置。');
}
}
onStopShare(): void {
this.screenShareService.stopScreenShare();
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
this.screenShareService.stopScreenShare();
}
}
ngOnDestroy 里一定要取消订阅并停止共享。否则组件销毁了,屏幕流还在跑,用户会一脸懵。
ChangeDetection优化:别让Angular累死
屏幕共享的流数据变化非常频繁——每一帧都在变。如果Angular的变更检测机制每次都去检查整个组件树,性能会急剧下降。
我建议的做法是:把视频渲染交给原生元素,绕过Angular的变更检测。
方案一:OnPush + 手动触发
把组件的 changeDetection 设为 ChangeDetectionStrategy.OnPush,然后只在真正需要更新UI的时候手动触发检测。
// screen-share.component.ts (优化版)
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-screen-share',
templateUrl: './screen-share.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ScreenShareComponent {
constructor(
private screenShareService: ScreenShareService,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
this.subs.push(
this.screenShareService.stream$.subscribe(stream => {
this.localStream = stream;
this.cdr.markForCheck(); // 手动标记检查
})
);
}
}
这样,Angular只会在 stream$ 发出新值时检查这个组件,不会波及整个组件树。
方案二:直接操作DOM(更极致)
如果你对性能有极致要求,可以完全绕过Angular,直接用原生API把流绑定到 <video> 元素上。
// 在组件中
@ViewChild('localVideo', { static: true }) videoRef!: ElementRef<HTMLVideoElement>;
ngOnInit(): void {
this.subs.push(
this.screenShareService.stream$.subscribe(stream => {
if (stream) {
this.videoRef.nativeElement.srcObject = stream;
this.videoRef.nativeElement.play();
} else {
this.videoRef.nativeElement.srcObject = null;
}
})
);
}
这种方式下,视频流的每一帧都不会触发Angular的变更检测。说白了,视频渲染完全交给浏览器底层去处理,Angular只负责管理“是否在共享”这个布尔状态。
知识结构图
下面这张图概括了本章的核心逻辑:
避坑指南
- 不要忘记取消订阅:我曾经在线上环境发现内存只增不减,查了半天发现是
stream$的订阅没在ngOnDestroy里清理。屏幕共享流一旦开启,订阅会一直持有引用,导致组件无法被垃圾回收。 - onended 回调的Zone问题:浏览器触发的
onended不在Angular Zone内,必须用ngZone.run()包裹状态更新。否则你会发现用户点了“停止共享”,UI却纹丝不动。 - 不要频繁调用 markForCheck:如果每帧都调,OnPush 也救不了你。只在你真正需要更新UI的时候(比如共享状态变化)才调用。
- 测试时记得Mock getUserMedia:屏幕共享API在无头浏览器里不支持,写单元测试时一定要用
jasmine.createSpyObj把Service mock掉。
总结
这一章的核心就三句话:
- Service封装:把屏幕共享逻辑抽离到Service里,组件只负责视图。
- 依赖注入:通过Angular的DI机制,让组件和Service解耦,方便测试和替换。
- ChangeDetection优化:用OnPush策略或直接操作DOM,避免高频流数据拖垮性能。
做到这三点,你的屏幕共享组件基本就稳了。下一章我们会聊更进阶的话题——如何在共享过程中动态切换屏幕窗口,以及处理多显示器场景。嗯,那个坑更多,到时候再细说。