1. 很明显的是我们返回的数据格式是固定的(code, msg, data),只有data的类型是不固定的,所以就可以用一个 interface来约束一下
  2. add的时候,接受得参数是什么貌似也没有约束,最起码看不到,过几天就会忘记了
  3. 如果路由得一个个写,10个以内得接口还可以接受,再多就要疯了

这3个问题,倒着解决。

改造路由的思路

  1. router.get('/list', async (ctx) => {
  2. const postRepository = connection.getRepository(Post)
  3. const posts = await postRepository.find()
  4. ctx.body = {
  5. code: 0,
  6. msg: 'success',
  7. data: posts
  8. }
  9. })

观察一下路由结构,路由其实就是3个变量组成,一个是http请求的类型,一个是路由地址,一个是控制器。在网上看教程的时候翻到了Typescript装饰器,写出来的路由我觉得很不错,大概是这个样子的:

  1. export default class Post { // Post 代表操作的表
  2. @get('/list') // 这个是列表接口
  3. async ListController (ctx) {
  4. ctx.body = '列表'
  5. }
  6. @post('/add') // 这个是新增接口
  7. async AddController (ctx) {
  8. ctx.body = '添加数据'
  9. }
  10. }

查了一下实现的思路,用 reflect-metadata 和装饰器,在import这个类的时候,把所有的路由的参数都收集起来,然后再统一挂载上去。主要用到2个方法,赋值和取值。

  1. // 在类上定义元数据,key 为 `metadataKey`,value 为 `metadataValue`
  2. Reflect.defineMetadata(metadataKey, metadataValue, target);
  3. let result = Reflect.getMetadata(metadataKey, target);
  4. // 在类的原型属性 `propertyKey` 上定义元数据,key 为 `metadataKey`,value 为 `metadataValue`
  5. Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
  6. let result = Reflect.getMetadata(metadataKey, target, propertyKey);
  7. // 怎么赋的值就是怎么取

开始实际写代码。

重新设置项目目录结构

先清空项目 src 下的所有文件和文件夹。建立以下几个目录和文件:

  • config // 配置
  • controllers // 所有的controller
  • core // 核心文件
  • entity // 数据实体
  • router // 路由
  • app.ts // 项目入口
  1. // src/app.ts
  2. import path from 'path';
  3. import Koa from 'koa';
  4. import koaStatic from 'koa-static';
  5. import bodyParser from 'koa-bodyparser';
  6. import { createConnection } from 'typeorm';
  7. import { mongoOptions } from './config/mongodb';
  8. createConnection(mongoOptions).then(async () => { // 创建数据库连接
  9. const app = new Koa();
  10. // Middlewares
  11. app.use(bodyParser());
  12. app.use(koaStatic(path.join(__dirname, '../public')));
  13. // app.use(router.routes()).use(router.allowedMethods());
  14. app.listen(3000, () => {
  15. console.log('application is running on port 3000');
  16. })
  17. }).catch((error: any) => console.log('TypeORM connection error: ', error));
  1. // src/config/mongodb.ts
  2. import path from 'path';
  3. import { ConnectionOptions } from 'typeorm';
  4. // mongodb的连接配置
  5. export const mongoOptions: ConnectionOptions = {
  6. type: 'mongodb',
  7. host: 'localhost',
  8. port: 27017,
  9. username: '',
  10. password: '',
  11. database: 'comments',
  12. synchronize: true,
  13. entities: [
  14. path.join(__dirname, '../entity/*.{ts, js}')
  15. ],
  16. useUnifiedTopology: true,
  17. logging: true
  18. };
  19. export default {
  20. mongoOptions
  21. }
  1. // src/entity/Post.ts
  2. import { Column, Entity, ObjectID, ObjectIdColumn } from 'typeorm'
  3. @Entity()
  4. export class Post {
  5. @ObjectIdColumn()
  6. id!: ObjectID;
  7. @Column()
  8. url!: string;
  9. @Column()
  10. content!: string;
  11. @Column()
  12. email!: string;
  13. @Column()
  14. create!: Date;
  15. }

实现Controller类

  1. // src/controllers/Post.ts
  2. import { Context, Next } from 'koa';
  3. import { getManager } from 'typeorm';
  4. import { prefix, get, post } from '../core/Decorators';
  5. import { Post } from '../entity/Post';
  6. @prefix('/post') // 期望有个前缀约束 /post/list /post/add
  7. export default class PostController {
  8. @get('/list')
  9. async List (ctx: Context) {
  10. ctx.body = 'list'
  11. }
  12. @post('/add')
  13. async Add (ctx: Context) {
  14. ctx.body = 'add'
  15. }
  16. }

创建文件后,就发现 prefixgetpost 都标红了。现在来实现这些方法。

  1. // src/core/Decorators.ts
  2. import 'reflect-metadata';
  3. import Router from 'koa-router';
  4. const router = new Router();
  5. // 定义一个http请求的枚举类型
  6. export enum HttpMethods {
  7. GET = 'get',
  8. POST = 'post',
  9. PUT = 'put',
  10. DEL = 'del',
  11. All = 'all'
  12. }
  13. // 前缀装饰器,类型是类装饰器
  14. export function prefix (path: string): ClassDecorator {
  15. return (target: Function) => {
  16. // 把前缀存起来
  17. Reflect.defineMetadata('prefix', path, target);
  18. };
  19. }
  20. // 用工厂生成http请求装饰器 post get等
  21. export function httpRequestDecorator (method: HttpMethods) {
  22. return function (path: string) {
  23. return function (target: any, key: string) {
  24. Reflect.defineMetadata('path', path, target, key);
  25. Reflect.defineMetadata('method', method, target, key);
  26. };
  27. };
  28. }
  29. export const get = httpRequestDecorator(HttpMethods.GET);
  30. export const post = httpRequestDecorator(HttpMethods.POST);
  31. export const put = httpRequestDecorator(HttpMethods.PUT);
  32. export const del = httpRequestDecorator(HttpMethods.DEL);
  33. export const all = httpRequestDecorator(HttpMethods.All);
  34. // 挂载路由
  35. export function getRouter (): Router {
  36. return router;
  37. }
  38. export class AppRouter {
  39. router: Router;
  40. constructor () {
  41. this.router = getRouter();
  42. };
  43. mount (controller: Function) {
  44. const prefix = Reflect.getMetadata('prefix', controller);
  45. const keys = Object.keys(controller.prototype)
  46. keys.forEach(key => {
  47. const path: string = Reflect.getMetadata('path', controller.prototype, key);
  48. const method: HttpMethods = Reflect.getMetadata('method', controller.prototype, key);
  49. const hanlder = controller.prototype[key];
  50. if (path && method && hanlder) {
  51. router[method](prefix + path, hanlder);
  52. }
  53. })
  54. return this;
  55. };
  56. }
  1. // src/router/index.ts
  2. import { AppRouter } from '../core/Decorators';
  3. import Post from '../controllers/Post';
  4. const appRouter = new AppRouter();
  5. appRouter.mount(Post);
  6. export default appRouter.router;
  1. // src/app.ts
  2. import path from 'path';
  3. import Koa from 'koa';
  4. import koaStatic from 'koa-static';
  5. import bodyParser from 'koa-bodyparser';
  6. import { createConnection } from 'typeorm';
  7. import { mongoOptions } from './config/mongodb';
  8. import router from './router/index'; // 加入路由
  9. createConnection(mongoOptions).then(async () => { // 创建数据库连接
  10. const app = new Koa();
  11. // Middlewares
  12. app.use(bodyParser());
  13. app.use(koaStatic(path.join(__dirname, '../public')));
  14. app.use(router.routes()).use(router.allowedMethods()); // 挂载到APP上
  15. app.listen(3000, () => {
  16. console.log('application is running on port 3000');
  17. })
  18. }).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

  1. // src/controllers/Post.ts
  2. import { Context, Next } from 'koa';
  3. import { getManager } from 'typeorm';
  4. import { prefix, get, post } from '../core/Decorators';
  5. import { Post } from '../entity/Post';
  6. @prefix('/post') // 期望有个前缀约束 /post/list /post/add
  7. export default class PostController {
  8. @get('/list')
  9. async List (ctx: Context) {
  10. const postRepository = getManager().getRepository(Post);
  11. const posts = await postRepository.find();
  12. ctx.body = posts;
  13. }
  14. @post('/add')
  15. async Add (ctx: Context) {
  16. const data = ctx.request.body || {};
  17. const postRepository = getManager().getRepository(Post);
  18. data.create = new Date()
  19. try {
  20. const res = await postRepository.save(data);
  21. ctx.body = {
  22. code: 0,
  23. data: res
  24. }
  25. } catch (err) {
  26. ctx.body = {
  27. code: 1,
  28. data: err
  29. }
  30. }
  31. }
  32. }

然后用postman 测试一下接口,完美!!!

现在添加用户的增删改查的功能,只需要分3步走:

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

可以尝试再添加一个controller。

解决问题2和1

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

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

3.typescript实现KOA路由.mp4 (61.42MB)