.9-淺析express源碼之請求處理流程(2)

来源:https://www.cnblogs.com/QH-Jimmy/archive/2018/04/26/8945483.html
-Advertisement-
Play Games

上節漏了幾個地方沒有講。 1、process_params 2、trim_prefix 3、done 分別是動態路由,深層路由與最終回調。 這節就只講這三個地方,案例還是express-generator,不過請求的方式更為複雜。 process_params 在講這個函數之前,需要先進一下path ...


  上節漏了幾個地方沒有講。

1、process_params

2、trim_prefix

3、done

  分別是動態路由,深層路由與最終回調。

  這節就只講這三個地方,案例還是express-generator,不過請求的方式更為複雜。

 

process_params

  在講這個函數之前,需要先進一下path-to-regexp模塊,裡面對字元串的正則化有這麼一行replace:

path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'))
// .replace...
.replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional, offset) {
    // ...
    keys.push({
        name: key,
        optional: !!optional,
        offset: offset + extraOffset
    });
    // ...
});

  這裡會對path裡面的:(...)進行匹配,然後獲冒號後面的字元串,然後作為key傳入keys數組,而這個keys數組是layer的屬性,後面要用。

  另外還要看一個地方,就是layer.mtach,在上一節,由於傳的是根路徑,所以直接從fast_slash跳出了。

  如果是正常的帶參數路徑,執行過程如下:

/**
 * @example path = /users/params
 * @example router.get('/users/:id')
 */
Layer.prototype.match = function match(path) {
    var match

    if (path != null) {
        // ...快速匹配

        match = this.regexp.exec(path)
    }

    if (!match) { /*...*/ }

    // 緩存params
    this.params = {};
    this.path = match[0]

    // [{ name: prarms,... }]
    var keys = this.keys;
    var params = this.params;

    for (var i = 1; i < match.length; i++) {
        var key = keys[i - 1];
        var prop = key.name;
        // decodeURIComponent(val)
        var val = decode_param(match[i]);
        // layer.params.id = params
        if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
            params[prop] = val;
        }
    }

    return true;
};

  根據註釋的案例,可以看出路由參數的匹配過程,這裡僅僅以單參數為例。

  下麵可以進入process_params方法了,分兩步講:

proto.process_params = function process_params(layer, called, req, res, done) {
    var params = this.params;

    // 獲取keys數組
    var keys = layer.keys;

    if (!keys || keys.length === 0) return done();

    var i = 0;
    var name;
    var paramIndex = 0;
    var key;
    var paramVal;
    var paramCallbacks;
    var paramCalled;

    function param(err) {
        if (err) return done(err);
        if (i >= keys.length) return done();

        paramIndex = 0;
        key = keys[i++];
        name = key.name;
        // req.params = layer.params
        paramVal = req.params[name];
        // 後面討論
        paramCallbacks = params[name];
        // 初始為空對象
        paramCalled = called[name];

        if (paramVal === undefined || !paramCallbacks) return param();

        // param previously called with same value or error occurred
        if (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) {
            // error...
        }

        // 設置值
        called[name] = paramCalled = {
            error: null,
            match: paramVal,
            value: paramVal
        };

        paramCallback();
    }

    // single param callbacks
    function paramCallback(err) {
        //...
    }

    param();
};

  這裡除去遍歷參數,有幾個變數,稍微解釋下:

1、paramVal => 請求路徑帶的路由參數

2、paramCallbacks => 調用router.params會填充該對象,請求帶有指定路由參數會觸發的回調函數

3、paramCalled => 一個標記對象

  當參數匹配之後,會調用回調函數paramCallback:

function paramCallback(err) {
    // 依次取出callback數組的fn
    var fn = paramCallbacks[paramIndex++];

    // 標記val
    paramCalled.value = req.params[key.name];

    if (err) {
        // store error
        paramCalled.error = err;
        param(err);
        return;
    }

    if (!fn) return param();
    // 調用回調函數
    try {
        fn(req, res, paramCallback, paramVal, key.name);
    } catch (e) {
        paramCallback(e);
    }
}

  僅僅只是調用在param方法中預先填充的函數。用法參見官方文檔的示例:

router.param('user', function(req, res, next, id) {
    // ...do something
    next();
})

  每當路由參數是user時,就會觸發調用後面註入的函數,其中4個參數可以跟上面源碼的形參對應。雖然源碼提供了5個參數,但是示例只有4個。

 

trim_prefix

  這個就比較簡單了。

  案例還是按照上一節的,假設有這樣的請求:

// app.js
app.use('/user',userRouter);
// userRouter.js
router.get('/abcd',()=>{...});
// client的get請求
path => '/users/abcd'

  此時,內部路由將其分發給了usersRouter,但是在分發之前有一個問題。

  在自定義的路由中,是不需要指定根路徑的,因為在app.use中已經寫明瞭,如果將完整的路徑傳遞進去,在路徑正則匹配時會失敗,這時候就需要進行trim_prefix了。

  源碼如下:

/**
 * 
 * @param   layer       匹配到的layer
 * @param   layerError  error
 * @param   layerPath   layer.path => '/users'
 * @param   path        req.url.pathname => '/users/abcd'
 */
function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
        // 保證路徑後面的字元串合法
        var c = path[layerPath.length]
        if (c && c !== '/' && c !== '.') return next(layerError)

        debug('trim prefix (%s) from url %s', layerPath, req.url);
        // 緩存被移除的path
        removed = layerPath;
        req.url = protohost + req.url.substr(protohost.length + removed.length);

        // 保證移除後的路徑以/開頭
        if (!protohost && req.url[0] !== '/') {
            req.url = '/' + req.url;
            slashAdded = true;
        }

        // 基本路徑拼接
        req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' ?
            removed.substring(0, removed.length - 1) :
            removed);
    }

    debug('%s %s : %s', layer.name, layerPath, req.originalUrl);

    // 將新的req.url傳進去處理
    if (layerError) {
        layer.handle_error(layerError, req, res, next);
    } else {
        layer.handle_request(req, res, next);
    }
}

  可以看出,源碼就是去掉路徑的頭,然後將新的路徑傳到二級layer對象中做匹配。

 

done

  這個最終回調麻煩的要死。

  註意:如果調用了res.send()後,源碼內部會調用res.end結束響應,回調將不會被執行,這是為了防止意外情況所做的保險工作。

  一層一層的來看最終回調的結構,首先是handle方法中的直接定義:

var done = restore(out, req, 'baseUrl', 'next', 'params');

  從方法名可以看出這就是一個值恢復的函數:

function restore(fn, obj) {
  var props = new Array(arguments.length - 2);
  var vals = new Array(arguments.length - 2);

  // 在請求到來的時候先緩存原始信息
  /**
   * props = ['baseUrl', 'next', 'params']
   * vals = ['url','next方法','動態路由的params']
   */
  for (var i = 0; i < props.length; i++) {
    props[i] = arguments[i + 2];
    vals[i] = obj[props[i]];
  }

  return function () {
    // 在請求處理完後對值進行回滾
    for (var i = 0; i < props.length; i++) {
      obj[props[i]] = vals[i];
    }

    return fn.apply(this, arguments);
  };
}

  簡單。

  下麵來看看這個fn是個啥玩意,預設情況下來源於一個工具:

var done = callback || finalhandler(req, res, {
  env: this.get('env'),
  onerror: logerror.bind(this)
});
function finalhandler(req, res, options) {
  // 獲取配置參數
  var opts = options || {}
  var env = opts.env || process.env.NODE_ENV || 'development'
  var onerror = opts.onerror

  return function (err) {
    // ...
  }
}

  在獲取參數後,返回了一個新函數,簡單看一下done的調用地方:

// 遇到router標記直接調用done
if (layerError === 'router') {
  setImmediate(done, null)
  return
}

// 走完了layer匹配
if (idx >= stack.length) {
  setImmediate(done, layerError);
  return;
}

// path為null
var path = getPathname(req);

if (path == null) {
  return done(layerError);
}

  基本上正常情況下就是null,錯誤情況下會傳了一個err,基本上符合node的err first模式。

  進入finalhandler方法:

