百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

英特尔架构师干货分享之Egg运行原理

ccwgpt 2024-10-12 02:43 29 浏览 0 评论

从egg创建工程起,并没有明确的入口文件,找了半天才大概明白,于是把过程写下来

参考https://github.com/SunShinewyf/issue-blog/issues/30

关于egg

egg是阿里开源的一个框架,为企业级框架和应用而生,相较于express和koa,有更加严格的目录结构和规范,使得团队可以在基于egg定制化自己的需求或者根据egg封装出适合自己团队业务的更上层框架

egg定位

天猪曾经在这篇优秀的博文中给出关于egg的定位,如下图:

egg.png

可以看到egg处于的是一个中间层的角色,基于koa,不同于koa以middleware为主要生态,egg根据不同的业务需求和场景,加入了plugin,extends等这些功能,可以让开发者摆脱在使用middleware功能时无法控制使用顺序的被动状态,而且还可以增加一些请求无关的一些功能。除此之外,egg还有很多其他优秀的功能,在这里不详述。想了解更多可以移步这里初始化项目

egg有直接生成整个项目的脚手架功能,只需要执行如下几条命令,就可以生成一个新的项目:

$ npm i egg-init -g
$ egg-init helloworld --type=simple
$ cd egg-helloworld
$ npm i

启动项目:

$ npm run dev
$ open localhost:7001

egg是如何运行起来的

下面通过追踪源码来讲解一下egg究竟是如何运行起来的:

查看egg-init脚手架生成的项目文件,可以看到整个项目文件是没有严格意义上的入口文件的,根据package.json中的script命令,可以看到执行的直接是egg-bin dev的命令。找到egg-bin文件夹中的dev.js,会看到里面会去执行start-cluster文件:

//dev.js构造函数中
this.serverBin = path.join(__dirname, '../start-cluster');
// run成员函数
* run(context) {
 //省略
 yield this.helper.forkNode(this.serverBin, devArgs, options);
}

移步到start-cluster.js文件,可以看到关键的一行代码:

require(options.framework).startCluster(options);

其中options.framework打印信息为:

/Users/wyf/Project/egg-example/node_modules/egg

找到对应的egg目录中的index.js文件:

exports.startCluster = require('egg-cluster').startCluster;

继续追踪可以看到最后运行的其实就是egg-cluster中的startCluster,并且会fork出agentWorker和appWorks,官方文档对于不同进程的fork顺序以及不同进程之间的IPC有比较清晰的说明,

主要的顺序如下:

  • Master 启动后先 fork Agent 进程
  • Agent 初始化成功后,通过 IPC 通道通知 Master
  • Master 再 fork 多个 App Worker
  • App Worker 初始化成功,通知 Master
  • 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功

通过代码逻辑也可以看出它的顺序:

//在egg-ready状态的时候就会执行进程之间的通信
this.ready(() => {
 //省略代码
 const action = 'egg-ready';
 this.messenger.send({ action, to: 'parent' });
 this.messenger.send({ action, to: 'app', data: this.options });
 this.messenger.send({ action, to: 'agent', data: this.options });
});
 
this.on('agent-exit', this.onAgentExit.bind(this));
this.on('agent-start', this.onAgentStart.bind(this));
this.on('app-exit', this.onAppExit.bind(this));
this.on('app-start', this.onAppStart.bind(this));
this.on('reload-worker', this.onReload.bind(this));
// fork app workers after agent started
this.once('agent-start', this.forkAppWorkers.bind(this));

通过上面的代码可以看出,master进程会去监听当前的状态,比如在检测到agent-start的时候才去fork AppWorkers,在当前状态为egg-ready的时候,会去执行如下的进程之间的通信:

master ---> parent
master ---> agent
master ---> app

fork出了appWorker之后,每一个进程就开始干活了,在app_worker.js文件中,可以看到进程启动了服务,具体代码:

//省略代码
function startServer() {
 let server;
 if (options.https) {
 server = require('https').createServer({
 key: fs.readFileSync(options.key),
 cert: fs.readFileSync(options.cert),
 }, app.callback());
 } else {
 server = require('http').createServer(app.callback());
 }
 //省略代码
}

然后就回归到koa中的入口文件干的事情了。

除此之外,每一个appWorker还实例化了一个Application:

const Application = require(options.framework).Application;
const app = new Application(options);

在实例化application(options)时,就会去执行node_modules->egg模块下面loader目录下面的逻辑,也就是agentWorker进程和多个appWorkers进程要去执行的加载逻辑,具体可以看到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(); // 依赖 controller
 }
}

这也是下面要讲的东西了

在真正执行业务代码之前,egg会先去干下面一些事情:

加载插件

egg中内置了如下一系列插件:

  • onerror 统一异常处理
  • Session Session 实现
  • i18n 多语言
  • watcher 文件和文件夹监控
  • multipart 文件流式上传
  • security 安全
  • development 开发环境配置
  • logrotator 日志切分
  • schedule 定时任务
  • static 静态服务器
  • jsonp jsonp 支持
  • view 模板引擎

加载插件的逻辑是在egg-core里面的plugin.js文件,先看代码:

