vue-router

2023-05-06 18:00:14

安裝

vue-router是一個vue的外掛,用來實現前端的路由, 推薦使用 pnpm add vue-router@4 進行安裝。推薦配合vue3組合式api使用

基礎

從一個例子開始

<!-- App.vue檔案 -->
<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!--使用 router-link 元件進行導航 -->
    <!--通過傳遞 `to` 來指定連結 -->
    <!--`<router-link>` 將呈現一個帶有正確 `href` 屬性的 `<a>` 標籤-->
    <router-link to="/hello/112233">hello</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的元件將渲染在這裡 -->
  <router-view></router-view>
</div>
// router/index.js 檔案

//createRouter 用來建立路由
//createWebHashHistory 指定路由是hash模式

import { createRouter,createWebHashHistory } from 'vue-router'

import Hello from '../components/Hello.vue'
import ErrorPage from '../components/ErrorPage.vue'

// 設定路由規則
const routers = [
    // 匹配所有路由(正則匹配權重低),自定義404
    {path: '/:all(.*)*', component: ErrorPage},
    {path: '/hello/:id',component: Hello}
]

// 匯出路由
export default createRouter({
    history: createWebHashHistory(),    // 這裡我們使用簡單的hash模式
    routes: routers
})
// main.js 檔案
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import routers from './routers'

createApp(App)
    .use(routers)		// 掛載路由
    .mount('#app')

專案中的路由物件

tips: 別忘記掛載路由哦 如 use(router)

  1. $router: 通過this呼叫,或者通過 import匯出路由也一樣,此路由物件,就是匯出的路由物件,可以呼叫push等 ... <-
  2. $route: 通過this呼叫,此路由物件是當前的路由物件,比如說 /Home 拿到的是當前 /Home 的路由物件,可以通過 $route.params 拿到路由引數等

動態路由

匹配規則

匹配模式 匹配路徑 匹配說明 $route.params $route.query $route.hash
/users/:username /users/eduardo?sid=123#ok ok
/users/:username/posts/:postId /users/eduardo/posts/123?sid=123#ok ok
/:pathMatch(.*)* /a/b 匹配所有
/user-:afterUser(.*) /user-admin 匹配user- 以開頭的
/:orderId(\\d+) /123 匹配數位
/:chapters+ /a/a/a 匹配重複的(一個或多個)

從上面可以看出,路由匹配是支援自定義正則的,官方推薦的路由偵錯工具 https://paths.esm.dev/

Sensitive 與 strict 路由設定

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { 
        // /users	ok
        // /Users	error
        // /user/	error
        path: '/users', 
        sensitive: true,	// 大小寫敏感
        strict: true, 		// 是否嚴格按照path匹配
        
    },
  ],
  strict: true,  // 或者這裡設定全域性
})

巢狀路由

比如我們有兩個路由 /home/user,/home/posts。可以看出來他們都是處於 /home下的,屬於是巢狀關係,我們可以使用以下程式碼實現

const routes = [
  {
    path: '/home',
    component: Home,
    children: [			// home的子級
      {
        path: 'user',
        component: User,
      },
      {
        path: 'posts',
        component: Posts,
      },
    ],
  },
]

程式設計式導航

頂文的有個例子使用 <router-link to="/home">點我啊</router-link> 進行路由導航,其實點選 <router-link :to="..."> 相當於呼叫 router.push(...)

程式設計時導航就是通過js程式碼來跳轉路由,通過以下例子介紹下

// 字串路徑
router.push('/users/admin')

// 帶有路徑的物件
router.push({ path: '/users/admin' })

// 命名的路由,讓路由建立 url, 可以傳遞params, query,hash, 傳遞的引數vue-router會自動編碼,如果放在path中需要先編碼一下
router.push({ name: 'user', params: { username: 'admin' }, query: { plan: '花兒為什麼這麼紅' }, hash: '#h1' } })


  • push: 這個方法會向 history 棧新增一個新的記錄,所以,當用戶點選瀏覽器後退按鈕時,會回到之前的 URL
  • replace: 會替代當前位置,無法回退,其餘的同上
  • forward: 向前移動一條記錄
  • back: 向後移動一條記錄
  • go: go(-1)後退,反之亦然

命名路由

除了 path 之外,你還可以為任何路由提供 name。這有以下優點:

  • 沒有寫死的 URL
  • params 的自動編碼/解碼。
  • 防止你在 url 中出現打字錯誤。
