3種常見的路由方式


  本文將會介紹文件路徑、MVC、RESTful三種常見的路由方式 

  --以下內容出自《深入淺出node.js》

 

  1. 文件路徑型

  1.1 靜態文件

  這種方式的路由在路徑解析的部分有過簡單描述,其讓人舒服的地方在於URL的路徑與網站目錄的路徑一致,無須轉換,非常直觀。這種路由的處理方式也十分簡單,將請求路徑對應的文件發送給客戶端即可。如:

// 原生實現
http.createServer((req, res) => {
  if (req.url === '/home') {
    // 假設本地服務器將html靜態文件放在根目錄下的view文件夾
    fs.readFile('/view/' + req.url + '.html', (err, data) => {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(data)
    })
  }
}).listen()

// Express 
app.get('/home', (req, res) => {
  fs.readFile('/view/' + req.url + '.html', (err, data) => {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.status(200).send(data)
  })
})

  1.2. 動態文件

  在MVC模式流行起來之前,根據文件路徑執行動態腳本也是基本的路由方式,它的處理原理是Web服務器根據URL路徑找到對應的文件,如/index.asp或/index.php。Web服務器根據文件名后綴去尋找腳本的解析器,並傳入HTTP請求的上下文。

  以下是Apache中配置PHP支持的方式:  

AddType application/x-httpd-php .php

  解析器執行腳本,並輸出響應報文,達到完成服務的目的。現今大多數的服務器都能很智能 地根據后綴同時服務動態和靜態文件。這種方式在Node中不太常見,主要原因是文件的后綴都是.js,分不清是后端腳本,還是前端腳本,這可不是什么好的設計。而且Node中Web服務器與應用業務腳本是一體的,無須按這種方式實現。

 

  2. MVC

  在MVC流行之前,主流的處理方式都是通過文件路徑進行處理的,甚至以為是常態。直到 有一天開發者發現用戶請求的URL路徑原來可以跟具體腳本所在的路徑沒有任何關系。

  MVC模型的主要思想是將業務邏輯按職責分離,主要分為以下幾種。

   控制器(Controller),一組行為的集合。

   模型(Model),數據相關的操作和封裝。

   視圖(View),視圖的渲染。

 

 

  這是目前最為經典的分層模式,大致而言,它的工作模式如下說明。

   路由解析,根據URL尋找到對應的控制器和行為。

   行為調用相關的模型,進行數據操作。

   數據操作結束后,調用視圖和相關數據進行頁面渲染,輸出到客戶端。

  2.1 手工映射

  手工映射除了需要手工配置路由外較為原始外,它對URL的要求十分靈活,幾乎沒有格式上的限制。如下的URL格式都能自由映射:

  '/user/setting' , '/setting/user'

  這里假設已經擁有了一個處理設置用戶信息的控制器,如下所示:  

exports.setting = (req, res) => {
 // TODO
}

 

  再添加一個映射的方法(路由注冊)就行,為了方便后續的行文,這個方法名叫use(),如下所示:  

const routes = []

const use = (path, action) => {
 routes.push([path, action]);
}

  我們在入口程序中判斷URL,然后執行對應的邏輯,於是就完成了基本的路由映射過程,如下所示:

(req, res) => {
  const pathname = url.parse(req.url).pathname
  for (let i = 0; i < routes.length; i++) {
    let route = routes[i];
    if (pathname === route[0]) {
      let action = route[1]
      action(req, res)
      return
    }
  }
  // 處理404請求
  handle404(req, res)
}

  手工映射十分方便,由於它對URL十分靈活,所以我們可以將兩個路徑都映射到相同的業務 邏輯,如下所示:  

use('/user/setting', exports.setting);
use('/setting/user', exports.setting);

  // 甚至   

use('/setting/user/jacksontian', exports.setting);

  2.1.1  正則匹配

  對於簡單的路徑,采用上述的硬匹配方式即可,但是如下的路徑請求就完全無法滿足需求了:

  '/profile/jacksontian' ,  '/profile/hoover'

  這些請求需要根據不同的用戶顯示不同的內容,這里只有兩個用戶,假如系統中存在成千上 萬個用戶,我們就不太可能去手工維護所有用戶的路由請求,因此正則匹配應運而生,我們期望通過以下的方式就可以匹配到任意用戶:  

use('/profile/:username',  (req, res) => {
 // TODO
}); 

  於是我們改進我們的匹配方式,在通過use注冊路由時需要將路徑轉換為一個正則表達式, 然后通過它來進行匹配,如下所示:

const pathRegexp = (path) => {
 let strict = path. path
= path .concat(strict ? '' : '/?') .replace(/\/\(/g, '(?:/') .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) { slash = slash || ''; return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '') + (star ? '(/*)?' : ''); }) .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); return new RegExp('^' + path + '$'); }

  上述正則表達式十分復雜,總體而言,它能實現如下的匹配:

