静心打磨手中利刃之Express

本文出自[Century's World]

不知从什么时候开始,node就开始风靡起来,我们甚至都没有谨慎的研究过他和其他服务器的区别,便开始跃跃欲试。相信很多人会和笔者一样,在接触node服务器的第一时刻,便接触到了express,我有个朋友也说过,学一个东西还是要结合框架来学比较好,当时我便听了话,开始了express+node的学习,当然,express非常顺手,给了我很好的体验。而今天,我们撇开浮躁,静下心来,仔细研究Express框架。

假如没有Express

说到底Express只是一个框架而已,那么,他是一个什么框架呢,我们撇开Express不谈,我们想要完成一个Node的服务,只要如下的代码就可以完成:

var http = require("http");
var server = http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write("Hello World!");
  response.end();
});
server.listen(80);

很简单的几行代码就实现了一个服务器,假如我们的需求只是简单的渲染一个页面的话,我们大可以用这几行代码完成我们想做的事情,其实http这系列的库做了很多事情,非常建议大家回头去看看这一系列Express底层的Api。当然了最后我们还是会用框架的,自己整理的也好,用现在流行的Expres或者Koa也好,目的是为了应对复杂的使用场景,减少重复繁琐的代码。

小小的实现一下

说起Express的特点,大概就是中间件吧,所有的东西都是通过中间件来完成的,那么中间件实现是怎么样的呢,假如我们自己实现一个Express,我们就应该先解决中间件的问题,那让我们来尝试一下实现一个简单的Express。

首先我们抽离createServer的参数

const app = function(req, res) {
  //TODO someting
}
var server = http.createServer(app);
server.listen(port)

这样我们逻辑处理就放到了app里面,有的时候一次运行可能产生多个app的实例,为了分隔环境,我们可以用一个工厂方法或者构造行数生成app

const express = function() {
  return function(req, res) {
  //TODO someting
  }
}

const app = express();
var server = http.createServer(app)
server.listen(port)

接下来我们先实现app.use


function Middleware = function(path, fn) {
  if (!(this instanceof Middleware)) {
    return new Middleware(...arguments);
  }
  this.path = path;
  this.fn = fn;
}

app.use = function(path, ...fns) {
  if (arguments.length == 1) {
    fns = path;
    path = '/';
  }
  for(var index in fns) {
    var fn = fns[index]
    const middleware = Middleware(path, fn);
    this.middlewares.push(middleware);
  }
}

我们定义了一个Middleware的对象,其中有个很多框架中常用的hack,也就是在一个class里面判断是否是使用new来创建对象的,因为使用new来创建对象的时候this一定是当前类的实例,所以我们可以根据this的类型来重定向一个new Function。Middleware用于封装一个中间件层,用于绑定中间件和对应的path。在app.use里面我们将中间件都保存在appmiddlewares属性中,那么接下来,我们就要实现这个中间件的处理过程。

首先我们要增加一个handler的function

function mathMiddleware(url, middleware) {
  // TODO match middleware
}
app.handler = function(req, res) {
  var url = req.url;
  var middlewares = app.middlewares;
  var idx = 0;
  var match = false;
  var middleware;
  function done() {
    //完成这次请求, 比如有error的情况
  }
  function next (err) {
    if (err) {
      done(err)
    }
    while(match === false && idx < middlewares.length) {
      middleware = middlewares[idx];
      match = mathMiddleware(url, middleware)
    }
    middleware.handle(req, res, next);
  }
}

在Middleware中加上一个handle

Middleware.prototype.handle = function(req, res, next) {
  try {
    this.fn(req, res, next);
  } catch(e)  {
    next(e)
  }
}

这只是简单的实现了一下Express的中间件的逻辑,这也大致是Express的实现逻辑,我们知道了这种中间件的实现方式,那么今后在我们的应用中,对某一块逻辑要使用策略模式、装饰着模式或者工厂模式的时候,我们也可以用一个这样的中间件的策略去切割代码,让逻辑的处理变的非常简单而清晰。

别YY了,看一下官方实现

官方的代码其实写的非常易懂,总的来说,给我的感觉,express就是一个微型的框架。

结构
├── application.js
├── express.js
├── middleware
│   ├── init.js
│   └── query.js
├── request.js
├── response.js
├── router
│   ├── index.js
│   ├── layer.js
│   └── route.js
├── utils.js
└── view.js
express.js

这是整个应用的入口,主要的工作的整合了application.js中的关于app的属性,构造了一个函数来输出app,将一些辅助函数输出,比如RouterRequest等等

router

这个类我们应该不陌生,我们在使用express的时候经常使用

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

这个router其实是整个app的核心,包括app.use的中间件实现也是通过Router实现的,以下是源码加上注释

