基于 Koa 的微服务 Node.js 框架设计思路与简单实现

发表于 3年以前  | 总阅读数:366 次

Koa 官网的介绍是这样介绍自己的:

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

从上面的描述中我们可以知道,Koa 是一种简单好用的 Web 框架。它的特点是优雅、简洁、表达力强、自由度高。其本身代码只有 1000 多行,所有功能都可以通过插件的方式扩展,很符合 KISS 原则与 Unix 哲学。比较有名的 Node.js 业务框架 egg.js 就是是继承自Koa。

但 Koa 的劣势也很明显,就是太过自由,并没有内置过多的功能,比如常见的请求体解析、路由、模板渲染等功能都没有,需要加载第三方中间件来实现。另外 Koa 只支持 Http 服务,无法满足业务方对于 RPC 服务的需求。

本文将对基于 Koa 的微服务 Node.js 框架设计思路做一些思考与探究,并且对实现方面做一些简单补充。让我们先从 Koa 的核心思想与原理开始。

Koa的核心思想与最简实现

核心思想:AOP 面向切面编程

AOP技术的诞生并不算晚,早在1990年开始,来自Xerox Palo Alto Research Lab(即PARC)的研究人员就对面向对象思想的局限性进行了分析。他们研究出了一种新的编程思想,借助这一思想或许可以通过减少代码重复模块从而帮助开发人员提高工作效率。随着研究的逐渐深入,AOP也逐渐发展成一套完整的程序设计思想,各种应用AOP的技术也应运而生。

这个名词听起来很高大上,可能很多人都听过,但是又没有彻底搞懂,到底什么叫面向切面编程?这里先不解释 AOP 的具体含义,而是举个简单的例子。

  • 农场的水果包装流水线一开始只有 采摘 - 清洗 - 贴标签

``

  • 为了提高销量,想加上两道工序 分类包装 但又不能干扰原有的流程,同时如果没增加收益可以随时撤销新增工序。

  • 最后在流水线的中的空隙插上两个工人去处理,形成采摘 - 分类 - 清洗 - 包装 - 贴标签 的新流程,而且工人可以随时撤回。

上面所说的每一道工序,都可以看作是一个切面。

回到 AOP 的含义:就是在现有代码程序中,在程序的生命周期横向流程中,加入或减去一个或多个功能,使原本功能不受影响

核心原理:koa-compose + Node.js http

Koa 可以被拆解为如下公式:

Koa = Node.js原生http服务 + 中间件引擎koa-compose

通过把中间件用 Promise + async/await 的方式嵌套组合,Koa 实现了比 Express 的线性模型中间件多了一倍切面的洋葱模型中间件,所以 Koa 能非常方便地实现类似响应时间计算、日志打印、鉴权等等常用功能。

下面举一个 Koa 官网的 demo 例子,可以看到这些功能的具体实现是多么的简单:

const Koa = require('koa');
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Node.js 原生 http 不用多说,下面着重讲一下中间件引擎 koa-compose 的实现,源码非常精简,核心代码只有 30 行左右:

