egg框架啟動分析

egg框架啟動分析

egg框架目前已經(jīng)使用了大半年醒叁。框架因koa特色泊业、強約束的feel把沼、“為了毫無保留榨干服務(wù)器性能”的slogan,脫穎而出∮跛牛框架使用大半年挺舒適饮睬,入手坡度不算太高。學(xué)而不思則罔篮奄,對這階段的也就和工作內(nèi)容做簡要總結(jié)捆愁,如果有任何錯誤,還請積極指正窟却,感謝昼丑。

有不少文章已經(jīng)對相關(guān)內(nèi)容作了闡述,感謝相關(guān)作者间校。值得深讀

漫談 egg-bin
Egg 源碼分析之 egg-cluster
co庫相關(guān)實現(xiàn)

所謂萬事開頭難矾克,對于egg的分析我也從其啟動上著手,egg框架生態(tài)十分完整憔足,致使閱讀代碼又愛又恨胁附,愛其結(jié)構(gòu)恨其調(diào)用關(guān)系鏈(懂了就好了),從啟動方式切入分析滓彰,主體引用鏈為

egg-bin -> egg-cluster -> egg -> egg-core

文章按照啟動鏈的順序?qū)gg-bin和egg-cluster做分析控妻。

寫在前面

文章著重對egg的啟動構(gòu)建做梳理總結(jié),建議把相關(guān)源碼下載下來揭绑,在node_modules下看難度有點大弓候,時常進去后出不來了。應(yīng)用層的使用脫離不了對架構(gòu)層的理解他匪。有任何問題還請告知菇存。

egg-bin

egg-bin基于common-bin(相關(guān)介紹源tao_npm,是node_modules中對于bin相關(guān)操作的共用組件庫),common-bin其重要地位可想而知邦蜜。發(fā)現(xiàn)大師的寫法也著實厲害依鸥,兩個文件足以覆蓋意蘊,command.js + helper.js完成任務(wù)悼沈。

egg-bin框架作為啟動指令包贱迟,需要對指令參數(shù)進行有效解讀姐扮,包內(nèi)使用yargs解析參數(shù),官方文檔對egg-bin的使用作了有效講解衣吠,相關(guān)地址,我們在開發(fā)的時候經(jīng)常會用到的指令均來自與此依賴庫茶敏。“egg-bin dev”作為開發(fā)啟動指令重重之中缚俏,做詳細分析惊搏。

egg-bin dev
egg-bin cov
egg-bin test
...

package.json文件中對egg-bin指令作了文件指向

"bin": {
    "egg-bin": "bin/egg-bin.js",
    ...
},

相關(guān)文件也寫的相當(dāng)完美,掛載了所有index.js文件的暴露文件袍榆。

bin/egg-bin.js文件內(nèi)容


const Command = require('..');

new Command().start();

./index.js文件內(nèi)容

class EggBin extends Command {
  constructor(rawArgv) {
    super(rawArgv);
    this.usage = 'Usage: egg-bin [command] [options]';

    // load directory
    this.load(path.join(__dirname, 'lib/cmd'));
  }
}

在依賴庫的index下胀屿,有相關(guān)exports.當(dāng)執(zhí)行指令時,‘dev’參數(shù)將作為process.argv進入到common-bin/command.js的構(gòu)造函數(shù)中塘揣,

class CommonBin {
    constructor(rawArgv) {
        this.rawArgv = rawArgv || process.argv.slice(2);
        ...
        this.yargs = yargs(this.rawArgv);
        ...
    }
}

process.argv的相關(guān)介紹請參考node官方文檔,執(zhí)行后argv參數(shù)樣式基本為

 this.rawArgv = [
     "dev"
 ]

剩下的都是內(nèi)部參數(shù)調(diào)整包裝包雀,接著看上文index.js的第二步,this.load方法亲铡,繼承關(guān)系才写,實現(xiàn)在common-bin/command.js

// fullPath value is 'lib/cmd'
load(fullPath) {
    ...
    // load entire directory
    const files = fs.readdirSync(fullPath);
    const names = [];
    for (const file of files) {
      if (path.extname(file) === '.js') {
        const name = path.basename(file).replace(/\.js$/, '');
        names.push(name);
        this.add(name, path.join(fullPath, file));
      }
    }
}

