[学习笔记] 基于 Koa 二次封装的应用程序 —— elpis-core

619 阅读3分钟

elpis-core

注:抖音 “哲玄前端”,《大前端全栈实践课》

这个项目是一个基于 Koa 框架的应用程序,它使用了一系列的 loader 来加载和组织 Koa 应用的各个部分,包括

  • middlewareLoader(中间件)
  • routerSchemaLoader(路由参数校验)
  • controllerLoader(控制器)
  • serviceLoader(服务)
  • configLoader(配置)
  • extendLoader(扩展)
  • globalMiddlewareLoader(全局中间件加载)
  • routerLoader(路由)

image.png

middlewareLoader

  • 中间件是 Koa 应用的基础组件,负责处理请求和响应的流程。
  • 项目中的中间件加载器会自动扫描 app/middleware 目录下的所有中间件文件,并将它们挂载到应用的上下文中。
  • 中间件的加载和组织方式使得应用可以灵活地扩展和维护,同时保持代码的清晰和可重用性。
async function middleware(app: Elpis.App) {
  if (!app.businessPath) {
    throw new Error('app.businessPath 不存在')
  }

  // 读取 app/middleware/**/*.ts 下所有文件
  const middlewarePath = path.resolve(app.businessPath, `.${sep}middleware`);
  const fileList = globSync(path.resolve(middlewarePath, `.${sep}**${sep}*.ts`), {
    windowsPathsNoEscape: true
  });

  const middlewares: Record<string, any> = {};
  const promiseArr = fileList.map(async (file) => {
    let fileName = path.resolve(file);

    // 截取路径 app/middlewares/custom-module/custom-middleware
    const splitName = `middleware${sep}`;
    fileName = fileName.substring(fileName.lastIndexOf(splitName) + splitName.length, fileName.lastIndexOf('.ts'))

    // `-` 转 驼峰命名
    fileName = getCamelCase(fileName);

    const fileNames = fileName.split(sep);
    let tempMiddleMares = middlewares;

    // 创建 middleware 实例挂载到 app
    const promiseArr = fileNames.map(async (name, index) => {

      if (index === fileNames.length - 1) {
        tempMiddleMares[name] = (await import(pathAdapter(file))).default(app);
        return;
      }

      // 初始化目录
      if (!tempMiddleMares[name]) tempMiddleMares[name] = {};

      tempMiddleMares = tempMiddleMares[name];
    })

    return Promise.all(promiseArr)
  });

  await Promise.all(promiseArr)

  app.middlewares = middlewares;
}

routerSchema

读取合并所有的 router-shema

async function routerSchema(app: Elpis.App) {
  if (!app.businessPath) {
    throw new Error('app.businessPath 不存在')
  }

  // 读取 app/router-schema/*.ts 下所有文件
  const middlewarePath = path.resolve(app.businessPath, `.${sep}router-schema`);
  const fileList = globSync(path.resolve(middlewarePath, `.${sep}**${sep}*.ts`), {
    windowsPathsNoEscape: true
  });

  const routerSchema = await fileList.reduce(async (pre, cur) => {
    return {
      ...await pre,
      ...(await import(pathAdapter(cur))).default
    }
  }, {})

  // 挂载到 app
  app.routerSchema = routerSchema;
}

controllerLoader

  • 控制器负责处理请求,并调用相应的服务方法来完成业务逻辑。
  • 项目中的控制器加载器会自动扫描 app/controller 目录下的所有控制器文件,并将它们挂载到应用的上下文中。
async function controller(app: Elpis.App) {
    ...

  // 读取 app/controller/**/*.ts 下所有文件
  const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);
  const fileList = globSync(path.resolve(controllerPath, `.${sep}**${sep}*.ts`), {
    windowsPathsNoEscape: true
  });

     ...
    // 截取路径 app/controllers/custom-module/custom-controller
    ...

    // `-` 转 驼峰命名
    fileName = getCamelCase(fileName);

    const fileNames = fileName.split(sep);
    let tempMiddleMares = controllers;

    // 创建 controller 实例挂载到 app
    const promiseArr = fileNames.map(async (name, index) => {

      if (index === fileNames.length - 1) {
        const controllerModule = (await import(pathAdapter(file))).default;
        tempMiddleMares[name] = new controllerModule(app);
        return;
      }

      // 初始化目录
      if (!tempMiddleMares[name]) tempMiddleMares[name] = {};

      tempMiddleMares = tempMiddleMares[name];
    })
    ...
}

serviceLoader

  • 服务负责处理具体的业务逻辑,它们可以被控制器调用。服务负责处理具体的业务逻辑,它们可以被控制器调用。
  • 项目中的服务加载器会自动扫描 app/service 目录下的所有服务文件,并将它们挂载到应用的上下文中。
async function service(app: Elpis.App) {
    ...
  // 读取 app/service/**/*.ts 下所有文件
  const servicePath = path.resolve(app.businessPath, `.${sep}service`);
  const fileList = globSync(path.resolve(servicePath, `.${sep}**${sep}*.ts`), {
    windowsPathsNoEscape: true
  });

    ...
    // 创建 service 实例挂载到 app
    const promiseArr = fileNames.map(async (name, index) => {

      if (index === fileNames.length - 1) {
        const serviceModule = (await import(pathAdapter(file))).default;

        tempMiddleMares[name] = new serviceModule(app);
        return;
      }

      // 初始化目录
      if (!tempMiddleMares[name]) tempMiddleMares[name] = {};

      tempMiddleMares = tempMiddleMares[name];
    })
    ...
}

configLoader

  • 配置文件用于定义应用的全局配置,如数据库连接、端口号等。
  • 项目中的配置加载器会自动扫描 config 目录下的所有配置文件,并将它们合并到应用的上下文中。
