再次思考:从浏览器输入 URL 到页面展示过程的过程中发生了什么?
通过前面的基础学习,我们了解了基于 Web 的应用基本流程:
通过上图不难发现,无论具体应用功能如何变化, 服务端
处理任务核心三个步骤:③、④、⑤ 中,③ 和 ⑤ 的模式基本是固定的(因为HTTP协议规范了),而 ④ 是最大的变量。
如果我们每次开发一个新的应用都要把 ③ 和 ⑤ 的逻辑重新实现一遍就会特别的麻烦。所以,我们可以封装一个框架(库)把 ③ 和 ⑤ 的逻辑进行统一处理,然后通过某种方式,把 ④ 的处理暴露给框架使用者。
egg.js
nest.js
nodemon的局部安装与快捷键
// 局部安装 开发模式
npm i nodemon -D
// 快捷启动方式,package.json文件里scripts下配置快捷键名称
"scripts": {
"aaa": "nodemon app.js"
}
// 快捷启动
npm run aaa
Koa
资源:
官网:https://koajs.com/
中文:https://koa.bootcss.com/
- 基于 NodeJS 的 web 框架,致力于 web 应用和 API 开发。
- 由 Express 原班人马打造。
- 支持 async。
- 更小、更灵活、更优雅。
安装
当前最新 Koa 依赖 node v7.6.0+、ES2015+ 以及 async 的支持。
具体请关注官网说明(依赖会随着版本的变化而变化)。
参考:https://koajs.com/#introduction
// 创建一个package.json文件
npm init
npm init -y
// 安装 koa
npm i koa
// 或者
yarn add koa
核心
Koa
对 NodeJS
原生 IncomingMessage
和 ServerResponse
对象和解析响应通用流程进行了包装,并提供了几个核心类(对象)用于其它各种用户业务调用。
- Application 对象
- Context 对象
- Request 对象
- Response 对象
应用代码
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.listen(8888);
Application 对象
该对象是 Koa
的核心对象,通过该对象来初始化并创建 WebServer
。
/**
* File: /node_modules/koa/lib/application.js
***/
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;
}
}
构造函数对 Application
创建进行了一些初始化工作,暂时不需要关注这里的太多细节,后续关注。
应用代码
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
listen 方法
WebServer
并不是在 Application
对象创建的时候就被创建的,而是在调用了 Application
下的 listen
方法的时候在创建。
/**
* File: /node_modules/koa/lib/application.js
***/
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
通过源码可以看到,其本质还是通过 NodeJS
内置的 http
模块的 createServer
方法创建的 Server
对象。并且把 this.callback()
执行后的结果(函数)作为后续请求的回调函数。
/**
* File: /node_modules/koa/lib/application.js
***/
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
返回的 handleRequest
函数,且该函数接收的 req
和 res
参数就是 NodeJS
中 HTTP
模块内置的两个对象 IncomingMessage
和 ServerResponse
对象。其中:
const ctx = this.createContext(req, res);
这里, Koa
会调用 Application
对象下的 createContext
方法对 req
和 res
进行包装,生成 Koa
另外一个核心对象: Context
对象 - 后续分析。
return this.handleRequest(ctx, fn);
接着调用 Application
对象下的 handleRequest
方法进行请求处理,并传入:
- ctx: 前面提到的
Context
对象。 - fn: 这个实际上是
const fn = compose(this.middleware);
这段代码得到的是一个执行函数,这里又称为:中间件函数
。
中间件函数
所谓的中间件函数,其实就是一开始我们提到的 ④,首先, Application
对象中会提供一个属性 this.middleware = [];
,它是一个数组,用来存储 ④ 需要处理的各种业务函数。这些业务函数会通过 Application
下的 use
方法进行注册(类似事件注册)。
为什么叫中间件
因为它是在 请求
之后, 响应
之前调用的函数,所以就叫它 中间件函数
。
响应流程处理
通过上述流程分析,可以看到,每一个请求都会执行到 Application
对象下的 handleRequest
方法。
/**
* File: /node_modules/koa/lib/application.js
***/
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
就是一系列中间件函数执行后结果(一个 Promise
对象),当所有中间件函数执行完成以后,会通过 then
调用 handleResponse
,也就是调用了 respond
这个方法。
响应处理
/**
* File: /node_modules/koa/lib/application.js
***/
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