likes
comments
collection
share

angular封装组件缓存复用的组件(仿路由复用)

作者站长头像
站长
· 阅读数 65

自定义路由复用策略是Angular中的一个强大功能,它允许开发者完全控制路由组件的缓存和重用行为。在本文中,将深入探讨自定义路由复用策略的概念、原理和实现。我们将首先介绍路由复用的概念,然后探讨为何需要自定义路由复用策略以及其优势。接着,我们将详细讲解如何使用Angular的RouteReuseStrategy接口来实现自定义路由复用策略,并提供实例演示。然后根据路由复用的源码的启示,简易封装组件缓存复用的组件。

什么是路由复用

在一个典型的Angular应用中,当用户在应用程序中导航到不同的路由时,Angular会销毁上一个路由组件并创建一个新的组件实例。这是默认的路由复用策略,也被称为"典型"策略。然而,有时候我们希望在导航到不同路由时,保留先前加载的组件状态,以便稍后重用。这就是路由复用的概念。

路由复用的优势

路由复用可以带来许多优势:

  • 性能提升:重用先前加载的组件状态,避免重复加载和初始化,提高应用程序的性能和响应速度。
  • 数据保留:当用户导航回之前的页面时,可以保留先前的数据和状态,提供更好的用户体验。
  • 避免重复请求:如果组件已加载并包含已获取的数据,重用该组件可以避免重复发起相同的请求。

自定义路由复用策略

在Angular中,我们可以通过实现RouteReuseStrategy接口来自定义路由复用策略。该接口定义了5个方法,每个方法都有特定的用途:

@Injectable({providedIn: 'root', useFactory: () => inject(DefaultRouteReuseStrategy)})
export abstract class RouteReuseStrategy {
  abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;

  abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle|null): void;

  abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;

  abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null;

  abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
}
  1. shouldDetach(route: ActivatedRouteSnapshot): boolean:

    • 该方法决定是否应该缓存特定的路由组件。
    • 在每次导航离开一个路由时,该方法都会被调用。如果返回true,则路由组件会被缓存;如果返回false,则路由组件不会被缓存。
  2. store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void:

    • 该方法在路由组件被缓存时调用,用于将路由组件的DetachedRouteHandle保存到缓存中。
  3. shouldAttach(route: ActivatedRouteSnapshot): boolean:

    • 该方法决定是否应该从缓存中获取路由组件。
    • 在每次导航到一个路由时,该方法都会被调用。如果返回true,则会从缓存中获取路由组件;如果返回false,则会重新创建路由组件。
  4. retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null:

    • 该方法在shouldAttach返回true时被调用,用于从缓存中获取之前缓存的路由组件。
  5. shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean:

    • 该方法在每次导航之前调用,决定当前路由是否应该重用。
    • 通过比较futurecurr两个参数,可以根据需要返回truefalse,来决定是否重用当前路由。

实现自定义路由复用策略

  • 实现自定义路由复用策略 创建一个新的文件custom-route-reuse-strategy.ts,然后实现RouteReuseStrategy接口。

// custom-route-reuse-strategy.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';

@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  private routeCache: { [key: string]: DetachedRouteHandle } = {};

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.routeConfig?.path === 'about';
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
    if (handle && route.routeConfig) {
      this.routeCache[route.routeConfig.path || ''] = handle;
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!route.routeConfig && !!this.routeCache[route.routeConfig.path || ''];
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    if (!route.routeConfig) {
      return null;
    }
    return this.routeCache[route.routeConfig.path || ''] || null;
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }
}

这个案例演示了如何通过自定义路由复用策略实现组件的缓存和重用。在实际应用中,应该根据特定的需求和业务逻辑来定义自己的自定义策略,以实现更复杂和灵活的路由缓存机制。

  • 注册自定义策略
