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程序开场,

  1. //index.js
  2.  
  3. const Koa = require('koa')
  4. const app = new Koa()
  5.  
  6. app.use(async ctx => {
  7. ctx.body = 'Hello World'
  8.  
  9. })
  10.  
  11. app.listen(3000, () => {
  12. console.log("server is running at 3000 port");
  13. })

  

在命令行执行

node index.js

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

中间件 middleware

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

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

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

  1. app.use(async function (ctx, next) {
  2.  
  3. let start = new Date()
  4.  
  5. await next()
  6.  
  7. let ms = new Date() - start
  8.  
  9. console.log('%s %s - %s', ctx.method, ctx.url, ms)
  10.  
  11. })

常用的中间件列表可以在这里找到: 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,我这里摘取了主要功能相关的 代码如下:

  1. /**
  2. * Shorthand for:
  3. *
  4. * http.createServer(app.callback()).listen(...)
  5. *
  6. * @param {Mixed} ...
  7. * @return {Server}
  8. * @api public
  9. */
  10.  
  11. listen(...args) {
  12. debug('listen');
  13. const server = http.createServer(this.callback());
  14. return server.listen(...args);
  15. }
  16.  
  17. /**
  18. * Use the given middleware `fn`.
  19. *
  20. * Old-style middleware will be converted.
  21. *
  22. * @param {Function} fn
  23. * @return {Application} self
  24. * @api public
  25. */
  26.  
  27. use(fn) {
  28. if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  29. if (isGeneratorFunction(fn)) {
  30. deprecate('Support for generators will be removed in v3. ' +
  31. 'See the documentation for examples of how to convert old middleware ' +
  32. 'https://github.com/koajs/koa/blob/master/docs/migration.md');
  33. fn = convert(fn);
  34. }
  35. debug('use %s', fn._name || fn.name || '-');
  36. this.middleware.push(fn);
  37. return this;
  38. }
  39.  
  40. /**
  41. * Return a request handler callback
  42. * for node's native http server.
  43. *
  44. * @return {Function}
  45. * @api public
  46. */
  47.  
  48. callback() {
  49. const fn = compose(this.middleware);
  50.  
  51. if (!this.listenerCount('error')) this.on('error', this.onerror);
  52.  
  53. const handleRequest = (req, res) => {
  54. const ctx = this.createContext(req, res);
  55. return this.handleRequest(ctx, fn);
  56. };
  57.  
  58. return handleRequest;
  59. }
  60.  
  61. /**
  62. * Handle request in callback.
  63. *
  64. * @api private
  65. */
  66.  
  67. handleRequest(ctx, fnMiddleware) {
  68. const res = ctx.res;
  69. res.statusCode = 404;
  70. const onerror = err => ctx.onerror(err);
  71. const handleResponse = () => respond(ctx);
  72. onFinished(res, onerror);
  73. return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  74. }

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

