背景

教室内组件业务开发模式相似度较高,导致代码的重复度也较高,具体体现在

  • 退订逻辑

退订逻辑包含在了TakeUntilDestory 中,无需手动管理

  1. // before
  2. private subscription: Subscription;
  3. this.subscription = this.roomService.protos$.subscribe(proto => this.onProtoMessage(proto));
  4. this.subscription.add(xxx
  5. );
  6. this.subscription.add(xxx)
  7. ngOnDestroy() {
  8. this.subscription.unsubscribe();
  9. }
  10. // after
  11. @TakeUntilDestroy
  12. export class TeamScoreRankComponent extends BaseClassRoomComponent implements OnDestroy {}
  • 取值逻辑

使用 GetObservable 和 GetObservableValue 取 Observable 和取值

  1. // before
  2. this.teamScoreRankState$ = this.store.select(getTeamScoreRankState).pipe(
  3. tap(state => {
  4. this.hidden = !state.showRank;
  5. }),
  6. );
  7. // after
  8. @GetObservable(getTeamScoreRankState)
  9. teamScoreRankState$;
  10. // before
  11. private teamId = 0;
  12. this.subscription.add(this.store.select(getTeam).subscribe(team => (this.teamId = team?.id ?? 0)
  13. // after
  14. @GetObservableValue(getTeam, team => team.id)
  15. readonly teamId = 0;
  • 错误处理

使用 RunSafe 处理错误

  1. // before
  2. private onRoomInfo(proto) {
  3. if (!proto) {
  4. return;
  5. }
  6. try{
  7. this.onTeamScoreState(proto.teamScoreState);
  8. this.tryGetTeamScoreRankWithApi();
  9. }catch(e){
  10. log.error('===')
  11. }
  12. }
  13. // after
  14. @RunSafe()
  15. private onRoomInfo(proto) {
  16. this.onTeamScoreState(proto.teamScoreState);
  17. this.tryGetTeamScoreRankWithApi();
  18. }

实现细节

  • GetObservable ```typescript import { takeUntil } from ‘rxjs/operators’;

export function GetObservable(selector: any): Function { return (target: Record, propName: string) => { let ob = null; Object.defineProperty(target, propName, { get(): T | unknown { if (_ob) { return _ob; } _ob = this.store.select(selector).pipe(takeUntil(this.takeUntilDestroy$)); this.takeUntilDestroy$.subscribe( => (_ob = null)); return _ob; }, set(ob) { _ob = ob.pipe(takeUntil(this.takeUntilDestroy$)); }, }); }; }

  1. - GetObservableValue
  2. ```typescript
  3. import { filter, map, takeUntil, tap } from 'rxjs/operators';
  4. export function GetObservableValue<T>(selector: any, mapFn: any = val => val, debug = false): Function {
  5. return (target: Record<any, any>, propName: string) => {
  6. let _val: T = target[propName];
  7. let ob = null;
  8. Object.defineProperty(target, propName, {
  9. get(): T | unknown {
  10. if (ob) {
  11. return _val;
  12. }
  13. ob = this.store
  14. .select(selector)
  15. .pipe(
  16. filter(val => val !== null && val !== undefined),
  17. map(mapFn),
  18. tap(val => {
  19. // eslint-disable-next-line no-console
  20. debug && console.log(`=== GetObservableValue ${selector.name} ===`, val);
  21. }),
  22. takeUntil(this.takeUntilDestroy$),
  23. )
  24. .subscribe(val => (_val = val));
  25. this.takeUntilDestroy$.subscribe(_ => (ob = null));
  26. return _val;
  27. },
  28. set(value) {
  29. _val = value;
  30. },
  31. });
  32. };
  33. }
  • TakeUntilDestroy ``typescript export function TakeUntilDestroy(constructor: Function): void { const originalDestroy = constructor.prototype.ngOnDestroy; if (typeof originalDestroy !== 'function') { // eslint-disable-next-line no-console console.warn(${constructor.name} is using @TakeUntilDestroy but does not implement OnDestroy`); } constructor.prototype.ngOnDestroy = function destroy(…args): void { if (typeof originalDestroy === ‘function’) {
    1. originalDestroy.apply(this, args);
    } if (this.takeUntilDestroy$) {
    1. this.takeUntilDestroy$.next();
    2. this.takeUntilDestroy$.complete();
    } else {
    1. // eslint-disable-next-line no-console
    2. console.error(
    3. `[Decorator TakeUntilDestroy] this.takeUntilDestroy$ does not exist which may cause memory leak, make sure your component extends InRoomBaseComponent or OutRoomBaseComponent`,
    4. );
    } }; }
  1. - RunSafe
  2. ```typescript
  3. interface IParams {
  4. returnValue: any;
  5. }
  6. type SafeDecorator<T, K extends keyof T> = T[K] | K;
  7. export function RunSafe(params: IParams = { returnValue: undefined }): Function {
  8. return function safe(
  9. target: Record<string, any>,
  10. propertyKey: string,
  11. descriptor: TypedPropertyDescriptor<Function>,
  12. ): TypedPropertyDescriptor<Function> {
  13. const originalMethod = descriptor.value;
  14. descriptor.value = function SafeWrapper() {
  15. try {
  16. return originalMethod.apply(this, arguments);
  17. } catch (error) {
  18. // eslint-disable-next-line no-console
  19. console.error(`[Safe Decorator]: execute ${propertyKey} function error:`, error);
  20. return params.returnValue !== undefined ? params.returnValue : false;
  21. }
  22. };
  23. return descriptor;
  24. };
  25. }