loadPlugin() {
 //省略代码
 //把本地插件,egg内置的插件以及app的框架全部集成到allplugin中
 this._extendPlugins(this.allPlugins, eggPlugins);
 this._extendPlugins(this.allPlugins, appPlugins);
 this._extendPlugins(this.allPlugins, customPlugins);
 
 //省略代码
 //遍历操作
 for (const name in this.allPlugins) {
 const plugin = this.allPlugins[name];
 //对插件名称进行一些校验
 this.mergePluginConfig(plugin);
 //省略代码
 }
 if (plugin.enable) {
 //整合所有开启的插件
 enabledPluginNames.push(name);
 }
 }

如上代码(只是贴出了比较关键的地方),这段代码主要是将本地插件、egg中内置的插件以及应用的插件进行了整合。其中this.allPlugins的结果如下:

egg2.png

可以看出,this.allPlugins包含了所有内置的插件以及本地开发者自定义的插件。先获取所有插件的相关信息,然后将所有插件进行遍历,执行this.mergePluginConfig()函数,这个函数主要是对插件名称进行一些校验。之后还对项目中已经开启的插件进行整合。plugin.js文件还做了一些其他事情,比如获取插件路径,读取插件配置等等,这里不一一讲解。

扩展内置对象

包括插件里面定义的扩展以及开发者自己写的扩展,这也是这里讲的内容。

在对内置对象进行扩展的时候,实质上执行的是extend.js文件,扩展的对象包括如下几个:

  • Application
  • Context
  • Request
  • Response
  • Helper

通过阅读extend.js文件可以知道,其实最后每个对象的扩展都是直接调用的loadExtends这个函数。拿Application这个内置对象进行举例:

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);
 }
},

将filepaths进行打印,如下图:

egg3.png

可以看出,filepaths包含所有的对application扩展的文件路径,这里会首先将所有插件中扩展或者开发者自己自定义的扩展文件的路径获取到,然后进行遍历,并且对内置对象的一些原有属性和扩展属性进行合并,此时对内置对象扩展的一些属性就会添加到内置对象中。所以在执行业务代码的时候,就可以直接通过访问application.属性(或方法)进行调用。

加载中间件

对中间件的加载主要是执行的egg-core中的middleware.js文件,里面的代码思想也是和上面加载内置对象是一样的,也是将插件中的中间件和应用中的中间件路径全部获取到,然后进行遍历。

遍历完成之后执行中间件就和koa一样了,调用co进行包裹遍历。

加载控制器

对控制器的加载主要是执行的egg-core中的controller.js文件

egg的官方文档中,插件的开发这一节提到:

插件没有独立的 router 和 controller

所以在加载controller的时候,主要是load应用里面的controller即可。详见代码;

loadController(opt) {
 opt = Object.assign({
 caseStyle: 'lower',
 directory: path.join(this.options.baseDir, 'app/controller'),
 initializer: (obj, opt) => {
 if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj)) {
 obj = obj(this.app);
 }
 if (is.promise(obj)) {
 const displayPath = path.relative(this.app.baseDir, opt.path);
 throw new Error(`${displayPath} cannot be async function`);
 }
 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);
 }
 if (is.generatorFunction(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);
},

这里主要是针对controller的类型进行判断(是否是Object,class,promise,generator),然后分别进行处理

加载service

加载service的逻辑是egg-core中的service.js,service.js这个文件比较简单,代码如下:

loadService(opt) {
 // 载入到 app.serviceClasses
 opt = Object.assign({
 call: true,
 caseStyle: 'lower',
 fieldClass: 'serviceClasses',
 directory: this.getLoadUnits().map(unit => path.join(unit.path, 'app/service')),
 }, opt);
 const servicePaths = opt.directory;
 this.loadToContext(servicePaths, 'service', opt);
 },

首先也是先获取所有插件和应用中声明的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的更新,这里有一段代码:

// define ctx.service
Object.defineProperty(app.context, property, {
 get() {
 // distinguish property cache,
 // cache's lifecycle is the same with this context instance
 // e.x. ctx.service1 and ctx.service2 have different cache
 if (!this[CLASSLOADER]) {
 this[CLASSLOADER] = new Map();
 }
 const classLoader = this[CLASSLOADER];
 
 //先判断是否有使用
 let instance = classLoader.get(property);
 if (!instance) {
 instance = getInstance(target, this);
 classLoader.set(property, instance);
 }
 return instance;
 },
});

在更新service的时候,首先会去获取service是否挂载在ctx中,如果没有,则直接返回,否则实例化service,这也就是service模块中的延迟实例化

加载路由

加载路由的逻辑主要是egg-core中的router.js文件

loadRouter() {
 // 加载 router.js
 this.loadFile(path.join(this.options.baseDir, 'app/router.js'));
},

可以看出很简单,只是加载应用文件下的router.js文件

加载配置

直接加载配置文件并提供可配置的方法。

设置应用信息

对egg应用信息的设置逻辑是对应的egg-core中的egg-loader.js,里面主要是提供一些方法获取整个app的信息,包括appinfo,name,path等,比较简单,这里不一一列出

