elpis-core 第一阶段学习心得与收获

一、阶段概述

第一阶段从零搭建了一个基于 Koa 2 的企业级 Node.js 框架内核 elpis-core,在第一阶段的学习中我深刻理解了框架每一层的分层设计理念,下面将总结每一层的职责、设计意图和实现细节。


二、整体架构总览

2.1 项目目录结构

csharp 复制代码
elpis/
├── index.js                          # 应用入口:调 elpis-core 启动
├── config/                           # 多环境配置
│   ├── config.default.js             #   基础配置(所有环境共享)
│   ├── config.local.js               #   本地开发覆盖
│   ├── config.beta.js                #   测试环境覆盖
│   └── config.prod.js                #   生产环境覆盖
├── elpis-core/                       # 框架内核(与应用业务完全解耦)
│   ├── index.js                      #   启动入口:编排加载顺序
│   ├── env.js                        #   环境识别
│   └── loader/                       #   7 个自动加载器
│       ├── middleware.js
│       ├── router-schema.js
│       ├── controller.js
│       ├── service.js
│       ├── config.js
│       ├── extend.js
│       └── router.js
└── app/                              # 业务代码(按约定放入对应目录即可)
    ├── middleware.js                  #   全局中间件注册(顺序编排)
    ├── controller/                   #   控制器层
    │   ├── base.js                   #     BaseController(公共能力下沉)
    │   ├── project.js                #     ProjectController
    │   └── view.js                   #     ViewController
    ├── service/                      #   服务层
    │   ├── base.js                   #     BaseService(注入 app/config/curl)
    │   └── project.js                #     ProjectService
    ├── middleware/                   #   中间件层(横切关注点)
    │   ├── error-handler.js          #     全局错误兜底
    │   ├── api-sign-verify.js        #     API 签名校验
    │   └── api-params-verify.js      #     API 参数 JSON Schema 校验
    ├── router/                       #   路由层(URL → Controller 映射)
    │   ├── project.js                #     /api/project/list
    │   └── view.js                   #     /view/:page
    ├── router-schema/                #   API 参数规则(与路由解耦)
    │   └── project.js
    ├── extend/                       #   应用扩展(挂载到 app 上)
    │   └── logger.js                 #     log4js 日志
    └── public/                       #   静态资源 & 模板
        ├── output/                   #     Nunjucks 模板 (.tpl)
        └── static/                   #     CSS / 图片

2.2 分层架构图

