nodejs 實踐:express 最佳實踐(二) 中間件

express 最佳實踐(二):中間件

第一篇 express 最佳實踐(一):項目結構css

express 中最重要的就是中間件了,能夠說中間件組成了express,中間件就是 express 的核心。下面來說幾個有用的中間件的寫法。html

錯誤處理中間件

這塊中間件很是基礎,分紅兩個維度,第一個維度:客戶端錯誤,服務器端錯誤;第二個維度:頁面錯誤,ajax錯誤。android

之因此分紅兩個維度來講,是由於,客戶端和服務器端處理的錯誤的地方在兩個中間件中,不在一個地方處理。git

客戶端的發出的錯誤只多是:路由錯誤。也就是說是沒有找到該找的頁面和接口,在 express 中就要這樣寫:github

// 在app 中註冊使用
// * 表明的是全部的路由都能匹配
app.all('*', pageNotFound);

// 這樣處理頁面
function pageNotFound (req, res, next) {
  if (req.xhr) {
    return res.status(404).json({
      code: 404,
      message: '抱歉,頁面不存在!'
    });
  }

  res.render('error-404.njk');
}

代碼中對 ajax 和 頁面請求進行單獨的處理,固然你也能夠,在裏面加些邏輯進行處理,不過不建議加到這裏,由於這裏的錯誤頗有多是用戶本身無心輸錯的。web

服務器端的錯誤狀況就多了,頗有多是業務代碼的問題,也有多是參數的問題,服務器端的錯誤就要用 express 處理錯誤的中間件形式:(err, req, res, next) 必須是寫成這樣:ajax

app.all('*', pageNotFound);

// 要放到客戶端問題下面
app.use(serverError);

function serverError (err, req, res, next) {
  if (req.xhr) {
    return res.json({
      code: 500,
      message: 'server error'
    });
  }

  res.render('error-500.njk');

  next(err) // 也能夠不要
}

服務器端也對頁面請求和 ajax 分別進行了處理,只是最後的錯誤沒有吞掉,仍然 next 到下一個中間件。express

這個中間件應該是全部中間件的最後的一個,只應該有一個錯誤處理中間件。npm

express 中最後的一箇中間件是 finalhandler, 若是到這個中間件話,開發環境下會打到網頁上,前提是你沒有 render ,我這裏就不行,若是是生產環境的話,就會打印到控制檯中。有不少第三方工具,就在這裏接入一箇中間件,就能把全部的錯誤都收集起來。json

這部分代碼能在個人項目:github,core 目錄中找到。

用戶鑑權中間件

在網站中,用戶系統是很是重要的一塊,好比用戶中心,只能已經登陸的用戶才能訪問,未登陸的用戶訪問就讓他跳到登陸頁,登陸後跳回原來要訪的頁面。

並非每一個頁面都須要用戶登陸,所以,咱們要作一箇中間件,只要須要用戶登陸的地方加上,他就能實現以上功能。

這個中間件我叫 auth:

function auth(req, res, next) {
    let refer = req.method === 'GET' ? req.get('Referer') : '';

    let loginAPI = helpers.urlFormat('/passport/login', {refer: refer});

    let loginPage = helpers.urlFormat('/passport/login', {refer: req.fullUrl()});

    if (_.isEmpty(req.user) || !req.user.uid || !req.user.uid.isValid()) {
        if (req.xhr) {
            return res.json({
                code: 400,
                message: '抱歉,您暫未登陸!',
                data: {refer: loginAPI}
            });
        }

        return res.redirect(loginPage);
    } 

    next();
};

這個中間件的邏輯就是也分紅 頁面請求和 ajax ,就去判斷用戶信息是否有效,若是有效,就 next(), 若是沒有效,就跳轉到登陸頁。

登陸頁會根據 url 中的 refer 進行登陸成功後的跳轉。

這裏沒有講 session 之類的處理,可使用 memcached,也可使用加密的 cookie。

如何傭使用這個中間件:

// 針對單個路由使用
router.get('/home/account', auth, change)
router.post('/home/order', auth, list)

// 也能夠針對整個模塊使用,這樣整個模塊都須要用戶登陸後才能使用
subApp.use(auth)

子域名中間件

有不少大型的網站,都有子域名。好比: map.baidu.com, blog.leancloud.cn。

通常來講,咱們把 www 叫成主域名,map, blog 叫成子域名,其實 www 也是二級域名。

固然也有三級域名:list.m.yohobuy.com 這個就是有三級域名。

忘了說一級域名:就是 baidu.com 之類的。

express 中對域名有一個設置是專門對於這塊的:

app.set('subdomain offset', 2); // 這裏的設置就是說最多到三級域名

