前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >koa 源码解析

koa 源码解析

原创
作者头像
测不准
发布2021-07-14 20:17:41
4590
发布2021-07-14 20:17:41
举报
文章被收录于专栏:与前端沾边与前端沾边

本次的文章是之前 koa 专题的延续,计划书写两篇文章,本篇从零实现一个简单版的 koa 框架(里面可能涉及一点 node 知识,不会太讲,大家如果遇到不了解的可以自行百度查看,也可以看官网文档了解使用)。包括上下文 ctx 组合 req, res 的实现,中间件机制的实现。第二篇写下 bodyparserrouter 中间件的简单实现,理解其原理。

koa 使用

test.js

代码语言:txt
复制
const Koa = require('koa')

const app = new Koa()
let port = 3000

app.use(async (ctx, next) => {
  ctx.body = '测不准1111'
  // 如果不调用 next, 页面会显示 测不准1111
  await next()
})

app.use(async (ctx, next) => {
  ctx.body = '测不准2222'
})
// 出现异常 端口号自动 +1
app.on('error', () => {
  app.listen(++port)
})

app.listen(port, () => {
  console.log(`正在监听 ${PORT} 端口`)
})

从上面的代码我们可以看出来:

  • Koa 是一个类,有 uselisten 方法(on 监听器明显是发布订阅)
  • 由于我们启动了一个服务,所以要用到 http 模块
  • 页面显示了‘测不准2222’,所以 ctx.body 的结果会赋值到 res.end()` 中
  • use 中的两个函数都是中间件,调用 next 方法执行下一个

初始化项目结构

  • package.json

主要配置下 main 字段,包的入口

代码语言:txt
复制
{
...
  "main": "./lib/application",
...
}
  • 目录结构保持一致

编写 application 文件

  • 初始化结构
代码语言:txt
复制
const EventEmitter = require('events')
const http = require('http')

// 继承 可以使用发布订阅
class Koa extends EventEmitter{
  constructor() {
    super()
  }
  use() {

  }
  listen() {

  }
}

module.exports = Koa
  • listen 中创建服务
代码语言:txt
复制
// es7 保证this。也可以使用bind
handleRequest = (req, res) => {

}
// 接收传的 端口号 和 回调
listen(...args) {
  const server = http.createServer(this.handleRequest)
  server.listen(...args)
}
  • 初始化 context request response 三个文件,application 中引入requestresponse 两个文件是对原生 reqres 的拓展
代码语言:txt
复制
const context = {

}
module.exports = context
------------------------------
const request = {

}
module.exports = request
---------------------------------
const response = {

}
module.exports = response
  • 创建新的上下文、请求、响应
代码语言:txt
复制
constructor() {
  super()
  /**
 * Koa 使用时可以创建多个 koa实例
 * let app1 = new Koa()
 * let app2 = new Koa()
 * 因为我们创建的上下文和请求响应是引用类型,如果改了一个,也会影响其他的实例。
 * 所以在创建时,就要新创建一个,互不干扰
 * 本质 this.context.__proto__ = context
 */
  this.context = Object.create(context)
  this.request = Object.create(request)
  this.response = Object.create(response)
}
use(middleware) {

}
// 
createContext(req, res) {

}

handleRequest = (req, res) => {
  // 创建服务的时候,创建上下文
  const ctx = this.createContext(req, res)
}

避免创建的上下文,请求响应冲突,也需要新创建对象,同时改写 ctx

代码语言:txt
复制
// 在创建上下文中 改写 ctx
createContext(req, res) {
  const ctx = Object.create(this.context)
  const request = Object.create(this.request)
  const response = Object.create(this.response)
  // res.xx req.xx 都是原生的
  ctx.request = request
  ctx.request.req = req = ctx.req = req

  ctx.response = response
  ctx.response.res = ctx.res = res
  return ctx
}
  • 中间件操作

我们知道中间件是通过 app.use 形式注入的,可以写多个,所以用数组形式存储,执行时按顺序执行。因为 koa 中间件是洋葱模型,所以我们只是返回第一个中间件的执行,其余的在第一个的执行过程中就被执行了,我们把所有中间件拼接成 promise 返回执行;同时我们还要注意使用中间件时一定要 async await,才能确保达到想要的结果

代码语言:txt
复制
constructor() {
  ...
  this.middlewares = []
}
// 中间件存储
use(middleware) {
  this.middlewares.push(middleware)
}
// 洋葱方式执行
compose() {
  let index = -1
  const dispatch = i => {
    // 传入的 i 值是不变的,如果在一个中间件中多次调用 await next(),那么内部中间件执行完执行当前中间件下一个 next 时,传入的i一定小于 index,大家可以自行打印
    if (i <= index) return Promise.reject('不要使用多个next')
    index = i
    // 执行到最后一项退出
    if (i == this.middlewares.length) return Promise.resolve()
    // 使用 Promise.resolve 包裹执行中间件
    let fn = this.middlewares[i]
    return Promise.resolve(fn(ctx, () => dispatch(i + 1)))
  }

  return dispatch(0)
}

处理完中间件(执行完毕)最后返回给页面 ctx.body 即可

代码语言:txt
复制
handleRequest = (req, res) => {
  const ctx = this.createContext(req, res)

  // 默认 404,当修改 ctx.body 时,改成 200
  // ctx.body 是在中间件中设置的,确保执行了
  ctx.status = 404
  this.compose(ctx).then(() => {
    // koa 可以直接读流
    if (ctx.body instanceof Stream) {
      ctx.body.pipe(res)
    } else if(typeof ctx.body == 'object' && ctx.body) {
      res.setHeader('Content-Type', 'application/json;charset=utf-8')
      res.end(JSON.parse(ctx.body))
    } else if (ctx.body) {
      res.setHeader('Content-Type', 'text/plain;charset=utf-8')
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }).catch(err => {
    this.emit('error', err)
  })
}

代理 ctx、request、response

添加一个测试中间件

代码语言:txt
复制
app.use(async (ctx, next) => {
  console.log(ctx.url)
  console.log(ctx.request.url)
  console.log(ctx.request.req.url) // 直接调用原生res
  await next()
})

打印结果

因为我们获取的数据都在原生的 reqres 中,我们想要获取 ctx.url,实际获取的是 req.rul ,这里我们就用到了类似 defineProperty 的代理,熟悉 vue 的小伙伴应该不陌生。

  • request.jsconst request = { get url() { // 谁调用的 this 就是谁 => ctx.request.req.url return this.req.url } } // 我们可以对每个属性进行 get 请求书写,但是比较麻烦 const url = require('url') const request = { get url() { return this.req.url }, get query() { return url.parse(this.url, true) }, get path() { return url.parse(this.url).pathname } }
  • response.js

我们把修改 body 的操作放在 response 文件中,因为 ctx.body 作为 res.end 的值得。默认的状态码是 404,在修改 body 的函数中设置状态码为 200

代码语言:txt
复制
const response = {
  // 我们可以设置多个 ctx.body = 'xxx',会以最后一个为准
  // 所以一定是操作某个值,中间件执行完成后才执行 res.end()
  _body: ?developer/article/1847440/undefined,
  get body() {
    return this._body
  },
  set body(val) {
    this.res.statusCode = 200
    this._body = val
  }
}

module.exports = response
  • context.js
代码语言:txt
复制
const context = {}
// 这里使用函数操作,这三种代理方式是一样的,但是 __defineGetter__ 
// 不推荐使用了,但是源码是这种方式
function defineGetter(target, key) {
  context.__defineGetter__(key, function() {
    return this[target][key]
  })
}

function defineSetter(target, key) {
  context.__defineSetter__(key, function(val) {
    this[target][key] = val
  })
}

defineGetter('request', 'url')
defineGetter('request', 'query')
defineGetter('request', 'path')
defineGetter('request', 'body')

defineSetter('response', 'body')

module.exports = context

这时我们再看执行的 test.js 打印就是正常的了。大家如果感兴趣可以自己发布到 npm 上,当做是一个学习分享了。

koa 的源码相对较少,比较简单;相比来说 express 的内容比较多,像路由这种都封装到内部了,而 koa 只是提供了个架子,辅助操作都是用中间件的形式。下一篇我们介绍下几个中间件的实现。

有疑问可以添加小编 wx: wajh123654789

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • koa 使用
    • 初始化项目结构
      • 编写 application 文件
        • 代理 ctx、request、response
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com