深入Vue-router最佳實踐


前言

最近再刷Vue周邊生態的官方文檔,因為之前的學習都是看視頻配合着文檔,但主要還是通過視頻學習,所以很多知識點都沒有了解,至從上次刷了Vuex的官方文檔就體會到了通讀文檔的好處,學習一門技術最好的還是去看它的官方文檔,這樣對於這門技術你就會了解的比較透徹,知識點也比較全面,所以在刷完Vuex文檔之后寫了篇《深入Vuex最佳實踐》,然后花了兩天(上班沒時間摸魚,都是晚上學習)的時間刷完了Vue-router官方文檔,所以有了這篇文章,所以后續還會一篇關於Vue-cli相關的配置文章,所以整篇文章主要從實踐角度切入,可能不會有那么多源碼解析(有點標題黨的味道,哈哈~🤣),但也會涉及到核心功能的源碼解讀

在線卑微,如果覺得這篇文章對你有幫助的話歡迎大家點個贊👻
tip: 文章首發於掘金並做了排版美化推薦掘金閱讀體驗更好 戳我跳轉

簡介

Vue-router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,讓構建單頁面應用變得易如反掌

先來了解兩點

  • 單頁面應用(SPA)
  • 路由管理器

單頁面應用

單頁面應用程序將所有的活動局限於一個Web頁面中,僅在該Web頁面初始化時加載相應的HTML、JavaScript 和 CSS。一旦頁面加載完成了,SPA不會因為用戶的操作而進行頁面的重新加載或跳轉。取而代之的是利用 JavaScript 動態的變換HTML的內容,從而實現UI與用戶的交互。

路由管理器

這里的路由管理器並不是我們並時生活中的硬件路由器,這里的路由就是單頁應用(SPA)的路徑管理器,就是為了解決Vue.js開發單頁面應用不能進行鏈接跳轉,我們通過路徑的的方式來管理不同的頁面

了解Vue-router所解決的問題之后, 我們開始學習Vue-router在項目中常用的一些功能

  • 嵌套的路由/視圖表
  • 模塊化的、基於組件的路由配置
  • 路由參數、查詢、通配符
  • 細粒度的導航控制

起步

在開始我們先體會下Vue-router的一些功能:

  • 動態路由匹配
  • 嵌套路由
  • 聲明式/編程式導航
  • 命名路由/命名視圖
  • 重定向和別名
  • 路由組件傳參

tip:本文章所有實例代碼倉庫地址在文章最后有給出

動態路由匹配

router.js

import Vue from 'vue' // 引入Vue
import Router from 'vue-router' // 引入vue-router
import Home from '@/pages/Home' //引入根目錄下的Hello.vue組件
 
// Vue全局使用Router
Vue.use(Router)

/*
	使用 Vue.js + Vue-router構建單頁面應用, 只要通過組合組件來組成我們的應用程序, 我們引入Vue-router,只要	將組件映射到路由,告訴Vue-router在那里渲染它們
*/

let routes = [ // 配置路由,這里是個數組
  { // 每一個鏈接都是一個對象
    path: '/', // 鏈接路徑
    name: 'Home', // 路由名稱,
    component: Home // 對應的組件模板
  },
  // 動態路徑參數 以冒號開頭
  { path: '/user/:username', // 動態路由
    component: () => import('../pages/User1'), // 按需加載路由對應的組件, 需要下載polyfill兼容ES6語法
  },
  {   // 多段路徑參數
    path: '/user/:id/post/:post_id', // 動態路由
    component: () => import('../pages/User2'), // 按需加載路由對應的組件, 需要下載polyfill兼容ES6語法
  },
]

export default new Router({
  routes
})

User1

用戶訪問 /#/user/xxx的時候展示該組件

<template>
  <div class="User1">
    User1 - 單個路徑參數
  </div>
</template>

User2

用戶訪問 /#/user/xxx/post/xxx的時候展示該組件

<template>
  <div class="User2">
    User2 - 多段路徑參數路由
  </div>
</template

那么問題來了,我們怎么知道用戶訪問的是那個動態參數路由呢?這個時候就要用到響應路由參數的變化

兩種方式:watch (監測變化) $route 對象, beforeRouteUpdate導航守衛

user1.vue增加下面代碼

<template>
  <div class="User1">
    <!-- 通過router對象可以獲取到路由屬性, 這種方式耦合了,后面會講路由組件傳參的方式 -->
    User1 -{{$route.params.username}} 單個路徑參數
  </div>
