本文將會介紹文件路徑、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端的各類客戶端應用。