2018-07-31 Koa Web框架學(xué)習(xí)

本文是我閱讀http://www.ruanyifeng.com/blog/2017/08/koa.html加上自己個人理解牡彻。
Koa 是javascript的web框架痰洒。

一友存, 基礎(chǔ)理解

1. 基礎(chǔ)版方法架設(shè)HTTP服務(wù)和利用Koa框架架設(shè)HTTP服務(wù)的區(qū)別:
基礎(chǔ)版方法:

這個程序運(yùn)行后訪問 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框架:

這個程序運(yùn)行后訪問 http://localhost:3000/ ,頁面顯示Not Found,表示沒有發(fā)現(xiàn)任何內(nèi)容子檀。這是因?yàn)槲覀儾]有告訴 Koa 應(yīng)該顯示什么內(nèi)容镊掖。- 阮一峰

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

app.listen(3000);

要把這段程序做成和上面一樣乃戈,只需補(bǔ)上一句中間件調(diào)用

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

app.use(ctx => { ctx.body = 'hello world' });//補(bǔ)上這句中間件調(diào)用
app.listen(3000);
2亩进。Koa的實(shí)現(xiàn)原理

其實(shí)Koa搭建HTTP服務(wù)的實(shí)現(xiàn)原理和最基礎(chǔ)的實(shí)現(xiàn)方式是一樣的症虑,萬變不離其宗,只是把一些看起來可以由程序自動判斷處理的東西封起來归薛,由此達(dá)到使用上的簡便谍憔。
來看上面兩段代碼的對比圖,除了設(shè)置head苟翻,右邊的koa不用做之外其他的動作看起來都做了韵卤,那是因?yàn)閍pp.listen()這個方法進(jìn)去,把所有不需要用戶手動判斷的事情都做了崇猫。

image.png

來看Koa的源碼https://github.com/koajs/koa.git沈条,看Application.js,找到http.createServer() 因?yàn)檫@個是javascript用于創(chuàng)建HTTP服務(wù)的核心。找到它就可以對應(yīng)上原始方法的
http.createServer((req,res)=>{...}).listen(8000);

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

也就是說this.callback() 對應(yīng)到基礎(chǔ)版的
(req, res)=>{
res.writeHead(200诅炉, {"content-type": "text/html"}); //寫head
res.end('hello world\n'); //返回信息
}
所以蜡歹,this.callback()就是真正做事情的回調(diào)函數(shù)了。
再看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;
  }

第一句就是聚合所有的中間件函數(shù)涕烧,(this.middleware是由app.use()方法把所有的中間件函數(shù)收集起來)月而,第二句先不看,第四句開始基本就跟基礎(chǔ)方法很像了议纯。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就是所有的中間件函數(shù),最后一句執(zhí)行所有中間件函數(shù)瞻凤,然后捕獲handleResponse,最后處理異常憨攒。 來看const handleResponse = () => respond(ctx); 看respond(),它用于判斷返回,看最后一句res.end(body);剛好匹配基礎(chǔ)版的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執(zhí)行的大致步驟了阀参,但是還沒看到具體中間件是以怎樣的方式執(zhí)行肝集,還有接下來的問題3。

3. Koa 用它的“use(中間件函數(shù))” 來加載中間件函數(shù)蛛壳,為什么說“每個中間件默認(rèn)接受兩個參數(shù)杏瞻,第一個參數(shù)是 Context 對象,第二個參數(shù)是next函數(shù)衙荐。只要調(diào)用next函數(shù)捞挥,就可以把執(zhí)行權(quán)轉(zhuǎn)交給下一個中間件∮且鳎”

next 非必須树肃,但是沒有的話中間件棧無法串起來,可能會出現(xiàn)中斷瀑罗。

這個問題要看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的調(diào)用都要傳2個參數(shù)(context, next), 這就決定了中間件函數(shù)參數(shù)的寫法劣像,如果某個中間件的參數(shù)漏了 next() , 后面的中間件是不會執(zhí)行的乡话。compose利用這個方法把所有的中間件串起來。于是看起來是異步調(diào)用的方法變成同步調(diào)用耳奕,比如拿阮一峰koa教程的一個例子來看:

下面是可以正常工作的2個route, logger執(zhí)行完后會執(zhí)行main, 因?yàn)閘ogger里有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里啟動程序后绑青,在網(wǎng)頁上訪問


image.png

而webStorm終端差不多同時(shí)也打印出信息,其實(shí)是先打印log后顯示Helloooo World.


image.png

接著把logger方法體里的next(); 刪掉屋群,啟動程序后闸婴,還是訪問一樣的url,會發(fā)現(xiàn)webstorm終端會輸出時(shí)間信息,但是網(wǎng)頁不再打印Helloooo World. 而是not found, 說明main中間件函數(shù)沒有被執(zhí)行芍躏。
image.png

