热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【从零到壹】Koa从理解到实现

【从零到壹】Koa从理解到实现-【点击查看文中的相关源码】根据官网的介绍,Koa是一个新的Web框架,致力于成为Web应用和API开发领域中的一个更小、更富有表现力和更健壮的基石。

【点击查看文中的相关源码】

根据官网的介绍,Koa 是一个新的 Web 框架,致力于成为 Web 应用和 API 开发领域中的一个更小、更富有表现力和更健壮的基石。

通过 async 函数,Koa 不仅远离回调地狱,同时还有力地增强了错误处理。而且,一个关键的设计点是在其低级中间件层中提供了高级“语法糖”,这包括诸如内容协商,缓存清理,代理支持和重定向等常见任务的方法。

基础

实际上,我们常见的一些 Web 框架都是通过使用 Http 模块来创建了一个服务,在请求到来时通过一系列的处理后把结果返回给前台,事实上 Koa 内部大致也是如此。

通过查看源码不难发现 Koa 主要分为四个部分:应用程序、上下文、请求对象和响应对象,当我们引入 Koa 时实际上就是拿到了负责创建应用程序的这个类。

我们先来看一下一个简单的 Hello World 应用:

const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
  ctx.body = 'Hello World'
})

app.listen(3000, () => console.log('The app is running on localhost:3000'))

运行上面的代码并访问 http://localhost:3000/,一个简单的应用就这样创建好了。

实现

根据上面的使用方式我们可以很容易的想到下面的实现:

const http = require('http')

module.exports = class Application {
  use(fn) {
    this.middleware = fn
  }

  callback() {
    const handleRequest = (req, res) => {
      this.middleware(req, res)
    }

    return handleRequest
  }

  listen(...args) {
    const server = http.createServer(this.callback())

    return server.listen(...args)
  }
}

在上面的例子中,中间件得到的参数还是原生的请求和响应对象。按照 Koa 的实现,现在我们需要创建一个贯穿整个请求的上下文对象,上下文中包括了原生的和封装的请求、响应对象。

// request.js
module.exports = {}

// response.js
module.exports = {}

// context.js
module.exports = {}

// application.js
const http = require('http')
const request = require('./request')
const respOnse= require('./response')
const cOntext= require('./context')

module.exports = class Application {
  constructor() {
    // 确保每个实例都拥有自己的 request response context 三个对象
    this.request = Object.create(request)
    this.respOnse= Object.create(response)
    this.cOntext= Object.create(context)
  }

  createContext() {
    // ...
  }

  callback() {
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      this.middleware(ctx)
    }

    return handleRequest
  }
}

在上面我们创建了三个对象并放置到了应用的实例上面,最后将创建好的上下文对象传递给中间件。在创建上下文的函数中首先要处理的就是请求、响应等几个对象之间的关系:

module.exports = class Application {
  createContext(req, res) {
    const cOntext= Object.create(this.context)
    const request = (context.request = Object.create(this.request))
    const respOnse= (context.respOnse= Object.create(this.response))

    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.respOnse= response
    response.request = request

    return context
  }
}

其中上下文上的 requestresponse 是我们后面要进一步封装的请求和响应对象,而 reqres 则是原生的请求和响应对象。

Context

如上,在每一次收到用户请求时都会创建一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。

除了自行封装的一些属性和方法外,其中也有许多属性和方法都是通过代理的方式获取的请求和响应对象上的值。

const delegate = require('delegates')

const cOntext= (module.exports = {
  onerror(err) {
    const msg = err.stack || err.toString()

    console.error(msg)
  },
})

delegate(context, 'response')
  // ...
  .access('body')

delegate(context, 'request')
  .method('get')
  // ...
  .access('method')

这里我们看到的 delegates 模块是由大名鼎鼎的 TJ 所写的,利用委托模式,它使得外层暴露的对象将请求委托给内部的其他对象进行处理。

Delegator

接下来我们来看看delegates 模块中的核心逻辑。

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target)

  this.proto = proto
  this.target = target
}

Delegator.prototype.method = function(name) {
  const proto = this.proto
  const target = this.target

  // 调用时这里的 this 就是上下文对象,target 则是 request 或 response
  // 所以,最终都会交给请求对象或响应对象上的方法去处理
  proto[name] = function() {
    return this[target][name].apply(this[target], arguments)
  }

  return this
}

Delegator.prototype.access = function(name) {
  return this.getter(name).setter(name)
}

Delegator.prototype.getter = function(name) {
  const proto = this.proto
  const target = this.target

  // __defineGetter__ 方法可以为一个已经存在的对象设置(新建或修改)访问器属性
  proto.__defineGetter__(name, function() {
    return this[target][name]
  })

  return this
}

Delegator.prototype.setter = function(name) {
  const proto = this.proto
  const target = this.target

  // __defineSetter__ 方法可以将一个函数绑定在当前对象的指定属性上,当那个属性被赋值时,绑定的函数就会被调用
  proto.__defineSetter__(name, function(val) {
    return (this[target][name] = val)
  })

  return this
}

module.exports = Delegator

通过 method 方法在上下文上创建指定的函数,调用时会对应调用请求对象或响应对象上的方法进行处理,而对于一些普通属性的读写则直接通过__defineGetter____defineSetter__ 方法来进行代理。

Request

Request 是一个请求级别的对象,封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。

module.exports = {
  get method() {
    // 直接获取原生请求对象上对应的属性
    return this.req.method
  },

  set method(val) {
    this.req.method = val
  },
}

和请求上下文对象类似,请求对象上除了会封装一些常见的属性和方法外,也会去直接读取并返回一些原生请求对象上对应属性的值。

Response

Response 是一个请求级别的对象,封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。

module.exports = {
  get body() {
    return this._body
  },

  set body(val) {
    // 省略了详细的处理逻辑
    this._body = val
  },
}

其中的处理方式和请求对象的处理类似。

中间件

和 Express 不同,Koa 的中间件选择了洋葱圈模型,所有的请求经过一个中间件的时候都会执行两次,这样可以非常方便的实现后置处理逻辑。

function compose(middlewares) {
  return function(ctx) {
    const dispatch = (i = 0) => {
      const middleware = middlewares[i]

      if (i === middlewares.length) {
        return Promise.resolve()
      }

      return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
    }

    return dispatch()
  }
}

module.exports = compose

Koa 的中间件处理被单独的放在了 koa-compose 模块中,上面是插件处理的主要逻辑,核心思想就是将调用下一个插件的函数通过回调的方式交给当前正在执行的中间件。

存在的一个问题是,开发者可能会多次调用执行下个中间件的函数(next),为此我们可以添加一个标识:

function compose(middlewares) {
  return function(ctx) {
    let index = -1

    const dispatch = (i = 0) => {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'))
      }

      index = i

      const middleware = middlewares[i]

      if (i === middlewares.length) {
        return Promise.resolve()
      }

      return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
    }

    return dispatch()
  }
}

module.exports = compose

由于在每一个 dispatch 函数(也就是中间件中的 next 函数)中 i 的值是固定的,在调用一次后它的值就和 index 的值相等了,再次调用就会报错。

Application

Application 是全局应用对象,在一个应用中,只会实例化一个,在它上面我们建立了几个对象之间的关系,同时还会负责组织上面提到的插件。

另外,之前我们的 use 方法直接将指定的插件赋值给了 middleware,可是这样只能有一个插件,因此我们需要改变一下,维护一个数组。

const compose = require('../koa-compose')

module.exports = class Application {
  constructor() {
    // ...
    this.middleware = []
  }

  use(fn) {
    this.middleware.push(fn)
  }

  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      fn(ctx)
    }

    return handleRequest
  }
}

目前为止,我们基本已经完成了本次请求的处理,但并没有完成响应,我们还需要在最后返回 ctx.body 上的数据。

module.exports = class Application {
  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  handleRequest(ctx, fnMiddleware) {
    const Onerror= err => ctx.onerror(err)
    const handleRespOnse= () => respond(ctx)

    return fnMiddleware(ctx)
      .then(handleResponse)
      .catch(onerror)
  }
}

function respond(ctx) {
  ctx.res.end(ctx.body)
}

现在一个基础的 Koa 就算实现了。

其它

这里写下的实现也只是提供一个思路,欢迎大家一起交流学习。

轻拍【滑稽】。。。


推荐阅读
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 深入理解Cookie与Session会话管理
    本文详细介绍了如何通过HTTP响应和请求处理浏览器的Cookie信息,以及如何创建、设置和管理Cookie。同时探讨了会话跟踪技术中的Session机制,解释其原理及应用场景。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • 技术分享:从动态网站提取站点密钥的解决方案
    本文探讨了如何从动态网站中提取站点密钥,特别是针对验证码(reCAPTCHA)的处理方法。通过结合Selenium和requests库,提供了详细的代码示例和优化建议。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文介绍了如何利用JavaScript或jQuery来判断网页中的文本框是否处于焦点状态,以及如何检测鼠标是否悬停在指定的HTML元素上。 ... [详细]
  • 导航栏样式练习:项目实例解析
    本文详细介绍了如何创建一个具有动态效果的导航栏,包括HTML、CSS和JavaScript代码的实现,并附有详细的说明和效果图。 ... [详细]
  • 深入理解Tornado模板系统
    本文详细介绍了Tornado框架中模板系统的使用方法。Tornado自带的轻量级、高效且灵活的模板语言位于tornado.template模块,支持嵌入Python代码片段,帮助开发者快速构建动态网页。 ... [详细]
  • PHP 5.2.5 安装与配置指南
    本文详细介绍了 PHP 5.2.5 的安装和配置步骤,帮助开发者解决常见的环境配置问题,特别是上传图片时遇到的错误。通过本教程,您可以顺利搭建并优化 PHP 运行环境。 ... [详细]
  • 1.如何在运行状态查看源代码?查看函数的源代码,我们通常会使用IDE来完成。比如在PyCharm中,你可以Ctrl+鼠标点击进入函数的源代码。那如果没有IDE呢?当我们想使用一个函 ... [详细]
  • 本文介绍了如何使用JQuery实现省市二级联动和表单验证。首先,通过change事件监听用户选择的省份,并动态加载对应的城市列表。其次,详细讲解了使用Validation插件进行表单验证的方法,包括内置规则、自定义规则及实时验证功能。 ... [详细]
  • DNN Community 和 Professional 版本的主要差异
    本文详细解析了 DotNetNuke (DNN) 的两种主要版本:Community 和 Professional。通过对比两者的功能和附加组件,帮助用户选择最适合其需求的版本。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • 作为一名新手,您可能会在初次尝试使用Eclipse进行Struts开发时遇到一些挑战。本文将为您提供详细的指导和解决方案,帮助您克服常见的配置和操作难题。 ... [详细]
  • 如何高效创建和使用字体图标
    在Web和移动开发中,为什么选择字体图标?主要原因是其卓越的性能,可以显著减少HTTP请求并优化页面加载速度。本文详细介绍了从设计到应用的字体图标制作流程,并提供了专业建议。 ... [详细]
author-avatar
3e83owut
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有