Koa

介绍

Koa 是基于 Node.js 的 Web 框架,其特点是轻量、健壮、富有表现力。

说到 Koa 肯定要说下 Express,也是 Node.js 的Web框架,Express 4 之前的版本主要基于 Connect,封装了大量便利的功能,如路由、视图处理、错误处理等。Express 4 之后不再依赖 Connect,除 express.static 外的内置中间件也全部作为单独模块安装。Express 主要采用 ES5 的语法,异步操作通过回调函数来处理。相较而言,Koa 对异步操作的处理更加简单,开发者不再需要面对讨厌的“回调地狱”。这是因为 Koa1 和 Koa2 分别采用了ES6 中的 Generator/yield + Promise 语句和 ES7 中的 async/await + Promise 来处理异步操作。

安装和起服务

  • 新建项目目录 koa-nodejs
  • cd koa-nodejsnpm init -ynpm install koa -S
  • 新建文件 app.js,然后输入内容如下。
  • node app.js 服务起好,浏览器就可以访问了,不过此时内容是 Not Found,不着急。
  • app.js 中加个中间件。
// app.js
const Koa = require('koa');
const app = new Koa()
const PORT = 2020;

// 中间件
app.use(async (ctx, next) => {
  await next();
  ctx.response.text = 'text/html';
  ctx.response.body = '你好,新朋友!';
});

