likes
comments
collection
share

koa学习笔记

作者站长头像
站长
· 阅读数 42

我们从以下几个方面来学习Koa

  • 创建应用程序函数
  • 扩展res和req
  • 中间件实现原理

创建应用程序函数

Koa 是依赖 node 原生的 http 模块来实现 http server 的能力,原生 http 模块可以通过几行代码就启动一个监听在 8080 端口的http服务,createServer 的第一个参数是一个回调函数,这个回调函数有两个参数,一个是请求对象,一个是响应对象,可以根据请求对象的内容来决定响应数据的内容;

const http = require("http");

const server = http.createServer((req, res) => {
  // 每一次请求处理的方法
  console.log(req.url);
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello NodeJS");
});

server.listen(8080);

在 Koa 中,createServer 回调函数中的 req 和 res 会被保存到 ctx 对象上,伴随整个处理请求的生命周期,Koa 源码中的 request.js 和 response.js 就是对这两个对象添加了大量便捷获取数据和设置数据的方法,如获取请求的方法、请求的路径、设置返回数据体、设置返回状态码等操作。

而Koa在封装创建应用程序的方法中主要执行了以下流程:

  • 组织中间件(监听请求之前)
  • 生成context上下文对象
  • 执行中间件
  • 执行默认响应方法或者异常处理方法
// application.js

// 这个方法是封装了http模块提供的http.createServer和listen方法
listen(...args) {
  debug('listen');
  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) => {
    //创建context上下文对象
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };
  return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  // 默认状态码为404
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  // 执行中间件
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

// 创建context上下文对象
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;
}

扩展res和req

NodeJS中原生的res和req是http.IncomingMessage和http.ServerResponse的实例,Koa中则是自定义request和response对象,保持对原生的res和req引用,然后通过getter和setter方法实现扩展。

// application.js

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; // 保存原生 req 对象
    context.res = request.res = response.res = res; // 保存原生 res 对象
    request.ctx = response.ctx = context;
    request.response = response; // response 拓展
    response.request = request; // request 拓展
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    // 最终返回完整的context上下文对象
    return context;
}

在Koa中要区别这两组对象:

  • request、response: Koa扩展的对象
  • res、req: NodeJS原生对象
// request.js
get header() {
  return this.req.headers;
},

set header(val) {
  this.req.headers = val;
}

此时已经可以采用这样的方式访问header属性:

ctx.request.header

delegates 属性代理

koa对response和request使用了属性代理,使我们可以直接在context中使用request和response的方法,其中method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter

// context.js
/**
 * 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');

delegates 实现

对于 setter 和 getter方法,是通过调用对象上的 __defineSetter__ 和 __defineGetter__ 来实现的

// delegates/index.js

// getter
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;
};

// setter
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;
};

// access
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

// method
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;
};

中间件实现原理

在Koa中,通过app.use() 来注册中间件,Koa支持三种不同类型的中间件:普通函数,async 函数,Generator函数,如果是Generator函数,那就用 convert 把函数包起来,然后在push到 this.middleware

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
        fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

convert作用

convert是用于将koa中以前基于generator写法的中间件转为基于promise写法。convert()的源码实现逻辑如下:

  1. convert 方法首先判断传入的中间件是否是一个函数,如果不是就抛出异常;
  2. 接着判断是否是一个 generator 函数,如果不是就直接返回,不做处理;
  3. 利用co将 generator 函数形式的中间件转成 promise 形式的中间件。

    function convert (mw) {
      if (typeof mw !== 'function') {
     throw new TypeError('middleware must be a function')
      }
      // assume it's Promise-based middleware
      if (
     mw.constructor.name !== 'GeneratorFunction' &&
     mw.constructor.name !== 'AsyncGeneratorFunction'
      ) {
     return mw
      }
      const converted = function (ctx, next) {
     return co.call(
       ctx,
       mw.call(
         ctx,
         (function * (next) { return yield next() })(next)
       ))
      }
      converted._name = mw._name || mw.name
      return converted
    }

    中间件的执行

    Koa中间件的执行流程主要通过koa-compose中的compose函数完成,基于洋葱圈模型:koa学习笔记koa-compose 的代码很短,一共才不到50行,主要执行顺序如下:

    // koa-compose 
    function compose (middleware) { //传入middleware数组
      // 不是数组抛出异常
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      // 判断每个middleware中的每一项是否为函数
      // 不是函数抛出异常
      for (const fn of middleware) {
     if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
      // 返回一个函数
      return function (context, next) {
     //index计数
     let index = -1
     return dispatch(0) //调用dispatch,从第一个中间件开始
     function dispatch (i) {
       // i小于index,证明在中间件内调用了不止一次的next(),抛出错误
       if (i <= index) return Promise.reject(new Error('next() called multiple times'))
       index = i // 更新index的值
       let fn = middleware[i] //middleware中的函数,从第i个开始
       if (i === middleware.length) fn = next //如果i走到最后一个的后面,就让fn为next,此时fn为undefined
       if (!fn) return Promise.resolve()// 那么这时候就直接resolve
       try {
        // 把下一个中间件作为当前中间件的next传入
         return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
       } catch (err) {
         return Promise.reject(err)
       }
     }
      }
    }
转载自:https://segmentfault.com/a/1190000041298381
评论
请登录