框架设计:如何基于 Egg 设计 Node 的服务框架

Node 的工具化价值自很少言,而服务化价值须要长期探索,小菜前端在服务化路上依然是小学生,目前的尝试是是 Cross 框架,尝到了一些甜头。前端

我想,几乎没有前端工程师会对 Node 不感兴趣,但用它适合干哪些事情,每一个人的答案都不一样了,好比小菜前端,咱们对于 Node 的深度尝试,能够在这里找到答案:《技术栈:为何 Node 是前端团队的核心技术栈》[1],但关于让 Node 作服务端的事情,却只有少数团队有这样的勇气。git

之因此缺少自信和勇气,本质的缘由在于 Node 尚未一个足够顺手的框架来让你快速证实驱动业务的价值,也在于对 Node 缺少足够的了解和信心,以及相对于服务端的强势,每每前端在侵蚀服务端领域的时候,会受到这般那样的挑战甚至刁难,这也成为了在团队推广 Node 常遇到的阻力,但愿你们从小菜团队身上能够找到一些答案,其中答案的一部分就是要对 Node 要有足够的了解和认知,才能够为通用问题抽象出通用的方案去实施,在小菜,就是对于 Node 框架的封装,这个框架还没有开源,名叫 Cross,寓意没有迈不过的技术门槛。github


分清楚 Node 的边界

先后端的团队自己是相爱相杀的关系,是左右手的双十合十,既有接口联调上的上下游数据立场,也有必须与对方精诚合做才能一次次拿下项目的战役,而在服务这件事情,前端就直接介入到了服务端的领域,并且从整个行业来看,这种介入在大中型公司已成为不可阻挡的趋势,不管是淘宝、天猫、支付宝、腾讯、网易、百度,包括创业独角兽大搜车、贝贝网、Rokid,海内外不分国籍不分领域的众多公司都有一个个团队在深度耕耘,因此这里的第一个边界是先后端的边界。面试

一千家公司可能就有一千种商业模式,一千种用户画像,一千种业务特征,既有高依赖算法的高实时计算的井喷式访问场景,也有日均几十 UV 几百 PV 的 toB 大客户产品,什么场景用 Node 合适,什么不合适,这第二个边界就是 Node 在业务领域里的服务边界。redis

只有弄清楚这两个边界,才有 Node 的生存土壤,脱离了这两个边界,就不免到处碰壁没法落地,针对先后端边界,我从前写过这样一段话:算法

数据的控制权和与视图所依赖的 API,这里就是目前先后端的边界,数据控制权属于后端,API 属于后端,把先后端简单看作是一个完整的系统,这个系统中自 API 向下天然是后端的,API 向上则属于前端。编程

在 API 下面,对于数据的业务流转流转逻辑,在上面对于数据的调用和组装,这就是数据层面的自然分界点,而 Node 植入进去,也必须在 API 这一层与 Java 保持规范的统一和兼容,经过 RPC 无缝的调用才能来谈边界,而这个边界个人理解它能够是非强业务耦合的,好比独立的内部协同系统,也能够是非高计算型的,能够是相对独立的异步的高并发的模块,好比消息堆栈的频繁拉取推送,好比日志的收集整理等等,总结起来就是非复杂业务流程的,非高计算型的这个地方能够做为 Node 进入的边界。小程序

而对于业务的服务边界,只要的小而美的相对独立的系统,只要不是核心业务,均可以用 Node 快速开发,好比小菜这里就有报表系统、打包系统、发布系统、市调系统、日志系统、可视化平台、招聘面试系统、Bug 跟踪系统等等。后端

以上的两个边界,你们在仔细评估的时候,必定不要忘了本身团队人员的能力配置,能不能 Hold 住 Node,有没有 Node 技术专家坐镇,否则仓促使用可能还拔苗助长。bash


为何要封装 Cross

在弄清楚上述的边界后,小菜前端在 1 年多的时间里,对 Node 进行深度的使用,从基建系统到相对独立的业务系统,整个走下来,团队更多同窗掌握了 Node 的使用,同时每一个系统之间的差别性也愈来愈大,有的用的是 Koa 有的是 Koa2,有的是 Thinkjs 有的是 Express,还有的是原生 NodeJS。

显然每一个人的偏好都不一样,代码质量也不一样,工程架构方式也不一样,这为后期的维护带来巨大的麻烦,尤为是作 Node 监控时候,发现无法用一套方案作批量的部署,也一样不能作水平的快速扩展,须要挑选一个框架基于它作统一的封装,从而把前端参与的全部服务端建设能够统一块儿来,并且现实是咱们的前端和 Node 应用因为整个工程的构建与服务部署方式的不一样,已经散落到各个服务器上,致使维护成为了瓶颈,也必须到作出改变的时候了,这是当时的部分零散的应用图:



为何选择 Eggjs

小菜前端在使用 Eggjs 做为 Nodejs 的基础服务框架以前使用过诸如 Koa、Express、Koa二、Thinkjs 等框架,其中与 Eggjs 最接近的当属奇舞团开源的 Thinkjs[2] , 一样的约定大于配置,一样的基于 Koa2 进行包装完善,一样的采用多级分层的设计方式(Controller, Service 等等),让应用开发变得更加清晰明了,然而有趣的是, Thinkjs 的开源时间(2013)早于 Eggjs 的开源时间,其在 github 上的 star 的增加速度倒是远远落后于 Eggjs,NPM 下载数亦然,虽然 thinkjs 开发体验也不错,小菜最后会选定 Eggjs 做为 Nodejs 服务框架的缘由,除了上述提到的优势以外,还有以下几点 :

  • 高度可扩展的插件机制

  • 方便定制上层框架

  • 丰富且活跃的社区生态

  • 渐进式开发

  • 多进程管理

