單頁(yè)面應(yīng)用利用了JavaScript動(dòng)態(tài)變換網(wǎng)頁(yè)內(nèi)容,避免了頁(yè)面重載;路由則提供了瀏覽器地址變化,網(wǎng)頁(yè)內(nèi)容也跟隨變化,兩者結(jié)合起來(lái)則為我們提供了體驗(yàn)良好的單頁(yè)面web應(yīng)用
前端路由實(shí)現(xiàn)方式
路由需要實(shí)現(xiàn)三個(gè)功能:
? ①瀏覽器地址變化,切換頁(yè)面;
? ②點(diǎn)擊瀏覽器【后退】帝嗡、【前進(jìn)】按鈕担神,網(wǎng)頁(yè)內(nèi)容跟隨變化孤里;
? ③刷新瀏覽器,網(wǎng)頁(yè)加載當(dāng)前路由對(duì)應(yīng)內(nèi)容
在單頁(yè)面web網(wǎng)頁(yè)中,單純的瀏覽器地址改變,網(wǎng)頁(yè)不會(huì)重載,如單純的hash網(wǎng)址改變網(wǎng)頁(yè)不會(huì)變化,因此我們的路由主要是通過(guò)監(jiān)聽(tīng)事件,并利用js實(shí)現(xiàn)動(dòng)態(tài)改變網(wǎng)頁(yè)內(nèi)容,有兩種實(shí)現(xiàn)方式:
hash
路由: 監(jiān)聽(tīng)瀏覽器地址hash值變化,執(zhí)行相應(yīng)的js切換網(wǎng)頁(yè)
history
路由: 利用history API實(shí)現(xiàn)url地址改變,網(wǎng)頁(yè)內(nèi)容改變
hash路由
首先定義一個(gè)Router
類(lèi)
class Router {
constructor(obj) {
// 路由模式
this.mode = obj.mode
// 配置路由
this.routes = {
'/index' : 'views/index/index',
'/index/detail' : 'views/index/detail/detail',
'/index/detail/more' : 'views/index/detail/more/more',
'/subscribe' : 'views/subscribe/subscribe',
'/proxy' : 'views/proxy/proxy',
'/state' : 'views/state/stateDemo',
'/state/sub' : 'views/state/components/subState',
'/dom' : 'views/visualDom/visualDom',
'/error' : 'views/error/error'
}
this.init()
}
}
路由初始化init()
時(shí)監(jiān)聽(tīng)load
,hashchange
兩個(gè)事件:
window.addEventListener('load', this.hashRefresh.bind(this), false);
window.addEventListener('hashchange', this.hashRefresh.bind(this), false);
瀏覽器地址hash值變化直接通過(guò)a標(biāo)簽鏈接實(shí)現(xiàn)
<nav id="nav" class="nav-tab">
<ul class='tab'>
<li><a class='nav-item' href="#/index">首頁(yè)</a></li>
<li><a class='nav-item' href="#/subscribe">觀察者</a></li>
<li><a class='nav-item' href="#/proxy">代理</a></li>
<li><a class='nav-item' href="#/state">狀態(tài)管理</a></li>
<li><a class='nav-item' href="#/dom">虛擬DOM</a></li>
</ul>
</nav>
<div id="container" class='container'>
<div id="main" class='main'></div>
</div>
hash值變化后,回調(diào)方法:
/**
* hash路由刷新執(zhí)行
*/
hashRefresh() {
// 獲取當(dāng)前路徑,去掉查詢字符串,默認(rèn)'/index'
var currentURL = location.hash.slice(1).split('?')[0] || '/index';
this.name = this.routes[this.currentURL]
this.controller(this.name)
}
/**
* 組件控制器
* @param {string} name
*/
controller(name) {
// 獲得相應(yīng)組件
var Component = require('../' + name).default;
// 判斷是否已經(jīng)配置掛載元素,默認(rèn)為$('#main')
var controller = new Component($('#main'))
}
考慮到存在多級(jí)頁(yè)面嵌套路由的存在,需要對(duì)嵌套路由進(jìn)行處理:
- 直接子頁(yè)面路由時(shí),按父路由到子路由的順序加載頁(yè)面
- 父頁(yè)面已經(jīng)加載,再加載子頁(yè)面時(shí),父頁(yè)面保留,只加載子頁(yè)面
改造后的路由刷新方法為:
hashRefresh() {
// 獲取當(dāng)前路徑,去掉查詢字符串,默認(rèn)'/index'
var currentURL = location.hash.slice(1).split('?')[0] || '/index';
// 多級(jí)鏈接拆分為數(shù)組,遍歷依次加載
this.currentURLlist = currentURL.slice(1).split('/')
this.url = ""
this.currentURLlist.forEach((item, index) => {
// 導(dǎo)航菜單激活顯示
if (index === 0) {
this.navActive(item)
}
this.url += "/" + item
this.name = this.routes[this.url]
// 404頁(yè)面處理
if (!this.name) {
location.href = '#/error'
return false
}
// 對(duì)于嵌套路由的處理
if (this.oldURL && this.oldURL[0]==this.currentURLlist[0]) {
this.handleSubRouter(item,index)
} else {
this.controller(this.name)
}
});
// 記錄鏈接數(shù)組,后續(xù)處理子級(jí)組件
this.oldURL = JSON.parse(JSON.stringify(this.currentURLlist))
}
/**
* 處理嵌套路由
* @param {string} item 鏈接list中當(dāng)前項(xiàng)
* @param {number} index 鏈接list中當(dāng)前索引
*/
handleSubRouter(item,index){
// 新路由是舊路由的子級(jí)
if (this.oldURL.length < this.currentURLlist.length) {
// 相同路由部分不重新加載
if (item !== this.oldURL[index]) {
this.controller(this.name)
}
}
// 新路由是舊路由的父級(jí)
if (this.oldURL.length > this.currentURLlist.length) {
var len = Math.min(this.oldURL.length, this.currentURLlist.length)
// 只重新加載最后一個(gè)路由
if (index == len - 1) {
this.controller(this.name)
}
}
}
這樣,一個(gè)hash路由組件就實(shí)現(xiàn)了廓旬。
使用時(shí),只需new一個(gè)Router實(shí)例即可:new Router({mode:'hash'})
history 路由
window.history
屬性指向 History 對(duì)象,是瀏覽器的一個(gè)屬性,表示當(dāng)前窗口的瀏覽歷史,History 對(duì)象保存了當(dāng)前窗口訪問(wèn)過(guò)的所有頁(yè)面地址。更多了解History對(duì)象,可參考阮一峰老師的介紹: History 對(duì)象
webpack開(kāi)發(fā)環(huán)境下,需要在devServer對(duì)象添加以下配置:
historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }
history路由主要是通過(guò)history.pushState()
方法向?yàn)g覽記錄中添加一條歷史記錄,并同時(shí)觸發(fā)js回調(diào)加載頁(yè)面
當(dāng)【前進(jìn)】、【后退】時(shí)薄扁,會(huì)觸發(fā)history.popstate
事件,加載history.state
中存放的路徑
history路由實(shí)現(xiàn)與hash路由的步驟類(lèi)似,由于需要配置路由模式切換,頁(yè)面中所有的a鏈接都采用了hash類(lèi)型鏈接,history路由初始化時(shí),需要攔截a標(biāo)簽的默認(rèn)跳轉(zhuǎn):
/**
* history模式劫持 a鏈接
*/
bindLink() {
$('#nav').on('click', 'a.nav-item', this.handleLink.bind(this))
}
/**
* history 處理a鏈接
* @param e 當(dāng)前對(duì)象Event
*/
handleLink(e) {
e.preventDefault();
// 獲取元素路徑屬性
let href = $(e.target).attr('href')
// 對(duì)非路由鏈接直接跳轉(zhuǎn)
if (href.slice(0, 1) !== '#') {
window.location.href = href
} else {
let path = href.slice(1)
history.pushState({
path: path
}, null, path)
// 加載相應(yīng)頁(yè)面
this.loadView(path.split('?')[0])
}
}
history路由初始化需要綁定load
、popstate
事件
this.bindLink()
window.addEventListener('load', this.loadView.bind(this, location.pathname));
window.addEventListener('popstate', this.historyRefresh.bind(this));
瀏覽是【前進(jìn)】或【后退】時(shí),觸發(fā)popstate
事件,執(zhí)行回調(diào)函數(shù)
/**
* history模式刷新頁(yè)面
* @param e 當(dāng)前對(duì)象Event
*/
historyRefresh(e) {
const state = e.state || {}
const path = state.path.split('?')[0] || null
if (path) {
this.loadView(path)
}
}
history路由模式首次加載頁(yè)面時(shí),可以默認(rèn)一個(gè)頁(yè)面,這時(shí)可以用history.replaceState
方法
if (this.mode === 'history' && currentURL === '/') {
history.replaceState({path: '/'}, null, '/')
currentURL = '/index'
}
對(duì)于404頁(yè)面的處理,也類(lèi)似
history.replaceState({path: '/error'}, null, '/error')
this.loadView('/error')
更多源碼請(qǐng)?jiān)L問(wèn)Github