使用node原生http模块创建server
const http = require('http');
function callback(req, res) {};
const server = http.createServer(callback);
// 使用事件监听执行callback
server.on('request', callback);
server.listen(3000);
使用node中的原生http模块很容易创建server,仅仅需要调用createServer方法,指定指定当接收到客户端请求时所需执行的callback即可。在callback中,使用两个参数,一个是http.IncomingMessage对象,此处代表一个客户端请求,另一个是http.ServerResponse对象,代表一个服务器端响应对象。如果createServer中不传入callback,也可以使用server监听request事件执行callback。然后调用listen方法指定server需要监听的端口、地址等。
server.listen(port, [host], [backlog], [callback]);
在listen方法中,可使用4个参数,其中后三个参数是可选参数。port参数值用于指定需要监听的端口号,参数值为0时将为HTTP服务器随机分配一个端口号,HTTP服务器将监听来自于这个随机端口号的客户端连接。host参数用于指定需要监听的地址,如果省略该参数,服务器将监听来自于任何IPv4地址的客户端连接。backlog参数值为一个整数值,用于指定位于等待队列中的客户端连接的最大数量,一旦超越这个长度,HTTP服务器将开始拒绝来自于新的客户端的连接,该参数的默认参数值为511。callback参数来指定listening事件触发时调用的回调函数,该回调函数中不使用任何参数。如果不在listen方法中传入callback参数,也可以使用事件监听。
server.on('listening', () => {});
使用koa创建server
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('enter middleware');
await next();
console.log('out middleware');
});
app.listen(3000);
使用koa创建server与使用node原生http模块创建server差别很大,但我们知道koa是base node.js的web框架。因此,其底层使用的node.js的技术,只是在此基础上进行了进一步的包装,从而可以使用各种独立功能的中间件操作上下文、操作req和res对象,实现请求解析,响应返回。
koa源码
koa源码主要逻辑包括两个部分,一部分是koa本身的逻辑,主要进行服务的创建、ctx、req、res这三个对象的管理和操作。另一部分就是其特有的洋葱模型的中间件流程控制,主要是koa-compose(koa-compose源码解析)中间件实现的该功能。
koa的源码都在lib目录下的四个文件。application.js是koa的入口文件,主要目标是创建服务,context.js是ctx上下文对象文件,request.js是对req对象进行处理的文件,response.js是对res进行处理的文件。下面我们从两个方面对源码进行阅读,首先是服务创建阶段即初始化阶段,其次是请求处理阶段。
初始化阶段
初始化阶段包括koa初始化,该部分初始化主要是实例话koa对实例化的app进行属性和方法的挂载。server初始化,这部分的初始化主要是创建server并指定请求到底是需要执行的操作。在代码表现就是:
// koa初始化
const app = new Koa();
// server初始化
app.listen(3000, () => {
console.log('server running: http://localhost:3000');
});
koa初始化
现在我们把不必要的代码去掉看看koa初始化做了哪些事情。
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
listen(...args) {}
toJSON() {}
inspect() {}
use(fn)() {}
callback() {}
handleRequest(ctx, fnMiddleware) {}
createContext(req, res) {}
onerror(err) {}
};
koa的初始化,主要是为app挂载各种属性和方法,如上所示,在constructor主要是属性挂载,其中包括我们经常使用的context上下文对象,request对象,response对象,middleware中间数组,env环境参数、proxy代理操作等。并在prototype上挂载了许多方法,其中handleRequest、createContext、onerror这三个方法是私有方法,主要是提供给callback方法使用的,目的分别是handleRequest通过compose的中间件对request进行处理,createContext是创建context上下文对象,并在上下文挂载state对象提供给视图进行数据传递使用,onerror主要是进行全局对错误。其他5个个方法分别作用是:listen初始化通过node的原生http模块createServer,toJSON和inspect主要是对app上的subdomainOffset, proxy, env三个属性通过only进行提取。use函数是对中间件函数进行管理,主要是push进middleware数组中,并return this进行链式调用,callback主要是return handleRequest指定了当接收到客户端请求时所需执行的操作。
server初始化
server初始化主要是createServer并指定server.on('request', callback)中的callback。这里面主要就是执行了实例化后的app的listen函数。下面我们看一下这个listen函数的具体内容。
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
看到listen源码其实就是跟我们使用node原生的http模块创建server一样了。createServer并指定监听的地址和端口号。其中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;
}
callback使用闭包return handleRequest进行请求到底的处理,如何进行请求处理我们在请求处理阶段进行详细介绍。其中compose(this.middleware)与this.on('error', this.onerror)分别是对中间价进行compose流程控制和注册全局error事件处理函数。
请求处理阶段
当一个请求过来时,它会进入到callback回调函数返回的handleRequest函数中进行处理。
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
handleRequest主要执行了两部分操作,一部分是调用私有方法createContext创建上下文对象,一部分是调用私有方法handleRequest进行请求处理。
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
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);
}
createContext主要是创建上下文对象也就是中间件入参中的ctx对象,并为context、request、response对象挂载各种属性。handleRequest对请求进行处理,该处理操作是通过compose以后的中间件实现的也就是fnMiddleware操作的。我们知道koa-compose处理以后的中间件返回的是一个匿名函数这里对应的是fnMiddleware,通过该函数对请求处理返回的是promise,然后进行请求resolve的reponse收尾处理,或reject的catch收尾处理。其中response是在koa中定义的私有方法。
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
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 && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
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);
}
其他
对于context.js、request.js、response.js这三个文件都只是简单的module.exports = {}对外暴露一个对象进行context、request、response操作并不是很复杂,有兴趣的同学可以直接看这三个文件的源码(koa源码)。