`Mkbug.js`的前世此生 - 一款`OOP`风格声明式`Nodejs`框架

原文来自个人blog

Mkbug.js的前世此生 - 一款OOP风格声明式Nodejs框架

早在 2014年的时候第一次接触 Nodejs,当时还在 IBM,而注明的 Express.js框架也是 IBM负责维护。因而理所固然的是用 Express.js开发 Nodejs应用。虽然国内对 Koa很是热衷,可是我仍是有 1000个理由选择 Express.js。成熟可靠的维护团队和企业级应用背景,即便在 2015IBM将维护 Express.js交给了 StrongeLoop

一切的开始都要从一个线上故障提及

当时我所在的团队负责一个内部工具系统开发,后台使用Nodejs。可是有一天用户反馈有一个线上问题。数据错了。咱们立刻进行回归,发现线下是没问题的。找了好久也没找到缘由,后来从代码发现这个分支是由一个环境变量做为条件进行的分支处理。经过上线跟踪日志,发现实际读取的环境变量不正确。php

原来上一次升级开发人员忘记了修改生产环境环境变量的设置致使的。git

一般咱们会将系统的配置信息配到一个 Shell脚本中。而后注入到环境变量中。因为这些环境变量是在 Docker内生效。所以并不会影响其它程序,也不会引发冲突和覆盖,而且环境迁移能力强。是一种很是常见的手动。

为何咱们不能像Java框架那样,从配置文件中获取呢?JavaPHP的框架都会有一个配置管理模块去自动根据当前的模式获取对应的配置信息,维护很是方便,很是智能。github

因而Mkbug.js第一个核心模块诞生了 ---- Config。一个专门用来解决系统配置信息管理的模块。通过多年的项目实践和打磨,最终演变成了Mkbug.js的核心模块。express

// 目录结构
  ├── src 
      ├── controller 
          ├── ConfigTest.js
      ├── config
          ├── index.conf
          ├── index.dev.conf
  ├── index.js 

  // src/config/index.conf
  TITLE=Mkbug.js
  Content=A OOP style declare Nodejs Web framework base on Express.js

  // src/config/index.dev.conf
  TITLE=Mkbug.js DEV

  // src/controller/ConfigTest.js
  const { BaseController, Config } = require('mkbugjs');

  module.exports = class ConfigTest extends BaseController {
    getAction () {
      const conf = new Config('index')
      return conf
    }
  }

当咱们不设置process.env.NODE_ENV启动的时候,使用curl请求接口返回的数据咱们发现,在特定环境下的配置信息会继承没有指定环境的配置信息。json

$ curl -XGET http://localhost:3001/api/configtest
  {"TITLE":"Mkbug.js","Content":"A OOP style declare Nodejs Web framework base on Express.js"}

而当咱们以process.env.NODE_ENV=dev启动的时候,使用curl请求接口返回的数据咱们发现,在特定环境下的配置信息会继承没有指定环境的配置信息。api

$ curl -XGET http://localhost:3001/api/configtest
  {"TITLE":"Mkbug.js DEV","Content":"A OOP style declare Nodejs Web framework base on Express.js"}
Notice:当咱们没有指定具体的运行环境的时候,Config会默认加载与初始化参数名相同的conf文件,好比index.conf。可是当指定具体的环境(也就是process.env.NODE_ENV=dev)后,将会用对应环境下的同名配置文件内容对默认配置内容进行覆盖。好比本例中index.dev.conf的内容会覆盖index.conf中相同的key对应的内容。

让繁琐的配置远去 -- BaseController路由抽象接口

在离开IBM后加入互联网公司一直从事Nodejs开发工做,互联网项目高速迭代的特色让Nodejs项目的路由庞大,复杂且难以维护。时间久了,哪些路由信息还在使用?哪些已通过时不用了?模块是否和路由设置一致?一个上百接口服务的路由虐的我痛不欲生。有人会说,你多拆几个文件就好啦。但是文件越多。管理起来也越繁琐。浏览器

因而第二个模块在2016年诞生了。也就是BaseController的雏形RouterMgt。能够自动遍历文件路径,加载模块,生成路由配置信息。本觉得一切都清净了。可是看着Koa的兼容新ES6语法仍是内心痒痒。可是无奈与Nodejs 8.x暂时还不支持那么多语法。而团队内也更倾向于Koa支持新语法的诱惑。可是很快因为公司业务发展不佳,公司解散了。服务器

再加入新公司后,Nodejs已经发展到了10.x。已经支持了99.7%的新语法。而我须要的功能也都支持了。因而BaseController完成了第二个版本。也就是如今
Mkbug.js核心模块 ---- OOP风格声明式路由管理模块。闭包

// 目录结构
  ├── src 
      ├── controller 
          ├── _params
              ├── _id.js
          ├── pathtest
              ├── HelloWorld.js
  ├── index.js 

  // src/controller/_params/_id.js
  const { BaseController } = require('mkbugjs');

  module.exports = class IdTest extends BaseController {
    getAction () {
      return 'Hello ! this message from IdTest';
    }
  }
  // src/controller/pathtest/HelloWorld.js
  const { BaseController } = require('mkbugjs');

  module.exports = class HelloWorld extends BaseController {
    // 为了不与上面的接口路径冲突,因此在末尾增长了test
    getTestAction () {
      return 'Hello! this message from pathtest/hellowrold!';
    }
  }

