Egg 源码分析之 egg-core

本文详细分析了Egg框架的核心组件Egg-Core,探讨了应用、框架、插件的关系,重点解析了EggLoader类及其load函数,包括加载配置、服务、中间件、控制器和路由的实现。Egg-Core通过约定优于配置的原则,简化了Node.js项目开发,提高了开发效率和代码质量。
摘要由CSDN通过智能技术生成

我们团队现在开发的node项目都是基于koa框架实现的,虽然现在也形成了一套团队内的标准,但是在开发的过程中也遇到了一些问题:

  1. 由于没有统一的规范,新人上手和沟通成本比较高,容易出现错误
  2. 仅局限于目前需求进行设计,扩展性不高
  3. 系统部署及配置信息维护成本较高
  4. 业务代码实现起来不是很优雅,比如(1)关于文件的引入,到处的require,经常会出现忘记require或者多余的require问题(2)因为在当前请求的上下文ctx中封装了很多有用的数据,包括response,request以及在中间件中处理的中间结果,但是如果我们想在service以下的js文件中获取到ctx必须需要主动以函数参数的方式传进去,不是特别友好

而阿里团队基于koa开发的egg框架,基于一套统一约定进行应用开发,很好的解决了我们遇到的一些问题,看了egg的官方开发文档后,比较好奇它是怎么把controller,service,middleware,extend,route.js等关联在一起并加载的,后面看了源码发现这块逻辑主要在egg-core这个库中实现的,所以关于自己对egg-core源码的学习收获做一个总结:

egg-core是什么

应用、框架、插件之间的关系

在学习egg-core是什么之前,我们先了解一下关于Egg框架中应用,框架,插件这三个概念及其之间的关系:

  • 一个应用必须指定一个框架才能运行起来,根据需要我们可以给一个应用配置多个不同的插件
  • 插件只完成特定独立的功能,实现即插即拔的效果
  • 框架是一个启动器,必须有它才能运行起来。框架还是一个封装器,它可以在已有框架的基础上进行封装,框架也可以配置插件,其中Egg,EggCore都是框架
  • 在框架的基础上还可以扩展出新的框架,也就是说框架是可以无限级继承的,有点像类的继承
  • 框架/应用/插件的关于service/controler/config/middleware的目录结构配置基本相同,称之为加载单元(loadUnit),包括后面源码分析中的getLoadUnits都是为了获取这个结构
# 加载单元的目录结构如下图,其中插件和框架没有controller和router.js
# 这个目录结构很重要,后面所有的load方法都是针对这个目录结构进行的
        loadUnit
        ├── package.json
        ├── app
        │   ├── extend
        │   |   ├── helper.js
        │   |   ├── request.js
        │   |   ├── response.js
        │   |   ├── context.js
        │   |   ├── application.js
        │   |   └── agent.js
        │   ├── service
        |   ├── controller
        │   ├── middleware
        │   └── router.js
        └── config
            ├── config.default.js
            ├── config.prod.js
            ├── config.test.js
            ├── config.local.js
            └── config.unittest.js
eggCore的主要工作

egg.js的大部分核心代码实现都在egg-core库中,egg-core主要export四个对象:

  • EggCore类:继承于Koa,做一些初始化工作,EggCore中最主要的一个属性是loader,也就是egg-core的导出的第二个类EggLoader的实例
  • EggLoader类:整个框架目录结构(controller,service,middleware,extend,route.js)的加载和初始化工作都在该类中实现的,主要提供了几个load函数(loadPlugin,loadConfig,loadMiddleware,loadService,loadController,loadRouter等),这些函数会根据指定目录结构下文件输出形式不同进行适配,最终挂载输出内容。
  • BaseContextClass类:这个类主要是为了我们在使用框架开发时,在controller和service作为基类使用,只有继承了该类,我们才可以通过this.ctx获取到当前请求的上下文对象
  • utils对象:几个主要的函数,包括转换成中间件函数middleware,根据不同类型文件获取文件导出内容函数loadFile等

所以egg-core做的主要事情就是根据loadUnit的目录结构规范,将目录结构中的config,controller,service,middleware,plugin,router等文件load到app或者context上,开发人员只要按照这套约定规范,就可以很方便进行开发,以下是EggCore的exports对象源码:


//egg-core源码 -> index文件导出的数据结构
const EggCore = require('./lib/egg');
const EggLoader = require('./lib/loader/egg_loader');
const BaseContextClass = require('./lib/utils/base_context_class');
const utils = require('./lib/utils');

module.exports = {
   
  EggCore,
  EggLoader,
  BaseContextClass,
  utils,
};

EggLoader的具体实现源码学习

EggCore类源码学习

EggCore类是算是上文提到的框架范畴,它从Koa类继承而来,并做了一些初始化工作,其中有三个主要属性是:

  • loader:这个对象是EggLoader的实例,定义了多个load函数,用于对loadUnit目录下的文件进行加载,后面后专门讲这个类的是实现
  • router:是EggRouter类的实例,从koa-router继承而来,用于egg框架的路由管理和分发,这个类的实现在后面的loadRouter函数会有说明
  • lifecycle:这个属性用于app的生命周期管理,由于和整个文件加载逻辑关系不大,所以这里不作说明