/profile/:username => /profile/jacksontian, /profile/hoover
/user.:ext => /user.xml, /user.json 

  現在我們重新改進注冊部分:

const use = (path, action) => {
  routes.push([pathRegexp(path), action]);
}

  以及匹配部分:

(req, res) => {
  const pathname = url.parse(req.url).pathname;
  for (let i = 0; i < routes.length; i++) {
    let route = routes[i];
    // 正則匹配
    if (route[0].exec(pathname)) {
      let action = route[1];
      action(req, res);
      return;
    }
  }
  // 處理404請求
  handle404(req, res);
}

  現在我們的路由功能就能夠實現正則匹配了,無須再為大量的用戶進行手工路由映射了。

  2.1.2 參數解析

  盡管完成了正則匹配,可以實現相似URL的匹配,但是:username到底匹配了啥,還沒有解決。為此我們還需要進一步將匹配到的內容抽取出來,希望在業務中能如下這樣調用:

use('/profile/:username', function (req, res) {
 var username = req.params.username;
 // TODO
});

  這里的目標是將抽取的內容設置到req.params處。那么第一步就是將鍵值抽取出來,如下所示:

const pathRegexp = function (path) {
  const keys = [];
  path = path
    .concat(strict ? '' : '/?')
    .replace(/\/\(/g, '(?:/')
    .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture,
      optional, star) {
      // 將匹配到的鍵值保存起來
      keys.push(key);
      slash = slash || '';
      return ''
        + (optional ? '' : slash)
        + '(?:'
        + (optional ? slash : '')
        + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
        + (optional || '')
        + (star ? '(/*)?' : '');
    })
    .replace(/([\/.])/g, '\\$1')
    .replace(/\*/g, '(.*)');
  return {
    keys: keys,
    regexp: new RegExp('^' + path + '$')
  };
}

  我們將根據抽取的鍵值和實際的URL得到鍵值匹配到的實際值,並設置到req.params處,如 下所示:

(req, res) => {
  const pathname = url.parse(req.url).pathname;
  for (let i = 0; i < routes.length; i++) {
    let route = routes[i];
    // 正則匹配
    let reg = route[0].regexp;
    let keys = route[0].keys;
    let matched = reg.exec(pathname);
    if (matched) {
      // 抽取具體值
      const params = {};
      for (let i = 0, l = keys.length; i < l; i++) {
        let value = matched[i + 1];
        if (value) {
          params[keys[i]] = value;
        }
      }
      req.params = params;
      let action = route[1];
      action(req, res);
      return;
    }
  }
  // 處理404請求
  handle404(req, res);
}

  至此,我們除了從查詢字符串(req.query)或提交數據(req.body)中取到值外,還能從路 徑的映射里取到值。

  2.2. 自然映射

  手工映射的優點在於路徑可以很靈活,但是如果項目較大,路由映射的數量也會很多。從前端路徑到具體的控制器文件,需要進行查閱才能定位到實際代碼的位置,為此有人提出,盡是路由不如無路由。實際上並非沒有路由,而是路由按一種約定的方式自然而然地實現了路由,而無須去維護路由映射。

  上文的路徑解析部分對這種自然映射的實現有稍許介紹,簡單而言,它將如下路徑進行了划分處理:

/controller/action/param1/param2/param3 

  以/user/setting/12/1987為例,它會按約定去找controllers目錄下的user文件,將其require出來后,調用這個文件模塊的setting()方法,而其余的值作為參數直接傳遞給這個方法。

(req, res) => {
  let pathname = url.parse(req.url).pathname;
  let paths = pathname.split('/');
  let controller = paths[1] || 'index';
  let action = paths[2] || 'index';
  let args = paths.slice(3);
  let module;

  try {
    // require的緩存機制使得只有第一次是阻塞的
    module = require('./controllers/' + controller);
  } catch (ex) {
    handle500(req, res);
    return;
  }
  let method = module[action]
  if (method) {
    method.apply(null, [req, res].concat(args));
  } else {
    handle500(req, res);
  }
}

  由於這種自然映射的方式沒有指明參數的名稱,所以無法采用req.params的方式提取,但是直接通過參數獲取更簡潔,如下所示:

exports.setting = (req, res, month, year) => {
  // 如果路徑為/user/setting/12/1987,那么month為12,year為1987
  // TODO
}; 

  事實上手工映射也能將值作為參數進行傳遞,而不是通過req.params。但是這個觀點見仁見智,這里不做比較和討論。

  自然映射這種路由方式在PHP的MVC框架CodeIgniter中應用十分廣泛,設計十分簡潔,在Node中實現它也十分容易。與手工映射相比,如果URL變動,它的文件也需要發生變動,手工映射只需要改動路由映射即可。

  3. RESTful

  MVC模式大行其道了很多年,直到RESTful的流行,大家才意識到URL也可以設計得很規范,請求方法也能作為邏輯分發的單元。

REST的全稱是Representational State Transfer,中文含義為表現層狀態轉化。符合REST規范的設計,我們稱為RESTful設計。它的設計哲學主要將服務器端提供的內容實體看作一個資源, 並表現在URL上。

  比如一個用戶的地址如下所示:

/users/jacksontian 

 

  這個地址代表了一個資源,對這個資源的操作,主要體現在HTTP請求方法上,不是體現在URL上。過去我們對用戶的增刪改查或許是如下這樣設計URL的:

POST /user/add?username=jacksontian
GET /user/remove?username=jacksontian
POST /user/update?username=jacksontian
GET /user/get?username=jacksontian 

  操作行為主要體現在行為上,主要使用的請求方法是POST和GET。在RESTful設計中,它是如下這樣的:

POST /user/jacksontian
DELETE /user/jacksontian
PUT /user/jacksontian
GET /user/jacksontian 

  它將DELETE和PUT請求方法引入設計中,參與資源的操作和更改資源的狀態。

  對於這個資源的具體表現形態,也不再如過去一樣表現在URL的文件后綴上。過去設計資源的格式與后綴有很大的關聯,例如:

GET /user/jacksontian.json
GET /user/jacksontian.xml 

  在RESTful設計中,資源的具體格式由請求報頭中的Accept字段和服務器端的支持情況來決定。如果客戶端同時接受JSON和XML格式的響應,那么它的Accept字段值是如下這樣的:

Accept: application/json,application/xml

  靠譜的服務器端應該要顧及這個字段,然后根據自己能響應的格式做出響應。在響應報文中,通過Content-Type字段告知客戶端是什么格式,如下所示:

Content-Type: application/json 

  具體格式,我們稱之為具體的表現。所以REST的設計就是,通過URL設計資源、請求方法定義資源的操作,通過Accept決定資源的表現形式。

  RESTful與MVC設計並不沖突,而且是更好的改進。相比MVC,RESTful只是將HTTP請求方法也加入了路由的過程,以及在URL路徑上體現得更資源化。

  3.1 請求方法

  為了讓Node能夠支持RESTful需求,我們改進了我們的設計。如果use是對所有請求方法的處理,那么在RESTful的場景下,我們需要區分請求方法設計。示例如下所示:

const routes = { 'all': [] };
const app = {};
app.use = function (path, action) {
  routes.all.push([pathRegexp(path), action]);
};
['get', 'put', 'delete', 'post'].forEach(function (method) {
  routes[method] = [];
  app[method] = function (path, action) {
    routes[method].push([pathRegexp(path), action]);
  };
}); 

  上面的代碼添加了get()、put()、delete()、post()4個方法后,我們希望通過如下的方式完成路由映射:

// 增加用戶
app.post('/user/:username', addUser);
// 刪除用戶
app.delete('/user/:username', removeUser);
// 修改用戶
app.put('/user/:username', updateUser);
// 查詢用戶
app.get('/user/:username', getUser);

  這樣的路由能夠識別請求方法,並將業務進行分發。為了讓分發部分更簡潔,我們先將匹配的部分抽取為match()方法,如下所示:

const match = (pathname, routes) => {
  for (let i = 0; i < routes.length; i++) {
    let route = routes[i];
    // 正則匹配
    let reg = route[0].regexp;
    let keys = route[0].keys;
    let matched = reg.exec(pathname);
    if (matched) {
      // 抽取具體值
      const params = {};
      for (let i = 0, l = keys.length; i < l; i++) {
        let value = matched[i + 1];
        if (value) {
          params[keys[i]] = value;
        }
      }
      req.params = params;
      let action = route[1];
      action(req, res);
      return true;
    }
  }
  return false;
}; 

  然后改進我們的分發部分,如下所示:

(req, res) => {
  let pathname = url.parse(req.url).pathname;
  // 將請求方法變為小寫
  let method = req.method.toLowerCase();
  if (routes.hasOwnPerperty(method)) {
    // 根據請求方法分發
    if (match(pathname, routes[method])) {
      return;
    } else {
      // 如果路徑沒有匹配成功,嘗試讓all()來處理
      if (match(pathname, routes.all)) {
        return;
      }
    }
  } else {
    // 直接讓all()來處理
    if (match(pathname, routes.all)) {
      return;
    }
  }
  // 處理404請求
  handle404(req, res);
} 

  如此,我們完成了實現RESTful支持的必要條件。這里的實現過程采用了手工映射的方法完成,事實上通過自然映射也能完成RESTful的支持,但是根據Controller/Action的約定必須要轉化為Resource/Method的約定,此處已經引出實現思路,不再詳述。

  目前RESTful應用已經開始廣泛起來,隨着業務邏輯前端化、客戶端的多樣化,RESTful模式以其輕量的設計,得到廣大開發者的青睞。對於多數的應用而言,只需要構建一套RESTful服務接口,就能適應移動端、PC端的各種客戶端應用。

 

  

 

  

 

  

  

  

  


免責聲明!

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



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