前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >源码共读-Koa

源码共读-Koa

作者头像
kai666666
发布2024-07-11 19:02:51
110
发布2024-07-11 19:02:51
举报
文章被收录于专栏:橙光笔记橙光笔记

Koa是基于 Node.js 平台的下一代 web 开发框架,它的源码可以看这里,本章通过源码来简绍一下Koa是怎么实现的。

核心代码

Koa的核心代码只有4个文件,如图。

核心代码
核心代码

各个文件的作用:

application.js:Koa的核心,对应Koa App类。 context.js:对应上下文对象ctx。 request.js:对应ctx.request对象。 response.js:对应ctx.response对象。

Koa实现

Koa使用

Koa使用如下:

代码语言:javascript
复制
const Koa = require('koa');
const app = new Koa();

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

app.listen(3000, () => {
  console.log("服务器启动成功!");、
});

Koa底层是基于原生http模块,原生http模块怎么启动一个服务呢?如下:

代码语言:javascript
复制
const http = require('http');

const server = http.createServer(function(req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end("Hello World");
});

server.listen(3000, () => {
  console.log("服务器启动成功!");、
});

观察上面的代码,两者是不是挺像的。

application源码

为了方便查看application的核心逻辑,下面是我去掉了部分非核心代码的application源码:

代码语言:javascript
复制
const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
const statuses = require('statuses')
const Emitter = require('events')
const Stream = require('stream')
const http = require('http')

class Application extends Emitter {
  constructor (options) {
    super()
    options = options || {}
    this.env = options.env || process.env.NODE_ENV || 'development'
    this.compose = options.compose || compose
    if (options.keys) this.keys = options.keys
    this.middleware = []
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }

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

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    this.middleware.push(fn)
    return this
  }

  callback() {
    const fn = this.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
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }

  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
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
  }

  onerror(err) {
    if (err.status === 404 || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
  }
}

function respond (ctx) {
  // allow bypassing koa
  if (ctx.respond === false) return

  if (!ctx.writable) return

  const res = ctx.res
  let body = ctx.body
  const code = ctx.status

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null
    return res.end()
  }

  if (ctx.method === 'HEAD') {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response
      if (Number.isInteger(length)) ctx.length = length
    }
    return res.end()
  }

  // status body
  if (body == null) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type')
      ctx.response.remove('Transfer-Encoding')
      ctx.length = 0
      return res.end()
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code)
    } else {
      body = ctx.message || String(code)
    }
    if (!res.headersSent) {
      ctx.type = 'text'
      ctx.length = Buffer.byteLength(body)
    }
    return res.end(body)
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body)
  if (typeof body === 'string') return res.end(body)
  if (body instanceof Stream) return body.pipe(res)

  // body: json
  body = JSON.stringify(body)
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body)
  }
  res.end(body)
}

当调用app.use的时候,实际上是把中间件函数加入到this.middleware数组当中。 当调用app.listen的时候,通过http.createServer来创建http服务并使用server.listen来监听服务。 这里比较难理解的是callback函数,它使用compose将中间件合并成一个调用函数,具体怎么合并的我们稍后再说。如果error事件没有监听的话,添加一个默认的监听函数,默认的onerror函数实际上就是打印错误信息;this.listenerCount是从哪里来的呢?实际上Application类是继承自node中的Emitter,该方法也是Emitter的方法。最后返回了一个handleRequest函数,该函数做了2件事,首先通过reqres构建ctx,然后调用this.handleRequest,注意this.handleRequestApplication类的属性而不是callback中的handleRequest,也就是这里并没有递归调用。 在this.handleRequest函数中调用了中间件函数fnMiddleware(ctx),当中间件函数都调用完了以后调用respond(ctx)respond通过不同的情况去处理res的结果;失败的时候调用ctx.onerror(err)。另外在中间件处理之前会调用onFinished(res, onerror)来监听出错的情况,onFinished的代码请看这里

koa-compose源码

在讲述源码之前我们先看看koa-compose中间件是怎么使用的。

代码语言:javascript
复制
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx, next) => {
  console.log('第1个中间件开始');
  await next();
  console.log('第1个中间件结束');
});

app.use(async(ctx, next) => {
  console.log('第2个中间件开始');
  await next();
  console.log('第2个中间件结束');
});

app.use(async(ctx, next) => {
  console.log('第3个中间件开始');
  await next();
  console.log('第3个中间件结束');
});

app.listen(3000, () => {
  console.log("服务器启动成功!");
});

客户端打印:

代码语言:javascript
复制
第1个中间件开始
第2个中间件开始
第3个中间件开始
第3个中间件结束
第2个中间件结束
第1个中间件结束

这就是Koa中间件著名的洋葱模型。

洋葱模型
洋葱模型

我们先不谈Koa只看看koa-compose做了什么事。

代码语言:javascript
复制
const compose = require('koa-compose');

const middleware = [
  async(ctx, next) => {
    console.log('第1个中间件开始');
    await next();
    console.log('第1个中间件结束');
  },
  async(ctx, next) => {
    console.log('第2个中间件开始');
    await next();
    console.log('第2个中间件结束');
  },
  async(ctx, next) => {
    console.log('第3个中间件开始');
    await next();
    console.log('第3个中间件结束');
  }
];
const fn = compose(middleware);
const ctx = {};
fn(ctx).then(() => {
  console.log('处理完成了');
});

上面打印:

代码语言:javascript
复制
第1个中间件开始
第2个中间件开始
第3个中间件开始
第3个中间件结束
第2个中间件结束
第1个中间件结束
处理完成了

koa-compose把多个中间件合并成一个函数,通过await next()来调用下一个中间件,其源码如下:

代码语言:javascript
复制
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

首先对middleware做类型检查,middleware必须是数组,同时每一个中间件必须是函数。然后返回一个函数,这个函数第一个参数是上下文对象,第二个参数是下个中间件执行的next函数。核心逻辑是上面的dispatch方法,在dispatch方法中会返回Promise。dispatch方法实际上就是next方法,首次会调用dispatch(0)来触发第一个中间件函数。当一个中间件中调用next方法后会把index标记为当前的索引,如果一个中间件多次调用next方法,那么由于第一次调用是index会标记为i,那么第二次调用的时候iindex是相等的,也就是第二次的时候会走if (i <= index) return Promise.reject(new Error('next() called multiple times'))逻辑,也就是会报错。每次调用的时候根据索引获取当前要执行的中间件函数,在第18行会执行当前中间件,并把下一个dispatch当作第二个参数next传入到下一个中间件中。当执行到最后一个中间件的时候,设置fn = next由于Application代码的第52行并没有传递第二个参数,所以此时nextundefined,那么compose中将会走第16行if (!fn) return Promise.resolve()的逻辑。如果传递了函数那么会执行传入的函数,当此函数中调用next以后,由于索引已经超过了middleware的长度,所以下次函数执行事也会走第16行的逻辑。

context源码

context是对上下文对象的封装,具体代码如下:

代码语言:javascript
复制
const util = require('util')
const createError = require('http-errors')
const httpAssert = require('http-assert')
const delegate = require('delegates')
const statuses = require('statuses')
const Cookies = require('cookies')

const COOKIES = Symbol('context#cookies')

const proto = module.exports = {
  inspect () {
    if (this === proto) return this
    return this.toJSON()
  },

  toJSON () {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    }
  },

  assert: httpAssert,

  throw (...args) {
    throw createError(...args)
  },

  onerror (err) {.
    if (err == null) return

    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))

    let headerSent = false
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true
    }

    // delegate
    this.app.emit('error', err, this)

    if (headerSent) {
      return
    }

    const { res } = this

    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name))
    } else {
      res._headers = {} // Node < 7.7
    }

    // then set those specified
    this.set(err.headers)

    // force text/plain
    this.type = 'text'

    let statusCode = err.status || err.statusCode

    // ENOENT support
    if (err.code === 'ENOENT') statusCode = 404

    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

    // respond
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  },

  get cookies () {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      })
    }
    return this[COOKIES]
  },

  set cookies (_cookies) {
    this[COOKIES] = _cookies
  }
}

/**
 * Response delegation.
 */
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable')

/**
 * Request delegation.
 */
delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip')

可见context实际上就是一个对象,它对Cookieonerror做了一个封装。最后使用delegate()来代理requestresponse对象,delegate不了解的同学可以看下面这个示例。

代码语言:javascript
复制
const delegate = require('delegates');

const obj = {
  aaa: {
    name: 'aaa',
    age: 18,
    isBoy: true,
    say() {
      console.log(`我是${this.name},今年${this.age}`);
    }
  }
};

delegate(obj, 'aaa')
  .method('say')
  .getter('name')
  .setter('age')
  .access('isBoy')


console.log(obj.name); // 打印 aaa
obj.age = 19; // 可以设置属性
obj.say(); // 打印 我是aaa,今年19
console.log(obj.isBoy); // 打印 true

上面代理了obj对象的aaa属性,所以直接可以通过obj来访问aaa中代理的属性和方法,其中method表示代理方法,getter表示代理get方法,setter表示代理set方法,access表示不但代理了get同时也代理了set。delegates的实现也不难:

代码语言:javascript
复制
function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

Delegator.auto = function(proto, targetProto, targetProp){
  var delegator = Delegator(proto, targetProp);
  var properties = Object.getOwnPropertyNames(targetProto);
  for (var i = 0; i < properties.length; i++) {
    var property = properties[i];
    var descriptor = Object.getOwnPropertyDescriptor(targetProto, property);
    if (descriptor.get) {
      delegator.getter(property);
    }
    if (descriptor.set) {
      delegator.setter(property);
    }
    if (descriptor.hasOwnProperty('value')) { // could be undefined but writable
      var value = descriptor.value;
      if (value instanceof Function) {
        delegator.method(property);
      } else {
        delegator.getter(property);
      }
      if (descriptor.writable) {
        delegator.setter(property);
      }
    }
  }
};

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  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){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);

  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name);

  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};

Delegator.prototype.fluent = function (name) {
  var proto = this.proto;
  var target = this.target;
  this.fluents.push(name);

  proto[name] = function(val){
    if ('undefined' != typeof val) {
      this[target][name] = val;
      return this;
    } else {
      return this[target][name];
    }
  };

  return this;
};

request与response

requestresponse就是一个简单的对象,没什么好说的,比如request代码大致如下:

代码语言:javascript
复制
module.exports = {
  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
  },

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

  // 省略其他代码
}

这里需要注意的是有一个this.req对象,这个对象是从哪里来的?请看ApplicationcreateContext方法的第61行,在这里把node的req挂载了上来,res同理。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 核心代码
  • Koa实现
    • Koa使用
      • application源码
        • koa-compose源码
          • context源码
            • request与response
            相关产品与服务
            消息队列 TDMQ
            消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档