当前位置:澳门贵宾厅 > 澳门贵宾厅 > 看一下使用 koa2 www.vip8888.com开启一个http 服务,response对象的原型
看一下使用 koa2 www.vip8888.com开启一个http 服务,response对象的原型
2020-04-29

时间: 2019-12-16阅读: 82标签: koa什么是 koa2

源码结构

Koa的源码中主要为lib目录下的application.js, context.js, request.js与response.js文件

.
├── AUTHORS
├── CODE_OF_CONDUCT.md
├── History.md
├── LICENSE
├── Makefile
├── Readme.md
├── benchmarks
├── docs
├── lib
│   ├── application.js
│   ├── context.js
│   ├── request.js
│   └── response.js
├── package.json
└── test

application.js: 框架入口,导出Application类,即使用时倒入的Koa类。
context.js: context对象的原型,代理request与response对象。
request.js: request对象的原型,提供请求相关的数据与操作。
response,js: response对象的原型,提供响应相关的数据与操作。

Nodejs官方api支持的都是callback形式的异步编程模型。问题:callback嵌套问题

Application

koa2 是由 Express原班人马打造的,是现在比较流行的基于Node.js平台的web开发框架,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量,而且扩展性很强。使用koa编写web应用,可以免除重复繁琐的回调函数。

Application@listen

Koa通过app.listen(port)函数在某个端口启动服务。
listen函数通过http模块开启服务:

/**
 * shorthand for:
 *  http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */
listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

实际上app.listen()为http.createServer(app.callback()).listen(...)的速记写法。http.createServer()用于创建Web服务器,接受一个请求监听函数,并在得到请求时执行。app.callback()用于处理请求,合并中间件与创建请求上下文对象等等。

koa2 的优点

Application@use

Koa通过app.use()添加中间件,并将中间件存储在app.middleware中。在执行app.callback()时会将app,middleware中的中间件合并为一个函数。

/**
 * Use the given middleware 'fn',
 *
 * Old-style middleware will be converted.
 *
 * @param {Function} fn
 * @return {Application} self
 * @api public
 */
