乐趣区

关于node.js:从零到壹Koa-从理解到实现

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

依据官网的介绍,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 就算实现了。

其它

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

轻拍【滑稽】。。。

退出移动版