复盘Vue Router

129 阅读7分钟

这篇文章主要收录了web发展阶段、前端路由以及Vue Router中的路由占位符、路由重定向、路由懒加载、路由嵌套、编程式导航、动态添加路由、导航守卫等等知识

1 web发展阶段

  • 后端路由阶段;
  • 前后端分离阶段;
  • 单页面富应用(SPA);

1.1 后端路由阶段

在早期的网站开发,整个html页面由服务器来渲染,服务器直接渲染好对应的html页面,返回给客户端进行展示;

一个页面有对应的网址,也就是URL,统一资源定位符;

URL会发送到服务器,服务器通过正则表达式对该URL进行匹配,最后交给一个Controller进行处理

Controller最终进行各种处理,最终生成html或者数据,返回给客户端;

这就是后端路由,后端路由有利于SEO

但是,后端路由缺点也非常明显:

  • 整个页面的模块由后端人员来编写;
  • 前端开发人员如果要开发页面,需要通过PHP和Java等语言来编写页面代码;
  • html代码和数据的处理逻辑会混在一起,编写和维护的体验都很糟糕

1.2 前后端分离阶段

随着Ajax技术的出现,有了前后端分离的开发模式;

后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过js将数据渲染到页面中;

这样前后端责任清晰,后端专注于数据上,前端专注于交互和可视化上;

1.3 单页面富应用

SPA(single page application)

  • SPA单页面应用程序:整个网站只有一个页面,内容的变化通过Ajax局部更新实现、同时支持浏览器地址栏的后退操作
  • SPA实现原理之一:基于url地址的hash(hash的变化会导致浏览器记录访问历史的变化,但是hash的变化不会触发新的url请求)
  • 在实现SPA过程中,最核心的技术点就是前端路由

2 前端路由

什么叫前端渲染?

每次请求涉及到的静态资源都会从静态服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染

前端路由其实就是由前端负责维护路径和组件之间的映射关系

2.1 改变路径而不刷新

url的hash

也叫锚点(#),本质的改变window.location的href属性

可以通过直接赋值location.hash来改变href,但页面不发生刷新

案例

<div id="app">
    <a href="#/home">home</a>
    <a href="#/about">about</a>
    <div class="content"></div>
</div>
const contentEl = document.querySelector('.content')
window.addEventListener('hashchange', () => {
  switch (location.hash) {
    case '#/home':
      contentEl.innerHTML = 'Home'
      break
    case '#/about':
      contentEl.innerHTML = 'about'
      break
    default:
    contentEl.innerHTML = 'Default'
      break;
  }
})

监听hash值的改变,当点击home或about的链接,location.hash会发生改变,根据不同hash显示不同内容~

html5的history

history是html5新增,有6种方式改变URL而不刷新页面

pushState(栈结构)

使用新的路径旧的路径压入历史记录栈中,所以可以回退

history.pushState({},'','home')

replaceState

新路径替换旧的路径,所以没有后退

replaceState({},'','home')

popState

路径的回退,在历史记录栈中找路径

go、forward、back

history.go(-1) = history.back()

history.go(1) = history.forward()

3 Vue Router

vue router(官网 router.vuejs.org/zh)是vue.js 官方的路由管理器

vue router包含的功能:

支持html5的history模式或hash模式

  • 支持嵌套路由
  • 支持路由参数
  • 支持编程式路由
  • 支持命名路由

3.1 基本使用

方式一:引入相关的库文件

<!-- 导入vue文件,为全局window对象挂载vue构造函数 -->
<script src="lib/vue_2.5.22.js"></script><!-- 导入vue-router文件,为全局window对象挂载vueRouter构造函数 -->
<script src="lib/vue-router_3.0.2.js"></script>

或者

方式二:npm安装

npm install vue-router -s

模块工程中使用它

  1. 导入路由对象,并且调用Vue.use(VueRouter)
  2. 创建router实例,传入路由映射配置routes
  3. 导出router实例
  4. Vue实例中挂载router实例
  5. 通过和使用

文件夹router下index.js

import VueRouter from 'vue-router'const routes = [
    {
        path: '/',
        components: ...
    }
]
const router = new VueRouter({
    routes
})
export default router

vueRouter3.x使用vueRouter() 创建可以被vue应用程序使用的路由实例

而vueRouter4.x使用createRouter() 创建路由实例

import { createRouter } from 'vue-router'const routes = [
    {
        path: '/',
        components: ...
    }
]
const router = new createRouter({
    routes
})
export default router

main.js

vue2.x写法

import Vue from 'vue'
import App from './App.vue'
import router from './router'Vue.use(router)
new Vue({
    router,
    render: h => h(App)
}).$mount('#app')

vue3.x写法

import { creatApp } from 'vue'
import { router } from './router'
import App from './App.vue'const app = creatApp(App)
app.use(router)
app.mount('#app')

为什么url上有#?

默认情况下,路径的改变使用的是url的hash

怎么去掉#?

路径的改变换成HTML5的history模式

创建router实例的时候再添加一个属性mode

VueRouter3.x写法

const router = new VueRouter({
    routes,
    mode: 'history'
})

VueRouter4.x写法

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
    {
        path: '/',
        components: ...
    }
]
const router = new createRouter({
    routes,
    history: createWebHistory()
})
export default router

