Koa 学习 02 Koa 实现原理和极简模拟案例

通过模拟一个极简版本的 Koa 学习实现原理。

初始化项目

# 安装 koa
npm i koa

添加启动文件:

// app.js
const Koa = require('koa')

const app = new Koa()

app.listen(3000, () => {
    
    
  console.log('server is running on http://localhost:3000')
})

nodemon ./app.js启动服务。

源码目录结构

查看 node_modules/koapackage.json,查看加载 koa 时实际加载的文件:

"main": "lib/application.js",
// node_modules\koa\lib\application.js
module.exports = class Application extends Emitter {
    
    
  listen(...args) {
    
    
    debug('listen');
    // 使用原生 http 模块开启 HTTP 服务,成功后调用回调
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
    
  callback() {
    
    
    // 获取全部中间件并依次调用
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
    
    
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  // 挂载中间件的方法
  use(fn) {
    
    ...}
  
  ...
}

Koa 中大量使用了 ES6 的语法(例如 class)进行编写,加载 koa,实际上加载的是 Application 类,通过实例化这个类创建 app

lib 下其他文件:

├─ application.js # 负责整个应用的创建组织
├─ context.js # 处理 context 上下文对象
├─ request.js # 内部封装的 context.request 对象
└─ response.js # 内部封装的 context.response 对象

下面开始模仿一个极简的 Koa。

基础结构

在项目目录下新建一个 my-koa 文件夹存放模拟代码。

mkdir my-koa
cd ./my-koa
# 初始化 npm 以在该目录下安装依赖
npm init -y

修改入口文件路径:

// my-koa/package.json
"main": "lib/application.js",

添加文件:

// my-koa\lib\application.js
const http = require('http')

class Application {
    
    
  listen(...args) {
    
    
    const server = http.createServer((req, res) => {
    
    
      res.end('My Koa')
    })

    server.listen(...args)
  }
}

module.exports = Application

重新访问 http://localhost:3000 测试结果。

中间件

示例代码

const one = (ctx, next) => {
    
    
  console.log('>> one')
  next()
  console.log('<< one')
}

const two = (ctx, next) => {
    
    
  console.log('>> two')
  // next()
  console.log('<< two')
}

const three = (ctx, next) => {
    
    
  console.log('>> three')
  next()
  console.log('<< three')
}

app.use(one)
app.use(two)
app.use(three)

console.log(app.middleware)

模拟实现

// my-koa\lib\application.js
const http = require('http')

class Application {
    
    
  constructor() {
    
    
    // 保存用户添加的中间件函数
    this.middleware = []
  }

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

    server.listen(...args)
  }

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

  // 异步递归遍历调用中间件处理函数
  compose(middleware) {
    
    
    // 返回一个高级函数,允许接受其他参数
    return function () {
    
    
      const dispatch = index => {
    
    
        if (index >= middleware.length) {
    
    
          return Promise.resolve()
        }

        const fn = middleware[index]

        // 将中间件函数调用包装为一个 Promise 兼容异步处理
        return Promise.resolve(
          // fn(ctx, next)
          fn({
    
    }, () => dispatch(index + 1))
        )
      }

      // 返回并调用第一个中间件处理函数
      return dispatch(0)
    }
  }

  callback() {
    
    
    // 获取调用第一个中间件函数的方法
    const fnMiddleware = this.compose(this.middleware)
    const handleRequest = (req, res) => {
    
    
      // 开始执行第一个中间件函数
      fnMiddleware().then(() => {
    
    
        console.log('end')
        res.end('My Koa')
      }).catch(err => {
    
    
        console.log(err.message)
      })
    }

    return handleRequest
  }
}

module.exports = Application

分析 context 对象的内容组成

Koa Context 实际上是将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。

每一个请求都将创建一个 Context,并在中间件中作为参数引用。

// 打印 Koa Context:
{
    
    
  request: {
    
    
    method: 'GET',
    url: '/',
    header: {
    
    ...}
  }, // Koa 封装的 request 对象
  response: {
    
    
    status: 404,
    message: 'Not Found',
    header: {
    
    ...}
  }, // Koa 封装的 response 对象
  app: {
    
     ... }, // app 实例
  originalUrl: '/',
  req: '<original node req>', // node 原生 request 对象
  res: '<original node res>', // node 原生 response 对象
  socket: '<original node socket>' //  // node 原生 socket 对象
}

示例代码

app.use((ctx, next) => {
    
    
  console.log(ctx)

  // node 原生对象
  console.log(ctx.req)
  console.log(ctx.res)
  console.log(ctx.req.url)

  // Koa 封装的 Request 对象
  console.log(ctx.request)
  console.log(ctx.request.header)
  console.log(ctx.request.method)
  console.log(ctx.request.url)
  console.log(ctx.request.path)
  console.log(ctx.request.query)

  // Request 别名
  console.log(ctx.header)
  console.log(ctx.method)
  console.log(ctx.url)
  console.log(ctx.path)
  console.log(ctx.query)

  // Koa 封装的 Response 对象
  console.log(ctx.response)
  // ctx.response.status = 200
  // ctx.response.message = 'Success'
  // ctx.response.type = 'plain'
  // ctx.response.body = 'Hello Koa'

  // Response 别名
  ctx.status = 200
  ctx.message = 'Success'
})

初始化 Context 上下文对象

// my-koa\lib\application.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Application {
    
    
  constructor() {
    
    
    // 保存用户添加的中间件函数
    this.middleware = []

    // 拷贝创建,避免互相污染
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }

  listen(...args) {
    
    ...}

  use(fn) {
    
    ...}

  // 异步递归遍历调用中间件处理函数
  compose(middleware) {
    
    
    // 返回一个高级函数,允许接受其他参数
    return function (context) {
    
    
      const dispatch = index => {
    
    
        ...

        // 将中间件函数调用包装为一个 Promise 兼容异步处理
        return Promise.resolve(
          // fn(ctx, next)
          fn(context, () => dispatch(index + 1))
        )
      }

      // 返回并调用第一个中间件处理函数
      return dispatch(0)
    }
  }

  // 构造上下文对象
  createContext(req, res) {
    
    
    // 为了避免请求之间 context 数据交叉污染
    // 这里为每个请求单独创建 context 对象
    const context = Object.create(this.context)
    // 在 context 中可以获取 Request
    const request = context.request = Object.create(this.request)
    // 在 context 中可以获取 Response
    const response = context.response = Object.create(this.response)

    context.app = request.app = response.app = this

    // 原生 Node 请求/响应对象
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res

    // 在 Request 和 Respon 中也可以获取 context 上下文对象
    request.ctx = response.ctx = context
    // Requset 中也可以获取 Response
    request.response = response
    // Response 中也可以获取 Requset
    response.request = request

    // 没有经过任何处理的请求路径
    context.originUrl = request.originUrl = req.url
    // 初始化 state 数据对象,用于给模板视图提供数据
    context.state = {
    
    }

    return context
  }

  callback() {
    
    
    // 获取调用第一个中间件函数的方法
    const fnMiddleware = this.compose(this.middleware)
    const handleRequest = (req, res) => {
    
    
      // 每个请求都会创建一个独立的 Context 对象,它们之间不会互相污染
      const context = this.createContext()

      // 开始执行第一个中间件函数
      fnMiddleware(context).then(() => {
    
    
        console.log('end')
        res.end('My Koa')
      }).catch(err => {
    
    
        console.log(err.message)
      })
    }

    return handleRequest
  }
}

