这篇文章主要介绍了玩转Koa之核心原理分析,本文从封装创建应用程序函数、扩展res和req、中间件实现原理、异常处理的等这几个方面来介绍,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。

Koa作为下一代Web开发框架,不仅让我们体验到了async/await语法带来同步方式书写异步代码的酸爽,而且本身简洁的特点,更加利于开发者结合业务本身进行扩展。
本文从以下几个方面解读Koa源码:

  • 封装创建应用程序函数
  • 扩展res和req
  • 中间件实现原理
  • 异常处理

一、封装创建应用程序函数

利用NodeJS可以很容易编写一个简单的应用程序:


const http = require('http') const server = http.createServer((req, res) => {
// 每一次请求处理的方法
console.log(req.url)
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello NodeJS')
}) server.listen(8080)

注意:当浏览器发送请求时,会附带请求/favicon.ico。
而Koa在封装创建应用程序的方法中主要执行了以下流程:

  • 组织中间件(监听请求之前)
  • 生成context上下文对象
  • 执行中间件
  • 执行默认响应方法或者异常处理方法

// application.js
listen(...args) {
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) => {
// 生成context上下文对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
}; return handleRequest;
} handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默认状态码为404
res.statusCode = 404;
// 中间件执行完毕之后 采用默认的 错误 与 成功 的处理方式
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

二、扩展res和req

首先我们要知道NodeJS中的res和req是http.IncomingMessage和http.ServerResponse的实例,那么就可以在NodeJS中这样扩展req和res:


Object.defineProperties(http.IncomingMessage.prototype, {
query: {
get () {
return querystring.parse(url.parse(this.url).query)
}
}
}) Object.defineProperties(http.ServerResponse.prototype, {
json: {
value: function (obj) {
if (typeof obj === 'object') {
obj = JSON.stringify(obj)
}
this.end(obj)
}
}
})

而Koa中则是自定义request和response对象,然后保持对res和req的引用,最后通过getter和setter方法实现扩展。


// application.js
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; // 保存原生req对象
context.res = request.res = response.res = res; // 保存原生res对象
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 最终返回完整的context上下文对象
return context;
}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920

所以在Koa中要区别这两组对象:

  • request、response: Koa扩展的对象
  • res、req: NodeJS原生对象

// request.js
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920

此时已经可以采用这样的方式访问header属性:


ctx.request.header

但是为了方便开发者调用这些属性和方法,Koa将response和request中的属性和方法代理到context上。
通过Object.defineProperty可以轻松的实现属性的代理:


function access (proto, target, name) {
Object.defineProperty(proto, name, {
get () {
return target[name]
},//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
set (value) {
target[name] = value
}
})
} access(context, request, 'header')

而对于方法的代理,则需要注意this的指向:


function method (proto, target, name) {
proto[name] = function () {
return target[name].apply(target, arguments)
}
}

上述就是属性代理和方法代理的核心代码,这基本算是一个常用的套路。

代理这部分详细的源码,可以查看node-delegates, 不过这个包时间久远,有一些老方法已经废除。

在上述过程的源码中涉及到很多JavaScript的基础知识,例如:原型继承、this的指向。对于基础薄弱的同学,还需要先弄懂这些基础知识。

三、中间件实现原理

首先需要明确是:中间件并不是NodeJS中的概念,它只是connect、express和koa框架衍生的概念。

1、connect中间件的设计

在connect中,开发者可以通过use方法注册中间件:


function use(route, fn) {
var handle = fn;
var path = route; // 不传入route则默认为'/',这种基本是框架处理参数的一种套路
if (typeof route !== 'string') {
handle = route;
path = '/';
}
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
...
// 存储中间件
this.stack.push({ route: path, handle: handle });
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
// 以便链式调用
return this;
}

use方法内部获取到中间件的路由信息(默认为'/')和中间件的处理函数之后,构建成layer对象,然后将其存储在一个队列当中,也就是上述代码中的stack。
connect中间件的执行流程主要由handle与call函数决定:


function handle(req, res, out) {
var index = 0;
var stack = this.stack;
...
function next(err) {
...
// 依次取出中间件
var layer = stack[index++]
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
// 终止条件
if (!layer) {
defer(done, err);
return;
} var path = parseUrl(req).pathname || '/';
var route = layer.route; // 路由匹配规则
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
...
call(layer.handle, route, err, req, res, next);
} next();
}

handle函数中使用闭包函数next来检测layer是否与当前路由相匹配,匹配则执行该layer上的中间件函数,否则继续检查下一个layer。
这里需要注意next中检查路由的方式可能与想象中的不太一样,所以默认路由为'/'的中间件会在每一次请求处理中都执行。


function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
try {
if (hasError && arity === 4) {
// 错误处理中间件
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// 请求处理中间件
handle(req, res, next);
return;
}
} catch (e) {
// 记录错误
error = e;
} // 将错误传递下去
next(error);
}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920

在通过call方法执行中间件方法的时候,采用try/catch捕获错误,这里有一个特别需要注意的地方是,call内部会根据是否存在错误以及中间件函数的参数决定是否执行错误处理中间件。并且一旦捕获到错误,next方法会将错误传递下去,所以接下来普通的请求处理中间件即使通过了next中的路由匹配,仍然会被call方法给过滤掉。
下面是layer的处理流程图:

上述就是connect中间件设计的核心要点,总结起来有如下几点:
通过use方法注册中间件;

  • 中间件的顺序执行是通过next方法衔接的并且需要手动调用,在next中会进行路由匹配,从而过滤掉部分中间件;
  • 当中间件的执行过程中发生异常,则next会携带异常过滤掉非错误处理中间件,也是为什么错误中间件会比其他中间件多一个error参数;
    在请求处理的周期中,需要手动调用res.end()来结束响应;

2、Koa中间件的设计

Koa中间件与connect中间件的设计有很大的差异:

  • Koa中间件的执行并不需要匹配路由,所以注册的中间件每一次请求都会执行。(当然还是需要手动调用next);
  • Koa中通过继承event,暴露error事件让开发者自定义异常处理;
  • Koa中res.end由中间件执行完成之后自动调用,这样避免在connect忘记调用res.end导致用户得不到任何反馈。
  • Koa中采用了async/await语法让开发者利用同步的方式编写异步代码。
    当然,Koa中也是采用use方法注册中间件,相比较connect省去路由匹配的处理,就显得很简洁:

use(fn) {
this.middleware.push(fn);
return this;
}

并且use支持链式调用。
Koa中间件的执行流程主要通过koa-compose中的compose函数完成:


function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
return function (context, next) {
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920
}
}
}

看到这里本质上connect与koa实现中间件的思想都是递归,不难看出koa相比较connect实现得更加简洁,主要原因在于:

  • connect中提供路由匹配的功能,而Koa中则是相当于connect中默认的'/'路径。
  • connect在捕获中间件的异常时,通过next携带error一个个中间件验证,直到错误处理中间件,而Koa中则是用Promise包装中间件,一旦中间件发生异常,那么会直接触发reject状态,直接在Promise的catch中处理就行。

上述就是connect中间件与Koa中间件的实现原理,现在在再看Koa中间件的这张执行流程图,应该没有什么疑问了吧?!

四、异常处理

对于同步代码,通过try/catch可以轻松的捕获异常,在connect中间件的异常捕获则是通过try/catch完成。
对于异步代码,try/catch则无法捕获,这时候一般可以构造Promise链,在最后的catch方法中捕获错误,Koa就是这样处理,并且在catch方法中发送error事件,以便开发者自定义异常处理逻辑。


this.app.emit('error', err, this);

前面也谈到Koa利用async/await语法带来同步方式书写异步代码的酸爽,另外也让错误处理更加自然:


// 也可以这样自定义错误处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500
ctx.body = err
}
})//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:619586920

相信看到这里,再回忆一下之前遇到的那些问题,你应该会有新的理解,并且再次使用Koa时会更加得心应手,这也是分析Koa源码的目的之一。

结语

感谢您的观看,如有不足之处,欢迎批评指正。

原文链接:https://my.oschina.net/u/3982182/blog/2999070

