淺談前後端路由與前後端渲染

Molunerfinn發表於2018-03-28

原文首發於我的部落格

最近經常會遇到有人問諸如類似下面的問題:

  • 為啥我寫的Vue應用在開發階段都沒問題,部署到服務端之後訪問不了除了/的頁面呢
  • 為啥我寫的SPA頁面的路由用hash模式都沒問題,改成history模式就問題百出呢
  • 啥是前端路由啥是後端路由,要怎麼配後端才能支援我的前端路由呢

這個問題是很多初學者會問的問題,於是結合我自己的學習經歷也來簡單的講解一下這二者的區別與聯絡,希望能對你們有所幫助。

老手可以繞道,去看些更有用的文章吧~

什麼是路由

理解Web路由這篇文章講得特別好了。

在Web開發過程中,經常會遇到『路由』的概念。那麼,到底什麼是路由?簡單來說,路由就是URL到函式的對映。

訪問的URL會對映到相應的函式裡(這個函式是廣義的,可以是前端的函式也可以是後端的函式),然後由相應的函式來決定返回給這個URL什麼東西。路由就是在做一個匹配的工作。

從後端路由講起

在web開發早期的「刀耕火種」年代裡,一直是後端路由佔據主導地位。不管是php,還是jsp、asp,使用者能通過URL訪問到的頁面,大多是通過後端路由匹配之後再返回給瀏覽器的。經典面試題,「你從瀏覽器位址列裡輸入www.baidu.com到你看到網頁這個過程中經歷了什麼」其實講的也是這個道理。

淺談前後端路由與前後端渲染

在web後端,不管是什麼語言的後端框架,都會有一個專門開闢出來的路由模組或者路由區域,用來匹配使用者給出的URL地址,以及一些表單提交、ajax請求的地址。通常遇到無法匹配的路由,後端將會返回一個404狀態碼。這也是我們常說的404 NOT FOUND的由來。

URL與Methods

如果你關注RESTful API,那麼將會很熟悉下面四種發起請求的型別:GETPOSTPUTDELETE

它們分別對應四種基本操作: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是什麼樣的?是你頁面長的那樣有buttonform的樣子麼?我想不是的。在生產模式下,你看看構建出來的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/1http://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開發年代。撒花~

總結

希望通過此文能夠讓你對於前後端路由和前後端渲染有所瞭解。在實際開發的過程中,也不應該僅僅關注於自己所在的領域,相關的領域也要有所涉獵,這樣才能面對問題遊刃有餘。

注:文中的圖我使用OmniGraffle製作。轉載請註明作者!

相關文章