使用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源码)。