import { NgModule } from '@angular/core';
import { RouterModule, RouteReuseStrategy, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { CustomRouteReuseStrategy } from './custom-route-reuse-strategy';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  providers: [
    { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

app.module.ts中导入和使用自定义策略

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { CustomRouteReuseStrategy } from './custom-route-reuse-strategy';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    AboutComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

仿照路由复用封装类似的复用组件

  1. 封装目的

    • 性能提升:重用先前加载的组件状态,避免重复加载和初始化,提高应用程序的性能和响应速度。
    • 数据保留:当用户导航回之前的视图时,可以保留先前的数据和状态,提供更好的用户体验。
    • 避免重复请求:如果组件已加载并包含已获取的数据,重用该组件可以避免重复发起相同的请求。
    • 相比路由复用使用更简便
  2. 具体实现

    • 创建CacheViewStrategyService

    import {ComponentRef, Injectable} from '@angular/core';
    
    export class ActivatedCacheSnapshot {
      constructor(public readonly path: string,
                  public readonly params: Record<string, string>,
                  public readonly data: Record<string, any>) {
      }
    }
    
    export type DetachedCacheHandle = {
      componentRef: ComponentRef<any>,
    };
    @Injectable({
      providedIn: 'root'
    })
    export class CacheViewStrategyService {
      private cache: Record<string, DetachedCacheHandle> = {}
      /**
       * 决定是否应该缓存某个组件。你可以基于某些条件来判断是否需要缓存该组件。
       * @param route
       */
      shouldDetach(route: ActivatedCacheSnapshot): boolean {
        return true
      }
    
      /**
       *  当shouldDetach返回true时,你可以在这里保存路由组件的DetachedCacheHandle,以便稍后重用。
       * @param route
       * @param handle
       */
      store(route: ActivatedCacheSnapshot, handle: DetachedCacheHandle|null): void {
        if (!handle) {
          return
        }
    
        this.cache[route.path] = handle;
      }
    
      /**
       * 决定是否应该重用缓存中的组件。你可以检查某些条件并决定是否从缓存中获取组件。
       * @param route
       */
      shouldAttach(route: ActivatedCacheSnapshot): boolean {
        return !!this.cache[route.path]
      }
    
      /**
       * 当shouldAttach返回true时,从缓存中获取组件的DetachedCacheHandle
       * @param route
       */
      retrieve(route: ActivatedCacheSnapshot): DetachedCacheHandle|null {
        return this.cache[route.path]
      }
    
      /**
       * 在每次切换之前调用此方法,决定当前组件是否应该重用。你可以根据当前组件快照和即将激活的组件快照进行比较,根据需要返回true或false。
       * @param future
       * @param curr
       */
      // shouldReuseRoute(future: ActivatedCacheSnapshot, curr: ActivatedCacheSnapshot): boolean {
      //   return false
      // }
      constructor() { }
    }
    

    与路由复用接口基本一致,不过本服务中没有使用到shouldReuseRoute,目前在暂时用不到

    • 创建cache-view模块&cache-view组件

      angular封装组件缓存复用的组件(仿路由复用)

    cache-view模块

    import {InjectionToken, ModuleWithProviders, NgModule} from '@angular/core';
    import { CommonModule } from '@angular/common';
    import {CacheViewComponent} from "./cache-view.component";
    
    export interface CacheConfig {
     path: string;
     component: Type<any>
     data?: Record<string, any>
    }
    
    export const CONFIG = new InjectionToken<CacheConfig[][]>('CACHE_VIEW_CONFIG');
    
    @NgModule({
     declarations: [
       CacheViewComponent
     ],
     imports: [
       CommonModule
     ],
     exports: [
       CacheViewComponent
     ]
    })
    export class CacheModule {
     static forRoot(config: CacheConfig[]): ModuleWithProviders<CacheModule> {
       return {
         ngModule: CacheModule,
         providers: [
           {provide: CONFIG, multi: true, useValue: config}
         ]
       }
     }
    }
    

    cache-view组件

      // cache-view.component.ts
      import {
        Component, ComponentRef,
        inject,
        Injector,
        Input,
        OnChanges,
        SimpleChange,
        ViewContainerRef
      } from '@angular/core';
      import {ActivatedCacheSnapshot, CacheViewStrategyService, DetachedCacheHandle} from "../cache-view-strategy.service";
      import {CacheConfig, CONFIG} from "./token";
    
      type TypedSimpleChanges<T> = {
        [P in keyof T]?: SimpleChange;
      };
    
      @Component({
        selector: 'app-cache-view',
        template: `
        `,
        styles: [],
      })
      export class CacheViewComponent implements OnChanges {
        private activated: ComponentRef<any> | null = null;
        public snapshot?: ActivatedCacheSnapshot;
        @Input() path = '';
        @Input() params: Record<string, string> = {};
        config: CacheConfig[] = inject(CONFIG, {optional: true})?.flat() ?? []
    
        constructor(private readonly viewContainerRef: ViewContainerRef,
                    private readonly injector: Injector,
                    private readonly cacheStrategy: CacheViewStrategyService) {
    
        }
    
        ngOnChanges(changes: TypedSimpleChanges<CacheViewComponent>): void {
    
          if (changes.params) {
          }
          if (changes.path || changes.params) {
            if (changes.path) {
              const currentValue = changes.path.currentValue;
              this.stateChange()
            }
          }
        }
    
        private detach(): ComponentRef<any> | null {
          if (!this.activated) {
            return null;
          }
    
          this.viewContainerRef.detach();
          const cmp = this.activated;
          this.activated = null;
    
          return cmp;
        }
    
        private activateWith() {
          const config: CacheConfig = (this.config.find(value => value.path === this.path) ?? {}) as unknown as CacheConfig;
          this.snapshot = new ActivatedCacheSnapshot(this.path, this.params, config.data || {});
          // 创建新的注入器,该注入器是动态组件的注入器,也就是说在对应的组件中可以获取this.snapshot
          const injector = Injector.create({
            providers: [
              { provide: ActivatedCacheSnapshot, useValue: this.snapshot },
            ],
            parent: this.injector
          });
    
          this.activated = this.viewContainerRef.createComponent(config.component, {
            injector,
            index: this.viewContainerRef.length
          })
        }
    
        private stateChange() {
          if (!this.activated) {
            this.activateWith()
          } else {
            const config = this.config.find(value => value.path === this.path)?.data ?? {};
            const snapshot = new ActivatedCacheSnapshot(this.path, this.params, config);
            // 判断是否需要从缓存中重用组件
            if (this.cacheStrategy.shouldAttach(snapshot)) {
              this.detachAndStore();
              // 取缓存
              const stored = <DetachedCacheHandle>this.cacheStrategy.retrieve(snapshot);
              this.activated = stored.componentRef;
              this.snapshot = snapshot;
              // 使用缓存的视图
              this.viewContainerRef.insert(stored.componentRef.hostView);
            } else {
              this.detachAndStore();
              this.activateWith()
            }
          }
        }
    
        private detachAndStore() {
          // 如果该组件需要缓存 则缓存视图
          if (this.cacheStrategy.shouldDetach(this.snapshot!)) {
            const componentRef = this.detach() || this.activated!;
            this.cacheStrategy.store(this.snapshot!, {componentRef})
          } else {
            // 将当前的视图从dom中分离
            this.detach()
          }
        }
      }
    

    AppModule

     import { NgModule } from '@angular/core';
     import { BrowserModule } from '@angular/platform-browser';
    
     import { AppRoutingModule } from './app-routing.module';
     import { AppComponent } from './app.component';
     import {CacheModule} from "./cahce-view/cache.module";
     import {Tab1Component} from "./tab1.component";
     import {Tab2Component} from "./tab2.component";
    
     @NgModule({
       declarations: [
         AppComponent
       ],
       imports: [
         BrowserModule,
         AppRoutingModule,
         // 注册配置 跟路由类似
         CacheModule.forRoot([
           {
             path: 'tab1',
             component: Tab1Component,
             data: {
               name: 'tab1'
             }
           },
           {
             path: 'tab2',
             component: Tab2Component,
             data: {
               name: 'tab2'
             }
           }
         ])
       ],
       providers: [],
       bootstrap: [AppComponent],
     })
     export class AppModule { }
    

    从视图组件中获取ActivatedCacheSnapshot

    import {Component, OnInit} from '@angular/core';
    import { CommonModule } from '@angular/common';
    import {ActivatedCacheSnapshot} from "./cache-view-strategy.service";
    
    @Component({
      selector: 'app-tab1',
      standalone: true,
      imports: [CommonModule],
      template: `
        <p>
          tab1 works!
        </p>
      `,
      styles: [
      ]
    })
    export class Tab1Component implements OnInit {
      // 获取ActivatedCacheSnapshot 类似获取路由快照
      constructor(private readonly snapshot: ActivatedCacheSnapshot) {
        console.log(this.snapshot);
      }
      ngOnInit() {
        // 切换视图看这里输出就知道该组件是否缓存了
        console.log('ngOnInit tab1')
      }
    }
    

    使用效果

    效果看上去跟router-view类似

    angular封装组件缓存复用的组件(仿路由复用)

    不管如何切换,被缓存了的组件都只初始化了一次

    angular封装组件缓存复用的组件(仿路由复用)

总结

所有源码github.com/chengdongha…

组件复用是我看了路由模块源码之后找到的灵感,上述也只是简单实现,也算是抛砖引玉了,实际使用还有很多需要完善的地方,比如params目前我并没有实现,这个是一个很常用的参数,因为不同参数渲染一个组件是再常见不过的事了。