背景
我们知道 Egg 内置了 Controller/Service
等目录规范,会自动挂载到对应的 ctx 上。
而在实际业务开发中,往往需要为我们的团队定制一些新规范,如何实现呢?
Egg 的定位是框架的框架,因此这类的能力是天然具备的,开发者只需要简单配置下即可。
下面我们来一起定制以下规范:
demo
└── app
├── enum
| ├── error.js
| └── status.js
├── utils
| └── formatter.js
└── rpc
└── user.js
app.enum
全局静态枚举,约定:app/enum/**
的文件都被挂载到 app.enum
只需要一个简单的配置:
// config/config.default.js
exports.customLoader = {
enum: {
directory: 'app/enum',
},
};
然后编写 app/enum/error.js
来提供对应的定义:
// app/enum/error.js
exports.ERR_AUTH = {
code: '403',
msg: 'not perm',
};
exports.ERR_NOTFOUND = {
code: '404',
msg: 'not found',
};
就可以直接在业务代码中使用了:
// app/controller/home.js
class HomeController extends Controller {
async index() {
console.log(this.app.enum.error.ERR_AUTH);
}
}
app.utils
经常有同学反馈,内置的 Helper
太简单的,只支持单文件。实际上 Helper 的定位是给模板渲染用的,如果大家有一些公共方法,可以自定义挂载到 app.utils
上。
配置类似上面的方式,此处演示下如何获取 app
对象:
// config/config.default.js
exports.customLoader = {
utils: {
directory: 'app/utils',
inject: 'app',
},
};
定义:
// app/utils/formatter.js
module.exports = class Formatter {
constructor(app) {
this.app = app;
this.config = app.config;
this.logger = app.logger;
}
// 会被挂载为 `app.utils.formatter.random()`
random(max) {
return Math.floor(Math.random() * Math.floor(max));
}
};
当然,你也可以简化为:
module.exports = app => {
return {
random() {},
};
};
ctx.rpc
内置的Service
一般用来作为业务逻辑层,但有些时候,我们也需要独立出一层,如把跟后端相关的逻辑,都封装为 rpc
的概念。
app/rpc/**
的文件都被挂载到 ctx.rpc
一样只需要简单配置:
// config/config.default.js
exports.customLoader = {
rpc: {
directory: 'app/rpc',
inject: 'ctx',
},
// ...
};
然后编写 app/rpc/test.js
来提供对应的定义:
// app/rpc/test.js
module.exports = class TestRpc {
constructor(ctx) {
this.ctx = ctx;
this.config = ctx.app.config;
this.logger = ctx.logger;
}
async sayHi(name) {
console.log(this.ctx.app.config.env);
return `hi, ${name}`;
}
};
就可以直接在业务代码中使用了:
// app/controller/home.js
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = await ctx.rpc.test.sayHi('egg');
}
}
生成声明
由于 Egg 是动态挂载的,如需 TS 和智能提示支持,需要通过我们的 ets 来自动生成映射。
简单配置下 package.json
,重启即可自动生成对应的 typings。
{
"name: "egg-showcase",
"egg": {
"declarations": true,
"tsHelper": {
"watchDirs": {
"enum": {
"enabled": true,
"directory": "app/enum",
"declareTo": "Application.enum"
},
"utils": {
"enabled": true,
"directory": "app/utils",
"declareTo": "Application.utils"
},
"rpc": {
"enabled": true,
"directory": "app/rpc",
"declareTo": "Context.rpc"
}
}
}
},
}
享受智能提示吧:
规范化 && 示例
上面的演示,都是在应用里面配置,但往往我们需要的是制定团队规范,因此推荐把上述配置封装到插件,或者上层框架里,这样可以统一管控和升级。
除此之外,如果我们期望想 Egg 内置规范那样,希望在插件里面定义的 rpc 文件也能被自动加载,怎么办呢?
很简单,只需要多配置下 loadunit: true
即可。
// config/config.default.js
exports.customLoader = {
rpc: {
directory: 'app/rpc',
inject: 'ctx',
loadunit: true,
},
// ...
};
最后,别忘了写单测哈。
完整的示例代码,可以参见:https://github.com/atian25/egg-showcase/pull/13/files
内部原理
上述的配置,其实就是调用了 Egg 内置的 app.loader
API 即可,等价于:
// some_plugin/app.js
module.exports = class AppLifeCycle {
constructor(app) {
this.app = app;
}
configDidLoad() {
// 所有的配置已经加载完毕,可以用来加载应用自定义的文件,初始化自定义的服务
// 只加载应用目录
const enumPaths = path.join(this.app.config.baseDir, 'app/enum');
this.app.loader.loadToApp(enumPaths, 'enum');
// 加载所有的 loadunit
const rpcPaths = this.app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/rpc'));
this.app.loader.loadToContext(rpcPaths, 'rpc');
}
};
Egg 进一步封装了这个能力,使得开发者只需要简单的一个配置即可加载。
有兴趣的也可以看下 egg-core
这个库的源码。
写在最后
从本文中,读者可以学习到如何在 Egg 上定制适合自己团队的目录规范。这也是 Egg 设计理念很重要的一点,它本身的定位就是框架的框架。我们内部的很多插件,都是通过这个方式来实现标准化的。
不过,在 2020 这个时间点,框架其实只是整个研发解决方案里面的一个单点,我们还需要打通上下游的 PaaS、云服务、部署平台等等深水区的事,如果你对此感兴趣的话,欢迎加入我们。
我们隶属于玉伯的蚂蚁体验技术部。
目前正致力于 为蚂蚁提供 轻研发、免运维 的下一代 Node.js 研发方案。
目前团队活多人少有挑战,撸起袖子拼命干,Base 地可以选:广州、杭州、上海。
- 这些年的体验技术部(六) · Node.js 基础服务 - 摸爬滚打才不负功名尘土
- 招聘 JD 和当前我们要做的事