1.什么是路由?
在Web開發過程中,經常會遇到『路由』的概念。那么,到底什么是路由?簡單來說,路由就是URL到函數的映射。
路由的概念最開始是由后端提出來的,在以前用模板引擎開發頁面的時候,是使用路由返回不同的頁面,
大致流程可以看成這樣:
(1)瀏覽器發出請求
(2)服務器端監聽到80端口或者443有請求過來,並解析url路徑
(3)根據服務器的路由配置,返回相應信息(可以是html文件,json數據,也可以是圖片)
(4)瀏覽器根據數據包的content-type來決定如何解析數據
簡單來說路由就是用來跟后端服務器進行交互的一種方式,通過不同的路徑來請求不同的資源,請求不同的頁面是路由的其中一項功能。
2.router 和 route 的區別
route就是一條路由,它將一個URL路徑和一個函數進行映射,例如:
/users -> getAllUsers()
/users/count -> getUsersCount()
這就是兩條路由,當訪問 /users 的時候,會執行 getAllUsers() 函數;當訪問 /users/count 的時候,會執行 getUsersCount() 函數。
而 router 可以理解為一個容器,或者說一種機制,它管理了一組 route。簡單來說,route 只是進行了URL和函數的映射,而在當接收到一個URL之后,去路由映射表中查找相應的函數,這個過程是由 router 來處理的。一句話概括就是 "The router routes you to a route"。
3.服務端路由
對於服務器來說,當接收到客戶端發來的HTTP請求,會根據請求的URL,來找到相應的映射函數,然后執行該函數,並將函數的返回值發送給客戶端。對於最簡單的靜態資源服務器,可以認為,所有URL的映射函數就是一個文件讀取操作。對於動態資源,映射函數可能是一個數據庫讀取操作,也可能是進行一些數據的處理,等等。
以 Express 為例:
app.get('/', (req, res) => { res.sendFile('index') }) app.get('/users', (req, res) => { db.queryAllUsers() .then(data => res.send(data)) })
這里定義了兩條路由:
- 當訪問 / 的時候,會返回 index 頁面
- 當訪問 /users 的時候,會從數據庫中取出所有用戶數據並返回
不僅僅是URL 在 router 匹配 route 的過程中,不僅會根據URL來匹配,還會根據請求的方法來看是否匹配。例如上面的例子,如果通過 POST 方法來訪問 /users,就會找不到正確的路由。
4.客戶端路由
對於客戶端(通常為瀏覽器)來說,路由的映射函數通常是進行一些DOM的顯示和隱藏操作。這樣,當訪問不同的路徑的時候,會顯示不同的頁面組件。客戶端路由最常見的有以下兩種實現方案:
(1)基於Hash
我們知道,URL中 # 及其后面的部分為 hash。例如:
const url = require('url') var a = url.parse('http://example.com/#/foo/bar') console.log(a.hash) // => #/foo/bar
hash僅僅是客戶端的一個狀態,也就是說,當向服務器發請求的時候,hash部分並不會發過去。
通過監聽 window 對象的 hashChange 事件,可以實現簡單的路由。例如:即根據哈希值的不同顯示不同的內容
window.onhashchange = function() { var hash = window.location.hash var path = hash.substring(1) //截取指定下標直接的字符,這是從下標1開始到結尾 switch (path) { case '/': showHome() break case '/users': showUsersList() break default: show404NotFound() } }
(2)基於History API
通過HTML5 History API可以在不刷新頁面的情況下,直接改變當前URL。詳細用法可以參考:
我們可以通過監聽 window 對象的 popstate 事件,來實現簡單的路由:
window.onpopstate = function() { var path = window.location.pathname switch (path) { case '/': showHome() break case '/users': showUsersList() break default: show404NotFound() } }
但是這種方法只能捕獲前進或后退事件,無法捕獲 pushState 和 replaceState,一種最簡單的解決方法是替換 pushState 方法,例如:
var pushState = history.pushState history.pushState = function() { pushState.apply(history, arguments) // emit a event or just run a callback emitEventOrRunCallback() }
不過,最好的方法還是使用實現好的 history 庫。
(3)兩種方法的比較
總的來說,基於Hash的路由,兼容性更好;基於History API的路由,更加直觀和正式。 但是,有一點很大的區別是,基於Hash的路由不需要對服務器做改動,基於History API的路由需要對服務器做一些改造。下面來詳細分析。 假設服務器只有如下文件(script.js被index.html所引用):
/-
|- index.html |- script.js
基於Hash的路徑有:
http://example.com/ http://example.com/#/foobar
基於History API的路徑有:
http://example.com/ http://example.com/foobar
當直接訪問 / 的時候,兩者的行為是一致的,都是返回了 index.html 文件。
當從 / 跳轉到 /#/foobar 或者 /foobar 的時候,也都是正常的,因為此時已經加載了頁面以及腳本文件,所以路由跳轉正常。
當直接訪問 /#/foobar 的時候,實際上向服務器發起的請求是 /,因此會首先加載頁面及腳本文件,接下來腳本執行路由跳轉,一切正常。
當直接訪問 /foobar 的時候,實際上向服務器發起的請求也是 /foobar,然而服務器端只能匹配 / 而無法匹配 /foobar,因此會出現404錯誤。
因此如果使用了基於History API的路由,需要改造服務器端,使得訪問 /foobar 的時候也能返回 index.html 文件,這樣當瀏覽器加載了頁面及腳本之后,就能進行路由跳轉了。
5.動態路由
上面提到的例子都是靜態路由,也就是說,路徑都是固定的。但是有時候我們需要在路徑中傳入參數,例如獲取某個用戶的信息,我們不可能為每個用戶創建一條路由,而是在通過捕獲路徑中的參數(例如用戶id)來實現。
例如在 Express 中:
app.get('/user/:id', (req, res, next) => { // ... ... })
在 Flask 中:
@app.route('/user/<user_id>') def get_user_info(user_id): pass
6.嚴格路由
在很多情況下,會遇到 /foobar 和 /foobar/ 的情況,它們看起來非常類似,然而實際上有所區別,具體的行為也是視服務器設置而定。
在 Flask的文檔 中,提到,末尾有斜線的路徑,類比於文件系統的一個目錄;末尾沒有斜線的路徑,類比於一個文件。因此訪問 /foobar 的時候,可能會重定向到 /foobar/,而反過來則不會。
如果使用的是 Express,默認這兩者是一樣的,也可以通過 app.set 來設置 strict routing,來區別對待這兩種情況。
什么是路由?
在Web開發過程中,經常會遇到『路由』的概念。那么,到底什么是路由?簡單來說,路由就是URL到函數的映射。
訪問的URL會映射到相應的函數里(這個函數是廣義的,可以是前端的函數也可以是后端的函數),然后由相應的函數來決定返回給這個URL什么東西。路由就是在做一個匹配的工作。
從后端路由講起
在web開發早期的「刀耕火種」年代里,一直是后端路由占據主導地位。不管是php,還是jsp、asp,用戶能通過URL訪問到的頁面,大多是通過后端路由匹配之后再返回給瀏覽器的。經典面試題,「你從瀏覽器地址欄里輸入www.baidu.com到你看到網頁這個過程中經歷了什么」其實講的也是這個道理。
在web后端,不管是什么語言的后端框架,都會有一個專門開辟出來的路由模塊或者路由區域,用來匹配用戶給出的URL地址,以及一些表單提交、ajax請求的地址。通常遇到無法匹配的路由,后端將會返回一個404狀態碼。這也是我們常說的404 NOT FOUND的由來。
URL 與 Methods
如果你關注RESTful API,那么將會很熟悉下面四種發起請求的類型:GET,POST,PUT,DELETE。
它們分別對應四種基本操作:GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。——來自阮一峰《理解RESTful架構》
雖然上面說的是RESTful API,但是實際上我們在地址欄輸入一個URL,並回車的時候,是以GET請求發出去的。這也體現了,URL地址和請求的method也應該是一一對應。下面給出一個例子:
router.post('/user/:id', addUser)
假如我的后端路由配置里只有這一句路由。那么我通過瀏覽器里訪問:http://xxx.com/user/123的話是無法訪問到的,也會返回一個404。因為后端只配了一個post方法的路由。如果要接受這個請求,那么必須有如下的路由:
router.get('/user/:id', getUser) // 配置get路由 router.post('/user/:id', addUser)
后端路由與服務端渲染
前面說了,「刀耕火種」的年代里,網頁通常是通過后端路由直接輸出給客戶端瀏覽器的。也就是網頁的html一般是在后端服務器里通過模板引擎渲染好再交給前端的。至於一些其他的效果,是通過預先寫在頁面里的jQuery、Bootstrap等常見的前端框架去負責的。
如果你說有些網站已經是通過ajax去實現的頁面,比如gmail,比如qq郵箱。那么你要注意到哪怕是這些頁面,它們頁面的「龍骨」也並非是全部通過ajax去實現的,依然還是后端直出——這也就是我們現在又老生常談的服務端渲染。
服務端渲染的好處有很多,比如對於SEO友好,一些對安全性要求高的頁面采用服務端渲染是更保險的。而在當時還沒有node.js的年代,為了良好地構建前端頁面,都是通過服務端語言對應的模板引擎來實現動態網頁、頁面結構的組織、組件的復用。比如Laravel的blade,用在Django上的jinja2,用在Struts的jsp等等。實際上到如今,一門后端語言想要能實現自己的web功能,都需要有自己對應的模板引擎。
node.js誕生之后,前端擁有自己的后端渲染的模板引擎也成為了現實。常見的比如pug、ejs、nunjucks等。這些模板引擎搭配Express、Koa等后端框架也在一開始風靡一時
不過在這個過程中,隨着web應用的開發越來越復雜,單純服務端渲染的問題開始慢慢的暴露出來了——耦合性太強了,jQuery時代的頁面不好維護,頁面切換白屏嚴重等等。耦合性問題雖然能通過良好的代碼結構、規范來解決,不過jQuery時代的頁面不好維護這是有目共睹的,全局變量滿天飛,代碼入侵性太高。后續的維護通常是在給前面的代碼打補丁。而頁面切換的白屏問題雖然可以通過ajax、或者iframe等來解決,但是在實現上就麻煩了——進一步增加了可維護的難度
於是,我們開始進入了前端路由的時代。
過渡到前端路由
前端路由——顧名思義,頁面跳轉的URL規則匹配由前端來控制。而前端路由主要是有兩種顯示方式:
- 帶有hash的前端路由,優點是兼容性高。缺點是URL帶有#號不好看
- 不帶hash的前端路由,優點是URL不帶#號,好看。缺點是既需要瀏覽器支持也需要后端服務器支持
前端路由應用最廣泛的例子就是當今的SPA的web項目。不管是Vue、React還是Angular的頁面工程,都離不開相應配套的router工具。前端路由帶來的最明顯的好處就是,地址欄URL的跳轉不會白屏了——這也得益於前端渲染帶來的好處。
前端路由與前端渲染
講前端路由就不能不說前端渲染。我以Vue項目為例。如果你是用官方的vue-cli搭配webpack模板構建的項目,你有沒有想過你的瀏覽器拿到的html是什么樣的?是你頁面長的那樣有button有form的樣子么?我想不是的。在生產模式下,你看看構建出來的index.html長什么樣:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vue</title> </head> <body> <div id="app"></div> <script type="text/javascript" src="xxxx.xxx.js"></script> <script type="text/javascript" src="yyyy.yyy.js"></script> <script type="text/javascript" src="zzzz.zzz.js"></script> </body> </html>
通常長上面這個樣子。可以看到,這個其實就是你的瀏覽器從服務端拿到的html。這里面空盪盪的只有一個 <div id="app"></div>
這個入口的div以及下面配套的一系列js文件。所以你看到的頁面其實是通過那些js渲染出來的。這也是我們常說的前端渲染。
前端渲染把渲染的任務交給了瀏覽器,通過客戶端的算力來解決頁面的構建,這個很大程度上緩解了服務端的壓力。而且配合前端路由,無縫的頁面切換體驗自然是對用戶友好的。不過帶來的壞處就是對SEO不友好,畢竟搜索引擎的爬蟲只能爬到上面那樣的html,對瀏覽器的版本也會有相應的要求。
需要明確的是,只要在瀏覽器地址欄輸入URL再回車,是一定會去后端服務器請求一次的。而如果是在頁面里通過點擊按鈕等操作,利用router庫的api來進行的URL更新是不會去后端服務器請求的。
Hash模式
hash模式利用的是瀏覽器不會對#號后面的路徑對服務端發起路由請求。也即在瀏覽器里輸入如下這兩個地址:http://localhost/#/user/1和http://localhost/其實到服務端都是去請求http://localhost這個頁面的內容。
而前端的router庫通過捕捉#號后面的參數、地址,來告訴前端庫(比如Vue)渲染對應的頁面。這樣,不管是我們在瀏覽器的地址欄輸入,或者是頁面里通過router的api進行的跳轉,都是一樣的跳轉邏輯。所以這個模式是不需要后端配置其他邏輯的,后台只要給前端返回http://localhost對應的html,剩下具體是哪個頁面,就由前端路由去判斷便可。
History模式
不帶#號的路由,也就是我們通常能見到的URL形式。router庫要實現這個功能一般都是通過HTML5提供的history這個api。比如history.pushState()可以向瀏覽器地址欄push一個URL,而這個URL是不會向后端發起請求的!通過這個特性,便能很方便地實現漂亮的URL。不過需要注意的是,這個api對於IE9及其以下版本瀏覽器是不支持的,IE10開始支持,所以對於瀏覽器版本是有要求的。vue-router會檢測瀏覽器版本,當無法啟用history模式的時候會自動降級為hash模式
上面說了,你在頁面里的跳轉,通常是通過router的api去進行的跳轉,router的api調用的通常是history.pushState()這個api,所以跟后端沒什么關系。但是一旦你從瀏覽器地址欄里輸入一個地址,比如http://localhost/user/1,這個URL是會向后端發起一個get請求的。后端路由表里如果沒有配置相應的路由,那么自然就會返回一個404了!這也就是很多朋友在生產模式遇到404頁面的原因
那么很多人會問了,那為什么我在開發模式下沒問題呢?那是因為vue-cli在開發模式下幫你啟動的那個express開發服務器幫你做了這方面的配置。理論上在開發模式下本來也是需要配置服務端的,只不過vue-cli都幫你配置好了,所以你就不用手動配置了。
那么該如何配置呢?其實在生產模式下配置也很簡單,參考vue-router給出的配置例子。一個原則就是,在所有后端路由規則的最后,配置一個規則,如果前面其他路由規則都不匹配的情況下,就執行這個規則——把構建好的那個index.html返回給前端。這樣就解決了后端路由拋出的404的問題了,因為只要你輸入了http://localhost/user/1這地址,那么由於后端其他路由都不匹配,那么就會返回給瀏覽器index.html。
瀏覽器拿到這個html之后,router庫就開始工作,開始獲取地址欄的URL信息,然后再告訴前端庫(比如Vue)渲染對應的頁面。到這一步就跟hash模式是類似的了。
當然,由於后端無法拋出404的頁面錯誤,404的URL規則自然是交給前端路由來決定了。你可以自己在前端路由里決定什么URL都不匹配的404頁面應該顯示什么。設置默認路由
前端路由與服務端渲染
雖然前端渲染有諸多好處,不過SEO的問題,還是比較突出的。所以react、vue等框架在后來也在服務端渲染上做着自己的努力。基於前端庫的服務端渲染跟以前基於后端語言的服務端渲染又有所不同。前端框架的服務端渲染大多依然采用的是前端路由,並且由於引入了狀態統一、vnode等等概念,它們的服務端渲染對服務器的性能要求比php等語言基於的字符串填充的模板引擎渲染對於服務器的性能要求高得多。所以在這方面不僅是框架本身在不斷改進算法、優化,服務端的性能也必須要有所提升。當初掘金換成SSR的時候也遇到了對應的性能問題,就是這個原因。
當然在二者之間,也出現了預渲染的概念。也即先在服務端構建出一部分靜態的html文件,用於直出瀏覽器。然后剩下的頁面再通過常用的前端渲染來實現。通常我們可以把首頁采用預渲染的方式。這個的好處是明顯的,兼顧了SEO和服務器的性能要求。不過它無法做到全站SEO,生產構建階段耗時也會有所提高,這也是遺憾所在。
關於預渲染,可以考慮使用prerender-spa-plugin這個webapck的插件,它的3.x版本開始使用puppeteer來構建html文件了。
前后端分離
得益於前端路由和現代前端框架的完整的前后端渲染能力,跟頁面渲染、組織、組件相關的東西,后端終於可以不用再參與了。
前后端分離的開發模式也逐漸開始普及。前端開始更加注重頁面開發的工程化、自動化,而后端則更專注於api的提供和數據庫的保障。代碼層面上耦合度也進一步降低,分工也更加明確。我們也擺脫了當初「刀耕火種」的web開發年代。
實列:使用Vue 和 Flask中創建單頁面應用去除導航欄中的#號
Vue項目去掉地址欄中的#號,最常用的方式就是在路由中使用 history 模式,存在的問題與原因如上文所述,HTML5的History-Mode在Vue-router中需要配置Web服務器的重定向,將所有路徑指向index.html
以Flask創建的 web server
為例,做法很簡單,將現有路由修改為以下:
@app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def catch_all(path): return render_template("index.html")
現在輸入網址localhost:5000/xxxx 都將重新定向到index.html和vue-router將處理路由。