快速开始
让我们从头开始写一个简单的模块 「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,支持快速配置。
以下配置使用
apii
和apic
命令生成。
// apiiimport * 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}
时,同一个字段如果有多个校验那么就会有多个判断