const fn = compose(this.middleware);

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

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

  1. function compose(middleware) {
  2.  
  3. return function (context, next) {
  4.  
  5. // last called middleware #
  6.  
  7. let index = -1
  8.  
  9. return dispatch(0)
  10.  
  11. function dispatch(i) {
  12.  
  13. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  14.  
  15. index = i
  16.  
  17. let fn = middleware[i]
  18.  
  19. if (i === middleware.length) fn = next
  20.  
  21. if (!fn) return Promise.resolve()
  22.  
  23. try {
  24.  
  25. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
  26.  
  27. } catch (err) {
  28.  
  29. return Promise.reject(err)
  30.  
  31. }
  32.  
  33. }
  34.  
  35. }
  36.  
  37. }

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

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

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

  1. Promise.resolve(function(context, next){
  2.  
  3. //中间件一第一部分代码
  4.  
  5. await/yield next();
  6.  
  7. //中间件一第二部分代码}());

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

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

  1. Promise.resolve(function(context, 中间件2){
  2.  
  3. //中间件一第一部分代码
  4.  
  5. await/yield Promise.resolve(function(context, next){
  6.  
  7. //中间件二第一部分代码
  8.  
  9. await/yield next();
  10.  
  11. //中间件二第二部分代码
  12.  
  13. }())
  14.  
  15. //中间件一第二部分代码}());

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

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

  1. Promise.resolve(function(context, 中间件2){
  2.  
  3. //中间件一第一部分代码
  4.  
  5. await/yield Promise.resolve(function(context, 中间件3){
  6.  
  7. //中间件二第一部分代码
  8.  
  9. await/yield Promise(function(context){
  10.  
  11. //中间件三代码
  12.  
  13. }());
  14.  
  15. //中间件二第二部分代码
  16.  
  17. })
  18.  
  19. //中间件一第二部分代码}());

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

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

  1. const compose = require('koa-compose')
  2.  
  3. const middleware1 = (ctx, next) => {
  4. console.log('here is in middleware1, before next:');
  5. next();
  6. console.log('middleware1 end');
  7. }
  8.  
  9. const middleware2 = (ctx, next) => {
  10. console.log('here is in middleware2, before next:');
  11. next();
  12. console.log('middleware2 end');
  13. }
  14.  
  15. const middleware3 = (ctx, next) => {
  16. console.log('here is in middleware3, before next:');
  17. next();
  18. console.log('middleware3 end');
  19. }
  20.  
  21. const middlewares = compose([middleware1, middleware2, middleware3])
  22. 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-

转载请注明来源

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

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

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

koa源码解读的更多相关文章

  1. 从koa-session源码解读session本质

    前言 Session,又称为"会话控制",存储特定用户会话所需的属性及配置信息.存于服务器,在整个用户会话中一直存在. 然而: session 到底是什么? session 是存在 ...

  2. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  3. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  4. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  5. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  6. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  7. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  8. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

  9. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

随机推荐

  1. CocoaPods(第三方类库管理工具)

    iOS安装CocoaPods详细过程  一.简介 什么是CocoaPods CocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods” ...

  2. poj1664【DFS】

    思路:搜一下,还想多了,记得以前做过把一个数搞成几个数的相加组合,然后这题无非就是多了个组合的个数<=m的,那么只要多加一个条件,当num>m的时候也return掉就好了. //#incl ...

  3. 易爆物(X-Plosives )基础并查集

    #include <iostream> #include <algorithm> using namespace std; + ; int fa[maxn]; int Find ...

  4. Centos 6.8安装 SVN

    SVN SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS.互联网上很多版本控制服务已从CVS迁移到Subver ...

  5. 设置Linux环境变量的方法和区别_Ubuntu/CentOS

    设置 Linux 环境变量可以通过 export 实现,也可以通过修改几个文件来实现,有必要弄清楚这两种方法以及这几个文件的区别. 通过文件设置 Linux 环境变量 首先是设置全局环境变量,对所有用 ...

  6. 微信小程序生成分享图片,保存到本地

    1.页面 <canvas canvas-id="shareCanvas" style="width:600px;height:900px">< ...

  7. Spark-SQL连接Hive

    第一步:修个Hive的配置文件hive-site.xml 添加如下属性,取消本地元数据服务: <property> <name>hive.metastore.local< ...

  8. linux/centos系统如何使用yum安装vi/vim?

    yum安装vim最简单的命令, yum -y install vim* 然后就可以使用vi命令了. 网上的文章: 要使用vim, 使用yum看了一下,发现有4个 vim-common.i386     ...

  9. P1615 西游记公司

    题目背景 一道极其无厘头的题目 题目描述 事情是这样的:西游记中的孙沙猪(孙杀猪)三徒弟在西天取经之后开始进入厦门大学经贸系学习经济,在1个小时的学习后,他们用暴力手段毕业了.然后,他们创办了三个公司 ...

  10. shutil模块详解2

    1.shutil.make_archive() 实际上是调用了两个模块来实现压缩打包的功能. zipfile和tarfile两个模块,shutil的两个封装的模块. zip是压缩文件,文件内存会变小, ...