use(fn) {
  if(typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if(isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' + 
              'See the documentation for examples of how to convert old middleware ' + 
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

Koa1.x版本使用Generator Function的方式写中间件,而Koa2改用ES6 async/await。所以在use()函数中会判断是否为旧风格的中间件写法,并对旧风格写法得中间件进行转换(使用koa-convert进行转换)。
可以注意到这里use()函数返回了this,这使得在添加中间件的时候能链式调用。

app
  .use(function(ctx, next) {
    //  do some thing
  })
  .use(function(ctx, next) {
    //  do some thing
  })
  // ...

优点这个东西,我直接说它多好,你可能又不开心,但是我们可以对比哦!这里我只说它对比原生的 Node.js开启 http 服务 带来了哪些优点!

Application@callback

app.callback()负责合并中间件,创建请求上下文对象以及返回请求处理函数等。

/**
 * Return a request handler callback
 * for node's native http server
 *
 * @return {Function}
 * @api public
 */

callback() {
  const fn = compose(this.middleware);

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

  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handlerResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse.catch(onerror));
  };

  return handleRequest;
}

通过compose函数(koa-compose)合并app.middleware中的所有中间件。查看关于koa-compose的分析。
app.callback()函数返回一个请求处理函数handleRequest。该函数即为http.createServer接收的请求处理函数,在得到请求时执行。

先看一下原生 Node.js 我开启一个 http 服务

handleRequest

handleRequest函数首先将响应状态设置为404,接着通过app.createContext()创建请求的上下文对象。
onFinished(res, onerror)通过第三方库on-finished监听http response,当请求结束时执行回调,这里传入的回调是context.onerror(err),即当错误发生时才执行。
最后返回fn(ctx).then(handleResponse).catch(onerror),即将所有中间件执行(传入请求上下文对象ctx),之后执行响应处理函数(app.respond(ctx)),当抛出异常时同样使用cintext,onerror(err)处理。

consthttp=require('http');((req,res)={res.writeHead(200);res.end('hi koala');}).listen(3000);
createContext

app.createContext()用来创建请求上下文对象,并代理Koa的request和response模块。

/**
 * Initialize a new context
 *
 * @api private
 */

createContext(req, res) {
  const context = Object.create(this.context);
  const request = context.request = Object.create(this.response);
  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.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  context.cookies = new Cookies(req, res, {
    keys: this.keys,
    secure: request.secure
  });
  request.ip = request.ips[0] || req.socket.remoteAddress || '';
  context.accept = request.accept = accepts(req); 
  context.state = {};
  return context;
}

这里对请求都对应在上下文对象中添加对应的cookies。

看一下使用 koa2 开启一个http 服务

respond

app.respond(ctx)函数,这就是app.createContext()函数中的handleResponse,在所有中间件执行完之后执行。
在koa中可以通过设置ctx.respond = false来跳过这个函数,但不推荐这样子。另外,当上下文对象不可写时也会退出该函数:

if (false === ctx.respond) return;
// ...
if (!ctx.writable) return;

当返回的状态码表示没有响应主体时,将响应主体置空:

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

当请求方法为HEAD时,判断响应头是否发送以及响应主体是否为JSON格式,若满足则设置响应Content-Length:

if('HEAD' == ctx.method) {
  if(!res.headersSent && isJSON(body)) {
    ctx.length = Buffer.byteLength(JSON.stringify(body));
  }
  return res.end();
}

当返回的状态码表示有响应主体,但响应主体为空时,将响应主体设置为响应信息或状态码。并当响应头未发送时设置Content-Type与Content-Length:

if (null == body) {
  body = ctx.message || String(code);
  if (!res.headersSent) {
    ctx.type = 'text';
    ctx.length = Buffer.byteLength(body);
  }
  return res.end(body);
}

最后,对不同的响应主体进行处理:

// response
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) 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);
constKoa=require('koa');constapp=newKoa();const{createReadStream}=require('fs');app.use(async(ctx,next)={if(ctx.path==='/favicon.ico'){ctx.body=createReadStream('./avicon.ico')}else{awaitnext();}});app.use(ctx={ctx.body='hi koala';})app.listen(3000);

Compose

在application.js中,callback()函数通过koa-compose组合所有的中间件,组合成单个函数。koa-compose的实现很简单:

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, function next() {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  }
}

首先判断传入的中间件参数是否为数组,并检查且该数组的元素是否为函数,然后返回了一个将中间件组合起来的函数。
重点关注返回的函数中的dispatch(i)函数,这个函数将获取第一个中间件,并在返回的Promise中执行。当中间件await next()时执行下一个中间件,即:dispatch(i+1)。
执行流程可以简单看做:

async function middleware1() {
  console.log('middleware1 begin');
  await middleware2();
  console.log('middleware1 end');
}

async function middleware2() {
  console.log('middleware2 begin');
  await middleware3();
  console.log('middleware2 end');
}

async function middleware3() {
  console.log('middleware3 begin');
  console.log('middleware3 end');
}

middleware1();
//  执行结果
middleware1 begin
middleware2 begin
middleware3 begin
middleware3 end
middleware2 end
middleware1 end

compose()函数通过Promise将这个过程串联起来,从而返回单个中间件函数。

我在 koa2 中添加了一个判断 /favicon.ico 的实现 通过以上两段代码,会发现下面几个优点

Context

Koa中的Context模块封装了request与response,代理了这两个对象的方法与属性。其中使用了Tj写的node-delegates库,用于代理context.request与context.response上的方法与属性。

/**
 * Response delegation
 */

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

context除了代理这两个模块外,还包含一个请求异常时的错误处理函数。在application.js的callback()众使用了这个函数。

const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);

传统的 http 服务想使用模块化不是很方便,我们不能在一个服务里面判断所有的请求和一些内容。而 koa2 对模块化提供了更好的帮助

