1. 如何理解Koa?
一个基于原始http服务和express的框架,它的实现原理很简单,因为把众多应用都外包出去作为中间件使用,所以我们只需要关注它是如何集成中间件的,还有上下文是如何联系的。
2. koa原理
至此,就需要掌握两点:
2.1 中间件机制
首先实现一个函数,内层函数的返回值作为外层函数的参数,类似于: fn1(fn2(fn3(...args)))
,由于请求从外向内,响应自内向外,我们把中间件机制可以用洋葱圈模型形象的表示
实现的方法也有多种,比如 reduce, 嵌套函数等,这里用一种比较简单的写法表示
// 处理同步
const compose = (...[first, ...other] => (...args) => {
let ret = first(...args)
other.forEach(fn=>{
ret = fn(ret)
})
return ret
})
// 处理异步
function compose(middlewares){
return dispatch(0)
function dispatch(i){
const fn = middleware[i]
if(!fn){
return Promise.resolve()
}
return Promise.resolve(fn(()=>dispatch(i+1)))
}
}
2.2 context上下文
context 的作用是简化API,将第一次请求对象贯穿全文可以一直向下传递,同时封装了 req,res的 getter和setter方法, 使用如下:
app.use(ctx=>{
ctx.body = 'hello'
})
核心代码如下:
// 分别封装request,response,context
// request.js
module.exports = {
get url(){return this.req.url},
get method(){return this.req.method.toLowerCase()}
}
// response.js
module.exports = {
get body(){return this._body},
set body(val){this._body = val}
}
// context.js
module.exports = {
get url(){return this.request.url},
get body(){return this.response.body},
set body(val){this.response.body = val},
get method(){return this.request.method}
}
声明koa类
// 导入依赖
const context = require('./context')
const request = reuqire('./reuqest')
...
class Koa {
listen(...args){
const server = http.createServer((req,res)=>{
let ctx = this.createContext(req, res)
this.callback(ctx)
res.end(ctx.body)
})
// ...
}
// 1. 构建上下⽂, 把res和req都挂载到ctx之上,
// 2. 在ctx.req和ctx.request.req同时保存
createContext(req, res){
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx;
}}
2.3 汇总
分别处理完 context 和 compose的实现,实际应用中肯定不是各搞各的,我们把他们合起来应用
- 首先在compose中传入上下文, 这样不管在middleware哪一层,都可以使用到context
compose(middlewares){
return function(ctx){ // 改动一: 最外层传参ctx
return dispatch(0);
function dispatch(i){
// ...
return Promise.resolve(
fn(ctx, ()=> dispatch(i+1)) 改动二: 递归传参ctx
)
}
}
}
- 其次在 koa.listen方法中加入中间件
listen(...args){
const server = http.createServer(async (req,res)=>{
// step1: 创建上下文
const ctx = this.createContext(req, res)
// step2: 合成中间件
const fn = this.compose(this.middlewares)
// step3: 传入上下文并执行合成
await fn(ctx);
// step4: 返回响应
res.end(ctx.body)
})
server.listen(...args)
}
我们来看看完整的Koa类
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Koa {
constructor() {
this.middlewares = []; // 初始化中间件数组
},
use(middleware) {
this.middlewares.push(middleware); // 将中间件加到数组⾥
},
listen(...args){
const server = http.createServer(async (req,res)=>{
const ctx = this.createContext(req, res)
const fn = this.compose(this.middlewares)
await fn(ctx);
res.end(ctx.body)
})
server.listen(...args)
},
compose(middlewares){
return function(ctx){
return dispatch(0);
function dispatch(i){
const fn = middleware[i]
if(!fn){
return Promise.resolve()
}
return Promise.resolve(
fn(ctx, ()=> dispatch(i+1))
)
}
}
},
createContext(req, res){
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx;
}
}
module.exports = Koa
2.4 题外话:middleware的实现
从compose方法我们可以看出, middleware是一层套一层,所以每个middleware必然有承上启下的作用,即接收上一层ctx, 执行本次任务,next进入下一层,就像下面这样
const mid = async (ctx, next) => {
// 来到中间件,洋葱圈左边
next() // 进⼊其他中间件
// 再次来到中间件,洋葱圈右边
};
举个请求拦截的示例:
module.exports = async (ctx, next) => {
const {res, req} = ctx;
const blackList = ['127.0.0.1'];
const ip = getClientIP(req);
// 拒绝黑名单中的ip访问
if (blackList.includes(ip)){
ctx.body = 'not allowed';
}else{
await next
}
}
function getClientIP(req){
return (
req.headers['x-forwarded-for'] || // 判断有无反向代理IP
req.connection.remoteAddress || // 判断connection远程IP
req.socket.remoteAddress || // 判断后端socket IP
req.connection.socket.remoteAddress
)
}