bash 复制代码
                        ┌──────────────────────┐
                        │     HTTP Request      │
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │  1. koa-static        │  静态文件?
                        │  2. koa-nunjucks-2    │  模板渲染准备
                        │  3. koa-bodyparser    │  解析 body
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │  4. error-handler     │  ←── 最外层 try/catch
                        │     ┌─────────────────┤      兜底所有异常
                        │     │  await next()   │
                        │     └─────────────────┤
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │  5. api-sign-verify   │  ←── 只拦截 /api/*
                        │     MD5 签名 + 时间戳  │      防篡改 + 防重放
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │  6. api-params-verify │  ←── 只拦截 /api/*
                        │     AJV JSON Schema   │      headers/body/query/params
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │     Router 路由层      │
                        │  /api/project/list    │──→ ProjectController.getList
                        │  /view/:page          │──→ ViewController.renderPage
                        │  *                    │──→ 302 重定向到首页
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │   Controller 控制层    │
                        │   获取参数              │
                        │   调用 Service          │
                        │   组装响应 (success/fail)│
                        └──────────┬───────────┘
                                   │
                        ┌──────────▼───────────┐
                        │    Service 服务层      │
                        │   业务逻辑              │
                        │   数据查询 / 外部 API    │
                        └──────────────────────┘

三、逐层深度解析

3.1 启动入口:elpis-core/index.js ------ 框架的"总调度"

这是整个框架的生命周期管理器ElpisCore.start() 被调用后,按严格顺序执行 7 个加载步骤:

scss 复制代码
// 1. 初始化 app 实例
const app = new koa();
app.options = options;          // 应用配置
app.baseDir = process.cwd();    // 项目根目录
app.businessPath = './app';     // 业务代码目录

// 2. 环境识别
app.env = env();

// 3. 按序加载各层
middlewareLoader(app);     // ① 中间件(先加载,供后续注册使用)
routerSchemaLoader(app);   // ② 参数校验规则
controllerLoader(app);     // ③ 控制器
serviceLoader(app);        // ④ 服务
configLoader(app);         // ⑤ 配置(最后加载,因为前面各层可能依赖 config)
extendLoader(app);         // ⑥ 扩展(挂载 logger 等到 app)

// 4. 注册全局中间件链
require('./app/middleware.js')(app);

// 5. 注册路由
routerLoader(app);

// 6. 启动 HTTP 服务
app.listen(port, host);

设计要点

  • 加载顺序即依赖关系:Middleware 和 RouterSchema 必须在 Controller/Service 之前加载,因为 Controller 可能不依赖它们,但后续的路由注册和中间件链需要所有组件就位。
  • app 对象是唯一的依赖注入容器 :所有层加载的结果都挂载到 app 上(app.controllerapp.serviceapp.config 等),各层之间通过 app 互相访问。
  • 业务与框架分离elpis-core/ 是纯框架代码,app/ 是纯业务代码。框架只定义规则,业务只按约定写。

3.2 环境识别层:elpis-core/env.js

职责:单一的环境判断,对外暴露 4 个方法:

scss 复制代码
app.env = {
  isLocal(),        // _ENV === 'local'
  isBeta(),         // _ENV === 'beta'
  isProduction(),   // _ENV === 'production'
  get()             // 返回当前 _ENV,默认 'local'
}

设计意图 :将环境判断封装成语义化 API,而不是让业务代码到处写 process.env._ENV === 'xxx'。配合 cross-env 在 npm scripts 中注入环境变量:

json 复制代码
"dev":  "cross-env _ENV=local node ./index.js"
"beta": "cross-env _ENV=beta nodemon ./index.js"
"prod": "cross-env _ENV=production nodemon ./index.js"

被谁使用:Config Loader 根据环境加载不同配置文件;Logger 根据环境决定是 console 输出还是文件落盘。


3.3 自动加载器(Loader)------ 框架的核心机制

7 个 Loader 实现原理高度一致,核心是三步:

perl 复制代码
glob 扫描目录 → 驼峰转换命名 → require + 工厂函数 → 挂载到 app

3.3.1 通用加载模式

controller loader 为例:

ini 复制代码
// ① glob 扫描:递归发现 app/controller/**/*.js
const fileList = glob.sync('app/controller/**/**.js');

fileList.forEach(file => {
    // ② 路径 → 驼峰命名
    // app/controller/custom-module/custom-controller.js
    //   → customModule.customController

    // ③ require 工厂函数,new 实例,挂载到 app.controller
    const ControllerClass = require(file)(app);  // 注入 app
    app.controller.customModule.customController = new ControllerClass();
});

这带来的好处

  • 零配置路由/注入 :开发者只需要在 app/controller/ 下新建文件,框架自动发现并挂载,无需手动 require 和注册。
  • 命名即命名空间 :目录结构直接映射为 app.controller.xxx.yyy,层次清晰。
  • 工厂函数注入 app :每个模块通过 (app) => Class 接收 app 实例,实现依赖注入。

3.3.2 各 Loader 对比

Loader 扫描目录 挂载到 加载方式 用途
middleware app/middleware/ app.middlewares require(file)(app) 返回函数 供全局中间件链调用
router-schema app/router-schema/ app.routerSchema require(file) 返回对象合并 供 api-params-verify 查找
controller app/controller/ app.controller new (require(file)(app)) 供 Router 映射
service app/service/ app.service new (require(file)(app)) 供 Controller 调用
config config/ app.config 按环境合并 default + env 全局配置
extend app/extend/ app[name] 直接挂载到 app 顶层 扩展 app 能力(如 logger)
router app/router/ KoaRouter 实例 require(file)(app, router) 注册路由规则

关键细节 :extend loader 在挂载前会检测命名冲突------如果 app 上已有同名属性,打印警告并跳过,防止覆盖框架核心属性。


3.4 全局中间件链:app/middleware.js

职责 :定义请求的处理管道------这是整个应用最关键的架构决策之一。

javascript 复制代码
module.exports = (app) => {
    // ① koa-static      ------ 静态文件(CSS/图片/JS),命中则直接返回
    // ② koa-nunjucks-2   ------ 模板引擎准备(ctx.render 可用)
    // ③ koa-bodyparser    ------ 解析请求体为 ctx.request.body
    // ④ error-handler     ------ 全局异常兜底(最外层 try/catch)
    // ⑤ api-sign-verify   ------ API 签名校验(仅拦截 /api/*)
    // ⑥ api-params-verify ------ API 参数校验(仅拦截 /api/*)
};

