从上一章《Egg.js最小系统》中,通过demo中的核心文件 001-mini/lib/egg.js可以看到一百多行代码实现了 Egg.js 的最小系统。从一百多行的代码中会不会发现,代码的执行流程比较错综复杂。举个例子,为什么在app/router.js编写路由(Router)和控制器(Controller)就可以注册路由和操作?这些问题在这一章就会讲解 Egg.js最小系统的执行流程,主要分两部分讲述执行流程。
- Egg.js 应用部分
- 路由文件
- 路由注册
- 控制器处理
- 路由文件
- Egg.js 服务部分
- 提供
http服务 - 读取
Egg.js 项目部分的路由文件(Router + Controller)
- 提供
- 从
npm start开始 app实例初始化EggApplication构建- 继承
EggCore- 继承
Koa - 注册内置路由器
this.router- this.router 初始化
new Router()- 继承
KoaRouter
- 继承
- this.router 初始化
- 将路由方法注册到 对象属性里
- 继承
- 注册内置加载器
AppWorkerLoader- 继承
EggLoader
- 继承
- 执行所有加载器
this.loader.loadAll();- 执行加载路由
this.loadRouter()- 加载
app/router.js注入this.app
- 加载
- 执行加载路由
- 继承
- 服务初始化
- 用
http.createServer注入app - 监听错误
- 启动
server
- 用
const EggApplication = require('./lib/egg');
// 初始化Egg.js应用
const app = new EggApplication({
baseDir: __dirname,
type: 'application',
});// EggApplication start
class AppWorkerLoader extends EggLoader {
loadAll() {
this.loadRouter();
}
}
class EggApplication extends EggCore {
constructor(options) {
super(options);
this.on('error', err => {
console.error(err);
});
this.loader.loadAll();
}
get [Symbol.for('egg#eggPath')]() {
return __dirname;
}
get [Symbol.for('egg#loader')]() {
return AppWorkerLoader;
}
}
// EggApplication endEggApplication的初始化过程中包括了AppWorkerLoader应用进程加载器的初始化。EggApplication中属性Symbol.for('egg#eggPath')是设置项目应用文件路径,通常是./app/...所在的路径。EggApplication中属性Symbol.for('egg#loader')是设置应用加载器的,这设置的是应用的项目进程加载器。AppWorkerLoader继承EggLoaderEggApplication是继承了EggCore,EggCore中构建时候就会把Symbol.for('egg#loader')属性的加载器注入到this.loader上
EggApplication在构建过程中,this.loader.loadAll()就执行了加载器的全部执行- 加载器执行了什么可以看
EggApplication的父类EggCore的构建过程
- 加载器执行了什么可以看
class EggCore extends Koa {
constructor(options) {
options.baseDir = options.baseDir || process.cwd();
options.type = options.type || 'application';
super(options);
const Loader = this[EGG_LOADER]; // this[Symbol.for('egg#loader')]
this.loader = new Loader({
baseDir: options.baseDir,
app: this,
});
}
get router() {
// this[Symbol('EggCore#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;
}
beforeStart(fn) {
process.nextTick(fn)
}
}
methods.concat(['resources', 'register', 'redirect' ]).forEach(function (method) {
EggCore.prototype[method] = function(...args) {
this.router[method](...args);
return this;
};
})EggCore 初始化过程主要有两个,一个是初始化加载器,加载器jiushi 派生类注入的loader(注:this[EGG_LOADER]就是指代EggApplication注入的this[Symbol.for('egg#loader')]属性),二是初始化路由能力。
EggCore派生类EggApplication会注入属性名称为Symbol.for('egg#loader')的加载器EggCore将属性Symbol.for('egg#loader')的加载器this.loader实例化,加入项目应用的路径baseDir和应用实例化(app)“指针”this。
EggCore初始化过程中会注入 属性名称为Symbol('EggCore#router')的路由实例this.router。EggCore.router实例中的路由方法(get、post、delete等),注入到EggCore的原型中。- 作用就是后续实例化应用
app就可以直接按照app.get('/xxx', [callback])类似的方式操作路由 - 实例化应用
app会在后面加载器内置过程会注入app/router.js中,直接在项目文件中使用app[method]方式使用路由。
- 作用就是后续实例化应用
Router的来源是继承了KoaRouter,是Koa.js的一个路由中间件,注意这里是使用了koa-router@7 +版本,主要是支持了Koa2+的async/await。
- 上面提到的
EggApplication中属性Symbol.for('egg#loader')是加载器AppWorkerLoader的实例 AppWorkerLoader继承了加载器的基类EggLoader
class EggLoader {
constructor(options) {
this.options = options;
this.app = this.options.app;
}
loadFile(filepath, ...inject) {
if (!fs.existsSync(filepath)) {
return null;
}
const extname = path.extname(filepath);
if (![ '.js', '.node', '.json', '' ].includes(extname)) {
return fs.readFileSync(filepath);
}
const ret = require(filepath);
// function(arg1, args, ...) {}
if (inject.length === 0) inject = [ this.app ];
return is.function(ret) ? ret(...inject) : ret;
}
}
const LoaderMixinRouter = {
loadRouter() {
// 加载Egg.js应用工程目录的路由
this.loadFile(path.join(this.options.baseDir, 'app/router.js'));
},
}
const loaders = [
LoaderMixinRouter,
];
for (const loader of loaders) {
Object.assign(EggLoader.prototype, loader);
}EggLoader是一个工具类,本身不会自动执行,只会在派生类的实例触发相关方法操作才会执行loaders是个临时变量,用来存放所有加载器内置方法的数组,最后所有方法都会注入到EggLoader中。因此就会出现这种情况,在AppWorkerLoader这样的派生类构建中,可以执行this.loadRouter()的操作EggLoader.loadFile()是加载器的核心方法,主要用于加载文件到缓存里,说白了就是对Node.js原生require()全局方法的封装。- 特别是在加载的文件内容类型是
Function时候,会注入EggApplication应用实例app。 - 这就是为什么执行
this.loadRouter()后,加载的路由配置文件app/router.js中,可以执行方法app.get()等路由方法。
- 特别是在加载的文件内容类型是
说了这么久,在这里的 EggApplication 只提供了项目应用的初始化,也可以说是项目实例的初始化。然而,长篇大论之后,到现在还没说到最基本的HTTP服务。EggApplication最底层是继承了Koa,本身是可以直接启动HTTP服务的,这里建议最好应用和服务分离解耦。以EggApplication只用来创建应用实例,独立用http模块启动,对于以后可以更加方便处理HTTPS迁移,多进程利用等服务操作。
const EggApplication = require('./lib/egg');
const http = require('http');
// 初始化Egg.js应用
const app = new EggApplication({
baseDir: __dirname,
type: 'application',
});
const server = http.createServer(app.callback());
server.once('error', err => {
console.log('[app_worker] server got error: %s, code: %s', err.message, err.code);
process.exit(1);
});
server.listen(7001, () => {
console.log('server started at 7001');
});- 初始化
app时候,传入的baseDir是整个项目的更目录,EggApplication就会将该参数一直透传到EggLoader和EggCore中 - 传入的
baseDir会在EggLoader执行读取对应${baseDir}/app/router.js项目应用文件。
首先说明一下,项目应用目录app/是主要写业务代码,和 Egg.js 的最小系统是完全解耦的,但是有严格的约定,这里先从 app/router.js 说起。
module.exports = app => {
app.get('/index', async ctx => {
ctx.body = 'hello index';
});
app.get('/', async ctx => {
ctx.body = 'hello world';
});
};app在上面 加载器初始化 中说过,是通过EggLoader.loadFile()方法,把app注入到app/router.js的路由注册里面。- 路由的使用方式和
koa-router的一致。