前端路由 Vue-Router 介紹
什么是路由?
路由(routing)就是通過互聯的網絡把信息從源地址傳輸到目的地址的活動 — 維基百科
路由器提供了兩種機制, 路由和轉發
- 路由是決定數據包從來源到目的地的路徑
- 轉發將輸入端的數據轉移到合適的輸出端
網站發展的幾個階段
后端路由階段
什么是后端路由
早期的網站開發, 整個 HTML 頁面都是是由服務器來渲染的, 服務器直接生產渲染好對應的 HTML 頁面, 返回給客戶端進行展示
但是, 服務器如何處理一個網站的諸多頁面呢?
首先, 一個頁面會有自己對應的網址, 也就是 URL, 客戶端發生請求時, URL 會發送到服務器, 服務器通過正則表達式對該 URL 進行匹配且最后交給 Controller 進行處理, Controller 進行各種處理后, 最終生成 HTML 或者數據, 返回給前端, 這就完成了一個IO操作, 這種操作, 就是 后端路由
后端路由的優點
當頁面中需要請求不同的路徑內容時, 交給服務器來進行處理, 服務器渲染好整個頁面, 并且將頁面返回給客戶端, 這種情況下渲染好的頁面, 不需要單獨加載任何的 JavaScript 和 CSS, 可以直接交給瀏覽器展示, 這樣也有利于 SEO 的優化
后端路由的缺點
- 整個頁面的模塊都要由后端人員來編寫和維護, 工作量太大
- 前端開發人員如果要開發頁面, 需要通過 PHP 和 Java 等語言來編寫頁面代碼, 增加了額外的學習成本
- HTML 代碼和數據以及對應的邏輯混在一起, 不利于編寫和維護
前端路由階段
前端路由的核心: 改變URL, 但是頁面不進行整體的刷新
前后端分離階段
隨著 Ajax 的出現, 有了前后端分離的開發模式: 后端只提供 API 來返回數據, 前端通過 Ajax 獲取數據, 并且可以通過 JavaScript 將數據渲染到頁面中
優點:
- 前后端責任變得很清晰, 后端專注于數據上, 前端專注于交互和可視化上
- 當移動端(IOS / Android)出現后, 后端不需要進行任何處理, 依然使用之前的一套API即可
單頁面富應用階段
單頁面富應用, 即單頁Web應用(single page web application, SPA), 就是只有一張 Web 頁面的應用, 是加載單個 HTML 頁面并在用戶與應用程序交互時動態更新該頁面的 Web 應用程序
簡單理解: 就是在前后端分離的基礎上加了一層前端路由
SPA 的特點
- 速度: 更好的用戶體驗, 讓用戶在 web app 感受 native app 的速度和流暢
- MVVM: 經典 MVVM 開發模式, 前后端各負其責
- ajax: 重前端, 業務邏輯全部在本地操作, 數據都需要通過 Ajax 同步、提交
- 路由: 在 URL 中采用 # 號來作為當前視圖的地址, 改變 # 號后的參數, 頁面并不會重載
SPA 的優點
良好的交互體驗:
- 用戶不需要重新刷新頁面, 獲取數據也是通過 Ajax 異步獲取, 頁面顯示流暢
良好的前后端工作分離模式:
- 單頁 Web 應用可以和 RESTful 規約一起使用, 通過 REST API 提供接口數據, 并使用 Ajax 異步獲取
- 這樣有助于分離客戶端和服務器端工作, 更進一步, 可以在客戶端也可以分解為靜態頁面和頁面交互兩個部分
減輕服務器壓力:
- 服務器只用出數據就可以, 不用管展示邏輯和頁面合成, 吞吐能力會提高幾倍
共用一套后端程序代碼:
- 不用修改后端程序代碼就可以同時用于 Web 界面、手機、平板等多種客戶端
SPA 的缺點
首屏渲染等待時長
- 必須等待加載完畢, 才能渲染出首屏
seo不友好:
- 爬蟲只能拿到一個 div, 認為頁面是空的, 不利于 seo
初次加載耗時多:
- 為實現單頁Web應用功能及顯示效果, 需要在加載頁面的時候將 JavaScript、CSS 統一加載, 部分頁面可以在需要的時候加載
- 所以必須對 JavaScript 及 CSS 代碼進行合并壓縮處理, 如果使用第三方庫, 建議使用一些大公司的 CDN, 因此帶寬的消耗是必然的
改變 URL, 頁面不刷新
URL 的 hash
URL 的 hash 也就是錨點(#), 本質上是改變 window.location 的 href 屬性, 可以通過直接賦值 location.hash 來改變 href, 但是頁面不發生刷新
HTML5 的 history 模式
history 接口是 HTML5 新增的, 它有五種模式改變 URL 而不刷新頁面 (具體有點像瀏覽器的前進和后退)
-
history.pushState(Object, "title", "url")
- 相當于入棧的操作(出入棧相當于往一個杯子里加東西, 只有一個出入口, 后進的會在先進的上面), 遵循后進先出的規則, 會保存歷史記錄, 可以返回
image
- 相當于入棧的操作(出入棧相當于往一個杯子里加東西, 只有一個出入口, 后進的會在先進的上面), 遵循后進先出的規則, 會保存歷史記錄, 可以返回
-
history.replaceState(Objectj, "title", "url")
- 同 pushState()
- 區別是: 它不是棧結構, 所以不保存歷史記錄, 不能返回
-
history.go(Number)
- 功能等價于 history.back(), 但是可以通過參數來進行具體的跳轉
- Number 負數: 表示出棧幾次
- Number 正數: 表示把之前出棧掉的 Number 個數據重新 push 進去
image
-
history.forward()
- 可以前進到下一個記錄的地址
- 等價于history.go(1)
-
history.back()
- 相當于出棧的操作, 遵循后進先出的規則, 可以返回上一個記錄的地址
- 等價于history.go(-1)
Vue-Router 基本使用
目前前端流行的三大框架, 都有自己的路由實現:
- Angular: ngRouter
- React: ReactRouter
- Vue: Vue-Router
Vue-Router 是 Vue.js 官方的路由插件, 它和 Vue.js 是深度集成的, 適合用于構建單頁面富應用程序
Vue-Router 是基于路由和組件的, 路由用于設定訪問路徑, 將路徑和組件映射起來, 在 Vue-Router 的單頁面應用中, 頁面路徑的改變就是組件的切換
安裝 Vue-Router
安裝
npm install vue-router --save
在模塊化工程中使用它(因為是一個插件, 所以可以通過Vue.use()
來安裝路由功能)
在 src 目錄下創建 router 文件夾, 并在 router 文件夾中創建 index.js, 并在 index.js 中進行如下配置 (第一步、第二步均在此文件中配置)
- 導入Vue、Vue-Router對象, 并且調用Vue.use(VueRouter)
// 導入vue對象 import Vue from 'vue' // 導入vue-router對象 import VueRouter from 'vue-router' // 注入插件 Vue.use(VueRouter)
- 創建路由實例, 并且傳入路由映射配置
//定義路由 const routes = [ // 里面是映射配置 ] // 創建路由實例 const router = new VueRouter({ routes }) // 導出 router 實例 export default router
- 在Vue 實例中掛載創建的路由實例
image
使用 Vue-Router
-
創建路由組件
image -
配置路由映射: 組件和路徑的映射關系
image -
使用路由: 通過
<router-link>
和<roter-view>
(該標簽為 router 內部已經注冊的兩個全局組件)-
<router-link>
: 該標簽是一個 Vue-Router 中已經內置的組件, 它默認會被渲染成一個<a>
標簽, 詳情請看 "router-link" 章節 -
<router-view>
: 該標簽會根據當前的路徑, 動態渲染出不同的組件, 放置的位置決定渲染的位置
image
-
最終效果如下
router-link標簽
該標簽是一個 Vue-Router 中已經內置的組件, 它默認會被渲染成一個 <a>
標簽
它有如下屬性
-
to: (URL)
- 用于指定跳轉的路徑
-
tag: (tagName)
- 最終渲染成tagName標簽
-
replace: (無值)
- 跳轉時使用 history.replaceState(), 即頁面不能前進和后退
-
active-class: (className)
- 當
<router-link>
對應的路由匹配成功時, 會自動給當前元素設置一個 router-link-active 的 class , 設置 active-class 可以修改默認的名稱 - 在進行高亮顯示的導航欄菜單或者底部 tabbar 時, 會使用到該類
- 但是通常不會修改類的屬性, 會直接使用默認的 router-link-active 即可
- 簡便寫法: 該屬性可以在VueRouter實例中添加屬性為
linkActiveClass
- 當
設置默認路徑(重定向)
如何讓路徑默認跳到首頁, 并且 <router-view>
渲染首頁組件呢?
非常簡單, 我們只需要配置多一個映射就可以了
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
}
];
配置解析:
- 我們在 routes 中又配置了一個映射
- path 配置的是根路徑
-
redirect 是重定向, 也就是我們將根路徑重定向到
/home
的路徑下, 這樣就可以得到我們想要的結果了
將 URL 的模式 hash(默認) 改為 HTML5 的 history
前面說過改變路徑的方式有兩種:
- URL 的 hash
- HTML5 的 history
默認情況下, Vue 路徑的改變使用的是 URL 的 hash, 這樣顯示出的頁面的地址中有一個 # 號, 不太美觀
可以使用 HTML5 的 history 模式來進行改變, 進行如下配置即可:
為 VueRouter 實例對象添加屬性 mode(模式), 值為 history
const router = new VueRouter({
routes,
mode: "history"
});
路由代碼跳轉(不通過 router-link 實現同樣的功能)
$router : VueRouter 實例對象, Vue 在所有組件中都添加了該屬性
$router == VueRouter
<template>
<div id="app">
<button @click="homeClick">home</button>
<button @click="aboutClick">about</button>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App",
methods: {
homeClick() {
// push --> pushState
// replace --> repalceState
this.$router.push("/home").catch(err => {
console.log(err)
})
},
aboutClick() {
this.$router.push("/about").catch(err => {
console.log(err)
})
}
}
};
</script>
動態路由
在某些情況下, 一個頁面的 path 可能是不確定的, 比如我們進入用戶界面時, 希望是如下路徑
- /user/Sunny 或 /user/kobe
- 除了有前面的/user 之外, 后面還跟上了用戶的 id
- 這種 path 和 Component 的匹配關系, 我們稱之為 動態路由 (也就是路由傳遞數據的一種方式)
步驟一: 路由映射
import User from "../components/User.vue";
const routes = [
{
path: "/user/:userId",
component: User
}
];
步驟二: 使用子組件路由, 且通過 v-bind 動態設置屬性
<router-link :to="'/user/' + userId">My</router-link>
<script>
export default {
data() {
return {
userId: "Sunny"
};
}
}
</script>
步驟三: 在子組件 User.vue 中, 通過 $route.params.userId 獲得當前用戶 id
<template>
<div>
<h1>{{ $route.params.userId }}</h1>
<h2>User Interface</h2>
<p>User information</p>
</div>
</template>
懶加載
當打包構建應用時, Javascript 包會變得非常大, 影響頁面加載, 如果我們能把不同路由對應的組件分割成不同的代碼塊, 然后當路由被訪問的時候才加載對應組件, 這樣就更加高效了
為了實現這種效果, 我們可以使用路由的懶加載
路由懶加載的主要作用就是將路由對應的組件打包成一個個的 js 代碼塊, 只有在這個路由被訪問到的時候, 才加載對應的組件
js 包為什么大?
- 首先, 我們知道路由通常會定義很多不同的頁面
- 這個頁面最后被打包放在哪里呢? 一般情況下, 是放在一個 js 文件中
- 但是, 頁面這么多放在一個 js 文件中, 必然會造成這個頁面非常的大
- 如果我們一次性從服務器請求下來這個頁面, 可能需要花費一定的時間, 甚至用戶電腦上還出現了短暫空白的情況
- 如何避免這種情況呢? 使用路由懶加載就可以了
- 路由懶加載做了什么?
- 路由懶加載的主要作用就是將路由對應的組件打包成一個個 js 代碼塊
- 只有在這個路由被訪問到的時候, 才加載對應的組件
懶加載的三種方式
方式一(早期): 結合 Vue 的異步組件和 webpack 的代碼分析
const Home = resolve => {
require.ensure(["../components/Home.vue"], () => {
resolve(require("../components/Home.vue"))
})
}
方式二: AMD 寫法
const Aoubt = resolve => require(["../components/About.vue"], resolve);
方式三(推薦): 在 ES6 中, 我們可以有更加簡單的寫法來組織 Vue 異步組件和 webpack 的代碼分割
const Home = () => import("../components/Home.vue");
嵌套路由
嵌套有路是一個很常見的功能
- 比如在 home 頁面中, 我們希望通過/home/news 和 /home/message 訪問一些內容
- 一個路徑映射一個組件, 訪問這兩個路徑也會分別渲染兩個組件
實現嵌套路由有兩個步驟:
- 創建對應的子組件, 并且在路由映射中配置對應的子路由
- 在組件內部使用
<router-view>
標簽
創建對應的子組件
<template>
<div>
<ul>
<li>消息1</li>
<li>消息2</li>
<li>消息3</li>
<li>消息4</li>
</ul>
</div>
</template>
<script>
export default {
name: "HomeMessage"
};
</script>
在路由映射中配置對應的子路由
// 注意: 子路由的path前面不能帶'/'
const routes = [
{
path: '/home',
component: Home,
children: [
{
path: '',
redirect: 'news'
// 默認路徑為/home/news
},
{
path: 'news',
component: HomeNews
},
{
path: 'message',
component: HomeMessage
}
]
},
]
在組件內部使用<router-view>
標簽
<template>
<div>
<h2>Home</h2>
<p>I'am is Home</p>
<router-link to="/home/news">news</router-link>
<router-link to="/home/message">message</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "Home"
};
</script>
嵌套路由默認路徑
在子路由中設置重定向
const routes = [
{
path: "/",
redirect: "/home"
},
{
path: "/home",
component: Home,
children: [
{
path: "",
redirect: "news"
},
{
path: "news",
component: HomeNews
}
]
}
];
keep-alive
keep-alive 是 Vue 內置的一個組件, 可以使被包含的組件保留狀態, 或避免重新渲染
router-view 也是一個組件, 如果直接被包在keep-alive里面, 所有路徑匹配到的視圖組件都會被緩存
通過 create 生命周期函數來驗證
可以通過 keep-alive, 監聽一個組件 "活躍" 和 "不活躍" 的狀態
-
activated()
活躍狀態自動調用該函數 -
deactivated()
不活躍狀態自動調用該函數
keep-alive屬性:
- include : 字符串或正則, 只有匹配的組件才會被緩存
-
exclude : 字符串或正則, 任何匹配的組件都不會被緩存
- 可以使用組件導出時的 name 作為匹配
- 注意: 如果有多個匹配, 用 , (逗號) 分開, 且逗號兩邊不能存在空格
參數傳遞
傳遞參數主要有兩種類型: params 和 query
- params($route.params) 的類型:
- 配置路由格式: /router/:id
- 傳遞的方式: 在 path 后面跟上對應的值
- 傳遞后形成的路徑: /router/123, /router/abc
- query ($route.query)的類型:
- 配置路由格式: /router
- 傳遞的方式: 對象中使用 query 的 key 作為傳遞方式
- 傳遞后形成的路徑: /router?id=123, /router?id=abc
<router-link
:to="{
path: '/profile',
query: {
name: 'Sunny',
age: 20,
height: 1.7
}
}">
Profile
</router-link>
這里獲取并打印這些數據:
<template>
<div>
<h1>{{ $route.query.name }}</h1>
<h2>Profile</h2>
<p>I'am is Profile</p>
<p>{{ $route.query }}</p>
</div>
</template>
不使用 router-link 實現 query
<template>
<div>
<button @click="profileClick">profile</button>
</div>
</template>
<script>
export default {
mathods: {
profileClick() {
this.$router.push({
path: '/profile',
query: {
name: 'Sunny',
age: 20,
height: 1.7
}
})
}
}
}
</script>
導航守衛
在 SPA 應用中, 如何改變頁面的標題呢?
- 網頁標題是通過
<title>
來顯示的, 但是 SPA 只有一個固定的 HTML, 切換不同的頁面時, 標題并不會改變 - 但是我們可以通過 js 來修改
<title>
的內容,document.title = 'new title'
- 那么在 Vue 項目中, 我們可以通過生命周期的
created
函數來實現 - 或調用 VueRouter 實例的
.beforeEach()
函數
const routes = [
{
path: "/about",
component: About,
meta: {
title: "About"
}
}
];
/**
* to: 即將要進入的目標的路由對象
* from: 當前導航即將要離開的路由對象
* next: 調用該方法后, 才能進入下一個鉤子
*/
router.beforeEach((to, from, next) => {
document.title = to.matched[0].meta.title;
next();
});
beforeEach 前置守衛
通過 VueRouter 實例對象調用
beforeEach(function (to, from, next) {});
- to: 即將要進入的目標路由對象
- from: 當前導航即將要離開的路由對象
- next: 調用該方法后, 才能進入下一個路由對象
afterEach 后置守衛
通過 VueRouter 實例對象調用
afterEach(function (to, from) {});
- to: 已經進入的目標路由對象
- from: 已經離開的路由對象
導航守衛補充
- 如果是后置鉤子, 也就是 afterEach, 不需要主動調用 next() 函數
- beforEach 必須要調用 next() 函數, 不然就會終止, 不會往下執行
- 上面使用的導航守衛, 被稱之為全局守衛, 除此之外, 還有路由獨享的守衛、組件內的守衛
$router 和 $route 的區別
- $router == Vue-Router 實例對象
- $route == 當前處于活躍狀態的路由
所有組件都繼承自 Vue 的原型, 所以, 所有組件都擁有 $router和$route