Egg.js 源码分析(1)

栏目: Node.js · 发布时间: 5年前

内容简介:前端时间抽出时间针对上面是我对Koa 源码分析的一些简单的理解, 后面我会将对Koa 的理解,进一步的记录下来。 Koa 是一个很小巧灵活的框架, 不像Express, Express 已经集成了很多的功能, 很多功能不再需要第三方的框架,比如说路由功能, Koa 需要引用第三方的库koa-router 来实现路由等。但是express 则不需要,下面是Koa 和Express, 两个实现一个简单的功能的Demo , 我们可以比较下其使用方式:哈哈,我们上面说了很多的废话(文字表达能力问题), 其实我是想

前端时间抽出时间针对 Koa2 源码进行了简单的学习, koa 源码是一个很简单的库, 针对分析过程, 想手把手的实现一个类型 koa 的框架,其 代码 , 根据一步步的完善实现一个简单版本的Koa, 每一个步骤一个 Branch , 如: stpe-1 , 对应的是我想实现第一步的代码, 代码仅供自己简单的学习,很多地方不完善,只是想体验下Koa 的思想。下面几点是我对Koa 的简单理解:

  • 所有的NodeJS 框架最基本的核心就是通过原生库 http or https 启动一个后端服务 http.createServer(this.serverCallBack()).listen(...arg) , 然后所有的请求都会进入 serverCallBack 方法, 然后我们可以通过拦截,在这个方法中处理不同的请求
  • Koa 是一个洋葱模型, 其是基于中间件来实现的.通过 use 来添加一个中间件, koa-router 其实就是一个 koa 的中间件,我们的所有的请求都会将所有的中间件都执行一遍,洋葱模型如下图所示
Egg.js 源码分析(1)
Egg.js 源码分析(1)

上面是我对Koa 源码分析的一些简单的理解, 后面我会将对Koa 的理解,进一步的记录下来。 Koa 是一个很小巧灵活的框架, 不像Express, Express 已经集成了很多的功能, 很多功能不再需要第三方的框架,比如说路由功能, Koa 需要引用第三方的库koa-router 来实现路由等。但是express 则不需要,下面是Koa 和Express, 两个实现一个简单的功能的Demo , 我们可以比较下其使用方式:

// Express
const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})
复制代码
// Koa 
var Koa = require('koa');
// 引用第三方路由库
var Router = require('koa-router');

var app = new Koa();
var router = new Router();
router.get('/', (ctx, next) => {
  // ctx.router available
});
// 应用中间件: router
app
  .use(router.routes())
  .use(router.allowedMethods());
app.listen(3000);
复制代码

哈哈,我们上面说了很多的废话(文字表达能力问题), 其实我是想分析下,怎么基于Koa 框架去应用, eggjs 就是基于Koa 框架基础上试下的一个框架, 我们下面来具体分析下 eggjs 框架。

Eggjs 基本使用

我们根据快速入门, 可以很快搭建一个Egg 项目框架,

$ npm i egg-init -g
$ egg-init egg-example --type=simple
$ cd egg-example
$ npm i
复制代码

我们可以用 npm run dev 快速启动项目.然后打开 localhost:7001 ,就可以看到页面输出:

hi, egg.

说明我们项目初始化已经完成,而且已经启动成功。我们现在可以学习下egg项目生成的相关代码。其代码文件结构如下:

Egg.js 源码分析(1)

分析整个文件结构,找了整个项目都没有发现app.js之类的入口文件(我一般学习一个新的框架,都会从入口文件着手),,发现 app 文件夹下面的应该对项目很重要的代码:

1, controller文件夹,我们从字面理解,应该是控制层的文件,其中有一个home.js 代码如下:

'use strict';

const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'hi, egg';
  }
}

module.exports = HomeController;

复制代码

这个类继承了egg 的Controller 类, 暂时还没有发现这个项目哪个地方有引用这个 Controller 类?

2, 一个 router.js 文件, 从字面意义上我们可以理解其为一个路由的文件,其代码如下:

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};

复制代码

这个文件暴露除了一个方法, 从目前来猜测应该就是路由的一些配置, 但是找遍整个项目也没有发现,哪个地方引用了这个方法, router.get('/', controller.home.index); , 但是从这个get 方法的第二个参数, 其似乎指向的是Controller 里面的home.js 文件index 方法,我们可以尝试修改下home.js 中的 this.ctx.body = 'hi, egg -> hello world!'; , 然后重新运行 npm run dev , 发现页面输出是 hi, egg -> hello world! , 看来 controller.home.index 这个指向的是home.js 里的index 方法无疑了, 但是 controller.home.index 这个index 方法绑定的是在一个 controller 对象上,什么时候绑定的呢?

我们接下来带着如下疑问来学些 eggjs :

  • 没有类似的app.js 入口文件,运行 npm run dev 如何启动一个项目(启动server, 监听端口, 添加中间件)?
  • 我们打开页面 http://localhost:7001/ ,怎么去通过router.js 去查找路由的,然后调用对应的回调函数?
  • Controller 是如何绑定到app 上面的controller 对象上的?

eggjs 启动

我们先查看一开始用 egg-init 命令创建的项目的package.json 文件,查看 scripts ,里面有一系列的命令,如下图:

Egg.js 源码分析(1)
我们可以通过 npm run start 来启动程序, 但是其中有一个命令 debug , 我们可以可以通过 npm run debug 命令来调试eggjs 程序, 其对用的命令是 egg-bin debug , 所以我们整个入口就是这个命令,我们下面来具体分析下 egg-bin debug

是如何工作的.

egg-bin

egg-bin 中的 start-cluster 文件, 调用了eggjs 的入口方法: require(options.framework).startCluster(options); 其中options.framework指向的就是一个绝对路径 D:\private\your_project_name\node_modules\egg (也就是 egg 模块), 直接执行 D:\private\your_project_name\node_modules\egg\index.js 暴露出来的 exports.startCluster = require('egg-cluster').startCluster;startCluster 方法。 下面我们就来分析 egg-cluster 模块。

egg-cluster

egg-cluster 的项目结构如下, 其中有两个主要的文件: master.js , app_worker.js 两个文件,

Egg.js 源码分析(1)

master.js 是跟nodejs的多线程有关,我们先跳过这一块,直接研究 app_worker.js 文件,学习eggjs 的启动过程。下面我们就是app_worker.js 执行的主要步骤。

  1. const Application = require(options.framework).Application; , 引入eggjs 模块, optons.framework 指向的就是 D:\private\your_project_name\node_modules\egg
  2. const app = new Application(options); (创建一个 egg 实例)
  3. app.ready(startServer); 调用egg 对象的** ready ** 方法,其startServer 是一个回调函数,其功能是调用nodejs 原生模块 http or httpscreateServer 创建一个nodejs 服务( server = require('http').createServer(app.callback()); , 我们后续会深入分析这个方法)。

上面三个步骤, 已经启动了一个nodejs 服务, 监听了端口。也就是已经解决了我们的第一个疑问:

没有类似的app.js 入口文件,运行npm run dev 如何启动一个项目(启动server, 监听端口, 添加中间件)?

上面其实我们还是只是分析了eggjs启动的基本流程, 还没有涉及eggjs 的核心功能库,也就是** egg ** 和** egg-core** 两个库,但是我们上面已经初实例化了一个eggjs 的对象 const app = new Application(options); , 下面我们就从这个入口文件来分析eggjs 的核心模块。

egg & egg-core

egg 和egg-core 模块下面有几个核心的类,如下:

Application(egg/lib/applicaton.js)----->EggApplication(egg/lib/egg.js)----->EggCore(egg-core/lib/egg.js)----->KoaApplication(koa)

从上面的关系可以,eggjs 是基于 koa 的基础上进行扩展的,所以我们从基类的构造函数开始进行分析(因为new Application 会从继类开始的构造函数开始执行)。

EggCore(egg-core/lib/egg.js)

我们将构造函数进行精简,代码如下

Egg.js 源码分析(1)

从上图可知,构造函数就是初始化了很多基础的属性,其中有两个属性很重要:

  1. this.lifecycle 负责整个eggjs 实例的生命周期,我们后续会深入分析整个生命周期
  1. this.loader (egg-core/lib/loader/egg_loader.js)解决了eggjs 为什么在服务启动后,会自动加载,将项目路径下的 router.js , controller/**.js , 以及 service/**.js 绑定到 app 实例上, 我们接下来会重点分析这个loader.

EggApplication(egg/lib/egg.js)

我们将构造函数进行精简,代码如下

Egg.js 源码分析(1)

这个构造函数同样也是初始化了很多基础的属性, 但是其中有调用 EggCore 构造函数初始化的 loaderloadConfig() 方法, 这个方法顾名思义就是去加载配置,其指向的是: egg/lib/loader/app_worker_loader .js 的方法 loadConfig , 这个方法,如下:

loadConfig() {
    this.loadPlugin();
    super.loadConfig();
  }

复制代码

其会加载所有的Plugin ,然后就加载所有的Config.

this.loadPlugin() 指向的是 egg-core/lib/loader/mixin/plgin.js 的方法 loadPlugin , 其会加载三种plugin:

  • const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.default')); ,应用配置的plugin , 也就是 your-project-name/config/plugin.js , 也就是每个应用需要配置的特殊的插件
  • const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default')); , 也就是从eggjs 框架配置的插件, 其路径是在 egg/config/plugin.js , 也就是框架自带的插件
  • process.env.EGG_PLUGINS 第三种, 是启动项目是,命令行带参数 EGG_PLUGINS 的插件, 应该使用不广。

最后将所有的plugin 挂在在app实例上 this.plugins = enablePlugins; ,。(后续会学习怎么这些plugin 怎么工作的。)

接下来会执行 super.loadConfig() 方法, 其指向的是 egg-core/lib/loader/mixin/config.jsloadConfig() 方法, 其同样会加载四种config:

  • const appConfig = this._preloadAppConfig(); , 应用配置的config , 也就是每个应用的特殊配置,其会加载两个配置:
const names = [
     'config.default',
     `config.${this.serverEnv}`,
   ];
复制代码

第一个一定会加载对应的 config.default 配置, 也就是 your-project-name/config/config.default.js ,跟运行环境没有关系的配置, 其次会加载跟运行环境有关的配置,如: config.prod.js , config.test.js , config.local.js , config.unittest.js

  • 会去加载所有的plugin 插件目录
if (this.orderPlugins) {
     for (const plugin of this.orderPlugins) {
       dirs.push({
         path: plugin.path,
         type: 'plugin',
       });
     }
   }
复制代码
  • 会去加载egg 项目目录, 也就是egg/config 目录
for (const eggPath of this.eggPaths) {
     dirs.push({
       path: eggPath,
       type: 'framework',
     });
   }
复制代码
  • 回去加载应用项目的目录, 也就是也就是 your-project-name/config

最后将合并的config 挂载在app 实例上 this.config = target;

我们可以打开 egg/config/config.default.js 文件,可以查看下,默认的都有什么配置,其中一个配置如下:

config.cluster = {
    listen: {
      path: '',
      port: 7001,
      hostname: '',
    },
  };
复制代码

很明显,这应该是一个对server 启动的配置,我们暂且可以这样猜测。

我们上面有分析在 egg-cluster/lib/app_worker.js 中,我们初始化 app 后,我们有调用 app.ready(startServer); 方法,我们可以猜测 startServer 方法就是启动nodejs server 的地方。

startServer 方法中,初始化了一个http server server = require('http').createServer(app.callback()); , 然后我们给listen server.listen(...args);; , 这样算是node js 的server 启动起来了, 我们可以查看下,我可以查看args 的参数:

const args = [ port ];
      if (listenConfig.hostname) args.push(listenConfig.hostname);
      debug('listen options %s', args);
      server.listen(...args);
复制代码

这里给args 添加了prot 端口参数, 我们可以跳转到prot定义的地方:

const app = new Application(options);
const clusterConfig = app.config.cluster || /* istanbul ignore next */ {};
const listenConfig = clusterConfig.listen || /* istanbul ignore next */ {};
const port = options.port = options.port || listenConfig.port;
复制代码

我们可以看到port 最终来源于: app.config.cluster.listen.port ,从这里我们得知, eggjs 的config 的使用方式。

问题:

如果我们不想在eggjs 项目启动时,默认打开的端口不是 7001 ,我们改怎么操作呢?

我们应该有如下两种方式:

  1. 在执行npm run debug 命令时,添加相应的参数
  2. 我们可以在我们项目的config/config.default.js 中添加配置,将默认的给覆盖掉,如:
module.exports = appInfo => {
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1541735701381_1116';

  // add your config here
  config.middleware = [];
  config.cluster = {
    listen: {
      path: '',
      port: 7788,
      hostname: '',
    },
  };
  return config;
};

复制代码

如上,我们再次启动项目的时候,打开的端口就是: 7788了。

思考:

我们已经知道可以在config 中进行相应的配置了, 我们还有什么其他的应用在config 上面呢?

我们知道在不同的运行环境下,会加载不同的配置,那如果我们在开发的时候,调用api 的路径是: http://dev.api.com , 但是在上线的时候,我们调用的app的路径是: http://prod.api.com , 我们就可以在 config.prod.js 中配置 apiURL:http://prod.api.com , 在 config.local.js 配置: apiURL:http://prod.api.com

然后我们在我们调用API的地方通过 app.apiURL 就可以。

Application(egg/lib/application.js)

Application(egg/lib/applicaton.js)----->EggApplication(egg/lib/egg.js)----->EggCore(egg-core/lib/egg.js)----->KoaApplication(koa)

我们已经将上述的两个核心的类: EggApplication(egg/lib/egg.js)----->EggCore(egg-core/lib/egg.js), 我们现在来分析最上层的类: Application(egg/lib/applicaton.js)。

我们还是从构造函数入手,我们发现了一行很重要的代码 this.loader.load(); 其指向的是: app_worker_loader.js (egg/lib/loader/app_worker_loader.js)的load 方法, 其实现如下:

load() {
    // app > plugin > core
    this.loadApplicationExtend();
    this.loadRequestExtend();
    this.loadResponseExtend();
    this.loadContextExtend();
    this.loadHelperExtend();
    // app > plugin
    this.loadCustomApp();
    // app > plugin
    this.loadService();
    // app > plugin > core
    this.loadMiddleware();
    // app
    this.loadController();
    // app
    this.loadRouter(); // Dependent on controllers
  }
复制代码

从这个方法可知,加载了一大批的配置,我们可以进行一一的分析:

this.loadApplicationExtend();

这个方法会去给应用加载很多的扩展方法, 其加载的路径是: app\extend\application.js, 会将对应的对象挂载在app 应用上。 (使用方法可以参考egg-jsonp/app/extend/applicaton.js 或者egg-session/app/extend/application.js)

this.loadResponseExtend(); this.loadResponseExtend(); this.loadContextExtend(); this.loadHelperExtend(); ,

this.loadApplicationExtend(); 加载的方式是一样的,只是对应的名称分别是: request.js, response.js, helper.js, context.js

this.loadCustomApp();

定制化应用, 加载的文件是对应项目下的app.js (your_project_name/app.js), 其具体的代码实现如下: (egg-core/lib/loader/mixin/custom.js)

[LOAD_BOOT_HOOK](fileName) {
    this.timing.start(`Load ${fileName}.js`);
    for (const unit of this.getLoadUnits()) { 
      const bootFilePath = this.resolveModule(path.join(unit.path, fileName));
      if (!bootFilePath) {
        continue;
      }
      const bootHook = this.requireFile(bootFilePath);
      // bootHook 是加载的文件
      if (is.class(bootHook)) {
        // if is boot class, add to lifecycle
        this.lifecycle.addBootHook(bootHook);
      } else if (is.function(bootHook)) {
        // if is boot function, wrap to class
        // for compatibility
        this.lifecycle.addFunctionAsBootHook(bootHook);
      } else {
        this.options.logger.warn('[egg-loader] %s must exports a boot class', bootFilePath);
      }
    }
    // init boots
    this.lifecycle.init();
    this.timing.end(`Load ${fileName}.js`);
  },
复制代码

从上可知** bootHook** 对应的就是加载的文件,从上面的 if else 可知, app.js 必须暴露出来的是一个 class 或者是一个 function ,然后调用 this.lifecycle.addFunctionAsBootHook(bootHook); , 其代码如下:

addFunctionAsBootHook(hook) {
    assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized');
    // app.js is export as a funciton
    // call this function in configDidLoad
    this[BOOT_HOOKS].push(class Hook {
      constructor(app) {
        this.app = app;
      }
      configDidLoad() {
        hook(this.app);
      }
    });
  }
复制代码

将对应的hook push 到this.lifecycle 的 BOOT_HOOKS 数组中, 并且包装成了一个类, 且在 configDidLoad 调用对应的hook.然后调用了 this.lifecycle.init(); 去初始化生命周期:

init() {
    assert(this[INIT] === false, 'lifecycle have been init');
    this[INIT] = true;
    this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app));
    this[REGISTER_BEFORE_CLOSE]();
  }
