koa底层实现


1.Koa

  • Koa是一个由 Express 原班人马打造的,尝试在 Web 应用开发中摆脱回调地狱,并增强错误处理的轻量化的 Node.js 框架。
  • Koa的目标是成为一个更小、更富表现力、更健壮的 Web 框架。

2.安装

Koa 需要 Node v12 或更高版本以支持 ES2015 和异步函数。 您可以使用您喜欢的版本管理器快速安装一个支持的 Node 版本:

nvm install 12
npm install koa
node my-koa-app.js

3.基本使用

Koa 应用是一个包含中间件函数数组的对象,这些函数在请求时以类似堆栈的方式组合并执行

在 Koa 框架中,ctx 是上下文对象,它封装了原生的 Node.js 请求和响应对象,并且提供了许多方便的方法和属性来处理 HTTP 请求和响应

ctx.reqctx.res 是 Node.js 的原生请求和响应对象。这些对象提供了低级别的请求和响应处理方法

ctx.res.end('ok') 是直接调用 Node.js 原生响应对象的 end 方法来结束响应并发送字符串 ok

import Koa from "koa"
// 创建一个Koa应用实例
const app = new Koa()
app.use(function (ctx) {
  // 处理请求,返回'hello'
  ctx.res.end("hello")
})
// 监听3000端口,并在控制台打印服务器运行信息
app.listen(3000, () => console.log("server is running at http://localhost:3000"))

4.request&response

在 Koa 中,ctx.requestctx.response 是封装了 Node.js 原生请求和响应对象(ctx.reqctx.res)的对象。相比于 Node.js 的原生请求和响应对象,ctx.requestctx.response 提供了更多的方法和属性,使得处理 HTTP 请求和响应更加方便和简单。

  • ctx.request:

    • ctx.request.query: 一个包含解析过的查询字符串的对象
    • ctx.request.method: 请求方法,例如 ‘GET’, ‘POST’ 等
    • ctx.request.url: 请求的 URL
    • ctx.request.header: 请求头对象
    • ctx.request.body: 请求体(需要额外的中间件如 koa-bodyparser 来解析请求体)
  • ctx.response :

    • ctx.response.body: 可以设置响应体的内容,它可以是一个字符串、对象或者流
    • ctx.response.status: 可以设置响应的 HTTP 状态码
    • ctx.response.message: HTTP 状态消息
    • ctx.response.header: 响应头对象

1.server

import Koa from "koa"
const app = new Koa()
app.use((ctx) => {
  // 打印请求方法
  console.log(ctx.request.method)
  // 打印请求URL
  console.log(ctx.request.url)
  // 打印请求路径
  console.log(ctx.request.path)
  // 打印查询字符串参数
  console.log(ctx.request.query)
  // 打印请求头
  console.log(ctx.request.header)
  // 设置响应状态码
  ctx.response.status = 200
  // 设置响应消息
  ctx.response.message = "OK"
  ctx.response.set("Content-Type", "text/html;charset=utf-8")
  // 设置响应体
  ctx.response.body = "hello"
  ctx.response.body = "world"
  ctx.response.body = "third"
})

app.listen(3000, () => {
  console.log("app runing http://localhost:3000")
})

2.server.js

const Koa = require("koa")
const app = new Koa()
app.use((ctx) => {
  // 打印请求方法
  console.log(ctx.method)
  // 打印请求URL
  console.log(ctx.url)
  // 打印请求路径
  console.log(ctx.path)
  // 打印查询字符串参数
  console.log(ctx.query)
  // 打印请求头
  console.log(ctx.header)
  // 设置响应状态码
  ctx.status = 200
  // 设置响应消息
  ctx.message = "OK"
  ctx.set("Content-Type", "text/html;charset=utf-8")
  // 设置响应体
  ctx.body = "hello"
  ctx.body = "world"
  ctx.body = "third"
})

app.listen(3001, () => {
  console.log("app runing http://localhost:3001")
})

5.中间件级联

5.1 同步中间件

const Koa = require("koa")
const app = new Koa()
const middleware1 = (ctx, next) => {
  console.log(1)
  next()
  console.log(2)
}
const middleware2 = (ctx, next) => {
  console.log(3)
  next()
  console.log(4)
}
const middleware3 = (ctx) => {
  console.log(5)
}
app.use(middleware1)
app.use(middleware2)
app.use(middleware3)
app.listen(3000, () => console.log("server is running at http://localhost:3000"))

