学习目标

  • 学会抽象代码,减少重复工作

文件结构

本节内容仍然主要聚焦于CoreModule

src/core
├── base
│   ├── data.service.ts
│   ├── index.ts
│   ├── repository.ts
│   ├── subscriber.ts
│   └── tree.repository.ts
├── constants.ts
├── core.module.ts
├── decorators
│   ├── dto-validation.decorator.ts
│   └── index.ts
├── helpers.ts
├── index.ts
├── providers
│   ├── app.filter.ts
│   ├── app.interceptor.ts
│   ├── app.pipe.ts
│   └── index.ts
└── types.ts

应用编码

核心代码

BaseRepository

这是一个通用的基础存储类,继承自自带的Repository

  • queryName属性是一个抽象属性,在子类中设置,用于在构建查询时提供默认模型的查询名称
  • buildBaseQuery方法用于构建基础查询
  • getQueryName方法用于获取queryName
// src/core/base/repository.ts
export abstract class BaseRepository<
    Entity extends ObjectLiteral,
> extends Repository {
    protected abstract queryName: string;
    buildBaseQuery(): SelectQueryBuilder
    getQueryName():string
}

TreeRepository

默认的TreeRepository基类的方法如findRoots等无法在QueryBuilder中实现排序,自定义query函数等,所以创建一个继承自默认基类的新的TreeRepository来实现

在实现此类之前先添加如下类型

// src/core/types.ts
/**
 * 排序类型,{字段名称: 排序方法}
 * 如果多个值则传入数组即可
 * 排序方法不设置,默认DESC
 */
type OrderQueryType =
    | string
    | { name: string; order: 'DESC' | 'ASC' }
    | Array<{ name: string; order: 'DESC' | 'ASC' } | string>;
/**
 * 查询参数
 * orderBy: 排序类型
 * getQuery: 查询回调,可以在这个函数中添加自定义查询
 */
type TreeQueryParam = {
    getQuery?: (query: SelectQueryBuilder) => SelectQueryBuilder;
    orderBy?: OrderQueryType;
};

TreeRepository包含BaseRepositoryqueryName等所有属性和方法

其余属性及方法列如下

如果params中不传orderBy则使用this.orderBy属性

  • findTree: 为findTrees添加添加参数
  • findRts: 为findRoots列表查询添加条件参数
  • findDts: 为findDescendants添加条件参数
  • findDtsTree: 为findDescendantsTree添加条件参数
  • countDts: 为countDescendants添加条件参数
  • createDtsQueryBuilder: 为createDescendantsQueryBuilder添加条件参数
  • findAts,findAtsTree,countAts,createAtsQueryBuilderDTS的方法类似,都是为对应的原方法添加条件查询参数
  • toFlatTrees: 打平并展开树
  • getOrderByQuery: 根据orderBy属性生成排序的query
// src/core/base/base.repository.ts
export abstract class BaseTreeRepository<
    E extends ObjectLiteral,
> extends TreeRepository {
    // 自定义排序规则,如果没有设置则(entity中有`order`字段则使用`order`字段,否则不排序)
    protected orderBy?: string | { name: string; order: 'DESC' | 'ASC' };
    buildBaseQuery(): SelectQueryBuilder
    getQueryName()
    // 查询树
    async findTree(params: TreeQueryParam = {}): Promise
    // 查询顶层列表
    findRts(params: TreeQueryParam = {}): Promise
    // 查询后代列表
    findDts(entity: E, params: TreeQueryParam = {}): Promise
    // 查询后代树
    findDtsTree(entity: E, params: TreeQueryParam = {}): Promise 
    // 查询后代数量
    countDts(entity: E, params: TreeQueryParam = {}): Promise
    // 创建后代查询器
    createDtsQueryBuilder(
        alias: string,
        closureTableAlias: string,
        entity: E,
        params: TreeQueryParam = {},
    ): SelectQueryBuilder
    // 查询祖先列表
    findAts(entity: E, params: TreeQueryParam = {}): Promise 
    //  查询祖先树
    findAtsTree(entity: E, params: TreeQueryParam = {}): Promise
    // 查询祖先数量
    countAts(entity: E, params: TreeQueryParam = {}): Promise
    // 创建祖先查询器
    createAtsQueryBuilder(
        alias: string,
        closureTableAlias: string,
        entity: E,
        params: TreeQueryParam = {},
    ): SelectQueryBuilder
    // 打平并展开树
    async toFlatTrees(trees: E[], level = 0): Promise
    // 生成排序的query
    protected getOrderByQuery(
        query: SelectQueryBuilder,
        alias: string,
        orderBy?: OrderQueryType,
    )
}

