目前單頁應用(SPA)越來越成爲前端主流,單頁應用一大特點就是使用前端路由,由前端來直接控制路由跳轉邏輯,而不再由後端人員控制,這給了前端更多的自由。
目前前端路由主要有兩種實現方式:hash
模式和 history
模式,下面分別詳細說明。
1. hash模式
比如在用超鏈接製作錨點跳轉的時候,就會發現,url後面跟了"#id",hash值就是url中從"#"號開始到結束的部分。
hash值變化瀏覽器不會重新發起請求,但是會觸發window.hashChange
事件,假如我們在hashChange事件中獲取當前的hash值,並根據hash值來修改頁面內容,則達到了前端路由的目的。
<!-- html:菜單中href設置爲hash形式,id爲app中放置頁面內容 -->
<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>
//在window.onhashchange中獲取hash值,根據不同的值,修改app中不同的內容,起到了路由的效果
function hashChange(e){
// console.log(location.hash)
// console.log(location.href)
// console.log(e.newURL)
// console.log(e.oldURL)
let app = document.getElementById('app')
switch (location.hash) {
case '#index':
app.innerHTML = '<h1>這是首頁內容</h1>'
break
case '#news':
app.innerHTML = '<h1>這是新聞內容</h1>'
break
case '#user':
app.innerHTML = '<h1>這是個人中心內容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
window.onhashchange = hashChange
hashChange()
上面這個實現方式比較簡陋,我們可以再封裝一下:
class Router {
constructor(){
this.routers = [] //存放我們的路由配置
}
add(route,callback){
this.routers.push({
path:route,
render:callback
})
}
listen(callback){
window.onhashchange = this.hashChange(callback)
this.hashChange(callback)() //首次進入頁面的時候沒有觸發hashchange,必須要就單獨調用一下
}
hashChange(callback){
let self = this
return function () {
let hash = location.hash
console.log(hash)
for(let i=0;i<self.routers.length;i++){
let route = self.routers[i]
if(hash===route.path){
callback(route.render())
return
}
}
}
}
}
let router = new Router()
router.add('#index',()=>{
return '<h1>這是首頁內容</h1>'
})
router.add('#news',()=>{
return '<h1>這是新聞內容</h1>'
})
router.add('#user',()=>{
return '<h1>這是個人中心內容</h1>'
})
router.listen((renderHtml)=>{
let app = document.getElementById('app')
app.innerHTML = renderHtml
})
實現一個Router類,通過add方法添加路由配置,第一個參數爲路由路徑,第二個參數爲render函數,返回要插入頁面的html;通過listen方法,監聽hash變化,並將每個路由返回的html,插入到app中。這樣我們就實現了一個簡單的hash路由。
2. history模式
hash模式看起來是比較醜的,都帶個"#"號,我們也可以採取history模式,history就是我們平時看到的正常的連接形式。history模式基於window.history
對象的方法。
在HTML4中,已經支持window.history
對象來控制頁面歷史記錄跳轉,常用的方法包括:
history.forward()
:在歷史記錄中前進一步history.back()
:在歷史記錄中後退一步history.go(n)
:在歷史記錄中跳轉n步驟,n=0爲刷新本頁,n=-1爲後退一頁。
在HTML5中,window.history
對象得到了擴展,新增的API包括:
history.pushState(data[,title][,url])
:向歷史記錄中追加一條記錄history.replaceState(data[,title][,url])
:替換當前頁在歷史記錄中的信息。history.state
:是一個屬性,可以得到當前頁的state信息。window.onpopstate
:是一個事件,在點擊瀏覽器後退按鈕或js調用forward()
、back()
、go()
時觸發。監聽函數中可傳入一個event對象,event.state
即爲通過pushState()
或replaceState()
方法傳入的data參數
history模式原理可以這樣理解,首先我們要改造我們的超鏈接,給每個超鏈接增加onclick方法,阻止默認的超鏈接跳轉,改用history.pushState
或history.replaceState
來更改瀏覽器中的url,並修改頁面內容。由於通過history的api調整,並不會向後端發起請求,所以也就達到了前端路由的目的。
如果用戶使用瀏覽器的前進後退按鈕,則會觸發window.onpopstate
事件,監聽頁面根據路由地址修改頁面內容。
也不一定非要用超鏈接,任意元素作爲菜單都行,只要在點擊事件中通過 history 進行調整即可。
<!--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>
//js:
//改造超鏈接,阻止默認跳轉,默認的跳轉是會刷新頁面的
document.querySelector('#menu').addEventListener('click',function (e) {
if(e.target.nodeName ==='A'){
e.preventDefault()
let path = e.target.getAttribute('href') //獲取超鏈接的href,改爲pushState跳轉,不刷新頁面
window.history.pushState({},'',path) //修改瀏覽器中顯示的url地址
render(path) //根據path,更改頁面內容
}
})
function render(path) {
let app = document.getElementById('app')
switch (path) {
case '/index':
app.innerHTML = '<h1>這是首頁內容</h1>'
break
case '/news':
app.innerHTML = '<h1>這是新聞內容</h1>'
break
case '/user':
app.innerHTML = '<h1>這是個人中心內容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
//監聽瀏覽器前進後退事件,並根據當前路徑渲染頁面
window.onpopstate = function (e) {
render(location.pathname)
}
//第一次進入頁面顯示首頁
render('/index')
上面這個寫法太low,我們可以用類封裝一下,通過 add 方法添加路由,通過 pushState 進行跳轉,初始化時更改所以超鏈接的跳轉方式:
class Router {
constructor(){
this.routers = []
this.renderCallback = null
}
add(route,callback){
this.routers.push({
path:route,
render:callback
})
}
pushState(path,data={}){
window.history.pushState(data,'',path)
this.renderHtml(path)
}
listen(callback){
this.renderCallback = callback
this.changeA()
window.onpopstate = ()=>this.renderHtml(this.getCurrentPath())
this.renderHtml(this.getCurrentPath())
}
changeA(){
document.addEventListener('click', (e)=> {
if(e.target.nodeName==='A'){
e.preventDefault()
let path = e.target.getAttribute('href')
this.pushState(path)
}
})
}
getCurrentPath(){
return location.pathname
}
renderHtml(path){
for(let i=0;i<this.routers.length;i++){
let route = this.routers[i]
if(path===route.path){
this.renderCallback(route.render())
return
}
}
}
}
let router = new Router()
router.add('/index',()=>{
return '<h1>這是首頁內容</h1>'
})
router.add('/news',()=>{
return '<h1>這是新聞內容</h1>'
})
router.add('/user',()=>{
return '<h1>這是個人中心內容</h1>'
})
router.listen((renderHtml)=>{
let app = document.getElementById('app')
app.innerHTML = renderHtml
})
當然,上面這個實現只是一個非常初級的 demo,並不能用於真正的開發場景,只是加深對前端路由的理解。
3. hash模式和history模式的區別
- hash 模式較醜,history 模式較優雅
- pushState 設置的新 URL 可以是與當前 URL 同源的任意 URL;而 hash 只可修改 # 後面的部分,故只可設置與當前同文檔的 URL
- pushState 設置的新 URL 可以與當前 URL 一模一樣,這樣也會把記錄添加到棧中;而 hash 設置的新值必須與原來不一樣纔會觸發記錄添加到棧中
- pushState 通過 stateObject 可以添加任意類型的數據到記錄中;而 hash 只可添加短字符串
- pushState 可額外設置 title 屬性供後續使用
- hash 兼容IE8以上,history 兼容 IE10 以上
- history 模式需要後端配合將所有訪問都指向 index.html,否則用戶刷新頁面,會導致 404 錯誤