路由和组件的对应关系就搞定了;

但这还没完,还得告诉页面哪个地方显示不同路由对应的组件内容,只有这样,当你切换路由时,页面才知道哪个地方显示不同组件内容,这是就要用到router-view了;

3.2 router-view

是vue提供的内置组件路由占位符,当路由切换时,会动态的显示不同内容

<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view></router-view>

当点击首页,router-view中的内容变成 /home对应的组件;

当点击关于,router-view中的内容变成 /about对应的组件;

增强

router-view也有作用域插槽,将props.Componet传给子组件;

搭配transition这个内置组件,可以实现路由跳转时动画效果

<router-view v-slot="props">
    <transition name="zsf">
        <component :is="props.Component"></component>
    </transition>
</router-view>
.zsf-enter-from,
.zsf-leave-to {
    opacity: 0;
}

.zsf-enter-to,
.zsf-leave-from {
    opacity: 1;
}

.zsf-enter-active,
.zsf-leave-active {
    transition: opacity 2s ease;
}

与keep-alive搭配

路由跳转的时候,组件内部的状态是没有被保存下来的,每次切换回来的时候都是重新渲染(重复执行组件的那些生命周期函数),如果不希望重新渲染呢?

<keep-alive>
    <router-view></router-view>
</keep-alive>

只有组件保持了状态使用了keep-alive时,生命周期函数activated()deactivated() 才是有效的

3.3 router-link

router-link默认会渲染成a标签

但要是想渲染成其它标签呢?

比如button

tag

添加一个tag="button"属性,会渲染成button元素

<router-link to="/home" tag="button">首页</router-link>

Vue Router4.x已经删除这个属性了

replace

添加一个replace属性

<router-link to="/home" replace>首页</router-link>

设置replace属性的话,点击时,会调用router.replace() ,新路径替换旧路径,且没有后退

当然这个用的比较少,因为允许用户返回体验更好~

如果点击到某个按钮时,希望它是红色(激活),怎么做?

active-class

添加一个active-class="active" 属性

<router-link to="/home" active-class="active">首页</router-link>
.active {
    color: red;
}

运用场景

导航栏菜单或者底部tabbar需要高亮显示时会用到

v-slot

Vue Router4.x已经删除了tag属性,要是渲染成其它元素或组件,直接写,内部用的是插槽的原理

元素

<router-link to="/home">
    <button>首页</button>
</router-link>

要想给router-link包裹的子组件或元素传递某些数据,可以使用作用域插槽

<router-link to="/home" v-slot="props">
    <button>首页</button>
</router-link>

props中属性很多,详情可查看官网~

3.4 路由重定向

用户在访问地址a的时候,强制用户跳转到地址c,从而展示特定的组件页面

通过路由规则的redirect属性,指定一个新的路由地址,可以很方便地设置路由的重定向 (路由的默认路径)

const routes = [
    { path:"/",redirect:"/user"},
    { path: "/user", component: User },
]

3.5 路由懒加载

打包构建应用时,如果所有所有的打包结果都放app.[hash].js这个文件,会变得非常大,影响首屏加载时间

所以我们需要对某些打包结果进行分包,放到别的文件~,这就需要使用webpack提供的import()

路由懒加载的作用:将路由对应的组件打包成一个个的js文件,只有在这个路由被访问到时才加载对应组件

简而言之,用到时再加载

方式

方式一:结合vue的异步组件和webpack的代码分割 (老、长)

const Home = resolve => { require.ensure(['../components/Home.vue'], () =>{ resolve(require('../components/Home.vue'))})};

方式二:AMD写法

