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