靜心打磨手中利刃之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閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容