vue 快速入門 系列 —— vue-router

其他章節請看:

vue 快速入門 系列

Vue Router

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

什麼是路由

路由就是根據不同的 url(網址) 返回不同的頁面(數據)。如果這個工作在後端做(或者說由後端程序員做),就是後端路由;在前端做就是前端路由。

絕大多數的網站是後端路由。流程就像這樣:

  1. 瀏覽器輸入 url,回車
  2. 後端服務器接收到請求
  3. 將請求(url)交給對應的處理邏輯 —— 有一個專門的正則配置列表來分發
  4. 各種操作完成後,最後將頁面(數據)返回給瀏覽器

平時總說的 SPA(單頁面應用)就是前後端分離的基礎上,再加一層前端路由。

Vue Router 的核心

無論是後端路由還是前端路由,解決的核心問題應該都是一樣的。

後端路由常見的 2 種場景:

  • 瀏覽器輸入 url,後端返回相應的頁面
  • 在網頁中點擊 a 標籤,跳轉到 a 標籤指向的頁面

Vue Router 與後端路由對應的場景:

  • 根據 url 返回相應的組件
    • 核心1:提供一個 map 的數據結構解決此事
  • 在網頁中點擊標籤,跳轉到相應的頁面
    • 核心2:提供內置標籤 <router-link>,默認是a標籤
  • 由於是做單頁面應用,也就是只有一個頁面,切換頁面也就是切換組件,那麼組件在哪裡顯示,這是前端路由需要解決的
    • 核心3:提供<router-view>來解決此事

環境準備

通過 vue-cli 創建項目

// 項目預設 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
$ vue create test-vue-router

啟動服務:

$ test-vue-router> npm run serve

頁面如果成功顯示,說明環境準備完成,下面的例子都是基於這個項目進行。

Tip:保存代碼如果遇到 eslint 的干擾(例如下面信息),可以通過配置 lintOnSave 生產構建時禁用 eslint-loader

test-vue-router\src\views\About.vue
   7:1   error  More than 1 blank line not allowed         no-multiple-empty-lines
  12:10  error  Missing space before function parentheses  space-before-function-paren
  16:14  error  Missing space before function parentheses  space-before-function-paren
  18:38  error  Extra semicolon                            semi
  20:7   error  Block must not be padded by blank lines    padded-blocks

✖ 5 problems (5 errors, 0 warnings)
  5 errors and 0 warnings potentially fixable with the `--fix` option.
// vue.config.js
module.exports = {
  lintOnSave: process.env.NODE_ENV !== 'production'
}

demo1 – 起步 – 歡迎來到首頁

需求:訪問 //localhost:8080/,頁面顯示 歡迎來到首頁

步驟如下:

首先我們查看 router 的核心文件:

