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