vue-router是什么,跟传统的浏览器路由有什么区别
一、vue-router是什么
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。这里的路由并不是指我们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器,vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。
二、跟传统的浏览器路由有什么区别
传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系。 通俗的讲就是,用传统的a标签跳转的页面,实际上相当于浏览器中跳到了另一个页面,而用单页面应用的路由,页面跳转本质上是指向同一个页面,只不过通过监听路由的切换,从而在页面中用逻辑实现组件的切换,从而达到页面视图切换的目的,也就是相当于更新视图而不重新请求页面。
vue-router的实现原理简单讲解
vue-router通过hash与History interface两种方式来实现前端路由的更新视图但不重新请求页面。
hash 模式
hash模式是vue-router的默认路由模式,它的特点就是url中会带有一个#号,这个#是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,#是用来指导浏览器动作的,跟服务器端完全没有关系,同时每一次改变#后的url,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置,同时这个hash值的改变又可以通过js自带的onhashchange事件去监听,从而触发相应逻辑。
那总结来说,vue-router用hash模式实现路由跳转就是通过在触发路由改变的时候,改变url中#后面的部分,再触发一开始监听的onhashchange,从而在不刷新页面的情况下,根据hash值获取对应路由下的数据,最后更新视图。
history 模式
history模式的话主要依赖于HTML5 的History API,能让开发人员在不刷新整个页面的情况下修改站点的URL。我们一般情况下会选择使用History模式,原因就是Hash模式下URL带着‘#’会显得不美观。history模式是使用了history interface的pushState跟replaceStateapi,这两个方法应用于浏览器记录栈,前者是将新的跳转url添加到浏览器的操作记录栈,后者是替换掉当前页面成为浏览器新的记录栈。这种模式的好处是跳转后的url的样子跟我们平时通过a标签跳转的地址没有区别,但是这与传统a标签的区别在于是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。
注意点: 使用history模式的时候是需要后端做一个重定向配置的,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 因为该模式下的url跳转只是通过改变浏览器的记录栈,当直接通过url访问时,服务端的静态资源其实没有对应路径的映射资源,所以会出现404的情况。
两种模式的选择
- hash模式下url会带有#,当你希望url更好看优雅时,可以使用history模式。
- 当使用history模式时,需要注意在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
- 当需要兼容低版本的浏览器时,建议使用hash模式。
vue-router的注入原理
vue-router的初始化使用步骤
1 vue-router的引入
import VueRouter from 'vue-router';
2 加载vue-router(这里利用了vue的插件机制)
Vue.use(VueRouter);
3.实例化 VueRouter
const router = new VueRouter({...routes})
4.把实例化后的router作为vue的实例化参数传入
const app = new Vue({
router
}).$mount('#app');
加载vue-router发生了啥?
vue的插件机制
vue会提供一个use方法,用来加载插件,代码如下:
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
var args = toArray(arguments, 1);
args.unshift(this);
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this
};
这段代码其实很简单,首先,use一个插件对象的时候,会判断这个插件是否已经注册过,如果注册过,直接return this,如果没有注册过,会判断插件是否有install的方法并且类型为function,如果有,则执行,否则判断该插件是否为function,是则执行插件本身,最后将这个插件缓存push入installedPlugins这个数组中缓存起来。
如何利用插件机制加载vue-router
可以看下vue-router源码的install方法:
- 首先是全局注入两个
mixin:beforeCreate和destroyed:
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
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
...
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
首先在每个组件beforeCreate的时候,都会判断这个组件是否有$options.router这个对象,
如果有这个对象的话,会把this赋值到this._routerRoot,同时把this.$options.router赋值到this._router并且调用init的方法去初始化当前的路由,最后通过Vue.util.defineReactive去把this下的_route跟this._router.history.current做一个双向的绑定,同时把这个值劫持添加到Vue的依赖收集中,即把这个对象变成一个reactive对象,具体可以查看Vue.util.defineReactive的官方源码。
如果没有$options.router这个对象,则会往上寻找$parent下的_routerRoot并且赋值到当前this下的_routerRoot,这样就做到了_routerRoot这个对象从root节点一直传到根节点,可以理解为子节点的_routerRoot其实是从根节点无限延伸下来的。
同时可以看到在beforeCreate最后以及destroyed里都会执行一个registerInstance方法,这个方法其实就是判断vm.$options._parentVnode.data.registerRouteInstance是否存在,存在的话会执行,我又翻了一下官方的registerRouteInstance如下:
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
代码里的current代表了route下的matched下的当前instance,在beforeCreate下,由于有val值且为vm组件实例,所以把vm组件实例赋值给当前路由下的instance,当destroyed的时候,val值为空,同时把当前路由下的instance置空。
- 绑定vue的
prototype上的$router和$route两个属性:
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
其实就是把上面定义的_routerRoot._router和_routerRoot._route绑定到每个组件的$router和$route上,这也是vue里每个组件都能调用this.$router和this.$route的原因。
- 注册
view跟link组件:
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
beforeRouteEnter、beforeRouteLeave和beforeRouteUpdate的merge策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
Vue.config.optionMergeStrategies这个对象里存放了vue组件的代码合并策略,这里这段代码的意思是,beforeRouteEnter、beforeRouteLeave 和 beforeRouteUpdate 的 merge 策略都按照created的合并策略来,具体可以看官方合并策略文档。
实例化vue-router
vue-router 实例化后会执行它的constructor方法。首先会初始化各种参数:
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
app跟apps将会用来存放vue的实例。options用来存放实例化传入的配置项。同时初始化beforeHooks,resolveHooks,afterHooks等钩子,初始化matcher对象,关于路由加载,管理,匹配都是VueRouter创建的实例上的一个属性router.matcher做的,这个属性后面会详细讲。
接着是匹配mode:
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}`)
}
}
从上面这段代码中我们可以看到几个关键点:
vue-router默认的mode是hash,并且当选择history默认且有兼容问题时,也会强行把模式向下兼容为hash。- 根据不同的模式,
vue-router会选择不同的实例化的类,HTML5History,HashHistory。 - 如果当前代码的执行环境不是在浏览器而是在服务端的时候,
vue-router会将mode置为abstract,同时提供一个抽象的类AbstractHistory供实例化。
实例化vue的时候
实例化vue的时候我们可以看到,router的实例也被传入vue的实例化参数中,那么这个过程发生了什么呢?
我们可以看vue官方的源码:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
vue实例化后会执行一个_init方法,再找到这个_init:
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
// expose real self
vm._self = vm;
debugger
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
把这段代码精简过后是这样的:
Vue.prototype._init = function (options) {
var vm = this;
...
vm.$options = mergeOptions(options || {}, vm);
...
initState(vm);
...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
...
};
}
跟vue-router有关的其实就是vm.$options = mergeOptions(options || {}, vm);这一段代码。它会把router实例挂载到根节点的$options上,这也就解释了上面的vue-router源码的install方法中可以在根节点的$options.router的来源了。
本篇就先介绍vue-router的基本原理跟vue-router是如何注入vue的,后面会详细讲vue-router的各个内部功能的实现。
参考文章
juejin.cn/post/684490…
www.jianshu.com/p/4295aec31…
segmentfault.com/a/119000001…