2018-07-31 Koa Web框架学习

本文是我阅读http://www.ruanyifeng.com/blog/2017/08/koa.html加上自己个人理解。
Koa 是javascript的web框架。

一, 基础理解

1. 基础版方法架设HTTP服务和利用Koa框架架设HTTP服务的区别:
基础版方法:

这个程序运行后访问 http://localhost:8000/ ,页面显示hello world

const http = require('http');
http.createServer((req, res) => {
    res.writeHead(200, {"content-type": "text/html"});
    res.end('hello world\n');
}).listen(8000);
用Koa框架:

这个程序运行后访问 http://localhost:3000/ ,页面显示Not Found,表示没有发现任何内容。这是因为我们并没有告诉 Koa 应该显示什么内容。- 阮一峰

const Koa = require('koa');
const app = new Koa();

app.listen(3000);

要把这段程序做成和上面一样,只需补上一句中间件调用

const Koa = require('koa');
const app = new Koa();

app.use(ctx => { ctx.body = 'hello world' });//补上这句中间件调用
app.listen(3000);
2。Koa的实现原理

其实Koa搭建HTTP服务的实现原理和最基础的实现方式是一样的,万变不离其宗,只是把一些看起来可以由程序自动判断处理的东西封起来,由此达到使用上的简便。
来看上面两段代码的对比图,除了设置head,右边的koa不用做之外其他的动作看起来都做了,那是因为app.listen()这个方法进去,把所有不需要用户手动判断的事情都做了。

2018-07-31 Koa Web框架学习_第1张图片
image.png

来看Koa的源码 https://github.com/koajs/koa.git,看Application.js,找到http.createServer() 因为这个是javascript用于创建HTTP服务的核心。找到它就可以对应上原始方法的
http.createServer((req,res)=>{...}).listen(8000);

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

也就是说this.callback() 对应到基础版的
(req, res)=>{
res.writeHead(200, {"content-type": "text/html"}); //写head
res.end('hello world\n'); //返回信息
}
所以,this.callback()就是真正做事情的回调函数了。
再看callback()源码:

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

第一句就是聚合所有的中间件函数,(this.middleware是由app.use()方法把所有的中间件函数收集起来),第二句先不看,第四句开始基本就跟基础方法很像了。const ctx = this.createContext(req, res); 把req,和res 封装到ctx, 这就是Koa的重要特色。最后看this.handleRequest(ctx, fn);

  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);
  }

fnMiddleware就是所有的中间件函数,最后一句执行所有中间件函数,然后捕获handleResponse,最后处理异常。 来看const handleResponse = () => respond(ctx); 看respond(),它用于判断返回,看最后一句res.end(body);刚好匹配基础版的res.end('hello world\n');

/**
 * Response helper.
 */

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

  const res = ctx.res;
  if (!ctx.writable) return;

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

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

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

  // status body
  if (null == body) {
    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 ('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);
}

源码看到这里知道了Koa执行的大致步骤了,但是还没看到具体中间件是以怎样的方式执行,还有接下来的问题3。

3. Koa 用它的“use(中间件函数)” 来加载中间件函数,为什么说“每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是next函数。只要调用next函数,就可以把执行权转交给下一个中间件。”

next 非必须,但是没有的话中间件栈无法串起来,可能会出现中断。

这个问题要看callback()里的const fn = compose(this.middleware);
由源码(https://github.com/koajs/compose.git,打开index.js)知道const compose = require('koa-compose'); 所以看compose源码在做什么:

function compose (middleware) {
  //...

  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)
      }
    }
  }
}

看return Promise.resolve(fn(context, function next () { 这行,就知道每个fn的调用都要传2个参数(context, next), 这就决定了中间件函数参数的写法,如果某个中间件的参数漏了 next() , 后面的中间件是不会执行的。compose利用这个方法把所有的中间件串起来。于是看起来是异步调用的方法变成同步调用,比如拿阮一峰koa教程的一个例子来看:

下面是可以正常工作的2个route, logger执行完后会执行main, 因为logger里有next():

const Koa = require('koa');
const app = new Koa();

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}

const main = ctx => {
  ctx.response.body = 'Helloooo World';
};

app.use(logger);
app.use(main);
app.listen(3000);

WebStorm里启动程序后,在网页上访问


2018-07-31 Koa Web框架学习_第2张图片
image.png

而webStorm终端差不多同时也打印出信息,其实是先打印log后显示Helloooo World.


2018-07-31 Koa Web框架学习_第3张图片
image.png

接着把logger方法体里的next(); 删掉,启动程序后,还是访问一样的url,会发现webstorm终端会输出时间信息,但是网页不再打印Helloooo World. 而是not found, 说明main中间件函数没有被执行。
2018-07-31 Koa Web框架学习_第4张图片
image.png

2018-07-31 Koa Web框架学习_第5张图片
image.png

这样能体会到next()在javascript中的作用了。

二,我们可以利用Koa来做什么事情

1. 中间件函数

1.1之所以叫中间件(middleware),是因为它处在 HTTP Request 和 HTTP Response 中间,用来实现某种中间功能。koa.use()用来加载中间件。

其实中间件不是koa特有,只是这个名字是它特有的。中间件函数跟我们的普通函数没什么区别,就是一个函数块,想象下买泡面付钱的时候你要做的几个动作:选中小卖部->选中泡面->打开支付宝扫码付钱->带泡面走人。你可以写4个中间件函数来完成这整个买泡面的动作。

1.2 多个中间件一起调用,如果确保每个中间件都有调用next(), 那么这些中间件就会形成一个栈结构,以"先进后出"(first-in-last-out)的顺序执行。如下面有3个中间件 one, two, three,最后用app.use() 顺序加载

const Koa = require('koa');
const app = new Koa();

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);
app.listen(3000);

执行后结果为:
·>> one
·>> two
·>> three
·<< three
·<< two
·<< one
1.3 读到这里,这几个中间件是怎么被连起来的呢?
来看下koa.use() 源码https://github.com/koajs/koa/blob/master/lib/application.js, use()方法就做了件正经事,把所有的中间件push入this.middleware这个数组里,然后,当callback()被调用的时候,所有的middleware被合成成一个fn:

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

//... other code

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

1.4 接着说中间件的合成,koa-compose模块,它可以将多个中间件合成为一个
所以上面的例子,三个app.use()可以用一个compse()替代。

const Koa = require('koa');
const app = new Koa();
const compose = require('koa-compose');

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);

const middlewares = compose([one, two, three]);
app.use(middlewares);

app.listen(3000);

1.5 异步中间件
前面的例子都是同步的中间件,如果中间件有异步操作,那么中间件必须要写成async 函数。
比如下面的fs.readFile()是异步操作,因此中间件main要写成async函数。

const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async function (ctx, next) {
  ctx.response.type = 'html';
  ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};

app.use(main);
app.listen(3000);
2. 路由,

简单理解就是我们可以定制一个URL,当用户访问这个URL,后台开始做一些业务处理并返回信息给用户。
Koa原生的方法是利用ctx.request.path先判断用户访问的URL,然后再根据URL走特定的代码。这样的话代码里就有很多的if...else...

const main = ctx => {
  if (ctx.request.path !== '/') {
    ctx.response.type = 'html';
    ctx.response.body = 'Index Page';
  } else {
    ctx.response.body = 'Hello World';
  }
};

所以就有了Koa-route模块, 这个模块将URL和封装成中间件的业务代码块组装在一起,看起来就很简洁也容易理解。
注意下,下面的中间件函数没有next参数,因为这里每个中间件函数只为一个URL提供处理,中间件之间没有前后调用的关系,因此不需要next

const route = require('koa-route');

const about = ctx => {
  ctx.response.type = 'html';
  ctx.response.body = 'Index Page';
};
const main = ctx => {
  ctx.response.body = 'Hello World';
};

app.use(route.get('/', main));
app.use(route.get('/about', about));

你可能感兴趣的:(2018-07-31 Koa Web框架学习)