超详细的《Express》核心原理解析(下)

2,351 阅读7分钟

您好,我是愣锤。

欢迎走进Express源码的世界。本篇文章是Express源码解析的下半部分,主要讲解requestresponse对象上核心API的实现原理。本文上半篇《超详细的《Express》核心原理解析(上)》主要讲解Express的中间件/路由架构实现原理,有兴趣的可以作为先前置阅读内容。

本篇解析文章主要讲解如下核心API的实现原理:

  • res.status() - 设置相应码原理
  • res.get()、res.set() - 获取/设置相应头字段原理
  • res.send() - 发送响应数据原理
  • res.sendFile() - 响应文件资源原理
  • res.render() - 响应视图模板原理
  • res.attachment() - 响应附件下载原理
  • req.get() - 获取请求头原理
  • req.path() - 获取请求path原理
  • req.fresh - 获取请求是否过期原理
  • req.query() - 获取请求query参数原理
  • req.body - 获取请求数据原理

res.status()设置相应码原理

res.status()方法主要用于设置http响应对象的响应状态码。

/**
 * 设置http的响应码
 *
 * @param {Number} code
 * @return {ServerResponse}
 * @public
 */
res.status = function status(code) {
  // this就是res对象
  this.statusCode = code;
  // 返回res支持链式调用
  return this;
};

res.status()方法定义在response.js中,实现比较简单,就是直接给http响应对象response设置statusCode。但是要注意的是为什么此处的this指代的是响应对象呢?

我们从expresscreateApplication实现中得知,只是在初始化的时候给app函数对象上挂载了response.js导出的对象上的内容,那这里this也不应该指向res对象,而是应该指向app对象呀。所以,为什么我们可以使用res对象上使用status()方法呢?

原因在于初始化中间件的时候给res对象做了扩展功能,我们回顾下express内置的init中间件的核心逻辑:

function expressInit(req, res, next){
    // 省略...

    // req增加res引用
    req.res = res;
    // res增加req引用
    res.req = req;
    req.next = next;

    /**
     * 扩展req和res对象,支持定义在request.js和response.js中的所有功能,
     * 做法是:
     *  - 设置req的原型对象为app.requset
     *  - 设置res的原型对象为app.response
     * 需要注意的是:虽然修改了req和res的原型对象,但是req和res并未丢失原来的原型对象,
     * 原因是在app.request和app.response的实现中是基于正确的原型对象创建的:
     * - var req = Object.create(http.IncomingMessage.prototype)
     * - var res = Object.create(http.ServerResponse.prototype)
     */
    setPrototypeOf(req, app.request)
    setPrototypeOf(res, app.response)

    // ...
  };

从这里可以看到我们在默认中间件执行的时候给reqres对象进行了功能扩展,将request.jsresponse.js对象上的所有方法扩展到了对应的reqres对象。

res.get()、res.get()获取/设置相应头字段原理

res.set()用于设置响应对象的响应头的字段,比如设置Content-Type等等,而res.get()则是从响应头获取相关的字段值。先看下res.set()的具体实现如下:

/**
 * 设置header字段
 * Examples:
 *    res.set('Foo', ['bar', 'baz']);
 *    res.set('Accept', 'application/json');
 *    res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
 * Aliased as `res.header()`.
 * @param {String|Object} field
 * @param {String|Array} val
 * @return {ServerResponse} for chaining
 * @public
 */
res.set =
res.header = function header(field, val) {
  if (arguments.length === 2) {
    var value = Array.isArray(val)
      ? val.map(String)
      : String(val);

    // 如果是content-type字段,则特殊处理
    if (field.toLowerCase() === 'content-type') {
      // content-type不允许是数组
      if (Array.isArray(value)) {
        throw new TypeError('Content-Type cannot be set to an Array');
      }

      // 如果content-type中没有指定charset编码,则添加charset编码
      if (!charsetRegExp.test(value)) {
        var charset = mime.charsets.lookup(value.split(';')[0]);
        if (charset) value += '; charset=' + charset.toLowerCase();
      }
    }

    // 调用http server的response对象的setHeader方法更新header字段
    this.setHeader(field, value);
  } else {
    for (var key in field) {
      this.set(key, field[key]);
    }
  }
  return this;
};