const routes = [
  {
    path: '/user/:username',
    name: 'user',
    component: User,
  },
]
router.push({ name: 'user', params: { username: 'any' } }

命名檢視

有時候我們想要在同一個頁面裡展示多個檢視,又不想巢狀展示,例如建立一個佈局,sidebar(側邊欄),main(主要區域),範例程式碼如下。當然也可以巢狀命名檢視,

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的縮寫
        LeftSidebar,
        // 它們與 `<router-view>` 上的 `name` 屬性匹配
        RightSidebar,
      },
    },
  ],
})


// 或使用巢狀命名檢視

{
  path: '/settings',
  // 你也可以在頂級路由就設定命名檢視
  component: UserSettings,
  children: [{
    path: 'emails',
    component: UserEmailsSubscriptions
  }, {
    path: 'profile',
    components: {
      default: UserProfile,
      helper: UserProfilePreview
    }
  }]
}

重定向和別名


// redirect可以是一個字串,一個物件,一個函數。範例如下
const routes = [
  {
    // /search/screens -> /search?q=screens
    path: '/search/:searchText',
    redirect: to => {
      // 方法接收目標路由作為引數
      // return 重定向的字串路徑/路徑物件
      return { path: '/search', query: { q: to.params.searchText } }
    },
  },
  {
    path: '/search',
    // ...
  },
]
// 別名,就是說當存取這個別名的時候,跳轉到此路由,不受路由限制。推薦少用少用少用!!!

const routes = [
    {
        path: '/home',
        alias: ['/',‘’]		// 也可以設定兩個別名
    }
]


給元件傳遞props

我們可以通過$route拿到引數,但是這樣寫的話元件過於依賴路由,我們有更好的一種方式,通過設定 props: true 開啟props傳參, 範例如下

const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    
    // 有以下幾種寫法
      
    // 1:props: true
    // 2:props: { default: true, sidebar: false }
    // 3:{ id: '傳入的引數' }	
    // 4: route=> ({ id: route.params.id })
  }
]

路由模式

Hash 模式

在瀏覽器url使用了雜湊字元 # 這部分url沒有被傳送伺服器。 在SEO優化 有不好的影響,如果擔心這個問題可以使用HTML5模式

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

HTML5 模式

這種模式看起來像一個比較正常的url 比如 https://example.com/home/user 。 需要在伺服器設定

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

伺服器設定

  1. nginx

    location / {
      try_files $uri $uri/ /index.html;
    }
    
  2. node

    const http = require('http')
    const fs = require('fs')
    const httpPort = 80
    
    http
      .createServer((req, res) => {
        fs.readFile('index.html', 'utf-8', (err, content) => {
          if (err) {
            console.log('We cannot open "index.html" file.')
          }
    
          res.writeHead(200, {
            'Content-Type': 'text/html; charset=utf-8',
          })
    
          res.end(content)
        })
      })
      .listen(httpPort, () => {
        console.log('Server listening on: http://localhost:%s', httpPort)
      })
    
    1. iis

      <?xml version="1.0" encoding="UTF-8"?>
      <configuration>
        <system.webServer>
          <rewrite>
            <rules>
              <rule name="Handle History Mode and custom 404/500" stopProcessing="true">
                <match url="(.*)" />
                <conditions logicalGrouping="MatchAll">
                  <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                  <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                </conditions>
                <action type="Rewrite" url="/" />
              </rule>
            </rules>
          </rewrite>
        </system.webServer>
      </configuration>
      

進階

好啦,看到這已經理解基礎了,想了解更的話繼續往下看,我們有時候需要做點別的 事情

導航守衛

導航守衛就是AOP的思想,在路由進入前進入後更新,做點事情。導航守衛可以使用多個會依次執行,類似於管道,路由匹配的元件會複用。 它可以掛載到全域性單個路由單個元件。各有妙用,望君細品。程式碼範例如下

全域性守衛

// 進入前
router.beforeEach((to, from)=>{
    //返回 false 以取消導航,也可以返回一個物件,比如 {name:'home'},相當於重定向
})

// 或者加入第三個引數 next,如果宣告了第三個引數,則必須呼叫一個,否則會一直等待
router.beforeEach((to, from, next)=>{
    
    // next(false) 或者 next({path: '/home'})
})

// 進入後
router.afterEach((to, from)=>{
    
})

