Elpis-Core 内核详解
Elpis-Core 本质上是一个极简的、基于 Koa 的应用框架内核,核心手法借鉴了 Egg.js:
- 约定优于配置:
app/下的目录名约定等于能力名(controller/service/middleware/extend/router/router-schema)。 - Loader 机制:每类目录有一个专用 loader,负责把文件按约定的路径和命名规则挂载到
app实例(Koa 实例)上。 - 统一入口
app:所有业务代码都通过app访问其他能力(app.controllers.xxx、app.services.xxx、app.config、app.logger等),解耦模块之间的直接require。 - 环境驱动:由
__ENV环境变量驱动local / beta / production三套配置,由npm run dev|beta|prod选择。
Elpis-core主要启动流程
elpisCore.start(options) 是唯一的入口,执行顺序严格有序,这个顺序决定了各能力间的依赖可行性:
new Koa()
├─ app.options = options // 项目配置(name、homePage 等)
├─ app.baseDir = process.cwd() // 基础路径
├─ app.businessPath = <baseDir>/app // 业务目录
├─ app.env = env() // 环境判断工具
│
├─ middlewareLoader(app) // 1. 加载 app/middleware/**.js → app.middlewares
├─ routerSchemaLoader(app) // 2. 加载 app/router-schema/**.js → app.routerSchema
├─ controllerLoader(app) // 3. 加载 app/controller/**.js → app.controllers
├─ serviceLoader(app) // 4. 加载 app/service/**.js → app.services
├─ configLoader(app) // 5. 加载 config/**.js → app.config
├─ extendLoader(app) // 6. 加载 app/extend/**.js → app.<name>
├─ require(app/middleware.js)(app) // 7. 执行「全局中间件装配」
└─ routerLoader(app) // 8. 加载路由,最后 app.use(router.routes())
主要架构如下:
不同Loader的具体意义
1. middlewareLoader
作用
在middlewareLoader中内置三个主要的中间件,全局异常捕获、API 签名校验和参数校验
### 1. 全局异常捕获中间件 errorHandler
- 全局拦截所有业务、接口、模板渲染抛出的错误;
- 统一错误响应格式:接口返回标准化错误 JSON;
- 特殊页面异常兼容:捕获
template not found,自动 302 重定向至首页,避免裸 404 错误暴露; - 屏蔽底层报错堆栈外泄,提升生产环境安全性。
2. API 签名校验中间件 apiSignVerify
- 面向
/api接口做接口鉴权; - 基于时间戳 + MD5 加密签名算法;
- 限制请求时效(10 分钟过期),防止签名盗用、重放攻击;
- 非法请求直接拦截,不进入 Controller 业务逻辑,减轻服务压力。
3. 参数校验中间件 apiParamsVerify
- 联动
routerSchemaLoader+ AJV,读取全局app.routerSchema; - 自动校验
query / body / headers入参:必填项、字段类型、参数规则; - 参数非法直接拦截并返回明确错误信息;
- 解耦校验与业务代码,Controller 只专注业务,不用手写大量
if判断。
2. routerSchemaLoader
专门负责:自动加载项目中所有 API 接口的参数校验规则(JSON Schema),合并成一张全局校验表。 它本身不做校验,只负责把所有校验规则收集起来,交给参数校验中间件使用。 示例:
module.exports = {
'/api/project/list': {
get:{
query:{
type: 'object',
properties:{
proj_key:{
type:'string'
}
},
required:['proj_key'],
},
}
}
}
3. controller.js 与 service.js
两者几乎对称:
controller文件导出(app) => class xxxController { ... };loadernew之后挂到app.controllers上。service文件导出(app) => class xxxService { ... };同上挂到app.services。app/controller/base.js与app/service/base.js提供基类,业务类通过extends baseController/baseService复用:baseController提供success(ctx, data)/fail(ctx, message, code),统一 API 返回结构。baseService挂载this.app、this.config、this.curl = superagent。
app/controller/project.js 就是典型模板:
module.exports = (app) => {
const baseController = require('./base')(app);
return class projectController extends baseController{
async getList(ctx){
const { proj_key: projKey } = ctx.request.query;
const { project: projectService } = app.services;
const projectList = await projectService.getList();
this.success(ctx,projectList);
}
}
}
4,config.js
- 扫描
config/根目录(注意不在app/下,属于项目根):config.default.js+ 对应环境config.local|beta|prod.js; - 合并策略:
Object.assign({}, defaultConfig, envConfig),环境配置覆盖默认配置;
5.extend.js
- 扫描
app/extend/**.js; - 每个扩展文件导出
(app) => <anyValue>,返回值直接挂到app[name](扁平命名空间); - 加载前会过滤已有的
app.key,避免覆盖 Koa 自带属性; - 典型例子:
app/extend/logger.js提供app.logger(本地环境直接用console,其他环境用log4js落盘)。
module.exports = (app) => {
let logger;
if(app.env.isLocal()){
logger = console;
}else{
6.router.js
- 使用
koa-router; - 扫描
app/router/**/**.js,每个文件导出(app, router) => { router.get(...) }; - 所有路由注册完后追加一条兜底:
router.get('*', ctx => ctx.redirect(app.options.homePage)),用 302 做未命中的临时重定向。
运行时关键机制
elpis-core/env.js 通过 process.env.__ENV 判定环境:
module.exports = (app) =>{
return {
isLocal() { return process.env.__ENV === 'local'; },
isBeta() { return process.env.__ENV === 'beta'; },
isProduction() { return process.env.__ENV === 'production'; },
get() { return process.env.__ENV ?? 'local'; }
}
}
SSR实现
1. SSR 核心定义
SSR:服务端渲染(Server Side Render)区别于前端纯客户端渲染(浏览器拼接 DOM、前端 AJAX 拿数据);Elpis-Core 实现的 SSR 是:
在 Koa 服务端 完成「模板渲染 + 数据注入 + 拼接完整 HTML」,直接把最终 HTML 字符串返回给浏览器。浏览器只负责解析展示页面,不负责模板拼接与数据请求。
2. 框架依赖 & 核心依赖
- 模板引擎:
koa-nunjucks-2 - 静态资源托管:
koa-static - 渲染挂载:全局中间件统一注入
- 模板文件:约定后缀
.tpl - 视图控制器:
app/controller/view.js统一接管页面路由
3. 框架内 SSR 完整实现原理
1. 中间件层前置挂载能力
在 app/middleware.js 全局中间件阶段,提前注册模板引擎:
-
自动给每一次请求的 ctx 上下文 挂载
ctx.render()方法 -
限定模板文件后缀为
.tpl -
模板根目录统一约束在
app/public
app.use(koaNunjucks({ ext:'tpl', path: 项目/app/public }));
2. 目录约定(强约束)
静态资源 + 模板文件共用一套 public 目录,简化部署与路径管理。
3. 视图控制器统一收口
- 接收路由动态参数(如
/view/:page) - 组装全局公共数据(项目名称、环境变量、配置、首页地址)
- 调用
ctx.render(模板路径, 页面数据)服务端渲染 - 直接返回完整 HTML
4.渲染底层流程
- 读取
public/output/xxx.tpl模板文件 - 将后端传入的 JSON 数据,注入模板变量
- nunjucks 引擎在服务端完成语法解析、循环、条件判断、变量替换
- 拼接生成完整 HTML 文本
- 响应头返回
text/html,浏览器直接渲染页面
个人思考:如果需要请求接口数据渲染在界面中,有什么方案?
- 数据请求统一放在服务端生命周期不写在页面组件
mounted/onMounted,而是强制在服务端阶段提前获取。 - 数据和页面渲染串行隔离先请求、再渲染,保证 HTML 一次性直出。
- 统一状态注入服务端拿到的数据,序列化注入 HTML 全局变量,前端水合时直接复用,避免二次请求。
- 水合后,可以通过全局管理获取数据,渲染页面。
<script> window.__INITIAL_DATA__ = {"list": [...]} </script> //例子
统一异常降级接口超时 / 失败 → 降级兜底页面可以走csr重新获取数据。
总结
- 内核即 loader 集合:Elpis-Core价值在于一整套「按约定把文件塞进
app」的规则,业务代码因此可以保持纯粹。 app是一等公民:所有跨模块访问都经过app,这既是解耦,也是测试/替换能力(如app.logger可以本地console、线上log4js)。- 中间件顺序就是框架语义:
app/middleware.js里use的顺序决定了 静态 → 渲染 → 解析 → 异常 → 鉴权 → 校验 → 路由 的链路;改这个文件就等于改框架行为。 - 具体不足:
apiParamsVerify挂在路由之前,所以ctx.params暂不可校验,routerSchema的 key 也不支持:param,const { params, path, method } = ctx语法上能取到params这个字段,但当前顺序下,它不是「下一层路由会填好的那份」 path params;要配:param的 schema,就要改执行顺序或挂载点。 - 整体实现方向:配置系统 → SSR + 静态资源 → 业务 API → 完整收敛;每一步都沿着「补齐 Web 框架必要件」推进,非常清晰的「微内核 + 渐进增强」节奏。