Egg.js was developed for enterprise-level frameworks and applications. It follows the principle of “convention over configuration” and follows a consistent set of conventions for application development, which can reduce the learning costs of developers within the team.

In the process of using, do you have the following doubts:

  • How does the service start when there is no entry file in the project?
  • Controllers and services are declared in their own directories. How do they end up thereappOn the object?

With that in mind, let’s take a look at the startup process of egg.js.

egg-bin

As described in the official documentation quickstart, we can quickly create an Egg project by using the following command:

mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i
Copy the code

Then run the NPM run debug command to start the project. What does this startup command actually do?

Looking at the package.json file in the root directory of the project, we can see that the command ultimately executes egg-bin debug. Egg-bin is a CLI development tool based on the common-bin package.

CommonBin abstracts and encapsulates a command line tool based on yargs, CO and other modules, providing support for async/ Generator features.

common-bin

At the heart of CommonBin are the load() and start() methods, which load JavaScript files in a specified directory and store the classes exposed by each file in a Map with the filename as the attribute name.

// @file: common-bin/lib/command.js
const COMMANDS = Symbol('Command#commands');

class CommonBin {
  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)); }}}add(name, target) {
    if(! (target.prototypeinstanceof CommonBin)) {
      target = require(target);
      // try to require es module
      if(target && target.__esModule && target.default) { target = target.default; }}this[COMMANDS].set(name, target); }}Copy the code

When the start() method is called, the “DISPATCH” method on the instance is executed to process it, and the run() method on the instance is called as appropriate.

const DISPATCH = Symbol('Command#dispatch');

class CommonBin {
  start() {
    co(
      function* () {
        yield this[DISPATCH]();
      }.bind(this)); } *[DISPATCH]() {// Check whether the Map exists
    if (this[COMMANDS].has(commandName)) {
      const Command = this[COMMANDS].get(commandName);
      const rawArgv = this.rawArgv.slice();

      rawArgv.splice(rawArgv.indexOf(commandName), 1); // Process parameters to get subcommands

      // If it exists, get the corresponding value and instantiate it
      const command = this.getSubCommandInstance(Command, rawArgv);
      // Call the "DISPATCH" method on the instance, and since the value obtained also inherits CommonBin, it will also go to that method
      // Since the parameters were changed the first time, the subcommands are recursively searched
      yield command[DISPATCH]();
      return;
    }

    // If there are no subcommands, the commands in the Map on the instance are defined here
    // It also checks if it is an autocomplete operation. If it is not, it calls the run method on the instance

    yield this.helper.callFn(this.run, [context], this); }}Copy the code

The processing logic for our actual commands is implemented through the run() method, and the arguments are the values of the context property on the instance. Alternatively, you can override the errorHandler() method to handle errors that occur during the process.

egg-bin debug

Going back to egg-bin, by looking at the package.json file, you can see that the bin/egg-bin.js file is executed when we execute the egg-bin debug command.

// @file: egg-bin/package.json
{
  "bin": {
    "egg-bin": "bin/egg-bin.js"."mocha": "bin/mocha.js"."ets": "bin/ets.js"
  },
  "main": "index.js"
}
Copy the code

The contents of this file are relatively simple, mainly executing the entry file, which instantiates the egg-bin extended class, and then calls the start() method of the instance:

// @file: egg-bin/bin/egg-bin.js
const Command = require('.. ');

new Command().start();
Copy the code

When instantiated, the load() method we mentioned above is used to automatically mount commands from the lib/ CMD folder under the instance object:

// @file: egg-bin/index.js
const path = require('path');
const Command = require('./lib/command'); // Inherit CommonBin and rewrite the context and errorHandler() methods

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

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

Therefore, when the start() method is called, the run() method corresponding to the subcommand debug is found according to the arguments passed, the most important of which is that the “serverBin” file is run with the subprocess:

// @file: egg-bin/lib/cmd/debug.js
const cp = require('child_process');

class DebugCommand {*run(context) {
    const eggArgs = yield this.formatArgs(context);
    const options = {
      / *... * /
    };

    // start egg
    const child = cp.fork(this.serverBin, eggArgs, options); }}Copy the code

In fact, the debug command inherits the dev command, where “serverBin” is specified in the corresponding file of the dev command and points to.. / start – cluster files.

// @file: egg-bin/lib/cmd/dev.js
class DevCommand extends Command {
  constructor() {
    // ...
    this.serverBin = path.join(__dirname, '.. /start-cluster');
    // ...}}Copy the code

In.. The /start-cluster file loads the framework in the directory specified by the framework parameters and executes the startCluster() method it exposes.

// @file: egg-bin/lib/start-cluster
const options = JSON.parse(process.argv[2]); The // argument is already processed when the file is executed in the debug command, which defaults to egg
require(options.framework).startCluster(options);
Copy the code

The startCluster() method exposed by the egg module is actually exposed in the egg-cluster module:

// @file: egg/index.js
exports.startCluster = require('egg-cluster').startCluster;
Copy the code

egg-cluster

The Egg-cluster module provides cluster management for eggs by starting multiple processes on the server at the same time, each running the same source code and listening on the same port.

├─ Index.js ├─ lib │ ├─ agent_worker.js# Agent Worker│ ├ ─ ─ app_worker. Js# App Worker│ ├ ─ ─ master. Js# Master process│ ├─ ├─ manager.js# Process logging and retrieval│ ├ ─ ─ messenger. Js# Interprocess communication│ ├ ─ ─ options. Js# config processing│ └ ─ ─ the terminate. Js# kill process└ ─ ─ package. JsonCopy the code

The startCluster method instantiates the Master to create a Master process:

// @file: egg-cluster/index.js
const Master = require('./lib/master');

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

The Master process is responsible for process management (similar to pM2). It does not do any specific work. It is only responsible for starting other processes.

+---------+ +---------+ +---------+ | Master | | Agent | | Worker | +---------+ +----+----+ +----+----+ | fork agent | |  +-------------------->| | | agent ready | | |<--------------------+ | | | fork worker | +----------------------------------------->| | worker ready | | |<-----------------------------------------+ | Egg ready  | | +-------------------->| | | Egg ready | | +----------------------------------------->|Copy the code

During the creation process, the Master and Worker communicate with each other through events. When all workers are ready, they will notify the Master. After all processes are successfully initialized, the Master will notify the Agent and Worker that the application has been successfully started.

// @file: egg-cluster/lib/master.js
class Master extends EventEmitter {
  constructor() {
    // fork app workers after agent started
    this.once('agent-start'.this.forkAppWorkers.bind(this));

    // start fork agent worker
    this.detectPorts().then(() = > {
      this.forkAgentWorker();
    });
  }

  forkAgentWorker() {
    // Create the Agent Worker and listen for its message event. When the Agent Worker is ready, the agent-start event will be sent
    // new Agent(options).ready(() => process.send({ action: 'agent-start', to: 'master' }));
  }

  forkAppWorkers() {
    // App-start notifies Maser when the service in the Worker is ready
    // cluster.on('listening', (worker, address) => { /* emit app-start */ });}}Copy the code

The Worker runs business code and is responsible for processing real user requests and scheduled tasks. Next, let’s look at how the service is started.

Application

The Application class is instantiated when the Worker process is created to handle user requests:

// @file: egg-cluster/lib/app_worker.js
const Application = require(options.framework).Application;
const app = new Application(options);

app.ready(startServer);

function startServer() {
  require('http') .createServer(app.callback()) .listen(... args); }Copy the code

This class comes from the egg module and inherits the EggApplication (Egg), EggCore (egg-core), and KoaApplication (KOA) classes.

KoaApplication is initialized according to the inheritance relationship. In Koa, four core objects (Application, Context, Request, and Response) are obtained:

Then EggCore. The EggCore constructor creates and loaders that manage the lifecycle and sets the Controller and Service properties.

// @file: egg-core/lib/egg.js
const EggConsoleLogger = require('egg-logger').EggConsoleLogger;
const EGG_LOADER = Symbol.for('egg#loader');
const Lifecycle = require('./lifecycle');

class BaseContextClass {
  constructor(ctx /* context instance */) {
    this.ctx = ctx;
    this.app = ctx.app;
    this.config = ctx.app.config;
    this.service = ctx.service; }}class EggCore extends KoaApplication {
  constructor(options = {}) {
    super(a);this.console = new EggConsoleLogger();
    this.BaseContextClass = BaseContextClass;
    // Base controller to be extended by controller in `app.controller`
    this.Controller = this.BaseContextClass;
    // Base service to be extended by services in `app.service`
    this.Service = this.BaseContextClass;

    this.lifecycle = new Lifecycle(/ *... * /);
    const Loader = this[EGG_LOADER];
    this.loader = new Loader({
      baseDir: options.baseDir,
      app: this.plugins: options.plugins,
      logger: this.console,
      serverScope: options.serverScope,
      env: options.env,
    });
  }

  get [EGG_LOADER]() {
    return require('egg-core/lib/loader/egg_loader.js'); }}Copy the code

EggApplication follows, which calls the loadConfig() method on the loader you just created, and sets the Controller and Service properties again.

// @file: egg/lib/egg.js
class EggApplication extends EggCore {
  constructor(options = {}) {
    super(options);
    this.loader.loadConfig();

    // EggBaseContextClass inherits the BaseContextClass as seen above, with the addition of logger logging
    this.Controller = EggBaseContextClass;
    this.Service = EggBaseContextClass; }}Copy the code

Finally, back to the initialization of the Application, the load() method of the loader is called.

// @file: egg/lib/application.js
const EGG_LOADER = Symbol.for('egg#loader');
const AppWorkerLoader = require('egg/lib/loader/app_worker_loader.js');

class Application extends EggApplication {
  constructor(options = {}) {
    super(options);
    this.loader.load();
  }

  // overrides the Loader originally set up in egg-core, meaning that the loadConfig() method called above is on AppWorkerLoader
  get [EGG_LOADER]() {
    returnAppWorkerLoader; }}Copy the code

In fact, the key to binding the controllers and services we created to our application is the AppWorkerLoader here.

Loader

AppWorkerLoader inherits from EggLoader, essentially overwriting the loadConfig() method and adding the load() method mentioned above.

// @file: egg/lib/loader/app_worker_loader.js
class AppWorkerLoader extends EggLoader {
  // Load the plug-in first and then the configuration
  loadConfig() {
    this.loadPlugin();
    super.loadConfig();
  }

  load(){}}Copy the code

Egg further enhances Koa. The most important thing is to manage the code in different directories based on certain conventions. EggLoader implements this convention and abstracts many low-level apis for further expansion.

As a base class, EggLoader provides some built-in methods based on the rules for file loading, such as getEggPaths() that can be used to get frame directories:

// @file: egg-core/lib/loader/egg_loader.js
class EggLoader {
  constructor(options) {
    this.options = options;
    this.app = this.options.app;
    this.eggPaths = this.getEggPaths();
  }

  getEggPaths() {
    const EggCore = require('.. /egg');
    const eggPaths = [];

    let proto = this.app;

    // Loop for the prototype chain
    while (proto) {
      proto = Object.getPrototypeOf(proto);
      if (proto === Object.prototype || proto === EggCore.prototype) {
        break;
      }

      const eggPath = proto[Symbol.for('egg#eggPath')];
      const realpath = fs.realpathSync(eggPath);

      if (!eggPaths.includes(realpath)) {
        eggPaths.unshift(realpath);
      }
    }

    returneggPaths; }}Copy the code

The EggLoader itself does not execute any of its exposed methods, but is called by the inherited class. As above, when the loadConfig() method above executes, the loadPlugin() method on the instance is called to load the plug-in.

When loading the plug-in, find the application and framework, load files such as config/plugin.js, and finally assign all legal plug-in configuration objects to the plugins property of the loader instance.

// @file: egg-core/lib/loader/mixin/plugin.js
module.exports = {
  loadPlugin() {
    // loader plugins from application
    const appPlugins = this.readPluginConfigs(
      path.join(this.options.baseDir, 'config/plugin.default'));// loader plugins from framework
    const eggPluginConfigPaths = this.eggPaths.map((eggPath) = >
      path.join(eggPath, 'config/plugin.default'));const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths);
    // loader plugins from process.env.EGG_PLUGINS
    let customPlugins = JSON.parse(process.env.EGG_PLUGINS);
    // loader plugins from options.plugins
    if (this.options.plugins) {
      customPlugins = Object.assign({}, customPlugins, this.options.plugins);
    }

    this.plugins = enablePlugins;
  }

  /* * Read plugin.js */ from multiple directories
  readPluginConfigs(configPaths) {
    const plugins = {};

    // Get all plugin configurations
    // plugin.default.js
    // plugin.${scope}.js
    // plugin.${env}.js
    // plugin.${scope}_${env}.js

    returnplugins; }}Copy the code

Then the loadConfig() method on the parent class, which is the loadConfig() method on the EggCore loader, is called to load the configuration:

// @file: egg-core/lib/loader/mixin/config.js
module.exports = {
  loadConfig() {
    const target = {};
    // Load Application config first
    const appConfig = this._preloadAppConfig();
    // plugin config.default
    // framework config.default
    // app config.default
    // plugin config.{env}
    // framework config.{env}
    // app config.{env}
    for (const filename of this.getTypeFiles('config')) {
      for (const unit of this.getLoadUnits()) {
      }
    }
    // load env from process.env.EGG_APP_CONFIG
    const envConfig = JSON.parse(process.env.EGG_APP_CONFIG);

    // All of the above loaded configurations are extended to target
    / / you can in the app. Js manipulation in the app. The config. Coremidualware and app. Config. AppMiddleware order
    target.coreMiddleware = target.coreMiddlewares =
      target.coreMiddleware || [];
    target.appMiddleware = target.appMiddlewares = target.middleware || [];

    this.config = target;
  },

  getLoadUnits() {
    const dirs = (this.dirs = []);

    // Get the set of paths to load the unit
    // From plug-ins to frameworks, and finally to applications
    // dirs.push({ path: xxx, type: xxx})

    returndirs; }};Copy the code

It can be seen that the configuration loading will load the configuration of each loading unit in a certain order:

Js -> plugin config.default.js -> framework config.default.js -> Apply config.default.js -> plugin config.prod.js -> framework config.prod.js -> Apply config.prod.jsCopy the code

The postloaded one overwrites the previous configuration of the same name, and the merged result is finally assigned to the config property of the loader instance.

Loader.load()

When the loadConfig() method above has finished, the load() method will then start executing:

class AppWorkerLoader extends EggLoader {
  load() {
    // Load the extension: app > plugin > core
    this.loadApplicationExtend();
    this.loadRequestExtend();
    this.loadResponseExtend();
    this.loadContextExtend();
    this.loadHelperExtend();

    this.loadCustomLoader();

    // app > plugin
    this.loadCustomApp();
    // app > plugin
    this.loadService(); // Load the service
    // app > plugin > core
    this.loadMiddleware(); // Load middleware
    // app
    this.loadController(); // Load the controller
    // app
    this.loadRouter(); // Dependent on controllers}}Copy the code

LoadExtend () is used to load files extending Application, Context, Request, Response, and Helper objects.

Loader.loadExtend()

The loadExtend() method is implemented primarily by manipulating access descriptors (iterating through properties on the Object to get the corresponding property descriptor, and then defining the stereotype of the specified extended Object via Object.defineProperty()).

The first step is to look for all possible extension files:

// @file: egg-core/lib/loader/mixin/extend.js
// eg: loadExtend('application', this.app)
module.exports = {
  loadExtend(name, proto) {
    // All extend files
    const filepaths = this.getExtendFilePaths(name);
    for (let i = 0, l = filepaths.length; i < l; i++) {
      const filepath = filepaths[i];
      filepaths.push(filepath + `.The ${this.serverEnv}`); }},getExtendFilePaths(name) {
    return this.getLoadUnits().map((unit) = >
      path.join(unit.path, 'app/extend', name) ); }};Copy the code

The properties of the objects exposed in these files are then iterated over and their property descriptors are retrieved, and if the property descriptor already exists on the object to be extended or on the object exposed by Koa, an attempt is made to replace the missing one with an existing access descriptor.

Finally, the resulting property descriptor is used to extend the new property on the target object.

module.exports = {
  loadExtend(name, proto) {
    for (let filepath of filepaths) {
      const ext = this.requireFile(filepath);
      const properties = Object.getOwnPropertyNames(ext).concat(
        Object.getOwnPropertySymbols(ext)
      );

      for (const property of properties) {
        const descriptor = Object.getOwnPropertyDescriptor(ext, property);
        // ...
        Object.defineProperty(proto, property, descriptor); }}}};Copy the code

So, if we need to extend the Application object, all we need to do is call the loadExtend() method:

class AppWorkerLoader extends EggLoader {
  loadApplicationExtend() {
    this.loadExtend('application'.this.app); }}Copy the code

This helps explain why the files we create in the app/extend directory expose objects that eventually extend to the appropriate objects.

Next, that is the loadCustomLoader() method.

Loader.loadCustomLoader()

When we write the configuration, we can specify to load the file in the specified directory to extend to the specified object by specifying the customLoader property:

// @file: config/config.default.js
module.exports = {
  customLoader: {
    // Extend the attribute name
    adapter: {
      BaseDir specifies the directory where the extension file resides relative to app.config.baseDir
      directory: 'app/adapter'.// Specify the target for the extension
      inject: 'app'.// Whether to load the directory of frameworks and plug-ins
      loadunit: false.// Other LoaderOptions can also be defined
      // ...,}}};Copy the code

This is consistent with the following:

// app.js
module.exports = (app) = > {
  const directory = path.join(app.config.baseDir, 'app/adapter');
  app.loader.loadToApp(directory, 'adapter');
};
Copy the code

Visible loadCustomLoader() is implemented at the bottom with the loadToApp() method, or loadToContext() method if CTX is extended.

// @file: egg-core/lib/loader/mixin/custom_loader.js
module.exports = {
  loadCustomLoader() {
    const customLoader = this.config.customLoader || {};

    for (const property of Object.keys(customLoader)) {
      const loaderConfig = Object.assign({}, customLoader[property]);
      const inject = loaderConfig.inject || 'app';
      // ...

      switch (inject) {
        case 'ctx': {
          this.loadToContext(/ * * /);
          break;
        }
        case 'app': {
          this.loadToApp(/ * * /);
          break;
        }
        default:
          throw new Error('inject only support app or ctx'); }}}};Copy the code

LoadToApp () doesn’t actually do much, but simply handles the configuration items by initializing the FileLoader class and calling the load() method on the instance.

class EggLoader {
  loadToApp(directory, property, opt) {
    const target = (this.app[property] = {});
    // ...

    newFileLoader(opt).load(); }}Copy the code

In the load() method, the parse() method is called to parse the files in a given directory and returns a list of items, each containing an array of attributes in the directory structure and exported results.

// @file: egg-core/lib/loader/file_loader.js
class FileLoader {
  parse() {
    const directories = this.options.directory;
    const filter = this.options.filter;
    const items = [];
    let files = this.options.match;
    let ignore = this.options.ignore;

    ignore = ignore.filter((f) = >!!!!! f).map((f) = > '! ' + f);
    files = files.concat(ignore);

    for (const directory of directories) {
      const filepaths = globby.sync(files, { cwd: directory });
      for (const filepath of filepaths) {
        const fullpath = path.join(directory, filepath);
        // get properties
        // app/service/foo/bar.js => [ 'foo', 'bar' ]
        const properties = getProperties(filepath, this.options);
        // app/service/foo/bar.js => service.foo.bar
        const pathName =
          directory.split(/ / / / \ \]).slice(-1) + '. ' + properties.join('. ');
        // get exports from the file
        // If initializer exists in the passed configuration, the exposed results will be processed by calling initializer first
        const exports = getExports(fullpath, this.options, pathName);
        // ignore exports when it's null or false returned by filter function
        if (exports= =null || (filter && filter(exports) = = =false)) continue;
        // set properties of class
        if (is.class(exports)) {
          exports.prototype.pathName = pathName;
          exports.prototype.fullPath = fullpath;
        }

        items.push({ fullpath, properties, exports}); }}returnitems; }}Copy the code

Each item is then appended to the target object, which maps the directory hierarchy to nested properties:

class FileLoader {
  load() {
    const items = this.parse();
    const target = this.options.target;
    for (const item of items) {
      // 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) {
          obj = item.exports;
        } else {
          obj = target[property] || {};
        }

        target[property] = obj;
        return obj;
      }, target);
    }
    returntarget; }}Copy the code

That is, after FileLoader has been processed, the contents exposed by the next file in the corresponding directory have been hierarchically saved to the specified target object.

LoadToContext () also calls FileLoader, but not directly. It calls the ContextLoader that inherits the FileLoader:

class EggLoader {
  loadToContext(directory, property, opt) {
    // ...

    newContextLoader(opt).load(); }}Copy the code

The results it reads from directories are not bound directly to the Context as before, but are defined to the Context using the object.defineProperty () method:

// @file: egg-core/lib/loader/context_loader.js
const CLASSLOADER = Symbol('classLoader');

class ContextLoader extends FileLoader {
  constructor(options) {
    // ...
    super(options);

    // define ctx.service
    Object.defineProperty(app.context, property, {
      get() {
        const classLoader = this[CLASSLOADER]
          ? this[CLASSLOADER]
          : (this[CLASSLOADER] = new Map());

        let instance = classLoader.get(property);
        if(! instance) { instance = getInstance(target,this); // If it is Class, an example will be returned
          classLoader.set(property, instance);
        }
        returninstance; }}); }}Copy the code

Here’s why Service is lazy, because we instantiate it in getInstance() only when we read the corresponding Service:

function getInstance(values, ctx) {
  const Class = values[EXPORTS] ? values : null;
  let instance;

  if (Class) {
  } else if() {}else {
    instance = new ClassLoader({ ctx, properties: values });
  }
  return instance;
}
Copy the code

This method is also used to read the Service we created as a nested directory, and when it encounters a directory that does not have any exports, it is handed over to the ClassLoader.

In the ClassLoader, it defines the underlying properties on the instance, and in the getter() method it does exactly the same as in the ContextLoader, where the getInstance() method is also called to get the instance.

class ClassLoader {
  constructor(options) {
    const properties = options.properties;
    this._cache = new Map(a);this._ctx = options.ctx;

    // Iterate over the lower layer properties to add to the instance
    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; }}); }}Copy the code

This creates recursion so that we can read the created Service according to the directory structure. In fact, the Service we normally access does not start loading here, but we will learn more about this later.

Loader.loadCustomApp()

The loadCustomApp() method basically loads the hooks that claim in each unit, adds them to the lifecycle that you created earlier to handle the lifecycle, and then calls its init() method to instantiate the registered class.

// @file: egg-core/lib/loader/mixin/custom.js
const LOAD_BOOT_HOOK = Symbol('Loader#loadBootHook');

module.exports = {
  loadCustomApp() {
    this[LOAD_BOOT_HOOK]('app');
    this.lifecycle.triggerConfigWillLoad();
  },

  [LOAD_BOOT_HOOK](fileName) {
    for (const unit of this.getLoadUnits()) {
      const bootFilePath = this.resolveModule(path.join(unit.path, fileName));
      const bootHook = this.requireFile(bootFilePath);

      if (is.class(bootHook)) {
        bootHook.prototype.fullPath = bootFilePath;
        this.lifecycle.addBootHook(bootHook);
      }
      // ...
    }

    this.lifecycle.init(); }};Copy the code

Lifecycle is also clear, storing related hooks and then executing them when the corresponding method is called:

// @file: egg-core/lib/lifecycle.js
const BOOT_HOOKS = Symbol('Lifecycle#bootHooks');

class Lifecycle extends EventEmitter {
  addBootHook(hook) {
    this[BOOT_HOOKS].push(hook);
  }

  init() {
    this[INIT] = true;
    this[BOOTS] = this[BOOT_HOOKS].map((t) = > new t(this.app));
  }

  triggerConfigWillLoad() {
    for (const boot of this[BOOTS]) {
      if(boot.configWillLoad) { boot.configWillLoad(); }}// ...}}Copy the code

So, if we need to do something in the framework’s life cycle, we just define app.js and agent.js as classes and export them.

Loader.loadService() & Loader.loadMiddleware()

The Service will then load the files in the app/ Service directory using the loadToContext() method described above and store the results in the application’s serviceClasses. Wait until the CTX API is called before instantiating the object.

// @file: egg-core/lib/loader/mixin/service.js
module.exports = {
  loadService(opt) {
    // ...
    this.loadToContext(servicePaths, 'service', opt); }};Copy the code

Middleware loads files in the App/Middleware directory under each loading unit using the loadToApp() method and then puts them in app.middlewares.

The middleware we write always exports a function to receive the user’s arguments. So it’s also iterating through the middleware and passing the corresponding configuration options and the current application that it read earlier to the function to get the real middleware.

// @file: egg-core/lib/loader/mixin/middleware.js
module.exports = {
  loadMiddleware(opt) {
    // ...
    this.loadToApp(opt.directory, 'middlewares', opt);

    for (const name in app.middlewares) {
      // By defining each object.defineProperty () to app.middleware
    }

    // use middleware ordered by app.config.coreMiddleware and app.config.appMiddleware
    const middlewareNames = this.config.coreMiddleware.concat(
      this.config.appMiddleware
    );
    for (const name of middlewareNames) {
      const options = this.config[name] || {};
      let mw = app.middlewares[name];
      mw = mw(options, app);
      // middlewares support options.enable, options.ignore and options.match
      mw = wrapMiddleware(mw, options);
      if(mw) { app.use(mw); }}}};Copy the code

Next we deal with options like enable, match and ignore, and only the middleware that meets the requirements is registered by calling app.use().

function wrapMiddleware(mw, options) {
  // support options.enable
  if (options.enable === false) return null;

  // support generator function
  mw = utils.middleware(mw);

  // support options.match and options.ignore
  if(! options.match && ! options.ignore)return mw;
  const match = pathMatching(options);

  const fn = (ctx, next) = > {
    if(! match(ctx))return next();
    return mw(ctx, next);
  };
  fn._name = mw._name + 'middlewareWrapper';
  return fn;
}
Copy the code

Loader.loadController()

The controller is also loaded by the loadToApp() method, in addition to the loading directory of app/controller under each loading unit and stored in app.controller, An Initializer () method has also been added to the option to call the loadToApp() method to preprocess the controller.

// @file: egg-core/lib/loader/mixin/controller.js
const opt = {
  initializer: (obj, opt) = > {
    // If it is a normal function, pass the app execution function to get the real controller
    // eg: 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);
    }
    // The Class way is the way we now write
    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'];
    }
    returnobj; }};Copy the code

The most commonly written Class, for example, creates a new Object and iterates through the controller’s prototype chain, wrapping all methods except constructor() and getter() and recording them on the new Object until it iterates through the Object’s prototype and returns the new Object.

function wrapClass(Controller) {
  let proto = Controller.prototype;
  const ret = {};
  // tracing the prototype chain
  while(proto ! = =Object.prototype) {
    const keys = Object.getOwnPropertyNames(proto);
    for (const key of keys) {
      // getOwnPropertyNames will return constructor
      // that should be ignored
      if (key === 'constructor') {
        continue;
      }
      // skip getter, setter & non-function properties
      const d = Object.getOwnPropertyDescriptor(proto, key);
      // prevent to override sub method
      if(is.function(d.value) && ! ret.hasOwnProperty(key)) { ret[key] = methodToMiddleware(Controller, key); } } proto =Object.getPrototypeOf(proto);
  }
  return ret;

  function methodToMiddleware(Controller, key) {
    return function classControllerMiddleware(. args) {
      const controller = new Controller(this);
      return utils.callFn(controller[key], [this], controller); }; }}Copy the code

That is, we then access the Controller as an object, and we specify a method on that object to handle the routing of the response. This function, when executed, initializes the Controller that was originally exposed, and then calls the corresponding method to handle the request.

const is = require('is-type-of');

/ * * *@param {Egg.Application} app - egg application
 */
module.exports = (app) = > {
  const { router, controller } = app;

  console.log(is.class(controller.home)); // false

  router.get('/', controller.home.index);
};
Copy the code

Loader.loadRouter()

Finally, the route is easier to load, using the loader.loadfile () method to load app/router.js directly and call the exposed function with the current application as an argument.

// @file: egg-core/lib/loader/mixin/router.js
module.exports = {
  loadRouter() {
    / / load the router. Js
    this.loadFile(path.join(this.options.baseDir, 'app/router')); }};Copy the code

It is worth mentioning that we did not add anything related to routing. When did the Router bind to the application? In fact, egg-Router is loaded by the application at the beginning of EggCore initialization to provide support:

const Router = require('@eggjs/router').EggRouter;
const ROUTER = Symbol('EggCore#router');

class EggCore extends KoaApplication {
  use(fn) {
    this.middleware.push(utils.middleware(fn));
    return this;
  }

