elpis-core的个人理解

0 阅读6分钟

Elpis-Core 内核详解

Elpis-Core 本质上是一个极简的、基于 Koa 的应用框架内核,核心手法借鉴了 Egg.js:

  1. 约定优于配置app/ 下的目录名约定等于能力名(controller / service / middleware / extend / router / router-schema)。
  2. Loader 机制:每类目录有一个专用 loader,负责把文件按约定的路径和命名规则挂载到 app 实例(Koa 实例)上。
  3. 统一入口 app:所有业务代码都通过 app 访问其他能力(app.controllers.xxxapp.services.xxxapp.configapp.logger 等),解耦模块之间的直接 require
  4. 环境驱动:由 __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())

主要架构如下:

image.png

不同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 { ... };loader new 之后挂到 app.controllers 上。
  • service 文件导出 (app) => class xxxService { ... };同上挂到 app.services
  • app/controller/base.jsapp/service/base.js 提供基类,业务类通过 extends baseController/baseService 复用:
    • baseController 提供 success(ctx, data) / fail(ctx, message, code),统一 API 返回结构。
    • baseService 挂载 this.appthis.configthis.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重新获取数据。

总结

  1. 内核即 loader 集合:Elpis-Core价值在于一整套「按约定把文件塞进 app」的规则,业务代码因此可以保持纯粹。
  2. app 是一等公民:所有跨模块访问都经过 app,这既是解耦,也是测试/替换能力(如 app.logger 可以本地 console、线上 log4js)。
  3. 中间件顺序就是框架语义app/middleware.js 里 use 的顺序决定了 静态 → 渲染 → 解析 → 异常 → 鉴权 → 校验 → 路由 的链路;改这个文件就等于改框架行为。
  4. 具体不足apiParamsVerify 挂在路由之前,所以 ctx.params 暂不可校验,routerSchema 的 key 也不支持 :param,const { params, path, method } = ctx 语法上能取到 params 这个字段,但当前顺序下,它不是「下一层路由会填好的那份」 path params;要配 :param 的 schema,就要改执行顺序或挂载点。
  5. 整体实现方向:配置系统 → SSR + 静态资源 → 业务 API → 完整收敛;每一步都沿着「补齐 Web 框架必要件」推进,非常清晰的「微内核 + 渐进增强」节奏。