渐进式Express源码学习
关注公众号
获取最新福利
微信公众号

渐进式Express源码学习

时间: 12/20/2017作者: ll浏览量: 1497

1. 万物归宗

1.1 预期特性和预期用法

这篇文章我们实现一个最最基础的Web框架,功能包括

  • 默认响应客户端请求
  • 当请求到来时,返回给用户一个字符串

具体的用法如下(我们用my-express表示这个框架)

const express = require('../index.js')
const app = express()
app.listen(3000)

1.2 源码及讲解

首先在Node中,和网络请求相关的模块是http,我们可以用http搭建一个简单的web服务器,如下代码

const http = require('http')
const server = http.createServer(function (req,res) {
    res.end('Response From Server')
})
server.listen(3000)

上面的代码中,我们调用http模块的createServer函数,传入一个相应请求的函数。这个就是一个简单的Web服务,非常好理解。Node文档里对createServer函数有说明。

基于上面的代码,我们稍微做下调整,就是我们第一个Express的源码,如下

const http = require('http')
const mixin = require('merge-descriptors')
module.exports = function createServer() {
  const app = function (req, res) {
    res.end('Response From Server')
  }
  mixin(app, proto, false)
  return app
}
const proto = Object.create(null)
proto.listen = function (port) {
  const server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

代码开头,我们引入http模块和merge-descriptors模块。http模块是Node原生模块不多说,merge-descriptors是第三方模块,主要是通过descrptors合并对象,他的用法是

var thing = {
  get name() {
    return 'jon'
  }
}

var animal = {
}

merge(animal, thing)

animal.name === 'jon'

更多关于merge-descriptors的实现可以看https://github.com/component/merge-descriptors

然后我们exports是一个函数,用户调用之后,会拿到一个对象,这个对象本身是一个函数,并且还有一个listen方法。当调用app.listen的时候,实际上就调用了http模块的createServer函数,然后把自己当做requestListener传入

1.3 动手实验

目前我们整个项目的文件结构是 image.png 我们首先通过命令node example/index.js运行样例 然后我们通过下面命令来测试我们的程序是否正常工作

curl http://localhost:3000

结果如图 image.png 所以我们的程序是正常工作了,我们第一个版本的Express也实现好了。现实的Express还有很多其他功能,我们一步一步添加。

1.4 本文总结

本文实现了一个最基础的Express,他能默认响应客户端的请求。本质上他是从http模块衍生来的,同样的,express也是从http衍生过来的。

2. 道士下山

2.1 目标特性和目标用法

这篇文章我们在第一篇文章的基础上,实现一个稍微加强版的Express,功能包括

  • 自定义处理用户的请求
  • 区分请求方法,对不同请求方法由不同的函数处理,例如GET请求

具体的用法如下(我们用my-express表示这个框架)

const express = require('../index.js')
const app = express()
// 处理GET请求
app.get(function(req, res) {
  res.end('You send GET request')
})
// 处理POST请求
app.post(function(req, res) {
  res.end('You send POST request')
})
// 处理PUT请求
app.put(function(req, res) {
  res.end('You send PUT request')
})
// 处理DELETE请求
app.delete(function(req, res) {
  res.end('You send DELETE request')
})

app.listen(3000)

2.2 源码及讲解

先展示下目前的项目结构 image.png 也就直说,我们这篇文章要实现的express总共有两个源文件,一个是express,一个是router文件夹下的layer。

注意看下面的讲解时要对照代码,先上一下express.js的源码,然后一点点讲解 image.png

首先看第33行到37行

image.png 既然我们对外提供get, post, put等方法,那我们就要给proto赋值,这个实现在33行。methods使我们引入的一个模块,他提供了http中常用的method,如GET, POST, PUT等,在33行到37行里,我们对app.get, app.post, app.put等进行了赋值。即创建了一个layer,然后push到 handles中

再看6-12行

image.png 对比下上篇文章的代码 image.png 和上之前不同的是,这次的入口函数中,我们不是直接返回用户一个信息,而是调用自身的handle函数,那么handle函数也许就是关键。handle函数在25-31行 image.png 很容易理解,在这里,我们把handles里面存储的layer全部拿出来,然后进行遍历调用。

到这里,我们大概可以知道实现原理:当用户调用app.get的时候,我们把处理函数保存到handles里面,然后请求来的时候,我们就遍历调用这些函数。

Layer封装了这些处理函数,如下是Layer的源码 image.png 每个Layer保存了一个method,handle。他的handle_method函数来判断这个请求的方法是不是和自己一致的,如果一致,就调用handel去处理,如果不一致,就直接返回

2.3 动手实验

我们首先通过命令node example/index.js运行样例 然后我们通过下面命令来测试我们的程序是否正常工作

curl -X GET http://localhost:3000
curl -X POST http://localhost:3000
curl -X PUT http://localhost:3000
curl -X DELETE http://localhost:3000

结果如图 image.png

3. 初露锋芒

3.1 目标特性和目标用法

这篇文章我们在第二篇文章的基础上,实现一个稍微加强版的Express,功能包括

  • 可以给一个方法添加多个中间件
  • 引入next参数

具体的用法如下 image.png

3.2 源码及讲解

先展示下目前的项目结构 image.png 和上篇文章不同的是,我们引入了route.js。目前源文件总共有三个,express.js, router/layer.js, router/route.js

这篇中,我们实现的大概思路是每一个layer对应一个中间件,用Route对象来保存layer。当请求到来时,调用Route的dispatch函数进行处理

上面的这个文字不理解没关系,看完代码就懂了,先看experss.js 和上篇文章相比,区别在两个函数,一个是init函数,一个是handle函数 image.png

对比上下篇文章 image.png 也就是说express.js里并没有接触Layer,而是初始化了一个Route,当请求到达是,调用route.dispatch进行处理。同样app.get等这些函数,也是调用Route对应的get函数。所以关键就是Route.js。先看下他的代码 image.png 在Route的构造函数里,Route保存了一个stack,保存了一个methods数组。当我们调用app.get(fn1, fn2)时,实际上调用的是route.get(fn1, fn2)。这部分实现在34-43行。对每一个handler,我们的做法是创建一个layer,然后push到route.stack中。

前方高能,重点是dispatch函数,这个地方要理解准确,实现很巧妙

当请求到来时,触发Route.dispatch函数,在dispatch函数中(20-32行),我们看看他是怎么做的。 首先他定义了一个计数器idx,定义了一个next函数。进入next之后,首先触发fn1, 传入的参数分别是req, res, next。注意最后这个next就是这个next函数。以我们自己的example做讲解

app.get(function (req, res, next) {
  req.user = {
    name: 'foo'
  }
  next()
})

上面代码中的next,其实就是dispatch的next函数。当我们在逻辑结尾调用next(), 其实也就是调用了dispatch中的next,从而idx 增加1 ,触发下一个layer的handle函数,直到我们不调用next(), 或者layer全部处理了

3.3 动手实验

我们首先通过命令node example/index.js运行样例 然后我们通过下面命令来测试我们的程序是否正常工作

curl -X GET http://localhost:3000

结果如图

image.png

按我们的预期工作

4. 如虎添翼

4.1 目标特性和目标用法

这篇文章里,我们在第三篇文章的基础上,实现一个稍微加强版的Express,功能包括

  • 引入path,即app.get(‘user’, handler)
  • 不同path由不同的函数来处理

这篇文章要实现的express的期望用法如下

const express = require('../index.js')
const app = express()

app.get('/foo', function (req, res, next) {
  res.end('Welcome to GET /foo')
})

app.get('/bar', function (req, res, next) {
  res.end('Welcome to GET /bar')
})

app.post('/foo', function (req, res, next) {
  res.end('Welcome to POST /foo')
})

app.listen(3000)

4.2 源码及讲解

Path的验证,核心是由layer.match进行的路由判断 为了实现这个功能,我们共需要作出4个改进

  • lib/layer.js
    • 构造函数传入path
    • 增加了match函数
  • lib/route.js
    • 构造函数传入path
  • 新增lib/route/index.js文件
    • express.js
  • 增加.lazyrouter函数
    • 首先看下目前的项目结构

image.png 从图中我们可以看出,我们把layer,route都放到了lib/route文件夹里,里面的index.js就是我们刚才说的新增的lib/route/index.js文件

我们先来看layer文件的变化

主要的变化在两点,第一点是构造函数 image.png 注意,构造函数传入了一个path,然后还调用了pathRegexp函数。而这个pathRegexp函数其实是从path-to-regexp模块来的,他的作用是解析路径做匹配,可以直接看一个例子

const pathRegexp = require('path-to-regexp')
const keys = []
const result = pathRegexp('/foo/:bar', keys)

运行之后,keys和result分别是

result = /^\/foo\/([^\/]+?)\/?$/i
keys = [{name: 'bar', prefix: '/'}]

也就是说,result是一个正则表达式,而keys保存了path中的参数

我们再看layer的第二个变化,就是新增了match函数,如下 image.png

最后两句是关键,调用正则表达式匹配目标path是否和layer的path相符合,并返回布尔值 到这里,我们就清晰了,path是在layer的match函数里匹配的。那match函数是在哪里调用的呢?继续往下看

我们再看lib/route/index.js文件

因为route.js几乎没什么变化,所以我们直接看lib/route/index.js文件 这个文件里,定义的是Router对象 我们的设计思路是:

  • app.get app.post等这类app.verb,最后本质上调用的是Router.verb
  • 当请求来临时,最后调用的是Router.handle 先看Router.verb

image.png 也就是说,当我们调用app.get(path, fn)的时候,Router创建了一个route进行了处理,同时把这个route的dispatch挂载到一个新的layer上,然后把这个layer推送到自己的stack中,大概是这样的结构 image.png

左边的三个layer的handle都是route.dispatch,右边的3个layer的handler才是我们传入app.get的函数

再看handle函数,当有请求时,这个函数会被调用 整体的流程是遍历stack中的layer,然后调用这个layer的handle_request,实际上是调用了layer对应的route的dispatch函数。他是通过一个next递归实现的。 image.png

看36行的matchLayer函数 image.png 到这里我们就明白了,layer的match函数是在这个地方被调用的

5. 全副武装

5.1 目标特性和目标用法

这篇文章我们在第四篇文章的基础上,实现一个稍微加强版的Express,功能包括

  • 可以获取req.params
  • 提供app.param能力

这篇文章要实现的express的期望用法如下

const express = require('../index.js')
const app = express()

app.get('/user/:userId', function (req, res, next) {
  res.end(`Welcome, the user.id = ${req.params.userId} and the user.name is ${req.user.name}`)
})

app.param('userId', function (req, res, next, userId, key) {
  req.user = {
    id: userId,
    name: 'foo'
  }
  next()
})

app.get('/article/:title', function (req, res, next) {
  res.end(`Welcome, the article's title is ${req.params.title}`)
})

app.listen(3000)

6. 独孤求败

6.1 目标特性和目标用法

这篇文章我们在第五篇文章的基础上,实现一个稍微加强版的Express,功能包括

  • next可以向下传递错误对象
  • 错误捕捉

这篇文章要实现的express的期望用法如下

const express = require('../index.js')
const app = express()

app.get('/foo', function handle1 (req, res, next) {
  next(new Error('Bang!'))
}, function handle2 (req, res, next) {
  res.end('Will not go here')
}, function handle3 (err, req, res, next) {
  console.log(`Error Caught! Error message is ${err.message}`)
  next(err)
})

app.get('/foo', function (req, res, next) {
  res.end('Will not go here too')
})

app.use('/foo', function (req, res, next) {
  res.end('Will not go here too')
})

app.get('/foo', function (err, req, res, next) {
  console.log(err.name)
  res.end('Will not go here too')
})

app.use('/foo', function (err, req, res, next) {
  console.log(`Error Caught! Error message is ${err.message}`)
  res.end('Go here')
})

app.listen(3000)

在阅读这篇文章之前,请务必了解express错误处理,例如上面的例子中,你需要知道抛出的错误是在哪个环节捕捉的,否则阅读这个文章会吃力

6.2 源码及讲解

这一节,会引入两个概念,路由中间件和非路由中间件 新的章节,主要有3个变化

  • lib/route/layer.js
    • 增加handle_error
  • lib/route/route.js
    • 修改.dispatch函数
      • 如果有error,调用layer.handle_error
      • 如果没有error,调用layer.handle_request
  • lib/route/index.js
    • 增加use函数
    • 调整handle函数

首先要讲解,路由中间件和非路由中间件。路由中间件,通过app.verb添加,结构是处理函数挂载到layer上,layer推送到route上,route的dispatch函数又挂载到一个新的layer,这个layer再推送到Router的stack中。 结构类似这样 image.png 而对于非路由中间件,直接挂载到layer上,然后推送到Router的stack 结构是这样的 image.png 所以,二者结合后,结构是这样的 image.png 理解了上面这些,我们看具体的代码 首先是lib/route/layer.js image.png 他们的区别是如果你的layer的fn是function(req, res, next) ,调用这个layer的handle_error会直接掉过,只有当这个layer的fn是function(err, req, res, next)才会有效

再看lib/route/route.js image.png 注意第44行到48行,route.dispatch函数会判断是否有error,如果有,调用layer的handler_error函数,这样正常的路由就会被跳过

再看lib/route/index.js image.png 首先是增加了一个use函数,这个函数用来增加非路由中间件,直接创建一个layer,绑定函数后推送到stack

最后,看Router.handle,我们聚焦在next函数 image.png 看代码第55行,这个地方判断是否是路由中间件,如果layer有route属性,说明是路由中间件,否则不是。 image.png 在process_params里也是,如果有错误,调用layer.handle_error,否则调用handle_request。

本小书的代码地址为:grow-to-express

极客教程网
——一个你值得来的网站
编程笔记:学习分享,与君共勉!
电子书:系统完整的学习!
轮子库:介绍各种库、插件的用途
在线工具:极客工具,在线工具,在线运行
前端导航:前端导航,前端资源聚合平台
微信公众号
极客教程(geekjc)
QQ群:
495489065

Copyright © 2018 - ~ All Rights Reserved. Made By ll 备案号:粤ICP备15001588号-4