Vue-router 源码解析

吐槽君 分类:javascript

Vue路由

什么是SPA

SPA 是single page application 的简称,翻译为单页应用。
简单来说SPA就是一个Web项目只有一个HTML页面,一但页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用JS动态的变换HTML的内容,来模拟多个视图之间的跳转。

前端路由的出现

随着AJAX技术的出现,才逐渐演化出了SPA,SPA的出现大大提高了用户的体验。在于用户的交互中,不再需要刷新页面,通过AJAX技术异步获取数据,页面展示变得更加流畅。

但由于SPA中用户的交互是通过JS改变HTML内容来实现的,页面本身的URL并没有发生变化,这导致了两个问题:

  • 1、SPA无法记录用户的前进、后退操作
  • 2、SPA中虽然业务的不同会有多种页面的展示,但是只有一个URL,对用户来说不够友好,每次刷新页面,都要重新点一遍要去到的页面
  • 3、没有路径-页面的统一概念,页面管理复杂,维护起来不友好

前端路由就是为了解决上述问题而出现的,如vue-router,react-router

Vue-router

前端路由就是一个路径管理器,通俗的说,vue-router就是一个WebApp的链接路径管理系统。vue的单页面应用是基于路由和组件的,路由用来设置访问路径,相当于页面对应的URL,并将路径和组件映射起来。
传统的页面应用,是采用超链接的方式来实现页面切换和跳转的。在vue-router中,则是路径之间的切换,也就是组件的切换。
路由模块的本质就是建立起URL和页面之间的映射关系,在刷新、前进、后退时均通过url来实现。

为什么不用a标签:
因为用vue做的都是单页应用,相当于只有一个主index.html(也就),所以a标签是起不到作用的,你必须使用vue-router来管理。

原理

简单的说,只有一个HTML页面,为每一个组件匹配一个路由(访问路径)。在用户刷新、前进、后退、跳转操作时,不更新整个页面,而是根据路由只更新某个组件。
要做到上面的需求我们满足以下两个核心:

  • 1、改变url而不让浏览器向服务器发送请求。
  • 2、可以监听到url的变化,并执行对应的操作(如组件切换)

对此,有两种模式实现的以上的功能:hash模式history模式

hash模式

vue-router默认采用hash模式:使用url后的#后面的字符。比如www.baidu.com/#aaaa其中的#aaaa就是我们要的hash值。
为什么可以用hash

  • 1、hash值的变化不会导致浏览器向服务器发送请求,不会引起页面刷新。
  • 2、hash值的变化改变会触发hashchange事件
  • 3、hash值改变也会在浏览器的历史中留下记录,使用浏览器的后退按钮,就可以回到上一个hash

H5history模式出现之前,基本都是使用hash模式实现前端路由的

history模式

HTML5之前,浏览器就已经有了history对象。但是只能用于页面跳转

  • history.go(n) // 前进或者后退,n代表页数,负数表示后退
  • history.forward() // 前进一页
  • history.back() // 后退一页

HTML5中,history新增了以下API

  • history.pushState() // 向历史栈中添加一新的状态
  • history.replaceState() // 修改当前项在历史栈中的记录
  • history.state // 返回当前状态对象

pushStatereplaceState均接受三个参数(state,title,url)

  • state: 合法的js对象,可以用在popstate事件中
  • title: 大多数浏览器忽略这个参数,可以传null占位
  • url: 任意有效的URL(必须与当前url同源,否则会抛出异常),用于更新浏览器的url。在调用pushStatereplaceState之后,会立即产生一个新的带有新url的历史记录(当前url已变成最新的),但浏览器不会立即加载这个url(页面不会刷新),可能会在稍后某些情况下加载这个url,比如用户重新打开浏览器或在地址栏中按回车键。

pushStatereplaceState的区别在于:

  • pushState会保留现有历史记录的同时,将带有新url的记录添加到历史栈中。
  • replaceState会将历史栈中的当前页面历史中的state、url替换为最新的

由于pushStatereplaceState方法可以在改变当前url的同时,并不刷新页面,这样就可以用来实现前端路由。

但是由于调用pushStatereplaceState不会触发事件(popstate事件只有在用户手动点击前进、后退按钮或者在js中调用go、back、forward方法时才会触发),所以有两种对应的解决方案:

  • 1、点击前进、后退按钮或者在js中调用go、back、forward方法:监听popstate事件,在事件回调中根据当前的url,渲染对应的页面即可。
  • 2、当需要跳转路径时(调用push方法时):先根据拿到的路径渲染对应的页面,然后在页面渲染完成之后通过pushStatereplaceState方法来更新浏览器的url。

这样就实现了history模式的前端路由

history 在修改了url后,虽然页面不会刷新,但是我们在手动刷新页面之后,浏览器会以当前url向服务器发送请求,但是由于我们只有一个html文件,浏览器在处理其他路径的时候,就会出现404的情况。此时需要在服务端增加一个覆盖所有情况的候选资源:如果url匹配不到静态资源,就返回单页应用的html文件。这样,就完全交给了前端来处理对应的路由。

hash、history模式的选择

hash优点:

  • 1、兼容性好,可以兼容IE8
  • 2、不需要服务端做任何配置即可处理单页应用

缺点:

  • 1、路径丑
  • 2、锚点功能会失效
  • 3、相同的hash不会更新历史栈

history优点:

  • 1、路径更好看
  • 2、锚点功能可用
  • 3、pushState可添加相同的记录到历史栈中

缺点:

  • 1、兼容性差,不能兼容IE9
  • 2、需要服务端支持

vue-router的使用

  • 1、在要使用路由的项目中打开CMD命令 输入npm install vue-router,安装vue的路由模块

  • 2、在页面中先引入vue.js,再引入vue-router.js,因为vue-router是基于vue的。利用Vue.js提供的插件机制,Vue.use(plugin)来安装VueRouter,这样做的目的是为了不依赖Vue的版本,两者的版本可以混用。这个插件机制的原理是调用插件的install方法(如果有install方法,没有就把传入的插件当成函数调用)。

import Vue from 'vue'
import VueRouter from 'vue-router'
// 将VueRouter当做插件传入:目的是为了不依赖vue,可以版本混用,也可以只用VueRouter
Vue.use = function(plugin, options){
  plugin.install(this, options)
}
Vue.use(VueRouter) // 会调用插件的install方法并把Vue传进去
 

install.js的作用:

  • 1、引入Vue并保存,方便之后使用
  • 2、将根组件注入到每一个组件上,保证每一个Vue的实例都可以拿到根组件上的router属性
  • 3、实现路由响应式原理,将根组件上的_route属性定义为响应式数据,这样_route属性改变就可以更新视图
  • 4、初始化路由
  • 5、注册全局路由组件
// install.js
import View from './components/view'
import Link from './components/link'

export let _Vue 

export function install(Vue){
  _Vue = Vue
  // 采用mixin 的 方式,将根组件注入到每一个组件上
  Vue.mixin({
    beforeCreate(){
      // 存在router属性则代表是根组件
      if(this.$options.router){
        this._routeRoot = this
        this._router = this.$options.router
        /* 
          此处init要放在defineReactive之前。
          因为首次渲染已经存在路径,需要在init时 transitionTo更新history.current,
          init之后,此时history.current已经是当前url对应的路由对应的记录,
          接下来我们只需要在 根组件上定义响应式属性_route,指向最新的路由记录,即可更新视图
        */
        this._router.init(this)
        Vue.utils.defineReactive(this,'_route', this._router.history.current)
      } else {
        this._routeRoot = this.$parent && this.$parent._routeRoot || this
      }
    }
  })
  // 为了在组件中方便使用属性路由属性,增加属性代理
  Object.defineProperty(Vue.property, '$route',{
    get(){
      return this._routeRoot._route
    }
  })
  Object.defineProperty(Vue.property, '$router',{
    get(){
      return this._routeRoot._router
    }
  })
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}
 