簡單說,這個地方掃描了lib/cmd下所有js文件奖蔓,將其名稱正則后赞草,KV形式加入到對應(yīng)序列用作存取使用。this.add就是一個this[COMMANDS]的set方法吆鹤,不粘貼了厨疙。

再然后便是new Commnad().start()方法,自然也是在common-bin包下疑务,不廢話沾凄,上代碼

 start() {
    co(function* () {
      const index = this.rawArgv.indexOf('--get-yargs-completions');
      if (index !== -1) {
        this.rawArgv.splice(index, 2, `--AUTO_COMPLETIONS=${this.rawArgv.join(',')}`);
      }
      yield this[DISPATCH]();
    }.bind(this)).catch(this.errorHandler.bind(this));
  }

大名鼎鼎的co包還是很有魅力的,具體請文章序幕文章知允。重要的在yield this[DISPATCH]()下撒蟀,繼續(xù)上代碼

* [DISPATCH]() {
    // if sub command exist
    if (this[COMMANDS].has(commandName)) {
      const Command = this[COMMANDS].get(commandName);
      ...
      const command = this.getSubCommandInstance(Command, rawArgv);
      yield command[DISPATCH]();
      return;
    }
    ...
    // print completion for bash
    if (context.argv.AUTO_COMPLETIONS) {
     ...
    } else {
      // handle by self
      yield this.helper.callFn(this.run, [ context ], this);
    }
}

其實說到底,也是去實例化了子實例温鸽,也就是本文的/lib/cmd/dev.js[DISPATCH]方法(在此前this[COMMANDS]的set方法還記得嘛),cmd/dev.js不實現(xiàn)DISPATCH保屯,因此直接跳過。然后到最后去執(zhí)行cmd/dev.js的run()

  * run(context) {
    ...
    yield this.helper.forkNode(this.serverBin, devArgs, options);
  }
  
  constructor(){
    ...
    this.serverBin = path.join(__dirname, '../start-cluster');
  }

很明顯啦涤垫,forkNode(先按照child_process.fork去理解),對應(yīng)的start-cluster的內(nèi)容為

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

而option.framework指的就是egg項目姑尺,egg的index.js文件中也指出

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

因此指向調(diào)用的就是egg-cluster啦。

至此告一段落蝠猬。egg-bin的dev指令流程也簡單的梳理了一下切蟋,得益于common-bin,使得egg-bin的代碼不是那么多層級吱雏,當(dāng)然egg-bin不止只有dev一個使用對象敦姻,其他的瘾境,下篇文章再做分析吧~

稍微休息一下,然后繼續(xù)吧..镰惦、

egg-cluster

egg-cluster的啟動繼承ready+events模式迷守,關(guān)于get-ready,這里不做過多敘述旺入,直接上代碼

const ready = require('get-ready');

const obj = {};
ready.mixin(obj);

// register a callback
obj.ready(() => console.log('ready func 1'));
obj.ready(() => console.log('ready func 2'));

console.log('ready1');
// mark ready
obj.ready(true);
console.log('ready2');

因此得出的console日志如下

/usr/local/bin/node ../ready.js
ready1
ready2
ready func 1
ready func 2

Process finished with exit code 0

結(jié)果很明顯了兑凿,對于mixin的對象,ready方法有鏈?zhǔn)叫Ч瘃虼嗽趀gg-cluster/index.js中有這么一句

exports.startCluster = function(options, callback){
  new Master(options).ready(callback);
};

而作為Master.js的constructor上有這么一段

class Master extends EventEmitter{
    constructor(){
        ...
        this.ready(() => {
            ...
        });
    }
}

因此cluster將會先執(zhí)行構(gòu)造方法里面的ready礼华,然后暴露到callback對象上。至于events呢拗秘,注意listener/on/once/emit方法即可圣絮,具體還請移步大師講述官方文檔

在master.js的constructor上雕旨,把關(guān)于當(dāng)前的調(diào)查信息羅列出來

class Master extends EventEmitter{
    constructor(options) {
        ...
        this.ready(() => {
            ...
            const action = 'egg-ready';
            this.messenger.send({ action, to: 'parent', data: { ... });
            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));
        ...
        detectPort((err, port) => {
            ...
            this.options.clusterPort = port;
            this.forkAgentWorker();
        });
     }
}

這里又有一個核心大佬放出的detect-portgithub倉庫這個包的作用就是在做一系列規(guī)范化檢查后使用net.createServer嘗試創(chuàng)建查看端口是否可用扮匠,如果不可用端口+1(使用時7001被占用系統(tǒng)會啟動在7002也是因為這個端口預(yù)創(chuàng)建測試放出探針結(jié)果)自然在master.constructor上的cb方法就祈禱作用了,會調(diào)用master.js#forkAgentWorker凡涩,這個方法主要以child_process.fork啟動了Agent棒搜,Agent繼承鏈,Agent >> Egg.Agent >> Egg.EggApplication活箕。

這里還得提前插一嘴力麸,關(guān)于messenger,因為接下來的調(diào)用都和消息有關(guān),而egg的內(nèi)部消息傳輸方向有相關(guān)規(guī)定,且傳輸消息均由messenger承擔(dān)育韩。

messengermaster.js的constructor階段創(chuàng)建克蚂,其作用指向便是消息傳輸,在源碼注釋里文件里面有個圖

 * master messenger,provide communication between parent, master, agent and app.
 *
 *             ┌────────┐
 *             │ parent │
 *            /└────────┘\
 *           /     |      \
 *          /  ┌────────┐  \
 *         /   │ master │   \
 *        /    └────────┘    \
 *       /     /         \    \
 *     ┌───────┐         ┌───────┐
 *     │ agent │ ------- │  app  │
 *     └───────┘         └───────┘

作為調(diào)用端座慰,也就是當(dāng)前請求startCluster的對象陨舱,擁有對process的動作監(jiān)聽,其便是(parent)版仔,在egg-clusterindex.js里創(chuàng)建了master游盲,在master中依靠接下來的順序創(chuàng)建了agentapp,于是四者便連同在一起蛮粮。在messenger中益缎,作為消息的發(fā)送傳遞者,有相關(guān)數(shù)據(jù)指向

調(diào)用方法 發(fā)送對象 to 備注
sendToMaster this.master.emit master
sendToParent process.send parent
sendToAppWorker sendMessage app data里receiverPid是全部app_worker的pid
sendToAgentWorker sendmessage agent

Q: master和parent區(qū)別是什么然想?
從流程上來看莺奔,parent(current process)上創(chuàng)建master

先放出一張啟動順序的消息傳輸圖片,記錄下啟動順序(非全量記錄)

msg_event.jpg

按照這個順序变泄,我們詳述

0. 放出探針(detect-port),獲取可用端口

1. (序號1 --> 3)端口獲取成功 發(fā)送’agent-start‘消息內(nèi)容

和上面貼出代碼一致令哟,this.forkAgentWorker()方法恼琼,會調(diào)用入child_processfork文件agent_worker.js

const Agent = require(options.framework).Agent;
const agent = new Agent(options);
agent.ready(err => {
  // don't send started message to master when start error
  if (err) return;
  ...
  process.send({ action: 'agent-start', to: 'master' });
});

這里比較好理解,目前已知option.framework就是/{項目目錄}/node_module/egg,因此屏富,這fork的就是egg.Agent,當(dāng)fork完畢后晴竞,Agent基于events調(diào)用this.ready(true),便會process發(fā)出消息agent-start指向master,而為子進程的agent的消息事件,在master上早就等著你了狠半,看這master.js

this.on('agent-start', this.onAgentStart.bind(this));
...
this.once('agent-start', this.forkAppWorkers.bind(this));

on和once的區(qū)別就不講了噩死,當(dāng)首次觸發(fā)agent-start時,回調(diào)到第二行代碼中神年,執(zhí)行forkAppWorker方法已维,至此,agent啟動完畢已日,接下來就是啟動worker(app)垛耳。于是乎,很順利的就跳轉(zhuǎn)回master.js去執(zhí)行接下來的邏輯鏈了捂敌。這里我嚴重艾扮、嚴重既琴、嚴重提示一下占婉,對于創(chuàng)建agent的,是childprocess.fork()甫恩,先留著逆济,以后做分析。

觸發(fā)到forkAppworker方法磺箕,貼出大概代碼

 forkAppWorkers() {
    cfork({
      exec: this.getAppWorkerFile(),
      ...
    });
    cluster.on('fork', worker => {
        ...
    });
    cluster.on('disconnect', worker => {
        ...
    });
    cluster.on('exit', (worker, code, signal) => {
        ...
    });
    cluster.on('listening', (worker, address) => {
      this.messenger.send({
        action: 'app-start',
        data: { workerPid: worker.process.pid, address },
        to: 'master',
        from: 'app',
      });
    });
  }

自然和fork出Agent類似奖慌,用了cfork去操作(還是一樣,cfork做分篇介紹)松靡,當(dāng)fork監(jiān)聽途中简僧,會觸發(fā)放出app-start消息至master,至此雕欺,完成序號1 -> 序號3的消息發(fā)送岛马。

我們趁熱打鐵,繼續(xù)走...

2. (序號3 --> 4)agent啟動成功屠列,發(fā)送’app-start‘消息內(nèi)容

當(dāng)收到前序(序號3:action = 'app-start')啦逆,查找master中相關(guān)監(jiān)聽實現(xiàn)

this.on('app-start', this.onAppStart.bind(this));
onAppStart(data) {

    ...
     const remain = this.isAllAppWorkerStarted ? 0 : this.options.workers - this.startSuccessCount;
    ...
    // send message to agent with alive workers
    this.messenger.send({
      action: 'egg-pids',
      to: 'agent',
      data: this.workerManager.getListeningWorkerIds(),
    });
    ...
    // Send egg-ready when app is started after launched
    if (this.isAllAppWorkerStarted) {
      this.messenger.send({ action: 'egg-ready', to: 'app', data: this.options });
    }
    ...
    this.isAllAppWorkerStarted = true;
    ...
    if (this.options.sticky) {
      this.startMasterSocketServer(err => {
        if (err) return this.ready(err);
        this.ready(true);
      });
    } else {
      this.ready(true);
    }
}

我們能看到,在app啟動方法中會發(fā)送egg-pids消息至agent中笛洛,并且判斷isAllAppworkerStarted夏志,確認都啟動后放出消息egg-ready至app

關(guān)于egg-pids,找了一大圈沒有發(fā)現(xiàn)監(jiān)聽方法苛让,在/test/fixture下有幾個測試demo是對其做了監(jiān)聽沟蔑,在egg包中湿诊,ipc有個Messenger做了監(jiān)聽,因此推測當(dāng)前消息是暴露給相關(guān)周邊開發(fā)者瘦材,更有效獲取pid用途,或者是更高深層層次的用法枫吧,我這個小白未能領(lǐng)悟到精神。

引用一下文章序中Egg 源碼分析之 egg-cluster說的比較重要的內(nèi)容

sticky 模式:Master 負責(zé)統(tǒng)一監(jiān)聽對外端口宇色,然后根據(jù)用戶 ip 轉(zhuǎn)發(fā)到固定的 Worker 子進程上九杂,每個 Worker 自己啟動了一個新的本地服務(wù)

非 sticky 模式:每個Worker 都統(tǒng)一啟動服務(wù)監(jiān)聽外部端口

繼續(xù)看,出發(fā)了ready(),監(jiān)聽事件

this.ready(() => {
    ...
    const action = 'egg-ready';
    this.messenger.send({ action, to: 'parent', data: { port: this[REALPORT], address: this[APP_ADDRESS] } });
    this.messenger.send({ action, to: 'app', data: this.options });
    this.messenger.send({ action, to: 'agent', data: this.options });

    // start check agent and worker status
    if (this.isProduction) {
        this.workerManager.startCheck();
    }
});

至此宣蠕,在cluster包下的啟動主流程粗略梳理了一下例隆。大家可以緩口氣,剩下的就是消息egg-ready消息在Agent抢蚀、App(Worker)镀层、Master中分別作了什么呢?這得看下一個包-egg皿曲。

egg & egg-core

打個比方唱逢,如果說整個框架組成了一個屋子,egg-bin可以說是門,egg-cluster是客廳,我想egg就是臥室了吧,egg的重要可想而知,對于Master屋休、Agent坞古、和Worker(App),其中Master在egg-cluster中定義劫樟,Agent和Worker皆為egg包下的繼承自EggApplication對象痪枫,
上幾個篇幅已經(jīng)簡單說明了Master的初始化創(chuàng)建情況,接下來我們繼續(xù)聊聊Agent和Worker叠艳。

egg.Agent

agent在官方文章中有相關(guān)定義:

Agent 好比是 Master 給其他 Worker 請的一個『秘書』奶陈,它不對外提供服務(wù),只給 App Worker 打工附较,專門處理一些公共事務(wù)

Agent 雖然是 App Worker 的『小秘』吃粒,但是業(yè)務(wù)相關(guān)的工作不應(yīng)該放到 Agent 上去做,不然把她累垮了就不好了

由于 Agent 的特殊定位拒课,我們應(yīng)該保證它相對穩(wěn)定徐勃。當(dāng)它發(fā)生未捕獲異常,框架不會像 App Worker 一樣讓他退出重啟捕发,而是記錄異常日志疏旨、報警等待人工處理

結(jié)合官方文檔的定義,Agent有如下特點:輕量扎酷、捕獲異常檐涝、少量業(yè)務(wù)代碼

我們先看下Agent是怎么實現(xiàn)的:

Agent對象在egg-cluster創(chuàng)建環(huán)節(jié)中被創(chuàng)建出來,繼承自egg.Agent對象,該對象繼承EggApplication,且loader為./lib/loader/agent_worker_loader.js文件谁榜,繼承自egg-core.eggLoader對象幅聘,

整體繼承鏈如上,繼續(xù)梳理窃植。
我們先看到對象的constructor
./lib/agent.js

class Agent extends EggApplication {

  constructor(options = {}) {
    options.type = 'agent';
    super(options);

    this.loader.load();

    // dump config after loaded, ensure all the dynamic modifications will be recorded
    
    this.dumpConfig();
    
    // keep agent alive even it doesn't have any io tasks
    setInterval(() => {}, 24 * 60 * 60 * 1000);

    this._uncaughtExceptionHandler = this._uncaughtExceptionHandler.bind(this);
    process.on('uncaughtException', this._uncaughtExceptionHandler);
  }
}

整個構(gòu)造方法做了四件事

  1. 完成父類構(gòu)建帝蒿、[seq1]
  2. 驅(qū)動loader執(zhí)行l(wèi)oad方法,[seq2]
  3. dump相關(guān)配置文件進入./run目錄下巷怜、[seq3]
  4. 監(jiān)聽異常事件葛超。 [seq4]

steo0. 有些話得說在前面

為什么說有些東西要在前面說明,因為在閱讀源碼的時候延塑,發(fā)現(xiàn)這是最基類的設(shè)計調(diào)用绣张,我先做告知,方便理解关带。

Q1:loader是個什么侥涵?
loader加載器在egg-core包下有相關(guān)實現(xiàn)定義,翻看源碼大致能看到這么個東西 egg-core/lib/loader/egg-loader.js

...
const loaders = [
  require('./mixin/plugin'),
  require('./mixin/config'),
  require('./mixin/extend'),
  require('./mixin/custom'),
  require('./mixin/service'),
  require('./mixin/middleware'),
  require('./mixin/controller'),
  require('./mixin/router'),
  require('./mixin/custom_loader'),
];

for (const loader of loaders) {
  Object.assign(EggLoader.prototype, loader);
}

module.exports = EggLoader;

很明顯能看到宋雏,當(dāng)前export對象做了mixin,這也是我感覺egg這么靈活的優(yōu)點芜飘。我們能夠看到作為外圍暴露的loader對象,將會擁有./mixin/[xxxx].js(表中增加的)所有function磨总。所以在子類\實現(xiàn)類下可以隨意調(diào)用任何位置的func嗦明,如果有l(wèi)oader方法想查找實現(xiàn),需要尋找到相關(guān)對象(一般從名字上能分辨出來)舍败。

Q2:loader的指向?
我在閱讀源碼的時候就會發(fā)現(xiàn),agent其實使用agent_loader,而原來本身也有l(wèi)oader,那在EggApplication里面調(diào)用的基礎(chǔ)Egg-Core.loader到底是哪個呢?
答:因為是Symbol,是最外圍定義的那個對象.如果不確定的地方建議debug一下就了解了.


分割線,讓我們繼續(xù)

step1. 我們先看下在父類做了什么./lib/egg.js(Agent和App都繼承當(dāng)前對象)

父類劍指EggApplication文件招狸,在EggApplication的constructor上,摘要部分信息 ./lib/egg.js

constructor(options = {}) {
    options.mode = options.mode || 'cluster';
    super(options);
    ...
    this.loader.loadConfig();
    ...
    this.ready(() => process.nextTick(() => {
      const dumpStartTime = Date.now();
      this.dumpConfig();
      this.dumpTiming();
    }));
    ...
    this._unhandledRejectionHandler = this._unhandledRejectionHandler.bind(this);
    process.on('unhandledRejection', this._unhandledRejectionHandler);

    this[CLUSTER_CLIENTS] = [];

    this.cluster = (clientClass, options) => {
      options = Object.assign({}, this.config.clusterClient, options, {...});
      const client = cluster(clientClass, options);
      this._patchClusterClient(client);
      return client;
    };
}

為了簡化理解難度邻薯,我們先抓幾個重要的流程做分析(來自1步驟,這里編號seq1)

  1. super方法 [seql1.1]
  2. 驅(qū)使loader.loadConfig() [seq1.2]
  3. events的ready事件 [seq1.3]
  4. cluster的初始化 [seq1.4]

我們先跳過seq1.1(不然量太大了乘凸,扛不住哈哈)厕诡,回來后補,

seq1.2關(guān)于loader.loadConfig()是loader的mixin里面config.js(曾經(jīng)的我年輕了,這里有個指向問題,指向的其實不是egg-core.loader,需要先觀察創(chuàng)建這個對象有沒有相關(guān)指向,Agent有指向,是agent_work_loader,也是繼承了這個基類,但是需要看其多態(tài)復(fù)寫),相關(guān)實現(xiàn)請參考源碼营勤,agent_worker.js

class AgentWorkerLoader extends EggLoader {
  loadConfig() {
    this.loadPlugin();
    super.loadConfig();
  }
}

強制先執(zhí)行了loadPlugin(),加載各類插件,相關(guān)加載數(shù)據(jù)請參考源碼,egg-core/lib/mixin/plugin.js,我摘取幾個順序的方法調(diào)用

this._extendPlugins(this.allPlugins, eggPlugins);
this._extendPlugins(this.allPlugins, appPlugins);
this._extendPlugins(this.allPlugins, customPlugins);

可以看到,先加載了egg-framework本身的框架,然后用戶的app中實現(xiàn)插件和custom自定義插件

關(guān)于super.loadConfig()就完全是基類方法實現(xiàn)啦,簡述就是讀取了所有你的配置文件灵嫌,具體讀取順序參考源碼注釋,留作記錄葛作。

//   plugin config.default
//     framework config.default
//       app config.default
//         plugin config.{env}
//           framework config.{env}
//             app config.{env}

seq1.3當(dāng)有ready(true)的時候會觸發(fā)方法,目前沒有仔細去尋找,留作下一階段任務(wù),dumpConfig()
seq1.4是cluster的初始化,因為是在Application上的,所以無論是agent還是app都可以使用,這里的用途建議參考官方文檔-多進程增強模式cluster-client這兩個文檔,這里我也有很多疑惑,稍后回來鉆研.

step2. this.loader.load()方法

按照之前Q1的敘述,loader為加載器,但是有個特殊的時,這里的使用對象是個symbol,在相關(guān)的class的get/set方法上做了定義,因此在Agent里面,自然也要尋找相關(guān)定義值,如下圖
./lib/agent.js

const EGG_LOADER = Symbol.for('egg#loader');
const AgentWorkerLoader = require('./loader').AgentWorkerLoader;
...

class Agent extends EggApplication {
  ...
  get [EGG_LOADER]() {
    return AgentWorkerLoader;
  }
  ...
}

抽取相關(guān)比較重要的,可以看到,作為Agent,是使用了AgentWorkerLoader,因此我們打開一下這個文件
./lib/loader/agent_loader.js

'use strict';

const EggLoader = require('egg-core').EggLoader;

/**
 * Agent worker process loader
 * @see https://github.com/eggjs/egg-loader
 */
class AgentWorkerLoader extends EggLoader {

  /**
   * loadPlugin first, then loadConfig
   */
  loadConfig() {
    this.loadPlugin();
    super.loadConfig();
  }

  load() {
    this.loadAgentExtend();
    this.loadContextExtend();

    this.loadCustomAgent();
  }
}

module.exports = AgentWorkerLoader;

量不大,直接全復(fù)制過來了.我們能夠發(fā)現(xiàn),執(zhí)行的load()方法其實就是三個方法的合集,對于所有插件的執(zhí)行方法理解在其他文章中,稍后整理后放出

step3. dump相關(guān)配置

dumpConfig方法在agent調(diào)用了多次,相關(guān)源碼

 dumpConfig() {
    const rundir = this.config.rundir;
    try {
      /* istanbul ignore if */
      if (!fs.existsSync(rundir)) fs.mkdirSync(rundir);

      // get dumpped object
      const { config, meta } = this.dumpConfigToObject();

      // dump config
      const dumpFile = path.join(rundir, `${this.type}_config.json`);
      fs.writeFileSync(dumpFile, CircularJSON.stringify(config, null, 2));

      // dump config meta
      const dumpMetaFile = path.join(rundir, `${this.type}_config_meta.json`);
      fs.writeFileSync(dumpMetaFile, CircularJSON.stringify(meta, null, 2));
    } catch (err) {
      this.coreLogger.warn(`dumpConfig error: ${err.message}`);
    }
  }

關(guān)于里面dumpConfigToObject方法,實則是讀取了全局配置this.config + 所有plugins指向值,做成json輸出到文件,如下源碼所示

dumpConfigToObject() {
    let ignoreList;
    try {
      // support array and set
      ignoreList = Array.from(this.config.dump.ignore);
    } catch (_) {
      ignoreList = [];
    }

    const json = extend(true, {}, { config: this.config, plugins: this.plugins });
    utils.convertObject(json, ignoreList);
    return {
      config: json,
      meta: this.loader.configMeta,
    };
  }

自然,在egg框架啟動的run/${type}_config.json文件肯定就有啦,application和agent將會產(chǎn)生如下文件,meta對應(yīng)文件指向,config.json為配置值

  • {project_root}/run/
    • agent_config.json
    • agent_config_meta.json
    • application_config.json
    • application_config_meta.json

step4. 對異常的捕獲

Agent的作用其中一點就是捕獲異常,對于node異常,可以對全局Process監(jiān)聽uncaughtException事件,做出處理.
egg上加了對日志的打印邏輯處理,可自行查看源碼

至此,egg.Agent構(gòu)造流程梳理完畢.

egg.Application

app也就是worker(下面統(tǒng)一說app吧,方便理解).
app的相對于agent都是繼承同一個對象,業(yè)務(wù)方向能夠感受到更加針對通信,先翻出源碼
application.js

class Application extends EggApplication {

  /**
   * @class
   * @param {Object} options - see {@link EggApplication}
   */
  constructor(options = {}) {
    options.type = 'application';
    super(options);

    // will auto set after 'server' event emit
    this.server = null;

    try {
      this.loader.load();
    } catch (e) {
      // close gracefully
      this[CLUSTER_CLIENTS].forEach(cluster.close);
      throw e;
    }

    // dump config after loaded, ensure all the dynamic modifications will be recorded
    const dumpStartTime = Date.now();
    this.dumpConfig();
    this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime));