res.set()的核心实现就是根据key/value,调用node原生语法httpresponse对象的setHeader方法设置请求头字段。唯一注意点是对Content-Type进行了特殊处理:如果Content-Type没有设置charset字符集,则利用mime库获取对应的字符集后添加上。

res.get()获取指定的响应头字段值的方法就比较简单了,直接利用http响应对象的getHeader方法获取:

/**
 * 获取响应头指定字段的值
 *
 * @param {String} field
 * @return {String}
 * @public
 */
res.get = function(field){
  return this.getHeader(field);
};

res.send()发送响应数据原理

res.send()是一个很重要的api,主要用于发送响应数据,例如res.status(200).send('Hello world!')先设置响应状态码为200然后响应数据为Hello world。下面我们看其内部是如何实现数据响应的:

/**
 * 发送http响应
 *
 * Examples:
 *     res.send(Buffer.from('wahoo'));
 *     res.send({ some: 'json' });
 *     res.send('<p>some html</p>');
 * @param {string|number|boolean|object|Buffer} body
 * @public
 */
res.send = function send(body) {
  var chunk = body;
  var encoding;
  var req = this.req;
  var type;

  // settings
  var app = this.app;
 
  /**
   * 处理参数,兼容res.send()方法两个参数的旧格式写法
   * 使用旧格式写法时会给出使用新语法的提示
   */
  if (arguments.length === 2) {
    // res.send(body, status) backwards compat
    if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') {
      deprecate('res.send(body, status): Use res.status(status).send(body) instead');
      this.statusCode = arguments[1];
    } else {
      deprecate('res.send(status, body): Use res.status(status).send(body) instead');
      this.statusCode = arguments[0];
      chunk = arguments[1];
    }
  }
  
  // disambiguate res.send(status) and res.send(status, num)
  if (typeof chunk === 'number' && arguments.length === 1) {
    // 如果没有设置Content-Type,res.send(status)则默认使用text/plain
    if (!this.get('Content-Type')) {
      this.type('txt');
    }

    // 对res.send(status)的调用给出新语法提示
    deprecate('res.send(status): Use res.sendStatus(status) instead');
    this.statusCode = chunk;
    // status对应的message
    chunk = statuses[chunk]
  }
  
  // ...省略
}

这里可以看到首先是根据send的参数格式和类型,做了旧语法的兼容,比如旧的语法res.send(status)res.send(status, body)res.send(body, status)

处理完参数之后,紧接着就该对响应内容进一步处理了,代码如下所示:

// 根据res.send()参数的类型做不同的处理
switch (typeof chunk) {
  case 'string':
    // 对应send内容为文本且没有设置Content-Type时默认使用text/html
    if (!this.get('Content-Type')) {
      this.type('html');
    }
    break;
  case 'boolean':
  case 'number':
  case 'object':
    if (chunk === null) {
      chunk = '';
    } else if (Buffer.isBuffer(chunk)) {
      // 如果是buffer数据且没有设置Content-Type,则默认使用application/octet-stream
      if (!this.get('Content-Type')) {
        this.type('bin');
      }
    } else {
      // 如果是布尔、数值、或者对象(非null非二进制流),则交由json方法处理
      // json方法处理完之后最后还是再次调用this.send以字符串的方式处理
      return this.json(chunk);
    }
    break;
}

这里根据send实例的参数数据进行不同的处理:

  • 如果是字符串且没有设置Content-Type则默认设置为text/html
  • 如果是null,则响应的数据为空字符串
  • 如果是Buffer数据且没有主动设置Content-Type则默认设置为application/octet-stream
  • 其他类型都交由this.json()序列化转成字符串之后再次调用this.send()进行处理。