在寫的時候沒有使用 express vhost 中間件,由於這個中間很差使用,設置的時候還要帶全域名。

咱們在處理子域的時候,就在進因此的進入業務邏輯以前進行處理,經過 req.subDomains 改寫 req.url:

function subDomain(req, res, next) {
  if (req.subdomains.length) {
      switch (req.subdomains[0]) {
          case 'www':
          case 'shop':
          case 'new':
          case 'item':
              break;
          case 'guang':
          case 'search': {
              let searchReg = /^\/product\//;

              if (!searchReg.test(req.path)) {
                  if (req.path === '/api/suggest') {
                      req.url = '/product/api/suggest';
                  } else {
                      req.url = '/product/search/index';
                  }
              }
              break;
          }
          case 'list':
          case 'sale':
          default: {
              let queryString = (function() {
                  if (!_.isEmpty(req.query)) {
                      return '&' + qs.stringify(req.query);
                  } else {
                      return '';
                  }
              }());

              req.query.domain = req.subdomains[0];
              if (!req.path || req.path === '/') {
                  req.url = `/product/index/brand?domain=${req.subdomains[0]}${queryString}`;
              } else if (req.path === '/about') {
                  req.url = `/product/index/brand?domain=${req.subdomains[0]}${queryString}`;
              }
              break;
          }
      }
  }
  next();
}

這個就是針對特殊狀況進行處理,固然你也能夠來個簡單的的,要注意處理只能針對有 req.url 進行處理。

url跳轉中間件

當網站大了,有需求作 seo, 要把動態請求改爲僞靜態頁面的時候,這就會有問題,你的模塊都是參數寫好的,你不能由於 url 要改就要變業務代碼,新老的url 都要支持,由於老的 url 都分享出去了,都在用了,不能讓不能使用。

有需求要變化的統一都在這個中間件中進行處理:核心仍是改寫新的url到老的,還有301 重定向。

function (req, res, next) {
    if (req.subdomains.length > 1 && req.subdomains[1] === 'www') {
        return res.redirect(301, helpers.urlFormat(req.path, req.query || '', req.subdomains[0]));
    }

    req.isMobile = /(nokia|iphone|android|ipad|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220)/i.test(req.get('user-agent')); // eslint-disable-line

    if (req.xhr || isJsonp(req)) {
        return next();
    }

    let rules = loadRule(req.subdomains[0]);
    let useRule = _.find(rules, rule => isNeedHandle(req, rule));

    if (!useRule) {
        return next();
    }

    let step1x = stepX(_.partial(step1, req, useRule, _));
    let step2x = stepX(_.partial(step2, req, useRule, _));
    let step3x = stepX(_.partial(step3, req, useRule, _));
    let processAfter = _.partial(getResultStatus, req, useRule, _);
    let processing = _.flow(step1x, step2x, step3x);

    let process = _.flow(processing, processAfter);
    let result = process(req.url);

    if (result.process) {
        if (result.needRedirect) {
            return res.redirect(301, result.url);
        }

        if (result.needNext) {
            req.url = result.url;
            return next();
        }
    }

    return next();
};

這裏是核心模塊,重要的思想都在裏面,rule 中放的規則,按二級域名進行劃分,框架啓動時載入。

限流中間件

這個中間件,主要是解決爬蟲的問題。可是有兩個問題須要解決。咱們的爬蟲是跟據一類的 url , 而不是某一個 url。 也就是文要拿到某個url 對應的 router。第二個問題,是針對整站的統計,而不針對某個具體的頁面,請求次數在哪一個地方進行統計。

第一個問題:
當咱們拿到 req 的時候,就有 req.app.mountpath, 這個就是 subApp 的掛載點; req.route 就是當前的部分路由,所以就把二者拼起來,就能得到當前頁面的完整路由了。

第二個問題:
咱們能夠在頁面進來的時候就進行處理,也能夠在頁面處理完髮送以前處理,還能夠在頁面發送完成後進行處理。

第一種處理方式,把中間件放在第一個中間件,而後對用戶IP和完整路由進行累加,而後判斷頻率進行處理。

第二種處理方式:須要知道頁面何時模板渲染完,也就是調用 render 以後,send 以前,能夠參照 on-rendered,這也是我如今在用的方式。

第三種處理方式:由於 res 是一個寫入流,所以會以一個 finish 方法,能夠在這裏面作文章。

總結

這一部分,主要是針對一些通用的中間件進行了一些講解,主要仍是要理解在 express 中那些能改,那些不能改。從哪重得到參數:
如何改寫 url , 如何得到 router, 如何進行參數處理。

大部分的功能都已經講到了。

該項目的 github

相關文章
相關標籤/搜索