其他章節請看:
Vue Router
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度整合,讓構建單頁面應用變得易如反掌。
什麼是路由
路由就是根據不同的 url(網址) 返回不同的頁面(資料)。如果這個工作在後端做(或者說由後端程式設計師做),就是後端路由;在前端做就是前端路由。
絕大多數的網站是後端路由。流程就像這樣:
- 瀏覽器輸入 url,回車
- 後端伺服器接收到請求
- 將請求(url)交給對應的處理邏輯 —— 有一個專門的正則配置列表來分發
- 各種操作完成後,最後將頁面(資料)返回給瀏覽器
平時總說的 SPA(單頁面應用)就是前後端分離的基礎上,再加一層前端路由。
Vue Router 的核心
無論是後端路由還是前端路由,解決的核心問題應該都是一樣的。
後端路由常見的 2 種場景:
- 瀏覽器輸入 url,後端返回相應的頁面
- 在網頁中點選 a 標籤,跳轉到 a 標籤指向的頁面
Vue Router 與後端路由對應的場景:
- 根據 url 返回相應的元件
- 核心1:提供一個 map 的資料結構解決此事
- 在網頁中點選標籤,跳轉到相應的頁面
- 核心2:提供內建標籤
<router-link>
,預設是a標籤
- 核心2:提供內建標籤
- 由於是做單頁面應用,也就是隻有一個頁面,切換頁面也就是切換元件,那麼元件在哪裡顯示,這是前端路由需要解決的
- 核心3:提供
<router-view>
來解決此事
- 核心3:提供
環境準備
通過 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 - 起步 - 歡迎來到首頁
需求:訪問 http://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>
流程是:
- 瀏覽器輸入
http://localhost:8080/
- 在 map 中匹配(行{1})
- 將 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>
測試:
瀏覽器輸入:http://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>
測試:
瀏覽器輸入:http://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>
測試:
瀏覽器輸入: http://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
},
// 新增
// 瀏覽器輸入:http://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'
},
...
]
測試:
瀏覽器訪問:http://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: '/'
}
]
測試:
輸入:http://localhost:8080/#/pay
變成:http://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
}
]
}
]
執行:
瀏覽器輸入:
http://localhost:8080/apple/ca/x1/x2
或
http://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
},
]
執行:
瀏覽器輸入: http://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>
測試:
瀏覽器輸入:http://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>
測試:
瀏覽器輸入:http://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()
})
測試:
瀏覽器輸入: http://localhost:8080/login 變為 http://localhost:8080/register
其他導航守衛請參考官網:
- 全域性後置守衛
- 路由獨享的守衛
- 元件內的守衛
- 全域性解析守衛
完整的導航解析流程
- 導航被觸發。
- 在失活的元件裡呼叫 beforeRouteLeave 守衛。
- 呼叫全域性的 beforeEach 守衛。
- 在重用的元件裡呼叫 beforeRouteUpdate 守衛 (2.2+)。
- 在路由配置裡呼叫 beforeEnter。
- 解析非同步路由元件。
- 在被啟用的元件裡呼叫 beforeRouteEnter。
- 呼叫全域性的 beforeResolve 守衛 (2.5+)。
- 導航被確認。
- 呼叫全域性的 afterEach 鉤子。
- 觸發 DOM 更新。
- 呼叫 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:更具體的資訊可以參考官網
其他章節請看: