本文是我閱讀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)去,把所有不需要用戶手動判斷的事情都做了崇猫。
來看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)頁上訪問
而webStorm終端差不多同時(shí)也打印出信息,其實(shí)是先打印log后顯示Helloooo World.
接著把logger方法體里的next(); 刪掉屋群,啟動程序后闸婴,還是訪問一樣的url,會發(fā)現(xiàn)webstorm終端會輸出時(shí)間信息,但是網(wǎng)頁不再打印Helloooo World. 而是not found, 說明main中間件函數(shù)沒有被執(zhí)行芍躏。
這樣能體會到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));