Context@onerror

context.onerror(err)首先对传入的err变量进行判断,当err为空时退出函数,或者当err不为空且不为Error类型时抛出异常。

if (null == err) return;
if (!(err instanceof Error))  err = new Error('non-error thrown: ${err}');

接着触发app自身的error事件,将错误抛给app。
在此之前,设置headerSent变量表示响应头是否发送,若响应头已发送,或者不可写(即无法在响应中添加错误信息等),则退出该函数。

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

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

// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
  return;
}

因为发生了错误,所以必须将之前中间设置的响应头信息清空。
这里使用了Node提供的http.ServerResponse类上的getHeaderNames()与removeHeader()方法。但getHeaderNames()这个函数在Node 7.7版本时加入的,所以当没有提供该方法时需要使用_header来清空响应头。详情可见:Node.js#10805。

//  first unset all headers
if (typeof res.getHeaderNames === 'function') {
  res.getHeaderNames().forEach(name => res.removHeader(name));
} else {
  res._headers = {};  //  Node < 7.7
}

清空之前中间件设置的响应头之后,将响应头设置为err.headers,并设置Content-Type与状态码。
当错误码为ENOENT时,意味着找不到该资源,将状态码设置为404;当没有状态码或状态码错误时默认设置为500。

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

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

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

//  default to 500
if('number' != typeof err.status || !statuses[err.status]) err.status = 500;

koa2 把 req,res 封装到了context中,更简洁而且方便记忆

Request

Request模块封装了请求相关的属性及方法。通过application中的createContext()方法,代理对应的request对象:

const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;

中间件机制,采用洋葱模型,洋葱模型流程记住一点(洋葱是先从皮到心,然后从心到皮),通过洋葱模型把代码流程化,让流水线更加清楚,如果不使用中间件,在createServer一条线判断所有逻辑确实不好。

Response

Response模块封装了响应相关的属性以及方法。与request相同,通过createContext()方法代理对应的response对象:

const response = context.response = Object.create(this.response);
// ...
context.res = request.res = response.res = res;
// ...
response.request = request;

看不到的优点也很多,error 错误处理,res的封装处理等。

自己实现一个koa2在实现的过程中会我看看可以学到哪些知识listen 函数简单封装

koa2 直接使用的时候,我们通过const app = new Koa();,koa应该是一个类,而且可以直接调用listen函数,并且没有暴漏出http服务的创建,说明在listen函数中可能创建了服务。到此简单代码实现应该是这样的:

