- 很明显的是我们返回的数据格式是固定的(code, msg, data),只有data的类型是不固定的,所以就可以用一个 interface来约束一下
- add的时候,接受得参数是什么貌似也没有约束,最起码看不到,过几天就会忘记了
- 如果路由得一个个写,10个以内得接口还可以接受,再多就要疯了
这3个问题,倒着解决。
改造路由的思路
router.get('/list', async (ctx) => {const postRepository = connection.getRepository(Post)const posts = await postRepository.find()ctx.body = {code: 0,msg: 'success',data: posts}})
观察一下路由结构,路由其实就是3个变量组成,一个是http请求的类型,一个是路由地址,一个是控制器。在网上看教程的时候翻到了Typescript装饰器,写出来的路由我觉得很不错,大概是这个样子的:
export default class Post { // Post 代表操作的表@get('/list') // 这个是列表接口async ListController (ctx) {ctx.body = '列表'}@post('/add') // 这个是新增接口async AddController (ctx) {ctx.body = '添加数据'}}
查了一下实现的思路,用 reflect-metadata 和装饰器,在import这个类的时候,把所有的路由的参数都收集起来,然后再统一挂载上去。主要用到2个方法,赋值和取值。
// 在类上定义元数据,key 为 `metadataKey`,value 为 `metadataValue`Reflect.defineMetadata(metadataKey, metadataValue, target);let result = Reflect.getMetadata(metadataKey, target);// 在类的原型属性 `propertyKey` 上定义元数据,key 为 `metadataKey`,value 为 `metadataValue`Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);let result = Reflect.getMetadata(metadataKey, target, propertyKey);// 怎么赋的值就是怎么取
开始实际写代码。
重新设置项目目录结构
先清空项目 src 下的所有文件和文件夹。建立以下几个目录和文件:
- config // 配置
- controllers // 所有的controller
- core // 核心文件
- entity // 数据实体
- router // 路由
- app.ts // 项目入口
// src/app.tsimport path from 'path';import Koa from 'koa';import koaStatic from 'koa-static';import bodyParser from 'koa-bodyparser';import { createConnection } from 'typeorm';import { mongoOptions } from './config/mongodb';createConnection(mongoOptions).then(async () => { // 创建数据库连接const app = new Koa();// Middlewaresapp.use(bodyParser());app.use(koaStatic(path.join(__dirname, '../public')));// app.use(router.routes()).use(router.allowedMethods());app.listen(3000, () => {console.log('application is running on port 3000');})}).catch((error: any) => console.log('TypeORM connection error: ', error));
// src/config/mongodb.tsimport path from 'path';import { ConnectionOptions } from 'typeorm';// mongodb的连接配置export const mongoOptions: ConnectionOptions = {type: 'mongodb',host: 'localhost',port: 27017,username: '',password: '',database: 'comments',synchronize: true,entities: [path.join(__dirname, '../entity/*.{ts, js}')],useUnifiedTopology: true,logging: true};export default {mongoOptions}
// src/entity/Post.tsimport { Column, Entity, ObjectID, ObjectIdColumn } from 'typeorm'@Entity()export class Post {@ObjectIdColumn()id!: ObjectID;@Column()url!: string;@Column()content!: string;@Column()email!: string;@Column()create!: Date;}
实现Controller类
// src/controllers/Post.tsimport { Context, Next } from 'koa';import { getManager } from 'typeorm';import { prefix, get, post } from '../core/Decorators';import { Post } from '../entity/Post';@prefix('/post') // 期望有个前缀约束 /post/list /post/addexport default class PostController {@get('/list')async List (ctx: Context) {ctx.body = 'list'}@post('/add')async Add (ctx: Context) {ctx.body = 'add'}}
创建文件后,就发现 prefix , get , post 都标红了。现在来实现这些方法。
// src/core/Decorators.tsimport 'reflect-metadata';import Router from 'koa-router';const router = new Router();// 定义一个http请求的枚举类型export enum HttpMethods {GET = 'get',POST = 'post',PUT = 'put',DEL = 'del',All = 'all'}// 前缀装饰器,类型是类装饰器export function prefix (path: string): ClassDecorator {return (target: Function) => {// 把前缀存起来Reflect.defineMetadata('prefix', path, target);};}// 用工厂生成http请求装饰器 post get等export function httpRequestDecorator (method: HttpMethods) {return function (path: string) {return function (target: any, key: string) {Reflect.defineMetadata('path', path, target, key);Reflect.defineMetadata('method', method, target, key);};};}export const get = httpRequestDecorator(HttpMethods.GET);export const post = httpRequestDecorator(HttpMethods.POST);export const put = httpRequestDecorator(HttpMethods.PUT);export const del = httpRequestDecorator(HttpMethods.DEL);export const all = httpRequestDecorator(HttpMethods.All);// 挂载路由export function getRouter (): Router {return router;}export class AppRouter {router: Router;constructor () {this.router = getRouter();};mount (controller: Function) {const prefix = Reflect.getMetadata('prefix', controller);const keys = Object.keys(controller.prototype)keys.forEach(key => {const path: string = Reflect.getMetadata('path', controller.prototype, key);const method: HttpMethods = Reflect.getMetadata('method', controller.prototype, key);const hanlder = controller.prototype[key];if (path && method && hanlder) {router[method](prefix + path, hanlder);}})return this;};}
// src/router/index.tsimport { AppRouter } from '../core/Decorators';import Post from '../controllers/Post';const appRouter = new AppRouter();appRouter.mount(Post);export default appRouter.router;
// src/app.tsimport path from 'path';import Koa from 'koa';import koaStatic from 'koa-static';import bodyParser from 'koa-bodyparser';import { createConnection } from 'typeorm';import { mongoOptions } from './config/mongodb';import router from './router/index'; // 加入路由createConnection(mongoOptions).then(async () => { // 创建数据库连接const app = new Koa();// Middlewaresapp.use(bodyParser());app.use(koaStatic(path.join(__dirname, '../public')));app.use(router.routes()).use(router.allowedMethods()); // 挂载到APP上app.listen(3000, () => {console.log('application is running on port 3000');})}).catch((error: any) => console.log('TypeORM connection error: ', error));
OK,现在跑一下服务。[http://localhost:3000/post/list](http://localhost:3000/post/list) 看到返回了list字样。
连上数据库操作一下
修改 src/controllers/Post.ts
// src/controllers/Post.tsimport { Context, Next } from 'koa';import { getManager } from 'typeorm';import { prefix, get, post } from '../core/Decorators';import { Post } from '../entity/Post';@prefix('/post') // 期望有个前缀约束 /post/list /post/addexport default class PostController {@get('/list')async List (ctx: Context) {const postRepository = getManager().getRepository(Post);const posts = await postRepository.find();ctx.body = posts;}@post('/add')async Add (ctx: Context) {const data = ctx.request.body || {};const postRepository = getManager().getRepository(Post);data.create = new Date()try {const res = await postRepository.save(data);ctx.body = {code: 0,data: res}} catch (err) {ctx.body = {code: 1,data: err}}}}
然后用postman 测试一下接口,完美!!!
现在添加用户的增删改查的功能,只需要分3步走:
- 创建一个表结构(entity)
- 创建一个controller
- 在src/router/index.ts 引入并挂载
可以尝试再添加一个controller。
解决问题2和1
仔细思考了下,这2个问题使用静态类型检查是做不到的,同时也不是问题,最多是个注释,这个是前端思维与后端思维的不一致导致的。
- 只有在代码运行时,才可能知道用户输入的参数是什么。静态检查是代码与代码之间的调用,角色是程序员和程序员之间,而不是用户与程序员之间。
- 输出的数据格式一致,是指的前端和后端进行数据交互时的约定,故而只能用接口文档来约束,代码本身没有办法约束。
- 问题1和2的最终解决方案是用接口文档来解决。

