快速开始

让我们从头开始写一个简单的模块 「Todo」,快速上手 BSV。代码位置:src/modules/todo/

数据库结构 Entity

定义 entity

src/modules/todo/entity/Todo.ts

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
import { CreatedUpdatedAt } from "/src/entity/extends/CreatedUpdated";
export enum TodoStatus {
todo = "todo",
doing = "doing",
completed = "completed",
deleted = "deleted",
}
export const todoStatus = Object.values(TodoStatus);
const entityName = "todo_item";
@Entity({ name: entityName })
export class TodoItem extends CreatedUpdatedAt {
static entityName = entityName;
@PrimaryGeneratedColumn()
id: number;
@Column()
/** 标题 */
title: string;
@Column({ type: "text" })
/** 状态(这里指定下数据库类型,因为 typeorm 生成真正的枚举类型,会存在一些编译问题) */
status: TodoStatus;
@Column({ nullable: true })
/** 备注 */
remark?: string;
}

注意:

Column 注释不能这么写:

/** 标题 */
@Column()
title: string;

目前导出脚本只支持匹配星号注释符,需要这么写:

@Column()
/** 标题 */
title: string;

注册 entity

src/modules/todo/entity/index.ts

import { registerEntites } from "/src/entity/entites-register";
import { TodoItem } from "./Todo";
export * from "./Todo";
registerEntites([TodoItem]);

src/modules/entity/entites-refs.ts

export * from "/src/modules/todo/entity";

业务逻辑 CRUD

  • 创建一个新的 todo
  • 更新一个 todo
  • 删除一个 todo
  • 查询一个 todo (给所有 todo 查询接口添加指定 todo 的 id 参数即可)
  • 分页查询所有 todo

src/modules/todo/services/Todo.service.ts

import { getRepository } from "typeorm";
import { TodoItem, TodoStatus } from "../entity";
import { RequireAtLeastOne } from "/src/types/common";
export default class TodoService {
public get todoRepo() {
return getRepository(TodoItem);
}
public queryTodoList({
page,
limit,
id,
}: {
id?: number;
page: number;
limit: number;
}) {
return this.todoRepo.find({
skip: (page - 1) * limit,
take: limit,
where: id ? { id } : {},
});
}
public createTodo(todo: Pick<TodoItem, "title" | "remark">) {
return this.todoRepo.insert({
...todo,
status: TodoStatus.todo,
});
}
public updateTodo({
id,
...todo
}: Pick<TodoItem, "id"> &
RequireAtLeastOne<Pick<TodoItem, "title" | "remark" | "status">>) {
return this.todoRepo.update(id, todo);
}
public deleteTodo(id: number) {
return this.todoRepo.delete(id);
}
}
export const todoService = new TodoService();

API 配置

一个 HTTP API 由以下几个部分组成:

  • 请求路径
  • 请求方法
  • 请求参数(TS 类型)
  • 请求参数校验
  • 响应数据(返回数据 TS 类型)

我们可以根据上面的组成部分,将 API 抽离成配置,配置可以前后端共享,通过配置生成可供前端直接调用的 API。

src/modules/todo/Todo.apiconf.ts

推荐使用 vscode,内置 snippet,支持快速配置。

以下配置使用 apiiapic 命令生成。

// apii
import * as yup from "yup";
import { ApiConfig, InsertResult, UserTypes } from "/src/types/common";
// apic
/**
* 创建todo接口配置 (结构说明{@link ApiConfig})
* @category API接口
*/
export const CreateTodoConfig: ApiConfig<CreateTodoReq> = {
desc: "创建todo接口配置",
name: "CreateTodo",
method: "post",
path: "/CreateTodo",
schema: yup.object().shape({
title: yup.string().required().label("todo.title"),
remark: yup.string().label("todo.remark"),
}),
level: 0,
disabled: false,
needAuth: true,
/* 接口类型,默认通用 */
apiType: UserTypes.user,
};
/**
* 创建todo接口请求结构
* @category 接口请求参数 ts 类型 Request
*/
export type CreateTodoReq = {
title: string;
remark?: string;
};
/**
* 创建todo接口返回结构
* @category 接口返回参数 ts 类型 Response
*/
export type CreateTodoRes = InsertResult;
// 创建todo接口配置结束

API 路由

src/modules/todo/user-routes/todo.userApi.ts

