Angular模块的共享模块设计
共享模块的概念与意义
在大型 Angular 应用开发中,模块的合理划分与共享是提高代码可维护性和复用性的关键。共享模块(Shared Module)是 Angular 中一种特殊的模块,它专门用于收集那些可以在多个其他模块中复用的组件、指令和管道。
想象一下,在一个企业级应用中,可能存在多个功能模块,如用户管理模块、订单管理模块、报表模块等。这些模块中可能会有一些通用的 UI 组件,比如按钮样式统一的按钮组件、用于格式化日期的管道等。如果每个模块都单独实现这些功能,将会导致大量的代码重复,增加维护成本。而共享模块的出现,就是为了解决这个问题。
共享模块将这些通用的部分集中起来,其他模块只需导入共享模块,就可以直接使用其中的组件、指令和管道,大大提高了代码的复用性。
创建共享模块
- 使用 Angular CLI 创建共享模块 使用 Angular CLI 创建共享模块非常简单。在项目的根目录下,打开终端并执行以下命令:
ng generate module shared
这条命令会在 src/app
目录下创建一个名为 shared
的模块文件结构,包含 shared.module.ts
文件。
- 共享模块的基本结构
打开
shared.module.ts
文件,其初始内容大致如下:
import { NgModule } from '@angular/core';
@NgModule({
declarations: [],
imports: [],
exports: []
})
export class SharedModule { }
declarations
数组用于声明该模块内的组件、指令和管道。imports
数组用于导入该模块所依赖的其他模块。exports
数组用于指定哪些声明的组件、指令和管道要暴露给其他模块使用。
向共享模块添加组件
- 创建可复用组件 假设我们要创建一个通用的加载指示器组件。使用 Angular CLI 执行以下命令:
ng generate component shared/loading - -module = shared
这会在 shared
模块目录下创建 loading
组件,并且会自动将其声明在 shared.module.ts
的 declarations
数组中。
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;
}
- 将组件添加到共享模块的导出列表
为了让其他模块能够使用这个
LoadingComponent
,我们需要将其添加到shared.module.ts
的exports
数组中:
import { NgModule } from '@angular/core';
import { LoadingComponent } from './loading/loading.component';
@NgModule({
declarations: [LoadingComponent],
imports: [],
exports: [LoadingComponent]
})
export class SharedModule { }
这样,其他模块导入 SharedModule
后,就可以在其模板中使用 <app - loading>
标签了。
向共享模块添加指令
- 创建自定义指令 假设我们要创建一个指令,用于将输入元素自动聚焦。使用 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();
}
}
- 将指令添加到共享模块
在
shared.module.ts
中,将AutofocusDirective
添加到declarations
和exports
数组中:
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>
。
向共享模块添加管道
- 创建自定义管道 假设我们要创建一个管道,用于将字符串首字母大写。使用 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);
}
}
- 将管道添加到共享模块
在
shared.module.ts
中,将CapitalizePipe
添加到declarations
和exports
数组中:
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 }}
。
共享模块的依赖管理
- 导入共享模块所需的模块
共享模块本身可能也依赖其他模块。例如,我们的
LoadingComponent
使用了 Angular 的CommonModule
来支持基本的模板语法。所以,需要在shared.module.ts
的imports
数组中导入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 { }
- 避免重复导入模块
需要注意的是,共享模块导入的模块,不应该在导入共享模块的其他模块中再次重复导入。例如,
CommonModule
只应在SharedModule
中导入,其他导入SharedModule
的模块无需再导入CommonModule
。这是因为 Angular 的模块系统会确保模块的单例性,重复导入可能会导致一些意想不到的问题。
共享模块与懒加载模块的关系
- 懒加载模块对共享模块的使用
在 Angular 应用中,经常会使用懒加载模块来提高应用的加载性能。懒加载模块同样可以使用共享模块。假设我们有一个懒加载的用户模块,其
user.module.ts
如下:
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
@NgModule({
imports: [SharedModule],
// 其他模块配置
})
export class UserModule { }
这样,用户模块就可以使用 SharedModule
中导出的组件、指令和管道了。由于懒加载模块是按需加载的,所以共享模块中的代码也会在需要时才被加载,不会影响应用的初始加载性能。
- 共享模块在懒加载场景下的优化 为了进一步优化性能,在共享模块中应尽量避免导入那些不必要的模块。例如,如果某些模块只有在特定的非懒加载模块中才使用,就不应该将其导入到共享模块中。这样可以减小懒加载模块的代码体积,提高加载速度。
共享模块的设计原则
- 高内聚低耦合 共享模块中的组件、指令和管道应该具有高内聚性,即它们应该围绕一个相对独立的功能集合。例如,将所有的 UI 组件相关的通用部分放在一个共享模块,将所有数据处理相关的管道放在另一个共享模块。同时,共享模块与其他模块之间应保持低耦合,尽量减少对特定业务模块的依赖。
- 单一职责原则 每个共享模块应该有单一的职责。例如,一个共享模块专门负责处理 UI 相关的复用,另一个共享模块专门处理数据格式化相关的复用。这样可以使共享模块的功能清晰,易于维护和扩展。
- 避免过度共享 虽然共享模块可以提高代码复用性,但过度共享也会带来问题。如果将一些只在少数几个模块中使用的功能放入共享模块,会增加共享模块的复杂性和体积。因此,在决定是否将某个功能放入共享模块时,需要权衡其复用性和模块的复杂性。
共享模块与全局状态管理
- 共享模块中的服务与全局状态 共享模块中也可以包含服务。但是,对于那些用于管理全局状态的服务,需要特别注意。例如,如果有一个用于管理用户登录状态的服务,将其放在共享模块中时,要确保它在整个应用中是单例的。在 Angular 中,服务默认是单例的,但如果在共享模块中导入了一些可能会影响服务单例性的模块,就需要小心处理。
- 与 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 { }
这样,其他导入共享模块的模块就可以使用这个通用的加载状态管理逻辑。
共享模块的测试
- 组件测试
对于共享模块中的组件,如
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();
});
});
- 指令测试
对于
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);
});
});
- 管道测试
对于
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');
});
});
通过对共享模块中各个部分进行全面的测试,可以确保共享模块的稳定性和可靠性,为整个应用的质量提供保障。
共享模块在实际项目中的应用案例
- 电商项目中的共享模块 在一个电商项目中,可能有商品展示模块、购物车模块、订单模块等。共享模块可以包含一些通用的组件,如商品卡片组件,用于展示商品的基本信息,包括图片、名称、价格等。这个商品卡片组件可以在商品展示模块和购物车模块中复用。
// 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>
组件了。
- 企业办公系统中的共享模块
在企业办公系统中,可能有员工管理模块、任务管理模块、文档管理模块等。共享模块可以包含一些通用的指令,比如权限指令。例如,
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 应用的开发效率和质量。