// src/router/index.js
// vue-cli自動生成。無需修改
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)
// 核心1 - map
const routes = [
  {
    path: '/',             // {1}
    name: 'Home',
    component: Home       // {2}
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
// 實例化路由器
const router = new VueRouter({
  routes
})

export default router

接着修改 App.vue 和 Home.vue 即可:

// src/views/Home.vue
<template>
    <p>歡迎來到首頁</p>
</template>
// src/App.vue
<template>
  <div id="app">
    <!-- 核心3 - <router-view> 渲染路徑匹配到的視圖組件-->
    <router-view/>                      
  </div>
</template>

流程是:

  1. 瀏覽器輸入 //localhost:8080/
  2. 在 map 中匹配(行{1})
  3. 將 map 中匹配到的組件(行{2})渲染到 <router-view/>

demo2 – 註冊

需求:在首頁中增加一個註冊按鈕,點擊註冊,跳轉到註冊頁。在註冊頁點擊註冊,在回到首頁。(建議在demo1基礎上做)

以下是核心代碼:

// views/Home.vue
<template>
  <div>
    <p>
      <!-- 核心2 - to 表示目標路由的鏈接 -->
      <router-link to="/register">註冊</router-link>
    </p>
    <p>歡迎來到首頁</p>
  </div>
</template>
// router/index.js
// 新增
import Register from '../views/Register'

const routes = [                    
  {
    path: '/',
    component: Home                  
  },
  // 新增
  {
    path: '/register',
    component: Register          
  }
]
// views/Register.vue
<template>
  <section>
    <p>用戶名:<input type="text" /></p>
    <p>密碼:<input type="password" /></p>
    <p><router-link to="/">註冊</router-link></p>
  </section>
</template>

動態路由匹配

$route.params

我們經常需要把某種模式匹配到的所有路由,全都映射到同一個組件。可以這樣:

// router/index
routes: [
    // 動態路徑參數 以冒號開頭
    { path: '/user/:id', component: User }
  ]

/user/foo/user/bar 都將映射到相同的路由

一個「路徑參數」使用冒號 : 標記。當匹配到一個路由時,參數值會被設置到 this.$route.params,可以在每個組件內使用。

需求:創建一個顯示文章的組件,根據不同的 id 顯示對應的文章,url 是 /article/:id

// router/index.js
// 新增
import Article from '../views/Article'

const routes = [                    
  // 新增
  {
    path: '/article/:id',
    component: Article          
  }
]
// views/Article.vue
<template>
    <div>
        <p>文章列表頁</p>
        <p>這是對應 <em>{{ $route.params.id }}</em> 的文章</p>
    </div>
</template>

測試:

瀏覽器輸入://localhost:8080/article/2

頁面輸出:
文章列表頁

這是對應 2 的文章

Tip$route(路由對象) 對象還提供了其它有用的信息:

  • $route.path,字符串,對應當前路由的路徑,總是解析為絕對路徑,如 “/foo/bar”
  • $route.query,一個 key/value 對象,表示 URL 查詢參數。例如,對於路徑 /foo?user=1,則有 $route.query.user == 1,如果沒有查詢參數,則是個空對象。
  • $route.hash,當前路由的 hash 值 (帶 #) ,如果沒有 hash 值,則為空字符串
  • $route.fullPath,完成解析後的 URL,包含查詢參數和 hash 的完整路徑。
  • $route.matched
  • $route.name,當前路由的名稱,如果有的話
  • $route.redirectedFrom,如果存在重定向,即為重定向來源的路由的名字

匹配優先級

有時候,同一個路徑可以匹配多個路由,此時,匹配的優先級就按照路由的定義順序:路由定義得越早,優先級就越高。

嵌套路由

嵌套路由也或叫子路由。即一個組件內可以有自己的<router-view>

常見的的一個場景是,有一個組件 Article,裏面有個坑位,這個坑位有時(根據url的變化)需要顯示 ArticleComponentA 組件,有時顯示 ArticleComponentB 組件。

index.js:

// 新增
import ArticleComponentA from '../views/ArticleComponentA'
import ArticleComponentB from '../views/ArticleComponentB'

const routes = [                    
  // 新增
  {
    path: '/article/:id',
    component: Article,
    children: [
      {
        // 當 /user/:id/ca 匹配成功,
        // ArticleComponentA 會被渲染在 User 的 <router-view> 中
        path: 'ca',
        component: ArticleComponentA
      },
      {
        // 當 /user/:id/cb 匹配成功
        path: 'cb',
        component: ArticleComponentB
      }
    ]       
  }
]
// views/ArticleComponentA.vue
<template>
  <div style="border: 1px solid">
    <p>我是A</p>
    <p>
      這是對應 <em>{{ $route.params.id }}</em> 的文章
    </p>
  </div>
</template>
// views/ArticleComponentA.vue
<template>
  <div style="border: 1px solid">
    <p>我是B</p>
    <p>
      這是對應 <em>{{ $route.params.id }}</em> 的文章
    </p>
  </div>
</template>
// views/Article.vue
<template>
  <div>
    <p>文章列表頁</p>
    <!-- 子路由 -->
    <router-view></router-view>
  </div>
</template>

測試:

瀏覽器輸入://localhost:8080/article/1/cb

頁面顯示:
文章列表頁

我是B

這是對應 1 的文章

編程式的導航

除了使用 <router-link> 創建 a 標籤來定義導航鏈接,我們還可以藉助 router 的實例方法,通過編寫代碼來實現。

需求:編寫支付組件,顯示支付成功,三秒後自動跳轉到首頁

index.js:

// 新增
import Pay from '../views/Pay'

const routes = [                    
  // 新增
  {
    path: '/pay',
    component: Pay          
  },
]

Pay.vue:

<template>
    <div>
        <p>支付成功,三秒後自動跳轉到首頁</p>
    </div>
</template>

<script>
export default {
    created: function(){
        setTimeout(()=>{
            // 編程式導航
            this.$router.push('/')
        }, 3000)
    }
}
</script>

測試:

瀏覽器輸入: //localhost:8080/#/pay

頁面顯示「支付成功,三秒後自動跳轉到首頁」,過三秒,頁面跳轉到首頁。

命名路由

有時候,通過一個名稱來標識一個路由顯得更方便一些,特別是在鏈接一個路由,或者是執行一些跳轉的時候。你可以在創建 Router 實例的時候,在 routes 配置中給某個路由設置名稱。

需求:給首頁路由起一個名字,新建一個組件(NamedRouter),組件中可以通過命名路由,使用鏈接(<router-link></router-link>)或方法跳轉到首頁

// views/NamedRouter.vue
<template>
  <div>
    <p>
      <!-- 通過命名路由跳轉 -->
      <router-link :to="{ name: 'home-page' }">鏈接跳轉</router-link>
    </p>
    <p>
      <button @click="handleClick">編程式跳轉</button>
    </p>
  </div>
</template>

<script>
export default {
    methods: {
        handleClick: function(){
            this.$router.push({ name: 'home-page'})
        }
    }
}
</script>

index.js:

// 新增
import NamedRouter from '../views/NamedRouter'

const routes = [  
  // 修改
  {
    path: '/',
    // 命名路由
    name: 'home-page',
    component: Home
  },
  // 新增
  // 瀏覽器輸入://localhost:8080/namedRouter
  {
    path: '/namedRouter',
    component: NamedRouter
  },
]

自行測試即可

命名視圖

有時候想同時 (同級) 展示多個視圖,而不是嵌套展示,例如創建一個布局,有 sidebar (側導航) 和 main (主內容) 兩個視圖,這個時候命名視圖就派上用場了。你可以在界面中擁有多個單獨命名的視圖,而不是只有一個單獨的出口。如果 router-view 沒有設置名字,那麼默認為 default。

用法1-命名視圖

直接上代碼:

// src/App.vue
<template>
  <div id="app">
    <!-- 定義兩個視圖(sidebar 和 main) -->
    <router-view class="sidebar" name='sidebar'></router-view>
    <router-view class="main" name="main"></router-view>
  </div>
</template>
// views/TestMain.vue
<template>
  <p>我是 TestMain</p>
</template>
// views/TestSidebar.vue
<template>
  <p>我是 TestSidebar</p>
</template>
// router/index.js
// 新增
import TestSidebar from '../views/TestSidebar'
import TestMain from '../views/TestMain'

const routes = [     
  // 修改               
  {
    path: '/',
    components: {
      main: TestMain,
      sidebar: TestSidebar
    },
    name: 'home-page'                  
  },
  ...
]

測試:

瀏覽器訪問://localhost:8080/

頁面顯示:
我是 TestSidebar

我是 TestMain

用法2-嵌套命名視圖

需求:某組件有兩個視圖,組件A,組件B分別放在這兩個視圖中

直接上代碼:

// App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>
// views/TestHome.vue
<template>
  <div class="test-home">
    <!-- 定義兩個視圖(sidebar 和 main) -->
    <router-view class="sidebar" name="sidebar"></router-view>
    <router-view class="main" name="main"></router-view>
  </div>
</template>
// router/index.js
// 新增
import TestSidebar from '../views/TestSidebar'
import TestMain from '../views/TestMain'
import TestHome from '../views/TestHome'

const routes = [      
  // 新增              
  {
    path: '/',
    component: TestHome,
    children: [
      {
        // path 為空,則能匹配 /
        // 如果 path 不為空(xx1),則只能匹配 /xx1
        path: '',
        components: {
          main: TestMain,
          sidebar: TestSidebar
        }
        
      }
    ],
    // 命名路由
    name: 'home-page'                  
  },
  ...
]

TestMain.vue 和 TestSidebar.vue 不變。

自行測試即可。

重定向和別名

重定向

需求:訪問支付(/pay),重定向到主頁(/)。

const routes = [ 
  { 
    path: '/pay', 
    redirect: '/' 
  }
]

測試:

輸入://localhost:8080/#/pay
變成://localhost:8080/#/

別名

「重定向」的意思是,當用戶訪問 /a 時,URL 將會被替換成 /b,然後匹配路由為 /b,那麼「別名」又是什麼呢?

/a 的別名是 /b,意味着,當用戶訪問 /b 時,URL 會保持為 /b,但是路由匹配則為 /a,就像用戶訪問 /a 一樣。

「別名」的功能讓你可以自由地將 UI 結構映射到任意的 URL,而不是受限於配置的嵌套路由結構。

需求:新增組件(Apple),裏面有個子組件(AppleComponentA),給子組件的路由起一個別名,方便訪問。

// views/Apple.vue
<template>
  <div>
    <p>蘋果</p>
    <router-view></router-view>
  </div>
</template>
// views/AppleComponentA.vue
<template>
  <p>我是嘎嘎果</p>
</template>
// router/index.js
// 新增
import Apple from '../views/Apple'
import AppleComponentA from '../views/AppleComponentA'

const routes = [                    
  // 新增
  {
    path: '/apple',
    component: Apple,
    children: [
      {
        path: 'ca/x1/x2',
        // 別名
        alias: '/cxx',
        component: AppleComponentA
      }
    ]
  }
]

運行:

瀏覽器輸入: 
//localhost:8080/apple/ca/x1/x2
或
//localhost:8080/cxx

頁面輸出:
蘋果

我是嘎嘎果

路由組件傳參

在組件中使用 $route 會使之與其對應路由形成高度耦合,從而使組件只能在某些特定的 URL 上使用,限制了其靈活性。

使用 props 將組件和路由解耦

需求:定義一個組件,通過 props 接收路由參數

// views/RouterToParams.vue
<template>
  <p>路由傳參 id={{ id }}</p>
</template>
<script>

export default {
    props:['id']
}
</script>
// router/index.js
// 新增
import RouterToParams from '../views/RouterToParams'

const routes = [  
  // 新增
  {
    path: '/routerToParams/:id',
    component: RouterToParams,
    props: true
  },   
]

運行:

瀏覽器輸入: //localhost:8080/routerToParams/2

頁面輸出:
路由傳參 id=2

對象模式

如果 props 是一個對象,它會被按原樣設置為組件屬性。當 props 是靜態的時候有用。

改為對象模式,直接上代碼:

// router/index.js
const routes = [
  {
    path: '/routerToParams/:id',
    component: RouterToParams,
    // props: true
    props: { id2: 10 }
  },
]
// views/RouterToParams.vue
<template>
  <div>
    <p>路由傳參 id={{ id }}</p>
    <p>路由傳參 id2={{ id2 }}</p>
  </div>
</template>
<script>

export default {
    props:['id2', 'id']
}
</script>

測試:

瀏覽器輸入://localhost:8080/routerToParams/2
頁面輸出:
路由傳參 id=

路由傳參 id2=10

函數模式

你可以創建一個函數返回 props。這樣你便可以將參數轉換成另一種類型,將靜態值與基於路由的值結合等等。

改為函數模式,直接上代碼:

// router/index.js
const routes = [
  {
    path: '/routerToParams/:id',
    component: RouterToParams,
    props: route => ({ id3: route.params.id })
  },
]
// views/RouterToParams.vue
<template>
  <div>
    <p>路由傳參 id3={{ id3 }}</p>
  </div>
</template>
<script>

export default {
    props:['id3']
}
</script>

測試:

瀏覽器輸入://localhost:8080/#/routerToParams/2

頁面顯示:
路由傳參 id3=2

HTML5 History 模式

vue-router 默認 hash 模式,如果不想要很醜的 hash,我們可以用路由的 history 模式。

改為 history 模式:

// router/index.js
const router = new VueRouter({
  mode: 'history',
  routes
})

Tip:history 模式還需要一些配置

導航守衛

「導航」表示路由正在發生改變。

正如其名,vue-router 提供的導航守衛主要用來通過跳轉或取消的方式守衛導航。有多種機會植入路由導航過程中:全局的, 單個路由獨享的, 或者組件級的。

參數或查詢的改變並不會觸發進入/離開的導航守衛。你可以通過觀察 $route 對象來應對這些變化,或使用 beforeRouteUpdate 的組件內守衛。

全局前置守衛

需求:使用 router.beforeEach 註冊全局前置守衛,如果訪問的是登錄(/login)則進入註冊頁(/register)

// router/index.js
import Register from '../views/Register'

const routes = [  
  {
    path: '/register',
    component: Register
  }
]

// 導航
router.beforeEach((to, from, next) => {
  if (to.path === '/login') next({ path: '/register' })
  // next: Function: 一定要調用該方法來 resolve 這個鉤子
  else next()
})

測試:

瀏覽器輸入: //localhost:8080/login 變為 //localhost:8080/register

其他導航守衛請參考官網:

  • 全局後置守衛
  • 路由獨享的守衛
  • 組件內的守衛
  • 全局解析守衛

完整的導航解析流程

  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 的回調函數,創建好的組件實例會作為回調函數的參數傳入。

路由元信息

Tip:直接根據官網例子來說一下

定義路由的時候可以配置 meta 字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

那麼如何訪問這個 meta 字段呢?

首先,我們稱呼 routes 配置中的每個路由對象為 路由記錄。路由記錄可以是嵌套的,因此,當一個路由匹配成功後,他可能匹配多個路由記錄

例如,根據上面的路由配置,/foo/bar 這個 URL 將會匹配父路由記錄以及子路由記錄。

一個路由匹配到的所有路由記錄會暴露為 $route 對象 (還有在導航守衛中的路由對象) 的 $route.matched 數組。因此,我們需要遍歷 $route.matched 來檢查路由記錄中的 meta 字段。

下面例子展示在全局導航守衛中檢查元字段:

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 確保一定要調用 next()
  }
})

如果路由記錄中的 meta.requiresAuth 為 true,則需要驗證

元數據,用於描述數據的數據。

過渡動效

<router-view> 是基本的動態組件,所以我們可以用 <transition> 組件給它添加一些過渡效果。
Transition 的所有功能(vue 官網中的「進入/離開 & 列表過渡」)在這裡同樣適用。

請看示意代碼:

<template>
  <div id="app">
    <p>
      <router-link to='/login'>登錄</router-link> |
      <router-link to='/'>首頁</router-link>
    </p>
    <transition name="fade">
      <router-view/>
    </transition>
  </div>
</template>
<style >
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>

點擊登錄首頁就能看到動畫。

Tip:上面的用法會給所有路由設置一樣的過渡效果,還有單個路由的過渡,以及基於路由的動態過渡。

數據獲取

有時候,進入某個路由後,需要從服務器獲取數據。例如,在渲染用戶信息時,你需要從服務器獲取用戶的數據。我們可以通過兩種方式來實現:

  • 導航完成之後獲取:先完成導航,然後在接下來的組件生命周期鉤子中獲取數據。在數據獲取期間顯示「加載中」之類的指示。

  • 導航完成之前獲取:導航完成前,在路由進入的守衛中獲取數據,在數據獲取成功後執行導航。

從技術角度講,兩種方式都不錯 —— 就看你想要的用戶體驗是哪種。

Tip:更具體的信息可以參考官網

滾動行為

使用前端路由,當切換到新路由時,想要頁面滾到頂部,或者是保持原先的滾動位置,就像重新加載頁面那樣。 vue-router 能做到,而且更好,它讓你可以自定義路由切換時頁面如何滾動。

: 這個功能只在支持 history.pushState 的瀏覽器中可用。

當創建一個 Router 實例,你可以提供一個 scrollBehavior 方法:

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // return 期望滾動到哪個的位置
  }
})

Tip:更具體的信息可以參考官網

其他章節請看:

vue 快速入門 系列

Tags: