ext get id js_如何为团队定制自己的 Node.js 框架?

ext get id js_如何为团队定制自己的 Node.js 框架?_第1张图片

作者:天猪

转发链接:https://mp.weixin.qq.com/s/WhqDlckGMwraEeOAs6IfTA

背景

回想下,当你需要新起一个 Node.js 应用的时候,会怎么做?

憨厚一点的就从头开始初始化,一个个插件的安装,CTRL +C 一个个的配置。好一点的,就会封装一个骨架,然后一键生成新项目。

那如果在应用中的一些实践,想下沉为基础能力,就需要修改骨架。此时,如何把旧项目升级呢?一两个还好说,如果十几个,甚至上百个呢?

我们的实践是:基于 Egg 封装一个适合特定团队业务场景的上层业务框架。

如果你的团队需要:

  • 统一的技术选型,比如数据库、模板、前端框架及各种中间件设施都需要选型,而框架封装后保证应用使用一套架构。

  • 统一的默认配置,开源社区的配置可能不适用于公司,而又不希望每个应用重复配置。

  • 统一的部署方案,通过框架和平台的双向控制,应用只需要关注自己的代码。

  • 统一的代码风格,框架不仅仅解决代码重用问题,还可以对应用做一定约束,并定制适合团队的目录加载规范。

鸭蛋炒鸡蛋 = ?

下面,我们来一起基于Egg 定制一个独属于我们的 鸭蛋框架(yadan),它提供以下能力:

  • 内置 nunjucks来提供服务端模板渲染能力。

  • 封装一套请求后端接口的协议,并自动加载 app/rpc/** 为ctx.rpc.clz.method() 方法。

// 请求后端接口,查询用户信息
const userInfo = await ctx.rpc.user.getDetail('yadan');
// 渲染首页
await this.ctx.render('home.tpl', { userInfo });

完整的示例代码可以参见 https://github.com/atian25/yadan,下文我们会讲解关键细节。

初始化

通过骨架一键初始化 Framework 代码:

$ npm init egg --type=framework yadan

可以看到,Framework 的目录结构,和一个 Egg 应用几乎一模一样,熟悉的 config、 app/extend 、 app/service 。

yadan
├── app
│   ├── extend
│   └── service
├── config
│   ├── config.default.js
│   └── plugin.js
├── lib
│   └── framework.js
├── test
│   ├── fixtures
│   └── framework.test.js
├── README.md
├── index.js
└── package.json

接下来我们逐个讲解下关键细节。

框架定义

首先来看下入口文件,其实就是继承了下 Application,然后把当前目录通过EGG_PATH 的约定,加入到 Egg 的LoadUnits中去。

骨架已经默认生成,基本上不用改,代码如下:

// lib/framework.js
const path = require('path');
const egg = require('egg');
const EGG_PATH = Symbol.for('egg#eggPath');
class Application extends egg.Application {  get [EGG_PATH]( "EGG_PATH") {
    return path.dirname(__dirname);
  }}class Agent extends egg.Agent {  get [EGG_PATH]( "EGG_PATH") {
    return path.dirname(__dirname);
  }}module.exports = Object.assign(egg, {  Application,  Agent,});

内置插件

我们要内置模板插件,先安装依赖:

tnpm i --save egg-view-nunjucks

再挂载下插件:

// config/plugin.js
exports.nunjucks = {  enable: true,
  package: 'egg-view-nunjucks',
};

默认配置

可以设置统一的默认配置,如把默认的模板引擎设置为 nunjucks :

// config/config.default.js
module.exports = () => {  const config = {};
  config.view = {
    defaultViewEngine: 'nunjucks',
    mapping: {      '.nj': 'nunjucks',
      '.tpl': 'nunjucks',
    },  };  return config;
};

RPC 规范

除了常规的扩展外,在实际业务开发中,我们往往需要为团队定制一些新的目录规范。

此处我们来定义一个 RPC 规范:

  • 约定 app/rpc/** 将被挂载为 ctx.rpc.**

  • 提供 egg.RPC 基类,对后端请求进行封装,供应用层继承。

定义 RPC 基类

直接 show me the code ,其实就是对 HTTP 协议做了一个上层封装,统一了响应格式。

该 RPC 类在framework.js里面会被引入到 egg 对象上。

// lib/rpc.js
class RPC {  constructor(ctx) {    this.ctx = ctx;
    this.app = ctx.app;
    this.logger = ctx.logger;
    this.config = ctx.app.config;
  }  async api(apiName, data) {    const host = this.config.rpc.host;
    try {
      const targetUrl = `${host}/api${apiName}`;
      this.logger.info(`[RPC] request api: ${targetUrl}`);
      const res = await this.ctx.curl(targetUrl, {
        dataType: 'json',
        contentType: 'json',
        timeout: 5000,
        data,      });      return this.handlerResult(res, { apiName, data });
    } catch (err) {
      return this.handlerError(err, { apiName, data });
    }  }  handlerResult(res) {    return {
      success: true,
      data: res.data,    };  }  handlerError(err, meta) {    this.logger.error(`[RPC] request ${meta.apiName} fail: ${err.message}`);
    return {
      success: false,
      error: {        message: err.message,      },    };  }}module.exports = RPC;

RPC 加载逻辑

在 《如何为团队量身定制 Egg 目录挂载规范?》[1]一文中有专门介绍过。

此处我们仅需要简单配置下:

// config/config.default.js
module.exports = () => {  const config = {};
  // ...  // 自定义加载规范  config.customLoader = {
    rpc: {      directory: 'app/rpc',
      inject: 'ctx',
      loadunit: true,
    },  };  return config;
};

然后我们如果在应用中添加app/rpc/user.js文件:

// app/rpc/user.js
const { RPC } = require('egg');
module.exports = class TestRPC extends RPC {
  async getDetail(id) {
    return await this.api('/user/detail', { id });
  }
};

在 Controller 那边就可以直接调用 ctx.rpc.user.getDetail()了。

class HomeController extends Controller {
      
  async detail() {
    const { ctx } = this;
    const name = ctx.params.name;
    const { data: userInfo } = await ctx.rpc.user.getDetail(name);
    await ctx.render('home.tpl', userInfo);
  }}

单元测试

单元测试很重要,尤其是 Framework 必须要求 100% 的测试覆盖率。

首先需要新增 fixtures ,可以看到,就是一个标准的 Egg 应用,用来模拟我们的业务场景。

└── test
    ├── fixtures
    │   └── example
    │       ├── app
    │       │   ├── rpc
    │       │   │   └── user.js
    │       │   ├── controller
    │       │   │   └── home.js
    │       │   └── router.js
    │       ├── config
    │       │   └── config.default.js
    │       └── package.json
    └── framework.test.js

然后编写一个个的单测:

跟 Egg 应用的单元测试几乎没区别,只是多了一个framework: true 的声明。

// test/framework.test.js
const mock = require('egg-mock');
describe('test/framework.test.js', () => {
  let app;  before(() => {    app = mock.app({      baseDir: 'example',
      // 声明是测试 Framework
      framework: true,
    });    return app.ready();
  });  after(() => app && app.close());  afterEach(mock.restore);  it('should GET /', async () => {
    return app.httpRequest()
      .get('/')
      .expect('
yadan
\n')
      .expect(200);
  });});

如果你的 Framework 提供了多个功能,我们建议拆为多个 fixtures,一个特性一个特性的测试,并覆盖完全。

通过 npm run cov 来查看你的单元测试覆盖率,我们内置骨架也帮你自动生成了GitHub Action的 CI 测试配置。

发布流程

跟平时发布 npm 没啥区别,此处介绍下我们的一些最佳实践。

本地验证

如果你想在发布前先测试,首先可以通过npm link 方式来短链到应用中

$ cd /path/to/demo
$ npm link /path/to/framework

详情参见你所不知道的模块调试技巧 - npm link #17[2]

发布 beta

接着就可以发布测试版本了,此时可以先发0.x :

  • 修改 package.json 为0.0.1

  • 发布指令为 npm publish --tag=beta

  • 在应用引入时为 npm i --save @eggjs/yadan@beta

这样的好处是,在 0.x 升级新版本的时候,应用那边能安装到最新的版本。

因为根据Semver规则, ^0.0.1 是安装不到 0.1.0 等版本的。

发布正式

当 beta 验证通过后,应该果断的发布 1.x 版本,禁止停留在 0.x 版本,否则你会踩坑。

Chromium 等都版本低了,你吝啬个啥啊,版本号又不值钱。

  • 修改 package.json 为 1.0.0

  • 发布指令去掉 beta,改为 npm publish

  • 在应用引入时为 npm i --save @eggjs/yadan

后续发版本,要严格遵循 **Semver **规则,不能有 break change,且要求应用不锁版本,通过^1 的方式引入依赖。

如果实在无法兼容,就发大版本,且最好提供 codemod 来帮旧应用自动升级。

应用层

在应用中使用你的框架很简单,只需要在 package.json 简单声明下:

{
      
  "name": "egg-showcase",
  "egg": {
    "framework": "@eggjs/yadan"
  },  "dependencies": {
    "yadan": "^1"
  }}

然后正常启动即可,会看到以下信息:

[master] yadan started on http://127.0.0.1:7001 (1511ms)

这样,所有依赖这个 Framework 的应用,都可以使用它提供的标准化能力和团队规范。

框架的框架

至此,我们就已经完成了一个基于 Egg 的上层业务框架的开发,是不是觉得很简单?

简单就对了!Egg 本身的定位就是框架的框架,帮助团队的技术负责人,来定制适合特定的业务场景的上层业务框架。

在阿里内部也是这么实践的:

ext get id js_如何为团队定制自己的 Node.js 框架?_第2张图片

实际上,框架还支持多层继承,在我们内部的继承关系其实是:

特定场景框架:      chair-serverless  |   midway-faas    |
                          ↑                 ↑团队业务框架:            chair       |     midway       |   nut     | ...
                          ↑                 ↑               ↑         ↑
阿里统一框架:           @ali/egg
                          ↑                 ↑               ↑         ↑
开源社区框架:             egg

框架的演进

从上面可以看到,Egg 的应用、插件、框架的目录结构几乎一模一样。

实际开发过程中,我们也有一套渐进式的演进方式,分享给大家:

  • 实验性的功能,可以先在应用里面实现,作为 inline plugin 通过 path 方式来挂载。

  • 功能稳定后,就抽出来变为独立的插件,应用再通过 npm 依赖方式引入,只需改两行代码即可。

  • 当该功能成熟后,成为团队的统一规范时,直接把这个插件集成到 Framework 中,所有应用只需重新安装下依赖,即可立刻享受到。

这个过程是闭环的,是渐进式,而且升级过程几乎不同。

详见文档渐进式开发[3]

最后补一张之前的 Slide:

ext get id js_如何为团队定制自己的 Node.js 框架?_第3张图片

写在最后

希望通过本文,让大家了解到 Egg 的三个概念,也能一窥我们如此设计架构的原因。

一个人的项目怎么样都无所谓,但当大规模应用的时候,数千个应用分布到数十个团队里面,此时的生态共建、差异化定制、应用治理能力,就变为一个很复杂的工程问题了(可以思考下这种规模下如何推动框架升级和治理)。

这也是我们为什么做 Egg 的初心,它的定位就是框架的框架,专注于提供一套 Loader 规范和插件框架体系,目标用户是团队的架构师。 它本身是不能跟市面上的框架直接对比的,基于它搭建的上层业务框架,才是一个合适的框架对比对象。

但实际上,框架只是整个链路中的很小的一点,Egg 也已经是我们 3 年前的实践了。

如何让前端同学可以在不增加额外学习成本的情况下,无感无痛地使用服务端能力,目前还有非常多急需解决的问题,需要深入到 PaaS、中间件基础设施、研发平台等等层面。我们还在路上,正致力于为蚂蚁提供 轻研发、免运维 的下一代 Node.js 研发方案。

推荐JavaScript学习相关文章

《细品原生JS从初级到高级知识点汇总(一)》

《细品原生JS从初级到高级知识点汇总(二)》

《细品原生JS从初级到高级知识点汇总(三)》

《细品原生JS从初级到高级知识点汇总(四)》

《学习 jQuery 源码整体架构,打造属于自己的 js 类库》

《细说RTSP实现前端直播流探索记「干货」》

《一文带你搞懂前端登陆设计》

《使用 Node.js 将图片中的苹果变成橘子「实践」》

《基于Canvas实现的高斯模糊(上)「JS篇」》

《基于Canvas实现的高斯模糊(下)「JS篇」》

《由浅入深,66条JavaScript面试知识点(一)》

《由浅入深,66条JavaScript面试知识点(二)》

《由浅入深,66条JavaScript面试知识点(三)》

《由浅入深,66条JavaScript面试知识点(四)》

《由浅入深,66条JavaScript面试知识点(五)》

《由浅入深,66条JavaScript面试知识点(六)》

《由浅入深,66条JavaScript面试知识点(七)》

《为什么 setTimeout 有最小时延 4ms ?》

《如何处理 Node.js 中出现的未捕获异常?》

《Angular v10.0.0 正式发布,不再支持 IE9/10》

《基于 Docker 的 SSR 持续开发集成环境实践》

《细聊图解webpack 指南手册》

《一文带你彻底搞懂 NPM 知识点「进阶篇」》

《细聊webpack性能优化面面观》

《JS实现各种日期操作方法汇总》

《「实践」细聊前端性能优化总结》

《「实践」浏览器中的画中画(Picture-in-Picture)模式及其 API》

《「多图」一文带你彻底搞懂 Web Workers (上)》

《「多图」一文带你彻底搞懂 Web Workers (中)》

《深入细聊前端下载总结「干货」》

《细品西瓜播放器功能分析(上)「实践」》

《细品西瓜播放器功能分析(下)「实践」》

《细聊50道JavaScript基础面试题「附答案」》

《webpack4主流程源码解说以及动手实现一个简单的webpack(上)》

《webpack4主流程源码解说以及动手实现一个简单的webpack(下)》

《细聊前端架构师的视野》

《细聊应用场景再谈防抖和节流「进阶篇」》

《前端埋点统一接入方案实践》

《细聊微内核架构在前端的应用「干货」》

《一种高性能的Tree组件实现方案「干货」》

《进击的JAMStack》

《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)上》

《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)中》

《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)下》

《一文带你搞懂 babel-plugin-import 插件(上)「源码解析」》

《一文带你搞懂 babel-plugin-import 插件(下)「源码解析」》

《JavaScript常用API合集汇总「值得收藏」》

《推荐10个常用的图片处理小帮手(上)「值得收藏」》

《推荐10个常用的图片处理小帮手(下)「值得收藏」》

《JavaScript 中ES6代理的实际用例》

《12 个实用的前端开发技巧总结》

《一文带你搞懂搭建企业级的 npm 私有仓库》

《教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?》

《细说前端开发UI公共组件的新认识「实践」》

《细说DOM API中append和appendChild的三个不同点》

《细品淘系大佬讲前端新人如何上王者「干货」》

《一文带你彻底解决背景跟随弹窗滚动问题「干货」》

《推荐常用的5款代码比较工具「值得收藏」》

《Node.js实现将文字与图片合成技巧》

《爱奇艺云剪辑Web端的技术实现》

《我再也不敢说我会写前端 Button组件「实践」》

《NodeX Component - 滴滴集团 Node.js 生态组件体系「实践」》

《Node Buffers 完整指南》

《推荐18个webpack精美插件「干货」》

《前端开发需要了解常用7种JavaScript设计模式》

《浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务》

《了不起的 Webpack HMR 学习指南(上)「含源码讲解」》

《了不起的 Webpack HMR 学习指南(下)「含源码讲解」》

《10个打开了我新世界大门的 WebAPI(上)「实践」》

《10个打开了我新世界大门的 WebAPI(中)「实践」》

《10个打开了我新世界大门的 WebAPI(下)「实践」》

《「图文」ESLint 在中大型团队的应用实践》

《Deno是代码的浏览器,你认同吗?》

《前端存储除了 localStorage 还有啥?》

《Javascript 多线程编程的前世今生》

《微前端方案 qiankun(实践及总结)》

《「图文」V8 垃圾回收原来这么简单?》

《Webpack 5模块联邦引发微前端的革命?》

《基于 Web 端的人脸识别身份验证「实践」》

《「前端进阶」高性能渲染十万条数据(时间分片)》

《「前端进阶」高性能渲染十万条数据(虚拟列表)》

《图解 Promise 实现原理(一):基础实现》

《图解 Promise 实现原理(二):Promise 链式调用》

《图解 Promise 实现原理(三):Promise 原型方法实现》

《图解 Promise 实现原理(四):Promise 静态方法实现》

《实践教你从零构建前端 Lint 工作流「干货」》

《高性能多级多选级联组件开发「JS篇」》

《深入浅出讲解Node.js CLI 工具最佳实战》

《延迟加载图像以提高Web网站性能的五种方法「实践」》

《比较 JavaScript 对象的四种方式「实践」》

《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》

《前端如何一次性处理10万条数据「进阶篇」》

《推荐三款正则可视化工具「JS篇」》

《如何让用户选择是否离开当前页面?「JS篇」》

《JavaScript开发人员更喜欢Deno的五大原因》

《仅用18行JavaScript实现一个倒数计时器》

《图文细说JavaScript 的运行机制》

《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》

《推荐Web程序员常用的15个源代码编辑器》

《10个实用的JS技巧「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你内存泄漏如何防范》

《手把手教你7个有趣的JavaScript 项目-上「附源码」》

《手把手教你7个有趣的JavaScript 项目-下「附源码」》

《JavaScript 使用 mediaDevices API 访问摄像头自拍》

《手把手教你前端代码如何做错误上报「JS篇」》

《一文让你彻底搞懂移动前端和Web 前端区别在哪里》

《63个JavaScript 正则大礼包「值得收藏」》

《提高你的 JavaScript 技能10 个问答题》

《JavaScript图表库的5个首选》

《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》

《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》

《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》

《可视化的 js:动态图演示 Promises & Async/Await 的过程》

《原生JS封装拖动验证滑块你会吗?「实践」》

《如何实现高性能的在线 PDF 预览》

《细说使用字体库加密数据-仿58同城》

《Node.js要完了吗?》

《Pug 3.0.0正式发布,不再支持 Node.js 6/8》

《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》

《JavaScript 20 年 中文版之创立标准》

《值得收藏的前端常用60余种工具方法「JS篇」》

《箭头函数和常规函数之间的 5 个区别》

《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node + H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传下载那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成下载》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传下载那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript+Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

你可能感兴趣的:(ext,get,id,js)