MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular模块的共享模块设计

2022-05-274.3k 阅读

共享模块的概念与意义

在大型 Angular 应用开发中,模块的合理划分与共享是提高代码可维护性和复用性的关键。共享模块(Shared Module)是 Angular 中一种特殊的模块,它专门用于收集那些可以在多个其他模块中复用的组件、指令和管道。

想象一下,在一个企业级应用中,可能存在多个功能模块,如用户管理模块、订单管理模块、报表模块等。这些模块中可能会有一些通用的 UI 组件,比如按钮样式统一的按钮组件、用于格式化日期的管道等。如果每个模块都单独实现这些功能,将会导致大量的代码重复,增加维护成本。而共享模块的出现,就是为了解决这个问题。

共享模块将这些通用的部分集中起来,其他模块只需导入共享模块,就可以直接使用其中的组件、指令和管道,大大提高了代码的复用性。

创建共享模块

  1. 使用 Angular CLI 创建共享模块 使用 Angular CLI 创建共享模块非常简单。在项目的根目录下,打开终端并执行以下命令:
ng generate module shared

这条命令会在 src/app 目录下创建一个名为 shared 的模块文件结构,包含 shared.module.ts 文件。

  1. 共享模块的基本结构 打开 shared.module.ts 文件,其初始内容大致如下:
import { NgModule } from '@angular/core';

@NgModule({
  declarations: [],
  imports: [],
  exports: []
})
export class SharedModule { }
  • declarations 数组用于声明该模块内的组件、指令和管道。
  • imports 数组用于导入该模块所依赖的其他模块。
  • exports 数组用于指定哪些声明的组件、指令和管道要暴露给其他模块使用。

向共享模块添加组件

  1. 创建可复用组件 假设我们要创建一个通用的加载指示器组件。使用 Angular CLI 执行以下命令:
ng generate component shared/loading - -module = shared

这会在 shared 模块目录下创建 loading 组件,并且会自动将其声明在 shared.module.tsdeclarations 数组中。

loading.component.ts 的代码可能如下:

import { Component } from '@angular/core';

@Component({
  selector: 'app-loading',
  templateUrl: './loading.component.html',
  styleUrls: ['./loading.component.css']
})
export class LoadingComponent { }

loading.component.html 的简单代码如下:

<div class="loading-spinner">
  Loading...
</div>

loading.component.css 用于定义加载指示器的样式:

.loading-spinner {
  color: blue;
  font - size: 20px;
}
  1. 将组件添加到共享模块的导出列表 为了让其他模块能够使用这个 LoadingComponent,我们需要将其添加到 shared.module.tsexports 数组中:
import { NgModule } from '@angular/core';
import { LoadingComponent } from './loading/loading.component';

@NgModule({
  declarations: [LoadingComponent],
  imports: [],
  exports: [LoadingComponent]
})
export class SharedModule { }

这样,其他模块导入 SharedModule 后,就可以在其模板中使用 <app - loading> 标签了。

向共享模块添加指令

  1. 创建自定义指令 假设我们要创建一个指令,用于将输入元素自动聚焦。使用 Angular CLI 创建指令:
ng generate directive shared/autofocus - -module = shared

autofocus.directive.ts 的代码如下:

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appAutofocus]'
})
export class AutofocusDirective {
  constructor(private elementRef: ElementRef) { }

  @HostListener('focus')
  onFocus() {
    this.elementRef.nativeElement.focus();
  }
}
  1. 将指令添加到共享模块shared.module.ts 中,将 AutofocusDirective 添加到 declarationsexports 数组中:
import { NgModule } from '@angular/core';
import { LoadingComponent } from './loading/loading.component';
import { AutofocusDirective } from './autofocus/autofocus.directive';

@NgModule({
  declarations: [LoadingComponent, AutofocusDirective],
  imports: [],
  exports: [LoadingComponent, AutofocusDirective]
})
export class SharedModule { }

现在,其他模块导入 SharedModule 后,就可以在模板中的输入元素上使用 appAutofocus 指令,如 <input type="text" appAutofocus>

向共享模块添加管道

  1. 创建自定义管道 假设我们要创建一个管道,用于将字符串首字母大写。使用 Angular CLI 创建管道:
ng generate pipe shared/capitalize - -module = shared

capitalize.pipe.ts 的代码如下:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {
  transform(value: string): string {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }
}
  1. 将管道添加到共享模块shared.module.ts 中,将 CapitalizePipe 添加到 declarationsexports 数组中:
import { NgModule } from '@angular/core';
import { LoadingComponent } from './loading/loading.component';
import { AutofocusDirective } from './autofocus/autofocus.directive';
import { CapitalizePipe } from './capitalize/capitalize.pipe';