</template>

<script>
export default {
  name: 'User1',
  // 偵聽route對象方式
  watch: {
    $route (to, from) {
      this.$message.success(`watch -> ${to.path}, ${from.path}`)
    },
    
  },
  // vue2.2引入的導航守衛,當路由參數發生變化調用
  beforeRouteUpdate (to, from, next) {
    this.$message.info(`導航守衛 -> ${to.path}, ${from.path}`)
    // 一定要調用next讓其繼續解析下一個管道中的路由組件
    next()
  }
}
</script>

演示

注意上面從ck->ks路由參數變化時兩種方式都監聽到了,我們可以在這兩個函數中做一些路由狀態變化時的操作

路由組件傳參

上面在<tempate>模板中通過$router.prarams.username方式獲取路由傳遞的參數已經於其對應路由形成高度耦合,限制了其靈活性, 我們可以通過props將組件和路由進行解耦

props傳遞路由組件參數有三種方式:

  • 布爾模式
  • 對象模式
  • 函數模式

代碼

router.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/pages/Home'

Vue.use(Router)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: home
  },
  {  // 動態路徑參數 以冒號開頭
    path: '/user1/:username', // 動態路由
    component: () => import('../pages/User1'),
    props: true  // 布爾模式: 如果 props 被設置為 true,route.params 將會被設置為組件屬性。
  },
  { 
    path: '/user2', 
    component: () => import('../pages/User2'),
    props: {username: 'ck'} // 對象模式: 只有靜態路由才能有效, 並且參數是寫死的
  },
  {
    path: '/user3/:username', 
    component: () => import('../pages/User3'),
    // 返回了用戶url中的參數 比如 /user3?username='ck' => {username: 'ck} 最終還是以對象模式的方式返回參數
    props: (route) => ({username: route.query.username}) // 函數模式
  }
]

export default new Router({
  routes
})

User1

布爾模式

<template>
  <div class="User1">
    User1 -{{username}} 
  </div>
</template>

<script>
export default {
  name: 'User1',
  props: ['username']  // 通過props獲取路由傳遞給對應組件的參數
}
</script>

User2

對象模式

<template>
  <div class="User2">
    User2 - {{username}} 
  </div>
</template>

<script>
export default {
  name: 'User2',
  props: ['username']  // 通過props獲取路由傳遞給對應組件的參數
}
</script>

User3

函數模式

<template>
  <div class="User3">
    User3 - {{username}}
  </div>
</template>

<script>
export default {
  name: 'User3',
  props: ['username']  // 通過props獲取路由傳遞給對應組件的參數
}
</script>

演示

從上面我們可以看出因為user2是靜態路由所以不支持動態參數而且其對應的路由組件傳遞過來的參數也是寫死的

嵌套路由

實際生活中的應用界面,通常由多層嵌套的組件組合而成。同樣地,URL 中各段動態路徑也按某種結構對應嵌套的各層組件,例如:

/user/ck/profile                     /user/ks/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

router.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/pages/Home'

Vue.use(Router)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: home,
  },
  {
    path: '/user/:username', // 動態路由
    name: 'User',
    component: () => import('../components/User'),
    children: [
      {
       // 當 '/user/:username/profile' 匹配成功, UserProfile 會被渲染在 User 的 <router-view> 中
        path: 'profile', // 可以匹配 /user/ks/profile
        name: 'Profile',
        component: () => import('../components/Profile')
      },
      {
        path: '/user/:usrname/posts', // 這樣也可以匹配 /user/ks/posts, 但其實是將其匹配為了根組件的/user:username動態組件下的 posts
        name: 'Posts',
        component: () => import('../components/Posts')
      },
      {
        path: '',
        name: 'UserHome',
        // 當 /user/:username 匹配成功,比如 /user/ks || /user/ck
        // UserHome 會被渲染在 User 的 <router-view> 中
        component: () => import('../components/UserHome')
      },
    ]
  },
  {
    path: '/footer',
    name: 'Foo',
    component: () => import('../components/Footer')
  }
]

export default new Router({
  routes
})

演示

聲明式/編程式導航