image.png

這樣能體會到next()在javascript中的作用了邪乍。

二,我們可以利用Koa來做什么事情

1. 中間件函數(shù)

1.1之所以叫中間件(middleware),是因?yàn)樗幵?HTTP Request 和 HTTP Response 中間对竣,用來實(shí)現(xiàn)某種中間功能庇楞。koa.use()用來加載中間件。

其實(shí)中間件不是koa特有否纬,只是這個名字是它特有的吕晌。中間件函數(shù)跟我們的普通函數(shù)沒什么區(qū)別,就是一個函數(shù)塊临燃,想象下買泡面付錢的時(shí)候你要做的幾個動作:選中小賣部->選中泡面->打開支付寶掃碼付錢->帶泡面走人睛驳。你可以寫4個中間件函數(shù)來完成這整個買泡面的動作。

1.2 多個中間件一起調(diào)用膜廊,如果確保每個中間件都有調(diào)用next(), 那么這些中間件就會形成一個棧結(jié)構(gòu)柏靶,以"先進(jìn)后出"(first-in-last-out)的順序執(zhí)行。如下面有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);

執(zhí)行后結(jié)果為:
·>> one
·>> two
·>> three
·<< three
·<< two
·<< one
1.3 讀到這里,這幾個中間件是怎么被連起來的呢痘昌?
來看下koa.use() 源碼https://github.com/koajs/koa/blob/master/lib/application.js, use()方法就做了件正經(jīng)事钥勋,把所有的中間件push入this.middleware這個數(shù)組里,然后辆苔,當(dāng)callback()被調(diào)用的時(shí)候算灸,所有的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 函數(shù)赊瞬。
比如下面的fs.readFile()是異步操作先煎,因此中間件main要寫成async函數(shù)。

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薯蝎,當(dāng)用戶訪問這個URL,后臺開始做一些業(yè)務(wù)處理并返回信息給用戶谤绳。
Koa原生的方法是利用ctx.request.path先判斷用戶訪問的URL占锯,然后再根據(jù)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和封裝成中間件的業(yè)務(wù)代碼塊組裝在一起消略,看起來就很簡潔也容易理解。
注意下瞎抛,下面的中間件函數(shù)沒有next參數(shù)艺演,因?yàn)檫@里每個中間件函數(shù)只為一個URL提供處理,中間件之間沒有前后調(diào)用的關(guān)系婿失,因此不需要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));
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钞艇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子豪硅,更是在濱河造成了極大的恐慌哩照,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懒浮,死亡現(xiàn)場離奇詭異飘弧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)砚著,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門次伶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人稽穆,你說我怎么就攤上這事冠王。” “怎么了舌镶?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵柱彻,是天一觀的道長。 經(jīng)常有香客問我餐胀,道長哟楷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任否灾,我火速辦了婚禮卖擅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己惩阶,他們只是感情好挎狸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著琳猫,像睡著了一般伟叛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脐嫂,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天统刮,我揣著相機(jī)與錄音,去河邊找鬼账千。 笑死侥蒙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的匀奏。 我是一名探鬼主播鞭衩,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼娃善!你這毒婦竟也來了论衍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤聚磺,失蹤者是張志新(化名)和其女友劉穎坯台,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘫寝,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜒蕾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焕阿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咪啡。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖暮屡,靈堂內(nèi)的尸體忽然破棺而出撤摸,到底是詐尸還是另有隱情,我是刑警寧澤褒纲,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布准夷,位于F島的核電站,受9級特大地震影響外厂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜代承,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一汁蝶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦掖棉、人聲如沸墓律。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耻讽。三九已至,卻和暖如春帕棉,著一層夾襖步出監(jiān)牢的瞬間针肥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工香伴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慰枕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓即纲,卻偏偏與公主長得像具帮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子低斋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 1.簡書 koa是由Express原班人馬打造蜂厅,致力于成為一個更小、更富有表現(xiàn)力膊畴、更健壯的Web框架掘猿。使用koa編...
    不去解釋閱讀 2,655評論 0 11
  • 使用體驗(yàn) koa express 注意:本文全部采用es6語法編寫,如果環(huán)境不支持請自行升級node或者使用bab...
    shanyy閱讀 3,433評論 0 10
  • 一巴比、基本用法 1.1 架設(shè) HTTP 服務(wù) // demos/01.jsconst Koa = require('...
    majun00閱讀 1,354評論 0 5
  • Koa 必須使用 7.6 以上的版本术奖。如果你的版本低于這個要求,就要先升級 Node轻绞。 基本用法 Koa 提供一個...
    Gukson666閱讀 2,445評論 0 1
  • 初始化 執(zhí)行koa()的時(shí)候初始化了一些很有用的東西采记,包括初始化一個空的中間件集合,基于Request政勃,Respo...
    _八神光_閱讀 768評論 1 3