關(guān)于路由
路由其實(shí)是根據(jù)不同的URL
地址展示不同的內(nèi)容或頁面塘幅;
廣義上來說昔案,訪問路由會(huì)映射到相應(yīng)的函數(shù)里,然后由相應(yīng)的函數(shù)來決定返回給這個(gè)URL
的內(nèi)容电媳。路由就是一個(gè)匹配過程踏揣;
后端路由
在Web前端開發(fā)早期,一直是后端路由占據(jù)主導(dǎo)地位匾乓,不管是PHP
捞稿,還是JSP、ASP
拼缝,用戶通過URL
訪問頁面時(shí)娱局,大多是通過后端路由匹配之后再返回給瀏覽器。經(jīng)典面試題[說說瀏覽器地址欄輸入www.baidu.com到網(wǎng)頁展示的過程]
其實(shí)也是在講這個(gè)道理珍促。
不管是什么語言的Web后端框架铃辖,都會(huì)有一個(gè)專門的路由模塊或路由區(qū)域,用于匹配用戶給出的URL
地址猪叙,以及一些表單提交娇斩、AJAX
請求的地址仁卷。通常遇到無法匹配的路由,后端會(huì)返回一個(gè)404
狀態(tài)碼犬第,這也是404 NOT FOUND
的由來锦积。
服務(wù)端渲染
在后端為主導(dǎo)的年代,網(wǎng)頁HTML
一般是后端通過模板引擎渲染好之后響應(yīng)給前端歉嗓,這就是服務(wù)端渲染丰介。瀏覽器在地址欄中切換不同的URL
時(shí),每次都會(huì)向后端發(fā)出請求鉴分,服務(wù)器響應(yīng)請求哮幢。
服務(wù)端渲染的好處有很多,比如對SEO
友好志珍,一些對安全性要求高的頁面采用服務(wù)端渲染更保險(xiǎn)橙垢。
Node.js
誕生以后,前端擁有自己的后端模板引擎成為了現(xiàn)實(shí)伦糯,常見的有ejs柜某、nunjucks
。這些模板引擎搭配Express敛纲、Koa
等Node
框架也風(fēng)靡一時(shí)喂击。
不過,隨著Web
應(yīng)用的開發(fā)越來越復(fù)雜,單純的服務(wù)端渲染問題開始慢慢暴露了出來:耦合性太強(qiáng)!耦合性問題雖然能通過良好的代碼結(jié)構(gòu)、規(guī)范來解決,但jQuery
時(shí)代的頁面不好維護(hù)也是有目共睹的邑彪,全局變量滿天飛,代碼入侵性太高衫嵌;后續(xù)維護(hù)通常也是在給前面的代碼打補(bǔ)峨剐凇;頁面切換白屏問題雖然可以通過AJAX
或iframe
等方案解決稚补,但實(shí)際上卻進(jìn)一步增加了可維護(hù)性的難度童叠。
前端路由
前端路由:頁面跳轉(zhuǎn)的URL
規(guī)則匹配由前端來控制;應(yīng)用最廣泛的例子就是當(dāng)今的SPA
的Web項(xiàng)目课幕。
前端渲染:以Vue
項(xiàng)目為例厦坛,瀏覽器從服務(wù)器拿到的HTML
里只有一個(gè)<div id="app"></div>
,并搭配一系列js
文件乍惊。所以杜秸,我們看到的頁面其實(shí)是通過這些js
渲染出來的。
前端渲染把渲染的任務(wù)交給了瀏覽器润绎,通過客戶端的算力來解決頁面的構(gòu)建撬碟,在很大程度上緩解了服務(wù)端的壓力诞挨。而且配合前端路由,無縫的頁面切換體驗(yàn)呢蛤,自然對用戶是友好的惶傻。不過帶來的壞處就是對
SEO
不友好,畢竟搜索引擎的爬蟲只能爬到上面那個(gè)空蕩蕩的HTML
其障,而且對瀏覽器的版本也會(huì)有相應(yīng)的要求银室。
注意:只要在瀏覽器地址欄輸入URL
再回車,是一定會(huì)去后端服務(wù)器請求一次的励翼。而如果是在頁面里通過點(diǎn)擊按鈕等操作蜈敢,利用router
庫的api
來進(jìn)行的URL
更新,則不會(huì)去后端服務(wù)器請求汽抚。
前端路由主要有兩種方式:
-
hash
模式扶认,錨點(diǎn)操作,利用hash
值的變化感知路由變化殊橙,優(yōu)點(diǎn)是兼容性高辐宾,缺點(diǎn)是URL
帶有#
號(hào)不好看,而且有些場景如微信分享 會(huì)破壞掉#
后面的內(nèi)容膨蛮; -
HTML5
的history
模式叠纹,優(yōu)點(diǎn)是URL
不帶#
號(hào),缺點(diǎn)是需要瀏覽器和后端同時(shí)支持敞葛。
hash模式
hash
是瀏覽器URL
中 #
后面的內(nèi)容誉察,包含#
。hash
是URL
中的錨點(diǎn)惹谐,代表網(wǎng)頁中的一個(gè)位置持偏,單單改變 #
后的部分,瀏覽器只會(huì)加載相應(yīng)位置的內(nèi)容氨肌,不會(huì)重新加載頁面鸿秆。
-
#
是用來指導(dǎo)瀏覽器動(dòng)作的,對服務(wù)器完全無用怎囚,HTTP
請求中并不包含#
卿叽; - 每一次改變
#
后的部分,都會(huì)在瀏覽器的訪問歷史中增加一個(gè)記錄恳守,點(diǎn)擊后退按鈕考婴,就可以回到上一個(gè)位置。
所以說Hash
模式通過錨點(diǎn)值的改變催烘,根據(jù)不同的值沥阱,渲染指定DOM
位置的不同數(shù)據(jù)。
https://www.abc.com/xv/Home/index#plan
https://www.abc.com/xv/Home/index/#/add/index
hash
值的變化不會(huì)讓瀏覽器重新發(fā)起請求伊群,但會(huì)觸發(fā) window.onhashChange
事件考杉;
觸發(fā)hashChange
事件的情況:
- 直接更改瀏覽器地址屁使,在最后面增加或改變
#hash
; - 改變
location.href
或location.hash
的值奔则; - 通過觸發(fā)點(diǎn)擊帶錨點(diǎn)的鏈接蛮寂;
- 瀏覽器前進(jìn)后退可能導(dǎo)致
hash
的變化,前提是兩個(gè)地址中的文檔相同易茬、但hash
值不同酬蹋。
如果我們在 hashChange
事件中獲取當(dāng)前的hash
值,根據(jù)hash
值來修改頁面內(nèi)容抽莱,就能達(dá)到前端路由的目的范抓。
另外, hashChange
事件回調(diào)的對象參數(shù)中食铐,有兩個(gè)比較重要的屬性newURL匕垫、oldURL
,分別表示當(dāng)前變化前虐呻、后的URL
簡略版
#html:
<ul>
<li><a href="#index">首頁</a></li>
<li><a href="#news">資訊</a></li>
<li><a href="#user">個(gè)人中心</a></li>
</ul>
<div id="app"></div>
#script: 應(yīng)該封裝成Router
const app = document.getElementById('app')
function hashChange(e){
// 當(dāng)前跳轉(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>這是個(gè)人中心內(nèi)容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
window.onhashchange = hashChange
hashChange()
除此之外象泵,還需要記錄當(dāng)前URL
,監(jiān)聽刷新事件(onload
)斟叼,在onhashchange
中實(shí)現(xiàn)回退和前進(jìn)等功能偶惠。。朗涩。
history模式
history
其實(shí)瀏覽器歷史棧(歷史記錄)的一個(gè)接口忽孽,基于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()
在歷史棧中前進(jìn)一步兄一; -
history.back()
在歷史棧中后退一步; -
history.go(n)
在歷史棧中跳轉(zhuǎn)n
步驟识腿,n=0
為刷新本頁出革,n=-1
為后退一頁。
-
- 在
HTML5
中覆履,window.history
對象得到了擴(kuò)展蹋盆,新增的API包括:-
history.pushState(data [,title] [,url])
向歷史棧中追加一條記錄费薄,data
表示需要保存的數(shù)據(jù)硝全,在觸發(fā)popstate
事件時(shí),可以在event.state
里獲壤懵铡伟众;# 當(dāng)前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])
替換當(dāng)前頁在歷史棧中的記錄,其他特性與pushState
一致召廷; -
history.state
是一個(gè)屬性凳厢,可以得到當(dāng)前頁的state
信息账胧; -
history.length
當(dāng)前歷史棧中的記錄數(shù); -
window.onpopstate
是一個(gè)事件先紫,只有在點(diǎn)擊瀏覽器前進(jìn)治泥、后退按鈕,js
調(diào)用forward()遮精、back()居夹、go()
時(shí)觸發(fā)。
-
-
注意:
-
IE9
及其以下版本瀏覽器是不支持的本冲,IE10
開始支持准脂。vue-router
會(huì)檢測瀏覽器版本,當(dāng)無法啟用history
模式時(shí)會(huì)自動(dòng)降級(jí)為hash
模式檬洞; -
pushState()/replaceState()
雖然可以改變歷史棧狸膏,讓瀏覽器地址欄中的URL
發(fā)生變化,但并不會(huì)向后端發(fā)起請求添怔! -
pushState()/replaceState()
對URL
的修改受同源策略限制湾戳,防止惡意腳本模仿其他網(wǎng)站的URL
欺騙用戶,所以當(dāng)違背同源策略時(shí)將會(huì)報(bào)錯(cuò)广料; - 火狐目前會(huì)忽略
title
參數(shù)院塞。
-
- 簡略版
# html
<ul id="menu">
<li><a href="/index">首頁</a></li>
<li><a href="/news">資訊</a></li>
<li><a href="/user">個(gè)人中心</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> 的默認(rèn)事件,默認(rèn)的跳轉(zhuǎn)會(huì)刷新頁面
//獲取超鏈接的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>這是個(gè)人中心內(nèi)容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
//監(jiān)聽瀏覽器前進(jìn)后退事件糜颠,并根據(jù)當(dāng)前路徑渲染頁面
window.onpopstate = e => {
render(location.pathname)
}
//第一次進(jìn)入頁面顯示首頁
render('/index')
我們還可以通過自定義事件汹族,實(shí)現(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ù)塞進(jìn)去
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)時(shí)其兴,通常是利用
history API
完成的顶瞒,router
庫調(diào)用history.pushState()
跟后端沒有任何關(guān)系。但是一旦從瀏覽器地址欄里輸入一個(gè)URL(不管是否有效)
并回車或者手動(dòng)刷新頁面元旬,那就會(huì)向后端發(fā)起一個(gè)GET
請求榴徐。而后端路由表中又沒有配置相應(yīng)的路由,那么自然就會(huì)返回404 NOT FOUND
匀归!這也就是為什么很多人在生產(chǎn)模式下遇到404
頁面的原因坑资。
-
hash
模式,發(fā)送的HTTP
請求是不變的穆端,不包含錨點(diǎn)部分袱贮,本質(zhì)上始終請求的是打包后的index.html
; -
history
模式体啰,發(fā)送的HTTP
請求是完整的瀏覽器地址攒巍,對后端來說嗽仪,這樣的路由是不存在,所以會(huì)出現(xiàn)404
柒莉。
這也就是history
模式為什么需要后端同時(shí)支持闻坚。
vue-router
文檔上給出了一個(gè)配置例子:在所有后端路由規(guī)則的最后,加上一個(gè)默認(rèn)匹配規(guī)則 --
如果URL
匹配不到任何靜態(tài)資源兢孝,則響應(yīng)同一個(gè) index.html
給前端鲤氢。
這樣就解決了后端路由拋出的404
問題,前端拿到的也始終是打包后的index.html
了西潘。再通過路由庫的處理卷玉,獲取地址欄的URL
信息,告知前端庫(Vue喷市、React
)渲染對應(yīng)的頁面相种。到了這一步就跟hash
模式類似了。
但 這樣在后端配置之后品姓,404
頁面的處理權(quán)又交回了前端寝并。以Nginx
和vue-router
為例,同時(shí)解決手動(dòng)刷新瀏覽器和手動(dòng)輸入URL
并回車的 404
問題:
# 后端:nginx.conf
server {
listen 8080;
server_name xx.xxx.xxx.xx;
root html; # vue項(xiàng)目的打包后的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')
},
// 當(dāng)輸入不存在的URL時(shí),在前端重定向到404頁面
{ path: "*", redirect: "/404" }
問題延伸:
在IE
瀏覽器下刷新仍然還是404
植酥,是因?yàn)?code>IE自作聰明镀岛,對于頁面大小 < 1024b
會(huì)被認(rèn)為十分不友好,所以ie就將改頁面給替換成自己的錯(cuò)誤提示頁面了友驮,而SPA
打包后的 index.html
可能會(huì)小于臨界值漂羊。
資源路徑問題
在history
模式下,訪問路由和嵌套路由頁面卸留,顯示正常走越,但是刷新頁面的時(shí)候,嵌套路由頁面就出異常了耻瑟!查看網(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
模式下是可行的低淡,因?yàn)?code>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)資源加載失敗問題皂吮。
解決方式也很簡單:相對于根目錄就可以了!
./
表示相對于當(dāng)前目錄税手,/
則是一個(gè)絕對目錄蜂筹,www.plysummer.com
映射的根路徑,對于上面的Nginx
配置(root html;
)芦倒,根路徑就是nginx/html
目錄艺挪!
在Vue
項(xiàng)目的Webpack
配置中,history
模式的 publicPath
應(yīng)該配置為/
兵扬,而并非./
等相對路徑麻裳;
在引入資源時(shí),如img:src
器钟,也應(yīng)該使用/static/xxx
津坑,而不是./static/xxx
。
其實(shí)hash
模式下的./
本身就是相對于根路徑傲霸,所以 /
的設(shè)置在兩種模式下是通用的疆瑰!
兩種模式的比較
-
history
模式是H5
新特性,URL
更優(yōu)雅昙啄,但history
模式需要服務(wù)器配合乃摹,而hash
不需要; -
pushState
設(shè)置的新URL
必須是與當(dāng)前URL
同源的任意URL
跟衅,而hash
只是修改#
后面的部分孵睬,所以只能設(shè)置與當(dāng)前同文檔的URL
; -
pushState
設(shè)置的URL即使與當(dāng)前URL
一樣伶跷,也會(huì)被添加進(jìn)歷史棧中掰读;而hash
設(shè)置的值必須與當(dāng)前的不一樣才會(huì)被添加進(jìn)歷史棧; -
pushState
可以通過第一個(gè)參數(shù)stateObject
向記錄中添加任意類型的數(shù)據(jù)叭莫,而hash
只能添加短字符串蹈集; -
pushState
可額外設(shè)置title
屬性供后續(xù)使用; -
hash
兼容IE8
以上雇初,history
兼容IE10
以上拢肆。
擴(kuò)展
另外,vue-router
還提供了第三種模式:abstract
,使用一個(gè)不依賴于瀏覽器的瀏覽歷史虛擬管理后端郭怪。
根據(jù)平臺(tái)差異可以看出支示,在 Weex
環(huán)境中只支持使用 abstract
模式。
不過鄙才,vue-router
自身會(huì)對環(huán)境做校驗(yàn)颂鸿,如果發(fā)現(xiàn)沒有瀏覽器的API
,vue-router
會(huì)自動(dòng)強(qiáng)制進(jìn)入abstract
模式攒庵,所以在使用 vue-router
時(shí)嘴纺,只要不聲明 mode
,默認(rèn)會(huì)在瀏覽器環(huán)境中使用hash
模式浓冒,在移動(dòng)端原生環(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ù)器的性能要求高得多!所以不僅前端框架本身在不斷改進(jìn)算法装哆、優(yōu)化罐脊,服務(wù)端的性能也必須有所提升。ps:
當(dāng)初掘金換成SSR
時(shí)也遇到了對應(yīng)的性能問題蜕琴,就是這個(gè)原因萍桌。
當(dāng)然,在二者之間凌简,也許出現(xiàn)了預(yù)渲染的概念上炎。即現(xiàn)在服務(wù)端構(gòu)建出一部分靜態(tài)的HTML
文件,剩下的頁面再通過常規(guī)的前端渲染來實(shí)現(xiàn)雏搂。通撑菏可以把首頁采用預(yù)渲染的方式。好處也比較明顯凸郑,兼顧了SEO
和服務(wù)器的性能要求裳食。不過,它無法做到全站SEO
芙沥,生產(chǎn)構(gòu)建階段耗時(shí)也會(huì)有所提高诲祸。
關(guān)于預(yù)渲染浊吏,可以考慮使用webpack
插件 prerender-spa-plugin
前后端分離
得益于前端路由和現(xiàn)代前端框架完整的前后端渲染能力,跟頁面渲染救氯、組織找田、組件相關(guān)的工作,后端終于可以不用再參與了径密。
前后端分離的開發(fā)模式也逐漸開始普及午阵。前端開始更加注重頁面開發(fā)的工程化躺孝、自動(dòng)化享扔,而后端則更專注于api
的提供和數(shù)據(jù)庫的保障。代碼層面上耦合度也進(jìn)一步降低植袍,分工也更加明確惧眠。
總結(jié)
-
后端路由
- 優(yōu)點(diǎn)
- 缺點(diǎn)
每次更新頁面都需要發(fā)起新的請求,服務(wù)器壓力會(huì)很大于个,如果網(wǎng)絡(luò)狀況不好氛魁,還會(huì)造成極差的用戶體驗(yàn)。
-
前端路由
- 優(yōu)點(diǎn)
- 用戶體驗(yàn)好厅篓,和后臺(tái)網(wǎng)速?zèng)]有關(guān)系秀存,不需要每次都從服務(wù)器全部獲取,界面展現(xiàn)快羽氮;
- 可以在瀏覽器中輸入指定想要訪問的
URL
路徑地址或链; - 實(shí)現(xiàn)了前后端的分離,方便開發(fā)档押。
- 缺點(diǎn)
- 等待js加載完畢澳盐,且執(zhí)行完畢,才能渲染出首屏頁面令宿;
- 對SEO不友好叼耙;頁面中只有一個(gè)元素
<div id="app"></div>
,爬蟲/搜索引擎認(rèn)為頁面是空的 - 在瀏覽器前進(jìn)和后退時(shí)會(huì)重新發(fā)送請求(因?yàn)榻M件重新掛載)粒没,沒有合理緩存數(shù)據(jù)筛婉;
- 優(yōu)點(diǎn)
-
SSR
是傳統(tǒng)服務(wù)端渲染與SPA
之間的一個(gè)折中方案,后端渲染出完整的首屏DOM
結(jié)構(gòu)癞松,返回給前端爽撒,后續(xù)的頁面操作再利用單頁的路由跳轉(zhuǎn)和渲染。-
vue SSR - Nuxt
的方法nuxtServerInit
僅在服務(wù)端初始化渲染時(shí) 執(zhí)行一次拦惋!在刷新瀏覽器時(shí)填充vuex匆浙,即持久化數(shù)據(jù) -
nuxt
隱藏了很多細(xì)節(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ù)端渲染(首屏)软能,因此需要
-