????窗外的綠色滿滿撞入我的眼睛,柳絮隔著紗窗熱情的邀舞,可惜我不能出去。好了,這些都是題外話。
一、實(shí)現(xiàn)原理
- ps: “更新視圖但不重新請求頁面”是前端路由原理的核心之一
(參考:https://zhuanlan.zhihu.com/p/27588422)
vue-router 三種運(yùn)行模式:
- hash: 使用 URL hash 值("#")來作路由。默認(rèn)模式。
- history: 依賴 HTML5 History API 和服務(wù)器配置。
- abstract: 支持所有 JavaScript 運(yùn)行環(huán)境,如 Node.js 服務(wù)器端。
- ps:在vue-router中是通過mode這一參數(shù)控制路由的實(shí)現(xiàn)模式的:
var router = new VueRouter({
mode: 'history', //默認(rèn)是hash
routes: [
...
]
});

hash模式

HTML5History
Hash模式
- hash(“
#”)符號的本來作用是加在URL中指示網(wǎng)頁中的位置; - hash雖然出現(xiàn)在URL中,但不會被包括在HTTP請求中。它是用來指導(dǎo)瀏覽器動(dòng)作的,對服務(wù)器端完全無用,因此,改變hash不會重新加載頁面;
-
hash模式下通過hashchange方法可以監(jiān)聽url中hash的變化,來實(shí)現(xiàn)更新頁面部分內(nèi)容的操作:
window.addEventListener("hashchange", function(){}, false)
- 每一次改變hash,都會在瀏覽器的訪問歷史中增加一個(gè)記錄(利用
HashHistory.push()或HashHistory.replace()),可以實(shí)現(xiàn)瀏覽器的前進(jìn)和后退功能。
1. HashHistory.push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
- 由此可見,push()方法最主要的是對window的hash進(jìn)行了直接賦值:
window.location.hash = route.fullPath
-
hash的改變會自動(dòng)添加到瀏覽器的訪問歷史記錄中。
HashHistory.push().jpg
2. HashHistory.replace()
- replace()方法與push()方法不同之處在于,它并不是將新路由添加到瀏覽器訪問歷史的棧頂,而是替換掉當(dāng)前的路由:
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
- 可以看出,它與push()的實(shí)現(xiàn)結(jié)構(gòu)上基本相似,不同點(diǎn)在于它不是直接對window.location.hash進(jìn)行賦值,而是調(diào)用window.location.replace方法將路由進(jìn)行替換。
總得來說就是初始化vueRouter的時(shí)候,建立路由和組件的映射關(guān)系,監(jiān)聽hashChange事件來更新路由,進(jìn)而渲染視圖
HTML5History模式
- 通過
window.history.pushState(stateObject, title, URL)與window.history.replaceState(stateObject, title, URL)修改瀏覽器地址,觸發(fā)更新; - 通過監(jiān)聽
popstate事件監(jiān)聽瀏覽器前進(jìn)或者后退,觸發(fā)更新;
- stateObject: 當(dāng)瀏覽器跳轉(zhuǎn)到新的狀態(tài)時(shí),將觸發(fā)popState事件,該事件將攜帶這個(gè)stateObject參數(shù)的副本。
- title: 所添加記錄的標(biāo)題(暫時(shí)沒有用處)。
-
URL: 所添加記錄的URL。
這兩個(gè)方法有個(gè)共同的特點(diǎn):當(dāng)調(diào)用他們修改瀏覽器歷史記錄棧后,雖然當(dāng)前URL改變了,但瀏覽器不會立即發(fā)送請求該URL,這就為單頁應(yīng)用前端路由“更新視圖但不重新請求頁面 ”提供了基礎(chǔ)。
history.pushState
history.replaceState()
兩種模式比較
| hash模式 | history模式 |
|---|---|
于多了一個(gè)#,所以url整體上不夠美觀 |
當(dāng)用戶刷新或直接輸入地址時(shí)會向服務(wù)器發(fā)送一個(gè)請求,所以需要服務(wù)端同學(xué)進(jìn)行支持, 將路由都重定向到根路由 |
| hash只可修改#后面的部分,故只可設(shè)置與當(dāng)前同文檔的URL | pushState設(shè)置的新URL可以是與當(dāng)前URL同源的任意URL |
| hash設(shè)置的新值必須與原來不一樣才會觸發(fā)記錄添加到棧中 | pushState設(shè)置的新URL可以與當(dāng)前URL一模一樣,這樣也會把記錄添加到棧中 |
| hash只可添加短字符串 | pushState通過stateObject可以添加任意類型的數(shù)據(jù)到記錄中 |
| pushState可額外設(shè)置title屬性供后續(xù)使用 |
vue-router源碼分析:
install.js 分析
- 首先會對重復(fù)安裝進(jìn)行過濾
- 全局混入
beforeCreate和destroyed生命鉤子,為每個(gè)Vue實(shí)例設(shè)置_routerRoot屬性,并為根實(shí)例設(shè)置_router屬性 - 調(diào)用Vue中定義的
defineReactive對_route進(jìn)行劫持,其實(shí)是執(zhí)行的依賴收集的過程,執(zhí)行_route的get就會對當(dāng)前的組件進(jìn)行依賴收集,如果對_route進(jìn)行重新賦值觸發(fā)setter就會使收集的組件重新渲染,這里也是路由重新渲染的核心所在
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) { // 設(shè)置根路由-根組件實(shí)例
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 定義響應(yīng)式的 _route 對象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 非根組件設(shè)置
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
- 為Vue原型對象定義router和router和router和route屬性,并對兩個(gè)屬性進(jìn)行了劫持,使我們可以直接通過Vue對象實(shí)例訪問到
- 全局注冊了Routerview和RouterLink兩個(gè)組件,所以我們才可以在任何地方使用這兩個(gè)組件,這兩個(gè)組件的內(nèi)容我們稍后分析
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
vue-router工作流程:
- 安裝 vue-router 插件(參考 install.js分析)
- new Router 實(shí)例
- 根實(shí)例創(chuàng)建之前,執(zhí)行init方法,初始化路由
- 執(zhí)行transitionTo方法,同時(shí)hash模式下對瀏覽器hashChange事件進(jìn)行了監(jiān)聽,執(zhí)行history.listen方法,將對_route重新賦值的函數(shù)賦給History實(shí)例的callback,當(dāng)路由改變時(shí)對_route進(jìn)行重新賦值從而觸發(fā)組件更新
- transitionTo方法根據(jù)傳入的路徑從我們定義的所有路由中匹配到對應(yīng)路由,然后執(zhí)行confirmTransition
- confirmTransition首先會有重復(fù)路由的判斷。如果進(jìn)入相同的路由,直接調(diào)用abort回調(diào)函數(shù),函數(shù)退出,不會執(zhí)行后面的各組件的鉤子函數(shù),這也是為什么我們重復(fù)進(jìn)入相同路由不會觸發(fā)組建的重新渲染也不會觸發(fā)路由的各種鉤子函數(shù);如果判斷不是相同路由,就會執(zhí)行各組件的鉤子函數(shù)
- 按順序執(zhí)行好導(dǎo)航守衛(wèi)后,就會執(zhí)行傳入的成功的回調(diào)函數(shù),從而對_route進(jìn)行賦值,觸發(fā)setter,從而使組件重新渲染
二、使用
1.路由文件的定義
/*
* 路由器模塊
* */
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter);
import Home from '../views/home'
import Messages from '../views/messages'
import MessagesDetail from '../views/messagesDetail'
function loadView(view) {
return () => import(`@/views/${view}.vue`)
}
var router = new VueRouter({
mode: 'history',
routes: [
{
name: 'app',
path: '/',
// component: App,
redirect: '/home',
},
{
name: 'about',
path: '/about',
component: loadView('about'),
children: [
{
name: 'news',
path: '/about/news',
component: loadView('news')
},
{
name: 'messages',
path: '/about/messages',
component: Messages,
children: [
{
path: '/about/messages/:id',
component: MessagesDetail,
//主要用于寫某個(gè)指定路由跳轉(zhuǎn)時(shí)需要執(zhí)行的邏輯
beforeEnter: (to, from, next) => {
console.log('beforeEnter-to: ', to)
console.log('beforeEnter-from: ', from)
next();
},
afterEnter: (to, from, next) => {
console.log('---afterEnter-to: ', to)
console.log('---afterEnter-from: ', from)
next()
}
},
],
/*
*某個(gè)路由的路由鉤子函數(shù)
*/
//主要用于寫某個(gè)指定路由跳轉(zhuǎn)時(shí)需要執(zhí)行的邏輯
beforeEnter: (to, from, next) => {
console.log('beforeEnter-to: ', to)
console.log('beforeEnter-from: ', from)
next();
},
afterEnter: (to, from, next) => {
console.log('-----afterEnter-to: ', to)
console.log('-----afterEnter-from: ', from)
next()
}
},
]
},
{
name: 'home',
path: '/home',
component: Home,
}
]
});
/*
*全局路由鉤子
*/
//這類鉤子主要作用于全局,一般用來判斷權(quán)限,以及以及頁面丟失時(shí)候需要執(zhí)行的操作
router.beforeEach((to, from, next) => {
console.log('beforeEach-to: ', to)
console.log('beforeEach-from: ', from)
next();
})
router.afterEach((to, from, next) => {
console.log('afterEach-to: ', to)
console.log('afterEach-from: ', from)
})
export default router

vue-router的結(jié)構(gòu).jpg
- 另:需將路由全局注入main.js
import Vue from 'vue'
import App from './App.vue'
import router from '@/router'
import less from 'less'
Vue.use(less)
require('./assets/style/iconfont.css')
Vue.config.productionTip = false; //作用是阻止 vue 在啟動(dòng)時(shí)生成生產(chǎn)提示
new Vue({
router,
render: h => h(App)
}).$mount('#app')
三、有關(guān)知識點(diǎn)
1. router 與 route
先上一張圖來:

routerPKroute.jpg
- 由此可見,router是VueRouter的一個(gè)對象,通過Vue.use(VueRouter)和VueRouter的構(gòu)造函數(shù)得到的實(shí)力對象,該對象是一個(gè)全局對象,包含了許多關(guān)鍵對象與屬性,例如:history:
methods: {
go(num){
if (num === 1){
this.$router.replace({ name: 'news' })
}else if (num === 2){
this.$router.push({ name: 'messages' })
}
}
},
- $route是一個(gè)當(dāng)前路由的路由對象,每個(gè)路由都會有一個(gè)route對象,是一個(gè)局部對象,可獲取對應(yīng)的name,path,params,query等
<h3>ID:{{$route.params.id}}</h3>
2. 路由鉤子(分三類)
在使用那部分已經(jīng)貼出完整的代碼,以及應(yīng)用場景,故此處只做簡單的列舉:
- 全局路由鉤子
——這類鉤子主要作用于全局,一般用來判斷權(quán)限,以及以及頁面丟失時(shí)候需要執(zhí)行的操作
router.beforeEach((to, from, next) => {
next();
})
router.afterEach((to, from, next) => {
})
- 某個(gè)路由獨(dú)有的路由鉤子
——主要用于寫某個(gè)指定路由跳轉(zhuǎn)時(shí)需要執(zhí)行的邏輯
beforeEnter: (to, from, next) => {
next();
},
afterEnter: (to, from, next) => {
next()
}
- 路由組件內(nèi)的路由鉤子
export default {
name: "messages",
data() {
return {}
},
beforeRouteEnter (to, from, next) {
// 在渲染該組件的對應(yīng)路由被 confirm 前調(diào)用
// 不能獲取組件實(shí)例 `this`
// 因?yàn)楫?dāng)鉤子執(zhí)行前,組件實(shí)例還沒被創(chuàng)建
next();
},
beforeRouteUpdate (to, from, next) {
// 在當(dāng)前路由改變,但是該組件被復(fù)用時(shí)調(diào)用
// 舉例來說,對于一個(gè)帶有動(dòng)態(tài)參數(shù)的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉(zhuǎn)的時(shí)候,
// 由于會渲染同樣的 Foo 組件,因此組件實(shí)例會被復(fù)用。而這個(gè)鉤子就會在這個(gè)情況下被調(diào)用。
// 可以訪問組件實(shí)例 `this`
next()
},
beforeRouteLeave (to, from, next) {
// 導(dǎo)航離開該組件的對應(yīng)路由時(shí)調(diào)用
// 可以訪問組件實(shí)例 `this`
next()
}
}
3. 子路由
<router-link :to="`/about/messages/${v.id}`" class="">{{v.content}}</router-link>
<keep-alive>
<router-view></router-view>
</keep-alive>
.router-link-active{
color: burlywood !important;
}
- 在動(dòng)態(tài)組件上使用
keep-alive
——當(dāng)在多個(gè)組件之間切換的時(shí)候,有時(shí)會想保持這些組件的狀態(tài),以避免反復(fù)重渲染導(dǎo)致的性能問題。
(https://cn.vuejs.org/v2/guide/components-dynamic-async.html?_sw-precache=3b921049bd7cca2444e9efa512a9d9f5#%E5%9C%A8%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-keep-alive "在動(dòng)態(tài)組件上使用 keep-alive") - 當(dāng)組件在 <keep-alive> 內(nèi)被切換,它的
activated和deactivated這兩個(gè)生命周期鉤子函數(shù)將會被對應(yīng)執(zhí)行。 -
activated鉤子函數(shù)
——keep-alive 組件激活時(shí)調(diào)用。該鉤子在服務(wù)器端渲染期間不被調(diào)用。 -
deactivated鉤子函數(shù)
——實(shí)例銷毀之前調(diào)用。在這一步,實(shí)例仍然完全可用。該鉤子在服務(wù)器端渲染期間不被調(diào)用。


