nodejs 實踐:express 最佳實踐(三) express 解析


nodejs 實踐:express 最佳實踐(三) express 解析

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

知其然,並知其所以然這是程序員的天性。所以把常用的模塊拿出來看看,看看高手怎么寫的,學習其想法,讓自己的技術能更近一步。

引言

前面一篇文章都已經研究過 express 3.x 中的核心框架 connect 的代碼,這一陣來看看 express 3.x 和 express 4.x 的核心代碼是怎樣,特別關注兩個部分:

  1. express 3.x 中 connect 的使用。
  2. express 4.x 中 的流程

解析

要使用 express 首先要明白幾個概念:

  • application (3.x)
  • subApplication (3.x)
  • router (3.x)
  • middleware (3.x)
  • route (3.x)
  • layer (4.x)

首先 application 就是指的一個 express 實例,這個實例可以有自己環境變量等,而 subApplication 是指的 application 下面又會嵌套 一個 express 實例對像。

每一個 application 中都只有一個 router 路由對像,這個對像管理這個 application 下面有所有的 subApplication, middleware 和 route.

subApplication 不說了, middleware 的表現形式在 express 中有三種: fn(req, res, next)fn(err, req, res, next), fn(req, res)

第一種是正常的中間件:你對數據進行處理,然后調用 next 調用下一個中間件。

而第二種是錯識中間件: 你出錯口才調用它,而且必須是 四個參數,不能多也不能少。

第三種也是對數據進行處理,但是沒有 next 說明數據到里結束。

route 就指的某個具體的路由,就是比如:get, post, delete, put, all等方法,這類方法能新建一個路由的對像。

layer 是 4.x 新出來的概念,這個也簡單,就是方法的執行體。所謂的執行體,在 3.x中, subApplication 調用 handle, route調用 handle, middleware 調用自己,而在 4.x 中,這些都變成了 layer ,統一起來了。 請求來了, 就遍歷 layer 看看哪個 layer匹配了路由,然后執行 layer.handle_request 方法。

上面說了這么多就是說 一個 application 就是由 subApplication, middleware, route 組成的, 這些才是真正的有執行體的地方, 下面說下, 這幾個部分怎樣加入到一個 application 中去的。

subApplication 加到入到 application 一般是調用 use 方法:

var userCenterApp = express()

var app = express()

app.use('/user', userCenterApp);

// app.use(subApplication);

在上面代碼中, /user 是指的 子應用的 掛載點,可以理解成,這個子應用的所有的方法,都是在 /user 這個 url 之下執行的。

如果沒有第一個參數, 那是個子應用的掛載點默認是 / 可以理解成根應用。

這其中,當子應用掛載成功到父應用上時,子應用會發出一個 mount 事件,express 會在默認情況下,把父應用的 settings, engines 等設置都拷過來。

  this.on('mount', function onmount(parent) {
    // inherit trust proxy
    if (this.settings[trustProxyDefaultSymbol] === true
      && typeof parent.settings['trust proxy fn'] === 'function') {
      delete this.settings['trust proxy'];
      delete this.settings['trust proxy fn'];
    }

    // inherit protos
    // 復制屬性到 子 express 的 request 和 response 中去
    setPrototypeOf(this.request, parent.request)
    setPrototypeOf(this.response, parent.response)
    setPrototypeOf(this.engines, parent.engines)
    setPrototypeOf(this.settings, parent.settings)
  });

如果你需要子應用與父應用有不同的設置,可以監聽這個事件,可以這樣做:

var subApp = express();

subApp.on('mount', function(parent) {
  // dosomething
  delete parent.locals.settings; // 不繼承父 App 的設置
  Object.assign(app.locals, parent.locals);
})

通常我們都不對 subApplication 進行設置,直接把父級 locals 變量直接合並過來。

middleWare 加入到 application 中也是使用 use 方法:

var app = express();

app.use(function(req, res, next) {
//正常使用
})