小菜前端从 18 年年初就开始使用 Eggjs 了,咱们的不少项目都是基于 Eggjs 搭建的,其中包括咱们的报表系统、GraphQL 网关、小程序后台服务等。在使用 Eggjs 开发这些项目的过程当中咱们逐渐造成了本身的一套适用于宋小菜的基于 Eggjs 的上层框架,基于小菜特定业务场景长出来的 Framework,它的定制程度很高,你们能够参考咱们实现这套框架时用到的技巧与方法,这些套路应该是通用的。


秉承怎样的设计理念

考虑授人以鱼不如授人以渔嘛,咱们先分享下咱们的设计理念,这是最简单却也最重要的开始部分,咱们的目标是风格统1、上手容易、维护方便:


而后就是总体需求的整理和开发集成,在开发集成个过程当中不断调优:


image.png

定完目标,设计好流程,就要准备具体的实施了,咱们实施涉及到过程,主要从下面四个方面着手:

  • 框架关系

  • 通用 API

  • 插件定制

  • 工程管理


如何设计 Framework


框架关系

咱们将全部通用的 API 和经常使用工具函数以及经常使用的插件(redis、gateway)等统一集成在基础框架 baseFramework 中,因为 Egg 支持多级框架继承,因此咱们能够根据基础框架 baseFramework 衍生出其余框架如 GraphQL 相关的框架、微服务相关的框架,它至关因而一颗框架种子,能够往不一样的方向定制:


image.png


通用 API


1. 请求参数统一获取

假定某个 HomeController 有成员函数 testAction 既要处理 post 请求又要处理 get 请求,就有可能出现如下状况:

const { Controller } = require('egg');

module.exports = class HomeController extends Controller {
	testAction(){
    const { ctx } = this;
    const { method } = ctx.request;
    const id = method === 'GET'? ctx.request.query.id : ctx.request.body.id;
    ...
	}
}复制代码

咱们能够将其优化为:

/* yourapp/app/controller/home.js */
const { BaseController } = require('egg');
// 或者
const { BaseController } = require('your-egg-framework');

module.exports = class HomeController extends BaseController {
	testAction(){
    const id = this.getParam('id');//
    ...
	}
}
  
/* egg-baseframework/core/base_controller.js */
const { Controller } = require('egg')

module.exports = class BaseController extends Controller {
	getParam(key) {
  	const { ctx } = this;
    const { method } = ctx.request;
    if (method === 'GET') {
    	if(key) {
      	...
      } else {
      	...
      }
    } else {
			...
    }
  }
}
/* your-egg-baseframework/lib/index.js */
const { BaseController } = require('../core/base_controller');

module.exports = {
  BaseController,
  ...
}

/* your-egg-framework/app.js */
module.exports = (app) => {
	require('egg').BaseController = BaseController
}复制代码


2. 返回数据格式化

方法同上,咱们能够在 BaseController 中定义统一的调用成功和调用失败返回函数,并在函数中处理返回数据从而避免返回数据不规范的问题


3. 通用工具函数

咱们能够将平时业务开发中可能会用到的工具函数统一经过框架扩展的额形式定义到内置对象helper 上,这些均可以以框架扩展(extend)的方式集成进来,好比参数转化啊,错误信息格式化等等。


4. 增长参数校验层

咱们能够将参数校验这一步抽离出来成为 logic 层。有两种方式能够作到:

  • 在框架加载时调用 app.loader.loadToContext 将全部 controller 对应的参数校验函数挂载到 context 上,在 controller 执行相应的处理函数时调用

  • 在你的框架继承的 appWorkerLoader 中覆写 eggjs 的 loadController , 对每个 controller 的处理函数都使用对应的 logic 进行代理


插件定制

Egg 的拥有着丰富的插件生态,然而总有些咱们须要用到的插件不太符合咱们的要求,好比:

  • egg-redis 长久不支持哨兵模式

  • egg-graphql 不支持链接其余 graphql 服务

  • egg-kafka 长久没有维护

这个时候就须要咱们本身动手编写或修改相应的插件了,而有些在公司层面上通用的功能如:Java 服务端网关请求(egg-gateway)、用户鉴权(egg-auth)等咱们也将其封装为插件集成到基础框架中,讲实话,整个框架开发中,让人最开心最后成就感的部分就是写插件的时候:


image.png


工程管理

因为插件和插件之间,插件和框架之间,框架和框架之间存在相互依赖的关系,代码管理就成为了比较头疼的问题,推荐使用目前比较火的 monorepo 来进行管理。规范版本发布流程,避免出现不兼容问题。


总结

关于 Cross 的建设咱们差很少投入了一个多月的周期,从投入产出比来看仍是很划算的一次尝试,可是在落地时候也会遇到很多问题,从人和团队的角度来看,这样的一套 Framework 须要有必定的 Node 编程能力的同窗才能较好的用起来,对于全部人依然有必定的心智成本,有没有可能把这个成本继续下降呢,走向 Pass 跟高阶的只关心业务逻辑不关心背后实现的阶段呢,这是一个很值得研究的课题,另外就是从事情的角度,若是业务中没有那么多的场景来承载这个框架,事实上它是很难继续进阶的,由于没有足够的应用和测试场景来暴露问题,这也是咱们当下遇到的一个实际困难,缺乏 Node 好手掣肘了咱们前进的步子,不过好消息是接下来的业务场景已经铺开了,团队也刚刚进了一个 Node 选手,接下来看看应用后发力效果如何。

相关文章
相关标签/搜索