它并不像egg或者thinkjs以及其它一些Nodejs框架那样须要本身手动管理不少配置信息。一切都会帮你配置好。你仅仅须要实现BaseController接口,那么api的路由信息就都帮你生成了。而且支持路由参数。浏览器请求结果:app

$ curl -XGET http://localhost:3001/helloworld/idtest
  Hello ! this message from IdTest

  $ curl -XGET http://localhost:3001/pathtest/helloworld/test
  Hello! this message from pathtest/hellowrold!

是否是很是简单便捷?不再须要那么冗余的配置文件了。工程变得更加简洁。这个模块早期版本的轻量级,门槛低,低成本,易于维护的特色,在18年落地了不少公司内部工具,更是在19年成功帮助前东家的新业务线签下两份订单。同时也经受住了大数据量访问的考验。同时提供了拦截器接口,能够获取接口响应时长等很是重要的信息。

在经历了3年多的打磨后,Mkbugjs也基于该模块诞生了。

Notice:由于Controller的方法名是很是关键的配置信息,类的方法名必须以HTTP协议的Methods名开头Action结尾,这样才会被识别为路由信息。若是在MethodsAction之间没有其它单词,则没有对应的路径。就像上面的例子同样。

Notice:http协议目前支持9个方法,固然,实际上不一样浏览器还有更多的方法。可是为了保持与标准同步,Mkbug.js支持9种方法,分别是:GET,HEAD,POST,PUT,DELETE,CONNECT,'OPTIONS,TRACE,PATCH

Expressjs尴尬的响应处理

使用过Express.js的开发者都知道,Express.js必须显示调用end, json, writeapi去结束响应,不然客户端会一直挂起,直至超时。而Mkbugjs提供了统一的响应返回机制,即只须要return,或者throw一个MkbugError异常对象,便可返回客户端请求。也就避免了请求挂起。同时也统一了响应返回的标准。统一系统风格。

// src/controller/StatusTest.js
  const { BaseController, MkbugError } = require('mkbugjs');

  module.exports = class StatusTest extends BaseController {
    getTestAction () {
      throw new MkbugError(500, 'Error Test')
    }
  }

执行结果以下:

$ curl -w "  status=%{http_code}" localhost:3001/statustest/test
  Error Test  status=500
Notice: 在Mkbug.js中,任何实现BaseController的接口,和BasePlugin的中间件均可以经过这种方式自动响应客户端请求。

下降中间件门槛 -- BasePlugin 中间件抽象接口

我带过的不少人对JS并非很是深刻,以致于常常把闭包,执行上下文写错。致使中间件常常出问题。而BasePlugin只须要用户实现本身的exec接口逻辑,便可自动完成中间件的配置和执行操做。并不须要开发者过多的参与底层配置。

BasePlugin提供一个exec接口,该接口主要用于实现中间件的业务逻辑,并提供了resreq接口。当咱们须要拦截请求,只须要抛出MkbugError异常便可。不然会执行下一步路由。

// 目录结构
  ├── src 
      ├── controller 
          ├── MiddleWare.js
      ├── plugin
          ├── TestMiddleware.js
  ├── index.js 

  // src/plugin/TestMiddleware.js
  const { BasePlugin, MkbugError } = require('mkbugjs');

  module.exports = class TestMiddleware extends BasePlugin {
    exec (req, res) {
      if (req.query.test === '1') {
        throw new MkbugError(200, 'Reject from TestMiddleware')
      }
    }
  }

  // src/controller/MiddleWare.js
  const { BaseController } = require('mkbugjs');

  module.exports = class MiddleWare extends BaseController {
    getAction () {
      return 'Hello World'
    }
  }

这里咱们建立了3个中间件,其中2个对请求中query.test等于12进行了拦截,分别返回200状态和401状态。

咱们先测试第一个中间件:

$ curl -w " status=%{http_code}" localhost:3001/api/MiddleWare?test=1
  {"msg":"Reject from TestMiddleware1"} status=200

在这里咱们能够看到经过MkbugError自定义返回的http请求的status和内容。而若是在中间件中什么也不作的话:

$ curl -w " status=%{http_code}" localhost:3001/api/MiddleWare
  Hello World status=200

能够看到对应的路由接口数据被正常返回。

Notice:这里须要注意的是MkbugError对象必须被throw出来,而不是return出来。

Mkbug.js的诞生

在2020年五一假期,由于疫情不能回家探亲,一我的在居所无聊,忽然冒出一个想法,为何不将这些有用的中间件组合成一个完整的框架呢?功能不亚于任何已有的Nodejs框架,并且风格更加新颖,也更接近于广大JSer对ES6新语法的诉求。

因而花了五天时间,创造了Mkbug.js

// index.js
  const express = require('express');
  const app = express();

  const { Mkbug } = require('mkbugjs');

  new Mkbug(app)
    .create('/') // 请求url前缀
    .use(bodyParser.json()) // 使用express中间件
    .start(3001, (err) => { // 启动,同app.listen
    if (!err)
      console.log('Server started!')
    else
      console.error('Server start failed!')
  })
  
  // src/controller/HelloWorld.js
  const { BaseController } = require('mkbugjs');

  module.exports = class HelloWorld extends BaseController {
    getAction () {
      return 'Hello World';
    }
  }
Notice:Mkbugjs提供了丰富的Web服务器经常使用的类。只须要继承并实现对应的类,便可实现自动注入。就像Java流行的Spring Boot或者PHPThinkphp同样。很是简单。

最后

我写了这么多你还没心动吗?目前安装量已经超过1400次/月。并完成了全部case的测试用例。还不来尝试一下吗?

Github
Document