@NgModule({
  declarations: [LoadingComponent, AutofocusDirective, CapitalizePipe],
  imports: [],
  exports: [LoadingComponent, AutofocusDirective, CapitalizePipe]
})
export class SharedModule { }

这样,其他模块导入 SharedModule 后,就可以在模板中使用 capitalize 管道,如 {{ 'hello' | capitalize }}

共享模块的依赖管理

  1. 导入共享模块所需的模块 共享模块本身可能也依赖其他模块。例如,我们的 LoadingComponent 使用了 Angular 的 CommonModule 来支持基本的模板语法。所以,需要在 shared.module.tsimports 数组中导入 CommonModule
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoadingComponent } from './loading/loading.component';
import { AutofocusDirective } from './autofocus/autofocus.directive';
import { CapitalizePipe } from './capitalize/capitalize.pipe';

@NgModule({
  declarations: [LoadingComponent, AutofocusDirective, CapitalizePipe],
  imports: [CommonModule],
  exports: [LoadingComponent, AutofocusDirective, CapitalizePipe]
})
export class SharedModule { }
  1. 避免重复导入模块 需要注意的是,共享模块导入的模块,不应该在导入共享模块的其他模块中再次重复导入。例如,CommonModule 只应在 SharedModule 中导入,其他导入 SharedModule 的模块无需再导入 CommonModule。这是因为 Angular 的模块系统会确保模块的单例性,重复导入可能会导致一些意想不到的问题。

共享模块与懒加载模块的关系

  1. 懒加载模块对共享模块的使用 在 Angular 应用中,经常会使用懒加载模块来提高应用的加载性能。懒加载模块同样可以使用共享模块。假设我们有一个懒加载的用户模块,其 user.module.ts 如下:
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  imports: [SharedModule],
  // 其他模块配置
})
export class UserModule { }

这样,用户模块就可以使用 SharedModule 中导出的组件、指令和管道了。由于懒加载模块是按需加载的,所以共享模块中的代码也会在需要时才被加载,不会影响应用的初始加载性能。

  1. 共享模块在懒加载场景下的优化 为了进一步优化性能,在共享模块中应尽量避免导入那些不必要的模块。例如,如果某些模块只有在特定的非懒加载模块中才使用,就不应该将其导入到共享模块中。这样可以减小懒加载模块的代码体积,提高加载速度。

共享模块的设计原则

  1. 高内聚低耦合 共享模块中的组件、指令和管道应该具有高内聚性,即它们应该围绕一个相对独立的功能集合。例如,将所有的 UI 组件相关的通用部分放在一个共享模块,将所有数据处理相关的管道放在另一个共享模块。同时,共享模块与其他模块之间应保持低耦合,尽量减少对特定业务模块的依赖。
  2. 单一职责原则 每个共享模块应该有单一的职责。例如,一个共享模块专门负责处理 UI 相关的复用,另一个共享模块专门处理数据格式化相关的复用。这样可以使共享模块的功能清晰,易于维护和扩展。
  3. 避免过度共享 虽然共享模块可以提高代码复用性,但过度共享也会带来问题。如果将一些只在少数几个模块中使用的功能放入共享模块,会增加共享模块的复杂性和体积。因此,在决定是否将某个功能放入共享模块时,需要权衡其复用性和模块的复杂性。

共享模块与全局状态管理

  1. 共享模块中的服务与全局状态 共享模块中也可以包含服务。但是,对于那些用于管理全局状态的服务,需要特别注意。例如,如果有一个用于管理用户登录状态的服务,将其放在共享模块中时,要确保它在整个应用中是单例的。在 Angular 中,服务默认是单例的,但如果在共享模块中导入了一些可能会影响服务单例性的模块,就需要小心处理。
  2. 与 NgRx 等状态管理库的结合 在使用 NgRx 等状态管理库时,共享模块可以与状态管理进行良好的结合。例如,可以在共享模块中创建一些通用的效果(Effects)或选择器(Selectors),供其他模块使用。假设我们有一个通用的加载状态管理,在共享模块中可以创建一个 LoadingEffects 和相关的选择器,用于管理不同组件的加载状态。
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, switchMap } from 'rxjs/operators';
import { someApiCall } from './some - api - service';
import { loadData, loadDataSuccess } from './loading - actions';

@Injectable()
export class LoadingEffects {
  loadData$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadData),
      switchMap(() =>
        someApiCall().pipe(
          map(data => loadDataSuccess({ data }))
        )
      )
    )
  );

  constructor(private actions$: Actions) { }
}

然后在共享模块的 NgModule 配置中,将 LoadingEffects 添加到 providers 数组中:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoadingComponent } from './loading/loading.component';
import { AutofocusDirective } from './autofocus/autofocus.directive';
import { CapitalizePipe } from './capitalize/capitalize.pipe';
import { LoadingEffects } from './loading - effects';
import { StoreModule } from '@ngrx/store';
import { loadingReducer } from './loading - reducer';

