大前端全栈实践记录与总结——基于NodeJS实现服务端内核引擎篇

211 阅读6分钟

引用抖音“哲玄前端”《大前端全栈实践》

前言

这是一篇记录和分享个人从0到1的大前端全栈项目落地的思考与总结,我将按照基于NodeJS实现服务端内核引擎、基于webpack5工程化建设、基于Vue3领域模型架构、基于Vue3动态组件库建设、框架npm包抽象封装与发布的顺序来分享个人的相关思考和总结。在这篇文章中我将介绍基于NodeJS实现服务端内核引擎的设计思路和总结,其他的内容会在后续的文章中进行更新。

提示:文章主要讲述个人的思考和总结,不会涉及到所有的代码实现,如果需要特别详细的讲解请联系抖音哲玄前端,本文章旨在记录和分享个人对大前端全栈的实践的思考总结。文章中若有不对的地方请多多指教,我会及时更正,保证文章内容的准确性。

设计背景

绝大数时候我们在进行从0到1完成一个项目的架构设计都脱离不了实际的业务,我们需要将业务进行抽象,创建一个适合自己业务的框架。而在前期,进行已有成熟方案的调研是必不可少的。以下是业界常见且成熟的方案。

一、大而全的触达系统,标准化流程相关痛点分析:

  • 不适合多客户交付场景
  • 交付时冗余过多无用能力
  • 定制化可拓展能力弱,往往牺牲客户需求

二、多个子系统配合,灵活配置各个运营场景相关痛点分析:

  • 不适合外部客户私有化交付
  • 通用建站能力不适用于领域性较强的场景
  • 过分灵活,搭建复杂,并无实质性提效
  • 未能体系化解决触达领域问题

通过对业界已有成熟方案进行分析和比较,我希望能保留两个方案的一些优点并解决其中一些痛点来搭建一个具有标准化流程且灵活支持定制化的架构,它应该具备以下特征:

  • AOP领域建模
  • 粒度:算子服务
  • 面向对象建站

至此,我们的架构图应该如下:

引用“哲玄前端”《大前端全栈实践》中的架构图 新对话.png

该项目被抽象成三个层面,展示层-BFF层-数据层,接下来先对BFF层进行服务端内核引擎的建设。首先我们得知道什么是BFF,BFF(Backend For Frontend,服务于前端的后端)  是一种架构设计模式,核心思想是 为不同的前端应用(如 Web、移动端、小程序等)定制专用的后端服务,而不是让前端直接调用通用的后端 API。

BFF 的典型架构

前端(Web/App/小程序) 
       ↓
专属 BFF(如 Web-BFF、Mobile-BFF)  
       ↓ 
通用后端服务(微服务/数据库/第三方API)
  • BFF 像是一个适配层,根据前端需求“裁剪”后端数据。

BFF 的示例代码

router.get('/mobile/user', async (ctx,next) => {
  const [profile, orders] = await Promise.all([
    fetchUserProfile(),
    fetchUserOrders(),
  ]);
  ctx.body = { profile, orders }; // 移动端专用数据结构
});

这篇文章对BFF有更详细的介绍zhuanlan.zhihu.com/p/634498512

接下来开始关注BFF层的设计(类似egg.js),首先我们需要完成一个自定义的内核引擎(elpis-core),在运行时,我们需要按照约定的目录结构创建我们的项目文件,elpis-core会自动扫描该目录下的所有文件并挂载到Koa的实例上,其中koa的实例(app)将贯穿我们整个项目。

微信图片_20250814184624_8.jpg

Koa洋葱圈模型

5a143c9495bb4a45be3a98d54708a006.jpeg 因此约定我们的项目文件应该按照如下结构去创建:

项目文件
| app
  |--middleware //中间件
     |--custom-module
       |--custom-middleware.js 
  |--controller //业务逻辑
    |--custom-module
      |--custom-controller.js
  |--service //服务层
    |--custom-module
      |--custom-service.js
  ...
  |--index.js

