全栈实践(1) - elpis-core

202 阅读3分钟

前言

对于前端开发来说,想了解和熟悉后端开发的世界观,比较友好的过渡方式是对 Node.js 进行学习,具体基础的学习这里就不展开啦。

在企业的开发中,一般都会通过框架来快速落地项目,但对于初学者来说,想更好的巩固基础,最好不要一上来就使用太上层的框架,因为很多方法框架都已经帮你封装好了,这会导致初学者对底层 Node.js 或逻辑交互的理解很不清晰,就不利于巩固学习。

因此为了更好的学习和理解,搭建了一个简易版的服务端框架 elpis-core,主要参考 Egg.js 的设计理念并基于 Koa.js 实现。

选择 Koa.js,是因为它不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。

设计思路

在开始 elpis-core 的讲解之前呢,我们先来看看 Egg.js 所创建的项目目录结构:

egg-project
├── package.json
├── app.js(可选)
├── agent.js(可选)
├── app
|   ├── router.js
│   ├── controller
│   │   └── home.js
│   ├── service(可选)
│   │   └── user.js
│   ├── middleware(可选)
│   │   └── response_time.js
│   ├── schedule(可选)
│   │   └── my_task.js
│   ├── public(可选)
│   │   └── reset.css
│   ├── view(可选)
│   │   └── home.tpl
│   └── extend(可选)
│       ├── helper.js(可选)
│       ├── request.js(可选)
│       ├── response.js(可选)
│       ├── context.js(可选)
│       ├── application.js(可选)
│       └── agent.js(可选)
└── config
    ├── plugin.js
    ├── config.default.js
    ├── config.prod.js
    ├── config.test.js(可选)
    ├── config.local.js(可选)
    └── config.unittest.js(可选)

Eggjs 官网有这样一句话:

Egg 奉行“约定优于配置”。

Egg.js 通过对目录的约定,来规范项目结构,减少了大量协作和沟通成本。我们来简化一下目录:

  • app/router.js 用于配置 URL 路由规则
  • app/controller/** 用于解析用户的输入,处理后返回相应的结果
  • app/service/** 用于编写业务逻辑层,建议使用
  • app/middleware/** 用于编写中间件
  • app/public/** 用于放置静态资源
  • app/extend/** 用于框架的扩展
  • config/config.{env}.js 用于编写配置文件
  • config/plugin.js 用于配置需要加载的插件
  • test/** 用于单元测试
  • app.js 和 agent.js 用于自定义启动时的初始化工作

那么思考一下:

  • 底层是如何将这些约定的目录进行统筹运作起来的呢?
  • 为什么只要创建好这些目录和文件,就能开启一个接口服务呢?

这两个思考,就交给今天的主角 elpis-core 来解答吧。

解析器:elpis-core

要让约定好的目录运行起来,我们可以想到,是不是只要有一个解析器,来负责读取约定好的目录结构,并通过遍历来执行对应文件中的方法,就可以实现统筹运作了呀

image.png

而我们接下来要讲的 elpis-core 就是一个解析器,底层由 Koa.js 实现,在这个解析器中,我们封装了针对不同目录所对应的 Loader(加载、解析的意思):

  • loader/config.js ➡️ app/config
  • loader/controller.js ➡️ app/controller
  • loader/extend.js ➡️ app/extend
  • loader/middleware.js ➡️ app/middleware
  • loader/router-schema.js ➡️ app/router-schema
  • loader/router.js ➡️ app/router
  • loader/service.js ➡️ app/service

以上就是 elpis-core 约定好的规范,每个 loader 负责去读取 app 中对应的文件目录,然后将执行后获取的类或方法,挂载到 koa 的实例上,这里就实现了将磁盘文件转化为内存(运行时)的过程了。

因此,有了这个 elpis-core,我们只需要关注 app 中文件的创建,相互之间共享着 app 上的属性,实现目录间的统筹运作。

目录参考

image.png

部分代码

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

/**
 * sep 兼容不同操作系统上的斜杠
 * 例如:`./app`  => `.${sep}app`
 */
const { sep } = path;

const env = require('./env');

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

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`);

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

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

    // 加载 routerSchema
    routerSchemaLoader(app);
    console.log(`-- [start] load routerSchema 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 --`);

    // 注册全局中间件(用户引用到的外部中间件)
    // app/middleware.js
    // 用户需要经历一系列验证中间件(洋葱圈模型),才能到达最终的数据返回
    try {
      require(`${app.businessPath}${sep}middleware.js`)(app);
      console.log(`-- [start] load global Middleware done --`);
    } catch (error) {
      console.log('[Exception] there is no global middleware file.');
    }

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

    // 启动服务
    // trycatch  保证服务启动的健壮性
    try {
      const port = process.env.PORT || 8080;
      const host = process.env.IP || '0.0.0.0';
      app.listen(port, host);
      console.log('Server running on port:', port);
    } catch (error) {
      console.log(error);
    }
  },
};

测试接口服务,以 SSR 渲染为例

  • router 中定义路由:

image.png

  • controller 中定义控制层来操作服务层 service,但这里只是获取渲染页面,就不演示 service 了:

image.png

  • ctx 会将 public/dist 目录下的模板入口页面,返回给前端:

image.png

image.png 更多学习:抖音 "哲玄前端",《全栈实践课》