面向 Node.js 微服务的 Application Runtime:同一套引擎覆盖 HTTP、CLI、定时任务,并提供 DI、Provider、中间件、配置、缓存、鉴权辅助、实体(Sequelize)、异常体系与 Swagger 生成。
消费方(人与 agent): 本 README 即为完整对外说明。使用本包不需要阅读仓库内的
docs/。
- Node.js ≥ 24
- ESM(
"type": "module") - npm(或其它可从 npm registry 安装的客户端)
npm install evaengine本包源码为 TypeScript,npm 发布 dist/ 编译产物并自带 .d.ts 类型声明(@types/express、@types/lodash 随 dependencies 自动安装,严格模式下类型开箱即用);上述导入契约不受影响。
可选脚手架:EvaSkeleton.js。
包的 default 导出 是一个 core 对象。顶层具名导出只有 default 与 core(同一对象)。
import eva from 'evaengine';
// 或:import { core as eva } from 'evaengine';
const {
EvaEngine,
Command,
DI,
Entities,
express,
wrapper,
services,
middlewares,
providers,
exceptions,
swagger,
utils,
commands, // 内置 CLI 命令
Joi,
sequelize
} = eva;不要依赖 import { EvaEngine } from 'evaengine'——该具名导出不存在。
new EvaEngine(meta, mode?)
→ 绑定 base 服务(env, config, logger, namespace, now, event_manager)
→ bootstrap() # web 服务 + 中间件 Provider
→ use(...) / registerCommands
→ run() | runHttps() | runCLI() | runCrontab() | runCommand()
| 模式 | 典型流程 |
|---|---|
web(默认) |
bootstrap() → use() → run() / runHttps() |
cli |
registerCommands() → runCLI() / runCrontab() / runCommand() |
进程级事实(按每进程一个 Engine 规划):
DI是全局容器。EvaEngine.getApp()是模块级 Express app 单例。bootstrap()注册 web 服务与中间件 Provider;CLI 路径在getCLI/runCrontab内注册 CLI 服务。- 内置
EventManager仅进程内(不是消息队列)。
import eva from 'evaengine';
const { EvaEngine, DI, wrapper, exceptions } = eva;
const { UnauthorizedException } = exceptions;
const engine = new EvaEngine({
projectRoot: process.cwd(),
port: Number(process.env.PORT) || 3000
// configPath、sourceRoot 可选
});
engine.bootstrap();
// 可选横切中间件(须在 bootstrap 之后)
engine.use(DI.get('trace')());
// engine.use(DI.get('session')());
// engine.use(DI.get('auth')());
engine.use('/health', (req, res) => {
res.json({ ok: true });
});
engine.use('/me', wrapper(async (req, res) => {
if (!req.auth?.uid) {
throw new UnauthorizedException('Login required');
}
res.json({ uid: req.auth.uid });
}));
engine.run();import eva from 'evaengine';
import * as UserCommands from './commands/user.js';
const { EvaEngine } = eva;
const engine = new EvaEngine({ projectRoot: process.cwd() }, 'cli');
engine.registerCommands(UserCommands);
await engine.runCLI();
// node app.js user:create --name=Ada命令不存在或 run() 抛出异常时,进程以退出码 1 结束。
import eva from 'evaengine';
import * as Jobs from './commands/jobs.js';
const { EvaEngine } = eva;
const engine = new EvaEngine({ projectRoot: process.cwd() }, 'cli');
engine.registerCommands([Jobs]);
// 六段 cron(含秒)等细节见 runCrontab 第三参数 useSeconds
engine.runCrontab('0/10 * * * * *', 'hello:world --id=EvaEngine');npx engine
npx engine make:entity
npx engine make:dbview
npx engine make:graphql
npx engine tramp:dump-configproject/
package.json # "type": "module"
config/
config.default.cjs
config.development.cjs
config.production.cjs
config.local.development.cjs # 本地覆盖,建议 gitignore
src/
app.js # web 入口
cli.js
commands/
entities/
routes/
test/
配置目录为 {projectRoot}/config(可用构造参数 configPath 覆盖),按以下顺序合并:
- 引擎内置默认(随包提供)
config.default.cjsconfig.<NODE_ENV>.cjs- 可选
config.local.<NODE_ENV>.cjs(不存在则忽略)
配置文件使用 CommonJS .cjs(经 require 加载)。
// config/config.default.cjs
module.exports = {
app: { name: 'my-service' },
redis: { host: '127.0.0.1', port: 6379, lazyConnect: true },
cache: { prefix: 'myapp', driver: 'redis' },
token: {
secret: process.env.TOKEN_SECRET || '',
provider: undefined, // 设为 'kong' 时使用 Kong JWT 与对应 auth 中间件
faker: { enable: false, key: 'eva', uid: 1 }
},
session: {
secret: process.env.SESSION_SECRET || 'change-me',
resave: true,
saveUninitialized: true,
cookie: { path: '/', httpOnly: true, secure: false, maxAge: 3600_000 }
},
db: {
dialect: 'mysql',
port: 3306,
database: '',
replication: {
write: { host: '', username: '', password: '', pool: {} },
read: []
}
}
};运行时读取:
const config = DI.get('config');
config.get('redis.host');
config.get(); // 完整对象redis 配置项原样透传给 ioredis(当前为 v6:默认以 RESP3 握手,服务端不支持时自动回落 RESP2;若经手的代理不支持 HELLO,可传 protocol: 2)。
| 变量 | 作用 |
|---|---|
NODE_ENV |
选择 config.<env>.cjs |
PORT |
常见应用端口(使用时传入构造参数) |
LOG_LEVEL |
覆盖日志级别 |
TZ |
moment 默认时区(未设置时为 Asia/Shanghai) |
CLI_NAME |
CLI 模式下 logger 标签 |
MAX_REQUEST_DEBUG_BODY |
debug 中间件 body 限制 |
SEQUELIZE_REPLICATION_CONFIG_KEY |
db 下 replication 配置的替代键名 |
DI.get('logger').info('hello');
DI.get('redis').getInstance();
DI.get('cache'); // 缓存门面
DI.get('jwt');
DI.get('http_client');
DI.get('rest_client');
DI.get('event_manager');
DI.get('namespace');
DI.get('now');
DI.get('env');
DI.get('validator_base');| DI 名 | 绑定时机 |
|---|---|
env、config、logger、namespace、now、event_manager |
构造时(base) |
redis、cache、http_client、rest_client、validator_base、jwt |
bootstrap()(web)或 CLI 执行路径 |
| 下文中间件名 | bootstrap() |
自定义 Provider:
import eva from 'evaengine';
const { DI, providers } = eva;
const { ServiceProvider } = providers.services;
class MyApiProvider extends ServiceProvider {
get name() { return 'my_api'; }
register() {
DI.bindValue(this.name, { ping: () => 'pong' });
}
}
engine.registerService(MyApiProvider);
// 或替换整表:
// EvaEngine.setServiceProvidersForWeb([...EvaEngine.getServiceProvidersForWeb(), MyApiProvider]);测试辅助:DI.reset()、DI.registerMockedProviders(providers, configPath)、DI.bindClass / bindValue / bindMethod。
DI.bindClass(name, Class, dependencies) resolves the explicit dependency list
instead of Class.dependencies. Named aliases for the same class share one
singleton, including an instance already resolved by DI.get(Class).
With namespaces enabled, nested namespace.run() scopes inherit the parent
values and keep child writes local. namespace.use().bind(callback) preserves
the captured context, callback arguments, return value, and caller's this.
Trace response listeners retain their own request span even when an event is
emitted outside that request's async context.
bootstrap() 之后按名称绑定工厂。需要调用工厂(注意部分场景二次调用):
engine.use(DI.get('trace')());
engine.use(DI.get('session')());
engine.use(DI.get('auth')());
// validator 是高阶工厂:
engine.use('/items', DI.get('validator')(() => ({
query: eva.Joi.object({ page: eva.Joi.number().integer().required() })
})), handler);| 名称 | 作用 |
|---|---|
session |
express-session(经 connect-redis 的 Redis 存储) |
auth |
从 X-Token 或 api_key 取 JWT,或 session uid;可选 faker token |
trace |
请求追踪(与 namespace 协作) |
validator |
Joi 请求校验 |
view_cache |
响应缓存辅助 |
debug |
调试输出 |
当 config.token.provider === 'kong' 时,jwt 服务与 auth 中间件均切换为 Kong 实现。
使用 wrapper(async (req,res) => …),以便抛出的 exceptions.* 进入默认错误处理器。
import eva from 'evaengine';
const { Command, DI } = eva;
export class HelloWorld extends Command {
static getName() { return 'hello:world'; }
static getDescription() { return 'Say hello'; }
static getSpec() {
return {
id: { type: 'string', description: 'Who to greet' }
};
}
async run() {
const { id = 'world' } = this.getOptions();
DI.get('logger').info(`Hello ${id}`);
}
}通过 engine.registerCommands(moduleExports) 或模块数组注册。名称来自 getName()。
Engine API:runCLI()、runCommand('name --flag=1')、runCrontab(expression, 'name --flag=1', useSeconds?)、clearCommands()、clearCrontabs()。
import path from 'path';
import eva from 'evaengine';
const { Entities, DI } = eva;
const entities = new Entities(path.join(process.cwd(), 'src/entities'));
entities.init(); // 按 config.db 构建 Sequelize 并扫描目录
const User = entities.get('user');
const all = entities.getAll();
const transaction = await entities.getTransaction();
try {
await User.create({ name: 'Ada' }, { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}实体文件(经 require 加载的 CJS 或 ESM 工厂):
// src/entities/user.cjs
module.exports = (sequelize, DataTypes) =>
sequelize.define('user', {
id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING, allowNull: false }
}, { tableName: 'users' });import eva from 'evaengine';
const {
StandardException,
LogicException,
InvalidArgumentException,
UnauthorizedException,
ResourceNotFoundException,
RuntimeException
// …完整列表见包导出 exceptions
} = eva.exceptions;默认 HTTP 错误处理器在 run / runHttps 时挂载:将 StandardException 子类映射为 JSON 与状态码;生产环境会剥离 stack 等细节。
使用 eva.swagger(ExSwagger、注解辅助等)从源码注释与模型生成 Swagger 2.0。在应用脚本中自行接入生成流程;UI 资源来自依赖 swagger-ui-dist。
constructor({ projectRoot, configPath?, sourceRoot?, port?, config?, logger?, namespace? }, mode?='web')
getMeta() getDI()
bootstrap() use(...args) run(port?) runHttps(port?, options?) getServer()
registerCommands(commands) getCommands() clearCommands() getCommand() getCommandName()
runCLI(name?) runCommand(commandString) runCrontab(seq, commandString, useSeconds?) clearCrontabs()
registerServiceProviders(providers) registerService(ProviderClass)
setDefaultErrorHandler / getDefaultErrorHandler
setUncaughtExceptionHandler / getUncaughtExceptionHandler
setServerErrorHandler / getServerErrorHandler
static getApp() createRouter() getVersion()
static get/set BaseServiceProviders | ServiceProvidersForWeb | ServiceProvidersForCLI | MiddlewareProviders
- 不只是 Express 薄封装——HTTP 只是入口之一
- 不是业务领域框架或业务规则层
- 不是消息总线(可靠投递请用真正的 MQ)
- 不是完整 ORM 产品——仅提供 Sequelize 集成辅助
git clone https://github.com/EvaEngine/EvaEngine.js.git
cd EvaEngine.js
npm install
npm run lint
npm run build
npm test # 部分测试需要本机 Redis 127.0.0.1:6379仓库源码为严格 TypeScript(仅可擦除语法),Node ≥24 原生直接运行 src/**/*.ts,日常开发无转译步骤;npm run build 用 tsc 编译发布产物 dist/ 并生成声明文件。本地 CLI 可直接 node src/bin.ts,或先 build 后走 ./bin/engine(shim → dist/bin.js)。
发版:在 main 上由 semantic-release(Conventional Commits)执行——仅发布 npm,不创建 GitHub Release。维护者文档在 git 的 docs/ 下,npm 消费方无需阅读。