An overview of
The analysis of egg as a whole involves too many NPM packages, but the analysis of individual packages one by one will destroy the integrity, so it is necessary to start from the whole and break the NPM packages involved one by one.
Mainly includes the following packages
- Activation:
egg-bin
egg-script
- Multiple processes:
egg-cluster
- Framework:
egg
egg-core
egg-router
- Some of the main dependency libraries:
common-bin
koa
get-ready
ready-callback
Start the
We’ll start with the bootstrap, including egg-bin and egg-script.
egg-bin
class DevCommand extends Command {
constructor(rawArgv) {
super(rawArgv);
this.defaultPort = 7001;
// serverBin is a start-cluster file under egg-bin
this.serverBin = path.join(__dirname, '.. /start-cluster');
}
* run(context) {
const devArgs = yield this.formatArgs(context);
const env = {
NODE_ENV: 'development'.EGG_MASTER_CLOSE_TIMEOUT: 1000};const options = {
execArgv: context.execArgv,
env: Object.assign(env, context.env),
};
debug('%s %j %j, %j'.this.serverBin, devArgs, options.execArgv, options.env.NODE_ENV);
// forkNode is a promise wrapper and graceful wrapper for child_process.fork
const task = this.helper.forkNode(this.serverBin, devArgs, options);
this.proc = task.proc;
yieldtask; }}Copy the code
The code of egg-bin start-cluster is as follows
#! /usr/bin/env node'use strict'; const debug = require('debug')('egg-bin:start-cluster'); const options = JSON.parse(process.argv[2]); debug('start cluster options: %j', options); StartCluster require(options.framework).startCluster(options);Copy the code
The startCluster of egg.js then points to the startCluster method exposed by egg-cluster.
exports.startCluster = require('egg-cluster').startCluster;
Copy the code
Question: The design does not understand why the egg-cluster was not pointed to in the first place.
egg-scripts
Start command
* run(context) {
const { argv, env, cwd, execArgv } = context;
const HOME = homedir();
const logDir = path.join(HOME, 'logs');
// egg-script start
// egg-script start ./server
// egg-script start /opt/app
let baseDir = argv._[0] || cwd;
if(! path.isAbsolute(baseDir)) baseDir = path.join(cwd, baseDir); argv.baseDir = baseDir;const isDaemon = argv.daemon;
argv.framework = yield this.getFrameworkPath({
framework: argv.framework,
baseDir,
});
this.frameworkName = yield this.getFrameworkName(argv.framework);
const pkgInfo = require(path.join(baseDir, 'package.json'));
argv.title = argv.title || `egg-server-${pkgInfo.name}`;
argv.stdout = argv.stdout || path.join(logDir, 'master-stdout.log');
argv.stderr = argv.stderr || path.join(logDir, 'master-stderr.log');
// normalize env
env.HOME = HOME;
env.NODE_ENV = 'production';
// it makes env big but more robust
env.PATH = env.Path = [
// for nodeinstall
path.join(baseDir, 'node_modules/.bin'),
// support `.node/bin`, due to npm5 will remove `node_modules/.bin`
path.join(baseDir, '.node/bin'),
// adjust env for win
env.PATH || env.Path,
].filter(x= >!!!!! x).join(path.delimiter);// for alinode
env.ENABLE_NODE_LOG = 'YES';
env.NODE_LOG_DIR = env.NODE_LOG_DIR || path.join(logDir, 'alinode');
yield mkdirp(env.NODE_LOG_DIR);
// cli argv -> process.env.EGG_SERVER_ENV -> `undefined` then egg will use `prod`
if (argv.env) {
// if undefined, should not pass key due to `spwan`, https://github.com/nodejs/node/blob/master/lib/child_process.js#L470
env.EGG_SERVER_ENV = argv.env;
}
const command = argv.node || 'node';
const options = {
execArgv,
env,
stdio: 'inherit'.detached: false};this.logger.info('Starting %s application at %s'.this.frameworkName, baseDir);
// remove unused properties from stringify, alias had been remove by `removeAlias`
const ignoreKeys = [ '_'.'$0'.'env'.'daemon'.'stdout'.'stderr'.'timeout'.'ignore-stderr'.'node' ];
const clusterOptions = stringify(argv, ignoreKeys);
// Note: `spawn` is not like `fork`, had to pass `execArgv` youself
const eggArgs = [ ...(execArgv || []), this.serverBin, clusterOptions, `--title=${argv.title}` ];
this.logger.info('Run node %s', eggArgs.join(' '));
// whether run in the background.
if (isDaemon) {
this.logger.info(`Save log file to ${logDir}`);
const [ stdout, stderr ] = yield [ getRotatelog(argv.stdout), getRotatelog(argv.stderr) ];
options.stdio = [ 'ignore', stdout, stderr, 'ipc' ];
// Start detached, the child process is permanent.
options.detached = true;
debug('Run spawn `%s %s`', command, eggArgs.join(' '));
const child = this.child = spawn(command, eggArgs, options);
this.isReady = false;
child.on('message'.msg= > {
// When the egg is started, the master sends a message to the parent process to tell the parent process to exit.
if (msg && msg.action === 'egg-ready') {
this.isReady = true;
this.logger.info('%s started on %s'.this.frameworkName, msg.data.address);
// Let the parent exit without waiting for the child to finish
child.unref();
child.disconnect();
this.exit(0); }});// check start status
yield this.checkStatus(argv);
} else {
options.stdio = [ 'inherit'.'inherit'.'inherit'.'ipc' ];
debug('Run spawn `%s %s`', command, eggArgs.join(' '));
const child = this.child = spawn(command, eggArgs, options);
child.once('exit'.code= > {
// command should exit after child process exit
this.exit(code);
});
// attach master signal to child
let signal;
[ 'SIGINT'.'SIGQUIT'.'SIGTERM' ].forEach(event= > {
process.once(event, () = > {
debug('Kill child %s with %s', child.pid, signal); child.kill(event); }); }); }}Copy the code
Stop command
* run(context) {
const { argv } = context;
this.logger.info(`stopping egg application ${argv.title ? `with --title=${argv.title}` : ' '}`);
// Use the ps-ef class to find the process that contains the start-cluster, i.e. the main process
// node /Users/tz/Workspaces/eggjs/egg-scripts/lib/start-cluster {"title":"egg-server","workers":4,"port":7001,"baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","framework":"/Users/t z/Workspaces/eggjs/test/showcase/node_modules/egg"}
let processList = yield this.helper.findNodeProcess(item= > {
const cmd = item.cmd;
return argv.title ?
cmd.includes('start-cluster') && cmd.includes(util.format(osRelated.titleTemplate, argv.title)) :
cmd.includes('start-cluster');
});
let pids = processList.map(x= > x.pid);
if (pids.length) {
this.logger.info('got master pid %j', pids);
this.helper.kill(pids);
// When the master process is killed, the master process will notify the Agent worker to exit. The master process will wait for 5 seconds to ensure that it exits completely
// wait for 5s to confirm whether any worker process did not kill by master
yield sleep(argv.timeout || '5s');
} else {
this.logger.warn('can\'t detect any running egg process');
}
// Find agent/worker processes that have not been killed by the main process
// node --debug-port=5856 / Users/tz/Workspaces/eggjs/test/showcase/node_modules / _egg - [email protected] @ egg - cluster/lib/agent_worker. Js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/show case","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406}
/ / node/Users/tz/Workspaces/eggjs/test/showcase/node_modules / _egg - [email protected] @ egg - cluster/lib/app_worker. Js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/show case","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406}
processList = yield this.helper.findNodeProcess(item= > {
const cmd = item.cmd;
return argv.title ?
(cmd.includes(osRelated.appWorkerPath) || cmd.includes(osRelated.agentWorkerPath)) && cmd.includes(util.format(osRelated.titleTemplate, argv.title)) :
(cmd.includes(osRelated.appWorkerPath) || cmd.includes(osRelated.agentWorkerPath));
});
pids = processList.map(x= > x.pid);
if (pids.length) {
this.logger.info('got worker/agent pids %j that is not killed by master', pids);
this.helper.kill(pids, 'SIGKILL');
}
this.logger.info('stopped');
}
Copy the code
Multiple processes
Multi-process mainly includes egg-cluster.
Sequence diagram
Before we get to egg-cluster, we need to look at the get-Ready and ready-callback packages, which basically intersperse the entire egg code.
Communication mechanism
get-ready
'use strict';
const is = require('is-type-of');
const IS_READY = Symbol('isReady');
const READY_CALLBACKS = Symbol('readyCallbacks');
const READY_ARG = Symbol('readyArg');
class Ready {
constructor() {
this[IS_READY] = false;
/ / callback queues
this[READY_CALLBACKS] = [];
}
ready(flagOrFunction) {
// register a callback
if (flagOrFunction === undefined || is.function(flagOrFunction)) {
return this.register(flagOrFunction);
}
// emit callbacks
this.emit(flagOrFunction);
}
/**
* Call the callbacks that has been registerd, and clean the callback stack.
* If the flag is not false, it will be marked as ready. Then the callbacks will be called immediatly when register.
* @param {Boolean|Error} flag - Set a flag whether it had been ready. If the flag is an error, it's also ready, but the callback will be called with argument `error`
*/
emit(flag) {
// this.ready(true);
// this.ready(false);
// this.ready(err);
this[IS_READY] = flag ! = =false;
this[READY_ARG] = flag instanceof Error ? flag : undefined;
// this.ready(true)
if (this[IS_READY]) {
this[READY_CALLBACKS]
.splice(0.Infinity)
// Put callback into the process.nextTick queue and wait for execution
.forEach(callback= > process.nextTick(() = > callback(this[READY_ARG]))); }}/ * * *@param {Object} obj - an object that be mixed
*/
static mixin(obj) {
if(! obj)return;
const ready = new Ready();
// delegate method
obj.ready = flagOrFunction= >ready.ready(flagOrFunction); }}function mixin(object) {
Ready.mixin(object);
}
module.exports = mixin;
module.exports.mixin = mixin;
module.exports.Ready = Ready;
Copy the code
ready-callback
Get-ready packaging
'use strict';
const EventEmitter = require('events');
const ready = require('get-ready');
const debug = require('debug') ('ready-callback');
const defaults = {
timeout: 10000.isWeakDep: false};/ * * *@class Ready* /
class Ready extends EventEmitter {
/ * * *@constructor
* @param {Object} opt
* - {Number} [timeout=10000] - emit `ready_timeout` when it doesn't finish but reach the timeout
* - {Boolean} [isWeakDep=false] - whether it's a weak dependency
* - {Boolean} [lazyStart=false] - will not check cache size automatically, if lazyStart is true
*/
constructor(opt) {
super(a); ready.mixin(this);
this.opt = opt || {};
this.isError = false;
this.cache = new Map(a);if (!this.opt.lazyStart) {
this.start(); }}start() {
// Execute in the check phase
setImmediate(() = > {
// fire callback directly when no registered ready callback
if (this.cache.size === 0) {
debug('Fire callback directly');
this.ready(true); }}); }}Copy the code
Get-ready implements a semantic event model, and ready-callback wraps get-Ready for execution in the Check phase.
So let’s go to the core, egg-core and egg.
The framework
In sequence diagrams in multiple processes, there arefork agent
As well asfork worker
Is actually instantiated here. The following figure shows the inheritance relationship between Agent and worker:The picture above has some key information
- For both agent and worker processes, the ready function is provided by lifcecycle in eggCore.
- The loader mechanism is also initialized in eggcore. Note that it is initialized in eggcore, although
egg-core
There will be a defaultloader
Parent class, but the final initialization isegg
It’s in the bagagentLoader
跟workLoader
Subclasses.
egg-core
Lifecycle and Loader will be lifecycle and loader will be lifecycle and loader will be lifecycle and loader will be lifecycle.
loader
class AgentWorkerLoader extends EggLoader {
/** * loadPlugin first, then loadConfig */
loadConfig() {
this.loadPlugin();
super.loadConfig();
}
load() {
this.loadAgentExtend();
this.loadContextExtend();
this.loadCustomAgent(); }}module.exports = AgentWorkerLoader;
Copy the code
'use strict';
const EggLoader = require('egg-core').EggLoader;
/**
* App worker process Loader, will load plugins
* @see https://github.com/eggjs/egg-loader
*/
class AppWorkerLoader extends EggLoader {
/**
* loadPlugin first, then loadConfig
* @since 1.0.0 * /
loadConfig() {
this.loadPlugin();
super.loadConfig();
}
/**
* Load all directories in convention
* @since 1.0.0 * /
load() {
// app > plugin > core
this.loadApplicationExtend();
this.loadRequestExtend();
this.loadResponseExtend();
this.loadContextExtend();
this.loadHelperExtend();
this.loadCustomLoader();
// app > plugin
this.loadCustomApp();
// app > plugin
this.loadService();
// app > plugin > core
this.loadMiddleware();
// app
this.loadController();
// app
this.loadRouter(); // Dependent on controllers}}module.exports = AppWorkerLoader;
Copy the code
Both call loadConfig, followed by the load method. The loadConfig call is relatively simple.
load****Extend
This method is called in both agent and worker processes.
/**
* mixin Agent.prototype
* @function EggLoader#loadAgentExtend
* @since 1.0.0 * /
loadAgentExtend() {
this.loadExtend('agent'.this.app);
},
/**
* mixin Application.prototype
* @function EggLoader#loadApplicationExtend
* @since 1.0.0 * /
loadApplicationExtend() {
this.loadExtend('application'.this.app);
},
/**
* mixin Request.prototype
* @function EggLoader#loadRequestExtend
* @since 1.0.0 * /
loadRequestExtend() {
this.loadExtend('request'.this.app.request);
},
/**
* mixin Response.prototype
* @function EggLoader#loadResponseExtend
* @since 1.0.0 * /
loadResponseExtend() {
this.loadExtend('response'.this.app.response);
},
/**
* mixin Context.prototype
* @function EggLoader#loadContextExtend
* @since 1.0.0 * /
loadContextExtend() {
this.loadExtend('context'.this.app.context);
},
/**
* mixin app.Helper.prototype
* @function EggLoader#loadHelperExtend
* @since 1.0.0 * /
loadHelperExtend() {
if (this.app && this.app.Helper) {
this.loadExtend('helper'.this.app.Helper.prototype); }},Copy the code
As you can see from the code, the main thing is to call this. LoadExtend.
loadExtend(name, proto) {
this.timing.start(`Load extend/${name}.js`);
// 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 + `.The ${this.serverEnv}`);
if (isAddUnittest) filepaths.push(filepath + '.unittest');
}
const mergeRecord = new Map(a);for (let filepath of filepaths) {
filepath = this.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 = this.requireFile(filepath);
// Get the list of attributes of an object, including symbol.
/ / note: Object. The keys and Object. GetOwnPropertyNames can only get the symbol attribute of the Object list
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);
}
// Get a method descriptor for an Agent extension
let descriptor = Object.getOwnPropertyDescriptor(ext, property);
// Get the current method descriptor from the object to be injected
let originalDescriptor = Object.getOwnPropertyDescriptor(proto, property);
if(! originalDescriptor) {// try to get descriptor from originalPrototypes
const originalProto = originalPrototypes[name];
if (originalProto) {
// Get the descriptor for the current method from the KOA layer
originalDescriptor = Object.getOwnPropertyDescriptor(originalProto, property); }}Merge if the descriptor is obtained from the injected object or koA native, merge the initial descriptor with the highest priority
if (originalDescriptor) {
// don't override descriptor
descriptor = Object.assign({}, descriptor);
if(! descriptor.set && originalDescriptor.set) { descriptor.set = originalDescriptor.set; }if (!descriptor.get && originalDescriptor.get) {
descriptor.get = originalDescriptor.get;
}
}
// Inject the descriptor into the object to be extended
Object.defineProperty(proto, property, descriptor);
// Enter the set structure and repeat the prompt
mergeRecord.set(property, filepath);
}
debug('merge %j to %s from %s'.Object.keys(ext), name, filepath);
}
this.timing.end(`Load extend/${name}.js`);
}
// don't care if app/extend is defined
// /Users/ld/Documents/bytedance/test/showcase/node_modules/egg-view/app/extend/agent
// Users/ld/Documents/bytedance/test/showcase/node_modules/egg/app/extend/agent
// /Users/ld/Documents/bytedance/test/showcase/app/extend/agent
getExtendFilePaths(name) {
The getLoadUnits method returns the directories of all the upper-layer framework layers, the egg layer, and the enable plug-ins involved in both
return this.getLoadUnits().map(unit= > path.join(unit.path, 'app/extend', name));
},
Copy the code
loadService
loadService(opt) {
this.timing.start('Load Service');
// Load it into 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);
this.timing.end('Load Service');
},
Copy the code
As you can see, the main one is this. LoadToContext. In fact, whenever you want to mount something to CTX, you need to call this.
loadToContext(directory, property, opt) {
opt = Object.assign({}, {
directory,
property,
inject: this.app,
}, opt);
const timingKey = `Load "The ${String(property)}" to Context`;
this.timing.start(timingKey);
new ContextLoader(opt).load();
this.timing.end(timingKey);
}
Copy the code
'use strict';
const assert = require('assert');
const is = require('is-type-of');
const FileLoader = require('./file_loader');
const CLASSLOADER = Symbol('classLoader');
const EXPORTS = FileLoader.EXPORTS;
class ClassLoader {
constructor(options) {
assert(options.ctx, 'options.ctx is required');
const properties = options.properties;
this._cache = new Map(a);this._ctx = options.ctx;
for (const property in properties) {
this.defineProperty(property, properties[property]); }}defineProperty(property, values) {
Object.defineProperty(this, property, {
get() {
let instance = this._cache.get(property);
if(! instance) { instance = getInstance(values,this._ctx);
this._cache.set(property, instance);
}
returninstance; }}); }}/**
* Same as {@link FileLoader}, but it will attach file to `inject[fieldClass]`. The exports will be lazy loaded, such as `ctx.group.repository`.
* @extends FileLoader
* @since 1.0.0 * /
class ContextLoader extends FileLoader {
/ * * *@class
* @param {Object} options - options same as {@link FileLoader}
* @param {String} options.fieldClass - determine the field name of inject object.
*/
constructor(options) {
assert(options.property, 'options.property is required');
assert(options.inject, 'options.inject is required');
const target = options.target = {};
if (options.fieldClass) {
options.inject[options.fieldClass] = target;
}
super(options);
const app = this.options.inject;
const property = options.property;
// 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(a); }const classLoader = this[CLASSLOADER];
let instance = classLoader.get(property);
if(! instance) { instance = getInstance(target,this);
classLoader.set(property, instance);
}
returninstance; }}); }}module.exports = ContextLoader;
function getInstance(values, ctx) {
// it's a directory when it has no exports
// then use ClassLoader
const Class = values[EXPORTS] ? values : null;
let instance;
if (Class) {
if (is.class(Class)) {
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;
}
Copy the code
FileLoder load method
load() {
const items = this.parse();
const target = this.options.target;
for (const item of items) {
debug('loading item %j', item);
// item { properties: [ 'a', 'b', 'c'], exports }
// => target.a.b.c = exports
item.properties.reduce((target, property, index) = > {
let obj;
const properties = item.properties.slice(0, index + 1).join('. ');
if (index === item.properties.length - 1) {
if (property in target) {
if (!this.options.override) throw new Error(`can't overwrite property '${properties}' from ${target[property][FULLPATH]} by ${item.fullpath}`);
}
obj = item.exports;
if(obj && ! is.primitive(obj)) { obj[FULLPATH] = item.fullpath; obj[EXPORTS] =true; }}else {
obj = target[property] || {};
}
target[property] = obj;
debug('loaded %s', properties);
return obj;
}, target);
}
return target;
}
Copy the code
LoaderService does not mount all services to CTX as loaderExtend does. If all services are mounted to CTX on each request, the CTX will not mount all services to CTX. This will result in huge memory consumption.
So the current approach is to put all the services in a single closure. The first call to ctx.service will return a large object consisting of each classLoader wrapped by serviceClass. GetInstance will determine if you are a class, and if it is, it will return the instance.
So, for example, when ctx.service.index.index() is executed, the flow chart looks like this:
Both ctx.service objects and ctx.service.index are retrieved with a map, but only on a single request.
loadToApp
loadToApp(directory, property, opt) {
const target = this.app[property] = {};
opt = Object.assign({}, {
directory,
target,
inject: this.app,
}, opt);
const timingKey = `Load "The ${String(property)}" to Application`;
this.timing.start(timingKey);
new FileLoader(opt).load();
this.timing.end(timingKey);
}
Copy the code
The loadToApp method is relatively crude, which is to mount the app directly, so I won’t go into details here.
loadController loadMiddleware
LoadMiddleware is relatively simple and can be used with the acquired middleware, while controllers need to be aligned to wrap methodToMiddleware.
The loadMiddleware link goes to this.loadToApp, stored in app.middleware, and then startServer, uses a wave.
For loadController, this. LoadToApp is also used to mount the controller to the APP, but because of the connection with routing, the controller wrap layer needs to be made into a middleware. And then the next time you access it, you still go koA-router logic.
lifecycle
User-images.githubusercontent.com/40081831/47…
Look no further than this official chart.
The end of the
Finally finished, the overall feeling is that egg’s source code reads a bit fragmentary, with a lot of self-packaged NPM packages, but the overall design is great.
Language finches link
www.yuque.com/docs/share/…