隨着前端框架(React/Vue/Angular)等漸進式框架發展,配合 webpack 等打包工具,完成單頁面的構建愈來愈簡單.javascript
對比傳統多頁面應用,單頁面應用優點:html
缺點:前端
爲了解決單頁面系統中,頁面跳轉路由實現,和改變視圖的同時不會向後端發出請求。引入了前端路由系統 React-Router-Dom/vue-router 等前端路由庫.vue
經過瀏覽器地址欄的 hashChange 和 HTML5 提供的 History interface 實現的地址改變觸發視圖改變.html5
這是一段簡單的示例程序, vue-router 在 vue 程序中的簡單應用java
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>
<body>
<div id="app">
<h1>Hello App!</h1>
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
<router-view></router-view>
</div>
<div id="root">
<h1>Hello root!</h1>
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
<router-view></router-view>
</div>
<script> const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } const routes = [{ path: '/foo', component: Foo }, { path: '/bar', component: Bar }] const router = new VueRouter({ routes }) new Vue({ router }).$mount('#app') new Vue({ router }).$mount('#root') </script>
</body>
</html>
複製代碼
上面這段示例代碼使用了 umd 模塊的加載方式,直接 script 加載到 window 上webpack
在加載 router 代碼塊的時候內部會判斷加載方式,若是是 script 加載,會直接調用 Vue.use 方法初始化使用 Vue-router 插件git
// vue-router/src/index.js
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
複製代碼
若是是基於 webpack 的打包方式的程序,還須要在引入了 vue-router 以後使用如下代碼把 router 加載安裝到咱們的 vue 程序中,實際上這是一個 vue-router 集成的開始github
Vue.use(Router)
複製代碼
Vue.use 會調用 Router 內部實現的 install 方法,這是使用router 的入口web
首先貼上刪除了部分不作分析的部分的源代碼
import View from './components/view'
import Link from './components/link'
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
const isDef = v => v !== undefined
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}
複製代碼
這裏的 install 中註冊 router 到 Vue 的過程當中,作了這幾件事情
核心的混入部分經過混入 beforeCreate 鉤子中,實現了在每一個組件中對根示例 _routerRoot 的訪問.
isDef 方法判斷了 Vue 實例的配置中是否有 router 定義,而 router 只在根示例中有定義,也就是:
new Vue({ router }).$mount('#app')
複製代碼
進入根實例的條件以後,在根實例上定義了 _routerRoot 保持對自己的訪問地址.在後面的全部組件中,給組件共享根組件的訪問.
而後把 Router 實例掛載到根 Vue 實例上,保持 Router 實例的訪問.
執行 Router 實例的init 方法, 該方法定義在 Router 的類定義中是 vue-router 的核心初始化流程,入參根組件.
調用 Vue.util.defineReactive 定義響應式對象,後續的組件更新依賴於 Vue 的響應式原理, 經過響應式對象的依賴收集,派發更新流程通知視圖的更新
vue-router 的核心實現是在 src/index.js 中定義的 VueRouter 類,類中實現了初始化邏輯,定義了實如下的例的屬性和方法:
這是 VueRouter 的構造函數.
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
複製代碼
構造函數裏面作的事情很簡單:
構造器的核心就是根據環境和配置生成路由模式
這裏能夠看到優先使用配置項中的 mode 若是沒有配置則使用 hash
let mode = options.mode || 'hash'
複製代碼
而後當配置中使用了 history 模式的時候,判斷是否支持 history ,不支持則降級使用 hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
複製代碼
不是在瀏覽器環境中則使用本身構造的路由事件系統來實現 History
if (!inBrowser) {
mode = 'abstract'
}
複製代碼
init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== 'production' &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 對照本文檔開始示例,一個 router 對象可能被多個 app 所使用,在後續的路由變動的時候,經過更改 apps 中全部 app 的響應式路由數據,觸發視圖變動.
this.apps.push(app)
// this.app 保存了是否還有在使用該 router 實例的 app ,也就是 VUE 應用
if (this.app) {
return
}
this.app = app
const history = this.history
// 針對 hash 和 history 模式作滾動行爲處理,初始化路由監聽器,跳轉第一個路由觸發響應視圖
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 添加路由變化回調函數,這個回調函數是 路由變化最終響應到視圖的關鍵步驟.也就是給響應式對象從新賦值.
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
複製代碼
小結 這裏初始化函數作了幾件事情:
這裏的 API 一部分對路由操做的都是對 History 對象上具體的方法的代理.
這是基於原生的 HTML5 History interface 的路由監聽器實現(刪減不作分析部分)
這裏 HTML5History 派生自 History
History 類實現了路由的核心跳轉處理.後面會作分析
HTML5History類實現了:
其實就是對 各類mode 之間的不一樣點提取到這裏進行特殊處理,基礎能力都定義在基類 History 中
/* @flow */
import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'
export class HTML5History extends History {
constructor (router: Router, base: ?string) {
super(router, base)
}
// 定義了初始化監聽路由變化的方法
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 滾動行爲處理
if (supportsScroll) {
this.listeners.push(setupScroll())
}
// 路由變化響應函數,調用核心跳轉實現 transitionTo
const handleRoutingEvent = () => {
const current = this.current
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
//監聽 popstate ⌚事件
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
go (n: number) {
window.history.go(n)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// 肯定路由是否正確,不正確向state 裏面推入正確路由
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {
return getLocation(this.base)
}
}
export function getLocation (base: string): string {
let path = decodeURI(window.location.pathname)
if (base && path.toLowerCase().indexOf(base.toLowerCase()) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
複製代碼
HashHistory 實現中實現的方法實際上與 HTML5History 中實現的是一致的,只是在路由操做中 添加了對 hash 標識符 # 的判斷,跳轉路由的生成不同,要多一些反作用的操做 hash
這裏不作過多的分析.
/* @flow */
import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
function checkFallback (base) {
const location = getLocation(base)
if (!/^\/#/.test(location)) {
window.location.replace(cleanPath(base + '/#' + location))
return true
}
}
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
let href = window.location.href
const index = href.indexOf('#')
// empty path
if (index < 0) return ''
href = href.slice(index + 1)
// decode the hash but not the search or hash
// as search(query) is already decoded
// https://github.com/vuejs/vue-router/issues/2708
const searchIndex = href.indexOf('?')
if (searchIndex < 0) {
const hashIndex = href.indexOf('#')
if (hashIndex > -1) {
href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
} else href = decodeURI(href)
} else {
href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
}
return href
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
複製代碼
上面提到的兩個 HTML5History 和 HashHistory 實際上都是派生自 History 基類,在基類上定義了 路由監聽的核心邏輯,接下來咱們來分析這部分的核心代碼
因爲這部分代碼輔助方法較多,不展現過多的代碼,只摘錄部分核心邏輯代碼展現:
/* @flow */
import { _Vue } from '../install'
import type Router from '../index'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { warn } from '../util/warn'
import { START, isSameRoute } from '../util/route'
import {
flatten,
flatMapComponents,
resolveAsyncComponents
} from '../util/resolve-components'
import {
createNavigationDuplicatedError,
createNavigationCancelledError,
createNavigationRedirectedError,
createNavigationAbortedError,
isError,
isNavigationFailure,
NavigationFailureType
} from '../util/errors'
export class History {
constructor (router: Router, base: ?string) {
...
}
// 外部經過 listen 註冊路由變化回調到這裏,當路由跳轉觸發回調函數通知外部執行對應方法,入參跳轉的 route 對象.
listen (cb: Function) {
this.cb = cb
}
onReady (cb: Function, errorCb: ?Function) { ... }
onError (errorCb: Function) { ... }
// 路由跳轉函數
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
try {
//調用 match方法獲得匹配的 route對象
route = this.router.match(location, this.current)
} catch (e) {...}
// 核心跳轉邏輯,會處理路由守衛鉤子方法,生成鉤子任務隊列,處理過渡等.
this.confirmTransition(
route,
() => {
// 跳轉處理完成回調中,調用 updateRoute 實現跳轉,觸發視圖更新
const prev = this.current
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
// fire ready cbs once
if (!this.ready) { ... }
},
err => { ... }
)
}
// 路由跳轉前處理函數,處理過渡,鉤子函數隊列
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {...}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
// 若是當前路由和以前路由相同 確認url 直接return
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
return abort(createNavigationDuplicatedError(current, route))
}
// 經過異步隊列來交叉對比當前路由的路由記錄和如今的這個路由的路由記錄
// 爲了能準確獲得父子路由更新的狀況下能夠確切的知道 哪些組件須要更新 哪些不須要更新
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 在異步隊列中執行響應的勾子函數
// 經過 queue 這個數組保存相應的路由鉤子函數
const queue: Array<?NavigationGuard> = [].concat(
/// leave 的勾子
extractLeaveGuards(deactivated),
// 全局的 before 的勾子
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// 將要更新的路由的 beforeEnter勾子
activated.map(m => m.beforeEnter),
// 異步組件
resolveAsyncComponents(activated)
)
this.pending = route
// 隊列執行的 iterator 遍歷函數
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 遞歸回調方式運行隊列函數
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
}
function resolveQueue ( current: Array<RouteRecord>, next: Array<RouteRecord> ): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
複製代碼
小結
這裏核心實現了路徑切換的邏輯,是整個router 路由切換跳轉的實現.主要實現瞭如下功能
history.listen(callback) ==> $router.push() ==> HashHistory.push() ==> History.transitionTo() ==>
History.confirmTransition() ==> History.updateRoute() ==> {app._route = route} ==> vm.render()
複製代碼