關(guān)于vue-router的使用及實現(xiàn)原理
vue-router使用及實現(xiàn)原理
前端路由是直接找到與地址匹配的一個組件或?qū)ο蟛⑵滗秩境鰜怼?/p>
改變?yōu)g覽器地址而不向服務(wù)器發(fā)出請求有兩種方式:
- 1. 在地址中加入#以欺騙瀏覽器,地址的改變是由于正在進行頁內(nèi)導(dǎo)航
- 2. 使用H5的window.history功能,使用URL的Hash來模擬一個完整的URL。
當(dāng)打包構(gòu)建應(yīng)用時,Javascript 包會變得非常大,影響頁面加載。
如果我們能把不同路由對應(yīng)的組件分割成不同的代碼塊,然后當(dāng)路由被訪問的時候才加載對應(yīng)組件,這樣就更加高效了。
目錄結(jié)構(gòu)
先來看看整體的目錄結(jié)構(gòu)
和流程相關(guān)的主要需要關(guān)注點的就是 components、history 目錄以及 create-matcher.js、create-route-map.js、index.js、install.js。
下面就從 basic 應(yīng)用入口開始來分析 vue-router 的整個流程。
import Vue from 'vue' import VueRouter from 'vue-router' // 1. 插件 // 安裝 <router-view> and <router-link> 組件 // 且給當(dāng)前應(yīng)用下所有的組件都注入 $router and $route 對象 Vue.use(VueRouter) // 2. 定義各個路由下使用的組件,簡稱路由組件 const Home = { template: '<div>home</div>' } const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } // 3. 創(chuàng)建 VueRouter 實例 router const router = new VueRouter({ mode: 'history', base: __dirname, routes: [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] }) // 4. 創(chuàng)建 啟動應(yīng)用 // 一定要確認(rèn)注入了 router // 在 <router-view> 中將會渲染路由組件 new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar">/bar</router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
作為插件
上邊代碼中關(guān)鍵的第 1 步,利用 Vue.js 提供的插件機制 .use(plugin) 來安裝 VueRouter,而這個插件機制則會調(diào)用該 plugin 對象的 install 方法(當(dāng)然如果該 plugin 沒有該方法的話會把 plugin 自身作為函數(shù)來調(diào)用);下邊來看下 vue-router 這個插件具體的實現(xiàn)部分。
VueRouter 對象是在 src/index.js 中暴露出來的,這個對象有一個靜態(tài)的 install 方法:
/* @flow */ // 導(dǎo)入 install 模塊 import { install } from './install' // ... import { inBrowser, supportsHistory } from './util/dom' // ... export default class VueRouter { // ... } // 賦值 install VueRouter.install = install // 自動使用插件 if (inBrowser && window.Vue) { window.Vue.use(VueRouter) }
可以看到這是一個 Vue.js 插件的經(jīng)典寫法,給插件對象增加 install 方法用來安裝插件具體邏輯,同時在最后判斷下如果是在瀏覽器環(huán)境且存在 window.Vue 的話就會自動使用插件。
install 在這里是一個單獨的模塊,繼續(xù)來看同級下的 src/install.js 的主要邏輯:
// router-view router-link 組件 import View from './components/view' import Link from './components/link' // export 一個 Vue 引用 export let _Vue // 安裝函數(shù) export function install (Vue) { if (install.installed) return install.installed = true // 賦值私有 Vue 引用 _Vue = Vue // 注入 $router $route Object.defineProperty(Vue.prototype, '$router', { get () { return this.$root._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this.$root._route } }) // beforeCreate mixin Vue.mixin({ beforeCreate () { // 判斷是否有 router if (this.$options.router) { // 賦值 _router this._router = this.$options.router // 初始化 init this._router.init(this) // 定義響應(yīng)式的 _route 對象 Vue.util.defineReactive(this, '_route', this._router.history.current) } } }) // 注冊組件 Vue.component('router-view', View) Vue.component('router-link', Link) // ... }
這里就會有一些疑問了?
為啥要 export 一個 Vue 引用?
插件在打包的時候是肯定不希望把 vue 作為一個依賴包打進去的,但是呢又希望使用 Vue 對象本身的一些方法,此時就可以采用上邊類似的做法,在 install 的時候把這個變量賦值 Vue ,這樣就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依賴包(前提是保證 install 后才會使用)。
通過給 Vue.prototype 定義 $router
、$route
屬性就可以把他們注入到所有組件中嗎?
在 Vue.js 中所有的組件都是被擴展的 Vue 實例,也就意味著所有的組件都可以訪問到這個實例原型上定義的屬性。
beforeCreate mixin 這個在后邊創(chuàng)建 Vue 實例的時候再細(xì)說。
實例化 VueRouter
在入口文件中,首先要實例化一個 VueRouter ,然后將其傳入 Vue 實例的 options 中。
現(xiàn)在繼續(xù)來看在 src/index.js 中暴露出來的 VueRouter 類:
// ... import { createMatcher } from './create-matcher' // ... export default class VueRouter { // ... constructor (options: RouterOptions = {}) { this.app = null this.options = options this.beforeHooks = [] this.afterHooks = [] // 創(chuàng)建 match 匹配函數(shù) this.match = createMatcher(options.routes || []) // 根據(jù) mode 實例化具體的 History let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsHistory 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) break default: assert(false, `invalid mode: ${mode}`) } } // ... }
里邊包含了重要的一步:創(chuàng)建 match 匹配函數(shù)。
match 匹配函數(shù)
匹配函數(shù)是由 src/create-matcher.js 中的 createMatcher 創(chuàng)建的:
/* @flow */ import Regexp from 'path-to-regexp' // ... import { createRouteMap } from './create-route-map' // ... export function createMatcher (routes: Array<RouteConfig>): Matcher { // 創(chuàng)建路由 map const { pathMap, nameMap } = createRouteMap(routes) // 匹配函數(shù) function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // ... } function redirect ( record: RouteRecord, location: Location ): Route { // ... } function alias ( record: RouteRecord, location: Location, matchAs: string ): Route { // ... } function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom) } // 返回 return match } // ...
具體邏輯后續(xù)再具體分析,現(xiàn)在只需要理解為根據(jù)傳入的 routes 配置生成對應(yīng)的路由 map,然后直接返回了 match 匹配函數(shù)。
繼續(xù)來看 src/create-route-map.js 中的 createRouteMap 函數(shù):
/* @flow */ import { assert, warn } from './util/warn' import { cleanPath } from './util/path' // 創(chuàng)建路由 map export function createRouteMap (routes: Array<RouteConfig>): { pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { // path 路由 map const pathMap: Dictionary<RouteRecord> = Object.create(null) // name 路由 map const nameMap: Dictionary<RouteRecord> = Object.create(null) // 遍歷路由配置對象 增加 路由記錄 routes.forEach(route => { addRouteRecord(pathMap, nameMap, route) }) return { pathMap, nameMap } } // 增加 路由記錄 函數(shù) function addRouteRecord ( pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { // 獲取 path 、name const { path, name } = route assert(path != null, `"path" is required in a route configuration.`) // 路由記錄 對象 const record: RouteRecord = { path: normalizePath(path, parent), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {} } // 嵌套子路由 則遞歸增加 記錄 if (route.children) { // ... route.children.forEach(child => { addRouteRecord(pathMap, nameMap, child, record) }) } // 處理別名 alias 邏輯 增加對應(yīng)的 記錄 if (route.alias !== undefined) { if (Array.isArray(route.alias)) { route.alias.forEach(alias => { addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path) }) } else { addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path) } } // 更新 path map pathMap[record.path] = record // 更新 name map if (name) { if (!nameMap[name]) { nameMap[name] = record } else { warn(false, `Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`) } } } function normalizePath (path: string, parent?: RouteRecord): string { path = path.replace(/\/$/, '') if (path[0] === '/') return path if (parent == null) return path return cleanPath(`${parent.path}/${path}`) }
可以看出主要做的事情就是根據(jù)用戶路由配置對象生成普通的根據(jù) path 來對應(yīng)的路由記錄以及根據(jù) name 來對應(yīng)的路由記錄的 map,方便后續(xù)匹配對應(yīng)。
實例化 History
這也是很重要的一步,所有的 History 類都是在 src/history/ 目錄下,現(xiàn)在呢不需要關(guān)心具體的每種 History 的具體實現(xiàn)上差異,只需要知道他們都是繼承自 src/history/base.js 中的 History 類的:
/* @flow */ // ... import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' import { START, isSameRoute } from '../util/route' // 這里從之前分析過的 install.js 中 export _Vue import { _Vue } from '../install' export class History { // ... constructor (router: VueRouter, base: ?string) { this.router = router this.base = normalizeBase(base) // start with a route object that stands for "nowhere" this.current = START this.pending = null } // ... } // 得到 base 值 function normalizeBase (base: ?string): string { if (!base) { if (inBrowser) { // respect <base> tag const baseEl = document.querySelector('base') base = baseEl ? baseEl.getAttribute('href') : '/' } else { base = '/' } } // make sure there's the starting slash if (base.charAt(0) !== '/') { base = '/' + base } // remove trailing slash return base.replace(/\/$/, '') } // ...
實例化完了 VueRouter,下邊就該看看 Vue 實例了。
實例化 Vue
new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar">/bar</router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
options 中傳入了 router,以及模板;還記得上邊沒具體分析的 beforeCreate mixin 嗎,此時創(chuàng)建一個 Vue 實例,對應(yīng)的 beforeCreate 鉤子就會被調(diào)用:
// ... Vue.mixin({ beforeCreate () { // 判斷是否有 router if (this.$options.router) { // 賦值 _router this._router = this.$options.router // 初始化 init this._router.init(this) // 定義響應(yīng)式的 _route 對象 Vue.util.defineReactive(this, '_route', this._router.history.current) } } })
具體來說,首先判斷實例化時 options 是否包含 router,如果包含也就意味著是一個帶有路由配置的實例被創(chuàng)建了,此時才有必要繼續(xù)初始化路由相關(guān)邏輯。
然后給當(dāng)前實例賦值_router
,這樣在訪問原型上的 $router
的時候就可以得到 router 了。
下邊來看里邊兩個關(guān)鍵:router.init 和 定義響應(yīng)式的 _route 對象。
router.init
然后來看 router 的 init 方法就干了哪些事情,依舊是在 src/index.js 中:
/* @flow */ import { install } from './install' import { createMatcher } from './create-matcher' import { HashHistory, getHash } from './history/hash' import { HTML5History, getLocation } from './history/html5' import { AbstractHistory } from './history/abstract' import { inBrowser, supportsHistory } from './util/dom' import { assert } from './util/warn' export default class VueRouter { // ... init (app: any /* Vue component instance */) { // ... this.app = app const history = this.history if (history instanceof HTML5History) { history.transitionTo(getLocation(history.base)) } else if (history instanceof HashHistory) { history.transitionTo(getHash(), () => { window.addEventListener('hashchange', () => { history.onHashChange() }) }) } history.listen(route => { this.app._route = route }) } // ... } // ...
可以看到初始化主要就是給 app 賦值,針對于 HTML5History 和 HashHistory 特殊處理,因為在這兩種模式下才有可能存在進入時候的不是默認(rèn)頁,需要根據(jù)當(dāng)前瀏覽器地址欄里的 path 或者 hash 來激活對應(yīng)的路由,此時就是通過調(diào)用 transitionTo 來達(dá)到目的;而且此時還有個注意點是針對于 HashHistory 有特殊處理,為什么不直接在初始化 HashHistory 的時候監(jiān)聽 hashchange 事件呢?
這個是為了修復(fù)vuejs/vue-router#725這個 bug 而這樣做的,簡要來說就是說如果在 beforeEnter 這樣的鉤子函數(shù)中是異步的話,beforeEnter 鉤子就會被觸發(fā)兩次,原因是因為在初始化的時候如果此時的 hash 值不是以 / 開頭的話就會補上 #/,這個過程會觸發(fā) hashchange 事件,所以會再走一次生命周期鉤子,也就意味著會再次調(diào)用 beforeEnter 鉤子函數(shù)。
來看看這個具體的 transitionTo 方法的大概邏輯,在 src/history/base.js 中:
/* @flow */ import type VueRouter from '../index' import { warn } from '../util/warn' import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' import { START, isSameRoute } from '../util/route' import { _Vue } from '../install' export class History { // ... transitionTo (location: RawLocation, cb?: Function) { // 調(diào)用 match 得到匹配的 route 對象 const route = this.router.match(location, this.current) // 確認(rèn)過渡 this.confirmTransition(route, () => { // 更新當(dāng)前 route 對象 this.updateRoute(route) cb && cb(route) // 子類實現(xiàn)的更新url地址 // 對于 hash 模式的話 就是更新 hash 的值 // 對于 history 模式的話 就是利用 pushstate / replacestate 來更新 // 瀏覽器地址 this.ensureURL() }) } // 確認(rèn)過渡 confirmTransition (route: Route, cb: Function) { const current = this.current // 如果是相同 直接返回 if (isSameRoute(route, current)) { this.ensureURL() return } // 交叉比對當(dāng)前路由的路由記錄和現(xiàn)在的這個路由的路由記錄 // 以便能準(zhǔn)確得到父子路由更新的情況下可以確切的知道 // 哪些組件需要更新 哪些不需要更新 const { deactivated, activated } = resolveQueue(this.current.matched, route.matched) // 整個切換周期的隊列 const queue: Array<?NavigationGuard> = [].concat( // leave 的鉤子 extractLeaveGuards(deactivated), // 全局 router before hooks this.router.beforeHooks, // 將要更新的路由的 beforeEnter 鉤子 activated.map(m => m.beforeEnter), // 異步組件 resolveAsyncComponents(activated) ) this.pending = route 每一個隊列執(zhí)行的 iterator 函數(shù) const iterator = (hook: NavigationGuard, next) => { // 確保期間還是當(dāng)前路由 if (this.pending !== route) return hook(route, current, (to: any) => { if (to === false) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) } else if (typeof to === 'string' || typeof to === 'object') { // next('/') or next({ path: '/' }) -> redirect this.push(to) } else { // confirm transition and pass on the value next(to) } }) } // 執(zhí)行隊列 runQueue(queue, iterator, () => { const postEnterCbs = [] // 組件內(nèi)的鉤子 const enterGuards = extractEnterGuards(activated, postEnterCbs, () => { return this.current === route }) // 在上次的隊列執(zhí)行完成后再執(zhí)行組件內(nèi)的鉤子 // 因為需要等異步組件以及是OK的情況下才能執(zhí)行 runQueue(enterGuards, iterator, () => { // 確保期間還是當(dāng)前路由 if (this.pending === route) { this.pending = null cb(route) this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => cb()) }) } }) }) } // 更新當(dāng)前 route 對象 updateRoute (route: Route) { const prev = this.current this.current = route // 注意 cb 的值 // 每次更新都會調(diào)用 下邊需要用到! this.cb && this.cb(route) // 執(zhí)行 after hooks 回調(diào) this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) } } // ...
可以看到整個過程就是執(zhí)行約定的各種鉤子以及處理異步組件問題,這里有一些具體函數(shù)具體細(xì)節(jié)被忽略掉了(后續(xù)會具體分析)但是不影響具體理解這個流程。
但是需要注意一個概念:路由記錄,每一個路由 route 對象都對應(yīng)有一個 matched 屬性,它對應(yīng)的就是路由記錄,他的具體含義在調(diào)用 match() 中有處理;通過之前的分析可以知道這個 match 是在 src/create-matcher.js 中的:
// ... import { createRoute } from './util/route' import { createRouteMap } from './create-route-map' // ... export function createMatcher (routes: Array<RouteConfig>): Matcher { const { pathMap, nameMap } = createRouteMap(routes) // 關(guān)鍵的 match function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute) const { name } = location // 命名路由處理 if (name) { // nameMap[name] = 路由記錄 const record = nameMap[name] const paramNames = getParams(record.path) // ... if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) // 創(chuàng)建 route return _createRoute(record, location, redirectedFrom) } } else if (location.path) { // 普通路由處理 location.params = {} for (const path in pathMap) { if (matchRoute(path, location.params, location.path)) { // 匹配成功 創(chuàng)建route // pathMap[path] = 路由記錄 return _createRoute(pathMap[path], location, redirectedFrom) } } } // no match return _createRoute(null, location) } // ... // 創(chuàng)建路由 function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { // 重定向和別名邏輯 if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } // 創(chuàng)建路由對象 return createRoute(record, location, redirectedFrom) } return match } // ...
路由記錄在分析 match 匹配函數(shù)那里以及分析過了,這里還需要了解下創(chuàng)建路由對象的 createRoute,存在于 src/util/route.js 中:
// ... export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { // 可以看到就是一個被凍結(jié)的普通對象 const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query: location.query || {}, params: location.params || {}, fullPath: getFullPath(location), // 根據(jù)記錄層級的得到所有匹配的 路由記錄 matched: record ? formatMatch(record) : [] } if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom) } return Object.freeze(route) } // ... function formatMatch (record: ?RouteRecord): Array<RouteRecord> { const res = [] while (record) { res.unshift(record) record = record.parent } return res } // ...
回到之前看的 init,最后調(diào)用了 history.listen 方法:
history.listen(route => { this.app._route = route })
listen 方法很簡單就是設(shè)置下當(dāng)前歷史對象的 cb 的值, 在之前分析 transitionTo 的時候已經(jīng)知道在 history 更新完畢的時候調(diào)用下這個 cb。
然后看這里設(shè)置的這個函數(shù)的作用就是更新下當(dāng)前應(yīng)用實例的 _route 的值,更新這個有什么用呢?請看下段落的分析。
defineReactive 定義 _route
繼續(xù)回到 beforeCreate 鉤子函數(shù)中,在最后通過 Vue 的工具方法給當(dāng)前應(yīng)用實例定義了一個響應(yīng)式的 _route 屬性,值就是獲取的 this._router.history.current,也就是當(dāng)前 history 實例的當(dāng)前活動路由對象。
給應(yīng)用實例定義了這么一個響應(yīng)式的屬性值也就意味著如果該屬性值發(fā)生了變化,就會觸發(fā)更新機制,繼而調(diào)用應(yīng)用實例的 render 重新渲染。
還記得上一段結(jié)尾留下的疑問,也就是 history 每次更新成功后都會去更新應(yīng)用實例的 _route 的值,也就意味著一旦 history 發(fā)生改變就會觸發(fā)更新機制調(diào)用應(yīng)用實例的 render 方法進行重新渲染。
router-link 和 router-view 組件
new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar">/bar</router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
可以看到這個實例的 template 中包含了兩個自定義組件:router-link 和 router-view。
router-view 組件
router-view 組件比較簡單,所以這里就先來分析它,他是在源碼的 src/components/view.js 中定義的:
export default { name: 'router-view', functional: true, // 功能組件 純粹渲染 props: { name: { type: String, default: 'default' // 默認(rèn)default 默認(rèn)命名視圖的name } }, render (h, { props, children, parent, data }) { // 解決嵌套深度問題 data.routerView = true // route 對象 const route = parent.$route // 緩存 const cache = parent._routerViewCache || (parent._routerViewCache = {}) let depth = 0 let inactive = false // 當(dāng)前組件的深度 while (parent) { if (parent.$vnode && parent.$vnode.data.routerView) { depth++ } 處理 keepalive 邏輯 if (parent._inactive) { inactive = true } parent = parent.$parent } data.routerViewDepth = depth // 得到相匹配的當(dāng)前組件層級的 路由記錄 const matched = route.matched[depth] if (!matched) { return h() } // 得到要渲染組件 const name = props.name const component = inactive ? cache[name] : (cache[name] = matched.components[name]) if (!inactive) { // 非 keepalive 模式下 每次都需要設(shè)置鉤子 // 進而更新(賦值&銷毀)匹配了的實例元素 const hooks = data.hook || (data.hook = {}) hooks.init = vnode => { matched.instances[name] = vnode.child } hooks.prepatch = (oldVnode, vnode) => { matched.instances[name] = vnode.child } hooks.destroy = vnode => { if (matched.instances[name] === vnode.child) { matched.instances[name] = undefined } } } // 調(diào)用 createElement 函數(shù) 渲染匹配的組件 return h(component, data, children) } }
可以看到邏輯還是比較簡單的,拿到匹配的組件進行渲染就可以了。
router-link 組件
再來看看導(dǎo)航鏈接組件,他在源碼的 src/components/link.js 中定義的:
// ... import { createRoute, isSameRoute, isIncludedRoute } from '../util/route' // ... export default { name: 'router-link', props: { // 傳入的組件屬性們 to: { // 目標(biāo)路由的鏈接 type: toTypes, required: true }, // 創(chuàng)建的html標(biāo)簽 tag: { type: String, default: 'a' }, // 完整模式,如果為 true 那么也就意味著 // 絕對相等的路由才會增加 activeClass // 否則是包含關(guān)系 exact: Boolean, // 在當(dāng)前(相對)路徑附加路徑 append: Boolean, // 如果為 true 則調(diào)用 router.replace() 做替換歷史操作 replace: Boolean, // 鏈接激活時使用的 CSS 類名 activeClass: String }, render (h: Function) { // 得到 router 實例以及當(dāng)前激活的 route 對象 const router = this.$router const current = this.$route const to = normalizeLocation(this.to, current, this.append) // 根據(jù)當(dāng)前目標(biāo)鏈接和當(dāng)前激活的 route匹配結(jié)果 const resolved = router.match(to, current) const fullPath = resolved.redirectedFrom || resolved.fullPath const base = router.history.base // 創(chuàng)建的 href const href = createHref(base, fullPath, router.mode) const classes = {} // 激活class 優(yōu)先當(dāng)前組件上獲取 要么就是 router 配置的 linkActiveClass // 默認(rèn) router-link-active const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active' // 相比較目標(biāo) // 因為有命名路由 所有不一定有path const compareTarget = to.path ? createRoute(null, to) : resolved // 如果嚴(yán)格模式的話 就判斷是否是相同路由(path query params hash) // 否則就走包含邏輯(path包含,query包含 hash為空或者相同) classes[activeClass] = this.exact ? isSameRoute(current, compareTarget) : isIncludedRoute(current, compareTarget) // 事件綁定 const on = { click: (e) => { // 忽略帶有功能鍵的點擊 if (e.metaKey || e.ctrlKey || e.shiftKey) return // 已阻止的返回 if (e.defaultPrevented) return // 右擊 if (e.button !== 0) return // `target="_blank"` 忽略 const target = e.target.getAttribute('target') if (/\b_blank\b/i.test(target)) return // 阻止默認(rèn)行為 防止跳轉(zhuǎn) e.preventDefault() if (this.replace) { // replace 邏輯 router.replace(to) } else { // push 邏輯 router.push(to) } } } // 創(chuàng)建元素需要附加的數(shù)據(jù)們 const data: any = { class: classes } if (this.tag === 'a') { data.on = on data.attrs = { href } } else { // 找到第一個 <a> 給予這個元素事件綁定和href屬性 const a = findAnchor(this.$slots.default) if (a) { // in case the <a> is a static node a.isStatic = false const extend = _Vue.util.extend const aData = a.data = extend({}, a.data) aData.on = on const aAttrs = a.data.attrs = extend({}, a.data.attrs) aAttrs.href = href } else { // 沒有 <a> 的話就給當(dāng)前元素自身綁定時間 data.on = on } } // 創(chuàng)建元素 return h(this.tag, data, this.$slots.default) } } function findAnchor (children) { if (children) { let child for (let i = 0; i < children.length; i++) { child = children[i] if (child.tag === 'a') { return child } if (child.children && (child = findAnchor(child.children))) { return child } } } } function createHref (base, fullPath, mode) { var path = mode === 'hash' ? '/#' + fullPath : fullPath return base ? cleanPath(base + path) : path }
可以看出 router-link 組件就是在其點擊的時候根據(jù)設(shè)置的 to 的值去調(diào)用 router 的 push 或者 replace 來更新路由的,同時呢,會檢查自身是否和當(dāng)前路由匹配(嚴(yán)格匹配和包含匹配)來決定自身的 activeClass 是否添加。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue2.x 通過后端接口代理,獲取qq音樂api的數(shù)據(jù)示例
今天小編就為大家分享一篇vue2.x 通過后端接口代理,獲取qq音樂api的數(shù)據(jù)示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-10-10Vue中定義src在img標(biāo)簽使用時加載不出來的解決
這篇文章主要介紹了Vue中定義src在img標(biāo)簽使用時加載不出來的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07