Node.js HTTP 模块
Node.js HTTP 模块怎么使用?
Node.js 中的 HTTP 模块, 是 Node 可以作为 Webserver 的基础. 是一个原生的模块. 基于这个模块, Node.js 有了处理 HTTP 请求的能力.
使用 HTTP 模块创建一个 HTTPServer 非常简单, 只需要短短的几行代码
const http = require('http')
const server = http.createServer((req, res) => {
// Handle request and response
rse.end(`The request path is ${req.url}`);
})
server.listen(3000, () => {
console.log('The server is listening at 3000')
})
复制代码
这就创建了一个最简单的 HTTP 服务器. 我们可以看到, 在createServer
中的回调函数, 就是我们整个 server 的服务逻辑.在这个例子里面, 我们直接返回了用户请求的 url 信息.
const server = http.createServer((req, res) => {
// Handle request and response
rse.end(`The request path is ${req.url}`);
})
复制代码
这个回调函数里面, 有两个参数, 一个是req
, 另外一个是res
. 他们分别是 Node 的 HTTP 模块对于用户的请求以及响应的一个封装. 具体的文档, 可以参考 http.ClientRequest 以及 http.ServerResponse.
这里内容有点多, 后面会单独写一遍文章进行相应的介绍.这里我们只需要知道所有 HTTP 的操作都是基于在两个对象上面进行的就可以了.
Koa.js
, 本质上就是对于 HTTP
模块的一个封装, 并且提供了中间件的加载能力.参照源码, 我们发现, Koa.js
的逻辑, 都封装在了callback
这个函数中, 这最重要的函数, 则是 compose. 因而要研究Koa.js
, 本质上就是在研究compose
这个函数在干什么.
module.exports = class Application extends Emitter {
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
listen(...args) {
debug("listen");
const server = http.createServer(this.callback());
return server.listen(...args);
}
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;
}
};
复制代码
Koa.js 中的中间件
Koa.js
提供了两个功能, 一个是HTTP
的请求和响应, 这个是对于 Node.js 中的HTTP
模块的调用. 另外一个重要的功能, 则是中间件
.
如何使用 Koa.js 的中间件?
根据官方文档的例子, 我们可以通过use
函数, 把我们所需要的中间件挂载到Koa
上面.
const Koa = require("koa");
const app = new Koa();
app.use(async function (ctx, next) {
console.log(">> one");
await next();
console.log("<< one");
});
app.use(async function (ctx, next) {
console.log(">> two");
await next();
ctx.body = "two";
console.log("<< two");
});
app.use(async function (ctx, next) {
console.log(">> three");
await next();
console.log("<< three");
});
app.listen(8080);
/**
* 访问浏览器, 我们可以看到
* >> one
* >> two
* >> three
* << three
* << two
* << one
* /
复制代码
上述例子可以在 codesanbox 中查看 example
我们会看到, next
函数执行之前的语句会依次执行, 然后则是按照倒序执行next
函数之后的函数, 有点栈的感觉. 为什么会出现这个结果呢? 这个就不得不提到 koa
中老生常谈的洋葱圈模型.
什么是洋葱圈模型?
洋葱圈模型, 实际上就是指一个 HTTP 的请求, 会在请求的时候路过一次, 返回的时候再路过一次. 他们的分界点就是在next
函数上面. 因而一次中间件有两次机会可以对一次完整的HTTP
请求做处理.
+-----------------+
| |
| +----------+ |
| | | |
| | +----+ | |
| | | | | |
Request | | | | | | Response
------------------->| |------------------>
| | | | | |
| | | | | |
| | +----+ | |
| | | |
| +----------+ |
| |
+-----------------+
复制代码
中间件引擎的实现, koa-compose
对于 middleware 的特性, 我们可以参考栈进行总结.
- 挂载的操作 First in, last out.
- 有统一的上下文
ctx
.
回到最上面提到Koa
初始化的例子, Koa
在创建一个 HTTP Server 的时候, 会把callback
作为回调函数传入.
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn) // use 函数把我们的中间件加入到 middleware 的数组中
return this
}
callback () {
const fn = compose(this.middleware) // 把 middleware 数组传入到 compose 函数, 得到一个新的函数.
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
}
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) // 在 handleRequest 函数中, 直接就是把 ctx 对象传入到 fn 中进行执行.
}
复制代码
compose 函数则是整个 koa 的核心.
刨除一些异常处理的语句, 我们看看 compose
做了什么
function compose (middleware) {
return function (context, next) {
// 记录上一次调用的中间件
let index = -1
return dispatch(0)
function dispatch (i) {
index = i // 赋值中间件索引
let fn = middleware[i]
if (i === middleware.length) fn = next
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) // 把下一个中间件的索引通过 bind 绑定
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
这里通过 bind 的链式调用, 把下一个挂载的中间件作为 next
参数传入, 达到了控制中间件队列的目的. 同时context
对象也是作为参数一并传入.
Koa.js 的核心实现
理解了 compose
的原理, 我们就可以自己动手去实现一个简单的 koa.js
了.
让我们先创建一个Application
的类.
然后把核心的函数编写好, 主要是两大类, 分别是 HTTP 请求处理的handleRequest
, 以及中间件相关的use
, callback
.
const Emitter = require("events");
const compose = require("koa-compose");
const http = require("http");
module.exports = class Application extends Emitter {
/**
* Initialize a new `Application`.
*
* @api public
*/
constructor(options) {
super();
this.middleware = [];
this.context = Object.create({});
}
/**
* 调用底层 HTTP 模块 createServer
*/
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* 挂载中间件
*/
use(fn) {
if (typeof fn !== "function")
throw new TypeError("middleware must be a function!");
this.middleware.push(fn);
return this;
}
/**
* 创建通用上下文
*/
createContext(req, res) {
let context = Object.create(this.context);
context.req = req;
context.res = res;
return context;
}
/**
* HTTP 请求处理的回调函数
*/
callback() {
const fn = compose(this.middleware); // 这里我们依旧借助 koa-compose.
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 创建请求的上下文
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const handleResponse = () => ctx.res.end();
return fnMiddleware(ctx).then(handleResponse);
}
};
复制代码
大家有兴趣可以尝试在 codesandbox 上面运行这里例子, 看看情况, 我这里也把自己实验的地址贴在这里了.
Koa example
总结
Koa 通过对Context
对象的创建, 以及中间件
机制的设计, 奠定了整个框架的运行和后续开发的方向.
后续所有的功能, 例如我们常见的koa-router
, koa-logger
等, 都是通过中间件的形式进行开发加载.
后面的内容, 主要则是围绕在如何通过开发中间件去扩展 koa 的功能.