注册顺序即执行顺序,这个顺序背后有明确的逻辑:

  1. 静态文件最先 ------如果请求的是 logo.png,直接返回,不浪费后续中间件的 CPU。
  2. 模板和 BodyParser 其次 ------为后续中间件准备好 ctx.renderctx.request.body
  3. 错误处理第四------包裹了后续所有中间件和路由,任何未捕获异常都在这里兜底。
  4. 签名和参数校验最后------只有 API 请求才触发,页面请求直接跳过。

3.4.1 error-handler:全局异常兜底

csharp 复制代码
try {
    await next();  // 包裹后续所有中间件 + 路由 + 控制器
} catch (err) {
    // 模板未找到 → 302 重定向到首页(用户体验)
    // 其他异常   → 返回 { success: false, code: 50000 }(统一错误格式)
}

设计意图

  • 区分"页面异常"和"API 异常"------页面异常重定向,API 异常返回 JSON。
  • 对外隐藏错误细节------生产环境不暴露 stack trace,统一返回"网络异常,请稍后再试"。

3.4.2 api-sign-verify:API 签名校验

csharp 复制代码
// 只拦截 /api/* 路径
if (!ctx.path.includes('/api')) return await next();

// 1. 取请求头中的签名 s_sign 和时间戳 s_t
// 2. 服务端用相同算法计算: md5(signKey + '_' + 时间戳)
// 3. 比对签名是否一致
// 4. 检查时间戳是否在 10 分钟内(防重放攻击)

设计意图:防止 API 被未授权调用或请求被篡改。这是一种轻量级的 API 安全方案,不依赖 Session/Token,适合微服务间调用或简单的前后端分离场景。

3.4.3 api-params-verify:AJV JSON Schema 参数校验

arduino 复制代码
// 1. 从 app.routerSchema 查找当前 path + method 的 JSON Schema
// 2. 依次校验 headers → body → query → params
// 3. 任一校验失败,返回 code: 442 + 详细错误信息

设计意图 :将验证规则与业务代码解耦 。Controller 里不再写 if (!proj_key) { ... },而是在 router-schema/project.js 中声明式定义:

css 复制代码
'/api/project/list': {
    get: {
        query: {
            type: 'object',
            properties: { proj_key: { type: 'string' } },
            required: ['proj_key']
        }
    }
}

这样当 API 增多时,所有校验规则集中管理,Controller 只关心业务逻辑。


3.5 路由层:app/router/

职责URL → Controller 方法 的映射,仅此而已。

javascript 复制代码
// app/router/project.js
module.exports = (app, router) => {
    const { project: projectController } = app.controller;
    router.get('/api/project/list', projectController.getList.bind(projectController));
};

// app/router/view.js
module.exports = (app, router) => {
    const { view: viewController } = app.controller;
    router.get('/view/:page', viewController.renderPage.bind(viewController));
};

设计约束

  • Router 只负责"哪个 URL 调哪个 Controller 的哪个方法",不写任何业务逻辑
  • Router loader 会自动注册一个 GET * 兜底路由 → 302 重定向到首页(防止 404 白屏)。
  • .bind(controller) 是必须的,因为 KoaRouter 调用 handler 时 this 会丢失。

3.6 控制器层:app/controller/

职责 :作为请求的"指挥者" ,协调参数获取、服务调用、响应组装。

BaseController:公共能力下沉

javascript 复制代码
class BaseController {
    constructor() {
        this.app = app;       // 可访问所有层
        this.config = app.config;  // 可读取配置
    }
    success(ctx, data, metadata) {  // 统一成功响应格式
        ctx.body = { sucess: true, data, metadata };
    }
    fail(ctx, message, code) {      // 统一失败响应格式
        ctx.body = { sucess: false, message, code };
    }
}

设计意图success()fail() 保证了所有 API 返回结构一致 。如果以后要加 traceIdtimestamp 等字段,只需改 BaseController 两个方法即可。

ProjectController:具体业务控制器

scala 复制代码
class ProjectController extends BaseController {
    async getList(ctx) {
        const { proj_key } = ctx.request.query;     // ① 拿参数
        const res = await app.service.project.getList(); // ② 调服务
        this.success(ctx, res);                     // ③ 组装响应
    }
}

控制器的"三件事"模式 :拿参数 → 调 Service → 返回结果。Controller 不写具体业务逻辑,它是一个编排者而非执行者。


3.7 服务层:app/service/

