大家好,我是前端点线面,毕业于华中科技大学,非科班出身的一枚新时代农民工,现在是百度前端研发工程师,著有《前端百题斩》、数十篇学习思维导图(go、ReactReduxVueVuex操作系统Linux设计模式jswebpacknginx、C++)以及大量前端进阶文章,大量同学已通过号主的系列内容获取心仪的offer,关注他获取海量资料、交流工作心得并进卧虎藏龙交流群。

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/

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

  1. Node.js 框架设计及企业 Node.js 基础建设相关讨论

    大家好,我是若川.19年我写的 lodash源码 文章投稿到海镜大神知乎专栏竟然通过了,后来20年海镜大神还star了我的博客,同时还转发了我的微博.时间真快啊.今天分享这篇Node.js的讨论. 2 ...

  2. 基于thrift的微服务框架

    前一阵开源过一个基于spring-boot的rest微服务框架,今天再来一篇基于thrift的微服务加框,thrift是啥就不多了,大家自行百度或参考我之前介绍thrift的文章, thrift不仅支 ...

  3. Node.js框架对比:Express/Koa/Hapi

    Node.js框架对比:Express/Koa/Hapi 引言 Express.js是当今使用最广泛的Node.js Web应用程序框架.它似乎是大多数Node.js Web应用程序中的基本依赖项,即 ...

  4. 通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解

    首先感谢张队@geffzhang公众号转发了上一篇文章,希望广大.neter多多推广dapr,让云原生更快更好的在.net这片土地上落地生根. 书接上回通过Dapr实现一个简单的基于.net的微服务电 ...

  5. 基于.NET CORE微服务框架 -谈谈surging API网关

    1.前言 对于最近surging更新的API 网关大家也有所关注,也收到了不少反馈提出是否能介绍下Api网关,那么我们将在此篇文章中谈谈surging Api 网关 开源地址:https://gith ...

  6. 基于.NET CORE微服务框架 -谈谈Cache中间件和缓存降级

    1.前言 surging受到不少.net同学的青睐,也提了不少问题,提的最多的是什么时候集成API 网关,在这里回答大家最近已经开始着手研发,应该在1,2个月内会有个初版API网关,其它像Token身 ...

  7. 基于.NET CORE微服务框架 -谈谈surging的服务容错降级

    一.前言 对于不久开源的surging受到不少.net同学的青睐,也受到.net core学习小组的关注,邀请加入.NET China Foundation 以方便国内.net core开源项目的推广 ...

  8. python sanic orm_基于sanic的微服务框架 - 架构分析

    感谢@songcser分享的<基于sanic的微服务基础架构>https://github.com/songcser/sanic-ms 最近正在学习微服务,发现这个repo不错,但不完整, ...

  9. JAVA SpringBlade 微服务开发平台框架,企业级的SaaS多租户微服务平台,基于Spring Boot 2.7

    SpringBlade微服务开发平台 完整代码下载地址:JAVA SpringBlade 微服务开发平台框架,企业级的SaaS多租户微服务平台 采用前后端分离的模式,前端开源两个框架:Sword (基 ...

最新文章

  1. 卷机神经网络的可视化(可视化中间激活)
  2. python频率_Python中的频率分析
  3. phpexcel中文教程-设置表格字体颜色背景样式、数据格式、对齐方式、添加图片、批注、文字块、合并拆分单元格、单元格密码保护
  4. 老板:kill -9 的原理都不知道就敢去线上执行?明天不用来了!
  5. 多元正态分布、多元t分布中的行列式求解 Java
  6. TwentyTwelve透明主题二次美化版
  7. Java中无法到达的语句
  8. java day49【综合案例day01】
  9. QTP是什么、QTP录制回放的原理、loadrunner、Selenium、QTP三者区别?QTP下载地址
  10. java面试简历精通n_Java简历与面试
  11. 新学期个人作息时间安排
  12. install pecl php_pecl安装以前的php版本
  13. k8s高可用环境部署7(Dashboard and metrics)
  14. 继承CAcUiStringEdit,改变编辑框的字体颜色,以及背景的颜色
  15. 第五章语言模型:n-gram
  16. Python画出时钟
  17. 1231: ykc买零食
  18. css 字体 图片 动画
  19. MathType可以和哪些Microsoft Office版本一起使用?
  20. 大专前端实习生如何挣到月薪 20k

热门文章

  1. dpkg ihr状态_基于ZIHR航向角修正方法的行人导航算法
  2. PMBOK 第六版十大知识领域
  3. 用docker安装redis集群报错“Bad directive or wrong number of arguments\n“,“stream“:“stderr“”
  4. 08.7. 通过时间反向传播
  5. 抖音新账号如何提升权重?提升抖音权重的四个方法!
  6. Excel中按照数字和汉字列对所有列排序
  7. iOS视频添加音乐 去除原声
  8. MPLAB X IDE v5.30离线安装MCC方法
  9. 【python镜像分词】运用到文章
  10. .NET中异常处理的最佳实践