@NgModule({
  declarations: [LoadingComponent, AutofocusDirective, CapitalizePipe],
  imports: [
    CommonModule,
    StoreModule.forFeature('loading', loadingReducer)
  ],
  exports: [LoadingComponent, AutofocusDirective, CapitalizePipe],
  providers: [LoadingEffects]
})
export class SharedModule { }

这样,其他导入共享模块的模块就可以使用这个通用的加载状态管理逻辑。

共享模块的测试

  1. 组件测试 对于共享模块中的组件,如 LoadingComponent,测试方法与普通组件测试类似。在 loading.component.spec.ts 中编写测试用例:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoadingComponent } from './loading.component';

describe('LoadingComponent', () => {
  let component: LoadingComponent;
  let fixture: ComponentFixture<LoadingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [LoadingComponent]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(LoadingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
  1. 指令测试 对于 AutofocusDirective,测试其功能是否正常。在 autofocus.directive.spec.ts 中编写测试:
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform - browser';
import { AutofocusDirective } from './autofocus.directive';

@Component({
  template: `<input type="text" appAutofocus>`
})
class TestComponent { }

describe('AutofocusDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let inputEl: DebugElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [AutofocusDirective, TestComponent]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(TestComponent);
    inputEl = fixture.debugElement.query(By.css('input'));
    fixture.detectChanges();
  });

  it('should autofocus the input', () => {
    expect(inputEl.nativeElement === document.activeElement).toBe(true);
  });
});
  1. 管道测试 对于 CapitalizePipe,在 capitalize.pipe.spec.ts 中编写测试:
import { CapitalizePipe } from './capitalize.pipe';

describe('CapitalizePipe', () => {
  it('should transform "hello" to "Hello"', () => {
    const pipe = new CapitalizePipe();
    const result = pipe.transform('hello');
    expect(result).toBe('Hello');
  });
});

通过对共享模块中各个部分进行全面的测试,可以确保共享模块的稳定性和可靠性,为整个应用的质量提供保障。

共享模块在实际项目中的应用案例

  1. 电商项目中的共享模块 在一个电商项目中,可能有商品展示模块、购物车模块、订单模块等。共享模块可以包含一些通用的组件,如商品卡片组件,用于展示商品的基本信息,包括图片、名称、价格等。这个商品卡片组件可以在商品展示模块和购物车模块中复用。
// product - card.component.ts
import { Component } from '@angular/core';
import { Product } from '../models/product.model';

@Component({
  selector: 'app - product - card',
  templateUrl: './product - card.component.html',
  styleUrls: ['./product - card.component.css']
})
export class ProductCardComponent {
  product: Product;

  constructor() { }
}
<!-- product - card.component.html -->
<div class="product - card">
  <img [src]="product.imageUrl" alt="{{product.name}}">
  <h3>{{product.name}}</h3>
  <p>{{product.price | currency:'USD'}}</p>
</div>

在共享模块中,将 ProductCardComponent 声明并导出:

import { NgModule } from '@angular/core';
import { ProductCardComponent } from './product - card/product - card.component';

@NgModule({
  declarations: [ProductCardComponent],
  imports: [],
  exports: [ProductCardComponent]
})
export class SharedModule { }

然后在商品展示模块和购物车模块中导入共享模块,就可以使用 <app - product - card> 组件了。

  1. 企业办公系统中的共享模块 在企业办公系统中,可能有员工管理模块、任务管理模块、文档管理模块等。共享模块可以包含一些通用的指令,比如权限指令。例如,CanAccessDirective 用于根据用户的权限来决定是否显示某个元素。
// can - access.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from '../services/auth.service';

@Directive({
  selector: '[appCanAccess]'
})
export class CanAccessDirective {
  @Input() set appCanAccess(permission: string) {
    const hasPermission = this.authService.hasPermission(permission);
    if (hasPermission) {
      this.viewContainerRef.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainerRef.clear();
    }
  }

  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef, private authService: AuthService) { }
}

在共享模块中,将 CanAccessDirective 声明并导出:

import { NgModule } from '@angular/core';
import { CanAccessDirective } from './can - access/can - access.directive';

@NgModule({
  declarations: [CanAccessDirective],
  imports: [],
  exports: [CanAccessDirective]
})
export class SharedModule { }

在各个功能模块的模板中,就可以使用 appCanAccess 指令,如 <button appCanAccess="create - task">创建任务</button>

通过这些实际项目案例可以看出,共享模块在提高代码复用性、降低开发成本方面具有重要作用,能够有效地提升 Angular 应用的开发效率和质量。