最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • node 接口开发指南

    正文概述 掘金(拳打南山敬老院)   2020-12-19   416

    接口请求与其它资源请求没有什么不同,都是借助 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 借助中间件来实现各种拓展,就是类似于插件的功能,它本身非常像洋葱结构 node 接口开发指南

    例如上面的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一定会让人抓狂。

    借助中间件的机制很容易编写出一个带有sendDatathrowError的功能,只需要在 ctx 中返回,之后调用 next 让后面的实例执行

    app.use(async (ctx, next) => {
      ctx.sendData = () => {};
      ctx.throwError = () => {};
      await next();
    });
    

    上面的例子是简化过的,这里稍微错开一下具体实现之后再详细讲解

    接口结构

    上面说了要有一个sendDatathrowError的方法来统一返回信息和抛出错误,这里就说下这两个方法的具体参数以及实现。

    首先接口的返回信息,期待它是固定成下面这种结构

    {
      "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>>;
    };
    
    

    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,如果有什么错误之处欢迎指出


    起源地下载网 » node 接口开发指南

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元