egg/lib/application.js

219 lines
5.9 KiB
JavaScript

'use strict';
const path = require('path');
const graceful = require('graceful');
const http = require('http');
const EggApplication = require('./egg');
const AppWorkerLoader = require('./loader').AppWorkerLoader;
const cluster = require('cluster-client');
const { assign } = require('utility');
const KEYS = Symbol('Application#keys');
const HELPER = Symbol('Application#Helper');
const LOCALS = Symbol('Application#locals');
const LOCALS_LIST = Symbol('Application#localsList');
const EGG_LOADER = Symbol.for('egg#loader');
const EGG_PATH = Symbol.for('egg#eggPath');
const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients');
/**
* Singleton instance in App Worker, extend {@link EggApplication}
* @extends EggApplication
*/
class Application extends EggApplication {
/**
* @constructor
* @param {Object} options - see {@link EggApplication}
*/
constructor(options = {}) {
options.type = 'application';
super(options);
try {
this.loader.load();
} catch (e) {
// close gracefully
this[CLUSTER_CLIENTS].forEach(cluster.close);
throw e;
}
// dump config after loaded, ensure all the dynamic modifications will be recorded
this.dumpConfig();
this.bindEvents();
}
get [EGG_LOADER]() {
return AppWorkerLoader;
}
get [EGG_PATH]() {
return path.join(__dirname, '..');
}
onServer(server) {
/* istanbul ignore next */
graceful({
server: [ server ],
error: (err, throwErrorCount) => {
if (err.message) {
err.message += ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')';
}
this.coreLogger.error(err);
},
});
}
/**
* global locals for view
* @member {Object} Application#locals
* @see Context#locals
*/
get locals() {
if (!this[LOCALS]) {
this[LOCALS] = {};
}
if (this[LOCALS_LIST] && this[LOCALS_LIST].length) {
assign(this[LOCALS], this[LOCALS_LIST]);
this[LOCALS_LIST] = null;
}
return this[LOCALS];
}
set locals(val) {
if (!this[LOCALS_LIST]) {
this[LOCALS_LIST] = [];
}
this[LOCALS_LIST].push(val);
}
/**
* Create egg context
* @method Application#createContext
* @param {Req} req - node native Request object
* @param {Res} res - node native Response object
* @return {Context} context object
*/
createContext(req, res) {
const app = this;
const context = Object.create(app.context);
const request = context.request = Object.create(app.request);
const response = context.response = Object.create(app.response);
context.app = request.app = response.app = app;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
/**
* Request start time
* @member {Number} Context#starttime
*/
context.starttime = Date.now();
return context;
}
/**
* Create an anonymouse context, the context isn't request level, so the request is mocked.
* then you can use context level API like `ctx.service`
* @member {String} Application#createAnonymousContext
* @param {Request} req - if you want to mock request like querystring, you can pass an object to this function.
* @return {Context} context
*/
createAnonymousContext(req) {
const request = {
headers: {
'x-forwarded-for': '127.0.0.1',
},
query: {},
querystring: '',
host: '127.0.0.1',
hostname: '127.0.0.1',
protocol: 'http',
secure: 'false',
method: 'GET',
url: '/',
path: '/',
socket: {
remoteAddress: '127.0.0.1',
remotePort: 7001,
},
};
if (req) {
for (const key in req) {
if (key === 'headers' || key === 'query' || key === 'socket') {
Object.assign(request[key], req[key]);
} else {
request[key] = req[key];
}
}
}
const response = new http.ServerResponse(request);
return this.createContext(request, response);
}
/**
* Run generator function in the background
* @see Context#runInBackground
* @param {Generator} scope - generator function, the first args is an anonymous ctx
*/
runInBackground(scope) {
const ctx = this.createAnonymousContext();
ctx.runInBackground(scope);
}
/**
* secret key for Application
* @member {String} Application#keys
*/
get keys() {
if (!this[KEYS]) {
if (!this.config.keys) {
if (this.config.env === 'local' || this.config.env === 'unittest') {
const configPath = path.join(this.config.baseDir, 'config/config.default.js');
console.error('Cookie need secret key to sign and encrypt.');
console.error('Please add `config.keys` in %s', configPath);
}
throw new Error('Please set config.keys first');
}
this[KEYS] = this.config.keys.split(',').map(s => s.trim());
}
return this[KEYS];
}
/**
* The helper class, `app/helper` will be loaded to it's prototype,
* then you can use all method on `ctx.helper`
* @member {Helper} Application#Helper
*/
get Helper() {
if (!this[HELPER]) {
this[HELPER] = class Helper extends this.BaseContextClass {};
}
return this[HELPER];
}
/**
* bind app's events
*
* @private
*/
bindEvents() {
// Browser Cookie Limits: http://browsercookielimits.squawky.net/
this.on('cookieLimitExceed', ({ name, value, ctx }) => {
const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`);
err.name = 'CookieLimitExceedError';
err.cookie = value;
ctx.coreLogger.error(err);
});
// expose server to support websocket
this.on('server', server => this.onServer(server));
}
}
module.exports = Application;