前端路由的定義
在spa流行之前,前端路由是沒有的;而像java之類的后台語言很早就有了,后端路由一般就是定義一系列的訪問地址規則,路由引擎根據這些規則匹配並找到對應的處理頁面,然后將請求轉發給頁面進行處理。
在spa應用中,前端路由是直接找到與地址匹配的一個組件或對象並將其渲染出來。改變瀏覽器地址而不向服務器發出請求有兩種做法,一是在地址中加入#以欺騙瀏覽器,地址的改變是由於正在進行頁內導航;二是使用HTML5的window.history功能,使用URL的Hash來模擬一個完整的URL。將單頁程序分割為各自功能合理的組件或者頁面,路由起到了一個非常重要的作用。它就是連接單頁程序中各頁面之間的鏈條。
路由與導航
單頁式應用是沒有“頁”的概念的,更准確地說,Vue.js是沒有頁面這個概念地,Vue.js地容器就只有組件。但我們用vue-router配合組件又會形成各種的“頁面”,那么我們可以這樣來約定和理解:
1.頁面是一個抽象的邏輯概念,用於划分功能場景
2.組件是頁面在Vue的具體實現方式
router-view
渲染路徑匹配到的視圖組件,它還可以內嵌自己的router-view
這里我主要記錄下在實際項目中,如何使用命名路由和嵌套命名視圖實現布局。下圖是我們需要實現的效果(這個效果標記A)index.vue:

很簡單吧,我相信每個人都可以設計出這樣布局的路由配置;不過,我這里有2個需求:
1.我希望main + aside這整塊區域可以跳轉路由;什么意思呢,就是從A可以跳轉到B(也就是下面這張圖)container.vue:

2.我希望main和aside兩塊是獨立的;也就是說,main里可以跳轉到其他路由,aside也可以跳轉到其他路由;(當然也可以只跳轉一個區域的路由,另一個路由不變)也就是從A直接跳轉到C(看下圖)article-detail.vue:

