一步步实现koa核心代码

栏目: Node.js · 发布时间: 6年前

内容简介:创建一个 http 服务,只绑一个中间件。从这段代码中可以看出我们再来看看 koa 源码的目录结构

创建一个 http 服务,只绑一个中间件。 创建 index.js

/** index.js */
const Koa = require('koa')

const app = new Koa()

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

app.listen(8080, function() {
  console.log('server start 8080')
})
复制代码

从这段代码中可以看出

  • Koa 是一个构造函数
  • Koa 的原形上至少有 ues、listen 两个方法
  • listen 的参数与 http 一致
  • ues 接受一个方法,在用户访问的时候调用,并传入上下文 ctx (这里先不考虑异步与next,一步步实现)

我们再来看看 koa 源码的目录结构

|-- koa
    |-- .npminstall.done
    |-- History.md
    |-- LICENSE
    |-- Readme.md
    |-- package.json
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js
复制代码

其中 application.js 是入口文件,打开后可以看到是一个 class。context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx

启动http服务

先编写 application.js 部分代码。创建 myKoa 文件夹,我们的koa代码将会放在这个文件内。 创建 myKoa/application.js

通过分析已经知道 application.js 导出一个 class,原形上至少有 listen 和 use 两个方法。listen 创建服务并监听端口号 http服务,use 用来收集中间件。实现代码如下

/** myKoa/application.js */
const http = require('http')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
  }
  // 收集中间件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 处理当前请求方法
  handleRequest(req, res) { // node 传入的 req、res
    res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
  }
  // 创建服务并监听端口号
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}
复制代码

代码很简单。 use 把中间件存入 middlewareslisten 启动服务,每次请求到来调用 handleRequest

创建 ctx (一)

context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx 。代码如下

/** myKoa/context.js */
const proto = {}

module.exports = proto
复制代码
/** myKoa/request.js */
module.exports = {}
复制代码
/** myKoa/response.js */
module.exports = {}
复制代码

三者的关系是: request.js、response.js 两个文件的导出会绑定到 context.js 文件导出的对象上,分别作为 ctx.request 和 ctx.response 使用。

koa 为了每次 new Koa() 使用的 ctx 都是相互独立的,对 context.js、request.js、response.js 导出的对象做了处理。源码中使用的是 Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__ 。一会在代码中演示用法

创建ctx之前,再看一下ctx上的几个属性,和他们直接的关系。一定要分清哪些是node自带的,哪些是koa的属性

app.use(async ctx => {
  ctx; // 这是 Context
  ctx.req; // 这是 node Request
  ctx.res; // 这是 node Response
  ctx.request; // 这是 koa Request
  ctx.response; // 这是 koa Response
  ctx.request.req; // 这是 node Request
  ctx.response.res;  // 这是 node Response
});
复制代码

为什么这个设计,在文章后面将会解答

开始创建 ctx。部分代码如下

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
    // 绑定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 创建上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中间件
  use(fn) {/* ... */}
  // 处理当前请求方法
  handleRequest(req, res) {
    // 创建 上下文,准备传给中间件
    const ctx = this.createContext(req, res)
    
    res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}
复制代码

此时就创建了一个基础的上下文 ctx。

创建 ctx (二)

获取上下文上的属性

实现一个 ctx.request.url 。开始前先考虑几个问题

  • request.js 导出的是一个对象,不能接受参数
  • ctx.req.urlctx.request.urlctx.request.req.url 三者直接应该始终相等

koa 是这样做的

  • 第一步 ctx.request.req = ctx.req = req
  • 访问 ctx.request.url 转成访问 ctx.request.req.url

没错,就是 get 语法糖

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
复制代码

此时的 this 指向的是 Object.create(request) 生成的对象,并不是 request.js 导出的对象

设置上下文上的属性

接下来我们实现 ctx.response.body = 'Hello World' 。当设置 ctx.response.body 时实际上是把属性存到了 ctx.response._body 上,当获取 ctx.response.body 时只需要在 ctx.response._body 上取出就可以了 。代码如下

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
复制代码

此时的 this 指向的是 Object.create(response) 生成的对象,并不是 response.js 导出的对象

设置 ctx 别名