app.use(function(next, req, res, next) {
//錯誤處理
})

app.use(function(req, res) {
// 到此為止
})

app.use('/user', function(req, res, next) {
//針對 /user 下的所有請求都調用此處方法
})

一般來說 錯誤處理中間件都會放在所有的中間件的最后面,這會有兩個處理中間件,一個是針對客戶端不可知路由,一個是針對服務器錯誤。

app.all('*', function clientError(req, res, next) {
  res.rend('404');
})

app.use(function serverError(err, req, res, next) {
  res.rend('404);
})

router 加入到 application 中也是使用 use 方法:

var router = express.Router()

var app = express();

router.get('/app', fn);

app.use(router); // 掛載到 根目錄 / 下
app.use('/user', router); // 掛載到 /user 下


route 加入到 application 中有兩種方法:app, router

// 通過 app 使用

// 這樣使用
app.get('/user', fn1, fn2)
app.post('/user', fn3, fn4)
app.delet('/user', fn5, fn6)

// 也可以這樣使用
var route = app.route('/user');

route.get(fn1, fn2)
     .post(fn3, fn4)
     .delete(fn5, fn6)

在 router 中,也可以這樣使用:

var router = express.Router()

router.get('/user', fn1, fn2)
router.post('/user', fn3, fn4)

// 另一種寫法
var route = router.route('/user')

route.get(fn1, fn2)
     .post(fn3, fn4)

前面 route 中的 fn*,其實也是中間件,遵守中間件的使用方法。

而 express 中核心的路由循環,就在 _router.handle 中,如下:

proto.handle = function handle(req, res, out) {
  var self = this;

  // some thing
  next();

  function next(err) {
    var layer;
    var match;
    var route;

    // 在這里每次請求會遍歷所有下一個路由,直到 match 成功后,會跳出循環
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path); // 這是核心方法
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      // 沒有route 的 layer, 會匹配 match === true, 然后再下面再執行
      if (!route) {
        // process non-route handlers normally
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      var method = req.method

      // 如果有路由,則看看有沒有該方法如 get post 方法。
      var has_method = route._handles_method(method);


      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {

        match = false;
        continue;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }
    // store route for dispatch on change
    if (route) {
      req.route = route;
    }

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      if (route) {
        return layer.handle_request(req, res, next);
      }
      // 移除多余的匹配
      trim_prefix(layer, layerError, layerPath, path);
    });
  }

  function trim_prefix(layer, layerError, layerPath, path) {

    // somenthing
    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }

流程就在,是否匹配路由,是否route中有方法,然后執行相應的方法上面。

調用流程: app() ===> app.handle ===> this._router.handle ===> if layer.match(path) ==> layer.handle_request

而 app.use 方法就是調用 _router.use 方法:

// app.use
 fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) { // 如果只是一個函數,直接加入到 route 中
      return router.use(path, fn);
    }

    // 下面針對的是一個子 express 的應用,就是指有 handle 的應用

    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) {
        setPrototypeOf(req, orig.request) // 這里每次都把屬性復制過來,就是怕第三方的把屬性給改了,之后讓框架崩潰
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

router.use 方法

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

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

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

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

    layer.route = undefined; // 這個layer是沒有路由的

    this.stack.push(layer);
  }

  return this;
};

總結

把 express 中的 middleware, application, router, route 概念弄清楚了,express 的用法也就清楚了。

另外,我在解析 express 代碼時,也運行了 express 的代碼, 同時打斷點進行調試,並且在關鍵的地方也有注釋,如果有需要,也可以從這個地址進行下查看:

study-express

看代碼主要是弄清楚具體的流程,和實現思想,給自己的架構添加想法。

下一篇要講講,express 中的最佳實踐,包括項目規划, 目錄結構,中間件使用,錯誤中間件,promise,co,await/await,限流,重寫,子域名等。


免責聲明!

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



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