1.Web路由
1.1 后端路由
??????Web路由的概念簡單來說就是根據(jù)不同URL渲染不同的頁面五垮。在前后端不分離的時(shí)代,路由往往指的是后端路由(服務(wù)端路由)第煮,即當(dāng)服務(wù)端接收到客戶端發(fā)來的 HTTP 請(qǐng)求,就會(huì)根據(jù)所請(qǐng)求的相應(yīng) URL抑党,進(jìn)行文件讀取包警,數(shù)據(jù)庫讀取等操作,使用模板引擎將相應(yīng)結(jié)果與模板結(jié)合后進(jìn)行渲染底靠,將渲染完畢的頁面發(fā)送給客戶端害晦。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):seo友好,爬蟲爬取到的頁面就是最終的渲染頁面暑中。
- 缺點(diǎn):每次發(fā)起請(qǐng)求都要刷新頁面壹瘟,用戶體驗(yàn)不好,服務(wù)器壓力大鳄逾。
1.2 前端路由
??????說到前端路由稻轨,必須先提一下Ajax與SPA。Ajax技術(shù)的興起促使了 SPA—單頁面應(yīng)用的出現(xiàn)雕凹,由于Ajax可以做到頁面的局部更新殴俱,因此單頁應(yīng)用頁面的交互和頁面的跳轉(zhuǎn)都是無刷新的,無刷新就意味著無需處理html文件的請(qǐng)求枚抵,因此用戶體驗(yàn)很好线欲。但相應(yīng)的,由于頁面數(shù)據(jù)需要通過Ajax獲取汽摹,因此爬蟲獲取到的html只是模板而不是最終的渲染頁面李丰,因此會(huì)不利于seo。為了實(shí)現(xiàn)單頁應(yīng)用逼泣,所以就有了前端路由趴泌。
??????前端路由的概念簡單來講就是,當(dāng)路由發(fā)生變化圾旨,不請(qǐng)求服務(wù)端踱讨,而是通過js的方式修改dom(組件替換),并發(fā)送Ajax獲取數(shù)據(jù)來達(dá)到頁面跳轉(zhuǎn)的效果砍的。因此實(shí)現(xiàn)前端路由有兩個(gè)關(guān)鍵點(diǎn):
- 如何改變url不讓瀏覽器向服務(wù)器發(fā)送請(qǐng)求痹筛。
- 如何監(jiān)聽到url的變化,并執(zhí)行對(duì)應(yīng)的操作
這里就要引出實(shí)現(xiàn)前端路由的兩種路由模式:hash模式和history模式
2.前端路由的實(shí)現(xiàn)模式
2.1 hash模式
概念
??????hash 就是指 url 后的 # 號(hào)以及后面的內(nèi)容
特點(diǎn)
hash模式有以下幾個(gè)特點(diǎn)
- hash值的變化不會(huì)導(dǎo)致瀏覽器向服務(wù)器發(fā)送請(qǐng)求,不會(huì)引起頁面刷新帚稠。
- hash值變化會(huì)觸發(fā)hashchange事件谣旁。
-
hash值改變會(huì)在瀏覽器的歷史中留下記錄,使用瀏覽器的
后退
按鈕滋早,就可以回到上一個(gè)hash值榄审。 - hash永遠(yuǎn)不會(huì)提交到服務(wù)端,即使刷新頁面也不會(huì)杆麸。
由此可見hash模式的特點(diǎn)完全可以滿足前端路由的實(shí)現(xiàn)需求搁进,所以在 H5 的 history 模式出現(xiàn)之前,基本都是使用 hash 模式來實(shí)現(xiàn)前端路由昔头。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 1饼问、兼容性好,支持低版本和IE瀏覽器揭斧。
- 2莱革、實(shí)現(xiàn)前端路由無需服務(wù)端的支持。
缺點(diǎn):
- URL帶#讹开,路徑丑
2.2 history模式
概念
??????在 HTML5 之前盅视,瀏覽器就已經(jīng)有了 history 對(duì)象來控制頁面歷史記錄跳轉(zhuǎn),主要有以下方法旦万。
history.forward():前進(jìn)
history.back():后退
history.go(n):加載歷史列表中的某個(gè)具體的頁面
??????在 HTML5 的規(guī)范中闹击,history 新增了以下幾個(gè) API:pushState(追加) 和 replaceState(替換),通過這兩個(gè) API 可以改變 url 地址且不會(huì)發(fā)送請(qǐng)求纸型,同時(shí)還新增popstate 事件拇砰。通過這些API就能用另一種方式來實(shí)現(xiàn)前端路由,其實(shí)現(xiàn)原理跟與hash模式 實(shí)現(xiàn)類似狰腌,只是用了 HTML5 的實(shí)現(xiàn)除破,單頁面應(yīng)用的 url 不會(huì)多出一個(gè)#,會(huì)更加美觀琼腔。
關(guān)于History模式有兩點(diǎn)需要說明:
-
history模式如何監(jiān)聽路由變化
history模式下瑰枫,瀏覽器的前進(jìn)后退(history.back(), history.forward()等)會(huì)觸發(fā)popstate 事件,但pushState丹莲,replaceState 并不會(huì)觸發(fā)popstate事件光坝。因此要實(shí)現(xiàn)路由變化的偵聽,我們需要重寫這兩個(gè)方法甥材,可以通過事件中心(EventBus)添加事件通知盯另,這里不具體展開,感興趣的小伙伴可以參考這里洲赵。 -
history模式需要后端支持
由于history模式?jīng)]有 # 號(hào)鸳惯,所以當(dāng)用戶手動(dòng)刷新或直接通過url進(jìn)入應(yīng)用時(shí)商蕴,瀏覽器還是會(huì)給服務(wù)器發(fā)送請(qǐng)求。但服務(wù)端無法識(shí)別這個(gè) url 芝发,因此為了避免出現(xiàn)這種情況绪商,history模式需要服務(wù)端的支持,即服務(wù)端需要把匹配不到的所有路由都重定向到根頁面辅鲸。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 路徑好看
缺點(diǎn):
- 1格郁、兼容性差,不能兼容IE9独悴。
- 2例书、需要服務(wù)端支持。
3.實(shí)現(xiàn)vue-router
??????介紹完前端路由的概念及其實(shí)現(xiàn)模式刻炒,接下來我們嘗試實(shí)現(xiàn)vue-router插件雾叭,具體包括vue-router類,兩個(gè)全局組件:router-link落蝙,router-view以及install方法。
3.1 實(shí)現(xiàn)router類
??????我們使用Hash模式來實(shí)現(xiàn)暂幼,因此vue-router具體要做的核心點(diǎn)就是要添加hashchange和load事件的事件偵聽筏勒,在回調(diào)中根據(jù)當(dāng)前url從路由表中取出對(duì)應(yīng)的路由組件,提供給router-view渲染旺嬉。因此一個(gè)首要的問題就是:如何根據(jù)url從路由表中取出組件管行?
??????一個(gè)基礎(chǔ)的思路是,我們只需在偵聽到url變化時(shí)邪媳,拿到當(dāng)前的hash值捐顷,然后遍歷路由表找到路徑為當(dāng)前hash值的選項(xiàng)的component即可。不過這樣做的問題也很明顯雨效,就是無法處理嵌套路由迅涮,如果我們?cè)诼酚杀碇信渲昧饲短茁酚桑瑒t單靠hash值是無法匹配到子代路由的徽龟。要解決這個(gè)問題叮姑,我們可以用一個(gè)matched數(shù)組來存儲(chǔ)從父代到子代匹配過程中的各級(jí)組件,這樣各級(jí)router-view組件只需按需渲染即可据悔。
??????說到這里传透,又會(huì)引出另一個(gè)問題,如何能做到在url變化時(shí)router-view也能響應(yīng)式的更新极颓。這里可以利用vue響應(yīng)式數(shù)據(jù)的特點(diǎn)朱盐,我們知道單文件組件中data中的數(shù)據(jù)都是響應(yīng)式的,當(dāng)數(shù)據(jù)更新時(shí)菠隆,所有用到該數(shù)據(jù)的地方都會(huì)響應(yīng)式的更新兵琳。而這里router-view組件顯然會(huì)用到matched數(shù)組狂秘,因此我們只需將matched變?yōu)轫憫?yīng)式數(shù)據(jù)即可。具體來說就是使Vue.util.defineReactive這個(gè)api闰围,它可以定義一個(gè)對(duì)象的響應(yīng)屬性赃绊,用法如下:
Vue.util.defineReactive(obj,key,value,fn)
obj: 目標(biāo)對(duì)象,
key: 目標(biāo)對(duì)象屬性羡榴;
value: 屬性值
??????我們用它將matched定義為router實(shí)例的一個(gè)響應(yīng)式屬性碧查,這樣即可實(shí)現(xiàn)matched變化時(shí),router-view也會(huì)響應(yīng)式的渲染校仑。這里還要注意忠售,使用該方法要用到vue實(shí)例,如何拿到vue實(shí)例迄沫?我們可以在vue-router的install方法中拿到并保存稻扬,關(guān)于這一點(diǎn)后面會(huì)解釋。接下來我們按照以上思路羊瘩,首先實(shí)現(xiàn)router類泰佳。
// 用于在Install方法中保存vue實(shí)例
let Vue
class myRouter{
constructor (options){
this.$options = options
// 保存當(dāng)前hash值,即匹配路徑
this.current = window.location.hash.slice(1) || '/' // 給初值
// 保存匹配過程中的各級(jí)路由信息
Vue.util.defineReactive(this, 'matched', [])
// match方法可以遞歸遍歷路由表尘吗,獲得匹配關(guān)系
this.match()
// 添加偵聽事件逝她,事件回調(diào)中用到this,因此要綁定上下文
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange () {
// 更新匹配路徑
this.current = window.location.hash.slice(1)
this.matched = []
this.match()
}
/**
* @description 遍歷路由表,保存匹配關(guān)系
*/
match(routes) {
// 默認(rèn)遍歷總路由表
routes = routes || this.$options.routes;
for (let i = 0; i < routes.length; i++) {
const route = routes[i];
// 嚴(yán)格匹配根路徑
if (route.path === "/" && this.current === "/") {
this.matched.push(route);
break;
// 當(dāng)前路由包含于url 則推入matched數(shù)組并遞歸遍歷其子路由
} else if (route.path !== "/" && this.current.includes(route.path)) {
this.matched.push(route);
if (route.children) {
this.match(route.children);
}
break;
}
}
}
}
3.2 實(shí)現(xiàn)兩個(gè)全局組件
vue-router有兩個(gè)全局組件分別是:
- router-link 路由跳轉(zhuǎn)
- router-view 路由占位符
我們分別來實(shí)現(xiàn)
router-link
??????router-link用來進(jìn)行路由跳轉(zhuǎn)睬捶,他的實(shí)現(xiàn)比較簡單黔宛,因?yàn)槠浔举|(zhì)其實(shí)就是a標(biāo)簽,因此實(shí)現(xiàn)router-link只需渲染一個(gè)a標(biāo)簽即可擒贸。但要注意的是臀晃,由于此時(shí)是運(yùn)行時(shí)環(huán)境,無法進(jìn)行模板編譯介劫,所以不能使用模板語法徽惋,我們可以使用render函數(shù)。
??????具體實(shí)現(xiàn)思路是蜕猫,使用render渲染一個(gè)a標(biāo)簽寂曹,herf屬性對(duì)應(yīng)router-link的to屬性,標(biāo)簽內(nèi)容就是用戶寫在router-view中的內(nèi)容回右,我們可以通過插槽(this.$slots
)來獲取隆圆,并將其添加在實(shí)際的a標(biāo)簽中。
export default {
props: {
to: {
type: String,
required: true
}
},
render (h) {
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
}
}
router-view
??????router-view用來渲染路由組件翔烁,我們前面實(shí)現(xiàn)的router中已經(jīng)添加了對(duì)路由匹配關(guān)系的處理渺氧,他會(huì)根據(jù)當(dāng)前url將各級(jí)匹配關(guān)系存入matched數(shù)組中,router-view如何根據(jù)matched數(shù)組按需渲染呢蹬屹?
??????其實(shí)侣背,對(duì)于一個(gè)嵌套路由來說白华,每一級(jí)路由都有一個(gè)router-view與之對(duì)應(yīng),即router-view也一定是嵌套的贩耐,因此router-view只需知道自身所處的層級(jí)弧腥,具體來說就是matched數(shù)組中的第幾項(xiàng)即可。實(shí)現(xiàn)這一點(diǎn)我們可以給每一個(gè)router-view添加一個(gè)標(biāo)記變量和一個(gè)深度計(jì)數(shù)變量潮太,router-view判斷自己的父節(jié)點(diǎn)有沒有這個(gè)標(biāo)記管搪,有則說明自己是子代路由,則深度加一同時(shí)繼續(xù)向上判斷直到不存在父節(jié)點(diǎn)铡买。這樣最終每個(gè)router-view都會(huì)得到自己所處的層級(jí)更鲁,只需根據(jù)這個(gè)層級(jí)從matched數(shù)組獲取對(duì)應(yīng)的路由組件并渲染即可。下面根據(jù)以上思路來實(shí)現(xiàn)奇钞,注意同樣不能使用模板語法澡为,要使用render函數(shù)。
export default {
render(h) {
// 標(biāo)記自己是父級(jí)router-view
this.$vnode.data.routerView = true;
// 統(tǒng)計(jì)深度
let depth = 0;
let parent = this.$parent;
// 獲取自己的深度
while (parent) {
const vnodeData = parent.$vnode && parent.$vnode.data;
if (vnodeData && vnodeData.routerView) {
depth++;
}
}
// 不斷向上查找
parent = parent.$parent;
}
let component = null;
// 獲取當(dāng)前層級(jí)對(duì)應(yīng)的路由
const route = this.$router.matched[depth];
// 獲取path對(duì)應(yīng)的component
if (route) {
component = route.component;
}
return h(component);
}
};
3.3 實(shí)現(xiàn)install方法
??????vue-router是個(gè)vue插件景埃,我們前面提到過vue插件的實(shí)現(xiàn)原理媒至。它要暴露一個(gè)install方法,用全局混入(Vue.mixin)的方式混入beforeCreate生命周期谷徙,這會(huì)使得所有的組件的beforeCreate鉤子都會(huì)觸發(fā)該行為塘慕。我們?cè)赽eforeCreate中將router實(shí)例掛載到vue原型上,便于在任何地方通過vue原型直接調(diào)用router蒂胞。如何做到這一點(diǎn)呢?
??????我們?cè)谑褂胿ue-router時(shí)會(huì)在main.js中創(chuàng)建Vue根實(shí)例条篷,引入并掛載router選項(xiàng)骗随,也就是說只有Vue根實(shí)例才有router這個(gè)選項(xiàng)。因此我們只需在beforeCreate鉤子中判斷當(dāng)前組件有沒有router選項(xiàng)即可赴叹,有則說明這是vue-router根實(shí)例鸿染,將router其掛載到vue原型即可。
??????前面實(shí)現(xiàn)router類時(shí)說過乞巧,我們要在install方法中保存vue實(shí)例涨椒,為什么可以這樣做呢?vue插件之所以要暴露一個(gè)install方法绽媒,是因?yàn)槲覀兪褂?strong>vue.use()方法注冊(cè)組件時(shí)會(huì)調(diào)用install方法蚕冬,并將vue作為參數(shù)傳入,因此可以在install方法中保存vue實(shí)例是辕。
??????此外囤热,install方法還要注冊(cè)前面實(shí)現(xiàn)的兩個(gè)全局組件。
接下來根據(jù)以上思路具體實(shí)現(xiàn):
myRouter.install = function (_Vue){
// 保存vue實(shí)例
Vue = _Vue
Vue.mixin({
beforeCreate () {
// 確保根實(shí)例的時(shí)候才執(zhí)行获三,因?yàn)橹挥懈鶎?shí)例才有router這個(gè)選項(xiàng)旁蔼。
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
//注冊(cè)組件
Vue.component('router-link', Link)
Vue.component('router-view', View)
}
至此锨苏,基于hash模式的丐版vue-router的已經(jīng)完成。
水平有限棺聊,歡迎指正??伞租。
參考:
https://juejin.cn/post/6844903695365177352#heading-15
https://juejin.cn/post/6854573222231605256#heading-14