接下来继续往后面看,如果是字符串,或者经由this.json()序列化成字符串之后是如何处理的:

/**
 * 响应的数据是字符串时,指定字符编码为utf8
 * 且对已设置的Content-Type进行容错处理,确保有charset编码
 */
if (typeof chunk === 'string') {
  encoding = 'utf8';
  type = this.get('Content-Type');

  // reflect this in content-type
  if (typeof type === 'string') {
    this.set('Content-Type', setCharset(type, 'utf-8'));
  }
}

通过前面逻辑得知代码能走到这里,首先可以确定的是已经设置好了Content-Type,这时候就是拿到设置好的Content-Type数据再次进行容错处理,调用setCharset确保Content-Type设置了charset编码格式。

我们知道res.send()是自动帮我们计算了Content-Length的,因此接下来的实现逻辑就是计算Content-Length了:

/**
 * etag的生成函数
 * - express的初始化工作时,调用了app.set('etag')
 * - 所以etagFn的生成函数时默认存在的,详细内容可以再翻看初始化时的app.set('etag')部分
 */
var etagFn = app.get('etag fn')
/**
 * 是否要生成ETag响应头
 * - 如果响应头中不包含ETag字段且上面的etagFn存在的话,则应该生成ETag
 * - 默认etagFn存在
 */
var generateETag = !this.get('ETag') && typeof etagFn === 'function'

// 生成Content-Length数据
var len
if (chunk !== undefined) {
  if (Buffer.isBuffer(chunk)) {
    // 如果是buffer数据,则直接获取buffer长度作为Content-Length
    len = chunk.length
  } else if (!generateETag && chunk.length < 1000) {
    // just calculate length when no ETag + small chunk
    len = Buffer.byteLength(chunk, encoding)
  } else {
    // 将字符串转换成buffer数据,再计算Content-Length
    chunk = Buffer.from(chunk, encoding)
    encoding = undefined;
    len = chunk.length
  }

  // 设置Content-Length值
  this.set('Content-Length', len);
}

这里先不看ETag的部分,先看len计算逻辑:

  • 如果chunkBuffer类型数据,则直接使用chunk.length获取buffer长度
  • 如果是小字符串,且也不需要转换成buffer以及生成ETag则直接通过Buffer.byteLength获取字符串的字节长度
  • 如果是大字符串则通过Buffer.from转换成buffer数据再获取长度

这里有个注意点是,对于小于1000字节的字符串数据,没有转换成buffer进行传输,因为不仅要考虑到传输的宽度和速率问题,还要考虑收到数据后的编解码的消耗,要做一个权衡考虑。

通过上面的逻辑,generateETag变量用于判断当前响应是否要生成ETag响应头,判断逻辑是如果响应头没有保护ETag字段且app设置中存在ETag的创建函数,则值为true表示需要生成ETag。接下来我们看是如何生成ETag的:

// 如果需要生成ETag,则创建ETag并添加到响应头中
var etag;
if (generateETag && len !== undefined) {
  if ((etag = etagFn(chunk, encoding))) {
    this.set('ETag', etag);
  }
}

这里是调用etagFn函数来创建ETagetagFn来自于app的设置,一开始应用初始化时默认添加了etagFn函数,该函数背后创建ETag的逻辑是调用etag库实现的,这里不再继续展开。继续往后看:

// 缓存尚未过期,则直接304不返回响应体
if (req.fresh) this.statusCode = 304;

/**
 * 响应码为204和304时移除内容相关的响应头``
 * - 204表示没有内容
 * - 304表示服务器资源没有变化,无需再次传输资源
 */
if (204 === this.statusCode || 304 === this.statusCode) {
  this.removeHeader('Content-Type');
  this.removeHeader('Content-Length');
  this.removeHeader('Transfer-Encoding');
  chunk = '';
}

这里通过req.fresh判断当前缓存数据是否未过期,如果未过期则设置响应状态码为304,然后针对204状态码(没有响应内容)和304状态码(资源未变化不需要重复传输)则直接移除响应的Content-Type等字段,并把响应内容设置为空。req.fresh的逻辑放在解析请求对象的时候讲解。