我們都知道,用vue-cli做項目,都會有一個頂層路由入口router-view寫在app.js里面;很顯然我們這里的header,main,aside,footer都在這個頂層入口里;我們先來實現一下需求1,需求1很簡單,就是在頂層入口里加一個子路由;但是考慮到需求二的原因,index.vue里面需要提前加入兩個命名視圖來渲染首頁,以便於需求二獨立渲染main和aside這兩個部分:
router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
let router = new Router({
path: '/',
name: 'index',
component: () => import ('@/views/index.vue')
children: [
{
path: '',
components: {
main: () => import('@/views/main.vue'),
aside: () => import('@/views/aside.vue')
}
},
{
path: 'container',
component: () => import ('@/views/container.vue')
}
]
})
index.vue
<template>
<myheader></myheader>
<router-view></router-view>
<router-view name="main"></router-view>
<router-view name="aside"></router-view>
<myfooter></myfooter>
</template>
實現需求二就和根路由設置一樣了,在路由里使用兩個組件來渲染即可:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
let router = new Router({
path: '/',
name: 'index',
component: () => import ('@/views/index.vue'),
children: [
{
path: '',
components: {
main: () => import('@/views/main.vue'),
aside: () => import('@/views/aside.vue')
}
},
{
path: 'container',
component: () => import ('@/views/container.vue')
},
{
path: 'article-detail',
components: {
main: () => import('@/views/article-detail.vue'),
aside: () => import('@/views/aside.vue')
}
}
]
})
除了上面這種做法,我再貼一個實現相同功能的代碼塊:
路由配置:
let router = new Router({
mode: 'history',
scrollBehavior: () => ({y: 0}),
routes: [
{
path: '/',
name: 'home',
redirect: '/home',
component: () => import('@/views/home.vue'),
children: [
{
path: 'home',
component: () => import('@/views/mainAndAside.vue'),
children: [
{
// 這里的path為空,當父組件匹配不到路由時,默認就會渲染這個子路由
path: '',
meta: {
title: '首頁'
},
components: {
main: () => import('@/views/main.vue'),
aside: () => import('@/views/aside.vue')
}
}
]
},
{
path: 'container',
component: () => import('@/views/container.vue'),
},
{
path: 'article/detail/:id',
component: () => import('@/views/mainAndAside.vue'),
props: true,
children: [{
path: '',
meta: {
title: '詳情頁'
},
components: {
main: () => import('@/views/articleMain.vue'),
aside: () => import('@/views/articleAside.vue')
},
props: {
main: true,
aside: false
}
}]
},
]
再看一看兩個核心組件的代碼:
home.vue
<template>
<home-layer>
<el-col slot="header">
<myheader></myheader>
</el-col>
<router-view slot="main"></router-view>
<div slot="footer">
<myfooter></myfooter>
</div>
<go-top></go-top>
</home-layer>
</template>
.......
mainAndAside.vue(這里用了element-ui)
<template>
<el-row class="main-wrap" :gutter="20">
<el-col class="aside" ref="aside" :md="8" :xl="6" :sm="24">
<div ref="asideWrap" class="aside-wrap">
<router-view name = "aside"></router-view>
</div>
</el-col>
<el-col class="main" :md="16" :xl="18" :sm="24">
<router-view name = "main" :key="key"></router-view>
</el-col>
</el-row>
</template>
其他無關緊要的組件,就不展示了。上面這種做法,更加靈活的控制了布局,而不是將三個router-view並列排在一起,而是以一個未命名的router-view作為總入口,然后在這個組件里再設置兩個命名視圖;這樣就可以只渲染總入口的router-view,也可以同時渲染總入口的router-view和子組件的兩個命名視圖;完全看路由的配置了,很靈活。
全局路由鈎子之beforeEach和afterEach
簡單看一下,實際應用中的代碼:
let loadingInstance = null
// 路由全局前置守衛
router.beforeEach((to, from, next) => {
loadingInstance = Loading.service({lock: true})
let token = getCookie('token')
// 修改網頁標題
window.document.title = to.meta.title
// token存在的情況(代表用戶登錄成功過)
if (token) {
if (!String(store.getters.token)) {
store.commit('setToken', token)
}
if (String(store.getters.nickname) === '') {
// 當vuex中沒有用戶數據時,從后台獲取
store.dispatch('getInfo')
}
forbidRedirect(to, next)
} else {
// 如果token不存在;判斷路由是否需要登錄權限
if (to.meta.requireAuth) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
}
})
// 路由全局后置守衛
router.afterEach((to, from) => {
loadingInstance.close()
})
在beforeEach中根據token判斷用戶是否登錄,如果登錄了,則查看vuex中有木有用戶信息,沒有則在vuex中執行getInfo的action獲取用戶信息;如果未登錄,則判斷將要跳轉的目標路由,是否需要登錄才能跳轉;如果是,則使用next()導航到登錄頁,否則,正常跳轉;另外,在beforeEach里,加載一個loading動畫,在afterEach中關閉這個loading動畫。
history模式
當我們把路由配置成history模式后,假如用戶點擊/index上的http://localhost/index)。如果我們直接在瀏覽器輸入http://localhost/index,你會驚奇的發現瀏覽器會出現404的錯誤!
這是由於直接在瀏覽器中輸入http://localhost/home,瀏覽器就會直接將這個地址請求發送至服務器,先由服務器處理路由,而客戶端路由的啟動條件是要訪問/index.html,這樣的話客戶端路由就完全失效了!
解決的辦法是將所有發送到服務器的請求利用服務端的URLRewrite模板重新轉發給/index.html,啟動VueRouter進行處理,而瀏覽器地址欄的URL保持不變。
這個問題在開發環境下是不會出現的,因為我們在開發環境中使用的是webpack的DevServer,DevServer是對這個問題進行了處理的,只要打開vue-cli(2.X版本)生成的項目中buid目錄下的webpack.dev.config.js找到devServer配置屬性就可以見到:
devServer: {
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
]
}
}
而當我們部署到生產環境時,就需要在web服務器上進行一些簡單配置以支持Fallback。
我只用到過nginx服務器,就以這個為例吧:
location / {
try_files $uri $uri/ /index.html;
}
一旦我們進行了上述配置,你的服務器就不會再返回404錯誤頁面,因為對於所有路徑都會返回index.html文件。為了避免發生這種情況,應該在Vue應用里面覆蓋所有的路由情況,然后再給出一個404頁面。
const router = new VueRouter({
mode: 'history',
routes: [
.....,
.....,
.....,
// 這個路由應該放在最后面,否則會覆蓋其他已有的路由
{ path: '*', component: 404.vue}
]
})