BaseSubscriber

添加一个SubcriberSetting类型用于添加设置

export type SubcriberSetting = {
    // 监听的模型是否为树模型
    tree?: boolean;
};

在构造函数中根据传入的参数设置连接,并在连接中加入当前订阅者,以及构建默认的repository

实现如下

// src/core/base/subscriber.ts
@EventSubscriber()
export abstract class BaseSubscriber
    implements EntitySubscriberInterface
{
...

    constructor(connection: Connection, repository?: Type>) {
        this.connection = connection;
        this.connection.subscribers.push(this);
        this.em = this.connection.manager;
        this.setRepository(repository);
        if (!this.setting) this.setting = {};
    }
    listenTo()
    async afterLoad(entity: any) {
        // 是否启用树形
        if (this.setting.tree && !entity.level) entity.level = 0;
    }
    protected setRepository(repository?: Type>) 
    // 判断某个属性是否被更新
    protected isUpdated(cloumn: keyof E, event: UpdateEvent) 
}

DataService

此类目的在于封装和简化一些常用的数据操作

更改PaginateDto使它支持泛型参数传入

// src/core/types.ts
export interface PaginateDto
    extends Omit, 'page' | 'limit'> {
    page: number;
    limit: number;
}

对于createupdate方法因为子类需要变化的地方比较多,所以直接交给子类去实现,如果子类没有实现则直接抛出403异常.repository属性则在子类中必须被定义,可使用依赖直接注入

// src/core/base/data.service.ts
export abstract class BaseDataService<
    E extends ObjectLiteral,
    P extends Record = {},
    M extends IPaginationMeta = IPaginationMeta,
> {
    // 服务默认存储类
    protected abstract repository: BaseRepository | BaseTreeRepository;
    // 获取数据列表
    async list(params?: P, callback?: QueryHook): Promise
    // 获取分页数据
    async paginate(
        options: PaginateDto,
        params?: P,
        callback?: QueryHook,
    ): Promise>
    // 获取数据详情
    async detail(id: string, callback?: QueryHook): Promise
    // 创建数据,如果子类没有实现则抛出404
    create(data: any): Promise
    // 更新数据,如果子类没有实现则抛出404
    update(data: any): Promise
    // 删除数据
    async delete(id: string)
    // 获取查询单个项目的QueryBuilder
    protected async getItemQuery(
        query: SelectQueryBuilder,
        callback?: QueryHook,
    )
    // 获取查询数据列表的 QueryBuilder
    protected async getListQuery(
        query: SelectQueryBuilder,
        params: P,
        callback?: QueryHook,
    )
    // 如果是树形模型,则此方法返回父项
    protected async getParent(id?: string)
}

修改应用

subscribers

使CategorySubscriberPostSubscriber分别继承BaseSubscriber,以CategorySubscriber为例,如下

CategoryEntity是一个树形模型,所以需要在设置中添加tree

// src/modules/content/subscribers/category.subscriber.ts
@EventSubscriber()
export class CategorySubscriber extends BaseSubscriber {
    protected entity = CategoryEntity;

    protected setting: SubcriberSetting = {
        tree: true,
    };

    constructor(protected connection: Connection) {
        super(connection, CategoryRepository);
    }
}

Services

三个服务类都继承BaseDataService,省略掉各自一些在父类中已经实现而无需修改的方法,以CategoryService为例,如下

createupdate方法需要自己封装

// src/modules/content/services/category.service.ts
export class CategoryService extends BaseDataService {
    protected entity = CategoryEntity;

    constructor(
        protected entityManager: EntityManager,
        protected repository: CategoryRepository,
    ) {
        super();
    }
  ...
}