基于node实现服务端内核引擎

5 阅读2分钟

由于工作职责长期处于基于数据渲染页面的纯前端工作,对后端世界的概念了解甚少,所以基于node实现一个服务端内核,拓展后端视野。

后端项目是怎么启动的?

当一个后端项目,通过某种方法启动,到服务监听端口,正常执行请求的处理,返回数据。整个过程是怎么启动起来的呢?请看下方的简易流程图

image.png

环境变量的读取

使用node启动的时候,通过cross-env包兼容wndows和linux环境变量的注入,例如:cross-env NODE_ENV=development node xxx.js

module.exports = (app) => {
  return {
    // 判断当前环境是否开发环境
    isDevelopment() {
      return process.env.NODE_ENV === "development";
    },

    // 判断当前环境是否测试环境
    isTest() {
      return process.env.NODE_ENV === "test";
    },

    // 判断当前环境是否生产环境
    isProduction() {
      return process.env.NODE_ENV === "production";
    },

    // 获取当前环境
    get() {
      return process.env.NODE_ENV ?? "development";
    },
  };
};

Loader实现

controller Loader实现

const path = require("path");
const glob = require("glob");
const { sep } = path;
/**
 * 把多个中间件函数注册到app.controller
 * @param {object} app koa 实例
 *
 * 文件系统:app/controller
 *              | custom-model
 *                     |  xxxx.js
 *                     |  custom-controller.js
 * 结果: app.controller.customModel.customController
 */

module.exports = (app) => {
  // 获取app/controller目录
  const controllerPath = path.join(app.businessPath, `controller`);
  // 获取所有文件
  const fileList = glob.sync(path.join(controllerPath, "**", "*.js"));
  const controller = {};
  // 遍历所有文件
  fileList.forEach((filePath) => {
    let tempController = controller;
    // 从路径中截取合法的子路径
    let relPath = path.relative(controllerPath, filePath);
    // 将 - 或者 _ 转换成驼峰
    relPath = relPath.replace(/[-_](\w)/gi, (_, letter) =>
      letter.toUpperCase(),
    );
    const parts = relPath.split(sep);
    let filename = parts.pop();
    filename = path.parse(filename).name;
    const names = [...parts, filename];

    // 创建嵌套对象指向

    for (let i = 0, len = names.length; i < len; ++i) {
      if (i == len - 1) {
        const Controller = require(filePath)(app);
        tempController[names[i]] = new Controller();
      } else {
        if (!tempController[names[i]]) {
          tempController[names[i]] = {};
        }
        tempController = tempController[names[i]];
      }
    }
  });
  // 挂载到app.controller中
  app.controller = controller;
};

config Loader实现

用于合并多个环境的配置

const path = require("path");
/**
 * 把多个config合并成一个
 * @param {object} app koa 实例
 *
 * 文件系统:config
 *           | config.development.js
 *           | config.default.js
 *           | config.test.js
 *           | config.production.js
 *
 * 结果: app.config = { ...config.default, ...config.[env] }
 */

module.exports = (app) => {
  // 获取config目录路径
  const configPath = path.join(app.baseDir, `config`);

  // 获取默认的config配置
  let defaultConfig = {};
  try {
    defaultConfig = require(path.join(configPath, "config.default.js"))(app);
  } catch (err) {
    console.error(`[default config load exception] ${err.message}`);
  }

  // 获取环境配置
  let envConfig = {};
  const { isDevelopment, isTest, isProduction } = app.$env;
  try {
    if (isDevelopment()) {
      envConfig = require(path.join(configPath, `config.development.js`))(
        app,
      );
    } else if (isTest()) {
      envConfig = require(path.join(configPath, `config.test.js`))(
        app,
      );
    } else if (isProduction()) {
      envConfig = require(
        path.join(configPath, `config.production.js`),
      )(app);
    }
  } catch (err) {
    console.error(`[env config load exception] ${err.message}`);
  }

  // 合并配置
  app.config = {
    ...defaultConfig,
    ...envConfig,
  };
};

其余Loader实现

这些都是相似的代码逻辑实现。暂不过多说明了...

解析引擎实现

核心就是通过多个loader解析对应的模块文件,挂载到运行时app实例中

// elpis-core/index.js
module.exports = {
  /**
   * 启动项目
   * @param {object} options 项目配置
   * @param {string} options.name 项目名
   */
  start(options = {}) {
    const app = new Koa();

    // 项目配置
    app.options = options;

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

    // 应用路径
    app.businessPath = path.resolve(app.baseDir, `.${sep}app`);

    // 环境变量加载器
    app.$env = env(app);
    console.log(`-- [start] env: ${app.$env.get()}`);

    // 配置加载器
    configLoader(app);
    console.log(`-- [start] config loader done--`);

    // 拓展加载器
    extendLoader(app);
    console.log(`-- [start] extend loader done--`);

    // 服务加载器
    serviceLoader(app);
    console.log(`-- [start] service loader done--`);

    // middleware加载器
    middlewareLoader(app);
    console.log(`-- [start] middleware loader done--`);

    // 路由参数配置加载器
    routerSchema(app);
    console.log(`-- [start] router schema loader done--`);

    // 控制器加载器
    controllerLoader(app);
    console.log(`-- [start] controller loader done--`);

    // 前置中间件注册
    try {
      require(path.resolve(app.businessPath, `.${sep}pre-middleware.js`))(app);
      console.log(`-- [start] global middleware loader done--`);
    } catch (err) {
      console.error(`-- [global middleware exception] ${err.message}--`);
    }

    // 路由加载器
    routerLoader(app, require(path.join(app.businessPath, `post-router.js`)));
    console.log(`-- [start] router loader done--`);

    // 启动服务
    try {
      const port = process.env.PORT || 8080;
      const host = process.env.HOST || "0.0.0.0";
      app.listen(port, host);
      console.log(`${app.options.name} Server listening on ${host}:${port}`);
    } catch (error) {
      console.error(error);
    }
  },
};

执行效果

image.png

尾言

以上就完成了一个服务端内核的实现。当调用start()方法的时候,会使用elpis-core/下的各个loader,按顺序将模块目录下的文件加载到内存中,并挂载到app实例中,然后启动服务。应用层就遵守[约定大于配置]的规则,根据业务自行组织了