前几小节铺垫了那么多,这一小节我们将使用前几小节介绍的和其他一些 koa 的中间件开发一个简单的论坛系统。Nodeclub 是一个优秀的开源论坛系统,已在 Node.js 中文技术社区 CNode(http://cnodejs.org) 得到应用,我们仿照并借鉴 Nodeclub 从头搭建一个论坛系统。首先,构建基础项目结构如下:

7.1.1

其中需要说明的有:

  • bin/start:启动脚本
  • config/:存放配置文件的目录,default.scheme.js 为 koa-scheme 所用
  • lib/:存放一般代码文件的目录
  • models/:存放模型文件的目录
  • routes/:存放路由文件的目录
  • theme/:存放主题模版文件的目录
  • test/:存放测试文件的目录

修改 package.json,添加如下内容:

{
  "name": "N-club",
  "version": "0.0.1",
  "description": "N-club for koa.",
  "scripts": {
    "start": "NODE_ENV=default DEBUG=* node --harmony app"
  },
  "dependencies": {
    "co-cache": "2.3.0",
    "co-ejs": "1.5.2",
    "config-lite": "1.5.0",
    "koa": "1.2.4",
    "koa-bodyparser": "2.3.0",
    "koa-errorhandler": "0.1.1",
    "koa-flash": "1.0.0",
    "koa-frouter": "0.5.0",
    "koa-generic-session": "1.11.4",
    "koa-generic-session-mongo": "0.3.1",
    "koa-gzip": "0.1.0",
    "koa-logger": "1.3.0",
    "koa-router-cache": "2.0.0",
    "koa-scheme": "2.2.1",
    "koa-static-cache": "3.1.7",
    "merge-descriptors": "1.0.1",
    "mongoose": "4.2.9",
    "validator": "6.2.0"
  },
  "engines": {
    "node": ">=4"
  }
}

保存并运行 npm install,其中:

  • koa-bodyparser:请求体解析中间件,相当于 express 中的 body-parser
  • koa-flash:相当于 connect-flash
  • koa-generic-session:通用的 session 中间件,可结合 mongodb、redis等使用
  • koa-generic-session-mongo:结合 koa-generic-session,将 session 存储到 mongodb 的中间件
  • koa-static-cache:静态文件缓存中间件
  • merge-descriptors:合并两个对象的工具模块
  • mongoose:mongodb 驱动模块
  • validator:参数验证工具模块

修改 app.js,添加如下代码:

var app = require('koa')();
var logger = require('koa-logger');
var bodyparser = require('koa-bodyparser');
var staticCache = require('koa-static-cache');
var errorhandler = require('koa-errorhandler');
var session = require('koa-generic-session');
var MongoStore = require('koa-generic-session-mongo');
var flash = require('koa-flash');
var gzip = require('koa-gzip');
var scheme = require('koa-scheme');
var router = require('koa-frouter');
var routerCache = require('koa-router-cache');
var render = require('co-ejs');
var config = require('config-lite');

// 不放到 default.js 是为了避免循环依赖
var merge = require('merge-descriptors');
var core = require('./lib/core');
var renderConf = require(config.renderConf);
merge(renderConf.locals || {}, core, false);

app.keys = [renderConf.locals.$app.name];

app.use(errorhandler());
app.use(bodyparser());
app.use(staticCache(config.staticCacheConf));
app.use(logger());
app.use(session({
  store: new MongoStore(config.mongodb)
}));
app.use(flash());
app.use(scheme(config.schemeConf));
app.use(routerCache(app, config.routerCacheConf));
app.use(gzip());
app.use(render(app, renderConf));
app.use(router(app, config.routerConf));

app.listen(config.port, function () {
  console.log('Server listening on: ', config.port);
});

中间件的加载顺序十分重要,如上面的 errorhandler 中间件需要放到最上面,这样才能捕获下游抛出的错误。flash 中间件需要放到 session 中间件之后,因为 flash 功能是基于 session 实现的。routerCache 需要放到 router 前面,而 scheme 需要放到 routerCache 前面。 gzip 压缩中间件需要放到 routerCache 之后,这样 routerCache 缓存的就是 gzip 压缩后的内容了,大大减少了内存消耗量。

修改 default.js ,添加如下代码:

var path = require('path');
var cache = require('koa-router-cache');
var MemoryCache = cache.MemoryCache;

module.exports = {
  port: process.env.PORT || 3000,
  mongodb: {
    url: 'mongodb://127.0.0.1:27017/club'
  },
  schemeConf: path.join(__dirname, './default.scheme'),
  staticCacheConf: path.join(__dirname, '../theme/publices'),
  renderConf: path.join(__dirname, '../theme/config'),
  routerConf: 'routes',
  routerCacheConf: {
    'GET /': {
      key: 'cache:index',
      expire: 10 * 1000,
      get: MemoryCache.get,
      set: MemoryCache.set,
      destroy: MemoryCache.destroy,
      passthrough: function* passthrough(_cache) {
        // 游客
        if (!this.session || !this.session.user) {
          if (_cache == null) {
            return {
              shouldCache: true,
              shouldPass: true
            };
          }
          this.type = 'text/html; charset=utf-8';
          this.set('content-encoding', 'gzip');
          this.body = _cache;
          return {
            shouldCache: true,
            shouldPass: false
          };
        }
        // 已登录用户
        return {
          shouldCache: false,
          shouldPass: true
        };
      }
    }
  }
};

我们尽量把 app.js 中使用的配置信息放到了配置文件里,其中 ./lib/core.js 是暴漏出来的核心文件,将它与模版中自定义的 locals 合并作为 co-ejs 渲染时的本地变量,模板中还自定义了一个 $app 变量,保存了模版的主题信息。我们规定模板目录下的 publices 目录用来存放静态文件,config.js 保存了 co-ejs 的配置。我们还针对未登录的用户对主页进行了缓存,并设置 10 秒生存期。当命中缓存的时候,直接将缓存中的数据赋值到 this.body 返回,并设置正确的 header。shouldCache 控制是否缓存该路由,游客为 true,已登录用户则为 false;shouldPass 控制是否允许通过并执行后面的路由,如果缓存为空则通过,否则不通过。注意 shouldCache 和 shouldPass 是有语义和功能上的区别的。