引言
Alice
测试上线,发现包体积太大,加载太慢。决定启用懒加载与预加载加速加载速度。
整三天,课也没去上。改得时候特别痛苦,哭了,为什么没有早点发现惰性加载这个东西。
星期一,重新设计前台架构,重构前台代码。
星期二,分模块加载,启用惰性加载与预加载。
星期三,修改单元测试,添加provide
。
星期四,写PPT
。
星期五,.NET
考试。
重构前台之后,觉得自己当前设计的架构很合理,遂分享出来,供大家学习交流。
架构
理论
架构理论主要参考外国老哥的一篇文章,Angular (2+): Core vs Shared Modules
CoreModule
:核心模块,只被 AppModule
引用,保证全局单例。
ShareModule
:共享模块,被各业务模块引用,存储各模块必备的组件、管道以及模板。
实践
CoreModule
核心Module
,全局只导入一次。
称之为核心,因为没有它应用跑不起来。
核心模块存放拦截器和服务,不过与正常的有些区别。
拦截器
@Injectable()
export class YunzhiInterceptor implements HttpInterceptor {}
@NgModule({
imports: [
NgZorroAntdModule,
RouterModule
],
providers: [{provide: HTTP_INTERCEPTORS, useClass: YunzhiInterceptor, multi: true}
]
})
export class CoreModule {}
服务
@Injectable({providedIn: CoreModule})
export class CollegeService {}
现在不往 root
里注入了,因为发现有的时候写 root
有人会搞不清楚模块的层级关系,然后就懵圈了。
为了规避这种问题,直接注入到核心模块中,防止有人误解。
norm
其实是想起一个规范的英文的,但是 spec
却被测试给用了,所以就去百度翻译了个放这了。
这个包主要是存储数据规范的。
entity
存储实体,对应后台实体。
target
存储自定义的规范对象,历史的教训告诉我们,如果把所有都放到实体包里,这很糟糕。
page
这个是向小程序抄来的,小组件可以复用,大组件就需要单建目录了,都放一起看着混乱。
分模块加载,每个功能一个单独的模块,模块职责划分清晰。
@NgModule({
declarations: [SetupComponent],
imports: [
SetupRouteModule,
ShareModule
]
})
export class SetupModule {}
模块中就这几行,什么废话都不要写,就声明本模块的组件,并导入本模块的路由和 Share
模块。其他的都不要写,第三方的导入交给 ShareModule
去处理。本模块只负责业务,不负责代码。
ShareModule
全局复用的组件,全局复用的管道,全局复用的验证器,以及其他第三方组件的导入导出。
@NgModule({
imports: [
ComponentModule,
PipeModule,
RouterModule
],
exports: [
ComponentModule,
PipeModule,
RouterModule
]
})
export class ShareModule {}
规规矩矩,整整洁洁。
ShareModule
的子模块的实现都放在 api
目录里。
子模块示例:
@NgModule({
declarations: [
CourseTypePipe,
SemesterStatusPipe,
YunzhiGradeStatusPipe,
YunzhiKlassStatusPipe,
YunzhiScoreStatusPipe
],
exports: [
CourseTypePipe,
SemesterStatusPipe,
YunzhiGradeStatusPipe,
YunzhiKlassStatusPipe,
YunzhiScoreStatusPipe
]
})
export class PipeModule {}
spec
测试目录,为什么单拿出来这个目录,主要是为了解决 Service
的测试数据问题。
本模块存储所有以 .test.service.ts
结尾的测试service
。
然后所有的测试 Service
去继承原 Service
,并重写里面的方法,这里的@Injectable()
注解中不用加 providedIn
,因为没有专业的测试模块,每个测试用例中我们使用provide
进行注入。
@Injectable()
export class CollegeTestService extends CollegeService {}
原来直接跑 ng test
特别快,根本看不清楚组件的创建,这两天发现了一个新套路,跑测试的时候上 YouTube
点开个视频看,然后电脑就特别卡,测试跑的时候就慢了。
然后就可以清楚地看到每个测试用例的执行过程,看到每个组件如何创建并显示。也不知道改了哪里,现在 Alice
跑测试的时候最后给出一个 Karma
的测试报告。
直接把错误报出来,也好修改。
测试
测试最后怎么设计的呢?也说不明白,看代码就是了。最近才发现之前的测试用例写得都不正确。
其实一个测试,就是构建了一个测试的模块,该模块和其他模块都相同。
发现很多测试中为了能跑过,直接把公共组件或公共管道写在了 declarations
里,这是不合理的,虽然测试能跑过,但是理论上,这个测试就是测这个的,所以模块中的 declarations
只有它自己。
然后这里的 imports
也是经过反复的测试,导入 BrowserAnimationsModule
、HttpClientTestingModule
、RouterTestingModule
、ShareModule
这四个模块,这个测试的所有依赖就都有了,其他的什么都不要导入,看着混乱。
providers
声明本模块中要注入的对象,这里受益于 CoreModule
的设计,Service
都放在了 CoreModule
里,而业务模块是不能导入 CoreModule
的,只能导入ShareModule
,所以模块中是用不了已有的Service
。
所以,乖乖地给我建一个测试的Service
,然后注入进去。也算是强制组员写测试的一种手段。
describe('SetupComponent', () => {
let component: SetupComponent;
let fixture: ComponentFixture<SetupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({declarations: [SetupComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
RouterTestingModule,
ShareModule
],
providers: [
{
provide: UserService,
useClass: UserTestService
}
]
}).compileComponents();}));
beforeEach(() => {fixture = TestBed.createComponent(SetupComponent);
component = fixture.componentInstance;
fixture.detectChanges();});
it('should create', () => {expect(component).toBeTruthy();});
});
总结
最佳实践,都是从坑里爬出来后才总结出来的。
上次我感慨 Angular
架构设计难的时候是 4
月26
日,当时只是对 Alice
进行小改,还不到一月,如今一次性对前台做了这么大的改动,坑也踩得多了,爬出坑后,最佳实践,其实就在眼前。
这次的架构设计得很整洁,打完包后也很快,我很满意。
古人学问无遗力,少壮工夫老始成。
纸上得来终觉浅,绝知此事要躬行。
——陆游《冬夜读书示子聿》以后再设计不能纸上谈兵,要努力去实践,经历得多了,最佳实践自然就出来了。