实例化VueRouter

  • 1、声明创建路由表,在路由表中将路径和对应的组件关联起来
  • 2、创建路由实例,初始化路由并传入路由表
  • 3、在vue的实例中注册路由
import App from './App'
let Home = { template:'<div>首页</div>' };
let List = { template:'<div>列表页</div>' };


// 1、创建路由实例
let router = new VueRouter({//初始化路由:传入路由表
  mode: 'history', // 需要后端做支持
  routes:[
    // 一级路径前必须加 ‘/’
    { path: '/', component:Home },//默认展示的路由(默认展示的不需要加/)
    { path: '/home', component:Home },//一个路径对应一个组件
    { path: '/list', component:List },
    { path: '*', redirect:'/home' }//用户随便输入路径时,重定向到home组件,防止出现404
  ] // es6简写
});

//2、在vue根实例中注入路由实例,之后就可以在页面中使用了
new Vue({
  el:'#app',
  router, // 注入路由(es6简写)
  render: h => h(App)
})
 
// vue路由src/index.js
export default class VueRouter {
  // 会根据对应的模式生成对应的 history实例,挂载到当前类的实例上
  init(app){ // 此处传入的是在install.js中调用router.init时 传入的根组件的实例
    const history = this.history
    const setupListener = ()=>{
      // 方便加一些其他的操作
      history.setupListener()
      // todo...
    }
    // 拿到当前的路径,调用transitionTo方法,根据路径匹配到对应的路由记录route,
    history.transitionTo(history.getCurrentLocation(), setupListener)
    history.listen(route =>{
    /* 
      因为在install.js中 根(app)组件的_route属性已经被定义为了响应式的
      Vue.util.defineReactive(this, '_route', this._router.history.current)
      所以只需要更新数据,就可以驱动视图更新
    */
      app._route = route
    })
  }
}
 

router-view

router-view(全局组件:用来渲染路由对应的组件)
在页面中使用router-view这个全局组件来将路由对应的页面渲染到页面上

<div id="app">
  <router-view></router-view>
</div>
 

router-link

使用router-link全局组件,来实现点击跳转

router-link存在两个属性:

  • to:跳转到哪个(必须加,值为要跳转的路径)
  • tag:要把router-link变为哪个标签(不改默认是a标签)
	<!-- 修改上面的HTML如下 -->
<div id="app">
  <router-link to="/home" tag="button">首页</router-link>
  <router-link to="/list" tag="button">列表页</router-link>
  <!-- 如果写成对象的形式,而且需要params参数的话,就只能用name来实现跳转了(用ptah的话会导致params不生效) -->
  <router-link :to="{name:'list',params:{userId:1}}" tag="button">列表页</router-link>
  <router-view></router-view>
</div>
 

路由信息与方法

当在vue的实例中注册过路由之后,每个vue组件实例都可以通过获取$router$route属性来获取根组件上的_router_route属性(因为在Vue.use注入的时候,install.js中已经做了代理);

  • $route:路由信息对象,表示当前激活的路由的状态信息,包含了当前URL解析的信息,还有URL匹配到的路由记录
  • $router:当前的路由的实例,原型上有各种跳转的方法

$router

  • this.$router.push():强制跳转到某个路径,参数为路径
  • this.$router.replace():路由替换,将当前路径替换为新的路径(很少用到)
  • this.$router.go():返回某一级,参数为返回多少级(-1为上一级,1为下一级)

$route

当前激活的路由信息对象。这个属性是只读的,里面的属性是 immutable (不可变) 的,不过可以 watch (监测变化) 它。

  • $route.path:字符串,对应当前路由的路径,总是解析为绝对路径,如 /foo/bar
  • $route.params:一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。
  • $route.query:一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1,如果没有查询参数,则是个空对象。
  • $route.hash:当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串。
  • $route.fullPath:完成解析后的 URL,包含查询参数和 hash 的完整路径。
  • $route.name:当前路由的名称,如果有的话。
  • $route.redirectedFrom:如果存在重定向,即为重定向来源的路由的名字。
