Koa 和 Express 的设计风格非常类似,底层也都是共用的同一套 HTTP 基础库,但是有几个显著的区别,除了上面提到的默认异步解决方案之外,主要的特点还有下面几个。
Middleware
Koa 的中间件和 Express 不同,Koa 选择了洋葱圈模型。
所有的请求经过一个中间件的时候都会执行两次,对比 Express 形式的中间件,Koa 的模型可以非常方便的实现后置处理逻辑,对比 Koa 和 Express 的 Compress 中间件就可以明显的感受到 Koa 中间件模型的优势。
Context
和 Express 只有 Request 和 Response 两个对象不同,Koa 增加了一个 Context 的对象,作为这次请求的上下文对象(在 Koa 1 中为中间件的 this,在 Koa 2 中作为中间件的第一个参数传入)。
我们可以将一次请求相关的上下文都挂载到这个对象上。类似 traceId 这种需要贯穿整个请求(在后续任何一个地方进行其他调用都需要用到)的属性就可以挂载上去。相较于 request 和 response 而言更加符合语义。
同时 Context 上也挂载了 Request 和 Response 两个对象。和 Express 类似,这两个对象都提供了大量的便捷方法辅助开发,例如
- get request.query
- get request.hostname
- set response.body
- set response.status
异常处理
通过同步方式编写异步代码带来的另外一个非常大的好处就是异常处理非常自然,使用 try catch
就可以将按照规范编写的代码中的所有错误都捕获到。这样我们可以很便捷的编写一个自定义的错误处理中间件。
1 2 3 4 5 6 7 8 9
| async function onerror(ctx, next) { try { await next(); } catch (err) { ctx.app.emit("error", err); ctx.body = "server error"; ctx.status = err.status || 500; } }
|
只需要将这个中间件放在其他中间件之前,就可以捕获它们所有的同步或者异步代码中抛出的异常了。
注:以上所有皆来自Egg.js 与 Koa
中间件模拟
Express
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| const http = require("http"); const slice = Array.prototype.slice;
class LikeExpress { constructor() { this.routes = { all: [], get: [], post: [] }; }
register(path) { const info = {}; if (typeof path === "string") { info.path = path; info.stack = slice.call(arguments, 1); } else { info.path = "/"; info.stack = slice.call(arguments, 0); } return info; }
use() { const info = this.register.apply(this, arguments); this.routes.all.push(info); }
get() { const info = this.register.apply(this, arguments); this.routes.get.push(info); }
post() { const info = this.register.apply(this, arguments); this.routes.post.push(info); }
match(method, url) { let stack = []; if (url === "/favicon.ico") { return stack; }
let curRoutes = []; curRoutes = curRoutes.concat(this.routes.all); curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach(routeInfo => { if (url.indexOf(routeInfo.path) === 0) { stack = stack.concat(routeInfo.stack); } }); return stack; }
handle(req, res, stack) { const next = () => { const middleware = stack.shift(); if (middleware) { middleware(req, res, next); } }; next(); }
callback() { return (req, res) => { res.json = data => { res.setHeader("Content-type", "application/json"); res.end(JSON.stringify(data)); }; const url = req.url; const method = req.method.toLowerCase();
const resultList = this.match(method, url); this.handle(req, res, resultList); }; }
listen(...args) { const server = http.createServer(this.callback()); server.listen(...args); } }
module.exports = () => { return new LikeExpress(); };
|
Koa
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 57 58
| const http = require("http");
function compose(middlewareList) { return function(ctx) { function dispatch(i) { const fn = middlewareList[i]; try { return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)) ); } catch (err) { return Promise.reject(err); } } return dispatch(0); }; }
class LikeKoa2 { constructor() { this.middlewareList = []; }
use(fn) { this.middlewareList.push(fn); return this; }
createContext(req, res) { const ctx = { req, res }; ctx.query = req.query; return ctx; }
handleRequest(ctx, fn) { return fn(ctx); }
callback() { const fn = compose(this.middlewareList);
return (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; }
listen(...args) { const server = http.createServer(this.callback()); server.listen(...args); } }
module.exports = LikeKoa2;
|
项目实践
Node.js 从零开发 web server博客项目,并使用express、koa2重构