- 很明显的是我们返回的数据格式是固定的(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.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步走:
- 创建一个表结构(entity)
- 创建一个controller
- 在src/router/index.ts 引入并挂载
可以尝试再添加一个controller。
解决问题2和1
仔细思考了下,这2个问题使用静态类型检查是做不到的,同时也不是问题,最多是个注释,这个是前端思维与后端思维的不一致导致的。
- 只有在代码运行时,才可能知道用户输入的参数是什么。静态检查是代码与代码之间的调用,角色是程序员和程序员之间,而不是用户与程序员之间。
- 输出的数据格式一致,是指的前端和后端进行数据交互时的约定,故而只能用接口文档来约束,代码本身没有办法约束。
- 问题1和2的最终解决方案是用接口文档来解决。