koa 给我们设置了很多别名,比如 ctx.body 就是 ctx.response.body

有了之前的经验,获取/设置属性就比较容易。直接上代码

/** myKoa/context.js */
const proto = {
  get url() {
    return this.request.req.url
  },
  get body() {
    return this.response.body
  },
  set body(v) {
    this.response.body = v
  },
}
module.exports = proto
复制代码

有没有感觉很简单。当然koa上下文部分没有到此结束。看 koa/lib/context.js 代码,在最下面可以看到这样的代码(从只挑选了 access 方法)

delegate(proto, 'response')
  .access('status')
  .access('body')

delegate(proto, 'request')
  .access('path')
  .access('url')
复制代码

koa 对 get/set 做了封装。用的是 Delegator 第三方包。核心是用的 __defineGetter____defineSetter__ 两个方法。这里为了简单易懂,只是简单封装两个方法代替 Delegator 实现简单的功能。

// 获取属性。调用方法如 defineGetter('response', 'body')
function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}
// 设置属性。调用方法如 defineSetter('response', 'body')
function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}
复制代码

myKoa/context.js 文件最终修改为

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto
复制代码

让 ctx.body 显示在页面上

这步非常简单,只需要判断 ctx.body 是否有值,并触发 req.end() 就完成了。相关代码如下

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    
    res.statusCode = 404 //默认 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}
复制代码

同理可以处理 header 等属性

实现同步中间件

中间件接受两个参数,一个上下文 ctx ,一个 next 方法。上下文 ctx 已经写好了,主要是怎么实现 next 方法。

写一个dispatch方法,他的主要功能是:比如传入下标0,找出数组中下标为0的方法 middleware ,调用 middleware 并传入一个方法 next ,并且当 next 调用时, 查找下标加1的方法。实现如下

const middlewares = [f1, f2, f3]
function dispatch(index) {
  if (index === middlewares.length) return
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)
复制代码

此时就实现了 next 方法。

在koa中,是不允许一个请求中一个中间件调用两次 next 。比如

app.use((ctx, next) => {
  ctx.body = 'Hello World'
  next()
  next() // 报错 next() called multiple times
})
复制代码

koa 用了一个小技巧。记录每次调用的中间件下标,当发现调用的中间件下标没有加1(中间件下标 <= 上一次中间件下标)时,就报错。修改代码如下

const middlewares = [f1, f2, f3] // 比如中间件中有三个方法
let i = -1 
function dispatch(index) {
  if (index <= i) throw new Error('next() called multiple times')
  if (index === middlewares.length) return
  i = index
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)
复制代码

中间件代码基本完成,传入 ctx 、加入 myKoa/application.js 文件。

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      middleware(ctx, next)
    }
    dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    this.compose(ctx)

    res.statusCode = 404 //默认 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}
复制代码

到此就实现了同步中间件

实现异步中间件

koa 中使用异步中间件的写法如下

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

app.use(async (ctx, next) => {
  await new Promise((res, rej) => setTimeout(res,1000))
  console.log('ctx.body:', ctx.body)
})
复制代码

上述代码接受请求后大约1s 控制台打印 ctx.body: Hello World 。可以看出,koa是基于 async/await 的。期望每次 next() 后返回的是一个 Promise

同时考虑到中间件变为异步执行,那么 handleRequest 应该等待中间件执行完再执行相关代码。那么 compose 也应该返回 Promise

可以通过 async 快速完成 普通函数 =》Promise 的转化。

修改 compose 代码和 handleRequest 代码

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默认 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}
复制代码

代码展示

application.js

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
    // 绑定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 创建上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中间件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('multi called next()')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默认 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 创建服务并监听端口号
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}
复制代码

context.js

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto
复制代码

request.js

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
复制代码

response.js

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
复制代码

以上所述就是小编给大家介绍的《一步步实现koa核心代码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Designing Data-Intensive Applications

Designing Data-Intensive Applications

Martin Kleppmann / O'Reilly Media / 2017-4-2 / USD 44.99

Data is at the center of many challenges in system design today. Difficult issues need to be figured out, such as scalability, consistency, reliability, efficiency, and maintainability. In addition, w......一起来看看 《Designing Data-Intensive Applications》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具