if (req.method === 'HEAD') {
 /**
  * 处理完请求头相关设置后,
  * 如果是HEAD请求则直接end(),不响应任何实体,只包含了响应头
  */
  this.end();
} else {
  // 响应数据
  this.end(chunk, encoding);
}

return this;

代码到这里,res.send()就结束了,最后支持了HEAD类型的请求,对于HEAD请求只返回响应头数据,不返回响应实体数据。否则的话则直接调用http response对象的end()方法响应数据对象。

划重点!划重点!!划重点!!!

我们最后总结一下res.send()主要做了哪些事情,也就是一个web框架对响应数据的封装要做哪些事情:

  • 根据响应的数据类型指定不同的Content-Type
  • 计算好响应对象的Content-Length
  • 支持ETag缓存是否过期,防止不必要的数据传输
  • 对于204304响应不做实体数据的响应
  • 要支持HEAD请求

res.sendFile()响应文件资源原理

res.sendFile()方法作用主要用于根据指定的文件路径响应给接收端资源文件。下面我们看是如何实现静态文件托管服务的:

var send = require('send');

/**
 * 根据给定的path响应指定的资源文件
 * @public
 */
res.sendFile = function sendFile(path, options, callback) {
  var done = callback;
  var req = this.req;
  var res = this;
  var next = req.next;
  var opts = options || {};
  
  // 省略部分参数处理和类型校验...
  
  // 编码路径
  var pathname = encodeURI(path);
  // 创建文件的可读流
  var file = send(req, pathname, opts);
  
  // 利用sendfile函数响应资源文件
  sendfile(res, file, opts, function (err) {
    if (done) return done(err);
    if (err && err.code === 'EISDIR') return next();

    // next() all but write errors
    if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
      next(err);
    }
  });
}

首先我们要了解一下send库,该库主要作用就是读取指定路径得到文件流,但是我们可以指定各种资源的处理逻辑和各个生命周期钩子的处理逻辑,非常灵活。想了解send库的使用和原理实现可以阅读我的这篇文章《详解《send》源码中NodeJs静态文件托管服务实现原理》。

利用send库读取指定的文件得到文件流后,我们看下是如何处理流的,也就是这里的sendfile函数逻辑:

/**
 * pipe的方式发送文件流
 */
function sendfile(res, file, options, callback) {
  var done = false;
  var streaming;
  
  // ...省略部分钩子函数,后面介绍
  
  file.on('directory', ondirectory);
  file.on('end', onend);
  file.on('error', onerror);
  file.on('file', onfile);
  file.on('stream', onstream);
  // res响应结束(完成、关闭、出错)之后,调用onfinish
  // 注意响应结束或者流读取出错之后,文件流会在send库内部被销毁,不需要手动销毁
  onFinished(res, onfinish);
  
  // 如果传递了响应头参数,则在流读取完成后更新响应头字段
  if (options.headers) {
    // set headers on successful transfer
    file.on('headers', function headers(res) {
      var obj = options.headers;
      var keys = Object.keys(obj);

      for (var i = 0; i < keys.length; i++) {
        var k = keys[i];
        res.setHeader(k, obj[k]);
      }
    });
  }

  // pipe方式将文件流响应给接收端
  file.pipe(res);
}

这里的主要逻辑是拿到调用send得到实例之后,监听一系列事件,最后将读取的流以pipe的形式响应给接收端。这些事件如下:

  • directory事件表示读取的路径是一个文件夹
  • end事件表示流读取结束了
  • error事件表示流读取出错了
  • file事件表示读取的是一个文件
  • stream事件是在通过fs模块创建了可读流之后触发
  • headers事件是在确定了路径能映射到资源之后触发,可以在此阶段添加响应头字段

接下来看各个事件到底做了什么事情?

