egg.js内部的进程守护已经有社区大佬分析过了,请看:https://cnodejs.org/topic/597445118f0313ff0d08d712
在egg进程里包括了parent,master,worker,agent三个进程,每个进程所担当的功能不同。master进程不执行任何业务逻辑,只用于进程守护。 worker进程专指工作进程,执行业务逻辑的进程,agent进程相当于秘书进程,辅助worker进程。
①:启动worker进程
egg-cluster/lib/master模块是master进程的主要执行代码,master进程负责启动agent进程,agent进程启动后通知master进程,master进程开始启动worker进程。forkAppWorkers进程代码如下:
forkAppWorkers() {
this.appStartTime = Date.now();
this.isAllAppWorkerStarted = false;
this.startSuccessCount = 0;
const args = [ JSON.stringify(this.options) ];
this.log('[master] start appWorker with args %j', args);
//使用cfork模块启动worker进程
cfork({
exec: appWorkerFile, //表示worker进程启动时要执行的js文件
args,
silent: false,
count: this.options.workers, //开启worker进程数,由传进参数来控制
// don't refork in local env
refork: this.isProduction, //根据当前的环境判断是否在worker进程挂掉时重启进程
});
}
如上所示使用到了cfork模块启动worker进程,所以跳转到cfork模块。在此模块中使用到了cluster.setupMaster(opts)方法预先设置fork worker进程所需要的参数。
for (var i = 0; i < count; i++) {
//fork worker进程
newWorker = forkWorker();
//将cluster.settings对象挂到newWorker身上,所以事件驱动函数里worker1对象身上就有了settings对象
//因为如cluster.on('exit',function(worker1){ }) 这个worker1对象和newWorker对象是同一个对象,所以我们在worker1可以找到cluster.settings
//便于在cluster.on订阅的回掉里使用到cluster.settings
newWorker._clusterSettings = cluster.settings;
}
function forkWorker(settings) {
//settings是cluster.setupMaster()方法的参数,用来设置后面的cluster.fork
if (settings) {
//cluster.settings设置成settings,后面的setupMaster()会自动获取settings作为参数,进而cluster.fork启动新进程时,
//会将使用到settings参数
cluster.settings = settings;
cluster.setupMaster();
}
return cluster.fork(attachedEnv);//fork新进程
}
②:执行worker进程文件代码(egg-cluster/lib/app_worker.js是启动worker进程时执行的js文件,同理egg-cluster/lib/agent_worker.js是启动agent进程执行的js文件)。
在文件中首先看到的是实例化一个Application,
//实例化Application,并且Application实例化过程中添加了ready方法。
const app = new Application(options);
//当实例化完成后启动监听端口服务。
app.ready(startServer);
function startServer(err) {
//如果有错误,退出进程
if (err) {
consoleLogger.error(err);
consoleLogger.error('[app_worker] start error, exiting with code:1');
process.exit(1);
}
let server;
//如果是https协议,worker进程启动监听端口。传入https需要的参数。
if (options.https) {
server = require('https').createServer({
key: fs.readFileSync(options.key),
cert: fs.readFileSync(options.cert),
}, app.callback());
} else {
//http模式,监听端口
server = require('http').createServer(app.callback());
}
//监听过程发生错误,退出进程。
server.once('error', err => {
consoleLogger.error('[app_worker] server got error: %s, code: %s', err.message, err.code);
//退出后,start模式,master进程会重启新worker进程。
process.exit(1);
});
②:探究Application(egg\lib\application.js)。
打开文件探究得知class Application extends EggApplication{},class EggApplication extends EggCore {},class EggCore extends KoaApplication {}。 所以本质上class Application 就是每个worker进程的主要逻辑所在。从父类EggCore实例化过程中可以看会为app添加一个loader属性,而这个loader对象是 所有app扩展的关键。如下:
this.loader = new Loader({
baseDir: options.baseDir, //项目根目录
app: this, //Application实例对象
plugins: options.plugins,
typescript: options.typescript,
logger: this.console,
serverScope: options.serverScope,
});
而Loader即是class AppWorkerLoader (egg\lib\loader\app_worker_loader.js),class AppWorkerLoader extends EggLoader {}
class AppWorkerLoader extends EggLoader {
/**
* loadPlugin first, then loadConfig
* @since 1.0.0
*/
//注意这里的AppWorkerLoader继承了EggLoader,虽然没有constroctor函数,但是不写constroctor函数默认情况下
//AppWorkerLoader实例化的参数都会被传到EggLoader里。
//这个方法在EggApplication(egg/lib/egg.js)实例化过程中this.loader.loadConfig()被调用。
loadConfig() {
//用户读取egg内置插件和用户自定义插件插件名,并且检查插件路径和是否存在和相互依赖关系等
//最后this.plugins和this.orderPlugins挂载了合法插件总对象,this指向AppWorkerLoader的实例。
//经过这个方法后,Application的的实例使用加载插件方法this.loader.xxx即可
//这个方法存在egg-core/lib/loader/mixin/plugin.js里
this.loadPlugin();
//这个方法是加载所有config,在this.config身上。
//这个方法存在egg-core/lib/loader/mixin/config.js里
super.loadConfig();
// super.loadConfig 指向父类EggLoader的loadConfig ,它来自于与 EggLoader.protorype.loadConfig
// 用super的原因是为了到父类super去寻找相关的属性。
// 如果使用this.loadConfig()去寻找(this指向this.loader对象),就会寻找到AppWorkerLoader类的loadConfig原型方法
// 就会出现死循环。
}
/**
* 开始加载所有约定目录
* @since 1.0.0
*/
//这个方法在egg/lib/application.js里调用
load() {
// app > plugin > core
//這些方法都定义在egg-core文件下的mixin文件夹里的js文件,在egg-core/lib/loader/egg_loader.js被加载
//并且将文件里的对象扩展到在到EggLoader.prototype身上
this.loadApplicationExtend(); //在extend.js文件里,给Application的实例扩展
this.loadRequestExtend(); //给Application的实例的request对象扩展
this.loadResponseExtend(); //给Application的实例的response对象扩展
this.loadContextExtend(); //给Application的实例的context对象扩展
this.loadHelperExtend(); //给Application的实例的helper对象扩展
// app > plugin
this.loadCustomApp(); //加载项目根目录,egg目录,自定义插件目录里的的app.js,这些app.js输出的对象是一个函数,那么函数被立即执行,并且将Application的实例对象传入到函数里。以便宜在app.js输出的函数里做相关操作
// app > plugin
this.loadService(); //加载service
// app > plugin > core
this.loadMiddleware(); //加载中间件
// app
this.loadController(); //加载controller
// app
this.loadRouter(); // 加载路由
}
}
如上代码所示我们首先看到的是this.loadPlugin,意味着首先加载插件对象的逻辑。代码如下:
module.exports = {
loadPlugin() {
// loader plugins from application
//加载路径 egg2.0/config/plugin.default.js的文件
//加载根目录下的plugin.default.js或者 plugin.js里的文件,并且将文件输出的用户定义的插件对象包装成:
// { enable: true,
// package: 'egg-view',
// name: 'view',
// dependencies: [],
// optionalDependencies: [],
// env: [],
// from: 'F:\\mywork\\mine\\egg2.0\\node_modules\\[email protected]@egg\\config\\plugin.js' }
// }
const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.default.js'));
//this.options.baseDir,this是指向new Loader()实例,定义在egg-core\lib\egg.js里
debug('Loaded app plugins: %j', Object.keys(appPlugins));
//loader plugins from framework
//this.eggPaths在egg-core/lib/loader/egg_loader.js里被定义,this指向new Loader实例
//this.eggPaths -> [ 'F:\\mywork\\mine\\egg2.0\\node_modules\\egg' ]
//F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config/plugin.default.js
const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default.js'));
//寻找到egg框架目录的config里的plugin.default.js,这个文件里记录这egg内置插件的情况,将文件输出的内置插件的对象包装成
//如下格式
//F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config/plugin.js对象,并且包装成如下格式
// { enable: true,
// package: 'egg-view',
// name: 'view',
// dependencies: [],
// optionalDependencies: [],
// env: [],
// from: 'F:\\mywork\\mine\\egg2.0\\node_modules\\[email protected]@egg\\config\\plugin.js' }
// }
const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths);
debug('Loaded egg plugins: %j', Object.keys(eggPlugins));
// loader plugins from process.env.EGG_PLUGINS
let customPlugins;
//加载process.env对象身上的自定义插件对象
//通过往process.env添加的插件控制对象
if (process.env.EGG_PLUGINS) {
try {
customPlugins = JSON.parse(process.env.EGG_PLUGINS);
} catch (e) {
debug('parse EGG_PLUGINS failed, %s', e);
}
}
// loader plugins from options.plugins
//加载this.options对象身上的自定义插件对象
//通过传入options上的plugins对象来控制插件
if (this.options.plugins) {
customPlugins = Object.assign({}, customPlugins, this.options.plugins);
}
//如果customPlugins对象存在,遍历customPlugins,并将里面插件对象每一子项转换为如下格式
// { enable: true,
// package: 'egg-view',
// name: 'view',
// dependencies: [],
// optionalDependencies: [],
// env: [],
// from: 'F:\\mywork\\mine\\egg2.0\\node_modules\\[email protected]@egg\\config\\plugin.js' }
// }
if (customPlugins) {
for (const name in customPlugins) {
this.normalizePluginConfig(customPlugins, name);
}
debug('Loaded custom plugins: %j', Object.keys(customPlugins));
}
this.allPlugins = {};//初始化所有插件对象为{}
this.appPlugins = appPlugins;//挂载用户插件对象egg2.0/config/plugin.default.js
this.customPlugins = customPlugins;//挂载用户在process.env.EGG_PLUGINS和通过this.options.plugins定义的插件对象
this.eggPlugins = eggPlugins;//挂载egg框架内部插件对象
//eggPlugins-》appPlugins-》customPlugins从左到右扩展到this.allPlugins身上
//如果eggPlugins身上有的插件对象已经扩展到this.allPlugins,并且appPlugins也存在同样的插件控制对象
//那么appPlugins的控制对象会掩盖掉eggPlugins的控制对象,customPlugins以此类推
//所以基本思路是egg内置插件的控制对象默认在整个egg框架里,当用户自定义某些插件控制对象在appPlugins身上
//会替换插件内置默认行为 ,customPlugins以此类推
this._extendPlugins(this.allPlugins, eggPlugins); //扩展eggPlugins到this.allPlugins身上
this._extendPlugins(this.allPlugins, appPlugins); //扩展appPlugins到this.allPlugins身上
this._extendPlugins(this.allPlugins, customPlugins);//扩展customPlugins到this.allPlugins身上
//enabledPluginNames里记录这个允许当前服务器环境,允许加载的插件的name
const enabledPluginNames = []; // enabled plugins that configured explicitly
const plugins = {};//挂载着当前服务器环境允许加载的插件的对象
const env = this.serverEnv;//获取服务器环境
//遍历所有插件对象
for (const name in this.allPlugins) {
const plugin = this.allPlugins[name];//插件对象身上的插件配置项
// resolve the real plugin.path based on plugin or package
//给每个插件配置项添加path(每个插件的真是路径)
//在添加path之前有个from属性,这个属性是表示该插件控制对象来自哪个文件的文件路径,并不是插件的路径。
plugin.path = this.getPluginPath(plugin, this.options.baseDir);
// read plugin information from ${plugin.path}/package.json
//读取插件文件夹里package.json的eggPlugin对象,判断plugin身上如name属性和package.json里的name是否相同
//并将package里的eggPlugin对象的'dependencies', 'optionalDependencies', 'env'属性赋值到plugin身上
//这样我们在写开发第三方插件时,我们锁定这个插件的依赖关系,可以将依赖关系写到package.json的eggPlugin属性里
this.mergePluginConfig(plugin);
// disable the plugin that not match the serverEnv
//env-> 服务器环境,如果服务器环境env变量存在,并且该插件对象env属性为有值数组,并且plugin.env和服务器环境变量不符合
//如果一个插件的的开启的服务环境和当前的服务器环境不同,那么跳过此插件 ,不需要加载此插件
if (env && plugin.env.length && plugin.env.indexOf(env) === -1) {
debug('Disable %j, as env is %j but got %j', name, plugin.env, env);
plugin.enable = false;//说明该插件在当前服务器环境变量情况下不需要加载
continue;//跳过此次循环
}
//挂载plugin到plugins对象身上,plugins过滤了与服务器环境不匹配的插件对象,但没有过滤手动写的enable=false插件
//因为plugins插件对象包含这所有和服务器匹配的插件对象,不过用户有没有允许他们开启
//它只负责提供该服务器环境匹配的所有插件对象
plugins[name] = plugin;
//enabledPluginNames里包含这所有用户允许开启的插件,enable=true,且这些插件和服务器环境匹配
if (plugin.enable) {//如果plugin.enable存在,那么说明该插件允许加载
//通过enable属性控制的允许加载的插件的name push到数组里
enabledPluginNames.push(name);//将插件的name push到enabledPluginNames数组里
}
}
// retrieve the ordered plugins
//this.orderPlugins之前并未定义所以为 undefined,调用方法后变成包含所有合法插件对象的集合
this.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames);
const enablePlugins = {};
for (const plugin of this.orderPlugins) {//遍历合法对象集合,拷贝到enablePlugins身上
enablePlugins[plugin.name] = plugin;
}
debug('Loaded plugins: %j', Object.keys(enablePlugins));
/**
* Retrieve enabled plugins
* @member {Object} EggLoader#plugins
* @since 1.0.0
*/
this.plugins = enablePlugins;//挂载合法对象
//this指向loadPlugin被调用时所属的对象->AppWorkerLoader/AgentWorkerLoader的实例
},
/*
* Read plugin.js from multiple directory
*/
readPluginConfigs(configPaths) {//路径,可能为纯字符串,也可能为数组形式
if (!Array.isArray(configPaths)) {
configPaths = [ configPaths ];//将路径包装成数组
}
// Get all plugin configurations
// plugin.default.js
// plugin.${scope}.js
// plugin.${env}.js
// plugin.${scope}_${env}.js
const newConfigPaths = [];
//this.getTypeFiles在egg-core/lib/loader/egg_loader.js里被定义
//用来根据服务器环境和serverScope来返回插件路径数组-> [ 'plugin.default.js', 'plugin.local.js' ]
//path.dirname如果参数是文件夹,那么直接返回,如果是js文件,则返回该文件所在文件夹路径
for (const filename of this.getTypeFiles('plugin')) {
for (let configPath of configPaths) {
//返回字符串形式的路径
//'F:\\mywork\\mine\\egg2.0\\config\\plugin.default.js
//'F:\\mywork\\mine\\egg2.0\\config\\plugin.local.js'
//'F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config\\plugin.default.js
//'F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config\\plugin.local.js'
configPath = path.join(path.dirname(configPath), filename);
//数组里包含着根目录下congfig文件夹里的plugin.default.js路径或者node_modules\\egg\\config\\plugin.default.js
//主要是看configPath传进来的路径
newConfigPaths.push(configPath);
}
}
const plugins = {};
for (let configPath of newConfigPaths) {
// let plugin.js compatible
//根据服务器环境组成的路径(如:F:\mywork\mine\egg2.0\config\plugin.local.js或者'F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config\\plugin.local.js')
//和包含着plugin.default.js的路径在遍历newConfigPaths过程中依次赋值给configPath
//如果configPath路径结尾是plugin.default.js,并且该文件不存在情况下
//configPath末尾的plugin.default.js替换成plugin.js
//这个操作的意思是判断路径结尾是否为plugin.default.js,并且用户未创建此文件情况下
//替换成plugin.js
if (configPath.endsWith('plugin.default.js') && !fs.existsSync(configPath)) {
configPath = configPath.replace(/plugin\.default\.js$/, 'plugin.js');
}
//判断configPath存在吗,不存在跳过本次循环
//如果configPath存在,但是configPath文件里什么代码也没有或者module.exports对象没有绑定对象,那么
//loadFile时返回是空对象 -> {}
if (!fs.existsSync(configPath)) {
continue;
}
//加载文件
const config = loadFile(configPath);
//遍历加载的文件对象
for (const name in config) {
this.normalizePluginConfig(config, name, configPath);
}
this._extendPlugins(plugins, config);
}
return plugins;
},
//plugins:被遍历的文件总对象,name:遍历过程中每个key(也是插件名),configPath:文件路径
normalizePluginConfig(plugins, name, configPath) {
const plugin = plugins[name];//遍历过程中的文件对象的key值
// plugin_name: false
//如果key值是布尔值,则包装成下面对象替换原来的 plugins[ name ]属性
if (typeof plugin === 'boolean') {
plugins[ name ] = {
name,//插件名
enable: plugin,//是否启用
dependencies: [],//依赖关系
optionalDependencies: [],//可选依赖关系
env: [],//变量
from: configPath,//来自哪个文件
};
return;//中断函数执行
}
//用in的方式判断enable是否存在这个插件对象里,是返回true
//in 判断对象是否包含某个属性。
//用于判断这个插件是否开启,如果用户定义了(enable存在plugin里),遵从用户选择是否开启
if (!('enable' in plugin)) {
plugin.enable = true;
}
//给插件对象添加/替换属性,保证每个对象都存在下面一系列属性
plugin.name = name;
plugin.dependencies = plugin.dependencies || [];
plugin.optionalDependencies = plugin.optionalDependencies || [];
plugin.env = plugin.env || [];
plugin.from = configPath;
depCompatible(plugin);
},
// Read plugin information from package.json and merge
// {
// eggPlugin: {
// "name": "", plugin name, must be same as name in config/plugin.js
// "dep": [], dependent plugins
// "env": "" env
// }
// }
mergePluginConfig(plugin) {//plugin-> 插件对象(包含插件真是路径)
let pkg;
let config;
//寻找每个插件里的package.json
const pluginPackage = path.join(plugin.path, 'package.json');
//如果package.json存在
if (fs.existsSync(pluginPackage)) {
pkg = require(pluginPackage);//加载package.json
// config -> 每个插件的package.json里的eggPlugin属性
// 如:{ name: 'onerror', optionalDependencies: [ 'jsonp' ] }
config = pkg.eggPlugin;
if (pkg.version) {//如果每个插件的package.json里记载了版本号,那么取出版本号赋值给plugin对象
plugin.version = pkg.version;
}
}
const logger = this.options.logger;
if (!config) {//如果插件的package.json里的eggPlugin属性不存在,提醒用户
logger.warn(`[egg:loader] pkg.eggPlugin is missing in ${pluginPackage}`);
return;
}
//插件的package.json里的记载的信息里的插件名和插件对象(plugin)身上的name不同提醒用户
if (config.name && config.name !== plugin.name) {
// pluginName is configured in config/plugin.js
// pluginConfigName is pkg.eggPath.name
logger.warn(`[egg:loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`);
}
// dep compatible
//依赖兼容,
//有的插件的package.json里的eggPluging里存在dep依赖,当不存在dependencies属性,或者存在dependencies属性,但是dependencies为空数组或不是数组
//将dep直接赋值给dependencies,并且删除dep,这样config对象就包含了dependencies属性
depCompatible(config);
//dependencies->依赖关系属性,optionalDependencies->可选依赖关系属性,env->环境变量(标明这个插件是什么环境加载的,如开发/生产环境)
for (const key of [ 'dependencies', 'optionalDependencies', 'env' ]) {
//如果某个插件对象身上的dependencies/optionalDependencies/env为空数组
//并且对应这个插件的config身上的dependencies/optionalDependencies/env存在,并且不为空数组
//用config[key]替换plugin[key]
if (!plugin[key].length && Array.isArray(config[key])) {
//这样plugin插件对象就从插件的package.json的eggPluging里获取到了'dependencies', 'optionalDependencies', 'env'属性
//赋值给plugin自己。
plugin[key] = config[key];
}
}
},
// allPlugins-> 过滤了和服务器环境不匹配的插件对象外的所有插件对象(包含手动写如enable=false的插件),
// enabledPluginNames -> 允许加载插件的name的数组,已经过滤了enable=false的插件和服务器环境不匹配的插件
// 所以这两个参数并不总是一一对应的
getOrderPlugins(allPlugins, enabledPluginNames) {
// no plugins enabled
if (!enabledPluginNames.length) {//如果enabledPluginNames里没有值return
return [];
}
//将插件对象和插件数组名传给函数,这个函数用来递归查找插件中的依赖插件或者可选依赖插件等等作用
//当依赖插件不存在allPlugins插件总对象里,就会记录在missing数组里
//当递归寻找插件对象的依赖插件时,发现依赖插件对象的依赖插件和该插件对象形成了循环引用如(a->b->c->a或者a->a),那么记录在recursive数组里
//只要recursive或者missing不为空数组,那么立即将result清空 抛出错误处理
const result = sequencify(allPlugins, enabledPluginNames);
// 输出结果如下:
// { sequence:
// [ 'onerror',
// 'session',
// 'i18n',
// 'watcher',
// 'multipart',
// 'security',
// 'development',
// 'schedule',
// 'logrotator',
// 'static',
// 'jsonp',
// 'view' ],
// missingTasks: [],
// recursiveDependencies: [] }
debug('Got plugins %j after sequencify', result);
// catch error when result.sequence is empty
//如果result是空数组,说明recursive或者missing不为空数组,抛出错误
if (!result.sequence.length) {
const err = new Error(`sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`);
// find plugins which is required by the missing plugin
for (const missName of result.missingTasks) {//遍历缺失的插件的名子的集合
const requires = [];
//遍历插件对象
for (const name in allPlugins) {
//如果[name]插件的dependencies里包含着missName
//说明这个缺失的插件missName是[name]插件的依赖插件
if (allPlugins[name].dependencies.indexOf(missName) >= 0) {
//push到数组里
requires.push(name);
}
}
//抛出错误说missName插件缺失,但是这个缺失插件是[name]插件所依赖的,所以不能缺失
err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]`;
}
err.name = 'PluginSequencifyError';
//抛出错误
throw err;
}
// log the plugins that be enabled implicitly
const implicitEnabledPlugins = [];//记录插件的enabled=false的依赖插件
const requireMap = {};
//遍历sequence,sequence包含插件和依赖或者可选依赖插件的name。但是这个数组不没有重复的。
//第二说明插件的依赖插件都存在于allPlugins对象里,并且依赖关系都是正常的,否则sequence就是空数组。
result.sequence.forEach(name => {
for (const depName of allPlugins[name].dependencies) {//遍历每个插件的依赖数组
//初始化requireMap[depName]
if (!requireMap[depName]) {
requireMap[depName] = [];
}
requireMap[depName].push(name);//将插件名push到requireMap身上的用该插件依赖组件命名的数组里
}
//该插件的enable=false(不加载)情况下,虽然插件都是enabled=true,才能push到sequence的也包含插件的依赖插件和可选依赖插件
//依赖插件存在于allPlugins对象身上,但是如果该依赖插件enabled=false,push到implicitEnabledPlugins数组里。
//这个判断就是用来过滤依赖插件或者可选依赖插件的enabled=false的情况
if (!allPlugins[name].enable) {
implicitEnabledPlugins.push(name);
//因为enabledPluginNames已经过滤了与服务器环境不匹配的插件和enabled=false的插件。
//allPlugins只过滤了与服务器环境不匹配的插件以外的所有插件,enabled=false的插件没有过滤。
//allPlugins是符合当前服务器环境的所有插件的总对象,所以可能包含了enabled=false的插件)
//因为name是依赖插件,被加载了,所以强制enable = true
allPlugins[name].enable = true;
}
});
//implicitEnabledPlugins里记载了enabled=false的依赖插件。
if (implicitEnabledPlugins.length) {
// Following plugins will be enabled implicitly.
// - configclient required by [hsfclient]
// - eagleeye required by [hsfclient]
// - diamond required by [hsfclient]
// 提醒这些依赖插件,因为被其他插件锁依赖,所以被隐式的开启了。
this.options.logger.info(`Following plugins will be enabled implicitly.\n${implicitEnabledPlugins.map(name => ` - ${name} required by [${requireMap[name]}]`).join('\n')}`);
}
//再一次遍历sequence,sequence是经过检测合法的插件名的集合,将allPlugins里和sequence匹配的插件对象return到新的数组里
//这个过滤掉和sequence不匹配的插件对象了,并返回合法的插件对象的集合(数组)
return result.sequence.map(name => allPlugins[name]);
},
// Get the real plugin path
getPluginPath(plugin) {//plugin->插件配置对象
if (plugin.path) { //插件配置对象有path属性 return
return plugin.path;
}
const name = plugin.package || plugin.name;//获取该插件的包装名或者name
const lookupDirs = [];
// 尝试在以下目录找到匹配的插件
// -> {APP_PATH}/node_modules -> 项目根目录的node_modules
// -> {EGG_PATH}/node_modules -> egg框架里的node_modules
// -> $CWD/node_modules -> process.cwd()根目录
//push 项目根目录的node_modules路径字符串到lookupDirs数组里
lookupDirs.push(path.join(this.options.baseDir, 'node_modules'));
// 到 egg 中查找,优先从外往里查找
// [ 'F:\\mywork\\mine\\egg2.0\\node_modules\\[email protected]@egg' ]
for (let i = this.eggPaths.length - 1; i >= 0; i--) {
const eggPath = this.eggPaths[i];//egg路径
//push egg目录里的node_modules路径字符串到lookupDirs数组里
lookupDirs.push(path.join(eggPath, 'node_modules'));
}
// should find the $cwd/node_modules when test the plugins under npm3
//push process.cwd()目录里的node_modules路径字符串到lookupDirs数组里
lookupDirs.push(path.join(process.cwd(), 'node_modules'));
//遍历lookupDirs数组
for (let dir of lookupDirs) {
//路径项目根目录/egg/process.cwd()下的node_modules里的plugin.package路径
dir = path.join(dir, name);
if (fs.existsSync(dir)) {//dir存在
return fs.realpathSync(dir);//返回真实路径,防止出现相对路径
}
}
//这个方法一层层找,在lookupDirs数组里,先在lookupDirs[0]的路径开始找,存在return出去,循环和函数终止
//所以 lookupDirs[1],lookupDirs[2]都不会再去遍历和寻找。以此类推,找到哪层,如果存在就在那层停止,后面
//不会再去找下层,都不存在报错
throw new Error(`Can not find plugin ${name} in "${lookupDirs.join(', ')}"`);
},
_extendPlugins(target, plugins) {//target->被扩展的对象 plugins对象
if (!plugins) {//插件对象不存在返回
return;
}
for (const name in plugins) {//遍历插件对象
const plugin = plugins[name]; //每一个插件配置项
let targetPlugin = target[name]; //被扩展对象身上相同命名的对象
if (!targetPlugin) {//如果不存
targetPlugin = target[name] = {};//初始化为{}
}
//package是每个插件配置项的包装名如:egg-view
//如果targetPlugin存在,并且targetPlugin.package === plugin.package
//表示target对象该插件的相关配置已经被扩展过了,打印日志提醒用户
if (targetPlugin.package && targetPlugin.package === plugin.package) {
this.options.logger.warn('plugin %s has been defined that is %j, but you define again in %s',
name, targetPlugin, plugin.from);
}
//如果plugin.path 或者 plugin.package存在
if (plugin.path || plugin.package) {
delete targetPlugin.path;//删除targetPlugin对象身上的path和package
delete targetPlugin.package;
}
//遍历该插件对象,将插件对象符合条件的属性逐个扩展到targetPlugin身上
for (const prop in plugin) {
if (plugin[prop] === undefined) {//插件对象身上某一个属性为undifined,跳过这个属性扩展
continue;
}
//如果targetPlugin身上存在某个属性,plugin身上也存在并且不为undefined,但是为空数组
//那么也跳过对这个属性的扩展,保留targetPlugin身上的该命名默认属性
//但是如果targetPlugin身上没有该属性情况下,不管 plugin身上的同命名属性是空数组还是长度不为0的数组都扩展上去
if (targetPlugin[prop] && Array.isArray(plugin[prop]) && !plugin[prop].length) {
continue;
}
//过滤掉不必扩展的属性,将剩下符合条件的属性扩展到targetPlugin身上
targetPlugin[prop] = plugin[prop];
}
}
},
};
//这个方法用来处理从(//F:\\mywork\\mine\\egg2.0\\node_modules\\egg\\config/plugin.js和egg2.0/config/plugin.default.js)
//加载的并且经过包装的插件对象和每个插件文件夹里package.json里记载关于插件信息的eggPlugin属性对象
function depCompatible(plugin) {
// 参数如下所示的插件对象
// 因为有两个不同的地方调用本函数,第一个地方调用是在插件控制对象里调用
// 如下所示传递的参数,当plugin对象包含了dep属性,并且自身的dependencies不是数组或者为空数组
// 就会将dep赋值给dependencies,并且删除dep。
// {
// name,//插件名
// enable: plugin,//是否启用
// dependencies: [],//依赖关系
// optionalDependencies: [],//可选依赖关系
// env: [],//变量
// from: configPath,//来自哪个文件
// };
//第二个调用地方是:每个插件文件夹的package.json里的eggPlugin属性当作参数传递到函数里如下所示,其他业务逻辑同上
//{ name: 'logrotator', dep: [ 'schedule' ] }
//
//
// 如果plugin.dependencies不是数组,或者是数组但是length为0
if (plugin.dep && !( Array.isArray(plugin.dependencies) && plugin.dependencies.length )) {
//egg内置插件的logrotator插件满足这个判断
//{ name: 'logrotator', dep: [ 'schedule' ] }
plugin.dependencies = plugin.dep;//dep赋值给dependencies并且删除dep
delete plugin.dep;
//因为plugin传入的是对象,所以和传入之前的插件对象或者eggPlugin属性对象都指向一个内存地址,所以改变
//plugin就会改变插件对象/eggPlugin属性对象
}
}
如上所示执行完成loadPlugin后,Application实例对象上拥有了this.orderPlugins(数组形式)和this.plugins(json形式)两个关于插件的属性。这个个属性是关于所有合法插件的对象的集合,合法插件是指命名合法,enable: true,env与当前服务器环境一致或者env为[],能在node_modules里找到插件文件夹(可能是局部,也可能是全局node_modules)部分集合如下:
[
{ enable: true,
package: 'egg-multipart',
name: 'multipart',
dependencies: [],
optionalDependencies: [],
env: [],
from: 'E:\\myLearning\\nodejs\\egg源码\\node_modules\\[email protected]@egg\\config\\plugin.js',
path: 'E:\\myLearning\\nodejs\\egg源码\\node_modules\\[email protected]@egg-multipart',
version: '2.0.0' },
{ enable: true,
package: 'egg-development',
name: 'development',
dependencies: [ 'watcher' ],
optionalDependencies: [],
env: [ 'local' ],
from: 'E:\\myLearning\\nodejs\\egg源码\\node_modules\\[email protected]@egg\\config\\plugin.js',
path: 'E:\\myLearning\\nodejs\\egg源码\\node_modules\\[email protected]@egg-development',
version: '2.2.0' }
]
所以在我们之后加载config,扩展对象(request,Application,response,context,helper),加载中间件,加载service,加载定时任务,都需要这个this.orderPlugins对象, 这个orderPlugins数组的每一个元素的path属性即是合法夹的绝对真实路径,我们在之后需扩展,或者加载就可以利用这个真是路径到插件文件夹里找到加载或者扩展文件。
至此我们可以在任何一个可以使用到Application对象的方法中使用这两个方法,例如路由里:
module.exports = app => {
const { router, controller } = app;
console.log(app.orderPlugins)
router.get('/', async function(ctx,next){next()},controller.home.index);
};
加载service,实际上调用 this.loadToContext给Application的context扩展serviceClasses属性:
loadService(opt) {
// 载入到 app.serviceClasses
opt = Object.assign({
call: true,
caseStyle: 'lower', //编码风格,lower
fieldClass: 'serviceClasses',
//getLoadUnits数组里存着所有合法插件egg框架和项目根目录下app/service绝对路径
directory: this.getLoadUnits().map(unit => path.join(unit.path, 'app/service')),
}, opt);
//service路径集合
const servicePaths = opt.directory;
//此方法在egg-core/egg_loader.js里定义
this.loadToContext(servicePaths, 'service', opt);
},
};
所以当我们在service里使用this.ctx.service就会触发下面的getter方法。
Object.defineProperty(app.context, 'service', {
get() {
// CLASSLOADER等于Symbol('EGG_LOADER_ITEM_EXPORTS')
//经测试默认情况下this[CLASSLOADER]=undefined
//this指向读取service属性的对象,在这里如果使用this.ctx.service那么
//this指向this.ctx,而this.ctx是继承koa框架的ctx上下文对象,每次当新的请求过来
//ctx都是新的,如:http.createServer((req,res)=>{}),客户端请求都会产生新的req,res
//所以基于req,res的ctx也是全新的。
//所以每次新的请求,在新的ctx对象上this[CLASSLOADER]都是undefined。
if (!this[CLASSLOADER]) {
//将CLASSLOADER设置为空的Map
this[CLASSLOADER] = new Map();
}
const classLoader = this[CLASSLOADER];
//从Map里获取这个[property]属性,默认是undefined
let instance = classLoader.get(property);
//如果不存在
if (!instance) {
instance = getInstance(target, this);
classLoader.set(property, instance);
}
return instance;
},
});
}
}
// user:
// { [Function: UserService]
// [Symbol(EGG_LOADER_ITEM_FULLPATH)]: 'E:\\MyLearning\\nodejs\\egg源码\\app\\service\\user.js',
// [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true
// }
//values-》Application实例上的serviceClasses的属性对象,默认为空对象
//ctx-》指向获取service属性的对象,默认是this.ctx
function getInstance(values, ctx) {
// it's a directory when it has no exports
// then use ClassLoader
//默认是EXPORTS(Symbol('EGG_LOADER_ITEM_EXPORTS');)不存在。
const Class = values[EXPORTS] ? values : null;
let instance;
//如果Class存在
if (Class) {
//class对象是一个class类
if (is.class(Class)) {
//实例化这个class对象,传入ctx上下文对象
instance = new Class(ctx);
} else {
// it's just an object
instance = Class;
}
// Can't set property to primitive, so check again
// e.x. module.exports = 1;
} else if (is.primitive(values)) {
instance = values;
} else {
instance = new ClassLoader({ ctx, properties: values });
}
return instance;
}
class ClassLoader {
//options-》{ ctx, properties: values }
constructor(options) {
assert(options.ctx, 'options.ctx is required');
//properties是Application实例对象上的serviceClasses属性。
const properties = options.properties;
this._cache = new Map();
this._ctx = options.ctx;
//遍历Application上的serviceClasses属性对象。
for (const property in properties) {
//遍历service对象
this.defineProperty(property, properties[property]);
}
// serviceClasses对象是项目根目录下的app/service里所有的js文件(service)的对象,这些对象都是class类
// 比如说user:其实是一个UserService class,并且在这个class上添加了Symbol(EGG_LOADER_ITEM_FULLPATH)]和
// [Symbol(EGG_LOADER_ITEM_EXPORTS)]两个静态属性。
// {
// admin:
// { [Function: AdminService]
// [Symbol(EGG_LOADER_ITEM_FULLPATH)]: 'E:\\MyLearning\\nodejs\\egg源码\\app\\service\\admin.js',
// [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true },
// user:
// { [Function: UserService]
// [Symbol(EGG_LOADER_ITEM_FULLPATH)]: 'E:\\MyLearning\\nodejs\\egg源码\\app\\service\\user.js',
// [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true
// }
// }
}
// property-》属性名,values-》属性值
defineProperty(property, values) {
//给ClassLoader实例对象绑定property属性,并且这个属性只有getter方法,其他默认
Object.defineProperty(this, property, {
get() {
//_cache map里寻找property属性
let instance = this._cache.get(property);
//如果不存在
if (!instance) {
instance = getInstance(values, this._ctx);
this._cache.set(property, instance);
}
return instance;
},
});
}
}
大佬,666 期待ing
cool
2233 顶 有空看下