内容简介:前端时间抽出时间针对上面是我对Koa 源码分析的一些简单的理解, 后面我会将对Koa 的理解,进一步的记录下来。 Koa 是一个很小巧灵活的框架, 不像Express, Express 已经集成了很多的功能, 很多功能不再需要第三方的框架,比如说路由功能, Koa 需要引用第三方的库koa-router 来实现路由等。但是express 则不需要,下面是Koa 和Express, 两个实现一个简单的功能的Demo , 我们可以比较下其使用方式:哈哈,我们上面说了很多的废话(文字表达能力问题), 其实我是想
前端时间抽出时间针对 Koa2 源码进行了简单的学习, koa 源码是一个很简单的库, 针对分析过程, 想手把手的实现一个类型 koa 的框架,其 代码 , 根据一步步的完善实现一个简单版本的Koa, 每一个步骤一个 Branch , 如: stpe-1 , 对应的是我想实现第一步的代码, 代码仅供自己简单的学习,很多地方不完善,只是想体验下Koa 的思想。下面几点是我对Koa 的简单理解:
- 所有的NodeJS 框架最基本的核心就是通过原生库
http
orhttps
启动一个后端服务http.createServer(this.serverCallBack()).listen(...arg)
, 然后所有的请求都会进入serverCallBack
方法, 然后我们可以通过拦截,在这个方法中处理不同的请求 - Koa 是一个洋葱模型, 其是基于中间件来实现的.通过
use
来添加一个中间件,koa-router
其实就是一个koa
的中间件,我们的所有的请求都会将所有的中间件都执行一遍,洋葱模型如下图所示
上面是我对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项目生成的相关代码。其代码文件结构如下:
分析整个文件结构,找了整个项目都没有发现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
,里面有一系列的命令,如下图:
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
两个文件,
master.js
是跟nodejs的多线程有关,我们先跳过这一块,直接研究 app_worker.js
文件,学习eggjs 的启动过程。下面我们就是app_worker.js 执行的主要步骤。
-
const Application = require(options.framework).Application;
, 引入eggjs 模块, optons.framework 指向的就是D:\private\your_project_name\node_modules\egg
-
const app = new Application(options);
(创建一个 egg 实例) -
app.ready(startServer);
调用egg 对象的** ready ** 方法,其startServer 是一个回调函数,其功能是调用nodejs 原生模块http
orhttps
的createServer
创建一个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)
我们将构造函数进行精简,代码如下
从上图可知,构造函数就是初始化了很多基础的属性,其中有两个属性很重要:
-
this.lifecycle
负责整个eggjs 实例的生命周期,我们后续会深入分析整个生命周期
-
this.loader
(egg-core/lib/loader/egg_loader.js)解决了eggjs 为什么在服务启动后,会自动加载,将项目路径下的router.js
,controller/**.js
, 以及service/**.js
绑定到app
实例上, 我们接下来会重点分析这个loader.
EggApplication(egg/lib/egg.js)
我们将构造函数进行精简,代码如下
这个构造函数同样也是初始化了很多基础的属性, 但是其中有调用 EggCore 构造函数初始化的 loader 的 loadConfig()
方法, 这个方法顾名思义就是去加载配置,其指向的是: 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.js
的 loadConfig()
方法, 其同样会加载四种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 ,我们改怎么操作呢?
我们应该有如下两种方式:
- 在执行npm run debug 命令时,添加相应的参数
- 我们可以在我们项目的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....
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Spark大数据分析技术与实战
董轶群、曹正凤、赵仁乾、王安 / 电子工业出版社 / 2017-7 / 59.00
Spark作为下一代大数据处理引擎,经过短短几年的飞跃式发展,正在以燎原之势席卷业界,现已成为大数据产业中的一股中坚力量。 《Spark大数据分析技术与实战》着重讲解了Spark内核、Spark GraphX、Spark SQL、Spark Streaming和Spark MLlib的核心概念与理论框架,并提供了相应的示例与解析。 《Spark大数据分析技术与实战》共分为8章,其中前4......一起来看看 《Spark大数据分析技术与实战》 这本书的介绍吧!