// 读取的资源是文件夹时的钩子
function ondirectory() {
  if (done) return;
  done = true;

  // 创建一个目标是文件夹的错误,e is dir
  var err = new Error('EISDIR, read');
  err.code = 'EISDIR';
  callback(err);
}
  
// 读取文件流出错的钩子
function onerror(err) {
  if (done) return;
  done = true;
  // 流出错时直接调用callback并传递错误
  callback(err);
}

// 读取文件流结束的钩子
function onend() {
  if (done) return;
  done = true;
  // 读取结束,调用callback
  callback();
}

// 读取的资源是文件的钩子
function onfile() {
  streaming = false;
}
  
// 开始读取流的钩子
function onstream() {
  streaming = true;
}

首先所有的钩子都判断了done的状态,也就是只要有兜底的操作处理过了就不再重复处理了。ondirectory说明读取的是个文件夹则直接创建一个EISDIR类型的错误,onerror说明出错了则直接把错粗传给回调函数,onend说明正常读取结束没有错误直接调用回调。onfileonstream就是打标记当前流的读取状态,是未读还是开始读了。

// res响应结束(完成、关闭、出错)的钩子
function onfinish(err) {
  // 客户端意外断开连接
  if (err && err.code === 'ECONNRESET') return onaborted();
  // 流读取或响应出错
  if (err) return onerror(err);
  // 如果已处理过结束状态,则不再做处理
  if (done) return;

  // 如果res响应结束了,但是还没有处理过callback,说明可能响应出现了意外
  setImmediate(function () {
    // 如果此时流还不处理结束的状态,则说明是连接意外关闭了,则直接onaborted
    if (streaming !== false && !done) {
      onaborted();
      return;
    }

    // 如果已经处理过了则不再继续处理
    if (done) return;
    done = true;
    // 否则调用callback
    callback();
  });
}

// 请求终止
function onaborted() {
  if (done) return;
  done = true;

  // 创建一个请求终止的错误,ECONNABORTED一般表示对方意外关闭了套接字
  var err = new Error('Request aborted');
  err.code = 'ECONNABORTED';
  // 调用callback并传入一个请求意外终止的错误
  callback(err);
}

我们重点看onfinish钩子的逻辑,也就是res响应结束(包括响应完成、关闭和出错)之后做了什么事情。

首先能走到onfinish逻辑说明res响应已经结束了,但是结束有可能是出错了、也有可能是正常结束。因此判断意外断开连接的情况创建一个ECONNRESET错误并调用callback,出错的情况则是的调用callback并传递错误。如果没出错则判断有没有已经调用过callback的操作了,有的话不做任何处理,没有的话则判断流的状态进行一些处理。

监听res响应结束逻辑是用的on-finished库,想了解该库的使用和实现原理的可以阅读我的这篇文章《小而美的《on-finished》源码全解析》。

res.render()响应视图模板原理

利用res.render()可以渲染指定的视图,例如我们在访问/路由时返回index.jade的视图模板,使用代码如下:

const express = require('express');
const router = express.Router();

const app = express();

/**
 * 设置视图引擎
 */
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

/* GET home page. */
router.get('/', function(req, res, next) {
  // 返回src/views/index.jade模板
  res.render('index', {
    title: 'Express',
  });
});

下面看res.render()内部是如何实现的:

// application.js中
res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  
  // 省略参数处理部分...

  // 响应后的callback
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };
  
  // 调用渲染方法
  app.render(view, opts, done);
}

可以看到res.render()渲染方法最终是调用的app.render()方法,并且传入指定的视图模板路径、选项参数和done的逻辑。下面看app.render()方法实现:

/**
 * 根据模板名称渲染对应的视图
 * application.js文件
 */
app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;
  
  // 省略参数合并/处理代码...
  
  // 如果允许使用缓存则取缓存模板
  if (renderOptions.cache) {
    view = cache[name];
  }
  
  // 实例化视图
  if (!view) {
    // 暂时省略实例化view的代码
  }
  
  // 调用tryRender方法渲染
  tryRender(view, renderOptions, done);
}

