引言
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;
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
进行小改,还不到一月,如今一次性对前台做了这么大的改动,坑也踩得多了,爬出坑后,最佳实践,其实就在眼前。
这次的架构设计得很整洁,打完包后也很快,我很满意。
古人学问无遗力,少壮工夫老始成。
纸上得来终觉浅,绝知此事要躬行。
——陆游《冬夜读书示子聿》以后再设计不能纸上谈兵,要努力去实践,经历得多了,最佳实践自然就出来了。