site/docs/advanced/framework.md
If your team have met with these scenarios:
gulpfile.js, webpack.config.js.If your team needs:
To satisfy these demands, Egg endows developers with the capacity of customizing a framework. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Furthermore, Egg apply a quantity of coding conventions based on Koa.
Therefore, a uniform spec can be applied on projects in which the differentiation fulfilled in plugins. And the best practice summed from those projects can be continuously extracted from these plugins to the framework, which is available to other projects by just updating the dependencies' versions.
See more details in Progressive Development。
The framework extension is applied to Multiprocess Model, as we know Multiprocess Model and the differences between Agent Worker and App Worker, which have different APIs and both need to inherit.
They both are inherited from EggCore, and Agent is instantiated during the initiation of Agent Worker, while App is instantiated during the initiation of App Worker.
We could regard EggCore as the advanced version of Koa Application, which integrates built-in features such as Loader、Router and asynchronous launch.
Koa Application
^
EggCore
^
┌──────┴───────┐
│ │
Egg Agent Egg Application
^ ^
agent worker app worker
Just use egg-boilerplate-framework to generates a scaffold for you.
$ mkdir yadan && cd yadan
$ npm init egg --type=framework
$ npm i
$ npm test
But in order to illustrate details, let's do it step by step. Here is the sample code.
Each of those APIs is required to be implemented almost twice - one for Agent and another for Application.
egg.startClusterThis is the entry function of Egg's multiprocess launcher, based on egg-cluster, to start Master, but EggCore running in a single process doesn't invoke this function while Egg does.
const startCluster = require('egg').startCluster;
startCluster(
{
// directory of code
baseDir: '/path/to/app',
// directory of framework
framework: '/path/to/framework',
},
() => {
console.log('app started');
},
);
All available options could be found in egg-cluster.
egg.Application And egg.AgentThese are both singletons but still different with each other. To inherit framework, it's likely to inherited these two classes.
egg.AppWorkerLoader and egg.AgentWorkerLoaderTo customize framework, Loader is required and has to be inherited from Egg Loader for the propose of either loading directories or rewriting functions.
If we consider a framework as a class, then Egg framework is the base class,and implementing a framework demands to implement entire APIs of Egg.
// package.json
{
"name": "yadan",
"dependencies": {
"egg": "^2.0.0"
}
}
// index.js
module.exports = require('./lib/framework.js');
// lib/framework.js
const path = require('path');
const egg = require('egg');
class Application extends egg.Application {
protected override customEggPaths() {
// return the path of framework
return [path.dirname(__dirname), ...super.customEggPaths()];
}
}
// rewrite Egg's Application
module.exports = Object.assign(egg, {
Application,
});
The name of framework, default as egg, is a indispensable option to launch an application, set by egg.framework of package.json, then Loader loads the exported app of a module named it.
{
"scripts": {
"dev": "egg-bin dev"
},
"egg": {
"framework": "yadan"
}
}
As a loadUnit of framework, yadan is going to load specific directories and files, such as app and config. Find more files loaded at Loader.
The path of framework is override customEggPaths() method to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessible through the prototype chain.
Given a triple-layer framework: department level > enterprise level > Egg
// enterprise
const Application = require('egg').Application;
class Enterprise extends Application {
protected override customEggPaths() {
return ['/path/to/enterprise', ...super.customEggPaths()];
}
}
// Customize Application
exports.Application = Enterprise;
// department
const Application = require('enterprise').Application;
// extend enterprise's Application
class department extends Application {
protected override customEggPaths() {
return ['/path/to/department', ...super.customEggPaths()];
}
}
// the path of `department` have to be designated as described above
const Application = require('department').Application;
const app = new Application();
app.ready();
These code are pseudocode to elaborate the framework's loading process, and we have provided scaffolds to development and deployment.
Egg's multiprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similar to Application, is also required to be implemented.
// lib/framework.js
const path = require('path');
const egg = require('egg');
class Application extends egg.Application {
protected override customEggPaths() {
// return the path of framework
return [path.dirname(__dirname), ...super.customEggPaths()];
}
}
class Agent extends egg.Agent {
protected override customEggPaths() {
return [path.dirname(__dirname), ...super.customEggPaths()];
}
}
// rewrite Egg's Application
module.exports = Object.assign(egg, {
Application,
Agent,
});
To be careful about that Agent and Application based on the same Class possess different APIs.
Loader, the core of the launch process, is capable of loading data code, adjusting loading orders or even strengthen regulation of code.
As the same as Egg-Path, Loader exposes itself at customEggLoader() to ensure it's accessibility on prototype chain.
// lib/framework.js
const path = require('path');
const egg = require('egg');
class YadanAppWorkerLoader extends egg.AppWorkerLoader {
load() {
super.load();
// do something
}
}
class Application extends egg.Application {
protected override customEggPaths() {
// return the path of framework
return [path.dirname(__dirname), ...super.customEggPaths()];
}
// supplant default Loader
protected override customEggLoader() {
return YadanAppWorkerLoader;
}
}
// rewrite Egg's Application
module.exports = Object.assign(egg, {
Application,
// custom Loader, a dependence of the high level framework, needs to be exported.
AppWorkerLoader: YadanAppWorkerLoader,
});
AgentWorkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at agent.js instead of app.js.
Many descriptions of launch process are scattered at Multiprocess Model, Loader and Plugin, and here is a summarization.
startCluster is invoked with baseDir and framework, then Master process is launched.framework param.agent.js and other files.agent.js is able to be customized, and it supports asynchronous launch after which it notifies Master and invoke the function passed to beforeStart.You'd better read unittest first, which is similar to framework testing in a quantity of situations.
Here are some differences between initiation of frameworks.
const mock = require('@eggjs/mock');
describe('test/index.test.js', () => {
let app;
before(() => {
app = mock.app({
// test/fixtures/apps/example
baseDir: 'apps/example',
// importent !! Do not miss
framework: true,
});
return app.ready();
});
after(() => app.close());
afterEach(mock.restore);
it('should success', () => {
return app.httpRequest().get('/').expect(200);
});
});
test/fixtures, otherwise it should be absolute paths.framework option is indispensable, which could be a absolute path or true meaning the path of the framework to be current directory.ready event in before hook, or some of the APIs is not available.app.close() after testing, which could arouse the exhausting of fds, caused by unclosed log files.mm.app enables cache as default, which means new environment setting would not work once loaded.
const mock = require('@eggjs/mock');
describe('/test/index.test.js', () => {
let app;
afterEach(() => app.close());
it('should test on local', () => {
mock.env('local');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
it('should test on prod', () => {
mock.env('prod');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
});
Multiprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model.
The option of mock.cluster have no difference with mm.app while their APIs are totally distinct, however, SuperTest still works.
const mock = require('@eggjs/mock');
describe('test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
afterEach(mock.restore);
it('should success', () => {
return app.httpRequest().get('/').expect(200);
});
});
Tests of stdout/stderr are also available, since mm.cluster is based on coffee in which multiprocess testing is supported.
const mock = require('@eggjs/mock');
describe('/test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
it('should get `started`', () => {
// set the expectation of console
app.expect('stdout', /started/);
});
});