//由于路径有很多,而我们不能把路径写死,所以要写成类似正则的形式来匹配路径
 /article/2/d //一个路径
 /article/:c/:a //表示路径匹配,和上面的匹配后产生一个对象,存在$route.params当中:{c:2,a:d}
 

路由的嵌套

可在路由表中嵌套二级路由,嵌套二级路由的一级路由的template也要做对应的修改;

<div id="app">
  <router-link to="/home">首页</router-link>
  <router-link to="/detail">详情页</router-link>
  <router-view></router-view><!--一级路由显示区域-->
</div>
<template id="detail">
  <div>
    <router-link to="/detail/info">个人中心</router-link>
    <router-link to="/detail/about">关于我</router-link>
    <router-view></router-view><!--二级路由显示区域-->
  </div>
</template>
 
//组件
let home={template:'<div>home</div>'};
let detail={template:'#detail'};
let info={template:'<div>info</div>'};
let about={template:'<div>about</div>'};
//创建路由表
let routes=[
  { path:'/home',component:home },
  {
    path:'/detail',
    component:detail,
    //二级路由写在childern属性当中
    children:[
    //二级以及二级以上路由的路径永远不带‘/’,如果带‘/’代表是一级路由
      { path:'info',component:info },
      { path:'about',component:about }
    ]
  },
];
//初始化路由并传入路由表
let router = new VueRouter({ 
  mode: 'history',
  routes
});

let vm = new Vue({
  el:'#app',
  //注册路由
  router
})
 

动态路由

需要用到的方法:

  • router.beforeEachvue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
  • router.addRoutes():动态挂载路由(此方法的作用只是在路径中访问时可以看到对应的页面,但是菜单展示还需要),参数必须是一个符合 routes 选项要求的数组

这里还有一个小hack的地方,就是router.addRoutes之后的next()可能会失效,因为可能next()的时候路由并没有完全add完成,好在查阅文档发现next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.

这样我们就可以简单的通过next(to)巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach这个钩子函数,这时候再通过next()来释放钩子,就能确保所有的路由都已经挂载完成了。

router index.js文件

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

// 创建静态路由表
export const constantRouterMap = [
  { path: '/404', component: () => import('@/views/404'), hidden: true },
  {
    path: '/',
    component: Layout,
    redirect: '/home',
    name: 'Home',
    children: [{
      path: 'home',
      component: () => import('@/views/home/index'),
      meta: { title: '首页', icon: 'home' }
    }]
  }
]
// 初始化路由传入静态路由表并导出
export default new Router({
  mode: 'history',// 使用history模式
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRouterMap
})

// 全部路由(需要过滤的动态路由表,以后加项目的话直接加在这里即可,拉到权限后需要根据权限过滤此表)
export const asyncRouterMap = [
  // 权限管理
  {
    path: '/access',
    component: Layout,
    name: 'access',
    children: [
      {
        path: 'index',
        name: 'access/index',
        component: () => import('@/views/access/index'),
        meta: { title: '权限管理', icon: 'lock' }
      }
    ]
  },
  // 运营后台
  {
    path: '/operation',
    component: Layout,
    name: 'operation',
    meta: {
      title: '运营后台',
      icon: 'operation'
    },
    children: [
      // 映客直播
      {
        path: 'live',
        component: () => import('@/views/operation/index'), // Parent router-view
        name: 'operation/live',
        meta: { title: '映客直播' },
        children: [
          // 意见反馈
          {
            path: 'feedback',
            name: 'operation/live/feedback',
            component: () => import('@/views/feedback/index'),
            meta: { title: '意见反馈' }
          }
        ]
      }
    ]
  }
]
 

路由创建好之后需要在拉取权限之后对全局路由表进行过滤,筛选掉没有用的路由。

一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

回复

我来回复
  • 暂无回复内容