3種常見的路由方式

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

  --如下內容出自《深刻淺出node.js》html

 

  1. 文件路徑型前端

  1.1 靜態文件node

  這種方式的路由在路徑解析的部分有過簡單描述,其讓人舒服的地方在於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. 動態文件 json

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

  如下是Apache中配置PHP支持的方式:  緩存

AddType application/x-httpd-php .php

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

 

  2. MVCapp

  在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端的各類客戶端應用。

相關文章
相關標籤/搜索