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()这个方法进去,把所有不需要用户手动判断的事情都做了。

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里启动程序后,在网页上访问


image.png

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


image.png

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

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 = '<a href="/">Index Page</a>';
  } 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 = '<a href="/">Index Page</a>';
};
const main = ctx => {
  ctx.response.body = 'Hello World';
};

app.use(route.get('/', main));
app.use(route.get('/about', about));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 1.简书 koa是由Express原班人马打造,致力于成为一个更小、更富有表现力、更健壮的Web框架。使用koa编...
    不去解释阅读 2,650评论 0 11
  • 使用体验 koa express 注意:本文全部采用es6语法编写,如果环境不支持请自行升级node或者使用bab...
    shanyy阅读 3,414评论 0 10
  • 一、基本用法 1.1 架设 HTTP 服务 // demos/01.jsconst Koa = require('...
    majun00阅读 1,345评论 0 5
  • Koa 必须使用 7.6 以上的版本。如果你的版本低于这个要求,就要先升级 Node。 基本用法 Koa 提供一个...
    Gukson666阅读 2,442评论 0 1
  • 初始化 执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Respo...
    _八神光_阅读 757评论 1 3