elpis-core引擎所要做的就是解析app/目录下所有的js文件并挂载到app实例上,保证我们在运行时可以对挂载的这些方法进行访问。

elpis-core引擎目录

elpis-core/
├── index.js          # 框架入口,应用启动器
├── env.js            # 环境管理模块
└── loader/           # 自动加载器目录
    ├── config.js     # 配置加载器
    ├── controller.js # 控制器加载器
    ├── service.js    # 服务层加载器
    ├── middleware.js # 中间件加载器
    ├── router.js     # 路由加载器
    ├── router-schema.js # 路由模式加载器
    └── extend.js     # 扩展加载器

app实例如何传到在这些文件中进行挂载呢?参考如下代码:

index.js 文件中


const middlewareLoader = require("./loader/middleware");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
...其他引擎文件

module.exports={
start(){
 const app = new Koa();
 middlewareLoader(app) //将app实例传入middleware编译器中
 controllerLoader(app)//将app实例传入controllerLoader编译器中
 serviceLoader(app)//将app实例传入serviceLoader编译器中
 }

elpis-core/loader/middleware.js文件中:

const path = require("path");
const { sep } = path;
const glob = require("glob");

module.exports = (app) => {
  //读取app/middleware目录下所有的*.js文件
  const middlewareDir = path.resolve(app.businessDir, `.${sep}middleware`);
  const fileList = glob.sync(
    path.resolve(middlewareDir, `.${sep}**${sep}*.js`)
  );
  const middlewares = {};
  //遍历所有的*.js文件,将其挂在到app.middleware上
  fileList.forEach((file) => {
    //获取文件名称
    let name = path.resolve(file);

    //截取文件名称
    const startIndex =
      name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length;
    name = file.slice(startIndex,-3);
    //将'-'去掉,并转换成小驼峰命名

    name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());

    //挂载到app.middleware上
    const nameList = name.split(sep);
    let temMiddleware = middlewares;

    for (let i = 0; i < nameList.length; i++) {
      const el = nameList[i];

      if (i === nameList.length - 1) {
      //这里将app实例再次传入暴露出的方法中
        temMiddleware[el] = require(path.resolve(file))(app);
      } else {
        if (!temMiddleware[el]) {
          temMiddleware[el] = {};
        }
        temMiddleware = temMiddleware[el];
      }
    }
  });
  app.middlewares = middlewares;
};

app/middleware/custom-module/custome-middleware.js文件中

module.exports = (app) => {

  return async (ctx, next) => {
    //你的处理逻辑
    ....
    await next();
  };
};
 

至此完成一个中间件的编译器与中间件功能封装,除此之外,它支持我们自由封装其他核心模块。接下来我将介绍该框架的核心原理和运行时的机制,它算是我对该框架的核心的总结和理解。

 框架核心原理

1. 基于 Koa.js 的中间件架构

  • 继承 Koa 的洋葱模型中间件机制
  • 利用 ctx 上下文对象传递请求状态
  • 通过 next() 函数控制中间件执行流程

2. 自动加载器机制

 核心原理:文件系统扫描 + 动态模块加载

const fileList = glob.sync(path.resolve(dir, `**/*.js`));
fileList.forEach((file) => {  const module = require(file)(app);  // 自动挂载到 app 实例上});

3. 约定优于配置的设计

  • 目录结构约定:app/controller/、app/service/、app/middleware/ 等
  • 命名约定:文件路径自动转换为命名空间
  • 模块约定:每个模块必须导出函数 (app) => {}

 运行时机制

  • 请求处理流程 HTTP请求/页面请求 → Koa中间件链 → 路由匹配 → 控制器处理 → 服务层业务逻辑 → 响应返回
  • 模块加载流程 启动应用 → 扫描目录 → 解析文件 → 实例化模块 → 挂载到app → 注册到框架
  • 配置合并流程 默认配置 → 环境判断 → 加载环境配置 → 深度合并 → 挂载到app.config

以上就是关于BFF设计与实践的全部内容,如有错误,请多指教。