前言
最近在翻查egg-cool-router源码查看如何使用装饰器来注册egg路由,看到了一些很牛逼的操作,想知道为什么就查了一下关于源码讲解的文章,解惑了所以摘抄了下来。
egg-core是什么
应用、框架、插件之间的关系
在学习egg-core是什么之前,我们先了解一下关于Egg框架中应用,框架,插件这三个概念及其之间的关系:
- 一个应用必须指定一个框架才能运行起来,根据需要我们可以给一个应用配置多个不同的插件
- 插件只完成特定独立的功能,实现即插即拔的效果
- 框架是一个启动器,必须有它才能运行起来。框架还是一个封装器,它可以在已有框架的基础上进行封装,框架也可以配置插件,其中Egg,EggCore都是框架
- 在框架的基础上还可以扩展出新的框架,也就是说框架是可以无限级继承的,有点像类的继承
- 框架/应用/插件的关于service/controler/config/middleware的目录结构配置基本相同,称之为加载单元(loadUnit),包括后面源码分析中的getLoadUnits都是为了获取这个结构
1 | # 加载单元的目录结构如下图,其中插件和框架没有controller和router.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对象源码:
1 | //egg-core源码 -> index文件导出的数据结构 |
EggLoader的具体实现源码学习
EggCore类源码学习
EggCore类是算是上文提到的框架范畴,它从Koa类继承而来,并做了一些初始化工作,其中有三个主要属性是:
- loader:这个对象是EggLoader的实例,定义了多个load函数,用于对loadUnit目录下的文件进行加载,后面后专门讲这个类的是实现
- router:是EggRouter类的实例,从koa-router继承而来,用于egg框架的路由管理和分发,这个类的实现在后面的loadRouter函数会有说明
- lifecycle:这个属性用于app的生命周期管理,由于和整个文件加载逻辑关系不大,所以这里不作说明
1 | //egg-core源码 -> EggCore类的部分实现 |
EggLoader类源码学习
如果说eggCore是egg框架的精华所在,那么eggLoader可以说是eggCore的精华所在,下面我们主要从EggLoader的实现细节开始学习eggCore这个库:
EggLoader首先对app中的一些基本信息(pkg/eggPaths/serverEnv/appInfo/serverScope/baseDir等)进行整理,并且定义一些基础共用函数(getEggPaths/getTypeFiles/getLoadUnits/loadFile),所有的这些基础准备都是为了后面介绍的几个load函数作准备,我们下面看一下其基础部分的实现:
1 | //egg-core源码 -> EggLoader中基本属性和基本函数的实现 |
各种loader函数的实现源码分析
上文中只是介绍了EggLoader中的一些基本属性和函数,那么如何将LoadUnits中的不同类型的文件分别加载进来呢,egg-core中每一种类型(service/controller等)的文件加载都在一个独立的文件里实现。比如我们加载controller文件可以通过’./mixin/controller’目录下的loadController完成,加载service文件可以通过’./mixin/service’下的loadService函数完成,然后将这些方法挂载EggLoader的原型上,这样就可以直接在EggLoader的实例上使用
1 | //egg-core源码 -> 混入不同目录文件的加载方法到EggLoader的原型上 |
我们按照上述loaders中定义的元素顺序,对各个load函数的源码实现进行一一分析:
loadPlugin函数
插件是一个迷你的应用,没有包含router.js和controller文件夹,我们上文也提到,应用和框架里都可以包含插件,而且还可以通过环境变量和初始化参数传入,关于插件初始化的几个参数:
- enable: 是否开启插件
- env: 选择插件在哪些环境运行
- path: 插件的所在路径
- package: 和path只能设置其中一个,根据package名称去node_modules里查询plugin,后面源码里有详细说明
1 | //egg-core源码 -> loadPlugin函数部分源码 |
loadConfig函数
配置信息的管理对于一个应用来说非常重要,我们需要对不同的部署环境的配置进行管理,Egg就是针对环境加载不同的配置文件,然后将配置挂载在app上,
加载config的逻辑相对简单,就是按照顺序加载所有loadUnit目录下的config文件内容,进行合并,最后将config信息挂载在this对象上,整个加载函数请看下面源码:
1 | //egg-core源码 -> loadConfig函数分析 |
loadExtend相关函数
这里的loadExtend是一个笼统的概念,其实是针对koa中的app.response,app.respond,app.context以及app本身进行扩展,同样是根据所有loadUnits下的配置顺序进行加载
下面看一下loadExtend这个函数的实现,一个通用的加载函数
1 | //egg-core -> loadExtend函数实现 |
loadService函数
如何在egg框架中使用service
loadService函数的实现是所有load函数中最复杂的一个,我们不着急看源码,先看一下service在egg框架中如何使用
1 | //egg-core源码 -> 如何在egg框架中使用service |
我们上面列举了service下的js文件的四种写法,都是从每次请求的上下文this.ctx获取到service对象,然后就可以使用到每个service文件导出的对象了,这里主要有两个地方需要注意:
为什么我们可以从每个请求的this.ctx上获取到service对象呢:
看过koa源码的同学知道,this.ctx其实是从app.context继承而来,所以我们只要把service绑定到app.context上,那么当前请求的上下文ctx自然可以拿到service对象,eggLoader也是这样做的
针对上述四种使用场景,具体导出实例是怎么处理的呢?
- 如果导出的是一个类,EggLoader会主动以ctx对象去初始化这个实例并导出,所以我们就可以直接在该类中使用this.ctx获取当前请求的上下文了
- 如果导出的是一个函数,那么EggLoader会以app作为参数运行这个函数并将结果导出
- 如果是一个普通的对象,直接导出
FileLoader类的实现分析
在实现loadService函数时,有一个基础类就是FileLoader,它同时也是loadMiddleware,loadController实现的基础,这个类提供一个load函数根据目录结构和文件内容进行解析,返回一个target对象,我们可以根据文件名以及子文件名以及函数名称获取到service里导出的内容,target结构类似这样:
1 | { |
下面我们先看一下fileLoader这个类的实现
1 | //egg-core源码 -> FileLoader实现 |
ContextLoader类的实现分析
上文中说道loadService函数其实最终把service对象挂载在了app.context上,所以为此提供了ContextLoader这个类,继承了FileLoader类,用于将FileLoader解析出来的target挂载在app.context上,下面是其实现:
1 | //egg-core -> ContextLoader类的源码实现 |
loadService的实现
有了ContextLoader类,那实现loadService函数就非常容易了,如下:
1 | //egg-core -> loadService函数实现源码 |
loadMiddleware函数
中间件是koa框架中很重要的一个环节,通过app.use引入中间件,使用洋葱圈模型,所以中间件加载的顺序很重要。
- 如果在上文中的config中配置的中间件,系统会自动用app.use函数使用该中间件
- 所有的中间件我们都可以在app.middleware中通过中间件name获取到,便于在业务中动态使用
1 | //egg-core -> loadMiddleware函数实现源码 |
loadController函数
controller中生成的函数最终还是在router.js中当作一个中间件使用,所以我们需要将controller中内容转换为中间件形式async function(ctx, next),其中initializer这个函数就是用来针对不同的情况将controller中的内容转换为中间件的,下面是loadController的实现逻辑:
1 | //egg-core源码 -> loadController函数实现源码 |
loadRouter函数
loadRouter函数特别简单,只是require加载一下app/router目录下的文件而已,而所有的事情都交给了EggCore类上的router属性去实现
而router又是Router类的实例,Router类是基于koa-router实现的
1 | //egg-core源码 -> loadRouter函数源码实现 |
Router类继承了KoaRouter类,并对其的method相关函数做了扩展,解析controller的写法,同时提供了resources方法,为了兼容restAPI的方式
关于restAPI的使用方式和实现源码我们这里就不介绍了,可以看官方文档,有具体的格式要求,下面看一下Router类的部分实现逻辑:
1 | //egg-core源码 -> Router类实现源码 |
总结
以上便是对EggCore的大部分源码的实现的学习总结,其中关于源码中一些debug代码以及timing运行时间记录的代码都删掉了,关于app的生命周期管理的那部分代码和loadUnits加载逻辑关系不大,所以没有讲到。EggCore的核心在于EggLoader,也就是plugin,config, extend, service, middleware, controller, router的加载函数,而这几个内容加载必须按照顺序进行加载,存在依赖关系,比如:
- 加载middleware时会用到config关于应用中间件的配置
- 加载router时会用到关于controller的配置
- 而config,extend,service,middleware,controller的加载都必须依赖于plugin,通过plugin配置获取插件目录
- service,middleware,controller,router的加载又必须依赖于extend(对app进行扩展),因为如果exports是函数的情况下,会将app作为参数执行函数