    this[WARN_CONFUSED_CONFIG]();
    this[BIND_EVENTS]();
  }
}

大體方向和agent差不多,具體可以產(chǎn)分成

  1. 父類構(gòu)建(之前淺談過了,這里就先跳過啦)
  2. loader的load方法 (Step.2)
  3. dumpConfig方法(Step.3)
  4. 觸發(fā) WARN_COUFUSED_CONFIG(Step.4)
  5. 觸發(fā) BIND_EVENTS(Step.5)

step2. loader.load

在app_worker_loader.js上,對load的定義比agent多點,也取決于設(shè)計理念的不同,相關(guān)源碼,還是一樣,對于插件的方法將會統(tǒng)一整理

class AppWorkerLoader extends EggLoader{
 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.loadCustomLoader();
  }
}

Step3. dumpConfig方法

這里的dumConfig指向的不再單純只是父類方法(Agent是直接應(yīng)用父類方法的.)config和plugins的相關(guān)信息
./application.js

  /**
   * save routers to `run/router.json`
   * @private
   */
  dumpConfig() {
    super.dumpConfig();

    // dump routers to router.json
    const rundir = this.config.rundir;
    const FULLPATH = this.loader.FileLoader.FULLPATH;
    try {
      const dumpRouterFile = path.join(rundir, 'router.json');
      const routers = [];
      for (const layer of this.router.stack) {
        routers.push({
          name: layer.name,
          methods: layer.methods,
          paramNames: layer.paramNames,
          path: layer.path,
          regexp: layer.regexp.toString(),
          stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'),
        });
      }
      fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2));
    } catch (err) {
      this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`);
    }
  }

這里的方法將會吧router的相關(guān)信息打印到run/router.json文件中寿羞。也符合app應(yīng)當(dāng)承擔(dān)的router路由業(yè)務(wù)設(shè)計理念。

step4. 觸發(fā)WARN_COUFUSED_CONFIG方法

能夠看到對一個全局this.config.confusedConfiguration對象作出遍歷合法判斷赂蠢,(貿(mào)貿(mào)然來句,這個框架各種this對象,有必要深挖一下,整理一下)后續(xù)分析吧

step5. 觸發(fā)BIND_EVENTS

待分析绪穆。


關(guān)于對啟動后的http/tcp/socket的監(jiān)聽邏輯。未完待續(xù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市玖院,隨后出現(xiàn)的幾起案子菠红,更是在濱河造成了極大的恐慌,老刑警劉巖难菌,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件试溯,死亡現(xiàn)場離奇詭異,居然都是意外死亡郊酒,警方通過查閱死者的電腦和手機遇绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燎窘,“玉大人试读,你說我怎么就攤上這事≤ⅲ” “怎么了钩骇?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長铝量。 經(jīng)常有香客問我倘屹,道長,這世上最難降的妖魔是什么慢叨? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任纽匙,我火速辦了婚禮,結(jié)果婚禮上拍谐,老公的妹妹穿的比我還像新娘烛缔。我一直安慰自己,他們只是感情好轩拨,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布践瓷。 她就那樣靜靜地躺著,像睡著了一般亡蓉。 火紅的嫁衣襯著肌膚如雪晕翠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天砍濒,我揣著相機與錄音淋肾,去河邊找鬼。 笑死爸邢,一個胖子當(dāng)著我的面吹牛樊卓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杠河,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼碌尔,長吁一口氣:“原來是場噩夢啊……” “哼浇辜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起七扰,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤奢赂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后颈走,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膳灶,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年立由,在試婚紗的時候發(fā)現(xiàn)自己被綠了轧钓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡锐膜,死狀恐怖毕箍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情道盏,我是刑警寧澤而柑,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站荷逞,受9級特大地震影響媒咳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜种远,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一涩澡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坠敷,春花似錦妙同、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至弄抬,卻和暖如春茎辐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掂恕。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弛槐,地道東北人懊亡。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像乎串,于是被迫代替她去往敵國和親店枣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容