koa 是一个web框架,通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理,使用起来非常方便
下面模拟实现一个精简版的Koa
初始化
初始化 Koa 对象
const Emitter = require("events");
class Koa extends Emitter {
constructor() {
super();
this.middlewares = [];
}
}
初始化 context
const context = {
_body: null,
req: null,
res: null,
get body() {
return this._body
},
set body(val) {
if (typeof val !== 'string') {
val = JSON.stringify(val)
}
this._body = val
this.res.end(this._body)
}
...
}
class Koa extends Emitter {
constructor() {
...
this.context = Object.create(context)
}
}
中间键
class Koa extends Emitter {
constructor() {
...
this.middlewares = []
}
use(middleware) {
...
this.middlewares.push(middleware)
}
}
启动 http 服务
class Koa extends Emitter {
callback(req, res) {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello World");
}
listen() {
const server = http.createServer(this.callback);
return server.listen(...arguments);
}
}
http 服务结合 compose 使用
class Koa extends Emitter {
constructor() {
super();
this.middlewares = [];
this.ctx = Object.create(context);
}
use(middleware) {
this.middlewares.push(middleware);
}
callback(req, res) {
res.statusCode = 200;
this.ctx.req = req;
this.ctx.res = res;
compose(this.middlewares)(this.ctx);
}
listen() {
const server = http.createServer(this.callback.bind(this));
return server.listen(...arguments);
}
}
完整源码
通过上面几个步骤,我们得到了下面的代码
const http = require("http");
const Emitter = require("events");
const compose = require("./compose"); //参考上页内容
const context = {
_body: null,
req: null,
res: null,
get body() {
return this._body;
},
set body(val) {
if (typeof val !== "string") {
val = JSON.stringify(val);
}
this._body = val;
this.res.end(this._body);
},
};
class Koa extends Emitter {
constructor() {
super();
this.middlewares = [];
this.ctx = Object.create(context);
}
use(middleware) {
this.middlewares.push(middleware);
}
callback(req, res) {
res.statusCode = 200;
this.ctx.req = req;
this.ctx.res = res;
compose(this.middlewares)(this.ctx);
}
listen() {
const server = http.createServer(this.callback.bind(this));
return server.listen(...arguments);
}
}
module.exports = Koa;
其中 compose 的源码可以参考 koa 中间键的洋葱模型 ,也可以使用 koa-compose
测试代码
const Koa = require("./koa");
const app = new Koa();
function middleware1() {
return async (ctx, next) => {
console.log("middleware1 start");
await next();
console.log("middleware1 end");
};
}
function middleware2() {
return async (ctx, next) => {
console.log("middleware2 start");
await next();
console.log("middleware2 end");
};
}
app.use(middleware1());
app.use(middleware2());
app.use(async (ctx, next) => {
ctx.body = {
status: 1,
};
});
console.log(app.middlewares);
app.listen(3000, () => {
console.log("sever listen http://localhost:3000");
});
访问 http://localhost:3000 ,控制台会输出,同时页面会显示 {"status": 1}
middleware1 start
middleware2 start
middleware2 end
middleware1 end
完善
完善请求处理
class Koa extends Emitter {
constructor() {
super();
this.middlewares = [];
}
callback() {
const fn = compose(this.middlewares);
const handleRequest = (req, res) => {
const ctx = creatContext(req, res);
this.handleRequest(ctx, fn);
};
return handleRequest;
}
use(middleware) {
this.middlewares.push(middleware);
}
handleRequest(ctx, fnMiddleware) {
fnMiddleware(ctx).then(() => {
if (!ctx.body) {
ctx.throw(404, "Not Found");
}
ctx.res.end(ctx.body);
});
}
listen() {
const server = http.createServer(this.callback());
server.listen(...arguments);
}
}
function creatContext(req, res) {
const ctx = Object.create(context);
ctx.req = req;
ctx.res = res;
return ctx;
}
重定向
const context = {
...
redirect(url) {
this.res.statusCode = 302
this.set('Location', url)
this.body = `Redirecting to ${url}.`
}
...
}
完善 url 解析和 query 参数处理
const { URL } = require("url");
function parseQuery(str) {
const res = Object.create(null);
str
.substr(1)
.split("&")
.forEach((item) => {
if (item) {
const i = item.search("=");
const key = item.substr(0, i);
const val = item.substr(i + 1);
res[key] = res[key]
? Array.isArray(res[key])
? [...res[key], val]
: [res[key], val]
: val;
}
});
return res;
}
const context = {
get path() {
return new URL(this.req.url, "http://localhost").pathname;
},
// 将?a=1&b=2 解析为 {a: "1", b: "2"}
get query() {
return parseQuery(new URL(this.req.url, "http://localhost").search);
},
};
其他
const context = {
...
get url() {
return this.req.url
},
set(filed, val) {
this.res.setHeader(filed, val)
},
throw(code, text) {
this.res.statusCode = code
this.res.end(text || 'Error')
},
get path() {
return this.req.url
}
...
}
完整代码
// koa.js
const Emitter = require("events");
const http = require("http");
const creatContext = require("./lib/context");
const compose = require("./lib/compose");
class Koa extends Emitter {
constructor() {
super();
this.middlewares = [];
}
callback() {
const fn = compose(this.middlewares);
const handleRequest = (req, res) => {
const ctx = creatContext(req, res);
this.handleRequest(ctx, fn);
};
return handleRequest;
}
use(middleware) {
// console.log(middleware)
this.middlewares.push(middleware);
}
handleRequest(ctx, fnMiddleware) {
fnMiddleware(ctx).then(() => {
if (!ctx.body) {
ctx.throw(404, "Not Found");
}
ctx.res.end(ctx.body);
});
}
listen() {
const server = http.createServer(this.callback());
server.listen(...arguments);
}
}
module.exports = Koa;
上下文 context
// context.js
const { URL } = require("url");
function parseQuery(str) {
const res = Object.create(null);
str
.substr(1)
.split("&")
.forEach((item) => {
if (item) {
const i = item.search("=");
const key = item.substr(0, i);
const val = item.substr(i + 1);
res[key] = res[key]
? Array.isArray(res[key])
? [...res[key], val]
: [res[key], val]
: val;
}
});
return res;
}
const context = {
_body: null,
req: null,
res: null,
get body() {
return this._body;
},
set body(val) {
if (typeof val !== "string") {
val = JSON.stringify(val);
}
this._body = val;
// this.res.end(this._body)
},
get method() {
return this.req.method;
},
set(filed, val) {
this.res.setHeader(filed, val);
},
throw(code, text) {
this.res.statusCode = code;
this.res.end(text || "Error");
},
redirect(url) {
this.res.statusCode = 302;
this.set("Location", url);
this.body = `Redirecting to ${url}.`;
},
get path() {
return new URL(this.req.url, "http://localhost").pathname;
},
get query() {
return parseQuery(new URL(this.req.url, "http://localhost").search);
},
};
function creatContext(req, res) {
const ctx = Object.create(context);
ctx.req = req;
ctx.res = res;
return ctx;
}
module.exports = creatContext;
compose
// compose.js
function compose(middlewares) {
console.log(middlewares);
return (context, next) => {
return dispatch(0);
function dispatch(i) {
let fn = middlewares[i];
if (i === middlewares.length) fn = next;
if (!fn) return Promise.resolve();
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
};
}
module.exports = compose;
模拟实现 koa 中间键的洋葱模型
在 koa 源码中,可以看到引入了 koa-compose
实现 koa 中间键的洋葱模型非常简单,实现思路就是每次执行中间键 await next() ,都要等到下次中间键执行完毕再往后执行
function middleware1() {
return async (ctx, next) => {
console.log("logger1", ctx.path);
// 等待下个中间键执行完毕
await next();
console.log("logger1", ctx.body);
};
}
实现代码
实现代码也非常简单
/**
* @param {Array} middleware
* @return {Function}
*/
function compose(middlewares) {
return (context, next) => {
return dispatch(0);
function dispatch(i) {
let fn = middlewares[i];
if (i === middlewares.length) fn = next;
if (!fn) return Promise.resolve();
// 返回一个Promise
// fn(context, dispatch.bind(null, i+1))
// context 上下文对象
// dispatch.bind(null, i+1) 就是next ,可以将next传递给中间键
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
};
}
下面是 koa-compose 的源码
"use strict";
/**
* Expose compositor.
*/
module.exports = compose;
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
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);
}
}
};
}
koa-router
使用 koa-router
var Koa = require('koa')
var Router = require('koa-router')
var app = new Koa()
var router = new Router()
router.get('/', (ctx, next) => {
// ctx.router available
})
router.get('/:category/:title', (ctx, next) => {
console.log(ctx.params)
// => { category: 'programming', title: 'how-to-node' }
})
app.use(router.routes()).use(router.allowedMethods())
实现
调用route.get('/xxx', middleware)、route.post('/xxx', middleware)等时,会将注册的方法和中间键都保存起来
实现代码也很简单
router.get('/', (ctx, next) => {
// ctx.router available
})
router.get('/aa', middleware)
app.use(router.routes())
调用app.use(router.routes())后,当有请求过来时,就会处理我们注册的路由,当访问路径和方法都匹配时,就执行该中间键
class Router {
routes() {
return async (ctx, next) => {
for (let i = 0, len = stack.length; i < len; i++) {
const routeItem = stack[i]
// 方法和路径都匹配
if (routeItem.path === ctx.path && routeItem.method === ctx.method) {
route = stack[i].middleware
route(ctx, next)
break
}
}
await next()
}
}
}
完整处理流程
class Layer {
constructor(path, methods, middleware, opts) {
this.path = path
this.methods = methods
this.middleware = middleware
}
}
class Router {
constructor() {
this.methods = ['HEAD', 'OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE']
this.stack = []
this.initMethods()
}
initMethods() {
this.methods.forEach((method) => {
// 当调用router.get...,会register注册路由,会将方法和中间键保存下来
this[method] = this[method.toLowerCase()] = (path, middleware) => {
this.register(path, [method], middleware)
}
})
}
// 路由注册,会将方法和中间键保存下来
register(path, methods, middleware, opts) {
let route = new Layer(path, methods, middleware, opts)
this.stack.push(route)
return this
}
routes() {
return async (ctx, next) => {
let stack = this.stack
let route
for (let i = 0, len = stack.length; i < len; i++) {
const routeItem = stack[i]
if (
routeItem.path === ctx.path &&
routeItem.methods.indexOf(ctx.method) > -1
) {
route = stack[i].middleware
break
}
}
if (typeof route === 'function') {
route(ctx, next)
}
await next()
}
}
}
下面实现动态路由
- 实现思路就是根据注册的路由生产动态正则
- /a/:id 生成正则
/^\/a\/([^/]+?)[/]?$/,并保存动态的键[id] - 发送请求时,根据正则去匹配,获取对应 id 的值,并将值添加到上下文对象上,如
{id: 1}
- /a/:id 生成正则
class Layer {
constructor(path, methods, middleware, opts) {
this.path = path
this.methods = methods
this.middleware = middleware
this.pathRegStrList = []
this.pathPramsKeyList = []
this.initPathToRegxExpConfig(path)
}
// 生产动态正则和参数
initPathToRegxExpConfig(path) {
const pathItemReg = /\/([^\/]{2,})/g
const paramsKeyReg = /\/\:([\w\_]+)/
// 所有地址
const pathItems = path.match(pathItemReg)
// 用来保存动态地址
const pathPramsKeyList = []
// 路径匹配正则数组
const pathRegList = []
if (Array.isArray(pathItems)) {
pathItems.forEach((path) => {
if (paramsKeyReg.test(path)) {
pathRegList.push(`/([^\/]+?)`)
pathPramsKeyList.push(path.replace(/\/\:/g, ''))
} else {
pathRegList.push(path)
}
})
}
this.pathPramsKeyList = pathPramsKeyList
this.pathReg = new RegExp(`^${pathRegList.join('')}[\/]?$`)
}
match(path, method) {
return this.methods.indexOf(method) > -1 && this.pathReg.test(path)
}
getParams(path) {
const execRes = this.pathReg.exec(path)
if (!execRes) {
return {}
}
const res = {}
this.pathPramsKeyList.forEach((item, index) => {
res[item] = execRes[index + 1]
})
return res
}
}