目录
- 前言
- midway
- 初始化
- 默认配置(config)
- 接口编写(controller)
- 服务(service)
- 参数校验(dto)
- jwt生成token
- 测试(test)
- swagger
- typeorm
- 创建实体(entity)
- 使用实体创建数据库连接(data-source.ts)
- 数据库使用(model)
- midway+typeorm
- 尾言
前言
近期学到了nodejs搭建企业级应用后台的一种解决方案,midway搭建的系统化服务端和typeorm对数据库的应用,记录一下。
midway
本文只做部分内容介绍,详见官方文档:midway。
初始化
$ npm init midway
因为是纯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;
在这里我们可以改服务启动的端口port
,默认是7001。
接口编写(controller)
我把原有的模板改成了这样,无论是get
还是post
请求都可以参照下面的例子仿写。
在这里我们还可以对query
和body
的入参进行参数校验,会在后面提到。
/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 };
}
}
服务(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',
};
}
}
而getUser
服务的需要的入参类型IUserOptions
,我们可以在interface.ts
文件中定义好。
/src/interface.ts
/**
* @description User-Service parameters
*/
export interface IUserOptions {
uid: number;
}
参数校验(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('') // 非必填字段传入空字符串
/src/dto/user.dto.ts
import { Rule, RuleType } from '@midwayjs/validate';
export class GetUserDTO {
@Rule(RuleType.number().required())
uid: number;
}
我们编辑好校验文件之后,需要进行两步:
- 要在接口加入
@Validate
装饰器。 - 然后对入参
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 };
}
}
jwt生成token
jwt
用于生成用户的token
,进行以下几步:
默认的项目可能没有,我们需要手动添加依赖:
$ yarn add @midwayjs/jwt
在/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;
然后就可以在controller
文件编写的接口中使用,使用时:
- 引入
JwtService
。 - 添加
jwt
的@Inject()
装饰器。 - 调用
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,
},
};
}
}
测试(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的对象、值结构严格相等
/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,
});
});
});
有时候我们还需要对接口请求时间进行严格控制,比如控制在一秒内。
我们可以在根目录下添加jest.setup.js
文件,将超时时长控制在1000(单位ms)。
// jest.setup.js
jest.setTimeout(1000);
然后引入根目录下的jest.config.js
文件:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], // 读取 jest.setup.js
};
swagger
$ yarn add @midwayjs/swagger@3 swagger-ui-dist
在 configuration.ts
中增加组件。
import { Configuration } from '@midwayjs/decorator';
import * as swagger from '@midwayjs/swagger';
@Configuration({
imports: [
// ...
swagger
]
})
export class MainConfiguration {
}
加入默认测试入参。
我们在参数校验内容的部分提到dto
文件,用于声明入参类型,我们可以在该文件中加入入参的swagger测试入参:
- 引入
ApiProperty
。 - 使用
@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;
}
启动之后访问(如果修改了默认端口,7001
也要同步修改):
typeorm
用于处理数据库操作的库,我们可以在midway项目基础上引入以下包来使用。
本文只做部分内容介绍,我们这里的示例使用电脑的内存数据库sqlite
,其余数据库使用方法详见文档地址:TypeORM 中文文档。
建议使用yarn
,npm
有时候会出现sqlite3
安装失败的问题。
$ yarn add reflect-metadata typeorm sqlite3
创建实体(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;
}
使用实体创建数据库连接(data-source.ts)
在src
下新建data-source.ts
文件,将实体引入,再引入reflect-metadata
和typeorm
的连接数据库方法。
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,
});
数据库使用(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 });
}
}
其中使用我们如何得到可操作数据库的对象呢,就是上面的这两句
const ds = await dbConnection;
const db = await ds.getRepository(UserEntity);
接下来我们就可以用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)
midway+typeorm
记得我说过,我们可以在midway
的service
中进行数据库操作,所以只要在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);
}
}
尾言
本文只是针对完全没有midway和typeorm基础的同学,内容没有涉及很全,如果还想要深入了解,还得去官网查询,或者对我提出建议或者问题,我会适当增加文章内容。