接口请求与其它资源请求没有什么不同,都是借助 http 协议
返回对应的资源,这篇文章简单介绍一下 node
如何开发接口以及如何管理多个接口情况和接口风格
标题关联了 node,主要因为 node 开启一个服务器是很简单,而且语法基本相同没有太多负担,这篇文章主要讲解思路,换算到其它语言也是可以的。
先看一个官网的例子,稍微改造一下让它返回一个固定的 json
数据
const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ name: 'hello wrold' })); res.end(); }); server.listen(port, hostname, () => { console.log(`服务器运行在 http://${hostname}:${port}/`); }); 复制代码
将上面代码复制到文件中,之后借助 node xxx.js
的形式就可以预览到效果了。
koa
上面是借助 node 的 http
原生模块实现的,当然这种实现没有什么问题,不过追求可扩展和简化开发的目的,这里选择了 koa 作为下面使用的的框架。
koa 号称是下一代的 web 开发框架,同样以上面的例子安装一下 koa ,看它怎么实现上面的功能
yarn add koa 复制代码
const Koa = require('koa'); const hostname = '127.0.0.1'; const port = 3000; const app = new Koa(); app.use(async (ctx) => { ctx.type = 'application/json'; ctx.body = { name: 'hello wrold' }; }); app.listen(port, hostname, () => { console.log(`服务器运行在 http://${hostname}:${port}/`); }); 复制代码
代码方面还是十分简洁的,这里主要介绍实现思路不过多介绍 koa 的语法,而且实际上 koa 只是对 http 模块进行了封装,文档也没多少推荐看一下官网的介绍即可。
说到 koa
这里还是聊一下 koa
的中间件,下面的代码会经常使用到, koa
借助中间件来实现各种拓展,就是类似于插件的功能,它本身非常像洋葱结构
例如上面的 app.use
就是中间件,中间件的执行顺序以 next
为分割,先执行 next
的前半部分,之后按照倒叙的结构执行后半部分的 next
代码,看一下例子
app.use(async (ctx, next) => { console.log(1); await next(); console.log(2); }); app.use(async (ctx, next) => { console.log(3); await next(); console.log(4); }); app.use(async (ctx, next) => { console.log(5); await next(); console.log(6); }); 复制代码
上面代码的打印结果是 1,3,5,6,4,2
,这块有点绕可以多想一下。
接口开发中一般都是通过 json
来传递消息, koa
本身的语法已经很简洁了,但是每次都需要返回想重复的部分,时间长了肯定也会有失误或者漏写拼错的情况,还有抛出错误也需要有一个公共的方法,下面是一个返回信息和抛出错误的设想。
app.use(async (ctx) => { ctx.sendData({ name: 'hello wrold' }); // 如果发生错误 ctx.throwError('不满足xxx'); }); 复制代码
如果代码都通过这种形式返回就简单多了,而且实际写在中间件部分的也是可能出现问题的,这里可以通过 koa
自带的监听错误来处理,或者通过一个 try
来包裹,可以预料的是一个个手动管理 try
一定会让人抓狂。
借助中间件的机制很容易编写出一个带有 sendData
和 throwError
的功能,只需要在 ctx 中返回,之后调用 next 让后面的实例执行
app.use(async (ctx, next) => { ctx.sendData = () => {}; ctx.throwError = () => {}; await next(); }); 复制代码
上面的例子是简化过的,这里稍微错开一下具体实现之后再详细讲解
中间件的顺序非常重要
接口结构
上面说了要有一个 sendData
和 throwError
的方法来统一返回信息和抛出错误,这里就说下这两个方法的具体参数以及实现。
首先接口的返回信息,期待它是固定成下面这种结构
{ "data": {}, "message": "ok", "code": 200 } 复制代码
这里 data
部分是需要手动返回的, message
是可选的,默认的时候可以给一个 ok 以及 200 的 code,这里 code
值是固定死的,方法不允许修改,这样做是因为成功返回一般不需要额外的 code 值
而错误信息,期待它是这种结构
{ "message": "", "code": "400" } 复制代码
这里 message 是必填,而 code 则是可选的。
这里稍微说一下错误到底使用 code
来做区分?还是通过 message
来做区分? 如果通过 code
来做不同状态的区分,那么必然要维护一个 code 列表,其实这是很繁琐的而且单纯的数字记忆也不符合人的记忆,而通过 message
来做提示则基本上可以做到大概可以猜到错误情况,例如可以这样返回
{ "message": "error_用户名不能为空" } 复制代码
前面类型后面提示,是不是简洁很多,这两种错误提示自己选择一种即可。
说了需要实现的功能,方法的实现就很简单了,下面代码是 code
值风格的实现
// 忽略顶层语法问题,这里是把实现提取出来了 async (ctx, next) => { const content = { ...ctx, sendData: (data, message = 'ok') => { ctx.body = { data, message, code: 200, }; ctx.type = 'application/json'; }, throwError: (message = '错误', code = 400) => { ctx.body = { code, message, }; ctx.type = 'application/json'; }, }; try { await callback(content); } catch (e) { ctx.body = { code: 400, message: (e instanceof Error ? e.message : e) || '系统出现错误', }; ctx.status = 400; } await next(); }; 复制代码
rest
rest 简单来说就是接口的一种规则,它主要有下面几种规则
get post put delete
说了这么多使用 rest
的好处有哪些呢?
首先 rest 只是一种规范,定义这种规范更方便理解和阅读,和代码规范是一个性质
自动导入
在项目开发中必然存在不同的接口,如何管理这些接口就很有必要的,一个个手动导入管理固然可以,不过当项目足够大的时候,业务变更的时候一个个调整一定让人抓狂。
下面借助 koa-router
和中间件就编写一个自动导入接口的功能,先看一下 koar-router
的简单使用
yarn add @koa/router 复制代码
const Koa = require('koa'); const Router = require('@koa/router'); const hostname = '127.0.0.1'; const port = 3000; const app = new Koa(); const router = new Router(); router.get('/', (ctx, next) => { ctx.type = 'application/json'; ctx.body = { name: 'hello wrold' }; }); app.use(router.routes()).use(router.allowedMethods()); app.listen(port, hostname, () => { console.log(`服务器运行在 http://${hostname}:${port}/`); }); 复制代码
要实现这个功能先定义一下规则
-
只导入
src
目录下index.js
结尾的接口文件搜索所有符合要求的
index.js
文件,可以借助glob
模块来实现,借助通配符'src/**/index.js'
即可。 -
导入文件,把对应模板返回的字段添加到
router
上这里可以通过 node 原生
require
来读取文件,在具体实现的时候需要稍微注意,必须满足格式的模块才能被导入,而且要添加try
来捕捉不是modules
的文件
在动手实现这个函数之前,还要约定一下 index.js
文件的内的模块格式是什么样的
const api = { url: '', methods: 'get' || ['post'], async callback(ctx) {}, }; 复制代码
上面是约定的格式,只有满足这样的结构才会被导入进来,因为开发用的是 ts
这里就不做转换 js
的操作了,如果不想使用 ts 直接忽略掉类型标注看大概实现即可。
utils.ts
import glob from 'glob'; import path from 'path'; import _ from 'lodash'; import { Iobj, Istructure } from '../../typings/structure'; export const globFile = (pattern: string): Promise<Array<string>> => { return new Promise((resolve, reject) => { glob(pattern, (err, files) => { if (err) { return reject(err); } return resolve(files); }); }); }; export const importModule = async () => { const pattern = 'src/**/index.ts'; const list = await globFile(pattern); const listMap = list.map((item) => { const f = path.resolve(process.cwd(), item); return import(f) .then((res) => { // 过滤掉default的属性,其它的返回 return _.omit(res, ['default']); }) .catch(() => null); }); return (await Promise.all(listMap)).filter((f) => f) as Array<Iobj<Istructure>>; }; 复制代码
这里注意一下,因为用的 ts 所以用了 import()
如果只是用 node 语法直接 require
即可
index.ts
import Router from '@koa/router'; import _ from 'lodash'; import { Ictx, Iobj } from '../../typings/structure'; import { importModule } from './utils'; import Koa from 'koa'; const route = async (koa: Koa) => { const router = new Router(); const list = await importModule(); for (const fileAll of list) { // 将数据解构,这里返回的是{xxx: {url,methods,callback}}这样解构 // 过滤不符合条件的模块 for (const file of Object.values(fileAll)) { if (!_.isObjectLike(file) || !['url', 'methods', 'callback'].every((f) => Object.keys(file).includes(f))) { continue; } const { url, methods, callback } = file; const methodsArr = _.isArray(methods) ? methods : [methods]; for (const met of methodsArr) { router[met](url, async (ctx, next) => { const content: Ictx = { ...ctx, sendData: (data: Iobj, message = 'ok') => { ctx.body = { data, message, code: 200, }; ctx.type = 'application/json'; }, throwError: (message = '错误', code = 400) => { ctx.body = { code, message, }; ctx.type = 'application/json'; }, }; try { await callback(content); } catch (e) { ctx.body = { code: 400, message: (e instanceof Error ? e.message : e) || '系统出现错误', }; ctx.status = 400; } await next(); }); } } } koa.use(router.routes()).use(router.allowedMethods()); }; export default route; 复制代码
接口测试
待补充,等待补充能量之后添加
日志
借助 koa 的中间件也很容易实现日志的功能,这里以 winston 为例
日志主要记录系统运行时的错误,还记的上面通过 try
来捕捉错误的例子么,现在让他继续抛出错误,直接通过中间件 try 捕捉错误写入到文件。
import winston from 'winston'; import Koa from 'koa'; import 'winston-daily-rotate-file'; const transport = new winston.transports.DailyRotateFile({ filename: 'log/%DATE%.log', datePattern: 'YYYY-MM-DD-HH', zippedArchive: true, maxSize: '20m', maxFiles: '14d', }); const logger = winston.createLogger({ transports: [transport], }); const asyncwinston = async (_ctx: Koa.ParameterizedContext<Koa.DefaultState, Koa.DefaultContext>, next: Koa.Next) => { try { await next(); } catch (err) { const data = { data: err, time: new Date().valueOf(), }; if (err instanceof Error) { data.data = { content: err.message, name: err.name, stack: err.stack, }; } logger.error(JSON.stringify(data)); } }; export default asyncwinston; 复制代码
启动
启动就很简单了,把上面暴露的 index.js 通过 koa
的 use 引入
App.js
const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const route = require('./middleware/route'); const winston = require('./middleware/winston'); const App = async () => { const app = new Koa(); app.use(winston); app.use(bodyParser()); await route(app); return app; }; module.exports = App; 复制代码
start.js
const Koa = require('koa'); const ip = require('ip'); const App = require('./App'); const start = async () => { const app = await App(); notice(app); }; const notice = (koa: Koa) => { const port = 3000; const ipStr = ip.address(); const str = `http://${ipStr}:${port}`; koa.listen(port, () => { console.log(`服务器运行在\n${str}`); }); }; start(); 复制代码
这里稍微说明一下为什么分成两个文件,这是因为方便接口测试特意分层的, start
只做启动的用途
最后添加一个 node-dev
的模块,就大功告成了
// 安装 yarn add node-dev // 启动 node-dev start.js 复制代码
通过 node-dev
启动主要是可以方便修改接口可以直接重载以及通知的方式更明显
最后
源码放置到了 仓库
如果对你有帮助欢迎 stat
,如果有什么错误之处欢迎指出