Express工作原理和源碼分析一:創建路由


Express是一基於Node的一個框架,用來快速創建Web服務的一個工具,為什么要使用Express呢,因為創建Web服務如果從Node開始有很多繁瑣的工作要做,而Express為你解放了很多工作,從而讓你更加關注於邏輯業務開發。舉個例子:

創建一個很簡單的網站:

1. 使用Node來開發:

var http = require('http');
var url = require("url");

http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/plain'
    });
    var url_str = url.parse(req.url,true);
    res.end('Hello World\n' + url_str.query);
}).listen(8080, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8080/');

這是一個簡單的 hello world,運行以后訪問http://127.0.0.1會打印相關字符串,這是最普通的頁面,但實際上真正的網站要比這個復雜很多,主要有:

(1) 多個頁面的路由功能

(2) 對請求的邏輯處理

那么使用node原生寫法就要進行以下處理

// 加載所需模塊
var http = require("http");

// 創建Server
var app = http.createServer(function(request, response) {
  if(request.url == '/'){
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Home Page!\n");
  } else if(request.url == '/about'){
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("About Page!\n");
  } else{
    response.writeHead(404, { "Content-Type": "text/plain" });
    response.end("404 Not Found!\n");
  }
});

// 啟動Server
app.listen(1984, "localhost");

代碼里在createServer函數里傳遞一個回調函數用來處理http請求並返回結果,在這個函數里有兩個工作要做:

(1)路由分析,對於不同的路徑需要進行分別處理

(2)邏輯處理和返回,對某個路徑進行特別的邏輯處理

在這里會有什么問題?如果一個大型網站擁有海量的網站(也就是路徑),每個網頁的處理邏輯也是交錯復雜,那這里的寫法會非常混亂,沒法維護,為了解決這個問題,TJ提出了Connect的概念,把Java里面的中間件概念第一次進入到JS的世界,Web請求將一個一個經過中間件,並通過其中一個中間件返回,大大提高了代碼的可維護性和開發效率。

// 引入connect模塊
var connect = require("connect");
var http = require("http");
 
// 建立app
var app = connect();
 
// 添加中間件
app.use(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Hello world!\n");
});
// 啟動應用 http.createServer(app).listen(1337);

但是TJ認為還應該更好一點,於是Express誕生了,通過Express開發以上的例子:

2. 使用Express來開發:

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});
app.get('/about', function (req, res) { res.send('About'); });
var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); });

從Express例子可以看出,使用Express大大減少了代碼函數,而且邏輯更為簡潔,所以使用Express可以提高開發效率並降低工程維護成本。

首先Express有幾個比較重要的概念:路由,中間件和模版引擎

開發人員可以為Web頁面注冊路由,將不同的路徑請求區分到不同的模塊中去,從而避免了上面例子1所說的海量路徑問題,例如

var express = require("express");
var http = require("http");
var app = express();
 
app.all("*", function(request, response, next) {
    response.writeHead(404, { "Content-Type": "text/plain" });
    next();
});
 
app.get("/", function(request, response) {
    response.end("Welcome to the homepage!");
});
 
app.get("/about", function(request, response) {
    response.end("Welcome to the about page!");
});
 
app.get("*", function(request, response) {
    response.end("404!");
});
 
http.createServer(app).listen(1337);

開發人員可以為特定的路由開發中間件模塊,中間件模塊可以復用,從而解決了復雜邏輯的交錯引用問題,例如

var express = require('express');
var app = express();

// 沒有掛載路徑的中間件,應用的每個請求都會執行該中間件
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// 掛載至 /user/:id 的中間件,任何指向 /user/:id 的請求都會執行它
app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

// 路由和句柄函數(中間件系統),處理指向 /user/:id 的 GET 請求
app.get('/user/:id', function (req, res, next) {
  res.send('USER');
});


var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

同時Express對Request和Response對象進行了增強,添加了很多工具函數。

其中路由和中間件還有很多細節問題,可以參考http://www.expressjs.com.cn/來學習

下面我們來看看Express的工作原理

我們首先來看看Express的源碼結構:

 

 

 

 

 

 

 

 

 

 

簡單介紹下:

Middleware:中間件

init.js 初始化request,response

query.js 格式化url,將url中的rquest參數剝離, 儲存到req.query中

Router:路由相關

index.js: Router類,用於存儲中間件數組

layer.js 中間件實體類

route.js route類,用於處理不同Method

 

Application.js 對外API

Express.js 入口

Request.js 請求增強

Response.js 返回增強

Utils.js 工具函數

View.js 模版相關

現在看不明白沒關系,可以先看看后面的解釋然后再回頭看就明白了:

我們前面有說道路由和中間件,那么我們就需要有地方來保存這些信息,比如路由信息,比如中間件回調函數等等,express中有一個對象Router對象專門用來存儲中間件對象,他有一個數組叫stack,保存了所有的中間件對象,而中間件對象是Layer對象。

Router對象就是router/index.js文件,他的代碼是:

Router對象的主要作用就是存儲中間件數組,對請求進行處理等等。

 

 

Layer對象在router/layer.js文件中,是保存中間件函數信息的對象,主要屬性有:

源碼見:

這里面的細節先不多考慮,只需要了解關鍵的信息path,handler和route

handler是保存中間件回調函數的地方,path是路由的url,route是一個指針,指向undefined或者一個route對象,為何會有兩種情況呢,是因為中間件有兩種類型:

