Elpis系列之基于koa实现elpis-core

103 阅读4分钟

elpis-core学习记录

前言

Elpis是一个js全栈开发的企业级应用,其目的是解决业务开发中重复的CRUD劳动。共有五个阶段:

  1. 基于 node + koa 实现一个服务端开发框架
  2. 基于 webpack5 + express 完成前端工程化建设
  3. 基于“领域模型方案”完成系统架构设计
  4. 基于 vue3 + elementPlus 完成前端动态组件
  5. 抽象封装 elpis 并发布到 NPM

此文是完成了第一个阶段elpis-core后记录一下自己的学习理解。

简介

elpis-core最大的特点是模块的自动加载,通过不同的loader,将各个功能模块注入到koa的实例app上:将app作为入参,在不同的loader里加载对应模块的所有文件,然后将文件的返回值作为一个属性挂载到app下。这种处理,方便各个模块的拆分,却同时又通过app关联起来,是一种高内聚低耦合的设计。

简单贴一下代码,详细的代码讲解可搜索站内其他elpis文章

入口文件index.js

const Koa = require("koa");
const path = require("path");

const env = require("./env");
const configLoader = require("./loader/config");
const routerLoader = require("./loader/router");
const routerSchemaLoader = require("./loader/router-schema");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
const extendLoader = require("./loader/extend");
const middlewareLoader = require("./loader/middleware");

const { sep } = path; // 兼容不同操作系统下的斜杠

module.exports = {
  /**
   * 启动项目
   * @param {object} options 项目配置
   * options = {
   *  name // 项目名称
   *  homePage // 项目首页
   * }
   */

  start(options = {}) {
    // koa 实例
    const app = new Koa();

    // 应用配置
    app.options = options;

    // 基础路径
    app.baseDir = process.cwd();

    // 业务文件路径
    app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
    console.log(
      `基础路径app.baseDir:${app.baseDir} 业务文件路径 app.businessPath:${app.businessPath}`
    );

    // 初始化环境配置
    app.env = env();
    console.log(`-- [start] env is: ${app.env.get()} --`);

    // 加载middleware
    middlewareLoader(app);
    // console.log(`${app.middlewares}`);
    console.log(`-- [start] load middleware done --`);

    // 加载路由schema
    routerSchemaLoader(app);
    // console.log(`${app.routerSchema}`);
    console.log(`-- [start] load router schema done --`);

    // 加载controller
    controllerLoader(app);
    // console.log(`${app.controller}`);
    console.log(`-- [start] load controller done --`);

    // 加载service
    serviceLoader(app);
    // console.log(`${app.service}`);
    console.log(`-- [start] load service done --`);

    // 加载config
    configLoader(app);
    // console.log(`${app.config}`);
    console.log(`-- [start] load config done --`);

    // 加载extend
    extendLoader(app);
    // console.log(`${app.extend}`);
    console.log(`-- [start] load extend done --`);

    // 注册全局中间件,即用户自定义loader(只会在app/middleware.js下)
    try {
      require(`${app.businessPath}${sep}middleware.js`)(app);
      console.log(`-- [start] load global middleware done --`);
    } catch (error) {
      console.log(`-- [exception] global middleware file error --`);
    }

    // 注册路由
    routerLoader(app);
    console.log(`-- [start] load router done --`);

    // 启动服务
    try {
      const port = process.env.port || "8080";
      const host = process.env.host || "0.0.0.0";
      app.listen(port, host);
      console.log("server running on port:", port);
    } catch (error) {
      console.error(error);
    }
  },
};

业务逻辑处理controller.js

const glob = require("glob");
const path = require("path");
const { sep } = path;
/**
 * controller loader
 * @param {object} app Koa 实例
 *
 * 加载所有 middleware 可通过 `app.controller.${目录}.${文件}` 访问
 *
 * 示例:
 * app/controller
 *    |
 *    | -- custom-module
 *       |
 *       | -- custom-controller.js
 * 输出 => app.middlewares.customModule.customController
 */

module.exports = (app) => {
  // 读取 app/controller/**/**.js 下所有文件
  const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);
  const fileList = glob.sync(
    path.resolve(controllerPath, `.${sep}**${sep}**.js`)
  );

  // 遍历所有文件目录,把内容加载到 app.controller下
  const controller = {};
  fileList.forEach((file) => {
    // 提取文件名称
    let name = path.resolve(file);

    // 截取路径 如:app/controller/custom-module/custom-controller.js => custom-module/custom-controller
    name = name.substring(
      name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,
      name.lastIndexOf(".")
    );

    // 把 '_'或'-' 统一改为驼峰式, custom-module/custom-controller  => customModule/customController
    name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());

    // 挂在 controller 到内存app对象中
    let tempController = controller;
    const names = name.split(sep); // ['customModule', 'customController']
    for (let i = 0, len = names.length; i < names.length; ++i) {
      if (i === len - 1) {
        const ControllerModule = require(path.resolve(file))(app);
        tempController[names[i]] = new ControllerModule();
      } else {
        if (!tempController[names[i]]) {
          tempController[names[i]] = {};
        }
        tempController = tempController[names[i]];
      }
    }
  });
  app.controller = controller;
};

  1. 环境配置:configLoader
  2. 功能扩展:extendLoader + middlewareLoader
  3. 逻辑分层:serviceLoader + controllerLoader
  4. 路由管理:routerSchemaLoader + routerLoader

收获

  • 这个阶段最大的收获是使用js完成了一个服务端开发框架。之前是纯前端业务开发,对于服务端的理解还停留在大学期间。从前端发送请求,到服务端接受到请求以后,通过controller处理业务,service处理数据,完成了整个流程的开发。

  • 对于koa洋葱圈模型的理解加深。刚开始讲解elpis-core架构的时候,看这张图是挺懵的,现在清楚了。一个请求会经过一层层的处理到达核心(拿数据或静态资源),在核心拿到数据资料后再一层层的返回。

    • 对于api请求。elpis-core中第一层是异常捕获,第二层是请求的签名合法性校验,第三层时api参数校验。经过这三层以后才会到controller中进行业务处理,controller中处理完毕后,到service中进行数据处理。处理好的数据再一层层的返回。
  • 约定优于配置,开发流程标准。目录结构十分清晰,只需按照约定,在不同的目录下添加自己的业务代码,即可完成自己的定制化需求。

  • 逆向思维的建立。课程中先写loader,再写app(loader中就已经用到app下的文件)。先处理的接口健壮性,再由前端发起验证(不是前端发现接口有问题的时候再处理接口错误)。对于顺向思维比较强的前端开发者,逆向思维的抽象十分利于建立自己的架构思维

课程优点

  • 一个个文件建,一行行代码敲;
  • 可见编码时的错误及错误解决,注重代码健壮性;
  • 不讲解api,更多的是思想体系的建立
  • 亲自代码评审且有注解
  • 十分适用于有一定经验的我

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