Node.js 路由设计&错误捕获&错误处理

2,691 阅读5分钟

本文主要分为以下三个部分

  • express 和 koa2 的中间件机制
  • Node.js 路由设计
  • Node.js 错误捕获&错误处理

express 和 koa2 的的中间件机制


express 与 koa2 是当下两大主流的 node 框架,下面我分别使用 express 和 koa2 实现了一个简单应用:

// express
var express = require('express')
var app = express()

app.use(function(req, res, next){
    console.log('start')
    next()
    console.log('end')
})

app.get('/', function (req, res) {
    res.send('Hello World!')
});

app.listen(3000);
// koa2
const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

从上面的代码中很容易就可以看出 express 和 koa 的中间件在写法上有很大差异,一个使用 callback 的写法,一个使用 async 写法,那我们就来看看它们在中间件实现上有什么差异。

express 的中间件机制

其实 express 中间件的原理很简单,express 内部维护一个函数数组,这个函数数组表示在发出响应之前要执行的所有函数,也就是中间件数组,每一次 use 以后,传进来的中间件就会推入到数组中,执行完毕后调用next方法执行函数的下一个函数,如果没用调用,调用就会终止。

下面我们实现一个简单的 Express 中间件功能:

function express() {
    var funcs = [] // 中间件存储的数组
    var app = function (req, res) {
        var i = 0  
        // 定义next()
        function next() {
            var task = funcs[i++]  // 取出中间件数组里的下一个中间件函数
            if (!task) {    // 如果中间件不存在,return
                return
            }
            task(req, res, next);   // 否则,执行下一个中间件
        }
        next()
    }
    // use方法则是将中间件函数推入到中间件数组中
    app.use = function (task) {
        funcs.push(task);
    }
    return app    // 返回实例
}

koa2 的中间件机制

koa 和 express 不同,因为没有 router 的实现,所有 this.middleware 就是普通的”中间件“函数而不会附加路由的逻辑; 所以中间处理的关键在compose方法, 它是一个独立的包 koa-compose, 把它拿了出来看一下里面的内容:

// compose.js

'use strict'

function compose (middleware) {

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

每个中间件是一个 async (ctx, next) => {}, 执行后返回的是一个 promise , 第二个参数 next 的值为 dispatch.bind(null, i + 1) , 用于传递”中间件“的执行,一个个中间件向里执行,直到最后一个中间件执行完,resolve 掉,它前一个”中间件“接着执行 await next() 后的代码,然后 resolve 掉,在不断向前直到第一个”中间件“ resolve 掉,最终使得最外层的 promise resolve 掉。

这里和 express 很不同的一点就是 koa 的响应的处理并不在"中间件"中,而是在中间件执行完返回的 promise resolve 后.

// express
const express = require('express')
const app = express()

app.use(function (req, res, next) {
    console.log('1');
    next()
    console.log('3');
    res.send('Hello!')
});

app.get('/', (req, res) => {
    console.log('2');
    res.send('World!')
})

app.listen(3000)

// Koa
const Koa = require('koa');
const app = new Koa();

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

// response

app.use(async ctx => {
    console.log(2)
    ctx.body = 'World';
});

app.listen(3001);

express 输出结果

1
2
3

koa 输出结果

1
2
3

Node.js 路由设计


express 路由的实现

Router 代表路由组件,负责应用程序的整个路由系统。组件内部由一个 layer 数组构成,每个 Layer 代表一组路径相同的路由信息,具体信息存储在 Route 内部,每个 Route 内部也是一个 Layer 对象,但是 Route 内部的 Layer 和 Router 内部的 Layer 是存在一定差异性的。

  • Router 内部的 Layer,主要包含 path、route 属性。
  • Route 内部的 Layer,主要包含 method、handle 属性。 如果一个请求来临,会先从头到尾扫描router内部的每一层,而处理每层的时候会对比URI,相同则扫描route的每一项,匹配成功则返回具体的信息,没有任何匹配则返回未找到。

应用场景

推荐参考 RESTful API 设计指南

router.get('/', function (req, res, next) {
  ...
  next();
});

router.get('/user/info', function(req, res){
  ...
  res.send('hello world');
});

router.get('/user/:id', function (req, res, next) {
  ...
  next();
});


router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
  ...
  res.send('commit range ' + from + '..' + to);
});

Node.js 错误捕获&错误处理


node.js 如何错误捕获

  • uncaughtException

捕获未捕获的异常,如果在程序执行期间抛出未捕获的异常,程序将崩溃。要解决此问题,需要在 process 对象上侦听 uncaughtException 事件:

process.on('uncaughtException', (err) => {
  console.error(err);
});
  • domains

try / catch 中无法处理异步方法调用抛出的错误。要解决这个问题,我们需要使用 domains。在node v0.8+版本的时候,发布了一个模块domain。这个模块做的就是try...catch所无法做到的:捕捉异步回调中出现的异常。 其中 run() 相当于 try, on('error') 相当于 catch

const domains = require(domain).create()
domains.on('error', function (){
    console.log('抛出:' + error)
})
domains.run(function(){
    setTimeout(()=>{
        a = 100
    },500)
})

  • callback(err)

通过回调返回错误是 Node.js 中最常见的错误处理模式。

let fn1 = function (obj,callback){
    if(obj !== 1){ return callback(new Error('error'))}
    return callback
}

fn1(123,(err)=>{
    if(err){
        
    }
    ...
})
  • emitter

当发出错误时,错误被广播给所有相关的订阅者,按照订阅顺序,间隔执行

const Events = require('events')
const emitter = new Events.EventEmitter()

var validateObject = function (a) {
    if(typeof a !== 'object') {
        emitter.emit('error', new Error('error'))
    }
}

emitter.on('error', function(err){
    console.log('Emitted:' + err.message)
})
validateObject('123')
  • Promise
fn().then().catch().finally()

  • Try...catch 捕获同步方法的异常,捕获 async/await 抛出的异常
async function f() {
    try {
        let response = await fetch('http://123.com')
    } catch (err) {
        console.log(err)
    }
}

fn()

如何错误处理

我们可以将把错误分成两大类:

  • 操作失败

    • 连接不到服务器
    • 无法解析主机名
    • 无效的用户输入
    • 请求超时
    • 服务器返回 500
    • 系统内存不足
  • 程序员失误

    • 读取 undefined 的一个属性
    • 调用异步函数没有指定回调
    • 该传对象的时候传了一个字符串
    • 该传IP地址的时候传了一个对象

处理操作失败

  • 直接处理。有的时候该做什么很清楚。
  • 把出错扩散到客户端。如果你不知道怎么处理这个异常,最简单的方式就是放弃你正在执行的操作,清理所有开始的,然后把错误传递给客户端。
  • 直接崩溃。对于那些本不可能发生的错误,或者由程序员失误导致的错误,可以记录一个错误日志然后直接崩溃。
  • 重试操作。对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题。
  • 记录错误。

处理程序员的失误

  • 自测
  • 代码 review
  • per-commit
  • 代码的自动化测试
  • 线上回归