Thanks to the active front-end community, the NodeJS application scene has become more and more rich in recent years, and JS has gradually become able to do this as well as that. The author has also been on the wave of NodeJS full stack applications, and has made NodeJS applications with tens of millions of daily visits. This article will summarize some of the “knowledge points” :
- The layered design
- Testability design
- Process Management (a little talk)
The layered design
I’ve always liked the quote by Martin Fowler in His book Design Patterns for Enterprise Application Architecture that “what I appreciate most about architectural design is layers.” With this in mind, I’ve learned a lot about code design, and it seems a lot easier to go back and draw the following diagram based on existing code:
The structure of the P.S. subject has been reconstructed 4 times in pursuit of perfection in mind [smirk].
The code starts out as a flow chart, which gradually becomes a structural design through refactoring
Global dependency: configuration
A Web application will inevitably need to run in several different environments, so configuration management becomes unnecessary. I don’t think you want to have many situations like this in your code:
if (process.env.NODE_ENV === 'prod') {
// set some value
}
Copy the code
So, I used process.env.node_env to get the corresponding configuration. For example, in my application root directory, I have the following folder:
-config-dev.js # locally developed configuration -test.js # unit test configuration -daily.js # integrated test environment configuration -online.js # online configuration -static.js # static configurationCopy the code
Then, in the main container, get the corresponding configuration
const envKey = process.env.NODE_ENV || 'dev';
const config = require('./config/' + envKey);
Copy the code
For example, the development environment dev.js:
const staticConfig = require('./static'); Const merge = require('lodash/merge'); Const config = merge(staticConfig, {mongoUrl: 'mongodb://127.0.0.1/dev-db'}); module.exports = config;Copy the code
Global dependencies: logs
Logging may be one of the few areas covered by the front end, but it is very important in a NodeJS application. Its functions include:
- Data records
- troubleshooting
Let’s take a look at a common piece of middleware code that logs application requests:
function listenResOver(res, cb) { const onfinish = done.bind(null, 'finish'); const onclose = done.bind(null, 'close'); res.once('finish', onfinish); res.once('close', onclose); function done(event){ res.removeListener('finish', onfinish); res.removeListener('close', onclose); cb && cb(); Exports = function(logger, config, appmodule. exports) Function log(CTX) {if(ctx.status === 200) {// If (ctx.status === 200) {// If (ctx.status === 200) logger.info(`>>>log.res-end:${ctx.href}>>>${ctx.mIP}>>>cost(${Date.now() - ctx.mBeginTime}ms)`); Logger. info(' >>>log.res-error:${ctx.href} error with status ${ctx.status}. '); } } app.use(function*(next) { this.mBeginTime = Date.now(); const mIP = this.header['x-real-ip'] || this.ip || ''; this.mIP = mIP; const ctx = this; listenResOver(this.res, () => { log(ctx); }); yield next; }); };Copy the code
Common NodeJS log modules are log4js and Bunyan
Secondary dependencies: the model layer
Some schemas such as Mongoose and Redis can be placed in this layer and managed by an object, such as:
function modTool(logger, config){
const map = {};
async function setMod(key, modFactory) {
map[key] = await modFactory(logger, config);
};
function getMod(key) {
return map[key];
};
return {
setMod,
getMod,
};
};
export default modTool;
Copy the code
For example, you might have a Mongo management tool:
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
async function mongoFactory(logger, config) {
const MONGO_URL = config.get('mongoUrl');
const map = {};
function getModel(modelName) {
if(map[modelName]) {
return map[modelName];
}
let schema = require(`./model/${modelName}`);
let model = mongoose.model(modelName, schema);
map[modelName] = model;
return model;
}
await mongoose.connect(mongoUrl, {
useMongoClient: true,
});
return { getModel };
}
export default mongoFactory;
Copy the code
The main container layer
The main container layer plays a series role, initializing the global dependencies, and then injecting the global dependencies [Logger, Config, modTool] into each middleware through a loader CLmLoader, and then connecting all middleware through a main routing middleware CL-Router.
The approximate code is as follows:
const appStartTime = Date.now(); const envKey = process.env.NODE_ENV || 'dev'; Const config = require('./config/' + envKey); const rootPath = config.get('rootPath'); Const logger = require(' ${rootPath}/utils/logger ')(config); const modTool = require(`${rootPath}/utils/mod-tool`)(logger, config); //# initialize Koa const Koa = require(' Koa '); const app = koa(); app.on('error', e => { logger.error('>>>app.error:'); logger.error(e); }); //# const loadModules = require('clmloader'); //# mainRouterFunc = require('cl-router'); const co = require('co'); Function *(){const deps = [logger, config, modTool]; yield modTool.addMod('mongo', modFactory); // Middleware const middlewareMap = yield loadModules({path: '${rootPath}/middlewares', deps: deps}); // const interfaces = yield loadModules({path: '${rootPath}/interfaces', deps: deps, attach: {commonMiddlewares: ['common', 'i-helper', 'csrf'], type: 'interface', } }); const routerMap = { i: interfaces, }; app.keys = [config.get('appKey')]; App. use(mainRouterFunc({middlewareMap, middlewareMap routerMap, // route Map defaultRouter: [' I ', 'index'], // Set default route logger,}); app.listen(config.get('port'), () => { logger.info(`App start cost ${Date.now() - appStartTime}ms. Listen ${port}.`); }); }).catch(e => { logger.fatal('>>>init.fatal-error:'); logger.fatal(e); });Copy the code
The middleware layer
There are two types of middleware: generic middleware and routing middleware. Routed middleware is the last piece of middleware in the Onion model (P.S. search [smirk] if you don’t know) and can no longer be placed behind other middleware.
Case of general middleware middlewares/post/index. Js:
const koaBody = require('koa-body');
module.exports = function(logger, config) {
return Promise.resolve({
middlewares: [koaBody()]
});
};
Copy the code
Interface middleware interface/example/index. Js;
module.exports = function(logger, config) {
return Promise.resolve({
middlewares: ['post', function*(next) {
const { name } = this.request.body;
this.body = JSON.stringify({
msg: `Hello, ${name}`,
});
}]
});
};
Copy the code
Test design
There are so many layers of design, most of which are designed for maintainability, and as the most critical part of the test for maintainability, of course, can not be less. If we mock out the global dependencies [Logger, config] and the main container layer, it’s not hard to isolate and test a single piece of middleware, as shown here:
The Mock code implements helper.js:
const path = require('path'); const should = require('should'); const rootPath = path.normalize(__dirname + '/.. '); const co = require('co'); const koa = require('koa'); Const testConfig = require(' ${rootPath}/config/test '); // mock out logger const sinon = require('sinon'); const testLogger = { info: sinon.spy(), debug: console.log, fatal: sinon.spy(), error: sinon.spy(), warn: sinon.spy() }; / / you can choose the configuration of the building function buildConfig (config) {config. DepMiddlewares = config. DepMiddlewares | | []; const l = config.logger || testLogger; const c = config.config || testConfig; const deps = [l, c, mdt]; config.mdt = mdt; config.deps = config.deps || deps; config.ctx = config.ctx || {}; const dir = config.dir = config.dir || 'interfaces'; config.defaultFile = dir === 'interfaces' ? 'index': 'node.main'; config.before = config.before || function*(next){ yield next; }; config.after = config.after || function*(next){ if(dir === 'interfaces') { this.body = this.body || '{ "status": 200, "data":"hello, world"}'; } else { this.body = this.body || 'hello, world'; } yield next; }; config.middlewares = config.middlewares || []; return config; // * Middlewares: middleware array, such as [' POST '] // * routerName: route name // * deps: factory-passed parameter array // * before: Add middleware before middleware, test using // * after: add middleware after middleware, test using // * config: custom configuration, default is testConfig // * logger: // * attach: attach // * dir: Koa function mockRouter(config) {const {name, depMiddlewares, deps, before, after, CTX, dir, defaultFile, middlewares, } = buildConfig(config); const routerName = name; return co(function*(){ const rFunc = require(`${rootPath}/${dir}/${routerName}/${defaultFile}`); const router = yield rFunc.apply(this, deps); router.name = routerName; router.path = `${rootPath}/${dir}/${routerName}`; router.type = dir === 'interfaces' ? 'interface': 'page'; middlewares = middlewares.concat(router.middlewares); const ms = []; for (let i = 0, l = middlewares.length; i < l ; i++) { let m = middlewares[i]; if(typeof m === 'string') { let mFunc = require(`${rootPath}/middlewares/${m}/`); let mItem = yield mFunc.apply(this, deps); ms = ms.concat(mItem.middlewares); } else if(m.constructor.name === 'GeneratorFunction') { ms.push(m); } } const app = koa(); app.keys = ['test.helper']; const keys = Object.keys(ctx); ms.unshift(before); ms.push(after); app.use(function*(next){ const tCtx = this; keys.forEach(key => { tCtx[key] = ctx[key]; }); for (let i = ms.length - 1; i >= 0; i--) { next = ms[i].call(this, next); } this.gRouter = router; this.gRouterKeys = ['i', routerName]; if(next.next) { yield *next; } else { yield next; }}); return app; }); } global._TEST = { rootPath, testConfig, testLogger, mockRouter, }; module.exports = global._TEST;Copy the code
The test code for the above case interface can be written as follows:
const { mockRouter, } = _TEST; const request = require('supertest'); Describe (' interface test ', () => {it('should return hello ${name}', async () => {const app = yield mockRouter({name: 'example' }); const res = await request(app.listen()) .post('/') .send({ name: 'yushan' }) .expect(200) res.msg.should.be.equal('Hello, yushan'); }); });Copy the code
Process management
Process management can be used in scenarios such as:
- If the access magnitude is less than a million, PM2 is sufficient
- If you are considering horizontal expansion, and the company has a mature environment (P.S. used Ali Cloud container solution, a handful of bitter tears), you can use Docker solution
The last
The above code should not be used to run, it may be wrong, oh, when writing the article to the original code to do the fifth reconstruction, but this time there is no test.
Creative Commons Attribution – Non-commercial Use – Same way Share 4.0 International License