// 解析守衛,發生在beforeEach後
router.beforeResolve((to, from)=>{
    
})



路由專用守衛

// 進入路由觸發(但是還沒有進入,在beforeResolve前觸發)


function removeQueryParams(to) {
  if (Object.keys(to.query).length)
    return { path: to.path, query: {}, hash: to.hash }
}

function removeHash(to) {
  if (to.hash) return { path: to.path, query: to.query, hash: '' }
}


const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],	// 可以是陣列,按順序呼叫
  },
]

元件內的守衛

// 選項式: beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
// 組合式:onBeforeRouteUpdate,onBeforeRouteLeave 

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染該元件的對應路由被驗證前呼叫
    // 不能獲取元件範例 `this` !
    // 因為當守衛執行時,元件範例還沒被建立!
  },
  beforeRouteUpdate(to, from) {
    // 在當前路由改變,但是該元件被複用時呼叫
    // 舉例來說,對於一個帶有動態引數的路徑 `/users/:id`,在 `/users/1` 和 `/users/2` 之間跳轉的時候,
    // 由於會渲染同樣的 `UserDetails` 元件,因此元件範例會被複用。而這個勾點就會在這個情況下被呼叫。
    // 因為在這種情況發生的時候,元件已經掛載好了,導航守衛可以存取元件範例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在導航離開渲染該元件的對應路由時呼叫
    // 與 `beforeRouteUpdate` 一樣,它可以存取元件範例 `this`
  },
}

完整的導航解析流程

  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 的回撥函數,建立好的元件範例會作為回撥函數的引數傳入。

路由元資訊

有時候想要將資訊附加到路由上,我們看下面這個例子

const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 只有經過身份驗證的使用者才能建立貼文
        meta: { requiresAuth: true }
      },
      {
        path: ':id',
        component: PostsDetail
        // 任何人都可以閱讀文章
        meta: { requiresAuth: false }
      }
    ]
  }
]
// 我們可以通過 $route.meta 拿到meta資料

router.beforeEach((to, from) => {
  // 而不是去檢查每條路由記錄
  // to.matched.some(record => record.meta.requiresAuth)
  if (to.meta.requiresAuth && !auth.isLoggedIn()) {
    // 此路由需要授權,請檢查是否已登入
    // 如果沒有,則重定向到登入頁面
    return {
      path: '/login',
      // 儲存我們所在的位置,以便以後再來
      query: { redirect: to.fullPath },
    }
  }
})

在組合式API的應用

在組合式API公開下面兩個導航守衛API

import { onBeforeRouteUpdate,onBeforeRouteLeave } from 'vue-router'

onBeforeRouteLeave: 即將離開路由

onBeforeRouteUpdate: 路由的hash或者query發生了變化

自定義router-link


<template>
    <div @click="handle">
    	<slot></slot>
    </div>
</template>


<script>

import { RouterLink, useLink } from 'vue-router'
import { computed } from 'vue'

export default {

name: 'AppLink',
props: {
 // 如果使用 TypeScript,請新增 @ts-ignore
 ...RouterLink.props,
 inactiveClass: String,
},
methods:{

 handle(){
   this.$router.push(this.to)
 }
},
setup(props) {
 	const {
       // 解析出來的路由物件
       route,
       // 用在連結裡的 href
       href,
       // 布林型別的 ref 標識連結是否匹配當前路由
       isActive,
       // 布林型別的 ref 標識連結是否嚴格匹配當前路由
       isExactActive,
       // 導航至該連結的函數
       navigate
       } = useLink(props)

 	const isExternalLink = computed(
  	 () => typeof props.to === 'string' && props.to.startsWith('http')
 	)

 	return { isExternalLink, href, navigate, isActive }
	
	},
}

</script>

過度動效

顧名思義就是給路由的切換新增動畫效果

例子如下

<router-view v-slot="{ Component, route }">
  <transition name="fade">
    <component :is="Component" :key="route.path" />
  </transition>
</router-view>

我們用v-slot拿到了 component和route,然後通過動態元件的方式來顯示,在transition指定動畫效果

由於vue會自動複用看起來類似的元件,所有我們需要加上唯一 key


捲動行為

這個比較簡單就是一個 api

例子如下

const router = createRouter({
 history: createWebHashHistory(),
 routes: [...],
 scrollBehavior (to, from, savedPosition) {
   // return 期望捲動到哪個的位置,範例如下
 }
})

/*
返回值支援以下,也可以返回promise,支援延遲捲動

return {
     // 也可以這麼寫
     // el: document.getElementById('main'),
     // el: to.hash,			 定位錨點
     // behavior: 'smooth',	 捲動流暢
     
     el: '#main',
     top: -10,
}
*/

如果返回一個 falsy(非真值) 的值,或者是一個空物件,那麼不會發生捲動。

返回 savedPosition,在按下 後退/前進 按鈕時,就會像瀏覽器的原生表現那樣:


路由懶載入

vue-router支援動態匯入我們這樣寫,範例如下

const router = createRouter({
 routes: [
     { 
         path: '/users/:id', 
         component: () => import('./views/UserDetails.vue') 
     }
 ],
})

為了避免請求過多,我們有時候需要元件按組分塊,範例如下

// 在路由下設定webpackChunkName,vite支援webpack的這種寫法。同名的設定為一組

const UserDetails = () =>
 import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
 import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
 import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')




// 我們在vite.config.js 設定

export default defineConfig({
 build: {
   rollupOptions: {
     // https://rollupjs.org/guide/en/#outputmanualchunks
     output: {
       manualChunks: {
         'group-user': [				// group-user組
           './src/UserDetails.vue',
           './src/UserDashboard.vue',
           './src/UserProfileEdit.vue',
         ],
       },
     },
   },
 },
})


// 設定後將會在build後 才能生效!!!!


在router-link元件上封裝一層(自定義 RouterLink 元件),範例如下

<template>
 <a v-if="isExternalLink" v-bind="$attrs" :href="to" target="_blank">
     
   <slot />
 </a>
 <router-link
   v-else
   v-bind="$props"
   custom
   v-slot="{ isActive, href, navigate }">
     
   <a
     v-bind="$attrs"
     :href="href"
     @click="navigate"
     :class="isActive ? activeClass : inactiveClass">
       
     <slot />
   </a>
 </router-link>
</template>

<script>
import { RouterLink } from 'vue-router'

export default {
 name: 'AppLink',
 inheritAttrs: false,

 props: {
   // 如果使用 TypeScript,請新增 @ts-ignore
   ...RouterLink.props,
   inactiveClass: String,
 },

 computed: {
   isExternalLink() {
     return typeof this.to === 'string' && this.to.startsWith('http')
   },
 },
}
</script>

使用元件

<AppLink
   v-bind="$attrs"
   class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"
   active-class="border-indigo-500 text-gray-900 focus:border-indigo-700"
   inactive-class="text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300"
 >
   點我點我
</AppLink>

路由導航檢測

有時候我們需要在push後做點什麼,可以在 push 前面加上 await 範例如下

通過 push等方法進行路由導航,有時候未必會成功跳轉,比如已經在目標路由,或者路由返回了false,範例如下


// push返回的是一個promise,因此我們可以這樣寫
await $router.push('/user/list')
this.isMenuOpen = false;

// 判斷是否導航成功

var navigationResult = await $router.push('/user/list');
if (navigationResult) {
  // 導航被阻止
} else {
  // 導航成功 (包括重新導航的情況)
  this.isMenuOpen = false
}

// 其實通過navigationResult物件可以拿到詳細的錯誤資訊,這個用的比較少,這裡就不寫了

動態路由

對於已經執行的程式,我們想新增路由,就看這裡,動態路由主要通過兩個函數實現 router.addRoute()router.removeRoute()


var removeRoute = router.addRoute({path: '/user', name: 'user', component: xxx })
// router.replace(router.currentRoute.value.fullPath)	// 然後手動呼叫replace覆蓋當前路由


// 可以覆蓋替換,當名字一樣的情況下
removeRoute('填入路由的名字')	// 呼叫它的返回值可以刪掉路由

// 新增巢狀路由
router.addRoute('父路由的名字', {path: '/user', name: 'user', component: xxx })

// 檢視現有路由

router.hasRoute()	// 檢查路由是否存在
router.getRoutes()	// 獲取一個包含所有路由記錄的陣列

擴充套件

unplugin-vue-router

基於檔案的自動路由,可以使用vite提供的功能(vite-plugin-pages)

也可以通過官方提供的一個外掛 unplugin-vue-router

emmmm... 這個內容有點多,還是看官網咖...

GitHub unplugin-vue-router


持續學習

閱讀 vue-router@4 API

詳細見官網

拜了個拜~