前端路由本质原理
监听URL变化,不需要刷新页面的情况下,按路由规则展示不同的页面部分。
单页面应用实现路由,只能通过两种方式:
- history
- hash
vue-router 实现
目录结构
components: 存放了两个vue组件<router-link>和<router-view>history:存放三种路由的实现方式util:存放路由功能类和功能方法create-matcher.js和create-router-map.js是匹配表生产方法文件index存放VueRouter类,也是整个插件的入口install.js提供插件安装方法
引入方法
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 注册插件,执行插件中install方法。
Vue.use(VueRouter)
// 2. 定义组件
const Home = { template: '<div>主页</div>' }
const Index = { template: '<div>首页</div>' }
// 定义路由,最终每个路由都会映射一个组件
const routes = [
{ path: '/home', component: Home },
{ path: '/index', component: Index }
]
// 3. 创建 router 实例,并传 `routes` 配置
const router = new VueRouter({
routes
})
// 4. 创建和挂载到vue实例上。
// router对象以参数形式注入到Vue,
// 使用 router-link 组件来导航
// 使用 router-view 组件来展示导航对应组件
const app = new Vue({
router,
template:
`<div id="app">
<ul>
<li><router-link to="/home">主页</router-link></li>
<li><router-link to="/index">首页</router-link></li>
<router-link tag="li" to="/home">/主页</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')
逐步分析
1. 注册插件,执行插件中install方法
import VueRouter from 'vue-router'
vue-router插件入口文件是index.js。index.js包含VueRouter构造函数的声明和一些构造函数的方法,install方法就是在这里给添加上的VueRouter.install = install
Vue.use(VueRouter)
在这个方法里调用了VueRouter的install函数。
最主要的两个实现:
- 利用
Vue.mixin全局注入beforeCreate和destroyed钩子函数方法。 - 给
Vue.prototype添加$router和$route属性,从而使所有组件可以访问到$router和$route。 - 全局注册
RouterView和RouterLink组件。
export function install (Vue) {
// 重复安装校验
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
// 调用到这个函数的话,证明已经创建了一个vueRouter实例了,也就是说new VueRouter和new Vue都已经执行
beforeCreate () {
// 只有根组件会进入if,否则走else
if (isDef(this.$options.router)) {
// vue实例的_routerRoot属性就是vue实例它本身
this._routerRoot = this
// vue实例的_routerRoot属性是router实例
this._router = this.$options.router
// 初始化vueRouter实例
this._router.init(this)
// 利用vue的defineReactive方法,给vue实例添加_route属性,并且把这个属性设置为响应式的。就是说_route变化后,用到这个属性的所有watcher都要触发更新。
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 这里是给子组件实例设置_routerRoot属性,先获取父组件实例,没有就是当前this。
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 疑问???????
registerInstance(this, this)
},
destroyed () {
// 疑问???????
registerInstance(this)
}
})
// 给Vue.prototype绑定$router属性
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 给Vue.prototype绑定$route属性
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局引入组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 注册合并钩子函数 疑问???????
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
创建 router 实例,并传 routes 配置
export default class VueRouter {
static install: () => void
static version: string
static isNavigationFailure: Function
static NavigationFailureType: any
static START_LOCATION: Route
app: any
apps: Array<any>
ready: boolean
readyCbs: Array<Function>
options: RouterOptions
mode: string
history: HashHistory | HTML5History | AbstractHistory
matcher: Matcher
fallback: boolean
beforeHooks: Array<?NavigationGuard>
resolveHooks: Array<?NavigationGuard>
afterHooks: Array<?AfterNavigationHook>
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 由路由配置数组生成map映射表
this.matcher = createMatcher(options.routes || [], this)
// 选择模式,默认hash
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
// 根据不同模式生成对应模式的history实例
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}`)
}
}
}
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.`
)
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
const history = this.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
})
})
}
...
}
这里最主要的两部分实现是:
- 把
routes路由配置数组对象构造成Map映射表 - 根据
mode类型不同,实例化history对象。
createMatcher
这个方法是用来生成路由配置对象映射表的。
主要逻辑是遍历routes属性,把每一个route生成一个record对象,再把这个对象放入到以path为key的map中。还有以name为key的map中。
如果有children路由,就遍历生成record,形成一个树形结构。
最终返回一个matcher对象,对象里包含pathList,nameMap和pathMap属性。
实例化history
根据参数mode和浏览器支持情况,实例化history对象。history可以是hashHistory或HTML5History
this._router.init(this)
VueRouter实例化后,就需要把实例化得来的对象以参数的形式传给Vue的实例化函数中,Vue对象的实例化必定会走到beforeCreate钩子函数,那么我们再回过头看下beforeCreate钩子里的_router的init方法,这个方法只有在实例化根组件时才会调用。
在init方法中,会调用history.transitionTo方法进行跳转。这个跳转方法在base.js中。
history.transitionTo里面还会调用history.confirmTransition方法,这两个方法实现的比较复杂,其主要逻辑如下:
- 获取到目标路由所对应的路由对象
- 判断目标路由和当前是否一致,如果一致,就不处理。
- 通过confirmTransition方法获取到需要更新的组件、激活的组件以及废弃的组件对象。
- 再将不同的钩子函数以及异步组件加载存入到数组 queue
- 通过 runQueue 方法顺序执行 queue 里的函数
- 整个执行完后调用 runQueue 中的回调函数,执行添加监听url变化的方法、after 钩子
Vue.util.defineReactive(this, '_route', this._router.history.current)
给Vue实例添加_route响应式属性,属性值是当前路由对象。
监听路由变化的方法
如果支持popstate事件就监听popstate,不支持就监听hashchange事件。监听到变化后就调用history.transitionTo方法。
push
调用history.transitionTo方法