博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一步步实现koa核心代码
阅读量:5788 次
发布时间:2019-06-18

本文共 11800 字,大约阅读时间需要 39 分钟。

导语:实现简单的koa核心代码,便于理解koa原理。顺便学习koa代码的那些骚操作

简单分析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  }}复制代码

转载地址:http://iwhyx.baihongyu.com/

你可能感兴趣的文章
JProfiler学习笔记
查看>>
Loadrunner脚本编程(4)-数据类型操作和字符串操作
查看>>
arpg网页游戏之地图(二)
查看>>
SQL SERVER 2008取出XML数据
查看>>
STL 算法
查看>>
分享:Backbone.js 样例站点与入门指南
查看>>
图的基本算法
查看>>
《架构之美》摘录三
查看>>
myeclipse6.5上基于JAX-WS开发Webservice(中文示例)
查看>>
HTML基础(一)
查看>>
谈谈冒烟测试
查看>>
boost.circular_buffer简介
查看>>
Database Appliance并非Mini版的Exadata-还原真实的Oracle Unbreakable Database Appliance
查看>>
CORTEX-M3 异常/中断控制(使能和除能)
查看>>
网页图片缩放(js)
查看>>
没有设计模式画小人,有趣画板
查看>>
简易高负载进程记录脚本
查看>>
在VMware上安装CentOS-6.5 minimal - 安装VMware Tools
查看>>
使用 Xtrabackup 在线对MySQL做主从复制
查看>>
linux sort/uniq 使用
查看>>