nodejs 實踐:express 最佳實踐(五) connect解析

nodejs 實踐:express 最佳實踐(五) connect解析

nodejs 發展很快,從 npm 上面的包託管數量就能夠看出來。不過從另外一方面來看,也是反映了 nodejs 的基礎不穩固,須要開發者創造大量的輪子來解決現實的問題。html

知其然,並知其因此然這是程序員的天性。因此把經常使用的模塊拿出來看看,看看高手怎麼寫的,學習其想法,讓本身的技術能更近一步。node

引言

express 是 nodejs 中最流行的 web 框架。express 中對 http 中的 request 和 response 的處理,還有以中間件爲核心的處理流程,很是靈活,足以應對任何業務的需求。程序員

而 connect 曾經是 express 3.x 以前的核心,而 express 4.x 已經把 connect 移除,在 express 中本身實現了 connect 的接口。能夠說 connect 造就了 express 的靈活性。es6

所以,我很好奇,connect 是怎麼寫的。web

爭取把每一行代碼都弄懂。express

connect 解析

咱們要先從 connect 的官方例子開始npm

var connect = require('connect');
var http = require('http');

var app = connect();

// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());

// store session state in browser cookie
var cookieSession = require('cookie-session');
app.use(cookieSession({
    keys: ['secret1', 'secret2']
}));

// parse urlencoded request bodies into req.body
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));

// respond to all requests
app.use(function(req, res){
  res.end('Hello from Connect!\n');
});

//create node.js http server and listen on port
http.createServer(app).listen(3000);

從示例中能夠看到一個典型的 connect 的使用:api

var app = connect()// 初始化

app.use(function(req, res, next) {
    // do something
})

// http 服務器,使用
http.createServer(app).listen(3000);

先倒着看,從調用的地方更能看出來,模塊怎麼使用的。咱們就先從 http.createServer(app) 來看看。服務器

nodejs doc 的官方文檔中能夠知, createServer 函數的參數是一個回調函數,這個回調函數是用來響應 request 事件的。從這裏看出,示例代碼中 app 中函數籤就是 (req, res),也就是說 app 的接口爲 function (req, res)cookie

可是從示例代碼中,咱們也能夠看出 app 還有一個 use 方法。是否是以爲很奇怪,js 中函數實例上,還以帶方法,這在 js 中就叫 函數對象,不只能調用,還能夠帶實例變量。給個例子能夠看得更清楚:

function handle () {
  function app(req, res, next) { app.handle(req, res, next)}

  app.handle = function (req, res, next) {
    console.log(this);
  }

  app.statck = [];

  return app;
}

var app = handle();

app() // ==> { [Function: app] handle: [Function], stack: [] }

app.apply({}) // ==>{ [Function: app] handle: [Function], stack: [] }

能夠看出:函數中的實例函數中的 this 就是指當前的實例,不會由於你使用 apply 進行環境改變。

其餘就跟對象沒有什麼區別。

再次回到示例代碼,因該能夠看懂了, connect 方法返回了一個函數,這個函數能直接調用,有 use 方法,用來響應 http 的 request 事件。

到此爲此,示例代碼就講完了。 咱們開始進入到 connect 模塊的內部。

connect 只有一個導出方法。就是以下:

var merge = require('utils-merge');

module.exports = createServer;

var proto = {};

function createServer() {

  // 函數對象,這個對象能調用,能加屬性
  function app(req, res, next){ app.handle(req, res, next); }
  merge(app, proto); // ===等於調用 Object.assign
  merge(app, EventEmitter.prototype); // === 等於調用 Object.assign
  app.route = '/';
  app.stack = [];
  return app;
}

從代碼中能夠看出,createServer 函數把 app 函數返回了,app 函數有三個參數,多了一個 next (這個後面講),app函數把 proto 的方法合併了。還有 EventEmitter 的方法也合併了,還增長了 route 和 stack 的屬性。

從前面代碼來看,響應 request 的事件的函數,是 app.handle 方法。這個方法以下:

proto.handle = function handle(req, res, out) {
  var index = 0;
  var protohost = getProtohost(req.url) || ''; //得到 http://www.baidu.com
  var removed = '';
  var slashAdded = false;
  var stack = this.stack;

  // final function handler
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  }); // 接口 done(err);

  // store the original URL
  req.originalUrl = req.originalUrl || req.url;

  function next(err) {
    if (slashAdded) {
      req.url = req.url.substr(1); // 除掉 / 以後的字符串
      slashAdded = false; // 已經拿掉
    }

    if (removed.length !== 0) {
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // next callback
    var layer = stack[index++];

    // all done
    if (!layer) {
      defer(done, err); // 沒有中間件,調用 finalhandler 進行處理,若是 err 有值,就返回 404 進行處理
      return;
    }

    // route data
    var path = parseUrl(req).pathname || '/';
    var route = layer.route;

    // skip this layer if the route doesn't match
    if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
      return next(err); // 執行下一個
    }

    // skip if route match does not border "/", ".", or end
    var c = path[route.length];
    if (c !== undefined && '/' !== c && '.' !== c) {
      return next(err); // 執行下一個
    }

    // trim off the part of the url that matches the route
    if (route.length !== 0 && route !== '/') {
      removed = route;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // ensure leading slash
      if (!protohost && req.url[0] !== '/') {
        req.url = '/' + req.url;
        slashAdded = true;
      }
    }

    // call the layer handle
    call(layer.handle, route, err, req, res, next);
  }

  next();
};

代碼中有相應的註釋,能夠看出,next 方法就是一個遞歸調用,不斷的對比 route 是否匹配,若是匹配則調用 handle, 若是不匹配,則調用下一個 handle.

call 函數的代碼以下:

function call(handle, route, err, req, res, next) {
  var arity = handle.length;
  var error = err;
  var hasError = Boolean(err);

  debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);

  try {
    if (hasError && arity === 4) {
      // error-handling middleware
      handle(err, req, res, next);
      return;
    } else if (!hasError && arity < 4) {
      // request-handling middleware
      handle(req, res, next);
      return;
    }
  } catch (e) {
    // replace the error
    error = e;
  }

  // continue
  next(error);
}

能夠看出一個重點:對錯誤處理,connect 的要求 是函數必須是 四個參數,而 express 也是如此。若是有錯誤, 中間件沒有一個參數的個數是 4, 就會錯誤一直傳下去,直到後面的 defer(done, err); 進行處理。

還有 app.use 添加中間件:

proto.use = function use(route, fn) {
  var handle = fn; // fn 只是一個函數的話 三種接口 // 1. err, req, res, next 2. req, res, 3, req, res, next
  var path = route;

  // default route to '/'
  if (typeof route !== 'string') {
    handle = route;
    path = '/';
  }

  // wrap sub-apps
  if (typeof handle.handle === 'function') { // 自定義中的函數對象
    var server = handle;
    server.route = path;
    handle = function (req, res, next) {  // req, res, next 中間件
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0]; // (req, res) // 最後的函數
  }

  // strip trailing slash
  if (path[path.length - 1] === '/') {
    path = path.slice(0, -1);
  }

  // add the middleware
  debug('use %s %s', path || '/', handle.name || 'anonymous');
  this.stack.push({ route: path, handle: handle });

  return this;
};

從代碼中,能夠看出,use 方法添加中間件到 this.stack 中,其中 fn 中間件的形式有兩種: function (req, res, next) 和 handle.handle(req, res, next) 這兩種均可以。還有對 fn 狀況進行特殊處理。

總的處理流程就是這樣,用 use 方法添加中間件,用 next 編歷中間件,用 finalHandle 進行最後的處理工做。

在代碼中還有一個函數很是奇怪:

/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

defer 函數中的 fn.bind.apply(fn, arguments),這個方法主要解決了,一個問題,不定參的狀況下,第一個參數函數,怎樣拿到的問題,爲何這樣說呢?若是中咱們要達到以上的效果,須要多多少行代碼?

function () {
    var cb = Array.from(arguments)[0];
    var args = Array.from(arguments).splice(1);
    process.nextTick(function() {
        cb.apply(null,args);
    })
}

這仍是 connect 兼容之前的 es5 之類的方法。若是在 es6 下面,方法能夠再次簡化

function(..args){ process.nextTick(fn.bind(...args)) }

總結

connect 作爲 http 中間件模塊,很好地解決對 http 請求的插件化處理的需求,把中間件組織成請求上的一個處理器,挨個調用中間件對 http 請求進行處理。

其中 connect 的遞歸調用,和對 js 的函數對象的使用,讓值得學習,若是讓我來寫,就第一個調個的地方,就想不到使用 函數對象 來進行處理。

並且 next 的設計如此精妙,整個框架的使用和概念上,對程序員基本上沒有認知負擔,這纔是最重要的地方。這也是爲何 express 框架最受歡迎。koa 相比之下,多幾個概念,還使用了不經常使用的 yield 方法。

connect 的設計理念能夠用在,相似 http 請求模式上, 如 rpc, tcp 處理等。

我把 connect 的設計方法叫作 中間件模式,對處理 流式模式,會有較好的效果。

相關文章
相關標籤/搜索