聲明式 編程式
<router-link :to="..." replace> router.replace(...)
<template>
  <div class="home">
       <!-- 聲明式 -->
    <router-link
  	  to="footer"
      tag="button"
    >
        to footer
    </router-link>  
      
    <!-- 編程式 -->
    <button @click="$router.push('footer')">字符串-寫路由路徑的方式</button>
    <button @click="$router.push({path: '/footer'})">對象-寫路徑的方式</button>
    <button @click="$router.push({name: 'Foo', params: {'userId': '123'}})">name和params - 寫路由名稱攜帶參數的方式</button>
    <button @click="$router.push({path: '/footer', query: {'userId': '456'}})">queyr和path - 寫路由路徑攜帶參數的方式</button>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'home',
  data () {
    return {
    }
  },
  methods: {
  }
}
</script>
<style>
button {
  display: block;
}
</style>

  • router.push(location, onComplete?, onAbort?)
  • router.replace(location, onComplete?, onAbort?)

這兩種的方式一樣, 唯一區別在於 push會產生路由的歷史記錄, 而repalce不會產生, 這對於window中的history是一致的

<!-- router.go方法 -->
<template>
     <button @click="goForward">go(1)-前進一步</button>
    <button @click="goBack">go(-1)-后退一步</button>
    <button @click="gogogo">go(100)-前進一白步</button>   
 </template>

<script>
 export default {
    name: 'home'
  	methods: {
        goForward () {
          // 從歷史路由中前進一步相當於 window.history.forward
          this.$router.go(1);
        },
        goBack () {
              // 從歷史路由中后退一步相當於 window.history.back
              this.$router.go(-1);
        },
        gogogo () {
              // 歷史路由中沒有100步, 就啥也不干
              this.$router.go(100);
        }
    }  
 }
</script>

演示

命名路由/命名視圖/重定向和別名

router.js

import Vue from 'vue'
import Router from 'vue-router'
import UserSettings from '@/pages/UserSettings'

Vue.use(Router)

let routes = [
  {
    path: '/',
    redirect: '/settings' // 重定向
  },
  {
    path: '/settings',
    name: 'Settings', // 命名路由
    alias: '/a', // 取別名,當url中訪問 /a -> 也是訪問的 settings組件但是路由匹配的是/a, 就相當於用戶訪問 /a一樣
    // 你也可以在頂級路由就配置命名視圖
    component: UserSettings,
    children: [
      {
        path: 'emails',
        component: () => import('../pages/UserEmails')
      }, 
      {
        path: 'profile',
        components: {
          default: () => import('../pages/UserProfile'),
          helper: () => import('../pages/UserProfilePreview')
        }
      }
    ]
  }
]

export default new Router({
  routes
})

UserSetttings

<template>
  <div class="UserSettings">
    <h1>User Settings</h1>
    <NavBar/>
    <router-view/>
    <!-- 命名視圖 -->
    <router-view name="helper"/>  
  </div>
</template>

<script>
import NavBar from '../components/NavBar'
export default {
  name: 'UserSettings',
  components: {
    NavBar
  }
}
</script>

通過上面的學習相信大家已經撐握了Vue-router在項目中所常用的功能,下面我們開始學習Vue-router的導航守衛

進階

導航守衛

“導航”表示路由正在發生改變。記住參數或查詢的改變並不會觸發進入/離開的導航守衛。你可以通過觀察 $route 對象響應路由參數的變化來應對這些變化,或使用 beforeRouteUpdate 的組件內守衛。

全局的守衛

  • 全局前置守衛 (router.beforeEach)
  • 全局解析守衛 (router.breforeResolve)
  • 全局后置鈎子 (router.afterEach) 注:這個鈎子中不存在next

路由獨享的守衛

你可以在路由配置上直接定義 beforeEnter 守衛:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // to -> 要跳轉過去的路由信息
        // from -> 當前的路由信息
        // next() => 一個函數,表示解析下一個管道中的路由記錄
      }
    }
  ]
})

組件內的守衛

最后,你可以在路由組件內直接定義以下路由導航守衛:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染該組件的對應路由被 confirm 前調用
    // 不!能!獲取組件實例 `this`
    // 因為當守衛執行前,組件實例還沒被創建
  },
  beforeRouteUpdate (to, from, next) {
    // 在當前路由改變,但是該組件被復用時調用
    // 舉例來說,對於一個帶有動態參數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,
    // 由於會渲染同樣的 Foo 組件,因此組件實例會被復用。而這個鈎子就會在這個情況下被調用。
    // 可以訪問組件實例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 導航離開該組件的對應路由時調用
    // 可以訪問組件實例 `this`
  }
}