//egg-core源码 -> EggCore类的部分实现

const KoaApplication = require('koa');
const EGG_LOADER = Symbol.for('egg#loader');

class EggCore extends KoaApplication {
   
    constructor(options = {
   }) {
   
        super();
        const Loader = this[EGG_LOADER];
        //初始化loader对象
        this.loader = new Loader({
   
            baseDir: options.baseDir,          //项目启动的根目录
            app: this,                         //EggCore实例本身
            plugins: options.plugins,          //自定义插件配置信息,设置插件配置信息有多种方式,后面我们会讲
            logger: this.console,             
            serverScope: options.serverScope, 
        });
    }
    get [EGG_LOADER]() {
   
        return require('./loader/egg_loader');
    }
    //router对象
    get router() {
   
        if (this[ROUTER]) {
   
          return this[ROUTER];
        }
        const router = this[ROUTER] = new Router({
    sensitive: true }, this);
        // register router middleware
        this.beforeStart(() => {
   
          this.use(router.middleware());
        });
        return router;
    }
    //生命周期对象初始化
    this.lifecycle = new Lifecycle({
   
        baseDir: options.baseDir,
        app: this,
        logger: this.console,
    });
}

EggLoader类源码学习

如果说eggCore是egg框架的精华所在,那么eggLoader可以说是eggCore的精华所在,下面我们主要从EggLoader的实现细节开始学习eggCore这个库:

EggLoader首先对app中的一些基本信息(pkg/eggPaths/serverEnv/appInfo/serverScope/baseDir等)进行整理,并且定义一些基础共用函数(getEggPaths/getTypeFiles/getLoadUnits/loadFile),所有的这些基础准备都是为了后面介绍的几个load函数作准备,我们下面看一下其基础部分的实现:

//egg-core源码 -> EggLoader中基本属性和基本函数的实现

class EggLoader {
   
    constructor(options) {
   
        this.options = options;
        this.app = this.options.app;
        //pkg是根目录的package.json输出对象
        this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
        //eggPaths是所有框架目录的集合体,虽然我们上面提到一个应用只有一个框架,但是框架可以在框架的基础上实现多级继承,所以是多个eggPath
        //在实现框架类的时候,必须指定属性Symbol.for('egg#eggPath'),这样才能找到框架的目录结构
        //下面有关于getEggPaths函数的实现分析
        this.eggPaths = this.getEggPaths();
        this.serverEnv = this.getServerEnv();
        //获取app的一些基本配置信息(name,baseDir,env,scope,pkg等)
        this.appInfo = this.getAppInfo();
        this.serverScope = options.serverScope !== undefined
            ? options.serverScope
            : this.getServerScope();
    }
    //递归获取继承链上所有eggPath
    getEggPaths() {
   
        const EggCore = require('../egg');
        const eggPaths = [];
        let proto = this.app;
        //循环递归的获取原型链上的框架Symbol.for('egg#eggPath')属性
        while (proto) {
   
            proto = Object.getPrototypeOf(proto);
            //直到proto属性等于EggCore本身,说明到了最上层的框架类,停止循环
            if (proto === Object.prototype || proto === EggCore.prototype) {
   
                break;
            }
            const eggPath = proto[Symbol.for('egg#eggPath')];
            const realpath = fs.realpathSync(eggPath);
            if (!eggPaths.includes(realpath)) {
   
                eggPaths.unshift(realpath);
            }
        }
        return eggPaths;
    }
    
    //函数输入:config或者plugin;函数输出:当前环境下的所有配置文件
    //该函数会根据serverScope,serverEnv的配置信息,返回当前环境对应filename的所有配置文件
    //比如我们的serverEnv=prod,serverScope=online,那么返回的config配置文件是['config.default', 'config.prod', 'config.online_prod']
    //这几个文件加载顺序非常重要,因为最终获取到的config信息会进行深度的覆盖,后面的文件信息会覆盖前面的文件信息
    getTypeFiles(filename) {
   
        const files = [ `${
     filename}.default` ];
        if (this.serverScope) files.push(`${
     filename}.${
     this.serverScope}`);
        if (this.serverEnv === 'default') return files;

        files.push(`${
     filename}.${
     this.serverEnv}`);
        if (this.serverScope) files.push(`${
     filename}.${
     this.serverScope}_${
     this.serverEnv}`);
        return files;
    }
    
    //获取框架、应用、插件的loadUnits目录集合,上文有关于loadUnits的说明
    //这个函数在下文中介绍的loadSerivce,loadMiddleware,loadConfig,loadExtend中都会用到,因为plugin,framework,app中都会有关系这些信息的配置
    getLoadUnits() {
   
        if (this.dirs) {
   
            return this.dirs;
        }
        const dirs = this.dirs = [];
        //插件目录,关于orderPlugins会在后面的loadPlugin函数中讲到
        if (this.orderPlugins) {
   
            for (const plugin of this.orderPlugins) {
   
                dirs.push({
   
                    path: plugin.path,
                    type: 'plugin',
                });
            }
        }
        //框架目录
        for (const eggPath of this.eggPaths) {
   
            dirs.push({
   
                path: eggPath,
                type: 'framework',
            });
        }
        //应用目录
        dirs.push({
   
            path: this.options.baseDir,
            type
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值