src/server.js

/**
 * 包含了整个服务端类,供`app.js`启动或测试调用。
 * @module server
 */

const http = require('http');
const Koa = require('koa');
const mongoose = require('mongoose');
const Redis = require('redis');
const Sio = require('socket.io');
const SioRedis = require('socket.io-redis');
const mailer = require('nodemailer');
const qs = require('koa-qs');
const Router = require('koa-router');
const serve = require('koa-static');
const mount = require('koa-mount');
const koaLogger = require('./koa-logger');
const Models = require('./models');
const Api = require('./api');
const {promisify} = require('./utils');
const redisCommands = require('redis-commands');
const {loadTaskTemplates} = require('./task-template');

mongoose.Promise = Promise;

redisCommands.list.forEach(key =>
  Redis.RedisClient.prototype[key + 'Async'] = promisify(Redis.RedisClient.prototype[key])
);
['exec', 'exec_atomic'].forEach(key =>
  Redis.Multi.prototype[key + 'Async'] = promisify(Redis.Multi.prototype[key])
);

/**
 * 整个服务端类。这里初始化了整个项目传递各种对象的`global`对象。
 *
 * `global`对象包含以下几个字段:
 * 1. `config`:项目的配置
 * 2. `db`、`redis`:Mongoose链接和Redis链接
 * 3. `sio`:Socket.IO对象
 * 4. `email`:Nodemailer对象
 * 5. `users`...:各种models
 *
 * 对于Koa的中间件来讲,这个对象可通过`ctx.global`获得。
 */
class Server {
  /**
   * 启动服务端,初始化所有model和路由等等。
   *
   * @param config {object} 项目配置,参见`example.config.json`
   * @returns {Promise.<void>} 监听成功后resolve,否则reject
   */
  async start(config) {
    /* ==== 初始化上下文环境 ==== */
    config = Server.normalizeConfig(config || {});
    const app = this.app = new Koa();
    app.proxy = true;
    const db = await mongoose.createConnection(config.db,
      {useMongoClient: true});
    const redis = Redis.createClient(config.redis);
    const sioRedis = Redis.createClient(config.redis);
    const server = http.createServer(app.callback());
    const sio = Sio(server);
    const taskTemplates = await loadTaskTemplates(config['task-template-dir']);
    sio.adapter(SioRedis({
      pubClient: redis,
      subClient: sioRedis
    }));
    const global = {
      config,              // 配置选项
      db,                  // MongoDB数据库的连接
      redis,               // Redis数据库的连接
      sioRedis,            // Redis数据库的连接,专门用于Socket.IO的监听事件
      server,              // HTTP server实例
      sio,                 // Socket.IO服务端
      taskTemplates: taskTemplates.ids,
      taskTemplatesByFile: taskTemplates.files
    };
    if (config.email)
      global.email = mailer.createTransport(config.email); // E-mail邮件传输
    const models = await Models(global);
    Object.assign(global, models); // 各种Models
    app.context.global = global;
    /* ==== 设置路由 ==== */
    qs(app);
    app.use(koaLogger);
    const router = new Router();
    const api = Api(global);
    router.use('/api', api.routes(), api.allowedMethods());
    app.use(router.routes());
    app.use(router.allowedMethods());
    if (config.static) {
      app.use(mount('/uploads', serve('uploads')));
      app.use(serve('public'));
    }
    if (config.port !== undefined)
      await new Promise((resolve, reject) =>
        server
          .listen(config.port, config.host, resolve)
          .once('error', reject)
      );
  }

  /**
   * 将默认的项目配置与config对象合并后返回。
   *
   * @param config {object} 项目配置
   * @return {object} 添加了默认配置的项目配置
   */
  static normalizeConfig(config) {
    const defaultConfig = {
      name: 'Crowd Sourcing',
      host: 'localhost',
      db: 'mongodb://localhost/crowdsource',
      redis: 'redis://localhost/',
      'upload-dir': 'uploads',
      'task-template-dir': './task-templates',
      'temp-dir': 'temp',
      'static': false
    };
    Object.assign(defaultConfig, config);
    if (defaultConfig.site === undefined)
      defaultConfig.site = `http://${defaultConfig.host}:${defaultConfig.port}`;
    return defaultConfig;
  }

  /**
   * 停止服务器,停止完毕后可以再调用调用`start()`
   *
   * @returns {Promise.<void>} 完成后resolve,否则reject
   */
  async stop() {
    const {db, redis, sioRedis, server, sio} = this.app.context.global;
    redis.quit();
    sioRedis.quit();
    await Promise.all([
      new Promise((resolve, reject) => server.close(resolve)),
      new Promise((resolve, reject) => sio.close(resolve)),
      db.close()
    ]);
  }
}

module.exports = Server;