本文是我阅读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()这个方法进去,把所有不需要用户手动判断的事情都做了。
来看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里启动程序后,在网页上访问
而webStorm终端差不多同时也打印出信息,其实是先打印log后显示Helloooo World.
接着把logger方法体里的next(); 删掉,启动程序后,还是访问一样的url,会发现webstorm终端会输出时间信息,但是网页不再打印Helloooo World. 而是not found, 说明main中间件函数没有被执行。
这样能体会到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));