const About = resolve => require(['../components/About.vue'], resolve)

方式三:es6 (推荐)

const Home = () => import('../components/Home.vue')

使用

const routes = [
    {
    	path: '/',
    	componenet: () => import('../components/Home.vue')
	}  
]

componenet属性可以是个函数,但这函数必须返回一个Promise,而import() 返回值恰好是Promise;

不过打包之后,由于使用了hash值命名,不知道是哪个组件对应的打包结果;

如果想对打包结果命名,可以使用魔法注释(magic comment),给import() 传入注释;

const routes = [
    {
    	path: '/',
    	componenet: () => import(/* webpackChunkName: '名字' */'../components/Home.vue')
	}  
]

/* webpackChunkName: '名字' */ 是固定格式,只有名字是自定义的,不过一般会在自定义名字的基础上,加上 -chunk,比如

/* webpackChunkName: 'home-chunk' */ 个人习惯~

3.6 动态路由匹配

某些情况下,一个页面的path路径可能是不确定的,比如进入用户界面时,希望是如下路径:

  • user/aaa或user/bbb
  • user/用户id

如果希望组件获取到那个用户id并展示,怎么做?

文件夹router下的index.js

routes: [
    // 动态路径参数,以冒号开头
    { 
        path: "/user/:username", 
        component: User 
    },
]

vue2写法

App.vue

<router-link :to="'/user/'+ userId">用户</router-link>
{{ userId }}
created() {
    userId () {
        return this.$route.params.id
    }
}

this.$route获取到的处于活跃状态的路由信息;

上面例子中,处于活跃的路由是{ path: "/user/:username", component: User }

vue3写法

由于setup() 中this获取不到当前组件实例,所以this.$route不可行;

Vue Router4.x提供了一个hook函数,useRoute() ,它返回当前组件对应的路由对象;

App.vue

<router-link :to="'/user/'+ username">用户</router-link>
{{ route.params.username }}
import { useRoute } from 'vue-router'

setup() {
    const route = useRoute()
    return {
        route
    }
}

注意

route,不是route**,不是 **router!

多条件匹配

当然,不止支持一个条件匹配,也支持多条件

routes: [
    // 动态路径参数,以冒号开头
    { 
        path: "/user/:username/id/:id", 
        component: User 
    },
]

NotFound

当某个路由没有对应组件时,页面会显示空白

这对用户体验非常不友好,应该给出提示

应该有个NotFound信息提示的组件;

routes: [
    // 动态路径参数,以冒号开头
    { 
        path: '/:pathMatch(.*)', 
        component: () => import('./NotFound.vue') 
    },
]

NotFound页面也可以获取到对应的路由信息,通过 $route.params.pathMatch

如果路径匹配时在使用 :pathMatch(.*) 的基础上再加个 ***** ,获取到的路由信息将以/为分隔符,放入一个数组中;

routes: [
    // 动态路径参数,以冒号开头
    { 
        path: '/:pathMatch(.*)*', 
        component: () => import('./NotFound.vue') 
    },
]

Vue Router3.x使用通配符*

3.7 路由嵌套

在home页面中,我们希望通过/home/news和/home/messages访问一些内容。

怎么做?

使用

router文件夹下的index.js

const routes = [
    {
    	path: '/home',
    	componenet: () => import('../components/Home.vue'),
    	children: [
    		{
    			path: 'news',
    			component: () => import('../components/HomeNews.vue')
			}
    	]
	}  
]

Home.vue

<div>
    <h2>Home组件</h2>
    <router-link to="/home/news"></router-link>
    <router-view>给HomeNews组件的占位符</router-view>
</div>

3.8 编程式导航

页面导航方式

  • 声明式导航:通过点击链接实现导航的方式,叫做声明式导航

    例如:普通网页中的链接或vue中的

  • 编程式导航:通过调用javascript形式的api实现导航的方式,叫做编程式导航

    例如:普通网页中的location.href

编程式导航

使用 $router对象

vue2写法

<button @click="goRegister">跳转到注册页面</button>
methods: {
    goRegister() {
        this.$router.push('/register');
    }
}

为什么this.$router没有定义却能使用?

因为vue-router给所有组件都添加了 router属性,所有组件可以通过this.router**属性,所有组件可以通过**this.router拿到

通过源码发现,所有vue组件都继承了vue的原型(prototype),当执行 Vue.prototype.name = 'zsf' 时,所有vue组件都有了name这个属性,方法同理,routerrouter和route就是这样给所有组件加上去的

router.push() 的参数规则

// 字符串(路径名称)
router.push('/home')
// 对象
router.push( { path:'/home' } )
// 命名的路由
router.push({ name: '/user', params: { userid: 123 } })
// 带查询参数,变成/register?uname=lisi
router.push({ path: '/register', query: { uname: 'lisi' } })

vue3写法

<button @click="goRegister">跳转到注册页面</button>
import { useRouter } from 'vue-router'

setup() {
    const router = useRouter()
    const goRegister = () => {
        router.push('/register')
    }
    return {
        goRegister
    }
}

3.9 动态添加路由

一级

一般情况,路由对象的routes属性是内容固定的,路由规则已经写好;

但某些情况下,希望routes的路由规则动态的,这时就需要动态添加路由了;

使用路由对象的addRoute()

// 假设路由对象router已创建
const routes = [...]
const homeRoute = {
	path: '/home',
    component: () => import('./Home.vue')
}
router.addRoute(homeRoute)

二级

二级路由呢?

addRoute() 如果有两个参数,第一个是一级路由

// 假设路由对象router已创建
const routes = [...]
const homeRoute = {
	path: '/home',
    component: () => import('./Home.vue')
}
router.addRoute('app', homeRoute)

效果与下面同理,不过这是动态添加

const routes = [
    {
    	path: '/app',
        name: 'app'
    	componenet: () => import('./App.vue'),
    	children: [
    		{
    			path: '/home',
    			component: () => import('./Home.vue')
			}
    	]
	}  
]

3.10 导航守卫

通过跳转或取消的方式守卫某一次导航;

比如一个登陆页面,当你填写完信息,点击登陆;

导航守卫会拦截你这次跳转,判断你的信息是否正确;

信息正确,导航到主页;

信息错误,导航到登陆页;

前置路由守卫

router.beforEach()导航时会触发回调

Vue Router3.x时,该回调函数传入三个参数:

  • to,即将跳转的route对象
  • from,当前route对象
  • next,next()
router.beforEach((to, from, next) => {
    
})

Vue Router4.x时,第三个参数不推荐使用了,因为会执行next()多次

router.beforEach()返回值有四种类型

  • false,不进行导航;
  • undefined,进行默认导航;
  • 字符串,跳转到对应路由;
  • 对象,类似router.push({...});

简单实现登陆逻辑

  • 登陆成功在localStorage设置token
  • 每次导航非登录页时,进行导航守卫
  • 如果localStoragetoken有值,返回undefined,允许导航(默认);
  • 如果localStoragetoken没有值,返回字符串('/login'),导航到登录页;

登陆成功时

const token = window.localStorage.setItem('token', 'zsf')

登陆导航守卫

router.beforEach((to, from) => {
    if(to.path !== '/login') {
        const token = window.localStorage.getItem('token')
        if(!token) {
            return '/login'
        }
    }
})

案例-改变网页标题

在一个SPA应用中,如何改变网页的标题?

网页标题是通过title标签来显示的,但是SPA只有一个固定的HTML,切换不同页面时,标题不会改变

但是可以通过javaScript来修改title的内容: window.document.title = '新标题'

那在vue项目中,在哪里修改?什么时候修改比较合适呢?

在生命周期函数created()中

created () {
    document.title = '首页'
} 

但是页面多了或者需求更改之后这个做法不好维护

既然页面通过路由跳转,那能不能监听一下路由跳转的过程?当每次监听发生跳转的时候,改成对应的标题就可以了

const routes = [
    {
    	path: '/',
        meta: {
    		title: '首页'
		}
    	componenets: Home,
    	childern: [
    		{
    			path: 'news',
    			components: HomeNews
			}
    	]
	}  
]
router.beforEach((to, from, next) => {
    document.title = to.matched[0].meta.title
    next()
})
  • 一定要调用next() ,不然页面不发生跳转
  • 要给每个route对象加个元数据meta

当然,还有其它守卫,详情看官方文档;

3.11 historyAPIFallback

主要作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误;

默认值是false,如果设置为true,那么刷新时,返回404错误时,会自动返回index.html的内容;

事实上devServer中实现historyApiFallback功能时通过connect-history-api-fallback库的;

如何修改这个配置?

  • 修改cli-service源码
  • vue.config.js中配置

这里只说vue.config.js中配置

module.exports = {
    configureWebpack: {
        devServer: {
            historyAPIFallback: true
        }
    }
}