import { FastifyInstance } from "fastify";
import { todoService } from "../services/Todo.service";
import {
CreateTodoConfig,
CreateTodoReq,
CreateTodoRes,
UpdateTodoConfig,
UpdateTodoReq,
UpdateTodoRes,
DeleteTodoConfig,
DeleteTodoReq,
DeleteTodoRes,
QueryTodoListConfig,
QueryTodoListReq,
QueryTodoListRes,
} from "../services/Todo.apiconf";
import { yupValidate } from "/src/api/middlewares/yup";
import { EntryOptions } from "/src/types/server";
export const todoUserApi = (
instance: FastifyInstance,
options: EntryOptions
): void => {
instance[CreateTodoConfig.method]<{ Body: CreateTodoReq }>(
CreateTodoConfig.path,
{
preValidation: [yupValidate({ bodySchema: CreateTodoConfig.schema })],
},
async (req, res) => {
const payload = req.body;
const result = await todoService.createTodo(payload);
return result;
}
);
instance[UpdateTodoConfig.method]<{ Body: UpdateTodoReq }>(
UpdateTodoConfig.path,
{
preValidation: [yupValidate({ bodySchema: UpdateTodoConfig.schema })],
},
async (req, res) => {
const payload = req.body;
const result = await todoService.updateTodo(payload);
return result;
}
);
instance[DeleteTodoConfig.method]<{ Body: DeleteTodoReq }>(
DeleteTodoConfig.path,
{
preValidation: [yupValidate({ bodySchema: DeleteTodoConfig.schema })],
},
async (req, res) => {
const payload = req.body;
const result = await todoService.deleteTodo(payload);
return result;
}
);
instance[QueryTodoListConfig.method]<{ Querystring: QueryTodoListReq }>(
QueryTodoListConfig.path,
{
preValidation: [yupValidate({ bodySchema: QueryTodoListConfig.schema })],
},
async (req, res) => {
const payload = req.query;
const result = await todoService.queryTodoList(payload);
return result;
}
);
};

如果需要登录,安装 API,src/api/user-routes/index.ts

/** 用户端接口 */
import { FastifyInstance } from "fastify";
import { EntryOptions } from "/src/types/server";
import { tokenUserApi } from "./user/tokenUserApi";
import { userApi } from "./user/userApi";
import { todoUserApi } from "/src/modules/todo/user-routes/todoUserApi";
export default function (
instance: FastifyInstance,
options: EntryOptions
): void {
// token 相关 api
tokenUserApi(instance, options);
// 注册/登录相关api
userApi(instance, options);
// todo
todoUserApi(instance, options);
}

如果不需要登录,安装 API,src/api/no-auth-routes-refs.ts

import { FastifyInstance } from "fastify";
import { EntryOptions } from "/src/types/server";
import { todoUserApi } from "/src/modules/todo/user-routes/todoUserApi";
/** 用户端不需要登录路由 */
export function setupUserNoAuthRoutes(
instance: FastifyInstance,
options: EntryOptions
) {
// todo 模块
todoUserApi(instance, options);
}
/** 管理端不需要登录路由 */
export function setupAdminNoAuthRoutes(
instance: FastifyInstance,
options: EntryOptions
) {
// code here
}

踩坑

  • *.apiconf.ts / entity/*.ts/ src/i18n/*.ts / src/common/*.ts 在导出前端 SDK 的时候,会挪到 packages/fe-sdk/src/ 目录, 其他文件不会导出,所以在这些文件之间的引入,不会报错。但是引入其他的模块代码,由于在前端 SDK 里面,这些模块不存在,会报错,写的时候需要注意下;
  • src/modules/ 之间的 entity/*.ts 互相引入,需要使用 /src/entity 路径引入,不能使用 ../[module name]/entity 等相对路径;
  • *.apiconf.ts 里面的命名需要唯一,否则生成 API 代码,因为命名重复会报错;
  • typeorm 如果是 number 类型,保存的是 Date 时间戳类型,是整数;如果想用小数类型,请使用 type: 'float'
  • typeorm 如果是 numberic,保存的是 string 类型数据,是字符串;如果想用数字,请使用 type: 'float'
  • typeorm 如果是 enum 类型,enum 声明的类型需要 @Column({type: 'text'}),防止报错: DataTypeNotSupportedError: Data type "Object"~~
  • yup validate 默认采用了 {stripUnknown: true},来移除没有在 schema 里面定义的字段,所以规则定义 schema 要么 undefined, 或者是定义全部字段效验;
  • yup 校验,schema 是从前到后顺序进行。比如输入框,首先验证不能为空,再验证格式是否正确,那么 yup.string().required().min(10) 按逻辑顺序这样即可
  • yup 效验 {abortEary: false} 时,同一个字段如果有多个校验那么就会有多个判断