module.exports = Application

// my-koa\lib\context.js
const context = {
    
    }

module.exports = context

// my-koa\lib\request.jsmy-koa\lib\request.js/
const request = {
    
    }

module.exports = request

// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {
    
    }

module.exports = response

扩展 Request 和 Response

使用对象的访问器属性(get 和 set)动态获取和设置属性。

// my-koa\lib\request.jsmy-koa\lib\request.js/
const url = require('url')

const request = {
    
    
  get header() {
    
    
    return this.req.headers
  },
  set header(val) {
    
    
    this.req.headers = val
  },
  get headers() {
    
    
    return this.req.headers
  },
  set headers(val) {
    
    
    this.req.headers = val
  },
  get url() {
    
    
    return this.req.url
  },
  get path() {
    
    
    return url.parse(this.req.url).pathname
  },
  get query() {
    
    
    return url.parse(this.req.url, true).query
  },
  get method() {
    
    
    return this.req.method
  }
}

module.exports = request

// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {
    
    
  set status(val) {
    
    
    this.res.statusCode = val
  },
  set message(msg) {
    
    
    this.res.statusMessage = msg;
  },
}

module.exports = response

处理 Context 中的代理别名

// my-koa\lib\context.js
const context = {
    
    
  get method() {
    
    
    return this.request.method
  },
  get header() {
    
    
    return this.request.header
  },
  ...
}

module.exports = context

可以看到 context 别名的 getter 函数处理逻辑都一样(归功于 request 中定义了同名的属性),所以可以将设置别名的操作提取为一个方法:

// my-koa\lib\context.js
const context = {
    
    }

definePorpperty('request', 'method')
definePorpperty('request', 'header')
definePorpperty('request', 'url')
definePorpperty('request', 'path')
definePorpperty('request', 'query')

function definePorpperty(target, name) {
    
    
  Object.defineProperty(context, name, {
    
    
    get() {
    
    
      return this[target][name]
    },
    set(value) {
    
    
      this[target][name] = value
    }
  })
}

module.exports = context

注意:Koa 中使用的 delegates 包,内部使用 Object.prototype.__defineGetter__()MDN)和 Object.prototype.__defineSetter__()MDN)设置 get/set 属性,不过该特性已从 Web 标准中删除。

设置和发送 body 数据

  • 本质上使用的是 node 的 Response 对象发送数据
  • 多次设置 body 最终响应的应该是最后一次设置的内容

示例代码

app.use((ctx, next) => {
    
    
  ctx.body = 'Hello Koa1'
  next()
  ctx.body = 'Hello Koa3'
})

app.use((ctx, next) => {
    
    
  ctx.body = 'Hello Koa2'
})

// 最终应该响应 Hello Koa3

模拟实现

在 Response 对象中设置 body 的 getter 和 setter:

// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {
    
    
  set status(val) {
    
    
    this.res.statusCode = val
  },
  set message(msg) {
    
    
    this.res.statusMessage = msg;
  },

  _body: '', // 真正用来存数据的属性
  get body() {
    
    
    return this._body
  },
  set body(val) {
    
    
    this._body = val
  }
}

module.exports = response

添加 context 的 body 别名:

// my-koa\lib\context.js
definePorpperty('response', 'body')

执行完中间件后(洋葱圈从进到出)将 body 返回给客户端:

// my-koa\lib\application.js
...

class Application {
    
    
  ...
  
  callback() {
    
    
    const fnMiddleware = this.compose(this.middleware)
    const handleRequest = (req, res) => {
    
    
      const context = this.createContext(req, res)

      // 开始执行第一个中间件函数,并传入上下文对象
      fnMiddleware(context).then(() => {
    
    
        res.end(context.body)
      }).catch(err => {
    
    
        res.end(err.message)
      })
    }

    return handleRequest
  }
}

module.exports = Application

处理 body 数据格式

示例代码

response.body 支持一下格式,如果 res.status 没有赋值,Koa会自动设置为 200204。:

// app.js
// const Koa = require('koa')
const Koa = require('./my-koa')
const fs = require('fs')
const fsPromises = require('fs').promises

const app = new Koa()

app.use(async (ctx, next) => {
    
    
  // 字符串
  ctx.body = 'string'

  // // 数字
  // ctx.body = 123

  // // buffer
  // ctx.body = await fsPromises.readFile('./package.json')

  // // 文件流
  // ctx.body = fs.createReadStream('./package.json')

  // // 对象&数组会转化成 JSON 字符串
  // ctx.body = { foo: 'bar' }
  // ctx.body = [1, 2, 3]

  // // 无响应内容
  // ctx.body = null
})

app.listen(3000, () => {
    
    
  console.log('server is running on http://localhost:3000')
})

模拟实现

执行完中间件后,调用一个方法处理 body 并返回给客户端:

// my-koa\lib\application.js
...
// 引入 node 原生 stream 构造函数
const Stream = require('stream')

class Application {
    
    
  ...

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

      fnMiddleware(context).then(() => {
    
    
        // res.end(context.body)
        // 调用函数处理 body
        respond(context)
      }).catch(err => {
    
    
        res.end(err.message)
      })
    }

    return handleRequest
  }
}

function respond(ctx) {
    
    
  const body = ctx.body
  const res = ctx.res

  // 字符串 和 Buffer 直接返回
  if (typeof body === 'string') return res.end(body)
  if (Buffer.isBuffer(body)) return res.end(body)
  // 可读流通过管道发送给可写流(res)
  if (body instanceof Stream) return body.pipe(res)
  // 数字转化成字符串
  if (typeof body === 'number') return res.end(body + '')
  // 对象和数组转化成 JSON 字符串
  if (body !== null && typeof body === 'object') {
    
    
    const jsonStr = JSON.stringify(body)
    return res.end(jsonStr)
  }

  res.statusCode = 204
  res.end()
}

module.exports = Application

猜你喜欢

转载自blog.csdn.net/u012961419/article/details/123915478
koa