V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lichnow
V2EX  ›  Node.js

Nestjs 最佳实践教程:4 排序,分页与过滤

  •  
  •   lichnow · 2022-07-10 09:45:39 +08:00 · 3803 次点击
    这是一个创建于 902 天前的主题,其中的信息可能已经有所发展或是发生改变。

    另,本人在找工作中,希望能有远程工作匹配(无法去外地),有需要的老板可以看一下我的个人介绍: https://pincman.com/about

    学习目标

    • 重载 TreeRepository 自带方法来对树形结构的数据进行扁平化处理
    • 对 Typeorm 查询出的数据列表进行分页处理
    • 通过请求中的 query 查询对数据进行筛选处理,比如排序,过滤等
    • 实现发布文章和取消发布的功能
    • Typeorm 模型事件和 Subscriber(订阅者)的使用
    • 使用sanitize-html对文章内容进行防注入攻击处理

    预装依赖

    ~ pnpm add nestjs-typeorm-paginate sanitize-html deepmerge && pnpm add @types/sanitize-html -D
    

    文件结构

    创建文件

    cd src/modules/content && \
    mkdir subscribers && \
    touch dtos/query-category.dto.ts \
    dtos/query-post.dto.ts \
    subscribers/post.subscriber.ts \
    subscribers/index.ts \
    services/sanitize.service.ts \
    && cd ../../../
    

    与上一节一样,这一节的新增和修改集中于ContentModule

    src/modules/content
    ├── constants.ts
    ├── content.module.ts
    ├── controllers
    │   ├── category.controller.ts
    │   ├── comment.controller.ts
    │   ├── index.ts
    │   └── post.controller.ts
    ├── dtos
    │   ├── create-category.dto.ts
    │   ├── create-comment.dto.ts
    │   ├── create-post.dto.ts
    │   ├── index.ts
    │   ├── query-category.dto.ts
    │   ├── query-post.dto.ts
    │   ├── update-category.dto.ts
    │   └── update-post.dto.ts
    ├── entities
    │   ├── category.entity.ts
    │   ├── comment.entity.ts
    │   ├── index.ts
    │   └── post.entity.ts
    ├── repositories
    │   ├── category.repository.ts
    │   ├── comment.repository.ts
    │   ├── index.ts
    │   └── post.repository.ts
    ├── services
    │   ├── category.service.ts
    │   ├── comment.service.ts
    │   ├── index.ts
    │   ├── post.service.ts
    │   └── sanitize.service.ts
    └── subscribers
        ├── index.ts
        └── post.subscriber.ts
    

    应用编码

    这节多了一个新的概念,即subscriber,具体请查阅typeorm文档,当然你也可以在模型中使用事件处理函数,效果没差别

    模型

    CategoryEntity

    代码:src/modules/content/entities/category.entity.ts

    • 添加order字段用于排序
    • 添加level属性(虚拟字段)用于在打平树形数据的时候添加当前项的等级

    PostEntity

    代码: src/modules/content/entities/post.entity.ts

    type字段的类型用enum枚举来设置,首先需要定义一个PostBodyTypeenum类型,可以添加一个constants.ts文件来统一定义这些enum和常量

    • 添加publishedAt字段用于控制发布时间和发布状态
    • 添加 type字段用于设置发布类型
    • 添加customOrder字段用于自定义排序

    存储类

    CategoryRepository

    代码: src/modules/content/repositories/category.repository.ts

    因为CategoryRepository继承自TreeRepository,所以我们在typeorm源码中找到这个类,并对部分方法进行覆盖,如此我们就可以对树形分类进行排序,覆盖的方法如下

    当然后面会讲到更加深入的再次封装,此处暂时先这么用

    • findRoots 为根分类列表查询添加排序
    • createDescendantsQueryBuilder 为子孙分类查询器添加排序
    • createAncestorsQueryBuilder 为祖先分类查询器添加排序

    DTO 验证

    新增QueryCategoryDtoQueryPostDto用于查询分类和文章时进行分页以及过滤数据和设置排序类型等

    在添加DTO之前,现在添加几个数据转义函数,以便把请求中的字符串改成需要的数据类型

    // src/core/helpers.ts
    
    // 用于请求验证中的 number 数据转义
    export function tNumber(value?: string | number): string |number | undefined
    // 用于请求验证中的 boolean 数据转义
    export function tBoolean(value?: string | boolean): string |boolean | undefined
    // 用于请求验证中转义 null
    export function tNull(value?: string | null): string | null | undefined
    

    修改create-category.dto.tscreate-comment.dto.tsparent字段的@Transform装饰器

    export class CreateCategoryDto {
    ...
        @Transform(({ value }) => tNull(value))
        parent?: string;
    }
    

    添加一个通用的DTO接口类型

    // src/core/types.ts
    
    // 分页验证 DTO 接口
    export interface PaginateDto {
        page: number;
        limit: number;
    }
    

    QueryCategoryDto

    代码: src/modules/content/dtos/query-category.dto.ts

    • page属性设置当前分页
    • limit属性设置每页数据量

    QueryPostDto

    除了与QueryCateogryDto一样的分页属性外,其它属性如下

    • orderBy用于设置排序类型
    • isPublished根据发布状态过滤文章
    • category过滤出一下分类及其子孙分类下的文章

    orderBy字段是一个enum类型的字段,它的可取值如下

    • CREATED: 根据创建时间降序
    • UPDATED: 根据更新时间降序
    • PUBLISHED: 根据发布时间降序
    • COMMENTCOUNT: 根据评论数量降序
    • CUSTOM: 根据自定义的order字段升序

    服务类

    SanitizeService

    代码: src/modules/content/services/sanitize.service.ts

    此服务类用于clean html

    sanitize方法用于对 HTML 数据进行防注入处理

    CategoryService

    代码:src/modules/content/services/category.service.ts

    添加一个辅助函数,用于对打平后的树形数据进行分页

    // src/core/helpers.ts
    export function manualPaginate<T extends ObjectLiteral>(
        { page, limit }: PaginateDto,
        data: T[],
    ): Pagination<T>
    

    新增paginate(query: QueryCategoryDto)方法用于处理分页

    async paginate(query: QueryCategoryDto) {
        // 获取树形数据
        const tree = await this.findTrees();
        // 打平树形数据
        const list = await this.categoryRepository.toFlatTrees(tree);
        // 调用手动分页函数进行分页
        return manualPaginate(query, list);
    }
    

    PostService

    代码:src/modules/content/services/post.service.ts

    • getListQuery: 用于构建过滤与排序以及通过分类查询文章数据等功能的query构建器
    • paginate: 调用getListQuery生成query,并作为nestjs-typeorm-paginate paginate的参数对数据进行分页
    async paginate(params: FindParams, options: IPaginationOptions) {
        const query = await this.getListQuery(params);
        return paginate<PostEntity>(query, options);
    }
    

    订阅者

    PostSubscriber

    代码: src/modules/content/subscribers/post.subscriber.ts

    • beforeInsert(插入数据前事件): 如果在添加文章的同时发布文章,则设置当前时间为发布时间
    • beforeUpdate(更新数据前事件): 更改发布状态会同时更新发布时间的值,如果文章更新为未发布状态,则把发布时间设置为 null
    • afterLoad(加载数据后事件): 对 HTML 类型的文章内容进行去标签处理防止注入攻击

    一个需要注意的点是需要在subcriber类的构造函数中注入Connection才能获取链接

       constructor(
            connection: Connection,
            protected sanitizeService: SanitizeService,
        ) {
            connection.subscribers.push(this);
        }
    

    注册订阅者

    把订阅者注册成服务后,由于在构造函数中注入了connection这个连接对象,所以typeorm会自动把它加载到这个默认连接的subscribers配置中

    // src/modules/content/subscribers/post.subscriber.ts
    import * as SubscriberMaps from './subscribers';
    const subscribers = Object.values(SubscriberMaps);
    @Module({
        ....
        providers: [...subscribers, ...dtos, ...services],
    })
    

    控制器

    CategoryController

    代码: src/modules/content/controllers/category.controller.ts

    • list: 通过分页来查找扁平化的分类列表
    • index: 把 url 设置成 @Get('tree')
        @Get()
        // 分页查询
        async list(
            @Query(
                new ValidationPipe({
                    transform: true,
                    forbidUnknownValues: true,
                    validationError: { target: false },
                }),
            )
            query: QueryCategoryDto,
        ) {
            return this.categoryService.paginate(query);
        }
    
        // 查询树形分类
        @Get('tree')
        async index() {
            return this.categoryService.findTrees();
        }
    

    PostController

    代码: src/modules/content/controllers/post.controller.ts

    修改index方法用于分页查询

    // 通过分页查询数据
    async index(
            @Query(
                new ValidationPipe({
                    transform: true,
                    forbidUnknownValues: true,
                    validationError: { target: false },
                }),
            )
            { page, limit, ...params }: QueryPostDto,
        ) {
            return this.postService.paginate(params, { page, limit });
        }
    
    11 条回复    2022-07-11 13:19:37 +08:00
    putaozhenhaochi
        1
    putaozhenhaochi  
       2022-07-10 10:10:34 +08:00 via Android
    大哥 不在这搞 seo 行吗
    golangLover
        2
    golangLover  
       2022-07-10 10:17:54 +08:00 via Android
    支持一下,辛苦了
    dinjufen
        3
    dinjufen  
       2022-07-10 11:10:46 +08:00   ❤️ 1
    网站订阅有点贵,但是内容又不多
    lichnow
        4
    lichnow  
    OP
       2022-07-10 11:29:16 +08:00
    @dinjufen 还可以啦,教程每天都会更新,后面 react18 立马也上线了
    Kipp
        5
    Kipp  
       2022-07-10 11:59:30 +08:00
    昨晚看还是全免费,今早就订阅者免费了,建议发推广
    lichnow
        6
    lichnow  
    OP
       2022-07-10 12:10:44 +08:00
    @Kipp 没办法,跟合伙人闹掰辞职了,在找工作,缺钱啊😄
    chenzhe
        7
    chenzhe  
       2022-07-10 23:38:53 +08:00 via iPhone
    昨晚点开看了一下
    说实在的,对于初学者来说,讲得太浅显,对于真正后端开发的熟手来说,好像又犯不着看这样的教程。
    感觉定位有点儿尴尬。
    而且这样的帖子明显是推广贴,应该换一个节点发。

    真的做付费视频,希望能够讲的更加详细一些,带着新手一步一步把 nestjs 吃透。
    数据库操作这边,选 mongoose 或者 typeorm 详细的讲一下。
    lichnow
        8
    lichnow  
    OP
       2022-07-11 10:11:55 +08:00
    @chenzhe 视频才更到 5 啊,后面还有 30 集,很多东西得一步步深入不是?像 typeorm 5-10 是专门讲解的,用户系统 11-15 讲解,16-18 就讲 RABC,不可能几集就能全部讲完啊
    lichnow
        9
    lichnow  
    OP
       2022-07-11 10:20:15 +08:00
    @chenzhe 并且看个视频肯定是无法直接吃透的,需要跟着视频里的做,然后有无法理解的东西在提问这样才行
    chenzhe
        10
    chenzhe  
       2022-07-11 12:36:31 +08:00
    @lichnow 的确才更新了一点儿,但是看前五集就能知道你这个视频并不是针对初学者的。至于跟着视频做,看了视频,连为啥这么做都不知道。
    lichnow
        11
    lichnow  
    OP
       2022-07-11 13:19:37 +08:00
    @chenzhe 光看视频肯定不行,需要跟着源代码一步步实现,我每一集的源代码单独一个包,然后实现过程中有问题(比如不知道为啥这么做)可以群里或者问答频道提问,我会耐心解答
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2748 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 13:54 · PVG 21:54 · LAX 05:54 · JFK 08:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.