hash定義
hash這個玩意是地址欄上#及后面部分,代表網頁中的一個位置,#后面部分為位置標識符。頁面打開后,會自動滾動到指定位置處。
位置標識符 ,一是使用錨點,比如<a name="demo"></a>
,二是使用id屬性,比如 <span id="demo" ></span>
帶hash的請求
當打開http://www.example.com/#print服務器實際收到的請求地址是http://www.example.com/,是不帶hash值的。
那么你真想帶#字符咋辦,轉義啊, #轉義字符為%23。也許有人會說,我咋知道這個轉義啊,呵呵噠encodeURIComponent。
hashchange事件
從The HashChangeEvent interface可以看到hashchange事件的參數HashChangeEvent繼承了Event,僅僅多了兩個屬性
- oldURL 先前會話歷史記錄的URL
- newURL 當前會話歷史記錄的URL
簡單的調用方式
window.onhashchange = function(e){
console.log('old URL:', e.oldURL)
console.log('new URL', e.newURL)
}
hash | CAN I USE 上可以看到除了IE8一下和那個尷尬的Opera Mini,hashchange事件都是支持得很好。那么怎么做到兼容,用MDN的代碼做個引子
function(window) {
if ("onhashchange" in window) { return; }
var location = window.location,
oldURL = location.href,
oldHash = location.hash;
setInterval(function() {
var newURL = location.href, newHash = location.hash;
if (newHash != oldHash && typeof window.onhashchange === "function") {
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
oldURL = newURL;
oldHash = newHash;
}
}, 100);
}
hash history 簡單版本實現
從上面可以得知,我們的實現思路就是監聽hashchange事件,這里先拋開兼容性問題。
1 首先監聽hashchange事件,定義個RouterManager函數
- bind(this)讓函數this指向RouterManager實例
- 取到oldURL和newURL,同時查找一下是否注冊,然后加載相關路由
function RouterManager(list, index) {
if (!(this instanceof RouterManager)) {
return new RouterManager(arguments)
}
this.list = {} || list
this.index = index
this.pre = null
this.current = null
win.addEventListener('hashchange', function (ev) {
var pre = ev.oldURL.split('#')[1],
cur = ev.newURL.split('#')[1],
preR = this.getByUrlOrName(pre),
curR = this.getByUrlOrName(cur)
this.loadWithRouter(curR, preR)
}.bind(this))
}
2 定義添加,刪除,加載,和初始化等方法
- add的時候,判斷是不是string, 如果是,重新構造一個新的router實例配置
- load這里主要是用來還原直接輸入帶hash的地址,比如 http://ex.com/#music
- loadWithRouter是最終渲染的入口
- getByUrlOrName,你可以通過名字和path查找路由,name是方便日后擴展
- setIndex設置默認路由地址
- go, back, forward同history的方法
- init里面會檢測地址是不是帶hash,然后走不通的邏輯。history.replaceState這是因為,如果不這么做, http://ex.com/跳轉到http://ex.com/#/music會產生兩條歷史記錄,這是我們不期望的。
RouterManager.prototype = {
add: function (router, callback) {
if (typeof router === 'string') {
router = {
path: router,
name: router,
callback: callback
}
}
this.list[router.name || router.path] = router
},
remove: function (name) {
delete this.list[name]
},
get: function (name) {
return this.getByUrlOrName(name)
},
load: function (name) {
if (!name) {
name = location.hash.slice(1)
}
var r = this.getByUrlOrName(name)
this.loadWithRouter(r, null)
},
loadWithRouter(cur, pre) {
if (cur && cur.callback) {
this.pre = this.current || cur
cur.callback(cur, pre)
this.current = cur
} else {
this.NOTFOUND('未找到相關路由')
}
},
getByUrlOrName: function (nameOrUrl) {
var r = this.list[nameOrUrl]
if (!r) {
r = Object.values(this.list).find(rt => rt.name === nameOrUrl || rt.path === nameOrUrl)
}
return r
},
setIndex: function (nameOrUrl) {
this.indexRouter = this.getByUrlOrName(nameOrUrl)
},
go: function (num) {
win.history.go(num)
},
back: function () {
win.history.back()
},
forward: function () {
win.history.forward()
},
init: function () {
// 直接輸入是帶hash的地址,還原
if (win.location.hash) {
/* 模擬事件
var ev = document.createEvent('Event')
ev.initEvent('hashchange', true, true)
ev.oldURL = ev.newURL = location.href
win.dispatchEvent(ev) */
this.load()
} else if (this.indexRouter) { // 是不帶hash的地址,跳轉到指定的首頁
if ('replaceState' in win.history) {
// 替換地址
win.history.replaceState(null, null, win.location.href + '#' + this.indexRouter.path)
} else {
win.location.hash = this.indexRouter.path
}
}
}
}
3 公布函數
RouterManager.prototype.use = RouterManager.prototype.add
win.Router = RouterManager
4 頁面怎么配置,簡單的利用a標簽href
<ul>
<li>
<li>
<a href="#/m1">菜單1</a>
</li>
<ul>
<li>
<a href="#/m11">菜單11</a>
</li>
<li>
<a href="#/m12">菜單12</a>
</li>
</ul>
</li>
<li>
<a href="#/m2">菜單2</a>
</li>
<li>
<a href="#/m3">菜單3</a>
</li>
</ul>
5 注冊,當然你也可以通過選擇器批量注冊
var router = new Router()
router.NOTFOUND = function (msg) {
content.innerHTML = msg
}
router.use('/m1', function (r) {
req(r.path.slice(1))
})
router.use('/m11', function (r) {
req(r.path.slice(1))
})
router.use('/m12', function (r) {
req(r.path.slice(1))
})
router.use('/m2', function (r) {
req(r.path.slice(1))
})
router.use('/m3', function (r) {
req(r.path.slice(1))
})
router.setIndex('/m1')
router.init()
為了方便演示,定義req,ajax方法,模擬ajax請求
function req(url) {
ajax(url, function (res) {
content.innerHTML = res
})
}
function ajax(id, callback) {
callback(
{
'm1': '菜單1的主區域內容',
'm11': '菜單11的主區域內容',
'm12': '菜單12的主區域內容',
'm2': '菜單2的主區域內容',
'm3': '菜單3的主區域內容'
}[id] || '404 Not Found!')
}
6 demo地址
7 源碼地址
簡單的前端hash路由
8 下一步
這就成了最簡單最基本的路由了。讓然還有很多要考慮,比如如下
- 動態路由匹配
- 嵌套路由
- 重定向和別名
- 錯誤捕捉
- 生命周期鈎子
- 等等等
hash | CAN I USE
The HashChangeEvent interface
onhashchange | MDN
window.location.hash 使用說明
JS單頁面應用實現前端路由(hash)
Ajax保留瀏覽器歷史的兩種解決方案(Hash&Pjax)
理解瀏覽器的歷史記錄
理解瀏覽器歷史記錄(2)-hashchange、pushState
Web開發中 前端路由 實現的幾種方式和適用場景
自己動手寫一個前端路由插件
vue-router
react-router