關(guān)于路由
路由其實是根據(jù)不同的URL地址展示不同的內(nèi)容或頁面;
廣義上來說,訪問路由會映射到相應(yīng)的函數(shù)里,然后由相應(yīng)的函數(shù)來決定返回給這個URL的內(nèi)容。路由就是一個匹配過程;
后端路由
在Web前端開發(fā)早期,一直是后端路由占據(jù)主導(dǎo)地位,不管是PHP,還是JSP、ASP,用戶通過URL訪問頁面時,大多是通過后端路由匹配之后再返回給瀏覽器。經(jīng)典面試題[說說瀏覽器地址欄輸入www.baidu.com到網(wǎng)頁展示的過程]其實也是在講這個道理。
不管是什么語言的Web后端框架,都會有一個專門的路由模塊或路由區(qū)域,用于匹配用戶給出的URL地址,以及一些表單提交、AJAX請求的地址。通常遇到無法匹配的路由,后端會返回一個404狀態(tài)碼,這也是404 NOT FOUND的由來。
服務(wù)端渲染
在后端為主導(dǎo)的年代,網(wǎng)頁HTML一般是后端通過模板引擎渲染好之后響應(yīng)給前端,這就是服務(wù)端渲染。瀏覽器在地址欄中切換不同的URL時,每次都會向后端發(fā)出請求,服務(wù)器響應(yīng)請求。
服務(wù)端渲染的好處有很多,比如對SEO友好,一些對安全性要求高的頁面采用服務(wù)端渲染更保險。
Node.js誕生以后,前端擁有自己的后端模板引擎成為了現(xiàn)實,常見的有ejs、nunjucks。這些模板引擎搭配Express、Koa等Node框架也風靡一時。
不過,隨著Web應(yīng)用的開發(fā)越來越復(fù)雜,單純的服務(wù)端渲染問題開始慢慢暴露了出來:耦合性太強!耦合性問題雖然能通過良好的代碼結(jié)構(gòu)、規(guī)范來解決,但jQuery時代的頁面不好維護也是有目共睹的,全局變量滿天飛,代碼入侵性太高;后續(xù)維護通常也是在給前面的代碼打補??;頁面切換白屏問題雖然可以通過AJAX或iframe等方案解決,但實際上卻進一步增加了可維護性的難度。
前端路由
前端路由:頁面跳轉(zhuǎn)的URL規(guī)則匹配由前端來控制;應(yīng)用最廣泛的例子就是當今的SPA的Web項目。
前端渲染:以Vue項目為例,瀏覽器從服務(wù)器拿到的HTML里只有一個<div id="app"></div>,并搭配一系列js文件。所以,我們看到的頁面其實是通過這些js渲染出來的。
前端渲染把渲染的任務(wù)交給了瀏覽器,通過客戶端的算力來解決頁面的構(gòu)建,在很大程度上緩解了服務(wù)端的壓力。而且配合前端路由,無縫的頁面切換體驗,自然對用戶是友好的。不過帶來的壞處就是對
SEO不友好,畢竟搜索引擎的爬蟲只能爬到上面那個空蕩蕩的HTML,而且對瀏覽器的版本也會有相應(yīng)的要求。
注意:只要在瀏覽器地址欄輸入URL再回車,是一定會去后端服務(wù)器請求一次的。而如果是在頁面里通過點擊按鈕等操作,利用router庫的api來進行的URL更新,則不會去后端服務(wù)器請求。
前端路由主要有兩種方式:
-
hash模式,錨點操作,利用hash值的變化感知路由變化,優(yōu)點是兼容性高,缺點是URL帶有#號不好看,而且有些場景如微信分享 會破壞掉#后面的內(nèi)容; -
HTML5的history模式,優(yōu)點是URL不帶#號,缺點是需要瀏覽器和后端同時支持。
hash模式
hash 是瀏覽器URL中 # 后面的內(nèi)容,包含#。hash是URL中的錨點,代表網(wǎng)頁中的一個位置,單單改變 # 后的部分,瀏覽器只會加載相應(yīng)位置的內(nèi)容,不會重新加載頁面。
-
#是用來指導(dǎo)瀏覽器動作的,對服務(wù)器完全無用,HTTP請求中并不包含#; - 每一次改變
#后的部分,都會在瀏覽器的訪問歷史中增加一個記錄,點擊后退按鈕,就可以回到上一個位置。
所以說Hash模式通過錨點值的改變,根據(jù)不同的值,渲染指定DOM位置的不同數(shù)據(jù)。
https://www.abc.com/xv/Home/index#plan
https://www.abc.com/xv/Home/index/#/add/index
hash值的變化不會讓瀏覽器重新發(fā)起請求,但會觸發(fā) window.onhashChange 事件;
觸發(fā)hashChange事件的情況:
- 直接更改瀏覽器地址,在最后面增加或改變
#hash; - 改變
location.href或location.hash的值; - 通過觸發(fā)點擊帶錨點的鏈接;
- 瀏覽器前進后退可能導(dǎo)致
hash的變化,前提是兩個地址中的文檔相同、但hash值不同。
如果我們在 hashChange 事件中獲取當前的hash值,根據(jù)hash值來修改頁面內(nèi)容,就能達到前端路由的目的。
另外, hashChange 事件回調(diào)的對象參數(shù)中,有兩個比較重要的屬性newURL、oldURL,分別表示當前變化前、后的URL
簡略版
#html:
<ul>
<li><a href="#index">首頁</a></li>
<li><a href="#news">資訊</a></li>
<li><a href="#user">個人中心</a></li>
</ul>
<div id="app"></div>
#script: 應(yīng)該封裝成Router
const app = document.getElementById('app')
function hashChange(e){
// 當前跳轉(zhuǎn)的新URL 上次的舊URL
console.log(e.newURL, e.oldURL)
// 根據(jù) hash 值決定顯示什么內(nèi)容
switch (location.hash) {
case '#index':
app.innerHTML = '<h1>這是首頁內(nèi)容</h1>'
break
case '#news':
app.innerHTML = '<h1>這是新聞內(nèi)容</h1>'
break
case '#user':
app.innerHTML = '<h1>這是個人中心內(nèi)容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
window.onhashchange = hashChange
hashChange()
除此之外,還需要記錄當前URL,監(jiān)聽刷新事件(onload),在onhashchange中實現(xiàn)回退和前進等功能。。。
history模式
history其實瀏覽器歷史棧(歷史記錄)的一個接口,基于window.history對象的方法
https://www.plysummer.com/#/plan/index // hash模式路由
https://www.plysummer.com/plan/index // history模式路由
- 在
HTML4中,已經(jīng)支持window.history對象來控制頁面歷史記錄跳轉(zhuǎn),常用的方法包括:-
history.forward()在歷史棧中前進一步; -
history.back()在歷史棧中后退一步; -
history.go(n)在歷史棧中跳轉(zhuǎn)n步驟,n=0為刷新本頁,n=-1為后退一頁。
-
- 在
HTML5中,window.history對象得到了擴展,新增的API包括:-
history.pushState(data [,title] [,url])向歷史棧中追加一條記錄,data表示需要保存的數(shù)據(jù),在觸發(fā)popstate事件時,可以在event.state里獲??;# 當前url:https://www.xxx.com/a/ # 1. 對新URL使用絕對路徑 history.pushState(null, null, '/qq/') // https://www.xxx.com/qq/ # 2. 對新URL使用相對路徑 history.pushState(null, null, './qq/') // https://www.xxx.com/a/qq/ # 3. 對新URL使用完整的同源路徑 history.pushState(null, null, 'https://www.xxx.com/kk/qq') // https://www.xxx.com/kk/qq -
history.replaceState(data [,title] [,url])替換當前頁在歷史棧中的記錄,其他特性與pushState一致; -
history.state是一個屬性,可以得到當前頁的state信息; -
history.length當前歷史棧中的記錄數(shù); -
window.onpopstate是一個事件,只有在點擊瀏覽器前進、后退按鈕,js調(diào)用forward()、back()、go()時觸發(fā)。
-
-
注意:
-
IE9及其以下版本瀏覽器是不支持的,IE10開始支持。vue-router會檢測瀏覽器版本,當無法啟用history模式時會自動降級為hash模式; -
pushState()/replaceState()雖然可以改變歷史棧,讓瀏覽器地址欄中的URL發(fā)生變化,但并不會向后端發(fā)起請求! -
pushState()/replaceState()對URL的修改受同源策略限制,防止惡意腳本模仿其他網(wǎng)站的URL欺騙用戶,所以當違背同源策略時將會報錯; - 火狐目前會忽略
title參數(shù)。
-
- 簡略版
# html
<ul id="menu">
<li><a href="/index">首頁</a></li>
<li><a href="/news">資訊</a></li>
<li><a href="/user">個人中心</a></li>
</ul>
<div id="app"></div>
# script: 應(yīng)該封裝為Router
document.querySelector('#menu').addEventListener('click', e => {
if(e.target.nodeName === 'A') {
e.preventDefault() // 阻止 <a> 的默認事件,默認的跳轉(zhuǎn)會刷新頁面
//獲取超鏈接的href,改為 pushState 跳轉(zhuǎn),不刷新頁面
const path = e.target.getAttribute('href')
// 修改瀏覽器中顯示的 url
window.history.pushState(null, null, path)
// 根據(jù)path,更改頁面內(nèi)容
render(path)
}
})
const app = document.getElementById('app')
function render(path) {
switch (path) {
case '/index':
app.innerHTML = '<h1>這是首頁內(nèi)容</h1>'
break
case '/news':
app.innerHTML = '<h1>這是新聞內(nèi)容</h1>'
break
case '/user':
app.innerHTML = '<h1>這是個人中心內(nèi)容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
//監(jiān)聽瀏覽器前進后退事件,并根據(jù)當前路徑渲染頁面
window.onpopstate = e => {
render(location.pathname)
}
//第一次進入頁面顯示首頁
render('/index')
我們還可以通過自定義事件,實現(xiàn)對history.pushState 和history.replaceState的監(jiān)聽。
var _rewrite = function(type) {
var fn = window.history[type] // 保存原函數(shù)的引用
var evt = new Event(type) // 自定義事件
return function() { // 閉包
// 調(diào)用原函數(shù)
var res = fn.apply(this, arguments)
evt.arguments = arguments // 把參數(shù)塞進去
window.dispatchEvent(evt) // 分發(fā)事件
return res
}
}
// 重寫方法
window.history.pushState = _rewrite('pushState')
window.history.replaceState = _rewrite('replaceState')
// 監(jiān)聽自定義事件
window.addEventListener('replaceState', e => {
console.log('replaceState: ', e.arguments)
})
window.addEventListener('pushState', e => {
console.log('pushState: ', e.arguments)
})
404問題
在前端做頁面跳轉(zhuǎn)時,通常是利用
history API完成的,router庫調(diào)用history.pushState()跟后端沒有任何關(guān)系。但是一旦從瀏覽器地址欄里輸入一個URL(不管是否有效)并回車或者手動刷新頁面,那就會向后端發(fā)起一個GET請求。而后端路由表中又沒有配置相應(yīng)的路由,那么自然就會返回404 NOT FOUND!這也就是為什么很多人在生產(chǎn)模式下遇到404頁面的原因。
-
hash模式,發(fā)送的HTTP請求是不變的,不包含錨點部分,本質(zhì)上始終請求的是打包后的index.html; -
history模式,發(fā)送的HTTP請求是完整的瀏覽器地址,對后端來說,這樣的路由是不存在,所以會出現(xiàn)404。
這也就是history模式為什么需要后端同時支持。
vue-router文檔上給出了一個配置例子:在所有后端路由規(guī)則的最后,加上一個默認匹配規(guī)則 -- 如果URL匹配不到任何靜態(tài)資源,則響應(yīng)同一個 index.html 給前端。
這樣就解決了后端路由拋出的404問題,前端拿到的也始終是打包后的index.html了。再通過路由庫的處理,獲取地址欄的URL信息,告知前端庫(Vue、React)渲染對應(yīng)的頁面。到了這一步就跟hash模式類似了。
但 這樣在后端配置之后,404頁面的處理權(quán)又交回了前端。以Nginx和vue-router為例,同時解決手動刷新瀏覽器和手動輸入URL 并回車的 404問題:
# 后端:nginx.conf
server {
listen 8080;
server_name xx.xxx.xxx.xx;
root html; # vue項目的打包后的dist
location / {
# 指向下面的@router,解決刷新出現(xiàn)404問題
try_files $uri $uri/ @router;
index index.html index.htm;
}
location @router {
# 重寫到index.html中,然后交給前端路由去處理請求資源
rewrite ^.*$ /index.html last;
}
}
# 前端:vue-router
{
path: "/404",
name: "404",
component: () => import('@/views/404.vue')
},
// 當輸入不存在的URL時,在前端重定向到404頁面
{ path: "*", redirect: "/404" }
問題延伸:
在IE瀏覽器下刷新仍然還是404,是因為IE自作聰明,對于頁面大小 < 1024b 會被認為十分不友好,所以ie就將改頁面給替換成自己的錯誤提示頁面了,而SPA打包后的 index.html 可能會小于臨界值。
資源路徑問題
在history模式下,訪問路由和嵌套路由頁面,顯示正常,但是刷新頁面的時候,嵌套路由頁面就出異常了!查看網(wǎng)絡(luò)請求發(fā)現(xiàn),請求加載的靜態(tài)資源(圖片、CSS、JS...)都是404!查看請求路徑發(fā)現(xiàn),根路徑發(fā)生了變化。
而資源的引入方式:
<link ref="stylesheet" href="./static/css/base.css" /> <script type="text/javascript" src="./static/js/app.js" /> <img src="./static/img/bg.png" />
這種引入方式在hash模式下是可行的,因為hash模式監(jiān)聽的是hash值的變化,./的相對路徑不變,始終是根路徑;
https://www.plysummer.com/#/login
https://www.plysummer.com/#/plan/index
但在history模式下,./的相對路徑是變化的
https://www.plysummer.com/login
https://www.plysummer.com/plan/index
/login映射的頁面中的資源路徑,與/plan/index映射的頁面資源路徑,各不相同!所以,在嵌套路由中出現(xiàn)資源加載失敗問題。
解決方式也很簡單:相對于根目錄就可以了!
./ 表示相對于當前目錄,/則是一個絕對目錄,www.plysummer.com映射的根路徑,對于上面的Nginx配置(root html;),根路徑就是nginx/html目錄!
在Vue項目的Webpack配置中,history模式的 publicPath 應(yīng)該配置為/,而并非./等相對路徑;
在引入資源時,如img:src,也應(yīng)該使用/static/xxx,而不是./static/xxx。
其實hash模式下的./本身就是相對于根路徑,所以 / 的設(shè)置在兩種模式下是通用的!
兩種模式的比較
-
history模式是H5新特性,URL更優(yōu)雅,但history模式需要服務(wù)器配合,而hash不需要; -
pushState設(shè)置的新URL必須是與當前URL同源的任意URL,而hash只是修改#后面的部分,所以只能設(shè)置與當前同文檔的URL; -
pushState設(shè)置的URL即使與當前URL一樣,也會被添加進歷史棧中;而hash設(shè)置的值必須與當前的不一樣才會被添加進歷史棧; -
pushState可以通過第一個參數(shù)stateObject向記錄中添加任意類型的數(shù)據(jù),而hash只能添加短字符串; -
pushState可額外設(shè)置title屬性供后續(xù)使用; -
hash兼容IE8以上,history兼容IE10以上。
擴展
另外,vue-router還提供了第三種模式:abstract,使用一個不依賴于瀏覽器的瀏覽歷史虛擬管理后端。
根據(jù)平臺差異可以看出,在 Weex 環(huán)境中只支持使用 abstract 模式。
不過,vue-router 自身會對環(huán)境做校驗,如果發(fā)現(xiàn)沒有瀏覽器的API,vue-router會自動強制進入abstract模式,所以在使用 vue-router時,只要不聲明 mode,默認會在瀏覽器環(huán)境中使用hash模式,在移動端原生環(huán)境中使用 abstract 模式。
SSR
雖然前端渲染有諸多好處,但SEO的問題還是比較突出的。所以React、Vue等框架也在服務(wù)端渲染上做了一些努力,也就是SSR,但又和傳統(tǒng)的服務(wù)端渲染有所不同。
前端框架的服務(wù)端渲染(SSR)大多依然采用前端路由,并且由于引用了狀態(tài)統(tǒng)一、VNode等等概念,導(dǎo)致SSR對服務(wù)器的性能要求比傳統(tǒng)的模板引擎渲染對服務(wù)器的性能要求高得多!所以不僅前端框架本身在不斷改進算法、優(yōu)化,服務(wù)端的性能也必須有所提升。ps:當初掘金換成SSR時也遇到了對應(yīng)的性能問題,就是這個原因。
當然,在二者之間,也許出現(xiàn)了預(yù)渲染的概念。即現(xiàn)在服務(wù)端構(gòu)建出一部分靜態(tài)的HTML文件,剩下的頁面再通過常規(guī)的前端渲染來實現(xiàn)。通??梢园咽醉摬捎妙A(yù)渲染的方式。好處也比較明顯,兼顧了SEO和服務(wù)器的性能要求。不過,它無法做到全站SEO,生產(chǎn)構(gòu)建階段耗時也會有所提高。
關(guān)于預(yù)渲染,可以考慮使用webpack插件 prerender-spa-plugin
前后端分離
得益于前端路由和現(xiàn)代前端框架完整的前后端渲染能力,跟頁面渲染、組織、組件相關(guān)的工作,后端終于可以不用再參與了。
前后端分離的開發(fā)模式也逐漸開始普及。前端開始更加注重頁面開發(fā)的工程化、自動化,而后端則更專注于api的提供和數(shù)據(jù)庫的保障。代碼層面上耦合度也進一步降低,分工也更加明確。
總結(jié)
-
后端路由
- 優(yōu)點
- 缺點
每次更新頁面都需要發(fā)起新的請求,服務(wù)器壓力會很大,如果網(wǎng)絡(luò)狀況不好,還會造成極差的用戶體驗。
-
前端路由
- 優(yōu)點
- 用戶體驗好,和后臺網(wǎng)速沒有關(guān)系,不需要每次都從服務(wù)器全部獲取,界面展現(xiàn)快;
- 可以在瀏覽器中輸入指定想要訪問的
URL路徑地址; - 實現(xiàn)了前后端的分離,方便開發(fā)。
- 缺點
- 等待js加載完畢,且執(zhí)行完畢,才能渲染出首屏頁面;
- 對SEO不友好;頁面中只有一個元素
<div id="app"></div>,爬蟲/搜索引擎認為頁面是空的 - 在瀏覽器前進和后退時會重新發(fā)送請求(因為組件重新掛載),沒有合理緩存數(shù)據(jù);
- 優(yōu)點
-
SSR是傳統(tǒng)服務(wù)端渲染與SPA之間的一個折中方案,后端渲染出完整的首屏DOM結(jié)構(gòu),返回給前端,后續(xù)的頁面操作再利用單頁的路由跳轉(zhuǎn)和渲染。-
vue SSR - Nuxt的方法nuxtServerInit僅在服務(wù)端初始化渲染時 執(zhí)行一次!在刷新瀏覽器時填充vuex,即持久化數(shù)據(jù) -
nuxt隱藏了很多細節(jié),如 開發(fā)過程中,頁面都是.vue文件,需要用vue-loader構(gòu)建,所以SSR環(huán)境需要webpack打包。- 頁面可能在服務(wù)端渲染(首屏),因此需要
Server entry執(zhí)行首屏渲染邏輯,將來打包輸出Server Bundle - 頁面也可能在客戶端渲染(瀏覽器端操作),因此需要
Client entry執(zhí)行相關(guān)控制邏輯,將來打包輸出Client Bundle
- 頁面可能在服務(wù)端渲染(首屏),因此需要
-