function compose(middleware) {
  // 如果middleware不是数组,或者元素不是函数,则抛异常
  if (!Array.isArray(middleware)) {
    throw new TypeError('Middleware stack must be an array!');
  }
  for (const fn of middleware) {
    if (typeof fn !== 'function') {
      throw new TypeError('Middleware must be composed of functions!');
    }
  }
  // 返回一个闭包函数
  return function (context, next) {
    // last called middleware #

    let index = -1;

    return dispatch(0);

    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'));
      }

      index = i;

      let fn = middleware[i];

      if (i === middleware.length) {
        fn = next;
      }

      if (!fn) {
        return Promise.resolve();
      }

      try {
        // 将每一个 middleware 函数作为前一个函数的 next 参数

        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

简化掉判断逻辑,compose执行后就是类似下面这样的结构:

// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = stack;

const fnMiddleware = function (context) {
  return Promise.resolve(
    fn1(context, function next() {
      return Promise.resolve(
        fn2(context, function next() {
          return Promise.resolve(
            fn3(context, function next() {
              return Promise.resolve();
            })
          );
        })
      );
    })
  );
};

实际上 koa-compose 返回的是一个 Promise,从中间件(传入的数组)中取出第一个函数,传入context和第一个next函数来执行。

第一个 next 函数也返回一个 Promise,从中间件(传入的数组)中取出第二个函数,传入context和第二个next函数来执行。

第二个 next 函数也返回一个 Promise,从中间件(传入的数组)中取出第三个函数,传入context和第三个next函数来执行。

第三个...

以此类推。最后一个中间件中如果调用了 next 函数,则返回 Promise.resolve()。这样就把所有中间件串联起来了。类似栈的先进后出,每个中间件都有两个切面,这就是洋葱模型的实现原理。

Koa 最简实现


const http = require('http');

const Emitter = require('events');

const compose = require('koa-compose'); // 上面的 compose

/**
 * 通用上下文
 */

const context = {
  _body: null,

  get body() {
    return this._body;
  },

  set body(val) {
    this._body = val;

    this.res.end(this._body);
  },
};

class MiniKoa extends Emitter {
  constructor() {
    super();

    this.middleware = [];

    this.context = Object.create(context);
  }

  /**
    * 服务事件监听
    * @param {*} args
  */

  listen(...args) {
    const server = http.createServer(this.callback());

    return server.listen(...args);
  }

  /**
    * 注册使用中间件
    * @param {Function} fn

  */

  use(fn) {
    this.middleware.push(fn);
  }

  /**
    * 中间件总回调方法
    */

  callback() {
    if (this.listeners('error').length === 0) {
      this.on('error', this.onerror);
    }

    const handleRequest = (req, res) => {
      let context = this.createContext(req, res);
      let { middleware } = this;
      // 执行中间件
      compose(middleware)(context).catch(err => this.onerror(err));
    };

    return handleRequest;
  }

  /**
   * 异常处理监听
   * @param {EndOfStreamError} err
   */
  onerror(err) {
    console.log(err);
  }
  /**
    * 创建通用上下文
    * @param {Object} req
    * @param {Object} res
    */
  createContext(req, res) {
    let context = Object.create(this.context);
    context.req = req;
    context.res = res;
    return context;
  }
}

/**
 * 测试一下
 */
const app = new MiniKoa();
const PORT = 3001;
app.use(async ctx => {
  ctx.body = 'hello';
});
app.listen(PORT, () => {
  console.log(`started at port ${PORT}`);
});

基于 Koa 的微服务 Node.js 框架设计

然而,Koa只是一个HTTP框架,在实际的业务场景中,业务方除了要编写HTTP服务,还可能要编写其他类型的服务,比如 Thrift 服务、WebSocket 服务、消息队列的 Consumer 服务等等。应该如何设计这样一个不仅支持HTTP,还支持其他服务类型的微服务 Node.js 框架呢?我们先从这些服务的共性出发。

设计思想

HTTP、Thrift、WebSocket 等服务虽然应用层协议不同,但归根结底都是 C/S 结构的软件系统,其工作流程都可以划分为请求响应两个阶段,如下图所示:

如果把整个客户端与服务端之间的交互过程看成是一个完整流水线的话,那么请求响应自然就可以作为整个请求过程中的两个切面,因此 Koa 的洋葱模型也同样适用于除 HTTP 之外其他类型的服务。所以我们可以基于 Koa进行封装和改造,构造一个通用的服务中间件处理模型,这样我们就可以用 Koa 的形式来编写任意类型的服务程序。

框架的基本架构如下图所示:

image.png

简单实现

我们可以根据上述架构图做一个简单的实现(基于 AbstractServer 构建 HttpServer 与 ThriftServer ):

某些方法的细节部分这里先不做展开,感兴趣的同学可以自行查阅更多资料。

AbstractServer

import compose from 'koa-compose';
import http from 'http';

export abstract class AbstractServer extends EventEmitter {
    public middlewares: any[];
    public context;
    public request;
    public response;

    /**
     * Initialize a new application.
     *
     * @constructor
     */
    constructor(options) {
        super();
        this.middlewares = [];
        this.context = Object.create(options.context);
        this.request = Object.create(options.request);
        this.response = Object.create(options.response);
    }
    /**
     * Listen to specific port.
     */
    public listen(...args) {
        const server = this.createServer(this.callback());
        return server.listen(...args);
    }

    /**
     * Use the given middleware `fn`.
     *
     * @param fn - middleware
     */
    public use(fn): this {
        if (typeof fn !== 'function') {
            throw new Error('middleware must be a function!');
        }
        this.middlewares.push(fn);
        return this;
    }

    /**
     * Return a request handler callback.
     */
    public callback() {
        const fn = compose(this.middlewares);
        return (req, res) => {
            const ctx = this.createContext(req, res);
            return this.handleRequest(ctx, fn);
        };
    }

    /**
     * Handle request in callback.
     *
     * @param ctx
     * @param fn
     */
    public handleRequest(ctx, fn): Promise<void> {
        return fn(ctx)
            .then(() => this.handleResponse(ctx))
            .catch((err) => ctx.onerror(err));
    }

    /**
     * Initialize a new context.
     *
     * @param {Object} req - request
     * @param {Object} res - response
     */
    public createContext(
        req,
        res,
    ) {
        const context = Object.create(this.context);
        const request = Object.create(this.request);
        const response = Object.create(this.response);
        context.app = this;
        context.request = request;
        context.response = response;
        context.req = req;
        context.res = res;
        context.state = {};

        request.app = this;
        request.ctx = context;
        request.req = req;
        request.res = res;
        request.response = response;

        response.app = this;
        response.ctx = context;
        response.req = req;
        response.res = res;
        response.request = request;
        return context;
    }

    /**
     * Default error handler
     *
     * @param err - error
     */
    public onerror(err: Error): void {
        const msg = err.stack || err.toString();
        console.error();
        console.error(msg.replace(/^/gm, '  '));
        console.error();
    }

    /**
     * Create server
     *
     * @param callback - server request callback
     */
    public abstract createServer(callback);
    /**
     * Handle response after all middlewares have been executed
     *
     * @param ctx - context
     */
    public abstract handleResponse(ctx): void;
}

HttpServer

export class HttpServer extends AbstractServer {
   /**
     * initialize http server
     * @param options
     */
  constructor(options) {
    // more detail...
    const { context, request, response } = options;
    super({
      context,
      request,
      response,
    });
  }
  /**
     * Handle request.
     *
     * @param ctx - context
     * @param fn - composed middleware
     */
  handleRequest(ctx, fn) {
    // more detail...

    return super.handleRequest(ctx, fn);
  }

  /**
     * Create context.
     *
     * @param req - raw request
     * @param res - raw response
     */
  createContext(req, res) {
    const context = super.createContext(req, res);
    // more detail...
    return context;
  }
  /**
     * Handle response after all middlewares have been executed.
     *
     * @param ctx
     */
  handleResponse(ctx) {
    let { body } = ctx;
    const { res } = ctx;
    const code = ctx.status;
    // more detail...
    body = JSON.stringify(body);
    return res.end(body);
  }

  /**
     * Error handler.
     *
     * @param err
     */

  onerror(err) {
    super.onerror(err);
  }

  /**
     * Create a http server.
     *
     * @param callback - request handler
     */
  createServer(callback, options?) {
    // more detail...

    return http.createServer(callback) as any;
  }
}

总结

本文对 Koa、基于 Koa 的微服务 Node.js 框架在思想、原理、实现方面做了一些探讨。

Koa 的核心思想是 AOP,AOP 中切面的概念可以类比于流水线上可以自由增加或减少的“环节”,对于这样有固定流程的“环节”,我们都可以把它们当做AOP的切面,利用洋葱模型的思想去处理。

参考资料

Koa设计模式:https://chenshenhai.github.io/koajs-design-note/

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/vV9PFMhOZjPZj2vJ9GgLUw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录