function tryRender(view, options, callback) {
  try {
    view.render(options, callback);
  } catch (err) {
    callback(err);
  }
}

这里可以看到主要就是判断要不要使用缓存的视图,然后根据需要实例化视图,调用视图实例的render方法进行渲染。接下来看是如何实例化视图的:

// 上述代码的if部分
if (!view) {
  // 获取View类
  var View = this.get('view');
  // 实例化View类
  view = new View(name, {
    defaultEngine: this.get('view engine'),
    root: this.get('views'),
    engines: engines
  });
  
  // 路径不存在则直接报错
  if (!view.path) {
    var err = new Error('...省略');
    err.view = view;
    return done(err);
  }
  
  // 创建缓存
  if (renderOptions.cache) {
    cache[name] = view;
  }
}

这里就是获取View类,然后实例化,然后将实例添加到缓存中。View类的来源是在我们express初始化配置的时候通过this.set('view', View)添加的类,其实现在lib/view.js中:

/**
 * 根据name初始化一个视图
 * @param {string} name
 * @param {object} options
 * * Options:
 *   - `defaultEngine` 默认的模板引擎
 *   - `engines` 所有加载的模板引擎
 *   - `root` 视图模板的跟路径
 * @public
 */
function View(name, options) {
  var opts = options || {};

  // 默认的模板引擎
  this.defaultEngine = opts.defaultEngine;
  // 文件的后缀名
  this.ext = extname(name);
  // 模板文件名称
  this.name = name;
  // 模板的根路径
  this.root = opts.root;
  
  var fileName = name;

  if (!this.ext) {
    // 根据扩展视图引擎获取模板文件的后缀名
    // 例如根据jade引擎获取的是.jade
    this.ext = this.defaultEngine[0] !== '.'
      ? '.' + this.defaultEngine
      : this.defaultEngine;
    // 完整的文件名,name.[ext]
    fileName += this.ext;
  }
  
  // ... 省略代码
}

首先这里根据传入的视图文件名称,尝试获取其文件m名和后缀名,如果不存在后缀名则利用依赖的模板引擎尝试获取,比如指定的jade引擎,则对应获取name.[ext]文件名和后缀名。

// 如果引擎还没有被加载,则调用require加载引擎
if (!opts.engines[this.ext]) {
  // 引擎名称
  var mod = this.ext.substr(1)

  // 加载对应的模板引擎
  var fn = require(mod).__express

  // 将模板引擎添加到engines缓存中
  opts.engines[this.ext] = fn
}

// 存储当前加载的引擎
this.engine = opts.engines[this.ext];

// 查找路径对应的文件,用于判断资源是否存在
this.path = this.lookup(fileName);

紧接着可以看到就是判断引擎有没有加载,没有加载则利用require(引擎名)加载模板引擎,加载后放在opts.engines缓存中,防止后续重复加载引擎。最后判断当前视图路径是否对应资源存在。

而我们前面指定,渲染视图的逻辑是拿到View类的实例后调用的其render方法,那么我们看下View类实例的render方法逻辑:

/**
 * 调用渲染引擎
 * @param {object} options
 * @param {function} callback
 * @private
 */
View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

这里就比较简单了,其实就是调用的刚才加载的视图引擎。最后总结一下整体的渲染逻辑图如下所示:

image.png

res.attachment()响应附件下载原理

如果在http响应中,需要接收端对响应资源进行附件下载并保存到本地的话,需要设置响应头的Content-Disposition字段,这块不清楚的话可以查阅资料。在express中则可以通过res.attachment()快速的实现该功能。接下来我们看其内部实现:

var contentDisposition = require('content-disposition');

/**
 * 指示接收端将响应资源以附件形式下载并保存到本地
 * @param {String} filename
 * @return {ServerResponse}
 * @public
 */