proto.use = function use(fn) {
  var offset = 0; //用于计算参数的个数来获取所有的中间件
  var path = '/'; //设置默认的path为 /
  if (typeof fn !== 'function') { // use('/', fn)的情况
    var arg = fn;
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }
  //通过offset获取所有的中间件
  var callbacks = flatten(slice.call(arguments, offset));
  //当中间件的个数为零的时候,抛出异常
  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }
    debug('use %o %s', path, fn.name || '<anonymous>')
    // 创建一个Layer,类似于之前自己实现的Middleware
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
    layer.route = undefined;
    this.stack.push(layer); //将这个layer加入到栈中
  }
  return this; //返回自己用于链式调用
};

之后会讲到这个Layer类,这是一个中间件的承载物,所有的中间件的处理方法和路径的绑定信息都被封装在这个类的实例中,而Router的use方法即使将这些中间件打包成为一个Layer,然后存储到自己的stack中用于之后使用。
然后是一个Router中的request处理类,用于使用中间件处理请求。

proto.handle = function handle(req, res, out) {
  var self = this; //用self指向老的this
  var idx = 0; // 迭代stack的迭代器
  var protohost = getProtohost(req.url) || '' //获取协议名
  var removed = ''; // 定义删除字段
  var slashAdded = false;
  var paramcalled = {};
  var options = [];
  var stack = self.stack; // layer 的集合
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');
  req.next = next;
  if (req.method === 'OPTIONS') { // 处理Options的请求,跨域问题
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;
  next();
  // 中间件的迭代方法
  function next(err) {
    var layerError = err === 'route' ? null : err;
    //计算req.url, req.baseUrl
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }
    //处理特殊情况,结束这次访问
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }
    //处理特殊情况,结束这次访问, 没有匹配的路由了
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }
    var path = getPathname(req); //获取当前path
    if (path == null) {
      return done(layerError);
    }
    var layer;
    var match;
    var route;
    while (match !== true && idx < stack.length) {
      // TODO: 循环获取匹配的Layer
    }
    // 没有匹配的
    if (match !== true) {
      return done(layerError);
    }
    //TODO: 根据layer注入req.params
    //TODO: 根据需要调用layer.handle_request或者layer.handle_error
  }
};

当然了我省略了一部分代码,给大家大致的介绍了一下router是如何处理中间件的,处理中间件的关键就这么两个函数,一个是use,一个是handlehandle用于以中间件的形式迭代处理请求,use用于注册中间件

application.js

这是一个app的类,定义了app的属性和函数,那么application又和router有什么联系呢,我们从api中可以发现,几乎router有的函数,在app中都可以使用,比如router.use和app.use,router.get和app.get,那么官方的实现中,是怎么实现的呢,是否是简单的继承呢。我们带着疑问去观察这个类,我们会发现application中有个router的属性,在中间件的表现上,application只是一个傀儡,大部分的实现都还是依靠router的,application的中间件的操作都是交由其router来处理的,也就是说app.use()是约等于app.router.use()的。比如:

app.param = function param(name, fn) {
  this.lazyrouter();
  if (Array.isArray(name)) {
    for (var i = 0; i < name.length; i++) {
      this.param(name[i], fn);
    }
    return this;
  }
  this._router.param(name, fn);
  return this;
};

话虽这么说,但是app的router是懒加载的,当调用use之类的函数的时候会判断当前是否已经创建了router,否则会创建一个router。除此之外,还会对参数做一些校验和转换,因此还是推荐不直接使用app.router的方式的。

middleware

(有点无聊)这个middleware目录下只是express内置的两个中间件,一个query是用于在req.query注入url中的query参数的,init是一个初始化的中间件,它把req、res相互引用了一些,并mixin了一些req, res的属性,还有x-powered-by额?默认给express打个广告?

Layer

(有那么点意思)这是一个藏在Router下的对象,用于包装中间件和对应的path。

request&response

(比较无聊)这两个类里面主要是一些api这个和express的核心部分的关系没有那么大,他的主要工作主要集中在封装了一些工具方法,一个方便开发者使用的req, res的属性集合的对象。

看源码有什么用?

总的来说,这次的Express源码之旅是很有帮助的,这是我开始这个源码计划的第一个项目,选择Express的原因是这个框架的代码确实看起来比较简单,不需要编译,其次Express还是我现在用的最多的node服务框架,当然之后会考虑使用koa,所以之后很有可能会带来koa的源码解读。作为一个node服务的框架,这次源码阅读让我更加了解Node的http这个模块的东西,有很多的基础的模块在jshttp中,阅读它们会让我更加理解一些关于http的问题,查看了整个中间件的实现,让我对这种模式豁然开朗,之后希望能够在项目中灵活的运用。对request和response的阅读让我知道了很多之前看文档没有仔细观察到的api。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容