beforeRouteEnter 守衛 不能 訪問 this,因為守衛在導航確認前被調用,因此即將登場的新組件還沒被創建。不過,你可以通過傳一個回調給 next來訪問組件實例。在導航被確認的時候執行回調,並且把組件實例作為回調方法的參數。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 在實例創建好之后會調用next傳遞過去的回調並且將實例當做參數傳遞進來,所以通過 `vm` 可以訪問組件實例
  })
}

注意 beforeRouteEnter 是支持給 next 傳遞回調的唯一守衛。對於 beforeRouteUpdatebeforeRouteLeave 來說,this 已經可用了,所以不支持傳遞回調,因為沒有必要了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

這個離開守衛通常用來禁止用戶在還未保存修改前突然離開。該導航可以通過 next(false) 來取消。

beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

實踐

上面講了那么多相信大家也是懵懵的,這些路由調用的時機是怎么樣的,順序又是怎么樣的,下面我們按照官方給的解釋實踐一下

完整的導航解析流程

  1. 導航被觸發。
  2. 在失活的組件里調用 beforeRouteLeave 守衛。
  3. 調用全局的 beforeEach 守衛。
  4. 在重用的組件里調用 beforeRouteUpdate 守衛 (2.2+)。
  5. 在路由配置里調用 beforeEnter
  6. 解析異步路由組件。
  7. 在被激活的組件里調用 beforeRouteEnter
  8. 調用全局的 beforeResolve 守衛 (2.5+)。
  9. 導航被確認。
  10. 調用全局的 afterEach 鈎子。
  11. 觸發 DOM 更新。
  12. 用創建好的實例調用 beforeRouteEnter 守衛中傳給 next 的回調函數。

router.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'
import {message} from 'ant-design-vue'

Vue.use(VueRouter)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/index',
    name: 'Index',
    component: () => import('../pages/Index'),
  },
  {
    path: '/user/:id',
    name: 'User',
    props: true,
    component: () => import('../pages/User'),
    beforeEnter: (to, from, next) => {
      message.success(`路由獨享守衛[beforeEnter] -> 從${from.path} 到 ${to.path}`);
      next()
    }
  }
]

let router = new VueRouter({
  routes
})
router.beforeEach((to, from, next) => {
  message.success(`全局前置守衛[beforeEach] -> 從${from.path} 到 ${to.path}`, 5)
  next();
})

router.beforeResolve((to, from, next) => {
  message.success(`全局解析守衛[beforeResolve] -> 從${from.path} 到 ${to.path}`, 5)
  next();
})

router.afterEach((to, from) =>  {
  // 鈎子沒有next, 也不會改變導航本身
  message.success(`全局后置鈎子[afterEach] -> 從${from.path} 到 ${to.path}`, 5)
})


export default router

Home.vue

<template>
  <div class="Home">
    <div class="title">Home</div>
    <a-button type="primary" @click="toIndexHanlder">
      to Index
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'Home',
  beforeRouteLeave(to, from, next) {
    this.$message.success(`組件內守衛[leave] -> 從${from.path} 到 ${to.path}`, 5);
    next();
  },
  methods: {
    toIndexHanlder() {
      this.$router.push({ path: '/index' });
    },
  },
};
</script>

Index.vue

<template>
  <div class="Index">
    <div class="title">Index</div>
    <a-button class="my-btn" type="primary" @click="BackHanlder">
      返回
    </a-button>
    <a-button class="my-btn" type="primary" @click="toUserHanlder">
      toUser
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'Index',
  beforeRouteLeave (to, from, next) {
    console.log(to);
    next()
  },
  methods: {
    BackHanlder () {
      this.$router.go(-1)
    },
    toUserHanlder () {
      this.$router.push({path: 'user/ck'})
    }
  }
}
</script>

User.vue

