本文最后更新于 2024-03-22T23:32:44+00:00
                  
                  
                
              
            
            
              
                
                官方文档:https://router.vuejs.org/zh/guide/ 
路由的演变 之前,部署到服务器的前端项目是由多个 HTML 文件组成,每个 HTML 都有对应服务器路径,前端称其为路由,路由之间使用location.href跳转,跳转路径就是另一个 HTML 的服务器地址。这时候的路由是由后端来管理的 后面单页应用流行,部署到服务器的前端项目就只有一个 HTML 文件,对应一个服务器路径。这时候为满足不同页面的展示,就需要借助框架提供的路由能力,至此路由的管理转移到前端身上。
路由的组成 即location的组成:location.protocal协议location.host 域名location.port 端口(多数省略了)location.pathname  路径location.search  参数,[? 后面,# 之前)的内容location.hash  锚点,# 后面的内容
路由的分类 单页应用下,分为:hash、historyhash:  路由上带 #,内容为 # 后面,用它来区分页面; 不需要服务端配合。
history:  路由上不带 #,内容为[域名后面,? 之前),用它来区分页面; 需要服务端配合。因为部署到服务器后,该模式实际上访问服务器的资源,但单页应用只有一个指向 html 的路径,所以这样访问会返回 404,一般需要配置让其指向 html 的路径
路由实现的核心原理 核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。 
Vue 路由 基础使用 初始化 Vue2 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import  Vue  from  'vue' import  VueRouter , { RouteConfig  } from  'vue-router' import  HomeView  from  '../views/HomeView.vue' Vue .use (VueRouter )const  routes : Array <RouteConfig > = [   {     path : '/' ,     name : 'home' ,     component : HomeView    },   {     path : '/about' ,     name : 'about' ,                    component : () =>  import ( '../views/AboutView.vue' )   } ]const  router = new  VueRouter ({   routes })export  default  routerimport  Vue  from  'vue' import  App  from  './App.vue' import  router from  './router'  Vue .config .productionTip  = false new  Vue ({   router,    render : h  =>  h (App ) }).$mount('#app' )
 
Vue3 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import  { createRouter, createWebHistory } from  'vue-router' import  HomeView  from  '../views/HomeView.vue' const  router = createRouter ({   history : createWebHistory (import .meta .env .BASE_URL ),   routes : [     {       path : '/' ,       name : 'home' ,       component : HomeView      },     {       path : '/about' ,       name : 'about' ,                            component : () =>  import ('../views/AboutView.vue' )     }   ] })export  default  routerimport  { createApp } from  'vue' import  App  from  './App.vue' import  router from  './router'  const  app = createApp (App ) app.use (router)  app.mount ('#app' )
 
页面中使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template>   <div  id ="app" >     <nav >        // ⭐️       <router-link  to ="/" > Home</router-link >        <router-link  @click ="bandleNavClick" > About</router-link >      </nav >      <router-view />  // ⭐️   </div >   </template><script >   export  default  {     computed : {       username ( ) {                  return  this .$route .params .username        },     },     methods : {       bandleNavClick ( ) {                  this .$router .push ({           path : '/about'          })       }     }   } </script > 
 
路由守卫 全局:beforeEach、beforeResolve、afterEach 路由配置:beforeEnter 组件内:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
完整的导航解析流程 
导航被触发。 
在失活的组件里调用 beforeRouteLeave 守卫。 
调用全局的 beforeEach 守卫。 
在重用的组件里调用 beforeRouteUpdate 守卫。 
在路由配置里调用 beforeEnter。 
解析异步路由组件。 
在被激活的组件里调用 beforeRouteEnter。 
调用全局的 beforeResolve 守卫。 
导航被确认。 
调用全局的 afterEach 钩子。 
触发 DOM 更新。 
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。 
 
源码解析 Vue-Router 3.x 对应 Vue2.xVueRouter部分源码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 export  default  class  VueRouter  {   static  install (Vue ) {               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 )       }     })                    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 )   },   constructor  (options : RouterOptions  = {}) {               this .options  = options          switch  (mode) {       case  'history' :         this .history  = new  HTML5History (this , options.base )         break        case  'hash' :         this .history  = new  HashHistory (this , options.base , this .fallback )         break             }   } }
 
RouterView源码解析 本质上注册了一个 Vue 动态组件,根据路由配置,找到对应component值,然后将其渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export  default  {   name : 'RouterView' ,   functional : true ,   props : {     name : {       type : String ,       default : 'default'      }   },      render (_, { props, children, parent, data }) {     const  h = parent.$createElement      const  name = props.name      const  route = parent.$route           const  matched = route.matched [depth]           const  component = matched && matched.components [name]          return  h (component, data, children)   } }
 
下面的代码可以粗略表述上述源码逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14  <template>   <component  :is ="componentContent"  />  </template><script >   export  default  {     computed : {       componentContent ( ) {                  return  this .$route .matched [0 ]?.components        }     }   } </script > 
 
