1. 認識路由
1.1 路由概念
路由是什麼?
路由是一個網路工程裡面的術語。
路由(routing)就是通過互聯的網路把資訊從源地址傳輸到目的地址的活動 --- 維基百科
路由器提供了兩種機制:路由和轉送
路由是決定資料包從來源到目的地的路徑
轉送將輸入端的資料轉移到合適的輸出端
路由中一個非常重要的概念:路由表
路由表本質上就是一個對映表,決定了資料包的指向
1.2 後端路由階段
早期的網站開發整個HTML頁面是由伺服器來渲染的,伺服器直接生產渲染好對應的HTML頁面返回給客戶端進行展示。
但是一個網站這麼多頁面伺服器如何處理呢?一個頁面有自己對應的網址也就是URL。URL會傳送到伺服器,伺服器會通過正則對該URL進行匹配,並且最後交給一個Controller進行處理。Controller進行各種處理,最終生成HTML或者資料返回給前端,這就完成了一個IO操作。
上面的這種操作就是後端路由,當我們頁面中需要請求不同的路徑內容時交給伺服器來進行處理,伺服器渲染好整個頁面並且將頁面返回給客戶端。這種情況下渲染好的頁面不需要單獨載入任何的js和css,可以直接交給瀏覽器展示,這樣也有利於SEO的優化。
後端路由也有缺點,一種情況是整個頁面的模組由後端人員來編寫和維護的,另一種情況是前端開發人員如果要開發頁面,需要通過PHP和Java等語言來編寫頁面程式碼。而且通常情況下HTML程式碼和資料以及對應的邏輯會混在一起編寫和維護都是非常糟糕的事情。
1.3 前端路由階段
前後端分離階段
隨著Ajax的出現有了前後端分離的開發模式,後端只提供API來返回資料,前端通過Ajax獲取資料並且可以通過JavaScript將資料渲染到頁面中。這樣做最大的優點就是前後端責任的清晰,後端專注於資料上前端專注於互動和視覺化上。並且當移動端(iOS/Android)出現後,後端不需要進行任何處理依然使用之前的一套API即可,目前很多的網站依然採用這種模式開發。
單頁面富應用階段
其實SPA最主要的特點就是在前後端分離的基礎上加了一層前端路由,也就是前端來維護一套路由規則。前端路由的核心是什麼呢?即改變URL但是頁面不進行整體的重新整理。如何實現呢?
1.4 前端路由的規則
URL的hash
URL的hash也就是錨點#
,本質上是改變window.location
的 href 屬性。我們可以通過直接賦值location.hash
來改變 href但是頁面不發生重新整理。
location.href
"http://localhost:8080/#/"
location.hash='/'
"/"
location.href
"http://localhost:8080/#/"
location.hash='/foo'
"/foo"
location.href
"http://localhost:8080/#/foo"
HTML5的history模式
history介面是HTML5新增的,它有五種模式改變URL而不重新整理頁面。
history.pushState()
push進棧,可以返回
location.href
"http://localhost:8080/#/foo"
history.pushState({},'','/foo')
undefined
location.href
"http://localhost:8080/foo"
history.pushState({},'','/')
undefined
location.href
"http://localhost:8080/"
history.replaceState()
直接替換,不可以返回
location.href
"http://localhost:8080/"
history.replaceState({},'','/foo')
undefined
location.href
"http://localhost:8080/foo"
history.replaceState({},'','/foo/bar')
undefined
location.href
"http://localhost:8080/foo/bar"
history.go()
可以前進後退,即直接出棧入棧幾個歷史
location.href
"http://localhost:8080/#/"
location.hash='/home'
"/home"
location.hash='/about'
"/about"
history.go(-1)
undefined
location.href
"http://localhost:8080/#/home"
history.go(-1)
undefined
location.href
"http://localhost:8080/#/"
history.go(1)
undefined
location.href
"http://localhost:8080/#/home"
補充:上面只演示了三個方法,還有兩個方法如下:
history.back()
等價於history.go(-1)
history.forward()
等價於history.go(1)
這三個介面等同於瀏覽器介面的前進後退。
2. vue-router基本使用
2.1 認識vue-router
目前前端流行的三大框架,都有自己的路由實現
-
Angular的ngRouter
-
React的ReactRouter
-
Vue的vue-router
vue-router是Vue.js官方的路由外掛,它和vue.js是深度整合的,適合用於構建單頁面應用。我們可以訪問其官方網站對其進行學習
vue-router是基於路由和元件的,路由用於設定訪問路徑,將路徑和元件對映起來。在vue-router的單頁面應用中, 頁面的路徑的改變就是元件的切換。
2.2 安裝和使用vue-router
安裝vue-router
npm install vue-router --save
建立router例項
掛載到Vue例項中
建立路由元件
配置元件和路徑的對映關係
//...
import Home from '../components/home.vue'
import About from '../components/about.vue'
//...
const routes = [
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
},
]
//...
使用路由
<router-link>
:該標籤是一個vue-router中已經內建的元件, 它會被渲染成一個<a>
標籤。
<router-view>
:該標籤會根據當前的路徑動態渲染出不同的元件。
網頁的其他內容比如頂部的標題/導航或者底部的一些版權資訊等,會和<router-view>
處於同一個等級。
在路由切換時切換的是<router-view>
掛載的元件其他內容不會發生改變。
<!-- App.vue -->
<template>
<div id="app">
<h1>我是標題</h1>
<router-link to="/home">首頁</router-link>
<router-link to="/about">關於</router-link>
<router-view></router-view>
<h2>我是底部資訊</h2>
</div>
</template>
<script>
export default {
name: 'app',
components: {
}
}
</script>
<style>
</style>
2.3 vue-router使用細節
路由的預設路徑
我們這裡還有一個不太好的實現,預設情況下進入網站的首頁我們希望<router-view>
渲染首頁的內容,但是我們的實現中預設沒有顯示首頁元件必須讓使用者點選才可以。
如何可以讓路徑預設跳到到首頁, 並且<router-view>
渲染首頁元件呢?非常簡單,我們只需要配置多配置一個對映就可以了。
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
},
]
配置解析:我們在routes中又配置了一個對映,path配置的是根路徑/
,redirect是重定向,也就是我們將根路徑重定向到/home
的路徑下,這樣就可以得到我們想要的結果了。
HTML5的History模式
我們前面說過改變路徑的方式有兩種:URL的hash,HTML5的history。預設情況下路徑的改變使用的URL的hash,如果希望使用HTML5的history模式進行如下配置即可。
//3.建立router例項
const router = new VueRouter({
routes,
mode: 'history'
})
router-link補充
在前面的<router-link>
中,我們只是使用了一個屬性to
用於指定跳轉的路徑,
<router-link>
還有一些其他屬性:
tag
:tag可以指定之後渲染成什麼元件,比如下面的程式碼會被渲染成一個 <li>
元素而不是<a>
。
<router-link to="/home" tag="li"></router-link>
replace
:replace不會留下history記錄,所以指定replace的情況下,後退鍵返回不能返回到上一個頁面中。active-class
:當對應的路由匹配成功時自動給當前元素設定一個 router-link-active
的class,設定active-class可以修改預設的名稱。在進行高亮顯示的導航選單或者底部tabbar時會使用到該類,但是通常不會修改類的屬性,直接使用預設的router-link-active即可。
<router-link to="/home" active-class="active"></router-link>
<router-link to="/about" active-class="active"></router-link>
.router-link-active {
color:red;
}
// 注意:如果在router-link標籤中修改active-class的話,當router-link很多時一 // 個一個修改很麻煩。
// => 我們可以在建立路由例項的時候修改active-class的值
const router = new VueRouter({
routes,
mode: 'history',
linkActiveClass:'active'
})
路由程式碼跳轉
有時候頁面的跳轉可能需要執行對應的JavaScript程式碼, 這個時候就可以使用第二種跳轉方式了。
<template>
<div id="app">
<h1>我是標題</h1>
<button @click="linkToHome">首頁</button>
<button @click="linkToAbout">關於</button>
<router-view></router-view>
<h2>我是底部資訊</h2>
</div>
</template>
<script>
export default {
name: 'app',
methods: {
linkToHome() {
this.$router.push('/home');
//this.$router.replace('/home');
},
linkToAbout() {
this.$router.push('/about');
//this.$router.replace('/about');
}
}
}
</script>
<style>
</style>
動態路由
在某些情況下一個頁面的path路徑可能是不確定的,比如我們進入使用者介面時希望是如下的路徑:/user/aaaa或/user/bbbb,除了有前面的/user之外後面還跟上了使用者的ID
,這種path和Component的匹配關係我們稱之為動態路由(也是路由傳遞資料的一種方式)。
<!-- router/index.js -->
const routes = [
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
},
{
path: '/user/:user_id',
component: User
},
]
<!-- User.vue -->
<template>
<div>
<h2>我是使用者介面</h2>
<p>我是使用者 {{userId}} 的相關資訊!</p>
</div>
</template>
<script>
export default {
name: "User",
computed: {
userId() {
//$router是總的vue-router物件
//$route是當前處於活躍狀態的那個路由物件
return this.$route.params.user_id
}
}
}
</script>
<style scoped>
</style>
<!-- App.vue -->
<template>
<div id="app">
<h1>我是標題</h1>
<router-link to="/home">首頁</router-link>
<router-link to="/about">關於</router-link>
<router-link :to="'/user/' + user_id">使用者</router-link>
<router-view></router-view>
<h2>我是底部資訊</h2>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
user_id: '123'
}
}
}
</script>
<style>
</style>
2.4 路由的懶載入
理解
官方給出的解釋:當打包構建應用時Javascript 包會變得非常大影響頁面載入。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被訪問的時候才載入對應元件這樣就更加高效了。
官方在說什麼呢?首先我們知道路由中通常會定義很多不同的頁面,這個頁面最後被打包在哪裡呢?一般情況下是放在一個js檔案中。但是頁面這麼多放在一個js檔案中必然會造成這個頁面非常的大,如果我們一次性從伺服器請求下來這個頁面可能需要花費一定的時間,甚至使用者的電腦上還出現了短暫空白的情況。如何避免這種情況呢? 使用路由懶載入就可以了。
路由懶載入做了什麼?路由懶載入的主要作用就是將路由對應的元件打包成一個個的js程式碼塊,只有在這個路由被訪問到的時候才載入對應的元件。
懶載入的方式
方式一:結合Vue的非同步元件和Webpack的程式碼分析
const Home = resolve => {
require.ensure(['../components/Home.vue'], () => {
resolve(require('../components/Home.vue'))
})
};
方式二:AMD寫法
const About = resolve => require(['../components/About.vue'], resolve);
方式三:在ES6中我們可以有更加簡單的寫法來組織Vue非同步元件和Webpack的程式碼分割
const Home = () => import('../components/Home.vue')
路由懶載入效果
未使用路由懶載入
使用路由懶載入後,一個使用了懶載入的路由打包一個js檔案
const routes = [
{
path: '/home',
component: () => import('../components/home.vue')
},
{
path: '/about',
component: () => import('../components/about.vue')
},
{
path: '/user/:user_id',
component: () => import('../components/User.vue')
},
]
3. vue-router巢狀路由
3.1 理解
巢狀路由是一個很常見的功能,比如在home頁面中,我們希望通過/home/news
和/home/message
訪問一些內容。一個路徑對映一個元件, 訪問這兩個路徑也會分別渲染兩個元件。路徑和元件的關係如下
實現巢狀路由有兩個步驟:
- 建立對應的子元件,並且在路由對映中配置對應的子路由
- 在元件內部使用
標籤
3.2 巢狀路由實現
在components中定義兩個元件
<!-- Message.vue News.vue也一樣 -->
<template>
<div>
<ul>
<li>Message1</li>
<li>Message2</li>
<li>Message3</li>
</ul>
</div>
</template>
<script>
export default {
name: "Message",
}
</script>
<style scoped>
</style>
在router路由表中配置home的子路由對映關係
//注意:子路由的path前面不能加 "/",如果要加的話就必須寫全如:/home/news
{
path: '/home',
component: () => import('../components/home.vue'),
children: [
{
path: 'news',
component: () => import('../components/News.vue'),
},
{
path: 'message',
component: () => import('../components/Message.vue'),
},
]
},
在home元件中使用
<template>
<div>
<h2>我是home</h2>
<router-link to="/home/message">訊息</router-link>
<router-link to="/home/news">新聞</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "home"
}
</script>
<style scoped>
</style>
注意:巢狀路由也是可以配置預設路徑的, 配置方式如下
{
path: '/home',
component: () => import('../components/home.vue'),
children: [
{
path: '',
redirect: 'message'
},
{
path: 'news',
component: () => import('../components/News.vue'),
},
{
path: 'message',
component: () => import('../components/Message.vue'),
},
]
},
4. vue-router引數傳遞
4.1 引數傳遞的方式
傳遞引數主要有兩種型別:params
和query
params的型別
- 配置路由格式:
/router/:id
- 傳遞的方式:在path後面跟上對應的值
- 傳遞後形成的路徑:
/router/123
,/router/abc
query的型別
-
配置路由格式:
/router
(也就是普通配置) -
傳遞的方式:物件中使用
query
的 key 作為傳遞方式 -
傳遞後形成的路徑:
/router?id=123
,/router?id=abc
如何使用它們呢? 也有兩種方式
- 先設定好路由對映關係
{
path: '/about/:num',
component: () => import('../components/about.vue')
},
<router-link>
方式
- JavaScript程式碼的方式
4.2 獲取引數
獲取引數通過 $route
物件獲取的。在使用了 vue-router 的應用中路由物件會被注入每個元件中,賦值為 this.$route
,並且當路由切換時路由物件會被更新。
通過$route獲取傳遞的資訊如下:
<!-- about.vue -->
<template>
<div>
<h2>我是about</h2>
<p>params: {{$route.params}}</p>
<p>query: {{$route.query}}</p>
</div>
</template>
<script>
export default {
name: "about"
}
</script>
<style scoped>
</style>
$route和$router的區別
- $router為 VueRouter例項 ,想要導航到不同URL則使用
$router.push
方法 - $route為 當前router跳轉物件 ,裡面可以獲取name,path,query,params等
5. 導航守衛
5.1 為什麼使用導航守衛?
我們來考慮一個需求:在一個SPA應用中如何改變網頁的標題呢?網頁標題是通過<title>
來顯示的,但是SPA只有一個固定的HTML, 切換不同的頁面時, 標題並不會改變。但是我們可以通過JavaScript來修改<title>
的內容:window.document.title = '新的標題'
那麼Vue專案中在哪裡修改?什麼時候修改比較合適呢?
普通的修改方式:我們比較容易想到的修改標題的位置是每一個路由對應的元件.vue檔案中,通過mounted宣告周期函式執行對應的程式碼進行修改即可。但是當頁面比較多時, 這種方式不容易維護(因為需要在多個頁面執行類似的程式碼)。有沒有更好的辦法呢? 使用導航守衛即可.
什麼是導航守衛?
vue-router提供的導航守衛主要用來監聽監聽路由的進入和離開的。vue-router提供了beforeEach
和afterEach
的鉤子函式, 它們會在路由即將改變前和改變後觸發。
5.2 導航守衛使用
我們可以利用beforeEach來完成標題的修改,首先我們可以在鉤子當中定義一些標題,可以利用meta來定義,其次利用導航守衛修改我們的標題。
//router/
//...
const routes = [
{
path: '/',
redirect: 'home',
},
{
path: '/home',
component: () => import('../components/home.vue'),
meta: {
title: '首頁'
},
children: [
{
path: 'message',
component: () => import('../components/Message.vue'),
meta: {
title: '訊息'
},
},
]
},
{
path: '/about/:message',
component: () => import('../components/about.vue'),
meta: {
title: '關於'
}
},
]
//...
router.beforeEach((to, from, next) => {
router.beforeEach((to, from, next) => {
window.document.title = to.meta.title
next()
})
})
//...
導航鉤子的三個引數解析
- to:即將要進入的目標的路由物件
to.matched的用法:to.matched是一個陣列,能夠拿到所有父級的元件的路由物件。如果你直接訪問一個子路由,那麼有可能子路由上不會定義是否需要登陸,直接在父路由上定義就好了。這樣當你訪問的是子路由,通過matched就可以拿到父級上是否需要登陸才能訪問
- from:當前導航即將要離開的路由物件nikeyi
- next:呼叫該方法後才能進入下一個鉤子
next(false)
:中斷當前的導航,例項:如果瀏覽器的URL改變了(可能是使用者手動或者瀏覽器後退按鈕),那麼URL地址會重複到 from 路由對應的地址next('/xx')
或next({path: '/xx'})
:跳轉到一個不同的地址。當前的導航被中斷,然後進行一次新的導航。next(error)
:如果傳入 next 的引數是一個Error例項,則導航會被終止且該錯誤會被傳遞給router.onError()
註冊過的回撥。
注意:後置鉤子沒有next引數
注意
如果是後置鉤子也就是afterEach,不需要主動呼叫next()函式
上面我們使用的導航守衛被稱之為全域性守衛,此外還有其他型別的守衛
- 路由獨享的守衛
- beforeEnter
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
- 元件內的守衛
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
//beforeRouteEnter 守衛不能訪問 this,不過你可以通過傳一個回撥給 next來訪問元件例項。在導航
//被確認的時候執行回撥,並且把元件例項作為回撥方法的引數。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通過 `vm` 訪問元件例項
})
}
beforeRouteUpdate
(2.2 新增)
//注意 beforeRouteEnter 是支援給 next 傳遞迴調的唯一守衛。對於 beforeRouteUpdate 和
//beforeRouteLeave 來說,this 已經可用了,所以不支援傳遞迴調,因為沒有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
beforeRouteLeave
//這個離開守衛通常用來禁止使用者在還未儲存修改前突然離開。該導航可以通過 next(false) 來取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('你還沒有儲存,確定離開?')
if (answer) {
next()
} else {
next(false)
}
}
更多內容可以檢視官網進行學習
6. keep-alive
6.1 keep-alive理解
發現問題
keep-alive簡介
keep-alive 是 Vue 內建的一個元件,可以使被包含的元件 保留狀態 或 避免重新渲染。
它有兩個非常重要的屬性
- include - 字串或正則表達,只有匹配的元件會被快取
- exclude - 字串或正規表示式,任何匹配的元件都不會被快取
router-view 也是一個元件(VueRouter內建),如果直接被包在 keep-alive 裡面,所有路徑匹配到的檢視元件都會被快取
6.2 使用keep-alive的案例
下面我們來解決我們剛才發現的問題
<!-- App.vue -->
<template>
<div id="app">
<h1>我是標題</h1>
<router-link to="/home">首頁</router-link>
<button @click="toAbout">關於</button>
<!-- 我們可以排除about元件,則每次跳轉路由時about還是會被銷燬建立 -->
<!-- 注意:
1.如果想排除多個直接在後面加逗號繼續寫
2.如果要使用正則,則exclude必須v-bind
-->
<keep-alive exclude="about">
<!-- 所有匹配到的元件都會被快取 -->
<router-view></router-view>
</keep-alive>
<h2>我是底部資訊</h2>
</div>
</template>
//...
<!-- home -->
<template>
<div>
<h2>我是home</h2>
<router-link to="/home/message">訊息</router-link>
<router-link to="/home/news">新聞</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "home",
data() {
return {
//不在路由表中配置子路由的重定向,而是來到這裡動態決定
path: "/home/message"
};
},
//activated,deactivated兩個函式,只有在該元件被保持了狀
//態(即使用了keep-alive快取時)才是有效的
activated() {
console.log("activated");
//重新為path賦值
if(this.$route.path != this.path){
this.$router.push(this.path);
}
},
deactivated() {
console.log("deactivated");
},
//元件內導航守衛,導航離開該元件的對應路由時呼叫
//記錄當前的path
beforeRouteLeave(to, from, next) {
this.path = this.$route.path;
next();
},
};
</script>
<style scoped></style>
7. 案例 - TabBar實現
如果在一個app應用下方有一個單獨的TabBar元件,你如何封裝?
封裝的元件
<!-- component/TabBar.vue -->
<template>
<div id="tab-bar">
<slot></slot>
</div>
</template>
<script>
export default {
name: "TabBar",
};
</script>
<style scoped>
<!-- 簡單的去掉margin的樣式-->
@import "../../assets/css/base.css";
#tab-bar {
display: flex;
background-color: #f6f6f6;
position: fixed;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 -3px 1px rgba(100, 100, 100, 0.1);
}
</style>
<!-- component/TabBarItem.vue -->
<template>
<div class="tab-bar-item" :style="activeStyle" @click="handleClick">
<slot name="item-icon"></slot>
<slot name="item-text"></slot>
</div>
</template>
<script>
export default {
name: "TabBarItem",
props: {
path: {
type: String,
default: () => ''
},
activeColor: {
type: String,
default: () => 'sandybrown'
}
},
data(){
return {
// isActive: false
}
},
computed: {
isActive() {
return this.$route.path == this.path
},
activeStyle() {
return this.isActive ? {color: this.activeColor} : {}
}
},
methods: {
handleClick() {
this.isActive = true
if(this.$route.path != this.path) {
this.$router.push(this.path)
}
}
}
};
</script>
<style scoped>
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
border-right: 1px solid rgba(100, 100, 100, 0.1);
}
.item-text {
margin-top: 0;
font-size: 14px;
}
</style>
使用封裝的元件
建立三個簡單的頁面home/message/profile,並配置路由
const routes = [
{
path:'/',
redirect:'home'
},
{
path: '/home',
component: () => import('@/views/Home')
},
{
path: '/message',
component: () => import('@/views/Message')
},
{
path: '/profile',
component: () => import('@/views/Profile')
}
]
App.vue中使用
<template>
<div id="app">
<router-view></router-view>
<tab-bar>
<tab-bar-item
v-for="(item, index) in tabBar"
:key="item.text"
:path="item.path"
:activeColor="'red'"
>
<i :class="item.icon" slot="item-icon"></i>
<div class="item-text" slot="item-text">{{ item.text }}</div>
</tab-bar-item>
</tab-bar>
</div>
</template>
<script>
import TabBar from "./components/tab_bar/TabBar";
import TabBarItem from "./components/tab_bar/TabBarItem";
export default {
name: "app",
components: {
TabBar,
TabBarItem,
},
data() {
return {
tabBar: [
{ icon: "el-icon-star-on", text: "主頁", path: "/home" },
{ icon: "el-icon-phone", text: "訊息", path: "/message" },
{ icon: "el-icon-user-solid", text: "我的", path: "/profile" },
],
};
},
methods: {},
};
</script>
<style scoped></style>