本文的主要内容是经过描述做者本身学习koa源代码的过程,来和你们一块儿来学习koa的源码,koa设计的初衷是 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石,说白了就是 小巧 ,扩展性高因此koa的源码阅读起来也相对容易,若是你跟着个人文章学习 还学不会那么必定是我写的很差。html
在上一篇文章中咱们学习了目录结构,知道了入口文件application.js,本篇咱们就来一块儿学习一下。node
打开 lib/appication 咱们发现这个文件也只有200多行,咱们如何阅读这个文件?
先看 Koa项目--hello word 的启动git
const Koa = require('koa');
const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000); 复制代码
这个基础的项目就干了4件事github
咱们根据下面的方式来看一下咱们的application.js 的内容。 (版本koa@2.13.0)web
看到第30行导出了application类 , 而且该类继承 Emitter ,而后看一下 Emitter 是经过第16行的 events 模块导入的。 node_modules里没有找到events模块,说明events模块是node的原生模块,application类继承了node原生的evetns模块,不了解events模块也不用纠结能够继续往下看,用到events模块的内容再看就是了。api
知道了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块app
const context = require('./context');
引入,也就是
lib/context.js
当咱们new 好了一个 application 对象以后,咱们开始使用调用application的方法,执行app.usedom
<!--const Koa = require('koa');-->
<!--const app = new Koa();--> // 看这一句 app.use(async ctx => { ctx.body = 'Hello World'; }); <!--app.listen(3000);--> 复制代码
看一下use这个方法的源代码,在application.js中的第122行。koa
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; } 复制代码
这个方法作的事情很是简单
咱们继续执行咱们的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模块的建立服务和服务监听进行了一次封装
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 事件处理逻辑的核心
先来看一下什么是洋葱模型
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 进行处理。
这行代码的意思就很明确了,就是判断是否监听过error信息,若是没有监听过error,就将错误信息经过 onerror 方法将错误信息进行包装返回。
先来看一下这个req和res是怎么来的,咱们上文说过this.callback()
是做为http.createServer
的参数使用的,那么很明显这里面的 req 和 res 也就是node中的经过http.createServe返回的req对象和res对象。
先是经过this.createContext函数建立了一个上下文对象 ctx,而后返回了this.handleRequest函数(和function handleRequest 不是一个函数)这里将ctx和经过compose转变过的middleware 做为参数。
咱们先来看一下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; } 复制代码
再来看一下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); } 复制代码
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 排版