前言
SPA是什么
- SPA 就是单页Web应用的英文缩写(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
- 浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。
- 让用户更新视图时不重新请求页面进入短暂的卡顿、空白、假死状态,拥有更好的用户体验
为什么要使用SPA
- 技术支持
- 随着前端的发展,css3、html5 等的加入,浏览器端的更新,我们发现现在一个网页已经可以承载一些相对不这么复杂的 应用程序 了
- 我们可以完成很多不同的交互,也有优美的动画转接,可以让用户短暂的忘记这是一个网页,而不是一个程序
- 用户需求
- 我们遇到最多的场景就是,一些小型的商城,卖东西、餐厅点餐,为了让用户有更好的体验,去除中间跳转页面的空白等
- 大白话说就是能让用户用的爽一点,又不用下载程序,感觉你这个很高科技,用着很舒服
- 开发需求
- 不同商家需要不同的定制化功能,UI等等,要频繁的更新功能,如果是以往用程序的形式开发,往往比较慢,而且更新麻烦,用户也需要更新程序才能使用新功能,用单页面应用的形式开发,快速,跨平台,热更新
- 如果是以企业的角度出发,以商城为例,当我体量不够大的时候,我可以通过单页面应用快速的吸引顾客,或者是试水,以较低的成本,较高的速度开发完成,当我体量差不多够大的时候,再开发原生的程序,可以提高效率
我们怎么开发SPA
- 以我自身的理解,三大框架就是为单页面应用而生的,当然你也可以用三大框架去进行多页面开发
- 单页面应用使得前端的业务加重,样式、元素、逻辑也越来越垄长、繁重,所以我们需要
mvvm模型,让我们的注意力集中在数据逻辑上,我们也需要组件去化繁为简,使得可读性、可维护性大大的增加,耦合性的降低 - 前面两篇文章我们认识到了
Vue的 常用API 及Vue的 组件,接下来我们学习下一个重点Vue-Router
Vue-Router
什么是Vue-Router
- 他是
Vue官方的路由管理器,与Vue核心深度集成,让构建单页面应用变得易如反掌 - 他的本质就是建立起 URL 和 页面 之间的映射关系,告诉
Vue-Router在哪里渲染它们 - 他与
v-show和v-if类似,都是可以显示隐藏某些东西,但又有所不同,v-if是根据boolean去判断,而Vue-Router则是通过 URL 判断
为什么要使用Vue-Router
- 在多页面应用里面我们会通过a标签等方式进行页面的跳转,视图的切换,这很符合用户逻辑
- 在单页面应用也有这种需求,那么如何能在 URL 变更的时候,不刷新页面,而且监听到变化呢?
- 所以我们需要使用
Vue-Router解决这样的需求问题,它可以实现对页面局部进行无刷新的替换,让用户感觉就像切换到了网页一样 - 一个统一管理组件映射的路由表也可以让我们维护项目的时候更加方便,轻松
安装
- 通过
npm安装Vue-Router到项目内
npm install vue-router
- 也可以在
create创建项目时在配置项选择Vue-Router安装
最简模型
目录结构
- 创建 router.js 用于管理路由
router.js 文件
// 1. 使用 `ESM` 引入 `Vue` 和 `VueRouter`
import Vue from 'vue';
import VueRouter from 'vue-router';
// 3. 引入所需组件
import Home from './views/Home.vue';
// 2. 全局注入插件 `VueRouter`
Vue.use(VueRouter);
// 4. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes: [{
path: '/', // 匹配路径
component: Home // 路径映射的组件
}]
});
// 5. 导出 router 实例
export default router;
main.js 文件
import Vue from 'vue'
import App from './App.vue'
// 1. 引入导出的 router 实例
import router from './router'
Vue.config.productionTip = false
new Vue({
router, // 创建和挂载根实例 *这里名字一定要是 router,内部逻辑依赖这个值*
render: h => h(App),
}).$mount('#app')
- 这个时候我们启动一下项目,发现啥效果都没有,为啥?
- 上面的操作我们引入了
VueRouter,创建了实例,进行了配置,也与Vue进行了绑定 - 我们建立了 URL 与页面之间的映射关系(关联了起来)
- 但我们没有告诉
VueRouter这个映射的页面在哪显示,所以我们要使用router-view告诉他,匹配到的组件要在这里显示
- 上面的操作我们引入了
App.vue
<template>
<div id="app">
app
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>
- 到这,一个简单的
Vue-Router路由的简单模型就弄好辣 - 就是将组件(components)映射到路由(routes),然后告诉
VueRouter在哪里渲染 - 我们可以看到 导航栏 上的 / 对应的是我们的 Home 组件
- 之后我们所有的路由其他乱七八糟 API 和需求都是围绕着这个模型展开的,都是建立在这个之上的,所以这个模型很重要,不懂可以多敲几次或者看看别的大佬的说明嗷
路由 routes
动态路由
- 我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个
User组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在vue-router的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果 - 当我们使用动态路由参数时,例如从
/class/123=>/class/456,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用 - 复用组件时,想对路由参数的变化作出响应的话,我们可以简单地 watch (监测变化)
$route对象
动态路由参数
- 动态路由以
/:paramsName表示
const router = new VueRouter({
routes: [{
path: '/',
component: Home
}, {
// 动态路径参数 以冒号开头
path: '/class/:id',
component: ClassView
}]
});
- 我们设置了一个 路径参数 ,可以通过
this.$route去获取
<script>
export default {
created() {
// 以 : 标记的参数,会被设置到 this.$route.params 内
// this.$route 可全局访问
console.log(this.$route.params);
},
};
</script>
- 除了通过
this.$route去获取路由参数,我们还能通过props获取,也可以通过函数式编程传递多个参数或定义传递逻辑
const router = new VueRouter({
routes: [{
path: '/class/:id',
component: ClassView,
// 这里需要把 props 设为 true,告诉 VueRouter 用 props 接收
props: true,
// props 设为 fn, route.params === 组件内this.$route.params
props(route) {
return {
id: route.params.id,
hhh: 456
}
}
}]
});
<script>
export default {
// 定义 props
// 函数式编程传递的hhh
props: ["id", "hhh"],
created() {
console.log(this.id);
console.log(this.hhh);
console.log(this.$route.params.id);
},
};
</script>
匹配优先级
同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高
// 路径 /,匹配 组件Home
const router = new VueRouter({
routes: [{
path: '/',
component: Home
}, {
path: '/',
component: classView
}]
});
捕获所有路由或 404 Not found 路由
-
如果想匹配任意路径,我们可以使用通配符 (
*): -
根据通配符
*和匹配优先级顺序,我们可以把path为*放在routes的最后,已达到没有找到对应路由走 404 逻辑 -
如果你使用了History 模式,请确保正确配置你的服务器
const router = new VueRouter({
routes: [{
path: '/',
component: Home
}, {
path: '/class/:id',
component: ClassView
}, {
// 以上的 URL 规则都匹配不到的情况下,走这里
path: '*',
component: NotFound
}]
});
嵌套路由
- 实际生活中的应用界面,通常由多层嵌套的组件组合而成,同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件
- 可以用
children字段去嵌套路由 - 不建议多层嵌套,使得耦合性增强,URL 也变得复杂,不好管理
children下的path可省略写;例:name === /class/:id/:name- 建议用省略写法,在父级路由改变时,不用去修改自己路由的
path,可联想相对路径 - 以
/开头的嵌套路径会被当作根路径
const router = new VueRouter({
routes: [{
path: '/class/:id',
component: ClassView,
children: [{
// /class/:id/:name
path: ':name',
component: ClassName
}]
}]
});
<template>
<div>
my name is ClassView
<!-- 不要忘了输出的组件位置 -->
<router-view></router-view>
</div>
</template>
命名视图
- 有时候我们会想同时 (同级) 展示多个视图,而不是嵌套展示
- 像具名插槽一样,我们也可以给路由进行命名从而达到同一个 URL 映射多个路由的目的
const router = new VueRouter({
routes: [{
path: '/',
// 路由配置通过 components 对象指定映射的组件
components: {
default: Home,
one: OneView,
two: twoView
}
}]
});
<template>
<div id="app">
app
<router-view></router-view>
// name 命名路由视图
<router-view name="one"></router-view>
<router-view name="two"></router-view>
</div>
</template>
重定向 和 别名
-
区别:
- 重定向:当用户访问
/a时,URL 将会被替换成/b - 别名:不会替换 URL,访问
/a就像访问/b一样
- 重定向:当用户访问
重定向
- 通过
redirect跳转去目标 URL
const router = new VueRouter({
routes: [{
path: '/home',
// 通过 redirect 从 '/home' 跳转至 '/'
redirect: '/'
}, {
path: '/',
component: Home
}]
});
别名
- 顾名思义 路由的 另一个名字
const router = new VueRouter({
routes: [{
path: '/',
// 使用 alias 设置别名
alias: '/home',
// 也可以使用数组设置多个别名
alias: ['/home1, '/home2],
component: Home
}]
});
元信息
- 我们可以通过 meta 携带一些信息,我们可以通过
$route.meta找到他们
const router = new VueRouter({
routes: [{
path: '/',
component: Home,
meta: {
info: 'any',
}
}]
});
<script>
export default {
created () {
console.log(this.$route.meta);
}
}
</script>
name
- 注意这个
name不是别名,相当于我们给这个 URL 设置了名称,在跳转处使用
const router = new VueRouter({
routes: [{
path: '/class/:id',
component: ClassView,
name: ClassView
}]
});
导航
组件
- 我们之前要跳转 URL,通常使用
a标签进行跳转,但是使用a标签进行跳转就会引起页面的重新刷新,这与我们的初衷相违背;或许我们可以通过锚点#防止页面跳转,在 hash 模式下我们当然也可以这么做,但是我们更加推荐Vue-Router的组件<router-link> <router-link>在 history 模式还是 hash 模式,它的表现行为一致,所以我们在切换路由模式的时候不许要再进行任何的改动- 在 history 模式下,
router-link会守卫点击事件,让浏览器不再重新加载页面 <router-link>也可以识别路由所命名的name,这很方便不是吗a标签用href指定跳转的 URL,而<router-link>使用to,其他与a标签无异
<template>
<div>
home
// 通过 router-link 跳转至 routes 中已定义路由
<router-link to="/class/123">go to class</router-link>
// to 可接收对象 name,对应 routes 中的 name,他会根据这个 name 跳转至对应 URL
<router-link :to="{ name: 'classView', params: { id: '123' } }">go to class</router-link>
</div>
</template>
- 更加推荐使用
name进行跳转,便于管理- URL 比
name在实际项目中更容易改变,这时我们修改起来就比较麻烦
- URL 比
- 使用
name跳转注意,如果你跳转的路由是 动态路由,需要使用params携带信息
编程式导航
- 除了使用
a标签跳转链接外,我们还会使用BOM去进行跳转操作,Vue-Router也有对应的实例方法,通过编写代码来实现
<script>
export default {
methods: {
handlePush() {
this.$router.push({ name: "classView", params: { id: "123" } });
},
handleReplace() {
this.$router.replace("/class/123");
},
},
};
</script>
-
使用
push和replace都可以跳转 URL -
push会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL,组件<router-link>就相当于内部调用push -
replace不会向 history 添加新记录,他会替换掉当前的 history 记录 -
类似于
window.history,Vue-Router也有gobackforward方法
<script>
export default {
methods: {
// 在 history 记录中向前或者后退多少步
handleGo() {
this.$router.go(-1);
},
// 在 history 记录中后退
handleBack() {
this.$router.handleBack();
},
// 在 history 记录中前进
handleForward() {
this.$router.forward();
},
},
};
</script>
导航守卫
Vue-Router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的- 导航就相当于走路,而守卫呢就是拦着路的墙,我们要根据定义的规则才能走过去
全局前置守卫
- 每个守卫方法接收三个参数
- to:即将要进入的目标路由对象
- from:当前导航正要离开的路由
- next:可以理解成
nodeKoa中间键的next,不执行next()将不跳转路由,也可以理解成Promise的reslove,不调用即不会进行下一步
router.beforeEach((to, from, next) => {
console.log('----beforeEach----');
console.log(to);
console.log(from);
console.log(next);
// next 的执行效果依赖 调用参数
next(); // 进行管道中的下一个钩子
next(false); // 中断当前的导航
// 跳转到一个不同的地址
next('/');
next({ path: '/' });
next(error); // 终止且该错误会被传递给 router.onError() 注册过的回调
})
全局后置钩子
- 跟全局前置守卫差不多,不同的是:这些钩子不会接受
next函数也不会改变导航本身
router.afterEach((to, from) => {
console.log('----afterEach----');
console.log(to);
console.log(from);
});
路由独享的守卫
- 路由配置上直接定义
beforeEnter守卫 - 这些守卫与全局前置守卫的方法参数是一样的
const router = new VueRouter({
routes: [{
path: '/',
component: Home,
beforeEnter(to, from, next) {
console.log('----beforeEnter----');
console.log(to);
console.log(from);
console.log(next);
next();
}
}]
});
组件内的守卫
- 我们也可以在路由组件内直接定义以下路由导航守卫
<script>
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
console.log("----beforeRouteEnter----");
console.log(to);
console.log(from);
console.log(next);
next(vm => {
// 可通过 vm 访问组件实例
});
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /class/:id,在 /class/1 和 /class/2 之间跳转的时候,
// 由于会渲染同样的 class 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
console.log("----beforeRouteUpdate----");
console.log(to);
console.log(from);
console.log(next);
next();
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
console.log("----beforeRouteLeave----");
console.log(to);
console.log(from);
console.log(next);
next();
},
};
</script>
完整的导航解析流程
- 这里就直接偷官网内容了,也建议大家在不懂的时候直接找 官方文档
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由模式
- 最后说说路由模式吧,
Vue-Router用两种方式实现了 URL 的跳转而不刷新页面的模式(hashhistory) vue-router默认使用 hash 模式,使用 URL 的 hash 来模拟一个完整的 URL,也就是我们熟知的锚点链接- 因为要带
#会变得很丑,我们也可以使用他的 history 模式,他利用了history.pushStateAPI
const router = new VueRouter({
// 我们可以通过 mode 来用不同的模式
mode: 'history',
routes: [...]
});
- 不过值得注意的是,history 模式需要后台配置支持,需要在服务端增加一个覆盖所有情况的候选资源:当 URL 匹配不到任何静态资源时,返回相同的页面,就是我们 app 所依赖的页面 后端配置