(1)普通中間件:普通中間件就是不管是什么請求,只要路徑匹配就執行回調函數

(2)路由中間件:路由中間件就是區分了HTTP請求的類型,比如get/post/put/head 等等(有幾十種)類型的中間件,就是說還有區分請求類型才執行。

所以有兩種Layer,一種是普通中間件,保存了name,回調函數已經undefined的route變量。

另外一種是路由中間件,除了保存name,回調函數,route還會創建一個route對象:

route對象在router/route.js文件中,

我們看到route對象有path變量,一個methods對象,也有一個stack數組,stack數組其實保存的也是Layer對象,這個Layer對象保存的是對於不同HTTP方法的不同中間件函數(handler變量)。

也許你會問,這個route的數組里面的Layer和上面router的數組里面的Layer有何不同,他們有一些相同之處也有一些不同之處,主要是因為他們的作用不同:

相同之處:他們都是保存中間件的實例對象,當請求匹配到指定的中間件時,該對象實例將會觸發。

不同之處:

Router對象的Layer對象有route變量,如果為undefined表示為普通中間件,如果指向一個route對象表示為路由中間件,沒有method對象。而route對象的Layer實例是沒有route變量的,有method對象,保存了HTTP請求類型。

所以Router對象中的Layer對象是保存普通中間件的實例或者路由中間件的路由,而route對象中的Layer是保存路由中間件的真正實例。

我們來看個例子,加入有段設置路由器的代碼:

app.use("/index.html",function(){ //此處省略一萬行代碼});

app.use("/contract.html",function(){ //此處省略一萬行代碼});
app.get("/index.html",function(){ //此處省略一萬行代碼});
app.post("/index.html",function(){ //此處省略一萬行代碼});
app.get("/home.html",function(){ //此處省略一萬行代碼});

代碼中注冊了2個普通中間件about.html和contract.html,兩個路由中間件,index.html和home.html,對index.html有get和post兩種中間件函數,對home.html只有get中間件函數,在內存中存儲的形式就是:

 

我們上面看到了幾種注冊中間件的方式,下面就來介紹下路由器的幾個動作邏輯:

 route對象:

router.METHOD(path,callback);//METHOD是HTTP請求方法(get/post等),他的實現過程在這里:

methods變量是一個數組包含了幾十個http請求類型,這段代碼給route對象添加了幾十個方法,主要邏輯就是創建一個Layer對象,保存中間件函數對象和Method方法,添加到route的stack數組中去。

我們再來看看Router對象的方法:

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires middleware functions');
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }

    // add the middleware
    debug('use %s %s', path, fn.name || '<anonymous>');

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};

這個就是app.use的實現方法,實際上app.use就是調用了router.use,后面詳細介紹,先看看這個方法做了什么,當我們調用app.use(function(){XXX});的時候,這里的函數首先判斷了參數類型,看看有沒有path傳遞進來,沒有path就是"/"有的話保存到path變量,然后對后面的所有中間件函數進行了以下處理:

創建了一個layer對象,保存了路徑,中間件函數並且設置了route變量為undefined,最后把這個變量保存到router的stack數組中去,到此一個普通中間件函數創建完成,為何要設置route變量為undefined,因為app.use創建的中間件肯定是普通中間件,app.METHOD創建的才是路由中間件。

當我面調用app.get("",function(){XXX})的時候調用的其實是router對象的route方法:

proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

route方法也創建了一個layer對象,但是因為本身是路由中間件,所以還會創建一個route對象,並且保存到layer的route變量中去。

現在我們總結一下:

1. route對象的主要作用是創建一個路由中間件,並且創建多個方法的layer保存到自己的stack數組中去。

2. router對象的主要作用是創建一個普通中間件或者路由中間件的引導着(這個引導着Layer對象鏈接到一個route對象),然后將其保存到自己的stack數組中去。

所以route對象的stack數組保存的是中間件的方法的信息(get,post等等)而router對象的stack數組保存的是路徑的信息(path)

好了,說完了這些基礎組件,下面說一下真正暴露給開發者的對外接口,很顯然剛才說的都是內部實現細節,我們開發者通常不需要了解這些細節,只需要使用application提供的對外接口。

 

application在application.js文件下,主要保存了一些配置信息和配置方法,然后是一些對外操作接口,也就是我們說的app.use,app.get/post等等,有幾個重要的方法:

app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate app.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset));

  if (fns.length === 0) {
    throw new TypeError('app.use() requires middleware functions');
  }

  // setup router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }

    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        req.__proto__ = orig.request;
        res.__proto__ = orig.response;
        next(err);
      });
    });

    // mounted an app
    fn.emit('mount', this);
  }, this);

  return this;
};

我們看到app.use在進行了一系列的參數處理后,最終調用的是router的use方法創建一個普通中間件。

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

同route一樣,將所有的http請求的方法創建成函數添加到application對象中去,從而可以使用app.get/post/等等,最終的效果是調用router的route方法創建一個路由中間件。

所有的方法再通過express入口文件暴露在對外接口中去。而middleware中的兩個文件是對application做的一些初始化操作,request.js和response.js是對請求的兩個對象的一些增強。

 

到此我們基本了解了express的創建路由和中間件函數的基本原理,下一篇我們來了解如何調用這些路由和中間件函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM