4

...

过了一遍 Angular 文档 的小伙伴大致都会记得最佳实践中提到过的有关CoreModule的一些解释和说明,其实关于名字的命名不是强制性的,只要团队中一致 pass,你把它命名为XXXModule都无所谓。但是最主要的,还是我们需要理解“core”的作用以及在项目中发挥更好的作用和地位。

我记录下我项目中对“core”的一些拙略见解和搭配。

core目录

纵观整个Angular项目结构以及最佳实践,我们通常把项目按功能划分文件夹,比如工具、共享、全局核心、页面模块组件、公用模块组件等等,“core”在这里相当于全局核心类型的范围,那全局核心类型到底是聚集了项目的哪些功能呢?我的理解是我们可以把 全局单例服务、只需要引入一次 的东西都归并到这里。

  • 全局单例服务:这些服务在整个应用生命周期内只存在一个实例,也就是数据是全局互通的,而在 Angular 中实现单例服务就需要一个中间提供商(module)来做中介,也就是所谓的“CoreModule”,然后在根模块引用一次便可全局使用,这也是官方推荐的一种单例服务做法。然而在 Angular 6 + 版本后,官方为 Injectable 装饰器提供了 providedIn: 'root' 的选项,让声明的服务直接成为单例服务,此后再不用通过“CoreModule”来提供服务,但是我们的单例服务仍然可以放在 core目录 中,通过 路径别名 配置来直接访问服务,因为实际上,单例服务只会乖乖在 core目录中 ,不会再有其他东西来干扰。
  • 只需引入一次的?:什么是项目中只需要引入一次的?举个例子,全局错误处理、根路由数据预加载、http请求拦截器等。这些都是通过一次配置就能一直用到老的东西,而且不可能会有其他兄弟来直接使用的东西,顺理成章就需要归并到 core目录 中,并且有的需要被“CoreModule”引用,有的需要被“AppModule”引用。

我列举来几个更加详细的例子来说说这些类别:

clipboard.png

应用初始数据加载

在开发单页应用特别是管理系统的时候,可能项目的构成除了中心主系统还衍生了很多个子项目系统,这种情况下登录授权一般都是在主系统完成,然后前后端通过单点登录确保子系统能使用。这时子系统一般都是一个新的项目,我们都知道 Angular 提供了强大的路由功能,可以通过路由守卫来预加载系统,然而我们需要的授权信息是相对整个应用而不是某个路由而言的,那这个时候我们就需要一个根级别的数据预加载功能来完成授权等功能。

Angular 还是帮你开辟好了入口,这时我们只需要一个APP_INITIALIZER就可以完成预加载。前提是我们定义好了预加载的数据操作逻辑,举个例子:

/**
 * app 初始化前身份验证操作
 */
@Injectable({
    providedIn: 'root'
})
export class AppInitAuthService {
    constructor(
        ...,
        private userInfoService: UserInfoService,
    ) { }

    /** 验证当前token身份 */
    tokenAuth(): Promise<any> {
        return new Promise((resolve,reject) => {
            return this.userInfoService.getUserInfoServer().subscribe(res => {
                if(res.reasonCode == 'notLoggedIn'){
                    //未登录 
                    //可以进行取消授权处理
                    ...
                }else{
                    //获取了授权数据,todo ...
                    resolve(true)
                }
            })
        })
     }
}

此处声明了一个基本的用户授权信息获取服务,接下来我们可以直接通过APP_INITIALIZER来完成数据预加载功能,只需要在 CoreModule 中声明刚才提供的处理服务,Angular 会自动在根组件初始化前查询并执行 APP_INITIALIZER 所注入的所有服务函数,由于我们提供的是一个 Promise 对象,所以 Angular 会等待执行结果:

@NgModule({
    ...
    providers: [
        ...
        {
            provide: APP_INITIALIZER,
            multi: true,
            useFactory: (appInit: AppInitAuthService) => {
                return () => appInit.tokenAuth()
            },
            deps: [AppInitAuthService]
        }
    ]
})
export class CoreModule {
    constructor(
        ...
    ) {}
}

只要AppModule引用了CoreModule,项目会自动完成预授权处理功能,完全无需其他组件掺入。


全局错误处理

有时候我们需要全局错误处理机制。比如我们编译更新了项目版本,多个某个模块功能,但是用户这边并没有去实时刷新,当意外去到某个原本不存在的路由时 Angular 会捕获到找不到模块的错误,这是我们就可以提前在错误处理中去对用户进行较友好的提示等等;又比如我们会想要去接入前端监控平台像 fundebug 等等,具体对实现方式也是一样通过 Angular 提供的捕错功能来实现。

一个最简单的错误处理服务如下:

import { ErrorHandler } from '@angular/core'

export class HandleCommon extends ErrorHandler{
    constructor(){
        super()
    }

    handleError(error: Error){
        //注意调基类处理函数,不然会覆盖默认行为,比如控制台不会看到报错
        super.handleError(error)
        if (/Loading chunk [\d]+ failed/.test(error.message)) {
            //捕获找不到模块 (服务端目录数据变动)
            ...
        } 
        //... 各种错误处理    
    }
}

然后我们直接在AppModule中声明一个 ErrorHandler 令牌对应的服务,就可以实现全局错误监听处理:

import { NgModule, ErrorHandler } from '@angular/core'
import { HandleCommon } from '../core'

@NgModule({
    ...
    providers: [
        ...
        {
            provide: ErrorHandler,
            useFactory: () => {
                return new HandleCommon()
            }
        }
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }

http请求拦截器

尽管 Angular 提供了十分漂亮的 HttpClient 给开发者舒服地进行网络请求操作,但是有很多针对网络请求的需求需要我们自己去开发,像 http 超时拦截、token 拦截、错误处理拦截等等,这些也都属于一次引用,全局使用的范畴。更漂亮的是 Angular 为我们提供了拦截器接口,我们只管开发拦截器逻辑功能,调用及使用全部控制权都在框架内。由于拦截器涉及比较多东西,这里放一个最为简单的实现如下:

import {
    HttpInterceptor,
    HttpRequest,
    HttpHandler,
    HttpEvent
} from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'

//拦截器 - 添加请求头
@Injectable()
export class TokenInterceptor implements HttpInterceptor {

    constructor() { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        //通过某些逻辑获取 token
        let token = 'xxxx'
        if (token) {
            token = `Bearer ${token}`
            req = req.clone({
                setHeaders: {
                    Authorization: token
                }
            })
        }
        return next.handle(req)
    }
}

只需要在 CoreModule 中通过 HTTP_INTERCEPTORS 令牌来声明我们写好的拦截器,框架会在正确的时机自动处理和调用拦截器逻辑:

@NgModule({
    providers: [
        ...,
        {
            provide: HTTP_INTERCEPTORS,
            useClass: TokenInterceptor,
            //必须设置,拦截器是个数组集合而不仅仅只有一个
            multi: true
        }
    ]
})
export class CoreModule {
    constructor() {}
}

一些单例服务等等

应用中或多或少有一些需要在全局流通的数据,比如全局的用户信息管理:

@Injectable({
    providedIn: 'root'
})
export class UserInfoService {
    //用户数据 全局共享 数据流
    userInfo$: BehaviorSubject<UserInfo> = new BehaviorSubject<UserInfo>(null)

    constructor() { }

    /**
     * 获取用户数据
     */
    getUserInfoServer(): Observable<UserInfo> {
        ...
    }

    /**
     * 退出登录
     */
    getUserLogoutServer(): Observable<boolean> {
        ...
    }
}

作为频繁被存取的介质,单例模式自然而然是它的特点,所以最好也一起归并到所谓的 core目录 中。

因人而异

前面列举了一些常用的类别来说明 core目录 以及 “CoreModule” 存在的意义。除了一些需要“CoreModule”来作为桥梁的例子,貌似 core目录 并不是必须要存放某些东西的,比如全局的单例对象就完全可以单独使用其他的文件夹来存放维护。是对的,没有一个统一的标准来约束我们到底是要去如何组织代码目录结构,所有项目都是因人而异,自己觉得舒服的、可维护的才最重要。

本记录只是为了更加贴近官方最佳实践而如此组织,纯粹作为一个记录以及给大家的一个参考。


oceania
2.2k 声望110 粉丝