5.2 异步中间件

const Koa = require("koa")
const app = new Koa()
const middleware1 = async (ctx, next) => {
  console.time("cost")
  console.log(1)
  await next()
  console.log(2)
  console.timeEnd("cost")
}
const middleware2 = async (ctx, next) => {
  console.log(3)
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log("middleware2 sleep 1s")
      resolve()
    }, 1000)
  })
  await next()
  console.log(4)
}
const middleware3 = async (ctx) => {
  console.log(5)
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log("middleware3 sleep 1s")
      resolve()
    }, 1000)
  })
  ctx.body = "hello"
  console.log(6)
}
app.use(middleware1)
app.use(middleware2)
app.use(middleware3)
app.listen(3000, () => console.log("server is running at http://localhost:3000"))

6. 核心实现

6.1application

import EventEmitter from "events"
import http from "http"
import context from "./context.js"
// 导入自定义的request模块
import request from "./request.js"
// 导入自定义的response模块
import response from "./response.js"
import compose from "./koa-compose.js"
export default class Application extends EventEmitter {
  constructor() {
    super()
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
    this.middleware = []
  }
  //存储中间件
  use(fn) {
    this.middleware.push(fn)
    return this
  }
  // 创建服务
  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
  //请求到来时候的回调函数
  callback() {
    //组合中间件
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      //创建上下文对象
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }
    return handleRequest
  }
  // 请求回调
  handleRequest(ctx, fnMiddleware) {
    const handleResponse = () => respond(ctx)
    const onerror = (err) => ctx.onerror(err)
    // 执行中间件 包装为 Promise 执行完成后响应 body
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }
  createContext(req, res) {
    //创建上下文对象
    const context = Object.create(this.context)
    // 创建一个新的request对象,这个对象继承自this.request,并将这个对象赋值给context.request
    const request = (context.request = Object.create(this.request))
    // 创建一个新的response对象,这个对象继承自this.response,并将这个对象赋值给context.response
    const response = (context.response = Object.create(this.response))

    context.req = request.req = req
    context.res = response.res = res
    //返回上下文对象
    return context
  }
}

// 响应 body
function respond(ctx) {
  let { res, body } = ctx
  if (Buffer.isBuffer(body)) return res.end(body)
  if (typeof body === "string") return res.end(body)
  if (body instanceof Stream) return body.pipe(res)
  res.end(JSON.stringify(body))
}
export { EventEmitter }

6.2 request

const parse = require("parseurl")
const qs = require("querystring")
module.exports = {
  get url() {
    return this.req.url
  },
  get path() {
    //把url路径转成对象,pathname是它的路径名 /a/b 参数req: IncomingMessage
    return parse(this.req).pathname
  },
  get method() {
    return this.req.method
  },
  //查询字符串,它的格式是一个字符串 ?a=1&b=2
  get querystring() {
    return parse(this.req).query
  },
  //它会调用qs.parse方法把查询字符串从字符串转成对象
  get query() {
    return qs.parse(this.querystring)
  },
  get header() {
    return this.req.headers
  },
  get headers() {
    return this.header
  },
}

6.3response

module.exports = {
  //设置状态码   response.status = 200;
  set status(code) {
    //把状态码code透传给原生的res响应对象
    this.res.statusCode = code
  },
  set message(msg) {
    //给响应状态码的原因短语赋值
    this.res.statusMessage = msg
  },
  set body(value) {
    //当调用response.body = xxx的时候,会把xxx暂存到response._body上
    this._body = value
    //一旦调用了res.end方法,则不能再次写入响应了
    //this.res.end(value);
  },
  get body() {
    return this._body
  },
  //set用来设置响应头
  set(field, value) {
    //调用原生的响应对象的setHeader方法,设置字符和值
    this.res.setHeader(field, value)
  },
}

6.4 context

const delegate = require("./delegates")
//创建一个空对象并将其导出
const proto = (module.exports = {
  onerror(err) {
    const { res } = this
    this.status = 500
    res.end(err.message)
  },
})

//使用代理模块将proto对象的一些属性代理到request对象上  proto.url=>proto.request.url
delegate(proto, "request")
  .access("method") //将request对象上method属性代理到proto对象上 access能读又能写
  .access("query")
  .access("url")
  .access("path")
  .getter("header") //只能读不能写

