本文主要分为以下三个部分
- 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
- 代码的自动化测试
- 线上回归