前端路由的定义
在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}
]
})