最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    正文概述 掘金(ELab)   2021-04-08   545

    1 原生实现

    1.1 启动一个服务

    node起一个服务有多简单,相信只要会上网,都能搜到类似下面的代码快速启动一个服务。

    const http = require('http')
    const handler = ((req, res) => {
      res.end('Hello World!')
    })
    http
      .createServer(handler)
      .listen(
        8888,
        () => {
          console.log('listening 127.0.0.1:8888')
        }
      )
    

    访问127.0.0.1:8888就可以看到页面上出现了'hello world!。随后就会发现修改路由还是请求方式,都只能拿到这样一个字符串。

    curl 127.0.0.1:8888
    curl curl -X POST http://127.0.0.1:8888
    curl 127.0.0.1:8888/about
    

    【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    这个时候肯定就会去找相关文档,然后发现刚刚回调函数的 req 居然内有乾坤。我们可以使用 method 属性和 url 属性针对不同的方法和路由返回不同的结果。于是很容易就想到类似下面的写法:

    const http = require('http')
    const handler = ((req, res) => {
      let resData = '404 NOT FOUND!'
      const { method, path } = req
      switch (path) {
        case '/':
          if (method === 'get') {
            resData = 'Hello World!'
          } else if (method === 'post') {
            resData = 'Post Method!'
          }
          break
        case '/about':
          resData = 'Hello About!'
      }
      res.end = resText
    })
    http
      .createServer(handler)
      .listen(
        8888,
        () => {
          console.log('listening 127.0.0.1:8888')
        }
      )
    

    但是一个服务不可能只有这么几个接口跟方法啊,总不能每加一个就增加一个分支吧,这样 handler 得变得多长多冗余,于是又很容易想到抽离 handler ,将 pathmethod 解耦。

    1.2 策略模式解耦

    如何解耦呢?从在新手村的代码中可以发现策略模式刚好可以拿来解决这个问题:

    const http = require('http')
    class Application {
      constructor () {
        // 收集route和method对应的回调函数
        this.$handlers = new Map()
      }
      // 注册handler
      register (method, path, handler) {
        let pathInfo = null
        if (this.$handlers.has(path)) {
          pathInfo = this.$handlers.get(path)
        } else {
          pathInfo = new Map()
          this.$handlers.set(path, pathInfo)
        }
        // 注册回调函数
        pathInfo.set(method, handler)
      }
      use () {
        return (request, response) => {
          const { url: path, method } = request
          this.$handlers.has(path) && this.$handlers.get(path).has(method)
            ? this.$handlers.get(path).get(method)(request, response)
            : response.end('404 NOT FOUND!')
        }
      }
    }
    const app = new Application()
    app.register('GET', '/', (req, res) => {
      res.end('Hello World!')
    })
    app.register('GET', '/about', (req, res) => {
      res.end('Hello About!')
    })
    app.register('POST', '/', (req, res) => {
      res.end('Post Method!')
    })
    http
      .createServer(app.use())
      .listen(
        8888,
        () => {
          console.log('listening 127.0.0.1:8888')
        }
      )
    

    【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    1.3 符合DRY原则

    但是这个时候就会发现:

    • 如果手抖把 method 方法写成了小写,因为 Http.Request.method 都是大写,无法匹配到正确的 handler ,于是返回 '404 NOT FOUND'
    • 如果我想在响应数据前增加一些操作,比如为每个请求增加一个时间戳,表示请求的时间,就必须修改每个 register 中的 handler 函数,不符合DRY原则

    此时再修改一下上面的代码,利用 Promise 实现按顺序执行 handler

    const http = require('http')
    class Application {
      constructor() {
        // 收集route和method对应的回调函数
        this.$handlers = new Map()
        // 暴露get和post方法
        this.get = this.register.bind(this, 'GET')
        this.post = this.register.bind(this, 'POST')
      }
      // 注册handler
      register(method, path, ...handlers) {
        let pathInfo = null
        if (this.$handlers.has(path)) {
          pathInfo = this.$handlers.get(path)
        } else {
          pathInfo = new Map()
          this.$handlers.set(path, pathInfo)
        }
        // 注册回调函数
        pathInfo.set(method, handlers)
      }
      use() {
        return (request, response) => {
          const { url: path, method } = request
          if (
            this.$handlers.has(path) &&
            this.$handlers.get(path).has(method)
          ) {
            const _handlers = this.$handlers.get(path).get(method)
            _handlers.reduce((pre, _handler) => {
              return pre.then(() => {
                return new Promise((resolve, reject) => {
                  _handler.call({}, request, response, () => {
                    resolve()
                  })
                })
              })
            }, Promise.resolve())
          } else {
            response.end('404 NOT FOUND!')
          }
        }
      }
    }
    const app = new Application()
    const addTimestamp = (req, res, next) => {
      setTimeout(() => {
        this.timestamp = Date.now()
        next()
      }, 3000)
    }
    app.get('/', addTimestamp, (req, res) => {
      res.end('Hello World!' + this.timestamp)
    })
    app.get('/about', addTimestamp, (req, res) => {
      res.end('Hello About!' + this.timestamp)
    })
    app.post('/', addTimestamp, (req, res) => {
      res.end('Post Method!' + this.timestamp)
    })
    http
      .createServer(app.use())
      .listen(
        8888,
        () => {
          console.log('listening 127.0.0.1:8888')
        }
      )
    

    【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    1.4 降低用户心智

    但是这样依旧有点小瑕疵,用户总是在重复创建 Promise,用户可能更希望无脑一点,那我们给用户暴露一个 next 方法,无论在哪里执行 next 就会进入下一个 handler,岂不美哉!!!

    class Application {
    // ...
      use() {
        return (request, response) => {
          const { url: path, method } = request
          if (
            this.$handlers.has(path) &&
            this.$handlers.get(path).has(method)
          ) {
            const _handlers = this.$handlers.get(path).get(method)
            _handlers.reduce((pre, _handler) => {
              return pre.then(() => {
                return new Promise(resolve => {
                 // 向外暴露next方法,由用户决定什么时候进入下一个handler
                  _handler.call({}, request, response, () => {
                    resolve()
                  })
                })
              })
            }, Promise.resolve())
          } else {
            response.end('404 NOT FOUND!')
          }
        }
      }
    }
    // ...
    const addTimestamp = (req, res, next) => {
      setTimeout(() => {
        this.timestamp = new Date()
        next()
      }, 3000)
    }
    

    2 Koa核心源码解析

    上面的代码一路下来,基本上已经实现了一个简单中间件框架,用户可以在自定义中间件,然后在业务逻辑中通过 next() 进入下一个 handler,使得整合业务流程更加清晰。但是它只能推进中间件的执行,没有办法跳出中间件优先执行其他中间件。比如在koa中,一个中间件是类似这样的:

    const Koa = require('koa');
    let app = new Koa();
    const middleware1 = async (ctx, next) => { 
      console.log(1); 
      await next();  
      console.log(2);   
    }
    const middleware2 = async (ctx, next) => { 
      console.log(3); 
      await next();  
      console.log(4);   
    }
    const middleware3 = async (ctx, next) => { 
      console.log(5); 
      await next();  
      console.log(6);   
    }
    app.use(middleware1);
    app.use(middleware2);
    app.use(middleware3);
    app.use(async(ctx, next) => {
      ctx.body = 'hello world'
    })
    app.listen(8888)
    

    可以看到控制台输出的顺序是1, 3, 5, 6, 4, 2,这就是koa经典的洋葱模型。

    【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    起源地下载网 » 【保姆级解析】我是如何从工作的视角看 Koa 源码的?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元