• 作者:老汪软件技巧
  • 发表时间:2024-12-30 21:04
  • 浏览量:

前言

一套好的框架往往伴随的是一套简单容易理解的规范以及用法,所谓的 egg.js (KOA) 上层框架, 既是约定了一套它的规范,可以更好更便捷的使用 KOA 这篇文章就来实现一下 elpis-core (简易的 egg.js 内核) 并补充说明一下我个人对洋葱圈模型的理解吧。

规范

每一套框架有每一套的规范,往往有框架会指出:实现 xxx 功能可以将 xxx 文件放到 xxx 文件夹下面,快速方便构造使用,elpis-core 也不例外,也会有自己的规范

|app (执行文件)
  |-- controller (主要存放处理主要逻辑的类文件 例如:解析接口数据,更加需求调动 service 内部类文件)
  |-- extend (主要存放拓张工具 例如:打印日志文件工具)
  |-- middleware (主要存放各种各样自定义中间件)
  |-- router (主要存放路由文件 例如:路由的定义 而后绑定 controller 类处理对应逻辑)
  |-- router-schema (主要存放路由对应的校验规则)
  |-- service (主要存放各种服务的类文件 例如:数据库的调用)
|config(环境文件)
  |-- config.default.js (运行时的容器,运行时只读取 default 里面的数据)
  |-- config.local.js (开发环境的文件,运行生产环境时合并到 default 中)
  |-- config.beta.js (测试环境的文件,运行生产环境时合并到 default 中)
  |-- config.prod.js (生产环境的文件,运行生产环境时合并到 default 中)

实现

调用方法

const ElpisCore = require("./elpis-core");
//项目启动
ElpisCore.start({
    name: "Elpis",
    homePage: "/view/page2"
});

上述调用可得知,启动需要提供一个 start 方法,并从规范得知需要创建七个 loader 处理对应的执行文件和环境文件,下述为文件目录

|elpis-core
  |-- loader (整合 对应文件夹下的 js 文件并将其挂载到 app 上)
        |-- config.js (判断环境,根据环境合并对应的文件)
        |-- controller.js 
        |-- extend.js
        |-- middleware.js
        |-- router.js
        |-- router-schema.js
        |-- service.js
  |-- index.js (内核启动文件)

项目启动文件

const Koa = require('koa');
const env = require('../env');
//为了兼容不同操作系统上的斜杠
const path = require('path');
const { sep } = path;
// 引入所有 loader
const configLoader = require('./loader/config');
const controllerLoader = require('./loader/controller');
const routerLoader = require('./loader/router');
const routerSchemaLoader = require('./loader/router-schema');
const serviceLoader = require('./loader/service');
const middlewareLoader = require('./loader/middleware');
const extendLoader = require('./loader/extend');
// 值得一提的是 router 必须最后去注册 因为要建立洋葱圈模型 经过中间件层次筛选才进入路由实现功能
module.exports = {
  /**
   * 项目启动入口
   * @param {object} options 项目配置参数
   * options = {
   *     name 名称,
   *     homePage 项目首页
   * }
   * */
  start (options = {}) {
    // 创建 Koa 实例
    const app = new Koa();
    // 挂载实例配置到 app 对象
    app.options = options;
    // 基础路径 一般指的项目根目录
    app.baseDir = process.cwd();
    // 业务文件路径 跟目录下的./app
    app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
    // 初始化环境配置
    app.env = env();
    console.log(`-- [start] env: ${app.env.get()} --`);
    // 加载所有 loader
    // 加载 middleware
    middlewareLoader(app);
    console.log(`-- [start] load middleware done --`);
    // 加载 router-schema
    routerSchemaLoader(app);
    console.log(`-- [start] load router-schema done --`);
    // 加载 controller
    controllerLoader(app);
    console.log(`-- [start] load controller done --`);
    // 加载 service
    serviceLoader(app);
    console.log(`-- [start] load service done --`);
    // 加载 config
    configLoader(app);
    console.log(`-- [start] load config done --`);
    // 加载 extend
    extendLoader(app);
    console.log(`-- [start] load extend done --`);
    // 注册全局中间件
    try {
      require(`${app.businessPath}${sep}middleware.js`)(app);
      console.log(`-- [start] load global middleware done --`);
    } catch (err) {
      console.log(`[exception] there is no global middleware file`);
    }
    // 注册路由
    routerLoader(app);
    console.log(`-- [start] load router done --`);
    // 启动 Koa 服务
    try {
      const port = process.env.PORT || 8080;
      const host = process.env.IP || '0.0.0.0';
      app.listen(port, host);
      console.log(`Listening on ${host}:${port}`);
    } catch (err) {
      console.error(err);
    }
  }
};

关键代码实现 (除 config.js router.js 外 其余基本类似 以 controller 为例)

config.js 主要功能是判断环境变量合并文件

router.js 大概与实例代码相似但最终是以 use 的方式直接注册挂载而不是单纯的放入 app 对象中