深入解析Koa之核心原理的更多相关文章

  1. 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现

    本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...

  2. 开源框架TLog核心原理架构解析

    前言 最近在做TLog 1.2.5版本的迭代,许多小伙伴之前也表示说很想参与开源项目的贡献.为了让项目更好更快速的迭代新特性以及本着发扬开源精神互相学习交流,很有幸招募到了很多小伙伴与我一起前行. 为 ...

  3. 「进阶篇」Vue Router 核心原理解析

    前言 此篇为进阶篇,希望读者有 Vue.js,Vue Router 的使用经验,并对 Vue.js 核心原理有简单了解: 不会大篇幅手撕源码,会贴最核心的源码,对应的官方仓库源码地址会放到超上,可以配 ...

  4. NET/ASP.NET Routing路由(深入解析路由系统架构原理)(转载)

    NET/ASP.NET Routing路由(深入解析路由系统架构原理) 阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模 ...

  5. Maven 核心原理

    Maven 核心原理 标签 : Java基础 Maven 是每一位Java工程师每天都会接触的工具, 但据我所知其实很多人对Maven理解的并不深, 只把它当做一个依赖管理工具(下载依赖.打包), M ...

  6. Redis核心原理

    Redis系统介绍: Redis的基础介绍与安装使用步骤:https://www.jianshu.com/p/2a23257af57b Redis的基础数据结构与使用:https://www.jian ...

  7. 深度解析 Vue 响应式原理

    深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...

  8. 使用C#写MVC框架(一:核心原理)

    目录: 一.MVC原理解析 二.HttpHandler 1.HttpHandler,IHttpHandler,MvcHandler的说明 2.IHttpHandler解析 3.MvcHandler解析 ...

  9. 【转】Spring学习---Spring IoC容器的核心原理

    [原文] Spring的两个核心概念:IoC和AOP的雏形,Spring的历史变迁和如今的生态帝国. IoC和DI的基本概念 IoC(控制反转,英文含义:Inverse of Control)是Spr ...

随机推荐

  1. 利用jsonrpc技术包装uiautomator

    昨天一天在网上搜索解决上一篇文章中的exception: monkeyrunner内置uiautomator出错的原因 尽管没找到解决办法.可是让我无意中发现了一个好工具,比sl4a更好用的工具.直接 ...

  2. base 64

    我们的图片大部分都是可以转换成base64编码的data:image. 这个在将canvas保存为img的时候尤其有用.虽然除ie外,大部分现代浏览器都已经支持原生的基于base64的encode和d ...

  3. NLM非局部均值算法相关

    NLM原文: 基于图像分割的非局部均值去噪算法 基于图像分割的非局部均值去噪算法_百度文库 https://wenku.baidu.com/view/6a51abdfcd22bcd126fff705c ...

  4. python学习之路----输出所有大小写字母

    print([chr(i) for i in range(48, 58)]) # 所有数字print([chr(i) for i in range(65, 91)]) # 所有大写字母print([c ...

  5. springside4

    https://github.com/springside/springside4/wiki/Design Design 1. Web MVC Framwork: SpringMVC3.0 Restf ...

  6. Hadoop 2.0 NameNode HA和Federation实践【转】

    Hadoop 2.0 NameNode HA和Federation实践 Posted on 2012/12/10 一.背景 天云趋势在2012年下半年开始为某大型国有银行的历史交易数据备份及查询提供基 ...

  7. 拨打电话<a href="tel:">跳转到邮件<a href="mailto:">

    拨打电话 <a href="tel:0571866000">0571-866000</a> 跳转到邮件 <a href="mailto:jo ...

  8. 移动APP自动化测试框架

    简介 移动APP的UI自动化测试长久以来一直是一个难点,难点在于UI的”变”, 变化导致自动化用例的大量维护.从分层测试的角度,自动化测试应该逐层进行.最大量实现自动化测试的应该是单元测试,最容易实现 ...

  9. [Android]反编译apk + eclipse中调试smali

    从来没有想过反编译apk是来的如此方便,并且还可以修改后重新编译运行,这比在win下修改pe容易多了,感谢apktool和smali工具的作者提供这么好的工具. 跟踪apk一般的做法是在反编译的sma ...

  10. js事件委托和jQuery事件绑定on , off , one , bind , unbind , die

    一. 事件委托什么是事件委托?用现实中的理解就是:有100 个学生同时在某天中午收到快递,但这100 个学生不可能同时站在学校门口等,那么都会委托门卫去收取,然后再逐个交给学生.而在jQuery 中, ...