• nodejs midway+typeorm搭建后台方案


    前言

    近期学到了nodejs搭建企业级应用后台的一种解决方案,midway搭建的系统化服务端和typeorm对数据库的应用,记录一下。

    midway

    本文只做部分内容介绍,详见官方文档:midway

    初始化

    $ npm init midway
    
    • 1

    因为是纯nodejs后台,我们选择默认的koa类型。

    最后输入项目名称,一个初始化项目就诞生了。

    默认配置(config)

    /src/config/config.default.ts

    import { MidwayConfig } from '@midwayjs/core';
    
    export default {
      // use for cookie sign key, should change to your own and keep security
      keys: '1661308181665_796',
      koa: {
        port: 7001,
      },
    } as MidwayConfig;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里我们可以改服务启动的端口port,默认是7001。

    接口编写(controller)

    我把原有的模板改成了这样,无论是get还是post请求都可以参照下面的例子仿写。

    在这里我们还可以对querybody的入参进行参数校验,会在后面提到。

    /src/controller/api.controller.ts

    import {
      Inject,
      Controller,
      Get,
      Query,
      Post,
      Body,
    } from '@midwayjs/decorator';
    import { Context } from '@midwayjs/koa';
    import { UserService } from '../service/user.service';
    
    // 表示下列所有接口的前缀为 /api
    @Controller('/api')
    export class APIController {
      @Inject()
      ctx: Context;
    
      @Inject()
      userService: UserService;
    
      // 结合前缀,该接口完整为 /api/get_user
      @Get('/get_user')
      async getUser(@Query() query) {
        const { uid } = query;
        const user = await this.userService.getUser({ uid });
        return { success: true, message: 'OK', data: user };
      }
      // 结合前缀,该接口完整为 /api/post_user
      @Post('/post_user')
      async postUser(@Body() body) {
        const { uid } = body;
        const user = await this.userService.getUser({ uid });
        return { success: true, message: 'OK', data: user };
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    服务(service)

    在上面接口中我们会发现使用到了service的服务。

    我来解释一下流程,

    一般api接口通过get或者post请求得到用户的入参,根据用户调用的接口要触发的行为,去调用服务。

    比如这里的接口是要通过传入uid得到用户的信息,我们就调用getUser服务,传入从接口得到的uid入参,通过服务来返回内容,这里的内容一般还需要通过数据库去查询

    /src/service/user.service.ts

    import { Provide } from '@midwayjs/decorator';
    import { IUserOptions } from '../interface';
    
    @Provide()
    export class UserService {
      async getUser(options: IUserOptions) {
        // 这里只是伪造的数据,实际项目需要通过数据库去进行真实的查询
        return {
          uid: options.uid,
          username: 'mockedName',
          phone: '12345678901',
          email: 'xxx.xxx@xxx.com',
        };
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    getUser服务的需要的入参类型IUserOptions,我们可以在interface.ts文件中定义好。

    /src/interface.ts

    /**
     * @description User-Service parameters
     */
    export interface IUserOptions {
      uid: number;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参数校验(dto

    前面我们提到api接口的入参我们可以进行参数校验,如果我们每次都在接口中进行判空,判断长度限制等操作,过于繁琐,所以我们需要使用@midwayjs/validate

    这里需要先提到/src/configuration.ts文件,可以简单理解为它是用来引入一些依赖的文件。

    可以看到koa项目默认已经引入validate了,如果你的项目没有,需要自己引入一下。

    在这里插入图片描述
    引入了之后,我们创建一个dto文件夹,在里面编写参数校验文件。

    Rule括号中就是对参数的校验,一个参数头顶对应一个Rule

    这里的RuleType.number().required()表示入uid必须是一个非空的数字

    常见的校验还有

    RuleType.number().required();               // 数字,必填
    RuleType.string().empty('')                 // 字符串非必填
    RuleType.number().max(10).min(1);           // 数字,最大值和最小值
    RuleType.number().greater(10).less(50);     // 数字,大于 10,小于 50
    
    RuleType.string().max(10).min(5);           // 字符串,长度最大 10,最小 5
    RuleType.string().length(20);               // 字符串,长度 20
    RuleType.string().pattern(/^[abc]+$/);      // 字符串,匹配正则格式
    
    RuleType.object().length(5);                // 对象,key 数量等于 5
    
    
    RuleType.array().items(RuleType.string());  // 数组,每个元素是字符串
    RuleType.array().max(10);                   // 数组,最大长度为 10
    RuleType.array().min(10);                   // 数组,最小长度为 10
    RuleType.array().length(10);                // 数组,长度为 10
    
    RuleType.string().allow('')                 // 非必填字段传入空字符串
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    /src/dto/user.dto.ts

    import { Rule, RuleType } from '@midwayjs/validate';
    
    export class GetUserDTO {
      @Rule(RuleType.number().required())
      uid: number;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们编辑好校验文件之后,需要进行两步:

    1. 要在接口加入@Validate装饰器。
    2. 然后对入参body或者query进行类型绑定,这里例子绑定的是GetUserDTO
    // ...
    import { Validate } from '@midwayjs/validate';
    import { GetUserDTO } from '../dto/user.dto';
    
    @Controller('/api')
    export class APIController {
      @Inject()
      ctx: Context;
    
      @Inject()
      userService: UserService;
    
      @Post('/post_user')
      @Validate() // 开启功能需要在这里加入@Validate装饰器
      async postUser(@Body() body: GetUserDTO) {
        const { uid } = body;
        const user = await this.userService.getUser({ uid });
        return { success: true, message: 'OK', data: user };
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    jwt生成token

    jwt用于生成用户的token,进行以下几步:

    默认的项目可能没有,我们需要手动添加依赖:

    $ yarn add @midwayjs/jwt
    
    • 1

    /src/configuration.ts文件中导入:

    在这里插入图片描述

    然后在/src/config/config.default.ts下加入jwt配置

    import { MidwayConfig } from '@midwayjs/core';
    
    export default {
      // use for cookie sign key, should change to your own and keep security
      keys: '1661225754650_9760',
      koa: {
        port: 3000,
      },
      jwt: {
        secret: 'mySecret', // key
        expiresIn: 60 * 60 * 24, // token储存时长
      },
    } as MidwayConfig;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后就可以在controller文件编写的接口中使用,使用时:

    1. 引入JwtService
    2. 添加jwt@Inject()装饰器。
    3. 调用this.jwt.sign()即可生成token。
    // ...
    import { JwtService } from '@midwayjs/jwt';
    
    @Controller('/api')
    export class APIController {
    // ...
    
      @Inject()
      jwt: JwtService;
    
      @Post('/user/login')
      @Validate()
      async loginUser(@Body() body: UserLoginDTO) {
          const user = {
            id: 1,
            username: 'admin',
            password: '123456',
          };
          const token = await this.jwt.sign({ ...user });
          return {
            code: 200,
            result: 'success',
            message: '登录成功',
            data: {
              token,
            },
          };
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    测试(test)

    midway的项目采用jest来进行测试。

    test文件夹中新建比如user.test.ts文件。

    我在这里提供一个可以复用app,可以在一个文件中执行多项test的模板。

    一个it函数就是一个测试。

    接口调用可以仿写,重点关注expect断言,

    这里只用到了这些,所有断言详见官方文档:jest断言

    expect(x).toBe(y) // 代表测试x必须等于y
    expect(x).toMatchObject(y); // 代表测试x对象包含y,也就是简单理解为y是x的子集
    expect(x).toHaveProperty(y); // 代表x对象要包含y属性,y可以嵌套
    expect(x).toStrictEqual(y); // 代表x要和y的对象、值结构严格相等
    
    • 1
    • 2
    • 3
    • 4

    /test/user.test.ts

    import {createApp, close, createHttpRequest} from '@midwayjs/mock';
    import {Framework, Application} from '@midwayjs/koa';
    
    describe('test/controller/user.test.ts', () => {
    
      let app: Application;
    
      beforeAll(async () => {
        // 只创建一次 app,可以复用
        try {
          // 由于Jest在BeforeAll阶段的error会忽略,所以需要包一层catch
          // refs: https://github.com/facebook/jest/issues/8688
          app = await createApp<Framework>();
        } catch (err) {
          console.error('test beforeAll error', err);
          throw err;
        }
      });
    
      afterAll(async () => {
        // close app
        await close(app);
      });
    
      it('正常登录测试', async () => {
        const result = await createHttpRequest(app).post('/api/user/login').send({
          username: 'jack',
          password: 'redballoon'
        });
    
        expect(result.status).toBe(200);
        expect(result.body).toMatchObject({
          code: 200,
          result: 'success',
          message: '登录成功',
        });
        expect(result.body).toHaveProperty('data.token')
      });
    
      it('异常登录测试', async () => {
        const result = await createHttpRequest(app).post('/api/user/login').send({
          username: 'jack',
          password: 'xxxxx'
        });
    
        expect(result.status).toBe(200);
        expect(result.body).toStrictEqual({
          code: 400,
          result: 'error',
          message: '账号或密码不正确',
          data: null,
        });
      });
    
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    有时候我们还需要对接口请求时间进行严格控制,比如控制在一秒内。

    我们可以在根目录下添加jest.setup.js文件,将超时时长控制在1000(单位ms)。

    // jest.setup.js
    jest.setTimeout(1000);
    
    • 1
    • 2

    然后引入根目录下的jest.config.js文件:

    // jest.config.js
    module.exports = {
      preset: 'ts-jest',
      testEnvironment: 'node',
      testPathIgnorePatterns: ['/test/fixtures'],
      coveragePathIgnorePatterns: ['/test/'],
      setupFilesAfterEnv: ['/jest.setup.js'],  // 读取 jest.setup.js
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    swagger

    $ yarn add @midwayjs/swagger@3 swagger-ui-dist 
    
    • 1

    configuration.ts 中增加组件。

    import { Configuration } from '@midwayjs/decorator';
    import * as swagger from '@midwayjs/swagger';
    
    @Configuration({
      imports: [
        // ...
        swagger
      ]
    })
    export class MainConfiguration {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    加入默认测试入参。

    我们在参数校验内容的部分提到dto文件,用于声明入参类型,我们可以在该文件中加入入参的swagger测试入参:

    1. 引入ApiProperty
    2. 使用@ApiProperty装饰器。

    /src/dto/user.dto.ts

    import { Rule, RuleType } from '@midwayjs/validate';
    import { ApiProperty } from '@midwayjs/swagger';
    
    export class GetUserDTO {
      @Rule(RuleType.number().required())
      @ApiProperty({
        example: '123456',
        description: '入参uid',
      })
      uid: number;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    启动之后访问(如果修改了默认端口,7001也要同步修改):

    1. ui: http://127.0.0.1:7001/swagger-ui/index.html
    2. json: http://127.0.0.1:7001/swagger-ui/index.json

    在这里插入图片描述

    typeorm

    用于处理数据库操作的库,我们可以在midway项目基础上引入以下包来使用。

    本文只做部分内容介绍,我们这里的示例使用电脑的内存数据库sqlite,其余数据库使用方法详见文档地址:TypeORM 中文文档

    建议使用yarnnpm有时候会出现sqlite3安装失败的问题。

    $ yarn add reflect-metadata typeorm sqlite3
    
    • 1

    创建实体(entity)

    在创建sqlite数据库连接之前,我们先要创造一个数据实体,就是数据库要存放的数据对象类型。

    我们在src下新建一个entity文件夹,然后我们可以创造一个这样的实体:

    可以定义类型、含义等内容。

    /src/entity/user.entity.ts

    import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
    
    @Entity()
    export class UserEntity {
      // PrimaryGeneratedColumn修饰,自动递增的主键
      @PrimaryGeneratedColumn({
        type: 'int',
        name: 'id',
        comment: '用户的自增ID',
      })
      id: number;
      
      // Column修饰,其他属性
      @Column('varchar', {
        name: 'username',
        comment: '用户名',
        length: 64,
      })
      username: string;
    
      @Column('varchar', {
        name: 'password',
        nullable: true,
        comment: '用户密码',
        length: 64,
      })
      password: string | null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    使用实体创建数据库连接(data-source.ts)

    src下新建data-source.ts文件,将实体引入,再引入reflect-metadatatypeorm的连接数据库方法。

    database这里:memory:指定的是内存数据库,如果想要创建可供观察的数据库,可以改成database.sqlite这样的,带sqlite后缀,会在项目根目录下生成sqlite文件作为数据库。

    import 'reflect-metadata';
    import { createConnection } from 'typeorm';
    import { UserEntity } from './entity/User.entity';
    
    export const dbConnection = createConnection({
      type: 'sqlite',
      database: ':memory:',
      dropSchema: true,
      entities: [UserEntity],
      synchronize: true,
      logging: false,
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    数据库使用(model)

    既然有连接了,接下来我们只要在需要使用的地方使用就好了。

    我们在src下创建model文件夹,创建有一些数据库操作的model文件。

    /src/model/user.model.ts

    import { Repository } from 'typeorm';
    import { InjectEntityModel } from '@midwayjs/typeorm';
    import { UserEntity } from '../entity/User.entity';
    import { dbConnection } from '../data-source';
    
    export class UserModel {
      @InjectEntityModel(UserEntity)
      userRepo: Repository<UserEntity>;
    
      /**
       * 根据用户名和密码获取用户信息
       * @param username {String} 用户名
       * @param password {String} 用户密码
       */
      async getUserByUsernameAndPassword(username, password): Promise<UserEntity> {
        const ds = await dbConnection;
        const db = await ds.getRepository(UserEntity);
        return await db.findOneBy({ username, password });
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    其中使用我们如何得到可操作数据库的对象呢,就是上面的这两句

        const ds = await dbConnection;
        const db = await ds.getRepository(UserEntity);
    
    • 1
    • 2

    接下来我们就可以用db进行一些操作:

        // 增
        const saveUser = new UserEntity();
        saveUser.username = 'jack';
        saveUser.password = 'redballoon';
        await db.save(saveUser);
        
        // 查
        const findUser = await db.findBy({id: 1})
        
        // 删(先查再删)
        const findUser = await db.findBy({id: 1})
        await db.remove(findUser)
       
        // 改(先查再改再存)
        const findUser = await db.findOneBy({id: 2})
        findUser.username = 'updatename'
        await db.save(findUser)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    midway+typeorm

    记得我说过,我们可以在midwayservice中进行数据库操作,所以只要在service文件中引入model中的各种数据库操作拿来使用即可。

    import { Provide } from '@midwayjs/decorator';
    import { IUserOptions } from '../interface';
    import { UserModel } from '../model/user.model';
    
    const userModel = new UserModel();
    
    @Provide()
    export class UserService {
      async loginUser(options: IUserOptions) {
        const { username, password } = options;
        return await userModel.getUserByUsernameAndPassword(username, password);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    尾言

    本文只是针对完全没有midway和typeorm基础的同学,内容没有涉及很全,如果还想要深入了解,还得去官网查询,或者对我提出建议或者问题,我会适当增加文章内容。

    如果本文对你有帮助的话,欢迎点赞收藏,感谢~。

  • 相关阅读:
    深入理解Java虚拟机读书笔记--10Java内存模型
    暑假打工 2 个 月,让我明白了 Keepalived 高可用的三种路由方案
    GPT-4o模型到底有多强
    Unity 2018发布在iOS 16.3偶尔出现画面不动的问题
    1743. 从相邻元素对还原数组-哈希表法
    基于神经网络的车牌识别系统在matlab上如何实现
    直播课堂系统08-腾讯云对象存储和课程分类管理
    iNFTnews | 元宇宙会终结智能手机时代吗?
    python下拉框选择测试
    MySQL查询的执行流程
  • 原文地址:https://blog.csdn.net/weixin_43877799/article/details/126499324