<template>
  <div class="User">
    <div class="title">User - {{id}}</div>
    <a-button class="my-btn" type="primary" @click="toUserJump">
      跳轉動態路由
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'User',
  data () {
    return {
      names: ['a', 'b', 'c', 'd', 'e', 'f'], // 隨機路由
      curNameIndex: 0,
      lastNameIndex: 0,
    }
  },
  props: ['id'],
  beforeRouteUpdate (to, from, next) {
    this.$message.success(`組件內的守衛[beforeRouteUpdate] -> 從${from.path} 到 ${to.path}`, 5)
    next()
  },
  beforeRouteEnter (to, from, next) {
    // 不能獲取this, 因為當守衛執行前,組件實例還沒被創建, 
    // 但是在這個守衛中next支持傳遞回調, 在實例創建完畢后調用 
    next((vm) => {
      // vm => 創建好的Vue實例
      vm.$message.success(`組件內的守衛[beforeRouteEnter] -> 從${from.path} 到 ${to.path}`, 5)
    })
  },
  methods: {
    // 獲取隨機路由的方法
   getRandomArbitrary (min, max) {
        this.curNameIndex = Math.round(Math.random() * (max - min) + min);
        return this.curNameIndex === this.lastNameIndex 
        ? this.getRandomArbitrary(min, max) 
        : this.curNameIndex;
    },
    toUserJump () {
      this.getRandomArbitrary(0, this.names.length -1)
      this.lastNameIndex = this.curNameIndex;
      this.$router.push({path: `/user/${this.names[this.curNameIndex]}`})
    }
  }
}
</script>

演示

上面動圖可能過於快了, 將其截圖下來每一步做下分析

上面標的數子是對應官方給的順序

從Home.vue跳轉到Index.vue觸發的路由守衛

    1. 點擊按鈕導航被觸發
    1. 在失活的組件守衛中(Home.vue)調用的beforeRouterLeave, 表示離開該組件
    1. 調用全局前置守衛beforeEach, 從route對象中可以獲取我們跳轉前和跳轉后的路由信息
    1. 路由解析完畢調用

上面標的數子是對應官方給的順序

Index.vue跳轉到user/ck觸發的路由守衛

    1. 調用全局前置守衛beforeEach
    1. 在路由配置(User.vue)中調用befreEnter
    1. 調用全局的 afterEach 鈎子。
    1. 用創建好的實例調用 beforeRouteEnter 守衛中傳給 next 的回調函數並且將創建好的實例傳遞進去了

user/ck跳轉到user/c觸發的路由守衛

    1. 因為這個組件是動態路由, 在/user/ck -> user/c重用同一個組件所以觸發beforeRoteUpdate

案列

該案列涉及到到了

  • 動態修改路由元信息,修改文檔標題
  • 基於路由的動態過渡
  • 基於導航守衛對用戶登錄狀態進行攔截
  • 對於沒有定義的組件投統一返回404頁面
  • 使用路由的離開守衛判對於用戶跳轉登錄頁面進行確認

戳我去GitHub倉庫地址,歡迎大家點個Start👻

源碼解讀

查看官方vue-router 源碼地址

vue-router 實現原理

vue-router 實例化時會初始化 this.history,傳入不同 mode 會對應不同的 history,下面來看下代碼

constructor (options: RouterOptions = {}) {
    this.mode = mode // 不傳mode, 默認就是hash
    
    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}`)
        }
    }
}
// => 上面通過HashHistory初始化之后會得到其實例,我們調用的一些 push、replace、go都是this.history上的方法

這里以 HashHistory 為例,vue-router 的 push 方法實現如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
}
// 在調用push方法之后調用了this.history的push方法

HashHistory 具體實現了 push 方法:

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path // 本質上還是通過對window.location.hash方法進行的封裝
  }
}

對路由的監聽通過 hash 相應的事件監聽實現:

window.addEventListener(
  supportsPushState ? 'popstate' : 'hashchange',
  () => {
    const current = this.current
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath)
      }
    })
  }
) 

// 對於路由的監聽也是通過監聽window對象提供的 popstate、hashchange兩個事件對於hash的監聽來做出不同的響應

所以,Vue-router最核心的也是通過History來實例相應的功能,而History是由傳遞進去的mode決定,不同的History調用的底層方法不一樣,但底層都是通過window.location提供的一些方法進行實例,比如hash改變就是通過hashchange這個事件監聽來支持的,所以Vue-router本質上就是對於原生事件的封裝

除此之外,vue-router 還提供了兩個組件:

Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// => 所以我們就可以在全局上使用 <router-view> <router-link> 這兩個內置組件

寫在最后

因為是是實踐文,所以這整篇文章都是通過代碼的方式來講的,對於一些概念性和基礎性語法的東西講的比較少。如果 這篇文章對你有幫助請點個贊🤓

看完兩件小事

如果你覺得我的文章對你挺有幫助,我想請你幫我兩個小忙:

  1. 關注我的 GitHub 博文,讓我們成為長期關系
  2. 關注公眾號「前端自學驛站」,所有文章、資料第一時間首發公眾號,公眾號后台回復「教程」 免費領取我精心整理的前端視頻教程

img


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM