1. 如何理解Koa?

一个基于原始http服务和express的框架,它的实现原理很简单,因为把众多应用都外包出去作为中间件使用,所以我们只需要关注它是如何集成中间件的,还有上下文是如何联系的。

2. koa原理

至此,就需要掌握两点:

2.1 中间件机制

首先实现一个函数,内层函数的返回值作为外层函数的参数,类似于: fn1(fn2(fn3(...args))) ,由于请求从外向内,响应自内向外,我们把中间件机制可以用洋葱圈模型形象的表示
image.png
实现的方法也有多种,比如 reduce, 嵌套函数等,这里用一种比较简单的写法表示

  1. // 处理同步
  2. const compose = (...[first, ...other] => (...args) => {
  3. let ret = first(...args)
  4. other.forEach(fn=>{
  5. ret = fn(ret)
  6. })
  7. return ret
  8. })
  9. // 处理异步
  10. function compose(middlewares){
  11. return dispatch(0)
  12. function dispatch(i){
  13. const fn = middleware[i]
  14. if(!fn){
  15. return Promise.resolve()
  16. }
  17. return Promise.resolve(fn(()=>dispatch(i+1)))
  18. }
  19. }

2.2 context上下文

context 的作用是简化API,将第一次请求对象贯穿全文可以一直向下传递,同时封装了 req,res的 getter和setter方法, 使用如下:

  1. app.use(ctx=>{
  2. ctx.body = 'hello'
  3. })

核心代码如下:

  1. // 分别封装request,response,context
  2. // request.js
  3. module.exports = {
  4. get url(){return this.req.url},
  5. get method(){return this.req.method.toLowerCase()}
  6. }
  7. // response.js
  8. module.exports = {
  9. get body(){return this._body},
  10. set body(val){this._body = val}
  11. }
  12. // context.js
  13. module.exports = {
  14. get url(){return this.request.url},
  15. get body(){return this.response.body},
  16. set body(val){this.response.body = val},
  17. get method(){return this.request.method}
  18. }

声明koa类

  1. // 导入依赖
  2. const context = require('./context')
  3. const request = reuqire('./reuqest')
  4. ...
  5. class Koa {
  6. listen(...args){
  7. const server = http.createServer((req,res)=>{
  8. let ctx = this.createContext(req, res)
  9. this.callback(ctx)
  10. res.end(ctx.body)
  11. })
  12. // ...
  13. }
  14. // 1. 构建上下⽂, 把res和req都挂载到ctx之上,
  15. // 2. 在ctx.req和ctx.request.req同时保存
  16. createContext(req, res){
  17. const ctx = Object.create(context)
  18. ctx.request = Object.create(request)
  19. ctx.response = Object.create(response)
  20. ctx.req = ctx.request.req = req
  21. ctx.res = ctx.response.res = res
  22. return ctx;
  23. }}

2.3 汇总

分别处理完 context 和 compose的实现,实际应用中肯定不是各搞各的,我们把他们合起来应用

  • 首先在compose中传入上下文, 这样不管在middleware哪一层,都可以使用到context
  1. compose(middlewares){
  2. return function(ctx){ // 改动一: 最外层传参ctx
  3. return dispatch(0);
  4. function dispatch(i){
  5. // ...
  6. return Promise.resolve(
  7. fn(ctx, ()=> dispatch(i+1)) 改动二: 递归传参ctx
  8. )
  9. }
  10. }
  11. }
  • 其次在 koa.listen方法中加入中间件
  1. listen(...args){
  2. const server = http.createServer(async (req,res)=>{
  3. // step1: 创建上下文
  4. const ctx = this.createContext(req, res)
  5. // step2: 合成中间件
  6. const fn = this.compose(this.middlewares)
  7. // step3: 传入上下文并执行合成
  8. await fn(ctx);
  9. // step4: 返回响应
  10. res.end(ctx.body)
  11. })
  12. server.listen(...args)
  13. }

我们来看看完整的Koa类

  1. const http = require("http");
  2. const context = require("./context");
  3. const request = require("./request");
  4. const response = require("./response");
  5. class Koa {
  6. constructor() {
  7. this.middlewares = []; // 初始化中间件数组
  8. },
  9. use(middleware) {
  10. this.middlewares.push(middleware); // 将中间件加到数组⾥
  11. },
  12. listen(...args){
  13. const server = http.createServer(async (req,res)=>{
  14. const ctx = this.createContext(req, res)
  15. const fn = this.compose(this.middlewares)
  16. await fn(ctx);
  17. res.end(ctx.body)
  18. })
  19. server.listen(...args)
  20. },
  21. compose(middlewares){
  22. return function(ctx){
  23. return dispatch(0);
  24. function dispatch(i){
  25. const fn = middleware[i]
  26. if(!fn){
  27. return Promise.resolve()
  28. }
  29. return Promise.resolve(
  30. fn(ctx, ()=> dispatch(i+1))
  31. )
  32. }
  33. }
  34. },
  35. createContext(req, res){
  36. const ctx = Object.create(context)
  37. ctx.request = Object.create(request)
  38. ctx.response = Object.create(response)
  39. ctx.req = ctx.request.req = req
  40. ctx.res = ctx.response.res = res
  41. return ctx;
  42. }
  43. }
  44. module.exports = Koa

2.4 题外话:middleware的实现

从compose方法我们可以看出, middleware是一层套一层,所以每个middleware必然有承上启下的作用,即接收上一层ctx, 执行本次任务,next进入下一层,就像下面这样

  1. const mid = async (ctx, next) => {
  2. // 来到中间件,洋葱圈左边
  3. next() // 进⼊其他中间件
  4. // 再次来到中间件,洋葱圈右边
  5. };

举个请求拦截的示例:

  1. module.exports = async (ctx, next) => {
  2. const {res, req} = ctx;
  3. const blackList = ['127.0.0.1'];
  4. const ip = getClientIP(req);
  5. // 拒绝黑名单中的ip访问
  6. if (blackList.includes(ip)){
  7. ctx.body = 'not allowed';
  8. }else{
  9. await next
  10. }
  11. }
  12. function getClientIP(req){
  13. return (
  14. req.headers['x-forwarded-for'] || // 判断有无反向代理IP
  15. req.connection.remoteAddress || // 判断connection远程IP
  16. req.socket.remoteAddress || // 判断后端socket IP
  17. req.connection.socket.remoteAddress
  18. )
  19. }