async function config(app: Elpis.App) {
  ...
  const configPath = path.resolve(app.baseDir, `.${sep}config`);
  // 根据环境配置读取对应的配置
  const currentEnv = app.appEnv!.get();

  const baseConfigPath = getBaseConfigPath(configPath, currentEnv);

  let defaultConfig = {};

  if (fs.existsSync(baseConfigPath.default)) {
    defaultConfig = (await import(pathAdapter(baseConfigPath.default))).default;
  } else {
    console.log('[exception]: config.default.ts 文件缺失');
  }

  let evnConfig = {};
  if (fs.existsSync(baseConfigPath[currentEnv])) {
    evnConfig = (await import(pathAdapter(baseConfigPath[currentEnv]))).default;
  } else {
    console.log(`[exception]: config.${currentEnv}.ts 文件缺失`);
  }

  // 注入到 koa 实例
  app.config = Object.assign({}, defaultConfig, evnConfig);
}

routerLoader

  • 路由负责将请求分发到相应的控制器方法。
  • 项目中的路由加载器会自动扫描 app/router 目录下的所有路由文件,并将它们挂载到应用的上下文中。
async function router(app: Elpis.App) {
   ...
  // 读取 app/router/*.ts 下所有文件
  const routerPath = path.resolve(app.businessPath, `.${sep}router`);

  // 初始化 KoaRouter
  const router = new KoaRouter()

  const fileList = globSync(path.resolve(routerPath, `.${sep}**${sep}*.ts`), {
    windowsPathsNoEscape: true
  });

  // 注册所有路由
  const promiseArr = await fileList.map(async (file) => {
    (await import(pathAdapter(file))).default(app, router);
  })

  // 路由兜底
  // router.get(/.*/g, (ctx, next) => {
  //   ctx.state = 302; // 临时重定向
  //   ctx.redirect(app?.options?.homePath || '/index');
  // });

  // 把路由注册到app
  app.use(router.routes());
  // 用于处理请求的 OPTIONS 方法以及设置响应头中的 Allow 头部,显示允许的 HTTP 方法。
  app.use(router.allowedMethods());

  ...
}

globalMiddlewareLoader

加载和应用全局中间件。在基于 Koa 框架的应用程序中,全局中间件是在应用程序的所有路由和请求处理之前执行的中间件。这些中间件通常用于处理跨域请求、日志记录、错误处理等通用逻辑。

使用

  • service
  • 定义一个service
  • app -> service -> project -> project1.ts
class Controller extends BaseService {
  getList(ctx: Elpis.Ctx) {
    this.app.logger.info(ctx.request.body);
    return [
      {
        id: '1',
        name: 'project1'
      },
      {
        id: '2',
        name: 'project2'
      },
      {
        id: '3',
        name: 'project3'
      },
    ]
  }
}
  • controller
  • 定义一个控制器
  • app -> controller -> project -> project1.ts
class Controller extends BaseController {
  /**
   * 获取项目列表
   * @param ctx 上下文
   */
  async getList(ctx: Elpis.Ctx) {
    const { project: projectService } = this.app.services!;
    const res = await projectService.project1.getList(ctx);
    this.success(ctx, res);
  }
}

export default Controller;
  • router
  • 定义一个路由
  • app -> router -> project.ts
function router2(app: Elpis.App, router: Elpis.Router) {
  const { project: projectController } = app.controller || {};

  router.get('/project', projectController.project1.getList.bind(projectController.project1));
  router.post('/project', projectController.project1.getList.bind(projectController.project1));
}
  • middleeware
  • 针对每个路由参数验证中间件使用
  • app -> middleware -> api-params-verify.ts
function ApiSignVerify(app: Elpis.App): Elpis.AppUse {
    // 使用 avj 验证接收的参数是否符合对应路由的 json-schema 规则
    const ajv = new Ajv();

    return async (ctx, next) => {
        const { path, method, params } = ctx;

        if (!path.includes('/api')) {
            return await next();
        }

        const { body, query, headers } = ctx.request;
        app.logger?.info(`[${method} ${path}]: body: ${JSON.stringify(body)}`);
        app.logger?.info(`[${method} ${path}]: query: ${JSON.stringify(query)}`);
        app.logger?.info(`[${method} ${path}]: params: ${JSON.stringify(params)}`);

        // 获取对应路径的 schema 规则
        const schema = app.routerSchema[path]?.[method.toLowerCase()];
        const appendValidate = {
            body: body,
            query: query,
            params,
        }

        if (!schema) {
            return await next();
        };

        const schemaKeys = Object.keys(schema) as (keyof typeof appendValidate)[];

        const validateArr = schemaKeys.map(async (sKey) => {
            const validate = await ajv.compile(schema[sKey]);
            const flag = await validate(appendValidate[sKey]);
            if (!flag) return Promise.reject({
                key: sKey,
                message: validate.errors
            });

            return flag;
        });

        await Promise.all(validateArr).then(() => {
            next();
        }).catch((err) => {
            let message = 'request validata fail';
            if ('key' in err) {
                const msg = err.message[0];
                message = `[${err.key}] request validata fail: ${msg?.instancePath?.slice(1)} ${msg.message}`
            }

            ctx.status = 200;
            ctx.body = {
                success: false,
                message,
                code: 442,
            }

            return;
        });
    }
}
  • globalMiddleware.ts

app -> middleware.ts

function middleware(app: Elpis.App) {
   ...
   // 参数合法性校验中间件
   app.use(app.middlewares?.apiParamsVerify);
   ...
}

总结

本项目是基于学习的目的搭建的,实现了自动扫描和加载应用中的各种组件,为开发者提供了一个高效、可扩展且易于维护的应用程序框架。