复制代码

这个 init 方法做了三件事情:

  • 将lifecycle 的INIT 状态标记为: true
  • 将BOOT_HOOKS 对应的类, 实例化一个对象,保存在 BOOTS
  • 调用REGISTER_BEFORE_CLOSE方法,其中会调用我们的hook 的 beforeClose 方法。

this.loadCustomApp(); 方法如下:

loadCustomApp() {
    this[LOAD_BOOT_HOOK]('app');
    this.lifecycle.triggerConfigWillLoad();
  },
复制代码

所以接下执行 this.lifecycle.triggerConfigWillLoad();

triggerConfigWillLoad() {
    for (const boot of this[BOOTS]) {
      if (boot.configWillLoad) {
        boot.configWillLoad();
      }
    }
    this.triggerConfigDidLoad();
  }

  triggerConfigDidLoad() {
    for (const boot of this[BOOTS]) {
      if (boot.configDidLoad) {
        boot.configDidLoad();
      }
    }
    this.triggerDidLoad();
  }
复制代码

其中 boot.configDidLoad(); 就是我们app.js 定义的hook, 被加工成的Hook 类:

class Hook {
      constructor(app) {
        this.app = app;
      }
      configDidLoad() {
        hook(this.app);
      }
    }
复制代码

然后就将app.js 与eggjs 关联起来了。

this.loadService();

查找的your_project_name/app/service/ .js, 然后将文件名称作为一个作为属性,挂载在 context**上下文上,然后将对应的js 文件,暴露的方法赋值在这个属性上, 比如说我们在如下路径下: your_project_name/app/service/home.js , 其代码如下:

'use strict';

// app/service/home.js
const Service = require('egg').Service;

class HomeService extends Service {
  async find() {
    // const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    const user = [
      {
        name: 'Ivan Fan',
        age: 18,
      },
    ];
    return user;
  }
}

module.exports = HomeService;
复制代码

我们在其他的地方就可以通过: this.ctx.service.home.find() 方法调用service里面的方法了,如在controller 中调用:

'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
  async index() {
    // this.ctx.body = 'hi, egg';
    this.ctx.body = await this.ctx.service.home.find();
  }
}
module.exports = HomeController;
复制代码

this.loadController();

这个方法是去加载controller , 其代码如下:

loadController(opt) {
    this.timing.start('Load Controller');
    opt = Object.assign({
      caseStyle: 'lower',
      directory: path.join(this.options.baseDir, 'app/controller'),
      initializer: (obj, opt) => {
        // return class if it exports a function
        // ```js
        // module.exports = app => {
        //   return class HomeController extends app.Controller {};
        // }
        // ```
        if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj) && !is.asyncFunction(obj)) {
          obj = obj(this.app);
        }
        if (is.class(obj)) {
          obj.prototype.pathName = opt.pathName;
          obj.prototype.fullPath = opt.path;
          return wrapClass(obj);
        }
        if (is.object(obj)) {
          return wrapObject(obj, opt.path);
        }
        // support generatorFunction for forward compatbility
        if (is.generatorFunction(obj) || is.asyncFunction(obj)) {
          return wrapObject({ 'module.exports': obj }, opt.path)['module.exports'];
        }
        return obj;
      },
    }, opt);
    const controllerBase = opt.directory;

    this.loadToApp(controllerBase, 'controller', opt);
    this.options.logger.info('[egg:loader] Controller loaded: %s', controllerBase);
    this.timing.end('Load Controller');
  },
复制代码

其加载的路径是: app/controller 下面的js 文件。

this.loadRouter();

这个方法,顾名思义就是去加载router, 其代码如下:

loadRouter() {
    this.timing.start('Load Router');
    // 加载 router.js
    this.loadFile(this.resolveModule(path.join(this.options.baseDir, 'app/router')));
    this.timing.end('Load Router');
  },
复制代码

只会加载对应项目下的 app/router.js , 也就是路由应该只有一个入口文件.如下Demo:

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};

复制代码

如上代码实现路由。

TODO....


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Visual Thinking

Visual Thinking

Colin Ware / Morgan Kaufmann / 2008-4-18 / USD 49.95

Increasingly, designers need to present information in ways that aid their audiences thinking process. Fortunately, results from the relatively new science of human visual perception provide valuable ......一起来看看 《Visual Thinking》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具