职责业务逻辑的承载者。这是唯一可以写数据查询、外部 API 调用、复杂计算的地方。

BaseService:基础设施注入

kotlin 复制代码
class BaseService {
    constructor() {
        this.app = app;
        this.config = app.config;
        this.curl = superagent;  // 预置 HTTP 客户端
    }
}

ProjectService:具体业务服务

scala 复制代码
class ProjectService extends BaseService {
    async getList() {
        // 当前返回硬编码数据
        // 后续会替换为: this.curl.get('https://api.xxx') 或 knex('projects').select()
        return [{ name: 'project1', desc: 'project1 desc' }, ...];
    }
}

设计意图

  • Controller 不写业务逻辑------复杂逻辑抽到 Service,Controller 只做调度。
  • Service 不操作 ctx------Service 是纯数据层,不依赖 HTTP 上下文,这样 Service 可以被 Controller、定时任务、甚至单元测试直接调用。
  • Service 之间可互相调用 ------app.service.xxx 可以访问其他 Service。

3.8 配置层:config/

职责环境差异的集中管理

arduino 复制代码
config.default.js  →  基础配置(所有环境共享)
       +
config.local.js    →  本地开发覆盖(如 name: 'tcb-local')
config.beta.js     →  测试环境覆盖
config.prod.js     →  生产环境覆盖
       ↓
    app.config     =  Object.assign({}, default, env)

设计意图 :开发环境的数据库地址、测试环境的 API 域名、生产环境的日志级别------这些差异不应该散落在代码的 if/else 里,而是通过配置文件 + 环境变量集中管理。


3.9 扩展层:app/extend/

职责 :给 app 实例附加能力,使其突破 Koa 原生功能。

javascript 复制代码
// app/extend/logger.js
module.exports = (app) => {
    if (app.env.isLocal()) {
        return console;              // 本地:控制台打印
    } else {
        return log4js.getLogger();   // 非本地:log4js 落盘
    }
};
// 挂载后:app.logger.info(...)、app.logger.error(...)

设计意图extend/ 是一个开放扩展点。未来可以挂载:

  • app.redis ------ Redis 客户端
  • app.mq ------ 消息队列生产者
  • app.cache ------ 本地缓存实例

Koa 原生只有 app.use()app.listen() 等基础方法,通过 extend 机制给 app 注入业务所需的一切能力。


四、核心架构设计原则总结

设计原则 在项目中的体现
约定优于配置 文件放在 app/controller/ 就自动成为控制器,无需手动注册
单一职责 Router 只做映射、Controller 只做编排、Service 只做业务、Middleware 只做横切
依赖注入 所有模块通过工厂函数 (app) => 接收依赖,不自己 require
开闭原则 extend 机制开放扩展,loader 机制对修改封闭
关注点分离 安全校验(middleware)、参数规则(router-schema)、业务逻辑(controller/service)各自独立
环境抽象 通过 env + config 将环境差异集中管理,业务代码不感知环境

五、最后

第一阶段最大的收获不是"写了一个能跑的项目",而是亲手实现了一个微型框架内核 。理解了企业级 Node.js 应用的本质不是堆砌中间件,而是通过加载器自动装配 + 分层解耦 + 依赖注入,让每一层各司其职,后续还需要把这些进一步的运用,加强自己的理解。

相关推荐
kfaino1 小时前
码农的AI翻身·前传 一个大模型从出生到上岗的全过程
后端·aigc
IT_陈寒1 小时前
Vue的这个响应式陷阱让我熬到凌晨三点
前端·人工智能·后端
葫芦和十三2 小时前
图解 MongoDB 17|大集合与工作集:数据超过内存怎么办
后端·mongodb·面试
kfaino9 小时前
码农的AI翻身(三)你好,我叫 Embedding
后端·ai编程
葫芦和十三10 小时前
图解 MongoDB 18|复制集拓扑:Primary、Secondary 和 Arbiter 的分工
后端·mongodb·面试
爱勇宝10 小时前
大多数人不是在使用 AI 赚钱,而是在帮 AI 公司赚钱
前端·后端·程序员
程序员cxuan13 小时前
虽迟但到!GPT-5.6 终于来了!
人工智能·后端·程序员
IT_陈寒15 小时前
React的这个渲染问题连官方文档都没说清楚
前端·人工智能·后端
葫芦和十三16 小时前
图解 MongoDB 15|journal 与持久化:写入怎么不丢,崩溃怎么恢复
后端·mongodb·面试