function done(err) {
  var headers
  var msg
  var status

  // 請求已發送的情況
  if (!err && headersSent(res)) {
    debug('cannot 404 after headers sent')
    return
  }

  // unhandled error
  if (err) {
    // ...
  } else {
    // not found
    status = 404
    msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
  }

  debug('default %s', status)

  // 處理錯誤
  if (err && onerror) {
    defer(onerror, err, req, res)
  }

  // 請求已發送銷毀req的socket實例
  if (headersSent(res)) {
    debug('cannot %d after headers sent', status)
    req.socket.destroy()
    return
  }

  // 發送請求
  send(req, res, status, headers, msg)
}

  原來這裡才是響應的實際地點,在保證無錯誤並且響應未手動提前發送的情況下,調用本地方法發送請求。

  這裡的send過程十分繁雜,暫時不想深究,直接看最終的發送代碼:

function write () {
  // response body
  var body = createHtmlDocument(message)

  // response status
  res.statusCode = status
  res.statusMessage = statuses[status]

  // response headers
  setHeaders(res, headers)

  // security headers
  res.setHeader('Content-Security-Policy', "default-src 'self'")
  res.setHeader('X-Content-Type-Options', 'nosniff')

  // standard headers
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))

  // 只請求頁面的首部
  if (req.method === 'HEAD') {
    res.end()
    return
  }

  res.end(body, 'utf8')
}

  因為註釋都解釋的很明白了,所以這裡簡單的貼一下代碼,最終調用的是node的原生res.end進行響應。

  至此,基本上完事了。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 轉載自:http://blog.sina.com.cn/s/blog_7b9d64af0101dfg8.html 最近用到搜索功能。於是,經過不斷的研究,終於,有點懂了。 那就來總結一下吧,好記性不如爛筆頭! 搜索,無疑可以使用UISearchBar控制項!那就先瞭解一下UISearchBar控制項吧! ...
  • 再打開手機app的時候,最先映入我們眼帘的是一個覆蓋手機全屏的歡迎界面,在這個界面顯示出來的時候整個手機屏幕只會顯示這一個界面,上面的標題欄,以及手機最頂端的狀態欄都會消失,只有歡迎頁面結束跳轉到其他頁面時,標題欄和狀態欄才回又顯示出來。 現在我們就來製作一下歡迎界面。 歡迎界面的設置 首先,我們需 ...
  • 新編html網頁設計從入門到精通共分為21章,全面系統地講解了html的發展歷史及4.0版的新特性、基本概念、設計原則、文件結構、文件屬性標記、用格式標記進行頁面排版、使用圖像裝飾頁面、超鏈接的使用、使用表格組織頁面、使用多媒體美化頁面、創建多框架頁面、動態網頁的製作、使用層疊樣式表(css)美化頁 ...
  • HTML5已經廣泛應用於各智能移動終端設備上,而且絕大部分技術已經被各種最新版本的測覽器所支持:逐一剖析HTML5標準中包含的最新技術,詳細介紹了HTML5新標準中提供的各種API,各種各樣的應用實例,可以直接應用於自己的HTML5程式中。 HTML5移動開發即學即用(雙色)書中幾乎涵蓋了HTML5 ...
  • HTML5權威指南是一本系統學習網頁設計的權威參考圖書。《HTML5權威指南》分為五部分:第一部分介紹學習本書的預備知識和HTML、CSS和JavaScript的最新進展;第二部分討論HTML元素,並詳細說明瞭HTML5中新增和修改的元素;第三部分闡述CSS,涵蓋了所有控制內容樣式的CSS選擇器和屬 ...
  • 《HTML5移動Web開發實戰》提供了應對這一挑戰的解決方案。通過閱讀本書,你將瞭解如何有效地利用最新的HTML5的那些針對移動網站的功能,橫跨多個移動平臺。全書共分10章,從移動Web、設備端配置和優化,變互、響應式設計、設備訪問,調試、性能測試、富媒體等角度出發,包含了60多個實用的示倒,詳細闡 ...
  • `今天看了github上面的某位大佬對原型與原型鏈的文章 講解很透徹` 文章下麵貼地址 prototype 每個函數都有prototype屬性,如下 這個函數的prototype到底是什麼呢,是函數的原型嗎? 其實,函數的prototype都會指向一個對象,這個對象 就是調用改構造函數而創建的 ,也 ...
  • 首先簡單瞭解js的typeof,會返回六種類型 即 number string boolen function object undefined 也就是六種基本數據類型 顯示類型轉換大概有以下幾種: Number() 轉換為number類型 String() 轉換為string類型 Boolean( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...