在低代码平台开发过程,由于物料管理系统以及Node Server 中间层的搭建使用过程中,发现很多通用的库和配置可以…

一、背景

在低代码平台开发过程,由于物料管理系统以及Node Server 中间层的搭建使用过程中,发现很多通用的库和配置可以沉淀,形成可复制的解决方案。所以,基于开源框架Egg.js(基于Koa2)进行了拓展封装形成了初版Apr。

整个团队依赖初版Apr作为统一的Node方案,随着业务场景越来越丰富,一些插件、middle等公共配置成为最佳实践后,逐渐下沉到Apr框架中,让Apr越来越成熟。而后,依赖Apr的其他项目仅需简单的升级下框架的版本即可享受到Apr带来的红利。

如果你的团队也在考虑搭建类似的框架,那么请考虑如下问题:

  • 如果你的团队遇到过:
    • 维护很多个项目,每个项目都需要复制拷贝诸如 gulpfile.js / webpack.config.js 之类的文件
    • 每个项目都需要使用一些相同的类库,相同的配置
    • 在新项目中对上面的配置做了一个优化后,如何同步到其他项目?
  • 如果你的团队需要:
    • 统一的技术选型,比如数据库、模板、前端框架及各种中间件设施都需要选型,而框架封装后保证应用使用一套架构
    • 统一的默认配置,开源社区的配置可能不适用于公司,而又不希望应用去配置
    • 统一的部署方案,通过框架和平台的双向控制,应用只需要关注自己的代码,具体查看应用部署
    • 统一的代码风格,框架不仅仅解决代码重用问题,还可以对应用做一定约束,作为企业框架是很必要的。Apr依赖于 Egg, Egg 在 Koa 基础上做了很多约定,框架可以使用 Loader 自己定义代码规则

如果需要,以下是Apr的简易版搭建过程,也许对你有所帮助:

稳定成熟版涉及到公司相关业务,故仅存内网中,不对外暴露,请勿私聊获取

二、框架与多进程

框架的扩展是和多进程模型有关的,我们已经知道多进程模型,我们需要扩展的类有 Agent 和 Application。

在 Agent Worker 启动的时候会实例化 Agent,而在 App Worker 启动时会实例化 Application,这两个类又同时继承 EggCore。

EggCore 可以看做 Koa Application 的升级版,默认内置 Loader、Router 及应用异步启动等功能,可以看做是支持 Loader 的 Koa。 而我们要搭建的Apr则依赖于EggCore。

      Koa Application
             ^
          EggCore

             ^
          AprCore
             ^
      ┌──────┴───────┐
      │              │
  Apr Agent      Apr Application
     ^               ^
agent worker     app worker

三、定制框架: Apr

1. create Apr & init

$ mkdir apr && cd apr
$ npm init egg --type=framework
$ npm i
$ npm test

2. 框架继承

框架支持继承关系,可以把框架比作一个类,基类是 Egg 框架,定义一个框架需要继承于Egg且实现 Egg 所有的 API。

// package.json
{
  "name": "apr",
  "dependencies": {
    "egg": "^2.0.0"
  }
}

// index.js
module.exports = require('./lib/framework.js');

// 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]() {
    // 返回 framework 路径
    return path.dirname(__dirname);
  }
}

// 覆盖了 Egg 的 Application
module.exports = Object.assign(egg, {
  Application,
});

应用启动时需要指定框架名(在 package.json 指定 egg.framework,默认为 egg),Loader 将从 node_modules 找指定模块作为框架,并加载其 export 的 Application

{
  "scripts": {
    "dev": "egg-bin dev"
  },
  "egg": {
    "framework": "apr"
  }
}

现在 apr 框架目录已经是一个 loadUnit,那么相应目录和文件(如 app 和 config)都会被加载

3. 自定义 Agent

上面的例子自定义了 Application,因为 Egg 是多进程模型,所以还需要定义 Agent,原理是一样的

// 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]() {
    // 返回 framework 路径
    return path.dirname(__dirname);
  }
}

class Agent extends egg.Agent {
  get [EGG_PATH]() {
    return path.dirname(__dirname);
  }
}

// 覆盖了 Egg 的 Application
module.exports = Object.assign(egg, {
  Application,
  Agent,
});

但因为 Agent 和 Application 是两个实例,所以 API 有可能不一致

4. 自定义 Loader

Loader 应用启动的核心,使用它还能规范应用代码,我们可以基于这个类扩展更多功能,比如加载数据代码。

扩展 Loader 还能覆盖默认的实现,或调整现有的加载顺序等

自定义 Loader 也是用 Symbol.for(’egg#loader’) 的方式,主要的原因还是使用原型链

// lib/framework.js
const path = require('path');
const egg = require('egg');
const EGG_PATH = Symbol.for('egg#eggPath');

class AppWorkerLoader extends egg.AppWorkerLoader {
  load() {
    super.load();
    // 自己扩展
  }
}

class Application extends egg.Application {
  get [EGG_PATH]() {
    // 返回 framework 路径
    return path.dirname(__dirname);
  }
  // 覆盖 Egg 的 Loader,启动时使用这个 Loader
  get [EGG_LOADER]() {
    return AppWorkerLoader;
  }
}

// 覆盖了 Egg 的 Application
module.exports = Object.assign(egg, {
  Application,
  // 自定义的 Loader 也需要 export,上层框架需要基于这个扩展
  AppWorkerLoader: AppWorkerLoader,
});

AgentWorkerLoader 扩展也类似,这里不再举例

AgentWorkerLoader 加载的文件可以于 AppWorkerLoader 不同,比如:默认加载时,Egg 的 AppWorkerLoader 会加载 app.js 而 AgentWorkerLoader 加载的是 agent.js。

5. 拓展其他功能

你还可以自己去丰富plugins、middle等各种各样的功能特效,让你的Apr功能变得变得越来越强大,此处以plugins为例。

  • 内置 nunjucks 来提供服务端模板渲染能力
  • 封装 egg-mysql
// Apr 框架配置
// package.json
{
  "name": "apr",
  "version": "1.0.0",
  "dependencies": {
    "egg-mysql": "^3.0.0",
    "egg-view-nunjucks": "^2.0.0"
  }
}

// config/plugin.js
/**
 * @desc: framework 集成各种 plugins
 * @path: 'config/plugin.js'
 * */
module.exports = {
  // mysql
  mysql: {
    enable: false,
    package: 'egg-mysql',
  },
  // add you build-in plugin here, example:
  nunjucks: {
    enable: false,
    package: 'egg-view-nunjucks',
  }
}

使用Apr的应用配置:

// 应用配置
// package.json
{
  "dependencies": {
    "apr": "^1.0.0",
  }
}

// config/plugin.js
module.exports = {
  // 开启插件
  mysql: true,
  nunjucks: true,
}

在框架的基础上还可以扩展出新的框架,也就是说框架是可以无限级继承的,有点像类的继承

+-----------------------------------+--------+
|      app1, app2, app3, app4       |        |
+-----+--------------+--------------+        |
|     |              |  framework2  |        |
+     |       Apr    +--------------+ plugin |
|     |              |  framework1  |        |
+     +--------------+--------------+        |
|                   Egg             |        |
+-----------------------------------+--------|
|                   Koa                      |
+-----------------------------------+--------+

四、单元测试

待补充…

参考

五、框架启动原理

  • startCluster 启动传入 baseDir 和 framework,Master 进程启动
  • Master 先 fork Agent Worker
    • 根据 framework 找到框架目录,实例化该框架的 Agent 类
    • Agent 找到定义的 AgentWorkerLoader,开始进行加载
    • AgentWorkerLoader,开始进行加载 整个加载过程是同步的,按 plugin > config > extend > agent.js > 其他文件顺序加载
    • agent.js 可自定义初始化,支持异步启动,如果定义了 beforeStart 会等待执行完成之后通知 Master 启动完成。
  • Master 得到 Agent Worker 启动成功的消息,使用 cluster fork App Worker
    • App Worker 有多个进程,所以这几个进程是并行启动的,但执行逻辑是一致的
    • 单个 App Worker 和 Agent 类似,通过 framework 找到框架目录,实例化该框架的 Application 类
    • Application 找到 AppWorkerLoader,开始进行加载,顺序也是类似的,会异步等待,完成后通知 Master 启动完成
  • Master 等待多个 App Worker 的成功消息后启动完成,能对外提供服务

六、Apr结构

Apr
├── README.md
├── app
│   ├── extend
│   │   ├── application.js
│   │   └── context.js
│   └── service
│       └── test.js
├── config
│   ├── config.default.js
│   └── plugin.js
├── index.js
├── lib
│   └── framework.js
├── package-lock.json
├── package.json
└── test
    ├── fixtures
    │   └── example
    │       ├── app
    │       │   ├── controller
    │       │   │   └── home.js
    │       │   └── router.js
    │       ├── config
    │       │   └── config.default.js
    │       └── package.json
    └── framework.test.js

引用示例

demo源码仓库,欢迎 star


最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~

微信公众号

本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!