执行业务逻辑

然后就会去执行如渲染页面等的逻辑

总结

这里只是我个人针对源代码以及断点调试总结的一些东西.

转发+关注,私信“私聊”即可获取内部资源分享!

相关推荐

详解DNFSB2毒王的各种改动以及大概的加点框架

首先附上改动部分,然后逐项分析第一个,毒攻掌握技能意思是力量智力差距超过15%的话差距会被强行缩小到15%,差距不到15%则无效。举例:2000力量,1650智力,2000*0.85=1700,则智力...

通篇干货!纵观 PolarDB-X 并行计算框架

作者:玄弟七锋PolarDB-X面向HTAP的混合执行器一文详细说明了PolarDB-X执行器设计的初衷,其初衷一直是致力于为PolarDB-X注入并行计算的能力,兼顾TP和AP场景,逐渐...

字节新推理模型逆袭DeepSeek,200B参数战胜671B,豆包史诗级加强

梦晨发自凹非寺量子位|公众号QbitAI字节最新深度思考模型,在数学、代码等多项推理任务中超过DeepSeek-R1了?而且参数规模更小。同样是MoE架构,字节新模型Seed-Thinkin...

阿里智能化研发起飞!RTP-LLM 实现 Cursor AI 1000 token/s 推理技术揭秘

作者|赵骁勇阿里巴巴智能引擎事业部审校|刘侃,KittyRTP-LLM是阿里巴巴大模型预测团队开发的高性能LLM推理加速引擎。它在阿里巴巴集团内广泛应用,支撑着淘宝、天猫、高德、饿...

多功能高校校园小程序/校园生活娱乐社交管理小程序/校园系统源码

校园系统通常是为学校、学生和教职工提供便捷的数字化管理工具。综合性社交大学校园小程序源码:同城校园小程序-大学校园圈子创业分享,校园趣事,同校跑腿交友综合性论坛。小程序系统基于TP6+Uni-app...

婚恋交友系统nuiAPP前端解决上传视频模糊的问题

婚恋交友系统-打造您的专属婚恋交友平台系统基于TP6+Uni-app框架开发;客户移动端采用uni-app开发,管理后台TH6开发支持微信公众号端、微信小程序端、H5端、PC端多端账号同步,可快速打包...

已节省数百万GPU小时!字节再砍MoE训练成本,核心代码全开源

COMET团队投稿量子位|公众号QbitAI字节对MoE模型训练成本再砍一刀,成本可节省40%!刚刚,豆包大模型团队在GitHub上开源了叫做COMET的MoE优化技术。COMET已应用于字节...

通用电气完成XA102发动机详细设计审查 将为第六代战斗机提供动力

2025年2月19日,美国通用电气航空航天公司(隶属于通用电气公司)宣布,已经完成了“下一代自适应推进系统”(NGAP)计划下提供的XA102自适应变循环发动机的详细设计审查阶段。XA102是通用电气...

tpxm-19双相钢材质(双相钢f60材质)

TPXM-19双相钢是一种特殊的钢材,其独特的化学成分、机械性能以及广泛的应用场景使其在各行业中占有独特的地位。以下是对TPXM-19双相钢的详细介绍。**化学成分**TPXM-19双相钢的主要化学成...

thinkphp6里怎么给layui数据表格输送数据接口

layui官网已经下架了,但是产品还是可以使用。今天一个朋友问我怎么给layui数据表格发送数据接口,当然他是学前端的,后端不怎么懂,自学了tp框架问我怎么调用。其实官方文档上就有相应的数据格式,js...

完美可用的全媒体广告精准营销服务平台PHP源码

今天测试了一套php开发的企业网站展示平台,还是非常不错的,下面来给大家说一下这套系统。1、系统架构这是一套基于ThinkPHP框架开发的HTML5响应式全媒体广告精准营销服务平台PHP源码。现在基于...

一对一源码开发,九大方面完善基础架构

以往的直播大多数都是一对多进行直播社交,弊端在于不能满足到每个用户的需求,会降低软件的体验感。伴随着用户需求量的增加,一对一直播源码开始出现。一个完整的一对一直播流程即主播发起直播→观看进入房间观看→...

Int J Biol Macromol .|交联酶聚集体在分级共价有机骨架上的固定化:用于卤代醇不对称合成的高稳定酶纳米反应器

大家好,今天推送的文章发表在InternationalJournalofBiologicalMacromolecules上的“Immobilizationofcross-linkeden...

【推荐】一款开源免费的 ChatGPT 聊天管理系统,支持PC、H5等多端

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍GPTCMS是一款开源且免费(基于GPL-3.0协议开源)的ChatGPT聊天管理系统,它基于先进的GPT...

高性能计算(HPC)分布式训练:训练框架、混合精度、计算图优化

在深度学习模型愈发庞大的今天,分布式训练、高效计算和资源优化已成为AI开发者的必修课。本文将从数据并行vs模型并行、主流训练框架(如PyTorchDDP、DeepSpeed)、混合精度训练(...

取消回复欢迎 发表评论: