前端路由的定义

在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:

vue-router嵌套子路由实际使用-青梅煮码

很简单吧,我相信每个人都可以设计出这样布局的路由配置;不过,我这里有2个需求:
1.我希望main + aside这整块区域可以跳转路由;什么意思呢,就是从A可以跳转到B(也就是下面这张图)container.vue:

vue-router嵌套子路由实际使用-青梅煮码

2.我希望main和aside两块是独立的;也就是说,main里可以跳转到其他路由,aside也可以跳转到其他路由;(当然也可以只跳转一个区域的路由,另一个路由不变)也就是从A直接跳转到C(看下图)article-detail.vue:

vue-router嵌套子路由实际使用-青梅煮码

我们都知道,用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}
 
  ]
})