RouterLink源码解析 本质上注册了一个 Vue 组件,该组件最终渲染为a 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export  default  {   name : 'RouterLink' ,   props : {     to : {       type : toTypes,       required : true      },     tag : {       type : String ,       default : 'a'      },        },   render (h : Function ) {     const  router = this .$router      const  current = this .$route                const  tagAttrs = {}     return  h (this .tag , tagAttrs, this .$slots .default )   } }
 
1 2 比如:<router-link to="/about">About</router-link> 渲染:<a href="#/about" class="">About</a>
 
Vue-Router 4.x 对应 Vue3.x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import  { createRouter, createWebHistory } from  'vue-router' import  HomeView  from  '../views/HomeView.vue' const  router = createRouter ({   history : createWebHistory (import .meta .env .BASE_URL ),   routes : [     {       path : '/' ,       name : 'home' ,       component : HomeView      },     {       path : '/about' ,       name : 'about' ,                            component : () =>  import ('../views/AboutView.vue' )     }   ] })export  default  routerimport  { createApp } from  'vue' import  App  from  './App.vue' import  router from  './router'  const  app = createApp (App ) app.use (router)  app.mount ('#app' )
 
createRouter源码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 export  function  createRouter (options: RouterOptions ): Router  {      const  matcher = createRouterMatcher (options.routes , options)      const  router : Router  = {          push,     replace,     go,     back : () =>  go (-1 ),     forward : () =>  go (1 ),          install (app: App ) {       const  router = this               app.component ('RouterLink' , RouterLink )              app.component ('RouterView' , RouterView )                    app.config .globalProperties .$router  = router                     Object .defineProperty (app.config .globalProperties , '$route' , {         enumerable : true ,         get : () =>  unref (currentRoute),       })              const  reactiveRoute = {} as  RouteLocationNormalizedLoaded        for  (const  key in  START_LOCATION_NORMALIZED ) {         Object .defineProperty (reactiveRoute, key, {           get : () =>  currentRoute.value [key as  keyof RouteLocationNormalized ],           enumerable : true ,         })       }                     app.provide (routerKey, router)       app.provide (routeLocationKey, shallowReactive (reactiveRoute))       app.provide (routerViewLocationKey, currentRoute)            },   }   return  router }
 
共同点 为啥跳转页面并不会刷新? 不管路由模式是hash、history,最终跳转页面时Vue Router都是用的window.history.pushState,用该 API 改变地址,页面将不会刷新。 路由模式差异体现在window.history.pushState的传参url上,带不带#而已
并且#的变化本身也不会引起页面的刷新
面试题 手写路由(简单版) 核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class  Router  {   constructor (options ) {     this ._options  = options;     this ._routes  = options.routes ;     this .routerHistory  = [];     this .currentIndex  = -1 ;     this .currentPath  = "" ;     this .init ();   }   init ( ) {     window .addEventListener ("hashchange" , this .refresh .bind (this ));     window .addEventListener ("load" , this .refresh .bind (this ));   }   refresh ( ) {     let  _path = Router .getPath ();     this .routerHistory  = this .routerHistory .slice (0 , this .currentIndex  + 1 );     this .routerHistory .push (_path);     this .currentIndex ++;     let  { component, path } = this .findRoute (_path);     if  (!component) {       path = "/404" ;       component = this .findRoute (path).component  || "404" ;     }     document .querySelector (".router-view-wrapper" ).innerHTML  = component;     Router .changeHash (path);   }   push (options ) {     if  (options.path ) {       Router .changeHash (options.path );     } else  if  (options.name ) {       let  { path } = this .findRoute (options.name , "name" );       Router .changeHash (path);     }   }   findRoute (value, key = "path"  ) {     let  _findRoute = this ._routes .find ((item ) =>  item[key] === value) || {};     if  (_findRoute.rederict ) {       return  this .findRoute (_findRoute.rederict , key);     }     return  _findRoute;   }   static  getPath ( ) {     let  path = window .location .hash ;     if  (path) return  path.replace ("#" , "/" );     else  return  "/" ;   }   static  changeHash (path ) {     window .location .hash  = path.slice (1 );   } }
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <!DOCTYPE html > <html  lang ="en" >    <head >      <meta  charset ="UTF-8"  />      <meta  name ="viewport"  content ="width=device-width, initial-scale=1.0"  />      <title > custom-router</title >    </head >    <body >      <div  id ="app" >        <div >          <a  onclick ="router.push({name:'home'})" > Home</a >          <a  onclick ="router.push({name:'about'})" > About</a >        </div >        <div  class ="router-view-wrapper" > </div >      </div >      <script  src ="./router.js" > </script >      <script >       const  routes = [         {           path : "/" ,           rederict : "/home" ,         },         {           path : "/home" ,           name : "home" ,           component : `<div>Home Content</div>` ,         },         {           path : "/about" ,           name : "about" ,           component : `<div>About Content</div>` ,         },         {           path : "/404" ,           name : "404" ,           component : `<div>Error 404</div>` ,         },       ];       const  router = new  Router ({ routes });      </script >    </body > </html >