app.listen(PORT, () => {
  console.log(`server is running at http://localhost:${PORT}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上下文

Koa 将 Node.js的Request(请求)和 Response(响应)对象封装到 Context 对象中,Context 对象称为一次对话的上下文,通过加工 Context 对象控制返回给用户的内容。

Context 对象还内置了一些常用属性,如 context.statecontext.appcontext.cookiescontext.throw 等,也可以在 Context 对象中自定义一些属性、配置以供全局使用。

常用属性和方法

  • ctx.request 是 Koa 的 Request 对象,是在 Node.js 的请求对象之上的抽象。
  • ctx.response是 Koa 的 Response 对象,是在 Node.js 的原生响应对象之上的抽象。
  • ctx.state 是推荐的命名空间,用于通过中间件传递信息和前端视图。类似 koa-views 这些渲染 View 层的中间件也会默认把 ctx.state 里面的属性作为 View 的上下文传入。
  • ctx.cookies用于获取和设置 Cookie。
  • ctx.throw 用于抛出错误,把错误信息返回给用户。

中间件

中间件函数是一个带有 ctxnext 两个参数的简单函数。

  • ctx 就是上下文,封装了 RequestResponse 等对象;
  • next用于把中间件的执行权交给下游的中间件。

next() 之前使用 await 关键字是因为 next() 会返回一个 Promise 对象,而在当前中间件中位于 next() 之后的代码会暂停执行,直到最后一个中间件执行完毕后,再自下而上依次执行每个中间件中 next() 之后的代码,类似于一种先进后出的堆栈结构。这里用官方给出的 “洋葱模型”示意图来解释中间件的执行顺序。

洋葱模型

简单演示

app.use(async (ctx, next) => {
  console.log('1');
  await next();
  console.log('6');
});

app.use(async (ctx, next) => {
  console.log('2');
  await next();
  console.log('5');
});

app.use(async (ctx, next) => {
  console.log('3');
  await next();
  console.log('4');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

控制台 1 启动服务 node app.js,控制台 2 输入 curl http://localhost:2000,就会输出

1
2
3
4
5
6
1
2
3
4
5
6

koa-compose

上面 3 个中间件,可以将多个中间件合并为一个单一的中间件组合使用,便于重用和导出。

// app.js
const compose = require('koa-compose');
const { compose1_6, compose2_5, compose3_4 } = require('./middleware/compose');
const composeAll = compose([compose1_6, compose2_5, compose3_4]);
app.use(composeAll);
1
2
3
4
5
// ./middleware/compose.js
const compose1_6 = async (ctx, next) => {
  const startTime = new Date().getTime();
  console.log('1');
  await next();
  const endTime = new Date().getTime();
  console.log('6')
  console.log(`请求地址:${ctx.path}, 使用时间:${endTime - startTime}`);
};

const compose2_5 = async (ctx, next) => {
  console.log('2')
  await next()
  console.log('5')
}

const compose3_4 = async (ctx, next) => {
  console.log('3')
  await next()
  console.log('4')
}

module.exports = {
  compose1_6,
  compose2_5,
  compose3_4
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

koa-bodyparser

POST 请求的参数解析到 ctx.request.body 中。

log4js

npm i log4js -S
1

1.简单日志

// middleware/mi-log/logger.js
const log4js = require('log4js');

module.exports = (options) => {
  return async (ctx, next) => { 
    const start = Date.now();
    log4js.configure({
      appenders: {
        cheese: { type: 'file', filename: 'cheese.log' },
      },
      categories: {
        default: { appenders: ['cheese'], level: 'info' }
      }
    });
    const logger = log4js.getLogger('cheese');
    await next();
    const end = Date.now();
    const responseTime = end - start;
    logger.info(`响应时间: ${responseTime/1000} s`);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// middleware/mi-log/index.js
const logger = require('./logger');

module.exports = () => {
  return logger();
}
1
2
3
4
5
6
// middleware/index.js
const miLog = require('./mi-log');

module.exports = app => {
  app.use(miLog());
}
1
2
3
4
5
6
// app.js
const middleware = require('./middleware');

middleware(app);
1
2
3
4

2.日志挂载到上下文并切割日志

const log4js = require('log4js');
const methods = ['trace', 'debug', 'info', 'warn', 'fatal', 'error', 'mark'];

module.exports = (options) => {
  const contextLogger = {};
  // 切割日志配置
  log4js.configure({
    appenders: {
      cheese: {
        type: 'dateFile', // 日志类型
        filename: 'logs/task', // 文件名
        pattern: 'yyyy-MM-dd.log', // 文件名后缀
        alwaysIncludePattern: true  // 是否总有后缀
      },
    },
    categories: {
      default: { appenders: ['cheese'], level: 'info' },
    },
  })
  const logger = log4js.getLogger('cheese')

  return async (ctx, next) => {
    const start = Date.now();
    // 循环将 methods 所有方法都挂载到 ctx 上
    methods.forEach(method => {
      contextLogger[method] = (message) => {
        logger[method](message);
      }
    });
    ctx.log = contextLogger;
    await next();
    const end = Date.now();
    const responseTime = end - start;
    logger.info(`响应时间: ${responseTime/1000} s`);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • pattern 上面是以天为单位进行日志划分,当然也可以以小时为单位或以其他粒度进行日志划分。

3.环境区分

在项目开发中,一般会分为开发、测试、验证、线上等环境。在不同的环境中,记录日志的方式可能会不一样。比如在开发环境中不需要输出日志文件,只需在控制台输出日志信息即可。

  • 开发环境中,希望记录 debug 级别的日志,方便调试;
  • 线上环境中,希望记录 info 级别的日志,可以减少日志量。

为了便于代码维护,一般将这些日志的配置信息抽取出来,存储在配置文件中集中配置:

// middleware/mi-log/logger.js
const log4js = require('log4js');
const methods = ['trace', 'debug', 'info', 'warn', 'fatal', 'error', 'mark'];

// 抽取公共日志参数对象,不同环境配置
const baseInfo = {
  appLogLevel: 'debug', // 日志级别
  dir: 'logs', // 日志存放目录
  env: 'dev', // 当前环境
  projectName: 'koa2-tutarial' // 项目名,记录在日志中的项目信息
};

module.exports = (options) => {
  const contextLogger = {};
  const appenders = {};
  const opts = Object.assign({}, baseInfo, options || {});
  const { dir, env, appLogLevel } = opts;

  appenders.cheese = {
    type: 'dateFile', // 日志类型
    filename: `${dir}/task`, // 文件名
    pattern: 'yyyy-MM-dd.log', // 文件名后缀
    alwaysIncludePattern: true  // 是否总有后缀
  };
  if (['dev', 'development'].indexOf(env) > -1) {
    appenders.out = {
      type: 'console'
    };
  }
  let config = {
    appenders,
    categories: {
      default: {
        appenders: Object.keys(appenders),
        level: appLogLevel
      },
    },
  }
  log4js.configure(config);
  const logger = log4js.getLogger('cheese')

  return async (ctx, next) => {
    const start = Date.now();
    // 循环将 methods 所有方法都挂载到 ctx 上
    methods.forEach(method => {
      contextLogger[method] = (message) => {
        logger[method](message);
      }
    });
    ctx.log = contextLogger;
    await next();
    const end = Date.now();
    const responseTime = end - start;
    logger.info(`响应时间: ${responseTime/1000} s`);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

部署

pm2

npm i pm2 -g
1

1.启动守护和监控的应用

pm2 start app.js,基本信息包括:应用名id、模式、系统pid、状态、重启次数、当前状态运行时间、CPU占用率、内存占用、启动用户、是否热重启等。

2.命名应用

为了能够适合管理和区分应用,可以使用命令 pm2 start app.js --name xxx 来启动并命名应用。

3.参数

部署中执行一个复杂的命令并不方便维护,所以在一些复杂的环境配置需求中,可以使用独立的 YAML 或 JSON 格式的配置文件。

{
  "app": {
    "script": "./app.js",
    "instances": -1,
    "exec_mode": "cluster",
    "watch": true,
    "env": {
      "NODE_ENV": "prod"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

Node.js 是一个单进程异步模型,在目前主流的多核CPU环境下,不能很好地利用 CPU 资源。在不修改任何代码的情况下,cluster 执行模式可以让 Node.js 开发的 Web 服务动态分配请求给多个 CPU 内核,从而提升服务的性能。

  • script 需要执行脚本
  • instances 启动示例数量,会启动 CPU 的所有核数量减 1 的 cluster,留至少一个内核保证在高负载下 CPU 也能处理其他工作。
  • exec_mode 执行模式
  • watch 热重启,保证在修改代码后应用会立即重启,适合配置在开发测试环境中。一般线上环境应当另外执行 pm2 restart 命令。
  • env 环境变量

4.运行情况

pm2 list 命令观察应用的运行状况。如果发现应用出现重启、CPU占用过高或内存占用过高的情况,应当检查并修复问题。使用命令 pm2 stop name/id 可以暂停应用,暂停应用时需要注意如果有资源需要释放或事务需要结束(例如数据库连接),可以监听并拦截 SIGINT 信号,清理完毕后手动结束进程来关闭应用。

5.日志

PM2 提供的另外一个强大功能是日志,虽然开发者应当在应用中记录很多日志,但是对于一些受环境影响导致的意外崩溃,可以通过 PM2 查询。另外一些程序问题导致的意外错误,如果在应用日志中缺少记录,PM2日志也是最后一个寻找问题的关键。开发者只需执行命令 pm2 logs 即可,也可以通过命令 pm2 logs /reg/ 来做内容过滤。

6.监控

PM2 官方还提供了更加强大的在线监控报表系统 PM2 Plus,地址为 https://app.pm2.io/#/。但是开发者不可能一直观察监控界面。如果出现内存泄漏的情况,内存占用量会一直增加。为了防止内存泄漏导致服务不可用,在 PM2 中可以设置阈值 { "max_memery_restart": "200M" },达到阈值后会自动重启服务。

应用容器引擎 Docker