  get router() {
    if (this[ROUTER]) {
      return this[ROUTER];
    }
    const router = (this[ROUTER] = new Router({ sensitive: true }, this));
    // register router middleware
    this.beforeStart(() = > {
      this.use(router.middleware());
    });
    returnrouter; }}Copy the code

The egg-router is actually forked from the KOa-router, with some additional functionality added.

conclusion

Going back to our original question, it should be clear by now.

  • There is no so-called entry file in the project, how does the service start?

CommonBin abstracts the packaged Nodejs command line tool based on Yargs, while EggBin automatically mounts the commands in the specified directory to the instance object based on CommonBin, and then invokes the corresponding scripts to process the commands.

In the case of running Debug, EggBin ends up calling the startCluster() method of the Egg module, which is actually defined in the EggCluster module, which is specifically used to provide cluster management for the Egg.

When EggCluster is running, it automatically creates an Agent and multiple workers. Each Worker creates an application to process user requests. After the multiple App workers are successfully started, the Master starts to provide external services.

  • Controllers and services are declared in their own directories. How do they end up thereappOn the object?

As an underlying framework, Egg itself supports fewer features and requires plug-ins to provide more features. In an Egg, a plug-in is a small application, and on top of an application, you can extend a framework based on an Egg. Egg refers to applications, frameworks, and plug-ins as loadUnits.

During initialization, when the loadConfig() method is called, the Egg iterates through all loadUnit load files and extends to the specified destination:

file application The framework The plug-in
package.json ✔ ︎ ✔ ︎ ✔ ︎
config/plugin.{env}.js ✔ ︎ ✔ ︎
config/config.{env}.js ✔ ︎ ✔ ︎ ✔ ︎
app/extend/application.js ✔ ︎ ✔ ︎ ✔ ︎
app/extend/request.js ✔ ︎ ✔ ︎ ✔ ︎
app/extend/response.js ✔ ︎ ✔ ︎ ✔ ︎
app/extend/context.js ✔ ︎ ✔ ︎ ✔ ︎
app/extend/helper.js ✔ ︎ ✔ ︎ ✔ ︎
agent.js ✔ ︎ ✔ ︎ ✔ ︎
app.js ✔ ︎ ✔ ︎ ✔ ︎
app/service ✔ ︎ ✔ ︎ ✔ ︎
app/middleware ✔ ︎ ✔ ︎ ✔ ︎
app/controller ✔ ︎
app/router.js ✔ ︎

When loading, it will be loaded according to certain priorities:

  1. Plug-in => Framework => Application;
  2. The order of plug-ins is determined by the dependency relationship, and the dependent party loads them first.
  3. Frameworks are loaded in inheritance order, starting at the bottom.

Appendix

  • Yargs framework uses Node.js to build a fully functional command line application, which can easily configure commands, resolve multiple parameters, set shortcuts, etc., and automatically generate help menus.
// test.js
const yargs = require('yargs');

const argv = yargs
  .usage('Usage: --s <filename>') // Declare the command format
  .describe('t'.'type')
  .alias('t'.'type')
  .demandOption(['type'].'type is required')
  .default('name'.'test')
  .option('s', {
    describe: 'File size'.// Description of the option
    alias: 'size'./ / alias
    demandOption: false.// If necessary
    default: 10./ / the default value
    type: 'number'./ / type
  })
  .example('--s a.txt'.'Set source file') // An example
  .help('help') // Displays help information
  .epilog('copyright').argv; // The help message is displayed at the end

console.log(argv);
// node test a b -t c --name d
Copy the code
  • A Cluster can run multiple processes simultaneously on a server, each running the same source code, and these processes can listen on a single port at the same time.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit'.function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http
    .createServer(function(req, res) {
      res.writeHead(200);
      res.end('hello world\n');
    })
    .listen(8000);
}
Copy the code

Refs

  • Source code analysis of egg.js – Project launch
  • From egg-bin to Command Line Interface Tool
  • Build your own personal command line toolset from scratch – Yargs Complete Guide