classKkb{constructor(){this.middlewares=[];}listen(...args){(async(req,res)={// 给用户返回信息 this.callback(req,res); res.writeHead(200); res.statusCode = 200; res.end('hello koala') }).listen(...args) }}module.exports = Kkb;

实现 context 的封装

实现了简单 listen 后,会发现回调函数返回的还是 req 和 res ,要是将二者封装到 context 一次返回就更好了!我们继续

constctx=this.createContext(req,res);

看一下 createContext 的具体实现

constrequest=require('./lib/request');constresponse=require('./lib/response');constcontext=require('./lib/context');createContext(req,res){// 创建一个新对象,继承导入的context const ctx = Object.create(context); ctx.request = Object.create(request); ctx.response = Object.create(response); // 这里的两等于判断,让使用者既可以直接使用ctx,也可以使用原生的内容 ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; }

context.js

module.exports={geturl(){returnthis.request.url;},getbody(){returnthis.response.body;},setbody(val){this.response.body=val;}}

request.js

module.exports={geturl(){returnthis.req.url;}}

这里在写 context.js 时候,用到了set 与 get 函数,get 语句作为函数绑定在对象的属性上,当访问该属性时调用该函数。set 语法可以将一个函数绑定在当前对象的指定属性上,当那个属性被赋值时,你所绑定的函数就会被调用。

实现洋葱模型compose 另一个应用场景

说洋葱模型之前先看一个函数式编程内容:compose 函数前端用过 redux 的同学肯定都很熟悉。redux 通过compose来处理 中间件 。原理是 借助数组的 reduce 对数组的参数进行迭代

// redux 中的 compose 函数export default function compose(...funcs) { if (funcs.length === 0) { return arg = arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) = (...args) = a(b(...args)))}

洋葱模型实现

再看文章开头 koa2 创建 http 服务函数,会发现多次调用 use 函数,其实这就是洋葱模型的应用。

洋葱是由很多层组成的,你可以把每个中间件看作洋葱里的一层,根据app.use的调用顺序中间件由外层到里层组成了整个洋葱,整个中间件执行过程相当于由外到内再到外地穿透整个洋葱

引用一张著名的洋葱模型图:

每次执行 use 函数,我们实际是往一个函数数组中添加了一个函数,然后再次通过一个 compose 函数,处理添加进来函数的执行顺序,也就是这个 compose 函数实现了洋葱模型机制。

具体代码实现如下:

// 其中包含一个递归 compose(middlewares){ return async function(ctx){// 传入上下文 return dispatch(0); function dispatch(i){ let fn = middlewares[i]; if(!fn){ return Promise.resolve(); } return Promise.resolve( fn(ctx,function next(){ return dispatch(i+1) }) ) } } }

首先执行一次 dispatch(0) 也就是默认返回第一个 app.use 传入的函数 使用 Promise 函数封装返回,其中第一个参数是我们常用的 ctx,

第二个参数就是 next 参数,next 每次执行之后都会等于下一个中间件函数,如果下一个中间件函数不为真则返回一个成功的 Promise。因此我们每次调用 next() 就是在执行下一个中间件函数。

来试试我们自己实现的koa2

使用一下我们自己的 koa2 吧,用它做一道常考洋葱模型面试题,我想文章如果懂了,输出结果应该不会错了,自己试一下!

constKKB=require('./kkb');constapp=newKKB();app.use(async(ctx,next)={ctx.body='1';awaitnext();ctx.body+='3';})app.use(async(ctx,next)={ctx.body+='4';awaitdelay();awaitnext();ctx.body+='5';})app.use(async(ctx,next)={ctx.body+='6'})asyncfunctiondelay(){returnnewPromise((reslove,reject)={setTimeout(()={reslove();},1000);})}app.listen(3000);

解题思路:还是洋葱思想,洋葱是先从皮到心,然后从心到皮

答案: 1 4 6 5 3

补充与说明

本文目的主要是让大家学到一个koa2的基本流程,简单实现koa2,再去读源码有一个清晰的思路。实际源码中还有很多优秀的值得我们学习的点,接下来再列举一个我觉得它很优秀的点——错误处理,大家可在原有基础上继续实现,也可以去读源码继续看!加油加油

源码中 koa 继承自Emiiter,为了处理可能在任意时间抛出的异常所以订阅了 error 事件。error 处理有两个层面,一个是 app 层面全局的(主要负责 log),另一个是一次响应过程中的 error 处理(主要决定响应的结果),koa 有一个默认 app-level 的 onerror 事件,用来输出错误日志。

// 在调用洋葱模型函数后面,koa 会挂载一个默认的错误处理【运行时确定异常处理】 if (!this.listenerCount("error")) this.on("error", this.onerror);

onerror(err){if(!(errinstanceofError))thrownewTypeError(util.format("non-error thrown: %j",err));if(404==err.status||err.expose)return;if(this.silent)return;constmsg=err.stack||err.toString();console.error();console.error(msg.replace(/^/gm," "));console.error();}

通过 Emiiter 实现了错误打印,Emiiter 采用了发布订阅的设计模式,如果有对 Emiiter 有不太清楚的小伙伴可以看我这篇文章[源码解读]一文彻底搞懂Events模块。

总结

本文注重思想,精简版本,代码与实现都很简单。封装,递归,设计模式都说了一丢丢,希望也能对你有一丢丢的提升和让你去看一下koa2源码的想法,下篇文章见。

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

原文:_Kqy78AHHw