//使用代理模块将proto对象的一些属性代理到response对象上  proto.body=>proto.response.body
delegate(proto, "response").access("status").access("message").access("body").method("set") //将response对象上的set方法代理到proto对象上

6.5delegates

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) {
    return new Delegator(proto, target)
  }
  this.proto = proto
  this.target = target
}
Delegator.prototype.getter = function (name) {
  const { proto, target } = this
  Object.defineProperty(proto, name, {
    get() {
      return this[target][name]
    },
    configurable: true,
  })
  return this
}
Delegator.prototype.setter = function (name) {
  const { proto, target } = this
  Object.defineProperty(proto, name, {
    set(val) {
      this[target][name] = val
    },
    configurable: true,
  })
  return this
}
Delegator.prototype.access = function (name) {
  return this.getter(name).setter(name)
}
Delegator.prototype.method = function (name) {
  const { proto, target } = this
  proto[name] = function () {
    return this[target][name].apply(this[target], arguments)
  }
  return this
}
module.exports = Delegator

6.6 Koa-compose

/**
 * 组合中间件函数,返回一个可以处理上下文的函数
 *
 * @param {Array} middleware - 中间件函数数组
 * @returns {Function} 处理上下文的函数
 */
function compose(middleware) {
  // 返回一个函数,该函数接收一个context参数
  return function (context) {
    let index = -1
    // 定义一个dispatch函数,用于控制中间件的执行顺序
    function dispatch(i) {
      // 如果已经执行过dispatch(i),则返回一个错误
      if (i <= index) return Promise.reject(new Error("next() called multiple times"))
      // 更新index的值
      index = i
      // 获取当前中间件函数
      let fn = middleware[i]
      // 如果中间件函数不存在,则返回一个已解决的Promise
      if (!fn) return Promise.resolve()
      try {
        // 执行中间件函数,并传入context和下一个中间件函数
        return Promise.resolve(fn(context, () => dispatch(i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
    // 开始执行中间件函数
    return dispatch(0)
  }
}
module.exports = compose

context.js

const Cookies = require("cookies")

const COOKIES = Symbol("context#cookies")

const proto = (module.exports = {
  // ........
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure,
      })
    }
    return this[COOKIES]
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies
  },
  //........
})

cookies.js

function Cookies(req, res) {
  this.req = req
  this.res = res
}
//用来写cookie,通过响应头的Set-Cookie
//Set-Cookie:name=name_value; path=/; httponly 每个set-cookie只能写入一个cookie

Cookies.prototype.set = function (name, value, attrs) {
  //获取响应头中的Set-Cookie的值
  const headers = this.res.getHeader("Set-Cookie") || []
  //根据name和value创建一个新的cookie
  let cookie = new Cookie(name, value, attrs)
  //把新的cookie加入到数组中
  headers.push(cookie.toHeader())
  //写回响应头
  this.res.setHeader("Set-Cookie", headers)
}
//用来读cookie,通过请求头中cookie
//请求头 Cookie:name=zhufeng; age=18
Cookies.prototype.get = function (name) {
  //name=name_domain
  //获取客户端发送过来的cookie

  let cookie = this.req.headers.cookie || ""
  return getValueFromHeader(name, cookie)
}
function getValueFromHeader(name, cookie) {
  //如果客户端根本没有传cookie过来,不用找了,直接返回
  if (!cookie) return
  //name=name_value; name_domain=; name_path=name_path_value

  let regexp = new RegExp("(?:^|;) *" + name + "=([^;]*)") //name_domain=([^;]*)
  let match = cookie.match(regexp)
  if (match) {
    return match[1]
  }
}
function Cookie(name, value, attrs) {
  this.name = name
  this.value = value
  //把用户传过来的cookie选项存放到cookie类的实例上等待被使用 path httpOnly
  for (let name in attrs) {
    this[name] = attrs[name]
  }
}
Cookie.prototype.toString = function () {
  return this.name + "=" + this.value
}
Cookie.prototype.toHeader = function () {
  let header = this.toString()
  if (this.path) header += `; path=` + this.path
  if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge)
  if (this.expires) header += `; expires=` + this.expires.toUTCString()
  if (this.domain) header += `; domain=` + this.domain
  if (this.httpOnly) header += `; httpOnly`
  console.log(header)
  return header
}
module.exports = Cookies

注意 : 可以对 cookie 进行签名

源码请看:https://www.npmjs.com/package/cookies?activeTab=code

7. 常见中间件的使用

import { fileURLToPath } from "url"
import path from "path"
import serve from "koa-static"
import Router from "koa-router"
import bodyParser from "koa-bodyparser"
import views from "koa-views"
import Koa from "koa"
import multer from "@koa/multer"

const app = new Koa()
const router = new Router()

// 模拟 __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// 配置 multer,用于处理 multipart/form-data(文件上传)
const upload = multer({ dest: "uploads/" })

// 1. 提供静态资源服务
app.use(serve(path.resolve(__dirname, "public")))
app.use(serve(path.resolve(__dirname)))

// 2. 使用 bodyparser 解析请求体
app.use(bodyParser()) // ctx.request.body = 请求体

// 3. 记录访问的时间
app.use(async function (ctx, next) {
  console.time("ok")
  await next()
  console.timeEnd("ok")
})

// 4. 错误捕获
app.use(async function (ctx, next) {
  try {
    await next()
  } catch (err) {
    console.error(err)
    ctx.status = err.status || 500
    ctx.body = { message: "Internal Server Error" }
  }
})

// 5. 设置模板引擎
app.use(
  views(path.resolve(__dirname, "views"), {
    extension: "ejs", // 确保渲染引擎为 ejs
  })
)

// 6. 使用路由系统
app.use(router.routes())
app.use(router.allowedMethods()) // 推荐:允许 HTTP 方法控制

// 上传文件的路由,同时传递参数
router.post("/login", upload.array("file", 2), async (ctx) => {
  const files = ctx.files // 获取上传的文件
  const params = ctx.request.body // 获取表单参数
  // console.dir("file=>", file.filename)
  ctx.body = {
    message: "File uploaded successfully",
    fileDetails: files,
    formData: params,
  }
})

router.get("/user", async (ctx) => {
  await ctx.render("user", { name: "zs" })
})

// 启动服务器
app.listen(3000, () => {
  console.log("Server started on http://localhost:3000")
})

1. koa-router

  • 功能:用于处理路由。允许定义路径和处理请求的方法,如 GET、POST 等。
  • 使用场景:创建 API 时进行路由控制。

2. koa-bodyparser

  • 功能:解析请求体,用于处理 POST 请求中的 JSON、表单等数据。
  • 使用场景:解析POST请求的请求体内容,尤其是 JSON 格式的数据。

3. koa-static

  • 功能:提供静态文件服务,用于提供诸如 HTML、CSS、JS、图片等文件。
  • 使用场景:当你需要为前端应用提供静态资源时,如图片或静态网页。

4. koa-multer

  • 功能:用于处理文件上传。
  • 使用场景:当应用需要处理用户上传的文件时。

5. koa-session

  • 功能:用于会话管理,存储和管理用户会话信息。
  • 使用场景:需要对用户登录状态进行管理。

6. koa-jwt

  • 功能:用于基于 JWT 的身份验证,解析请求中的 JWT token。
  • 使用场景:用于保护路由,需要对用户身份进行认证。

7 koa-view

  • 功能:用于在服务器端渲染 HTML 模板,可以动态生成页面
  • 使用场景:需要服务端渲染动态内容的场景,比如渲染用户数据、生成 HTML 电子邮件等
    • EJS:简单、易用,适合小项目。
    • Pug:简洁的缩进式语法,适合中大型项目。
    • Nunjucks:功能强大,支持高级特性,适合复杂应用。
    • Handlebars:轻量化,适合生成静态页面。

8. 中间件实现

1.koa-router

const methods = ["get", "head", "options", "put", "patch", "post", "delete"]
function Router() {
  this.stack = [] //stack里存放路由规则
}
for (const method of methods) {
  Router.prototype[method] = function (path, middleware) {
    //把请求的方法,请求的路径以及对应的请求处理中间件函数包装成一个对象并放入stack数组
    this.stack.push({ path, method, middleware })
  }
}
Router.prototype.routes = function () {
  //这个才是真正用来处理请求的中间件函数
  return async (ctx, next) => {
    //在stack数组中找到一个元素,那个元素path和当前的请求的路径相同,它的method和请求的方法名相同
    const matchedLayer = this.stack.find((layer) => {
      return layer.path === ctx.path && layer.method === ctx.method.toLowerCase()
    })
    if (!matchedLayer) {
      return await next()
    }
    await matchedLayer.middleware(ctx, next)
  }
}

module.exports = Router

2.koa-bodyparser

  • 包含解析application/x-www-form-urlencodedapplication/json和文件上传multipart/form-data原理
import querystring from "querystring"
import fs from "fs/promises"
import { v4 } from "uuid"
import path from "path"
const starts = {
  "application/x-www-form-urlencoded"(content) {
    return querystring.parse(content.toString())
  },
  "application/json"(content) {
    return JSON.parse(content.toString())
  },
}

Buffer.prototype.split = function (sep) {
  let headerLines = []
  sep = Buffer.isBuffer(sep) ? sep : Buffer.from(sep)
  let offset = 0
  let index = 0
  while (-1 !== (index = this.indexOf(sep, offset))) {
    headerLines.push(this.slice(offset, index))
    offset = index + sep.length
  } // str.indexOf(分隔符,查找的位置)
  headerLines.push(this.slice(offset)) // 分割后的最后一部分也放到数组里
  return headerLines
}
async function formData(content, boundary, uploadDir) {
  let headerLines = content.split("--" + boundary)
  headerLines = headerLines.slice(1, -1)

  const result = {}
  await Promise.all(
    headerLines.map(async (line) => {
      let [head, ...content] = line.split("\r\n\r\n") // type 和 内容之间是两个换行
      let contentType = head.toString()
      let name = contentType.match(/name="(.+?)"/)[1]
      if (contentType.includes("Content-Type")) {
        content = Buffer.concat(content).slice(0, -2)
        const filename = v4()
        await fs.writeFile(path.join(uploadDir, filename), content)
        let file = {
          originalFilename: contentType.match(/filename="(.+?)"/)[1],
          type: contentType.match(/Content-Type: ([^\r\n]+)/)[1],
          size: content.length,
          filename,
        }
        result[name] = result[name] || []
        result[name].push(file)
      } else {
        result[name] = Buffer.concat(content).toString().slice(0, -2)
      }
    })
  )
  return result
}
function bodyParser({ uploadDir }) {
  return async (ctx, next) => {
    // 解析请求体,将结果 给 赋值到ctx.request.body
    await new Promise((resolve, reject) => {
      const arr = []
      ctx.req.on("data", function (chunk) {
        arr.push(chunk)
      })
      ctx.req.on("end", function (chunk) {
        let type = ctx.get("Content-Type")
        // 给上下文中的request自定一个body属性

        if (type) {
          const content = Buffer.concat(arr)
          if (type.includes("multipart/form-data")) {
            // 表单内容, 表单的分割符号
            formData(content, type.split("=")[1], uploadDir) // 将文件上传到某个目录中
            // 信息保存到 ctx.request.body
            ctx.request.body = {}
          } else {
            ctx.request.body = starts[type](content)
          }
        }

        resolve()
      })
    })
    return next()
  }
}

export default bodyParser

3.koa-static

import path from "path"
import { stat } from "fs/promises"
import { createReadStream } from "fs"
import mime from "mime"

export default function serve(dirname) {
  return async (ctx, next) => {
    //先向后执行
    await next()
    let filePath = path.join(dirname, ctx.path)

    try {
      const statObj = await stat(filePath)
      if (statObj.isFile()) {
        ctx.set("Content-Type", mime.getType(filePath) || "text/plain" + ";charset=utf-8")
        ctx.body = createReadStream(filePath)
      } else {
        return next()
      }
    } catch (e) {
      return next()
    }
  }
}

4. koa-session

function generateKoaSession() {
  return Math.random() + "" + Math.random() + "" + Date.now() + "" + Math.random()
}
function session() {
  //服务端会有一段内存用来保存每个用户的数据
  //用户对就在的会话数据就放在这个对象里
  const sessionStorage = {}
  return async function (ctx, next) {
    //获取用户传递过来的koa.sess
    let koasess = ctx.cookies.get("koa.sess")
    //如果没有传递,说明一个新的
    if (!koasess) {
      //生成一个新的koasess
      koasess = generateKoaSession()

      sessionStorage[koasess] = {}
      //通过cookie把这个新的卡号发给或者说种植到客户端
      ctx.cookies.set("koa.sess", koasess, { httpOnly: true })
    }

    ctx.session = sessionStorage[koasess]
    await next()
  }
}
module.exports = session

文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录