loadExtend(name, proto) {
// All extend files
const filepaths = this.getExtendFilePaths(name);
// if use mm.env and serverEnv is not unittest
const isAddUnittest = 'EGG_MOCK_SERVER_ENV' in process.env && this.serverEnv !== 'unittest';
for (let i = 0, l = filepaths.length; i < l; i++) {
const filepath = filepaths[i];
filepaths.push(filepath + `.${this.serverEnv}.js`);
if (isAddUnittest) filepaths.push(filepath + '.unittest.js');
}
const mergeRecord = new Map();
for (let filepath of filepaths) {
filepath = utils.resolveModule(filepath);
if (!filepath) {
continue;
} else if (filepath.endsWith('/index.js')) {
// TODO: remove support at next version
deprecate(`app/extend/${name}/index.js is deprecated, use app/extend/${name}.js instead`);
}
const ext = utils.loadFile(filepath);
//获取内置对象的原有属性
const properties = Object.getOwnPropertyNames(ext)
.concat(Object.getOwnPropertySymbols(ext));
//对属性进行遍历
for (const property of properties) {
if (mergeRecord.has(property)) {
debug('Property: "%s" already exists in "%s",it will be redefined by "%s"',
property, mergeRecord.get(property), filepath);
}
// Copy descriptor
let descriptor = Object.getOwnPropertyDescriptor(ext, property);
let originalDescriptor = Object.getOwnPropertyDescriptor(proto, property);
if (!originalDescriptor) {
// try to get descriptor from originalPrototypes
const originalProto = originalPrototypes[name];
if (originalProto) {
originalDescriptor = Object.getOwnPropertyDescriptor(originalProto, property);
}
}
//省略代码
//将扩展属性进行合并
Object.defineProperty(proto, property, descriptor);
mergeRecord.set(property, filepath);
}
debug('merge %j to %s from %s', Object.keys(ext), name, filepath);
}
},
关于
egg
egg
所处的定位天猪曾经在这篇优秀的博文中给出关于
egg
的定位,如下图:可以看到
egg
处于的是一个中间层的角色,基于koa
,不同于koa
以middleware
为主要生态,egg
根据不同的业务需求和场景,加入了plugin
,extends
等这些功能,可以让开发者摆脱在使用middleware
功能时无法控制使用顺序的被动状态,而且还可以增加一些请求无关的一些功能。除此之外,egg
还有很多其他优秀的功能,在这里不详述。想了解更多可以移步这里初始化项目
egg
有直接生成整个项目的脚手架功能,只需要执行如下几条命令,就可以生成一个新的项目:启动项目:
egg
是如何运行起来的下面通过追踪源码来讲解一下
egg
究竟是如何运行起来的:查看
egg-init
脚手架生成的项目文件,可以看到整个项目文件是没有严格意义上的入口文件的,根据package.json
中的script
命令,可以看到执行的直接是egg-bin dev
的命令。找到egg-bin
文件夹中的dev.js
,会看到里面会去执行start-cluster
文件:移步到
start-cluster.js
文件,可以看到关键的一行代码:其中
options.framework
打印信息为:找到对应的
egg
目录中的index.js
文件:继续追踪可以看到最后运行的其实就是
egg-cluster
中的startCluster
,并且会fork
出agentWorker
和appWorks
,官方文档对于不同进程的fork
顺序以及不同进程之间的IPC
有比较清晰的说明, 主要的顺序如下:通过代码逻辑也可以看出它的顺序:
通过上面的代码可以看出,
master
进程会去监听当前的状态,比如在检测到agent-start
的时候才去fork
AppWorkers
,在当前状态为egg-ready
的时候,会去执行如下的进程之间的通信:master
--->parent
master
--->agent
master
--->app
fork
出了appWorker
之后,每一个进程就开始干活了,在app_worker.js
文件中,可以看到进程启动了服务,具体代码:然后就回归到
koa
中的入口文件干的事情了。除此之外,每一个
appWorker
还实例化了一个Application
:在实例化
application(options)
时,就会去执行node_modules->egg
模块下面loader
目录下面的逻辑,也就是agentWorker
进程和多个appWorkers
进程要去执行的加载逻辑,具体可以看到app_worker_loader.js
文件中的load()
:这也是下面要讲的东西了
在真正执行业务代码之前,
egg
会先去干下面一些事情:加载插件
egg
中内置了如下一系列插件:加载插件的逻辑是在
egg-core
里面的plugin.js
文件,先看代码:如上代码(只是贴出了比较关键的地方),这段代码主要是将本地插件、
egg
中内置的插件以及应用的插件进行了整合。其中this.allPlugins
的结果如下:可以看出,
this.allPlugins
包含了所有内置的插件以及本地开发者自定义的插件。先获取所有插件的相关信息,然后将所有插件进行遍历,执行this.mergePluginConfig()
函数,这个函数主要是对插件名称进行一些校验。之后还对项目中已经开启的插件进行整合。plugin.js
文件还做了一些其他事情,比如获取插件路径,读取插件配置等等,这里不一一讲解。扩展内置对象
在对内置对象进行扩展的时候,实质上执行的是
extend.js
文件,扩展的对象包括如下几个:通过阅读
extend.js
文件可以知道,其实最后每个对象的扩展都是直接调用的loadExtends
这个函数。拿Application
这个内置对象进行举例:将
filepaths
进行打印,如下图:可以看出,
filepaths
包含所有的对application
扩展的文件路径,这里会首先将所有插件中扩展或者开发者自己自定义的扩展文件的路径获取到,然后进行遍历,并且对内置对象的一些原有属性和扩展属性进行合并,此时对内置对象扩展的一些属性就会添加到内置对象中。所以在执行业务代码的时候,就可以直接通过访问application.属性(或方法)
进行调用。加载中间件
对中间件的加载主要是执行的
egg-core
中的middleware.js
文件,里面的代码思想也是和上面加载内置对象是一样的,也是将插件中的中间件和应用中的中间件路径全部获取到,然后进行遍历。遍历完成之后执行中间件就和
koa
一样了,调用co
进行包裹遍历。加载控制器
对控制器的加载主要是执行的
egg-core
中的controller.js
文件egg
的官方文档中,插件的开发这一节提到:所以在加载
controller
的时候,主要是load
应用里面的controller
即可。详见代码;这里主要是针对
controller
的类型进行判断(是否是Object
,class
,promise
,generator
),然后分别进行处理加载service
加载
service
的逻辑是egg-core
中的service.js
,service.js
这个文件比较简单,代码如下:首先也是先获取所有插件和应用中声明的
service.js
文件目录,然后执行this.loadToContext()
loadToContext()
定义在egg-loader.js
文件中,继续追踪,可以看到在loadToContext()
函数中实例化了ContextLoader
并执行了load()
,其中ContextLoader
继承自FileLoader
,而且load()
是声明在FileLoader
类中的。 通过查看load()
代码可以发现里面的逻辑也是将属性添加到上下文(ctx
)对象中的。也就是说加载context
对象是在加载service
的时候完成的。而且值得一提的是:在每次刷新页面重新加载或者有新的请求的时候,都会去执行
context_loader.js
里面的逻辑,也就是说ctx
上下文对象的内容会随着每次请求而发生改变,而且service
对象是挂载在ctx
对象下面的,对于service
的更新,这里有一段代码:在更新
service
的时候,首先会去获取service
是否挂载在ctx
中,如果没有,则直接返回,否则实例化service
,这也就是service
模块中的延迟实例化加载路由
加载路由的逻辑主要是
egg-core
中的router.js
文件可以看出很简单,只是加载应用文件下的
router.js
文件加载配置
直接加载配置文件并提供可配置的方法。
设置应用信息
对
egg
应用信息的设置逻辑是对应的egg-core
中的egg-loader.js
,里面主要是提供一些方法获取整个app
的信息,包括appinfo
,name
,path
等,比较简单,这里不一一列出执行业务逻辑
然后就会去执行如渲染页面等的逻辑
总结
这里只是我个人针对源代码以及断点调试总结的一些东西,如有不同见解或者觉得有哪些错误的地方,可以私聊拍砖。