原创

koa 源码解析

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

koa 使用

test.js

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 字段,包的入口

{
...
  "main": "./lib/application",
...
}
  • 目录结构保持一致

编写 application 文件

  • 初始化结构
const EventEmitter = require('events')
const http = require('http')

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

  }
  listen() {

  }
}

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

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

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

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

}
module.exports = response
  • 创建新的上下文、请求、响应
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

// 在创建上下文中 改写 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,才能确保达到想要的结果

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 即可

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

添加一个测试中间件

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

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

module.exports = response
  • context.js
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

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • koa源码解析,理解洋葱模型

    之前,我一直在使用express做简单的后台server,写一些api,给自己做的前端来提供服务,觉着吧挺好用的,虽然koa也出来挺久的,但是我一直没有更换过,...

    brzhang
  • Koa源码解析,带你实现一个迷你版的Koa

    本文是我在阅读 Koa 源码后,并实现迷你版 Koa 的过程。如果你使用过 Koa 但不知道内部的原理,我想这篇文章应该能够帮助到你,实现一个迷你版的 Koa ...

    WahFung
  • Koa源码分析

    Koa 是一个类似于 Express 的Web开发框架,创始人也都是TJ。Koa 的主要特点是,使用了 ES6 的 Generator 函数,进行了架构的重新设...

    xiangzhihong
  • Koa-router源码解读

    链式调用 在 koa 中,对中间件的使用是支持链接调用的。同样, 对于多个路径的请求,koa-router 也支持链式调用: router .get(‘/‘...

    xiangzhihong
  • koa源码阅读[1]-koa与koa-compose

    接上次挖的坑,对koa2.x相关的源码进行分析 第一篇。 不得不说,koa是一个很轻量、很优雅的http框架,尤其是在2.x以后移除了co的引入,使其代码变得更...

    贾顺名
  • koa框架源码解读

    jeremyxu
  • koa源码阅读[2]-koa-router

    首先,因为koa是一个管理中间件的平台,而注册一个中间件使用use来执行。 无论是什么请求,都会将所有的中间件执行一遍(如果没有中途结束的话) 所以,这就会让开...

    贾顺名
  • koa源码阅读[0]

    Node.js也是写了两三年的时间了,刚开始学习Node的时候,hello world就是创建一个HttpServer,后来在工作中也是经历过Express、K...

    贾顺名
  • Koa 源码研读

    Koa 是一个非常轻量的 web 开发框架,由 Express 团队打造。相较于 Express,Koa 使用 async 函数解决异步的问题,并且完全脱离中间...

    李振
  • koa-route 源码阅读

    周末阅读完了 koa 的源码,其中的关键在于 koa-compose 对中间件的处理,核心代码只有二十多行,但实现了如下的洋葱模型,赋予了中间件强大的能力,网上...

    IMWeb前端团队
  • 关于koa2,你不知道的事

    koa 是一个基于 node 实现的一个新的 web 框架,它是由 express 框架的原班人马打造。特点是优雅、简洁、表达力强、自由度高。和 express...

    lucifer210
  • 手写koa-static源码,深入理解静态服务器原理

    本文会接着讲一个常用的中间件----koa-static,这个中间件是用来搭建静态服务器的。

    蒋鹏飞
  • 以小白的角度解读Koa源码

    使用Koa已有一段时间,为什么会从Express转向Koa呢,那还是得从Express上说起。对于服务端的Web框架来说,Express更为贴近「Web Fra...

    JowayYoung
  • 从 koa-body 入手分析,搞懂 Node.js 文件上传流程

    作者:陈关羽 (作者投稿) 原文地址:https://juejin.cn/post/6997060777462988837

    coder_koala
  • 从一个优秀开源项目来谈前端架构

    Peter谭金杰
  • 【一题】通过手写 koa 源码更加深入洋葱模型

    当我们在深入学习一个框架或者库时,为了了解它的思想及设计思路,也为了更好地使用和避免无意的 Bug,有时很有必要研究源码。对于 koa 这种极为简单,而应用却很...

    山月
  • node.js之koa2知识点总结

    YungFan
  • express, koa, redux三者中间件对比

    Link: http://yoursite.com/2018/09/14/express-koa-redux三者中间件对比/

    落落落洛克
  • express, koa, redux三者中间件简单对比分析

    Link: http://yoursite.com/2018/09/14/express-koa-redux三者中间件对比/

    coder_koala

扫码关注云+社区

领取腾讯云代金券