We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
接口请求与其它资源请求没有什么不同,都是借助 http 协议返回对应的资源,这篇文章简单介绍一下 node 开发接口以及如何管理多个接口情况和接口风格。
http
node
标题关联了 node,主要因为 node 开启一个服务器是很简单,而且语法基本相同没有太多负担,这篇文章主要讲解思路,换算到其它语言也是可以的。
先看一个官网的例子,稍微改造一下让它返回一个固定的json数据
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 的形式就可以预览到效果了。
node xxx.js
上面是借助 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 借助中间件来实现各种拓展,就是类似于插件的功能,它本身非常像洋葱结构
koa
例如上面的app.use就是中间件,中间件的执行顺序以next为分割,先执行next的前半部分,之后按照倒叙的结构执行后半部分的next代码,看一下例子
app.use
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,这块有点绕可以多想一下。
1,3,5,6,4,2
接口开发中一般都是通过json来传递消息,koa本身的语法已经很简洁了,但是每次都需要返回想重复的部分,时间长了肯定也会有失误或者漏写拼错的情况,还有抛出错误也需要有一个公共的方法,下面是一个返回信息和抛出错误的设想。
app.use(async (ctx) => { ctx.sendData({ name: 'hello wrold' }); // 如果发生错误 ctx.throwError('不满足xxx'); });
如果代码都通过这种形式返回就简单多了,而且实际写在中间件部分的也是可能出现问题的,这里可以通过 koa 自带的监听错误来处理,或者通过一个try来包裹,可以预料的是一个个手动管理try一定会让人抓狂。
try
借助中间件的机制很容易编写出一个带有sendData和throwError的功能,只需要在 ctx 中返回,之后调用 next 让后面的实例执行
sendData
throwError
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 值
data
message
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 是一种接口风格,简单可以概括成以下几种
get
post
put
delete
说了这么多使用rest的好处有哪些呢?
rest
首先 rest 只是一种规范,定义这种规范更方便理解和阅读,和代码规范是一个性质
在项目开发中必然存在不同的接口,如何管理这些接口就很有必要的,一个个手动导入管理固然可以,不过当项目足够大的时候,业务变更的时候一个个调整一定让人抓狂。
下面借助koa-router和中间件就编写一个自动导入接口的功能,先看一下koar-router的简单使用
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结尾的接口文件
src
index.js
搜索所有符合要求的index.js文件,可以借助glob模块来实现,借助通配符'src/**/index.js'即可。
glob
'src/**/index.js'
导入文件,把对应模板返回的字段添加到router上
router
这里可以通过 node 原生require来读取文件,在具体实现的时候需要稍微注意,必须满足格式的模块才能被导入,而且要添加try来捕捉不是modules的文件
require
modules
在动手实现这个函数之前,还要约定一下index.js文件的内的模块格式是什么样的
const api = { url: '', methods: 'get' || ['post'], async callback(ctx) {}, };
上面是约定的格式,只有满足这样的结构才会被导入进来,因为开发用的是ts这里就不做转换js的操作了,如果不想使用 ts 直接忽略掉类型标注看大概实现即可。
ts
js
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 即可
import()
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只做启动的用途
start
最后添加一个node-dev的模块,就大功告成了
node-dev
10/21 补充
上面的 node-dev,是开发环境下使用的,方便代码的快速重启,在生产环境下可以使用 pm2
// 安装 yarn add node-dev // 启动 node-dev start.js
通过node-dev启动主要是可以方便修改接口可以直接重载以及通知的方式更明显
12/21 补上
首先在 src 目录下 新建一个 index.ts 文件,用于测试的接口
import { Istructure } from '../typings/structure'; const testGet: Istructure = { url: '/api/:id', methods: 'get', async callback(ctx) { const { id } = ctx?.params; ctx?.sendData({ name: 'hello', id }); }, }; const testPost: Istructure = { url: '/api', methods: 'post', async callback(ctx) { const body = ctx?.request.body; ctx?.sendData(body || {}); }, }; export { testGet, testPost };
上面是一个很简单的 post 和 get 请求,之后我们新建一个__test__目录,在里面新建一个index.test.js文件
__test__
index.test.js
yarn add @babel/core @babel/preset-env @babel/preset-typescript babel-jest jest supertest
在根目录新建babel.config.js文件
babel.config.js
// babel.config.js module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript', ], };
简单说下,这个 babel 的作用是让我们可以在 js 里面使用 es6 module 的语法,同时将 ts 文件转成 js,否则我们这个测试用例是 ts 的根本跑不起来,测试框架方面选用了jest测试 http 库使用了supertest,其实这块都是可以调整的,单元测试的目的就是对比数据是否符合预期
es6 module
jest
supertest
__test__ index.test.js
import App from '../App'; import supertest from 'supertest'; test('get请求测试', async () => { const app = await App(); const request = supertest(app.listen()); const id = 6; const data = { name: 'hello', id: `${id}` }; const res = await request.get(`/api/${id}`).expect(200); const body = res.body.data; expect(body).toEqual(data); }); test('post请求测试', async () => { const app = await App(); const request = supertest(app.listen()); const data = { name: 'hello', id: 8 }; const res = await request.post(`/api/`).send(data).expect(200); const body = res.body.data; expect(body).toEqual(data); });
运行 npx jest ,命令行如果没有抛出异常说明我们的代码符合预期,关于更多的 jest 内容可以查看文档
npx jest
如果对你有帮助欢迎 stat,如果有什么错误之处欢迎指出,关于代码本来全部想用 ts 举例的,但是 ts 并不是一定要上的,所以某些场景我就手动转了一下,看起来有点风格不统一还望谅解
stat
The text was updated successfully, but these errors were encountered:
No branches or pull requests
接口请求与其它资源请求没有什么不同,都是借助
http
协议返回对应的资源,这篇文章简单介绍一下node
开发接口以及如何管理多个接口情况和接口风格。标题关联了 node,主要因为 node 开启一个服务器是很简单,而且语法基本相同没有太多负担,这篇文章主要讲解思路,换算到其它语言也是可以的。
先看一个官网的例子,稍微改造一下让它返回一个固定的
json
数据将上面代码复制到文件中,之后借助
node xxx.js
的形式就可以预览到效果了。koa
上面是借助 node 的
http
原生模块实现的,当然这种实现没有什么问题,不过追求可扩展和简化开发的目的,这里选择了 koa 作为下面使用的的框架。koa 号称是下一代的 web 开发框架,同样以上面的例子安装一下 koa ,看它怎么实现上面的功能
代码方面还是十分简洁的,这里主要介绍实现思路不过多介绍 koa 的语法,而且实际上 koa 只是对 http 模块进行了封装,文档也没多少推荐看一下官网的介绍即可。
说到
koa
这里还是聊一下koa
的中间件,下面的代码会经常使用到,koa
借助中间件来实现各种拓展,就是类似于插件的功能,它本身非常像洋葱结构例如上面的
app.use
就是中间件,中间件的执行顺序以next
为分割,先执行next
的前半部分,之后按照倒叙的结构执行后半部分的next
代码,看一下例子上面代码的打印结果是
1,3,5,6,4,2
,这块有点绕可以多想一下。接口开发中一般都是通过
json
来传递消息,koa
本身的语法已经很简洁了,但是每次都需要返回想重复的部分,时间长了肯定也会有失误或者漏写拼错的情况,还有抛出错误也需要有一个公共的方法,下面是一个返回信息和抛出错误的设想。如果代码都通过这种形式返回就简单多了,而且实际写在中间件部分的也是可能出现问题的,这里可以通过
koa
自带的监听错误来处理,或者通过一个try
来包裹,可以预料的是一个个手动管理try
一定会让人抓狂。借助中间件的机制很容易编写出一个带有
sendData
和throwError
的功能,只需要在 ctx 中返回,之后调用 next 让后面的实例执行上面的例子是简化过的,这里稍微错开一下具体实现之后再详细讲解
接口结构
上面说了要有一个
sendData
和throwError
的方法来统一返回信息和抛出错误,这里就说下这两个方法的具体参数以及实现。首先接口的返回信息,期待它是固定成下面这种结构
这里
data
部分是需要手动返回的,message
是可选的,默认的时候可以给一个 ok 以及 200 的 code,这里code
值是固定死的,方法不允许修改,这样做是因为成功返回一般不需要额外的 code 值而错误信息,期待它是这种结构
这里 message 是必填,而 code 则是可选的。
这里稍微说一下错误到底使用
code
来做区分?还是通过message
来做区分?如果通过
code
来做不同状态的区分,那么必然要维护一个 code 列表,其实这是很繁琐的而且单纯的数字记忆也不符合人的记忆,而通过message
来做提示则基本上可以做到大概可以猜到错误情况,例如可以这样返回前面类型后面提示,是不是简洁很多,这两种错误提示自己选择一种即可。
说了需要实现的功能,方法的实现就很简单了,下面代码是
code
值风格的实现REST 风格
rest 是一种接口风格,简单可以概括成以下几种
get
来获取资源post
来发送请求put
来更新资源delete
来删除资源说了这么多使用
rest
的好处有哪些呢?首先 rest 只是一种规范,定义这种规范更方便理解和阅读,和代码规范是一个性质
自动导入
在项目开发中必然存在不同的接口,如何管理这些接口就很有必要的,一个个手动导入管理固然可以,不过当项目足够大的时候,业务变更的时候一个个调整一定让人抓狂。
下面借助
koa-router
和中间件就编写一个自动导入接口的功能,先看一下koar-router
的简单使用要实现这个功能先定义一下规则
只导入
src
目录下index.js
结尾的接口文件搜索所有符合要求的
index.js
文件,可以借助glob
模块来实现,借助通配符'src/**/index.js'
即可。导入文件,把对应模板返回的字段添加到
router
上这里可以通过 node 原生
require
来读取文件,在具体实现的时候需要稍微注意,必须满足格式的模块才能被导入,而且要添加try
来捕捉不是modules
的文件在动手实现这个函数之前,还要约定一下
index.js
文件的内的模块格式是什么样的上面是约定的格式,只有满足这样的结构才会被导入进来,因为开发用的是
ts
这里就不做转换js
的操作了,如果不想使用 ts 直接忽略掉类型标注看大概实现即可。utils.ts
index.ts
日志
借助 koa 的中间件也很容易实现日志的功能,这里以winston为例
日志主要记录系统运行时的错误,还记的上面通过
try
来捕捉错误的例子么,现在让他继续抛出错误,直接通过中间件 try 捕捉错误写入到文件。启动
启动就很简单了,把上面暴露的 index.js 通过
koa
的 use 引入App.js
start.js
这里稍微说明一下为什么分成两个文件,这是因为方便接口测试特意分层的,
start
只做启动的用途最后添加一个
node-dev
的模块,就大功告成了10/21 补充
通过
node-dev
启动主要是可以方便修改接口可以直接重载以及通知的方式更明显接口测试
12/21 补上
首先在 src 目录下 新建一个
index.ts
文件,用于测试的接口上面是一个很简单的 post 和 get 请求,之后我们新建一个
__test__
目录,在里面新建一个index.test.js
文件在根目录新建
babel.config.js
文件简单说下,这个 babel 的作用是让我们可以在 js 里面使用
es6 module
的语法,同时将 ts 文件转成 js,否则我们这个测试用例是 ts 的根本跑不起来,测试框架方面选用了jest
测试 http 库使用了supertest
,其实这块都是可以调整的,单元测试的目的就是对比数据是否符合预期__test__ index.test.js
运行
npx jest
,命令行如果没有抛出异常说明我们的代码符合预期,关于更多的 jest 内容可以查看文档最后
如果对你有帮助欢迎
stat
,如果有什么错误之处欢迎指出,关于代码本来全部想用 ts 举例的,但是 ts 并不是一定要上的,所以某些场景我就手动转了一下,看起来有点风格不统一还望谅解The text was updated successfully, but these errors were encountered: