前言
本文的主要内容是通过描述作者自己学习koa源代码的过程,来和大家一起来学习koa的源码,koa设计的初衷是 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石,说白了就是 小巧 ,扩展性高所以koa的源码阅读起来也相对容易,如果你跟着我的文章学习 还学不会那么一定是我写的不好。
在上一篇文章中我们学习了目录结构,知道了入口文件application.js,本篇我们就来一起学习一下。
Application
打开 lib/appication 我们发现这个文件也只有200多行,我们如何阅读这个文件?
先看 Koa项目--hello word 的启动
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
这个基础的项目就干了4件事
require Koa,也就是导入了我们这个application.js 创建一个实例,new 了一个我们application导出的类 调用了这个实例的use方法,传入了一个function 调用了listen 方法,监听了3000端口
我们根据下面的方式来看一下我们的application.js 的内容。 (版本koa@2.13.0)
先看导出的内容
看到第30行导出了application类 , 并且该类继承 Emitter ,然后看一下 Emitter 是通过第16行的 events 模块导入的。
constructor
知道了application的继承,继续看application的初始化。下面代码我加了一点自己的注释,下文会有一些解读。
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;
}
}
复制代码
我将上面的constructor的代码大致分为3块
①配置项相关部分
option通过new 的时候传入的参数来控制自身的一些属性,通过名字可以大致的猜到什么意思但是也不能明确到底是干什么的,也不知道在什么情况下会使用,所以不用纠结,大致知道是有一些属性是在这里定义的并且给了一些默认值就好了,用的时候再来看就是了。②重要的属性
如果你使用过koa,那你就可以大致猜到这几个属性是干什么的middleware koa可以实现洋葱模型的中间件工作的基础 context 上下文对象,通过 const context = require('./context');
引入,也就是lib/context.jsrequest koa 的 Request 对象,就是 lib/request.js response koa 的 Response 对象,就是 lib/response.js
③检查相关
也不用纠结用到再说
use
当我们new 好了一个 application 对象之后,我们开始使用调用application的方法,执行app.use
<!--const Koa = require('koa');-->
<!--const app = new Koa();-->
// 看这一句
app.use(async ctx => {
ctx.body = 'Hello World';
});
<!--app.listen(3000);-->
复制代码
看一下use这个方法的源代码,在application.js中的第122行。
use(fn) {
// ①保证参数必须为一个function
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// ②如果是generator函数要进行一次转化
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状态下的输出
debug('use %s', fn._name || fn.name || '-');
// ④将方法push到我们的中间件数组
this.middleware.push(fn);
// ⑤想要链式调用,必须返回自身
return this;
}
复制代码
这个方法做的事情非常简单
①保证参数必须为一个function ②如果是generator函数要进行一次转化 先判断是否为generator函数,看到isGeneratorFunction这个方法名就知道是干什么的,我也不管怎么实现的(想要写出高质量的代码,命名真的非常重要 描述了v3版本不再支持gennorator函数作为参数,同时告诉你怎么去转化 convert方法是引入的koa-convert包,简单来说功能就是Generator函数转换成将async函数。更准确的说是将Generator函数转换成使用co包装成的Promise对象。
③debug状态下的输出
我们看到很多项目都有debug模式会将执行信息暴露出来,用的就是类似的方法,不影响主流程就不深入看了。④将方法push到我们的中间件数组
整个use方法最核心的就这一句了,将use调用的方法push到middleware数组中,本质上app.use的调用就是为了完成将参数(方法)push到middleware数组中这件事。⑤想要链式调用,必须返回自身
listen
我们继续执行我们的koa程序
<!--const Koa = require('koa');-->
<!--const app = new Koa();-->
<!--app.use(async ctx => {-->
<!-- ctx.body = 'Hello World';-->
<!--});-->
app.listen(3000); // 看这一句
复制代码
看一下listen函数,在application.js 的第79行
listen(...args) {
debug('listen');
// ①调用node中的http模块的createServer方法
const server = http.createServer(this.callback());
// ②http.Server实例调用listen方法
return server.listen(...args);
}
复制代码
这个listen也很简单,就是对node原生的http模块的创建服务和服务监听进行了一次封装
①调用node中的http模块的createServer方法
这块先看一下node文档的http模块,了解一下http.createserver方法的使用,以及返回了http.server实例②http.Server实例调用listen方法
可以看一下server.listen的文档,通常最简单的使用一般就是传一个端口号。
callback
koa的listen方法中调用了callback方法,我们来看看callback方法干了什么事情。 代码在application.js 的第143行
callback() {
// ①洋葱模型原理核心
const fn = compose(this.middleware);
// ②错误监听相关
if (!this.listenerCount('error')) this.on('error', this.onerror);
// ③koa封装的requestListener
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
复制代码
这个callback方法可以说是 koa 事件处理逻辑的核心
①compose 洋葱模型的核心原理
先来看一下什么是洋葱模型
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(6);
}
const middleware2 = async (ctx, next) => {
console.log(2);
await next();
console.log(5);
}
const middleware3 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
ctx.body = 'hello world'
})
app.listen(3001)
// 输出1,2,3,4,5,6
复制代码
通过分析之前的代码我们知道 app.use(fn) 函数最主要的作用就是对 fn 进行 this.middleware.push(fn)
在上面的代码中 我们通过app.use(middleware1); app.use(middleware2); app.use(middleware3);
将this.middleware 数组变成了 [middleware1, middleware1, middleware1]
callback函数 第一句是执行了const fn = compose(this.middleware);
找到compose的定义
koa-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)
}
}
}
}
复制代码
代码非常的短只有几十行
比较关键的就是这个dispatch函数了,它将遍历整个middleware,然后将context和dispatch(i + 1)传给middleware中的方法。
巧妙的实现了
将 context
一路传下去给中间件将 middleware
中的下一个中间件fn
作为未来next
的返回值
最终通过compose将[middleware1, middleware1, middleware1]
转变成了类似 middleware1(middleware2(middleware3()));
的函数。
运用了 compose 的特性,结合 async await 中 next 的等待执行,形成了洋葱模型,我们可以利用这一特性在 next 之前对 request 进行处理,而在 next 之后对 response 进行处理。
②错误监听相关
调用this.listenerCount 函数, 我找了application.js代码中没有相关函数,想起Application继承 自events 就翻阅了一下node的api,listenerCount方法找到了主要功能如下
onerror 方法将错误信息进行包装返回。
③koa封装的requestListener ,这里定义了一个handleRequest函数并返回
先来看一下这个req和res是怎么来的,我们上文说过this.callback()
是作为http.createServer
的参数使用的,那么很明显这里面的 req 和 res 也就是node中的通过http.createServe返回的req对象和res对象。
先是通过this.createContext函数创建了一个上下文对象 ctx,然后返回了this.handleRequest函数(和function handleRequest 不是一个函数)这里将ctx和通过compose转变过的middleware 作为参数。
createContext
我们先来看一下this.createContext函数如何创建上下文对象ctx 代码在 177行。
createContext(req, res) {
// ① 创建context 、request、response 对象
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// ② context 、request 、 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;
// ③ 记录原始url 并返回自身
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
复制代码
① 创建context 、request、response 对象 通过context.js、request.js、response.js 分别创建对象具体内容会在后面讲,这里只要知道在这里创建即可 ② context 、request 、 response 互相绑定 在koa使用中我们经常会通过 app.ctx.request 这种方式调用,就是在这块进行的互相挂载比较简单就不多解释了,同时注意一下 ctx.request !== ctx.req ③ 记录原始url 并返回自身 记录一下原始的url信息,以及创建了一个state对象(也不用管用到时候再说)并且返回了context实例,也即是我们说的 上下文对象ctx,同时我们也知道这个上下文对象在这里获得了request实例、response实例以及通过node返回的 req和res。
handleRequest
再来看一下callback 最终返回的handleRequest函数,代码在162行。
handleRequest(ctx, fnMiddleware) {
// ①将res的statusCode 设置为 404
const res = ctx.res;
res.statusCode = 404;
// ②定义catch 时调用的onerror函数,以及正常返回时的 handleResponse函数
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// ③ 顾名思义,判断是否结束以及调用的某方法
onFinished(res, onerror);
// ④ 调用之前通过compose生产的函数
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
①将res的statusCode 设置为 404 ②定义catch 时调用的onerror函数,以及正常返回时的 handleResponse函数 ③ 顾名思义,判断是否结束以及调用的某方法 ④ 调用之前通过compose生产的函数 这个就是我们最终通过listen --> callback --> handleRequest 返回的结果, 这里面的fnMiddleware 就是我们生成的 middleware1(middleware2(middleware3()))
函数,handleResponse就是this.response函数,通过将ctx、request、response、req、res,一路向下传后一路向上返回最终返回结果。
还有response 函数我就不细说了,也比较简单主要就是根据不同的返回状态返回不同的结果,主要包括对method === "HEAD"的返回,以及对返回body类型时res对象的一些处理
梳理
最后我用一张图来梳理整个Application.js的过程
总结
本篇application.js我们先分析到这里,下一篇会讲述关于context.js的内容,本文完全按照作者自己学习源码的过程进行描述,文笔不好读起来可能会有一点流水账,但是作者会努力描述清楚,并且把阅读源码的一些方法技巧分享,请收藏点赞支持。
相关文章
手把手和你一起学习Koa源码(二)——Appilication
本文使用 mdnice 排版