koa源码解读

koa是有express原班人马打造的基于node.js的下一代web开发框架。koa 1.0使用generator实现异步,相比于回调简单和优雅和不少。koa团队并没有止步于koa 1.0, 随着node.js开始支持async/await,他们又马不停蹄的发布了koa 2.0,koa2完全使用Promise并配合async/await来实现异步,使得异步操作更臻完美。

一、快速开始

koa使用起来非常简单,安装好node.js后执行以下命令安装koa:

npm init

npm install --save koa

一个简单的Hello World程序开场,

//index.js

const Koa = require(‘koa‘)

const app = new Koa()

app.use( async ctx  => {

ctx.body = ‘Hello World‘

})

app.listen(3000,()=>{

console.log("server is running at 3000 port");

})

在命令行执行

node index.js

打开浏览器查看http://localhost:3000就可以看到页面输出的 Hello World。

中间件 middleware

Koa中使用 app.use()用来加载中间件,基本上Koa 所有的功能都是通过中间件实现的。

中间件的设计非常巧妙,多个中间件会形成一个栈结构(middle stack),以”先进后出”(first-in-last-out)的顺序执行。每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是 next函数。只要调用 next函数,就可以把执行权转交给下一个中间件,最里层的中间件执行完后有会把执行权返回给上一级调用的中间件。整个执行过程就像一个剥洋葱的过程。

比如你可以通过在所有中间件的顶端添加以下中间件来打印请求日志到控制台:

app.use(async function (ctx, next) {

let start = new Date()

await next()

let ms = new Date() - start

console.log(‘%s %s - %s‘, ctx.method, ctx.url, ms)

})

常用的中间件列表可以在这里找到: https://github.com/koajs/koa/wiki

二、koa源码解读

打开项目根目录下的node_modules文件夹,打开并找到koa的文件夹,如下所示:

打开lib文件夹,这里一共有4个文件,

  • application.js - koa主程序入口
  • context.js - koa中间件参数ctx对象的封装
  • request.js - request对象封装
  • response.js - response对象封装

我们这里主要看下application.js,我这里摘取了主要功能相关的 代码如下:

/**

* 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);

}

/**

* Use the given middleware `fn`.

*

* Old-style middleware will be converted.

*

* @param {Function} fn

* @return {Application} self

* @api public

*/

use(fn) {

if (typeof fn !== ‘function‘) throw new TypeError(‘middleware must be a function!‘);

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(‘use %s‘, fn._name || fn.name || ‘-‘);

this.middleware.push(fn);

return this;

}

/**

* Return a request handler callback

* for node‘s native http server.

*

* @return {Function}

* @api public

*/

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;

}

/**

* Handle request in callback.

*

* @api private

*/

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);

}

通过注释我们可以看出上面代码主要干的事情是初始化http服务对象并启动。我们注意到 callback()方法里面有这样一段代码 :

const fn = compose(this.middleware);

compose其实是Node模块koa-compose,它的作用是将多个中间件函数合并成一个大的中间件函数,然后调用这个中间件函数就可以依次执行添加的中间件函数,执行一系列的任务。遇到await next()时就停止当前中间件函数的执行并把执行权交个下一个中间件函数,最后next()执行完返回上一个中间件函数继续执行下面的代码。

它是用了什么黑魔法实现的呢?我们打开node_modules/koa-compose/index.js,代码如下 :

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, dispatch.bind(null, i + 1)));

} catch (err) {

return Promise.reject(err)

}

}

}

}

乍一看好难好复杂,没事,我们一步一步的来梳理一下。

这个方法里面的核心就是dispatch函数(废话,整个compose方法就返回了一个函数)。没有办法简写,但是我们可以将dispatch函数类似递归的调用展开,以三个中间件为例:

第一次,此时第一个中间件被调用,dispatch(0),展开:

Promise.resolve(function(context, next){

//中间件一第一部分代码

await/yield next();

//中间件一第二部分代码}());

很明显这里的next指向dispatch(1),那么就进入了第二个中间件;

第二次,此时第二个中间件被调用,dispatch(1),展开:

Promise.resolve(function(context, 中间件2){

//中间件一第一部分代码

await/yield Promise.resolve(function(context, next){

//中间件二第一部分代码

await/yield next();

//中间件二第二部分代码

}())

//中间件一第二部分代码}());

很明显这里的next指向dispatch(2),那么就进入了第三个中间件;

第三次,此时第二个中间件被调用,dispatch(2),展开:

Promise.resolve(function(context, 中间件2){

//中间件一第一部分代码

await/yield Promise.resolve(function(context, 中间件3){

//中间件二第一部分代码

await/yield Promise(function(context){

//中间件三代码

}());

//中间件二第二部分代码

})

//中间件一第二部分代码}());

此时中间件三代码执行完毕,开始执行中间件二第二部分代码,执行完毕,开始执行中间一第二部分代码,执行完毕,所有中间件加载完毕。

再举一个例子加深下理解。新建index.js并粘贴如下代码:

const compose = require(‘koa-compose‘)

const middleware1 = (ctx, next) => {

console.log(‘here is in middleware1, before next:‘);

next();

console.log(‘middleware1 end‘);

}

const middleware2 = (ctx, next) => {

console.log(‘here is in middleware2, before next:‘);

next();

console.log(‘middleware2 end‘);

}

const middleware3 = (ctx, next) => {

console.log(‘here is in middleware3, before next:‘);

next();

console.log(‘middleware3 end‘);

}

const middlewares = compose([middleware1, middleware2, middleware3])

console.dir(middlewares())

在命令行输入node index.js执行,输出结果如下:

here is in middleware1, before next:

here is in middleware2, before next:

here is in middleware3, before next:

middleware3 end

middleware2 end

middleware1 end

Promise { undefined }

可以看到每个中间件都按照“剥洋葱”的流程一次执行。当我们初始化app对象并调用app.use()时,就是在不断往app.middleware数组里添加中间件函数,当调用app.listen()再执行组合出来的函数。

-END-

转载请注明来源

扫描下方二维码,或者搜索 前端提高班 关注公众号,即可获取最新走心文章

记得把我设为星标或置顶哦

在公众号后台回复 前端资源 即可获取最新前端开发资源

原文地址:https://www.cnblogs.com/lightzone/p/9746334.html

时间: 2024-08-03 06:11:31

koa源码解读的相关文章

十分钟带你看完 KOA 源码

前段时间看了 koa 源码,得益于 koa 良好抽象,不仅提供了简洁的 api ,同时也使得源码相当的简洁和优雅.今天花点时间画了一张 koa 源码的结构图来分析其源码,在总结的同时,希望能够帮到相关的同学. 注:源码是基于 2.x 版本,源码结构与 1.x 完全一致,代码更加简洁直观一点. 基础知识 任何用过 node 的人对下面的代码都不会陌生,如下: const http = require('http'); const server = http.createServer((req, r

QCustomplot使用分享(二) 源码解读

一.头文件概述 从这篇文章开始,我们将正式的进入到QCustomPlot的实践学习中来,首先我们先来学习下QCustomPlot的类图,如果下载了QCustomPlot源码的同学可以自己去QCustomPlot的目录下documentation/qcustomplot下寻找一个名字叫做index.html的文件,将其在浏览器中打开,也是可以找到这个库的类图.如图1所示,是组成一个QCustomPlot类图的可能组成形式. 一个图表(QCustomPlot):包含一个或者多个图层.一个或多个ite

vue源码解读预热-0

vueJS的源码解读 vue源码总共包含约一万行代码量(包括注释)特别感谢作者Evan You开放的源代码,访问地址为Github 代码整体介绍与函数介绍预览 代码模块分析 代码整体思路 总体的分析 从图片中可以看出的为采用IIFE(Immediately-Invoked Function Expression)立即执行的函数表达式的形式进行的代码的编写 常见的几种插件方式: (function(,){}(,))或(function(,){})(,)或!function(){}()等等,其中必有

SpringMVC源码解读 - RequestMapping注解实现解读 - RequestCondition体系

一般我们开发时,使用最多的还是@RequestMapping注解方式. @RequestMapping(value = "/", param = "role=guest", consumes = "!application/json") public void myHtmlService() { // ... } 台前的是RequestMapping ,正经干活的却是RequestCondition,根据配置的不同条件匹配request. @Re

jdk1.8.0_45源码解读——HashMap的实现

jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变.  除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代col

15、Spark Streaming源码解读之No Receivers彻底思考

在前几期文章里讲了带Receiver的Spark Streaming 应用的相关源码解读,但是现在开发Spark Streaming的应用越来越多的采用No Receivers(Direct Approach)的方式,No Receiver的方式的优势: 1. 更强的控制自由度 2. 语义一致性 其实No Receivers的方式更符合我们读取数据,操作数据的思路的.因为Spark 本身是一个计算框架,他底层会有数据来源,如果没有Receivers,我们直接操作数据来源,这其实是一种更自然的方式

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现 一. Set架构 如上图: (01) Set 是继承于Collection的接口.它是一个不允许有重复元素的集合.(02) AbstractSet 是一个抽象类,它继承于AbstractCollection.AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利.(03) HastSet 和 TreeSet 是Set的两个实现类.        HashSet依赖于HashMa

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

hadoop源码解读namenode高可靠:HA;web方式查看namenode下信息;dfs/data决定datanode存储位置

点击browserFilesystem,和命令查看结果一样 当我们查看hadoop源码时,我们看到hdfs下的hdfs-default.xml文件信息 我们查找${hadoop.tmp.dir}这是引用变量,肯定在其他文件有定义,在core-default.xml中查看到,这两个配置文件有个共同点: 就是不要修改此文件,但可以复制信息到core-site.xml和hdfs-site.xml中修改 usr/local/hadoop 是我存放hadoop文件夹的地方 几个关于namenode的重要文