用TS来实现相关的功能

  1. 很明显的是我们返回的数据格式是固定的(code, msg, data),只有data的类型是不固定的,所以就可以用一个 interface来约束一下
  2. add的时候,接受得参数是什么貌似也没有约束,最起码看不到,过几天就会忘记了
  3. 如果路由得一个个写,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.ts
import 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();

  // Middlewares
  app.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.ts
import 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.ts
import { 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.ts
import { 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/add
export 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.ts
import '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.ts
import { AppRouter } from '../core/Decorators';
import Post from '../controllers/Post';

const appRouter = new AppRouter();

appRouter.mount(Post);

export default appRouter.router;

// src/app.ts
import 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();

  // Middlewares
  app.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.ts
import { 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/add
export 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步走:

  1. 创建一个表结构(entity)
  2. 创建一个controller
  3. 在src/router/index.ts 引入并挂载

可以尝试再添加一个controller。

解决问题2和1

仔细思考了下,这2个问题使用静态类型检查是做不到的,同时也不是问题,最多是个注释,这个是前端思维与后端思维的不一致导致的。

  1. 只有在代码运行时,才可能知道用户输入的参数是什么。静态检查是代码与代码之间的调用,角色是程序员和程序员之间,而不是用户与程序员之间。
  2. 输出的数据格式一致,是指的前端和后端进行数据交互时的约定,故而只能用接口文档来约束,代码本身没有办法约束。
  3. 问题1和2的最终解决方案是用接口文档来解决。