const glob = require('glob');
const path = require('path');
const { sep } = path;
/**
 * controller loader
 * @param {object} app Koa 实例
 *
 * 加载所有的 controller, 可通过 'app.controller.${目录}.${文件}' 访问
 *
 * 例子:
 * app/controller
 *  |
 *  | -- custom-module
 *          |
 *          | -- custom-controller.js
 * 通过解析后可以通过
 * app.controller.customModule.customController
 * 形式访问
 * */
module.exports = (app) => {
  // 读取 app/controller/*/*/.../*.js

_按钮点击实现图片移动js实现_js实现拖拽实现工作流

const controllerPath = path.resolve(app.businessPath, `.${sep}controller`); const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}**.js`)); // 遍历所有的文件目录,把内容加载到 app.controller 下 const controllers = {}; 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('.')); // 将 “-” 的文件名称 改为驼峰式 name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase()); // 将 controllers 存到 app 对象中 let tempController = controllers; const names = name.split(sep); names.forEach((name_each, index, self) => { if (index < self.length - 1) { if (!tempController[name_each]) tempController[name_each] = {}; tempController = tempController[name_each]; } else { // 值得一提的是 除了两个类文件 其他的直接引入即可 无需特意 new 出来 // tempController[name_each] = require(path.resolve(file))(app) // 而 extend 无需深层循环毕竟只是工具文件 注意不要跟 app 中自带的方法冲突即可 const controllerModule = require(path.resolve(file))(app); tempController[name_each] = new controllerModule(); } }); }); app.controller = controllers; };

应用

就这样一个基础的内核就实现了那么如何使用呢 (利用内核运行一个简单的接口)

1.首先理所应当的是根据规范创建对应的文件夹

2.注册路由并创建对应的 controller 文件和 service 文件

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

// app/controller/project.js
module.exports = (app) => {
  return class ProjectController {
    /**
     * 获取项目列表
     * @param {object} ctx 上下文
     * */
    async getList (ctx) {
      const { project: projectService } = app.service;
      const projectList = await projectService.getList();
      ctx.status = 200;
      ctx.body = {
        success: true,
        data: projectList
      };
    }
  };
};

// app/service/project.js
module.exports = (app) => {
  return class ProjectService {
    async getList () {
      return [
        {
          name: 'project1',
          desc: 'project 1 description'
        },
        {
          name: 'project2',
          desc: 'project 2 description'
        },
        {
          name: 'project3',
          desc: 'project 3 description'
        },
        {
          name: 'project4',
          desc: 'project 4 description'
        }
      ];
    }
  };
};

最后启动服务在浏览器中看到该内容就成功啦!

image.png

补充 洋葱圈模型

相信很多朋友多多少少都听到过这个词吧 其实也很好理解 下面图所示吧

image.png

很好理解吧,每加一层中间件校验都在路由外层包裹,通过各种中间件,无论是自定义的还是 KOA 自带的中间件,不断的去调用,每调用一次就加一层,最后就会得到这么个很像洋葱的玩意,没理解完全没关系,接下来来也用该内核实现一个简单的中间件案例吧。

tips:在 elpis-core 的入口文件中一定要先注册中间件 再注册路由才能达到洋葱圈模型的效果创建两个中间件

// app/middleware/first-middleware-verify.js
moudle.exports = (app) => {
  retrun async (_, next) => {
    console.log("中间件1");
    await next();
    console.log("中间件1");
  }
}

// app/middleware/second-middleware-verify.js
moudle.exports = (app) => {
  retrun async (_, next) => {
    console.log("中间件2");
    await next();
    console.log("中间件2");
  }
}

调用中间件

// app/middleware.js
module.exports = (app) => {
  app.use(app.middlewares.firstMiddlewareVerify);
  app.use(app.middlewares.secondMiddlewareVerify);
}

在路由中打印点东西验证洋葱圈模型

// app/controller/project.js
module.exports = (app) => {
  return class ProjectController {
    async getList (ctx) {
      ···
      console.log("路由");
      ···
    }
  };
};

接下来执行路由可以在控制台看到

image.png

由中间件1 -> 中间件2 -> 路由 -> 中间件2 -> 中间件1 完成整个路由的流程或者理解成 KOA 提供的 next 方法可以看成就是下一个中间件或路由

moudle.exports = (app) => {
  retrun async () => {
    console.log("中间件1");
    console.log("中间件2");
     ···
      console.log("路由");
     ···
    console.log("中间件2");
    console.log("中间件1");
  }
}

总结

以上就是我对上层框架内核和洋葱圈模型的理解和运用了,如有错误请广大网友帮我指正,幸苦咯~~

当然内核也不是特别的完善 像根本不支持 ts 和没有强约束 毕竟规范只是规定,也可以用一些流氓的写法例如不用service 呀不用 controller 呀,直接处理在 router 中也不是不行,就是不符合 elpis-core 的规范罢了,总之有理解不到位的地方请指正吧。

powered by 抖音“哲玄前端”,《全栈实践课》


上一条查看详情 +使用 Webpack 优雅的构建微前端应用
下一条 查看详情 +没有了