res.attachment = function attachment(filename) {
  // 根据文件类型设置Content-Type
  if (filename) {
    this.type(extname(filename));
  }

  // 利用contentDisposition库生成响应头的Content-Disposition字段值
  // 从而支持附件下载
  this.set('Content-Disposition', contentDisposition(filename));

  return this;
};

可以看到内部就是通过content-disposition库实现的。那么为什么不直接自己拼接字符串呢?因为要考虑到大量的字符编码的问题。对content-disposition库源码实现有兴趣的话,可以查阅我的这篇博文《详解Content-Disposition源码中Node附件下载服务原理》

req.get()获取请求path原理

/**
 * 获取指定的请求头字段值
 * 注意:`Referrer`和`Referer`都是一样的,两者是可互相替换的
 * @param {String} name
 * @return {String}
 * @public
 */
req.get =
req.header = function header(name) {
  // 省略参数校验部分...

  var lc = name.toLowerCase();

  switch (lc) {
    case 'referer':
    case 'referrer':
      return this.headers.referrer
        || this.headers.referer;
    default:
      return this.headers[lc];
  }
};

这块没什么好说的,就是直接从请求对象上的headers上获取指定字段和值,利用的Node的原生语法直接获取。但是注意的是对于ReferrerReferer字段是等价的,可以互相替换。

req.path()实现原理

req.path可以获取req上的urlpathname部分,比如/users?name=jack获取到的是/users,其实现原理如下:

var parse = require('parseurl');

/**
 * 从req上解析path路径
 * @return {String}
 * @public
 */
defineGetter(req, 'path', function path() {
  return parse(this).pathname;
});

/**
 * 在一个对象上创建getter属性的辅助函数
 * @param {Object} obj
 * @param {String} name
 * @param {Function} getter
 * @private
 */
function defineGetter(obj, name, getter) {
  Object.defineProperty(obj, name, {
    configurable: true,
    enumerable: true,
    get: getter
  });
}

这里首先创建了一个defineGetter辅助函数,用于快速在req对象上创建属性,且属性只能读取不能修改。req.path的实现就是利用的parseurl库解析得到的pathname,有兴趣的可以了解下其实现,源码不多。

req.fresh获取请求是否过期原理

req.fresh用于判断响应资源是否还新鲜(还未过期),比如在res.send()内部实现中,就判断当前res是否还新鲜,如果还新鲜的话则直接响应304,接下来我们看req.fresh的内部实现:

/**
 * 检查请求是否还未过期,或者说叫做
 * Last-Modified或ETag是否匹配
 * @return {Boolean}
 * @public
 */
defineGetter(req, 'fresh', function(){
  var method = this.method;
  var res = this.res
  var status = res.statusCode

  /**
   * 只有GET和HEAD请求才存在未过期一说
   */
  if ('GET' !== method && 'HEAD' !== method) return false;

  // 2xx or 304 as per rfc2616 14.26
  if ((status >= 200 && status < 300) || 304 === status) {
    return fresh(this.headers, {
      'etag': res.get('ETag'),
      'last-modified': res.get('Last-Modified')
    })
  }

  return false;
});

响应数据是否还新鲜,针对的是GETHEAD类型的请求,所以首先判断了非GET和非HEAD的请求就直接false了。然后对应[200, 300)、200范围的状态码,调用fresh库进行判断是否还新鲜,有兴趣的可以了解了解。

req.query获取请求query参数原理

req.query的实现都在middleware/query.js文件中实现的,这部分在讲解中间件架构中已经详细的提到了,可以回过头去翻看。

req.body获取请求数据原理

req.body也是一个很重要的API,用于获取解析post的数据,该部分的实现在调用的第三方中间件中实现,类似的API还有一些,暂时不过多结束,在后续的中间件原理分析中会讲解。

结束语

百尺竿头、日进一步,我是愣锤。

如果喜欢本文,欢迎点赞收藏留着以后学习参考。Express依赖了一些第三方模块快速实现的一些功能,如果对这些第三方模块的使用和实现原理感兴趣,欢迎查看下面的这些源码解析: