Vue详细介绍及使用(路由、Vuex)
一、路由
前面我们说到了Vue是单页应用,那么什么是单页应用? 单页面应用程序-SPA(Single Page Application):整个网站只有一个页面,内容的变化通过局部更新实现、同时支持浏览器地址栏的前进和后退操作,而实现SPA过程中,最核心的技术点就是前端路由。
路由是一个比较广义和抽象的概念,路由的本质就是对应关系。对于前端来说,其实浏览器配合超链接就很好的实现了路由功能。
但是对于单页面应用来说,浏览器和超级连接的跳转方式已经不能适用。而前端路由是根据不同的用户事件,显示不同的页面内容 ,本质上是用户事件与事件处理函数之间的对应关系,即在页面不刷新的前提下实现url
变化,通常有两种方式:一是利用url中的hash字段;二是使用HTML5提供的history API。
Vue为了解决页面跳转的问题,官方也给出了路由的方案: vue-router插件。
Vue框架的兼容性非常好,他也可以很好的跟其他第三方的路由框架进行结合。
1、Vue-router是什么
Vue-router是给Vue.js提供路由管理的插件,它和vue.js是深度集成的,利用hash值的变化控制动态组件的切换,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。
官网中列出包含的功能有:
- 嵌套的路线/视图映射
- 模块化,基于组件的路由器配置
- 路由参数,查询,通配符
- 查看由Vue.js过渡系统提供动力的过渡效果
- 细粒度的导航控制
- 与自动活动CSS类的链接
- HTML5历史记录模式或哈希模式,在IE9中具有自动回退
- 可自定义的滚动行为
查看官网API:https://router.vuejs.org/zh/api/#router-link,有时候总觉得不知道从哪里下手,于是花了点时间整理了一张Vue-router思维导图:
2、Vue-router基本使用
2.1、Vue-router的安装
1)新建项目stu_vueRouter
由于前面的内容比较多,这里我们安装一个新的项目,安装过程中会告诉我们要不要安装路由,这里先选择了不安装。
2)进入项目目录,安装路由npm install vue-router
安装完成后,stu_vueRouter\node_modules\目录下就可以看到有个vue-router目录
3)将stu_vueRouter导入到Visual Studio Code中,并启动成功
在第一步中我们没有直接安装vue-router和直接安装的App.vue中对组件HelloWorld.vue的引用方式不一样
<!--没有直接安装的局部注册组件-->
<template>
<div id="app">
<img src="./assets/logo.png">
<HelloWorld/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<!--直接安装路由的通过router-view引用-->
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
2.2、Vue-router的基本使用一(视图)
官网的介绍https://router.vuejs.org/guide/#html,这里我们直接使用HelloWorld.vue组件
1)引入vue-router插件并注册插件
2)配置路由:创建路由实例,并传递路由选项(每个路由都应该映射到一个组件)
3)装载根实例时,注入router选项以使整个应用程序感知路由
4)视图位置加载
main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import VueRouter from 'vue-router'
import HelloWorld from './components/HelloWorld'
// 1 引入vue-router插件并注册插件
Vue.use(VueRouter)
// 2 配置路由:创建路由实例,并传递路由选项(每个路由都应该映射到一个组件)
const router = new VueRouter({
routes: [ // 定义(路由)组件
{
path: '/',
component: HelloWorld
}
]
})
Vue.config.productionTip = false
// 3 装载根实例时,注入router选项以使整个应用程序感知路由
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
App.vue
<template>
<div id="app">
<img src="./assets/logo.png">
<!-- 4 视图位置加载 =路由出口:路由匹配到的组件将渲染在这里-->
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
vue单独提成了一个文件,router/index.js,更清晰。
2.3、Vue-router的基本使用二(跳转)
新建home.vue和about.vue,实现home、HelloWorld、about页面切换。
home.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
</div>
</template>
<script>
export default {
data: function () {
return { msg: 'Hello, vue router!' }
}
}
</script>
<style scoped>
.hm {
background: rosybrown;
}
</style>
about.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
</div>
</template>
<script>
export default {
data: function () {
return { msg: 'this is about view!' }
}
}
</script>
<style scoped>
.hm {
background: rgb(100, 142, 233);
}
</style>
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '../components/HelloWorld'
import home from '../components/home'
import about from '../components/about'
// 1 引入vue-router插件并注册插件
Vue.use(Router)
export default new Router({
routes: [
{ path: '/home', component: home },
{ path: '/hello', component: HelloWorld },
{ path: '/about', component: about }
]
})
main.js
import Vue from 'vue'
import App from './App'
import router from './router'
var topbarTemp = `
<nav>
<ul>
<li v-for="item in NavList">
<router-link :to="item.url">{{ item.name }}</router-link>
</li>
</ul>
<hr/>
</nav>
`
// 定义组件:topbar
Vue.component('top-bar', {
template: topbarTemp,
data: function () {
return {
NavList: [
{ name: '首页', url: '/home' },
{ name: 'hello', url: '/hello' },
{ name: '关于', url: '/about' }
]
}
}
})
// 定义组件 footerbar
Vue.component('footer-bar', {
template: `
<footer>
<hr/>
<p>版权所有@</p>
</footer>
`
})
// 创建service 模块
Vue.component('v-service', {
template: `<div> {{ msg }}</div>`,
data: function () {
return { msg: 'this is service view' }
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
App.vue
<template>
<div id="app">
<!-- 4 视图位置加载 =路由出口:路由匹配到的组件将渲染在这里-->
<top-bar> </top-bar>
<router-view class="view one"></router-view>
<footer-bar></footer-bar>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
ul,
li {
list-style: none;
}
ul {
overflow: hidden;
}
li {
float: left;
width: 100px;
}
h2 {
background-color: rosybrown;
}
.view {
background-color: rosybrown;
}
</style>
案例效果:
3、<router-link>的使用
<router-link>
组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to
属性指定目标地址,默认渲染成带有正确链接的 <a>
标签,可以通过配置 tag
属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。
<router-link>
比起写死的 <a href="...">
会好一些,理由如下:
- 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
- 在 HTML5 history 模式下,
router-link
会守卫点击事件,让浏览器不再重新加载页面。 - 当你在 HTML5 history 模式下使用
base
选项之后,所有的to
属性都不需要写 (基路径) 了。
<router-link> 组件的属性Props-【to、replace、append、tag、active-class、exact、event、exact-active-class、aria-current-value】
3.1、to属性的使用(必选属性)
表示目标路由的链接。当被点击后,内部会立刻把 to
的值传到 router.push()
,所以这个值可以是一个字符串或者是描述目标位置的对象。
<!-- 字符串 -->
<router-link to="home">Home</router-link>
<!-- 渲染结果 -->
<a href="home">Home</a>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>
<!-- 不写 v-bind 也可以,就像绑定别的属性一样 -->
<router-link :to="'home'">Home</router-link>
<!-- 同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
<!-- 带查询参数,下面的结果为 /register?plan=private 这个在后面参数传递中再一起介绍-->
<router-link :to="{ path: 'register', query: { plan: 'private' }}"
>Register</router-link
>
案例:
<template>
<div id="app">
<nav>
<!-- 不建议使用a标签,同样可以达到效果 -->
<a href="#/login"
class="aaa">使用a链接|</a>
<!-- router-link默认渲染为一个a标签 to 类型: string | Location to 的值传到 router.push()-->
<!-- 字符串 相当于router.push('apple')-->
<router-link to="/login">登陆-字符串|</router-link>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="myregister">注册-v-bind-属性|</router-link>
<!-- 使用 v-bind 的 JS 表达式-对象 -->
<router-link :to="{ path: '/login' }">登陆-v-bind-对象|</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'myabout' }">关于-v-bind-命名路由</router-link>
</nav>
<hr>
<!-- 视图位置加载 =路由出口:路由匹配到的组件将渲染在这里-->
<router-view></router-view>
<hr>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
mylogin: 'login',
myregister: 'register'
}
}
}
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
案例效果:
3.2、replace属性的使用
默认值: false。
设置 replace
属性的话,当点击时,会调用 router.replace()
而不是 router.push()
,于是导航后不会留下 history 记录。
<!-- 上面的案例中,当我们点击一次链接后,浏览器的前进后退按钮就被激活,可以前进或者后退,我们清空浏览记录,并且给第三个加上个replace,不会有history 记录 -->
<router-link :to="myregister"
replace>注册-v-bind-属性|</router-link>
案例效果:
3.3、append属性的使用
默认值: false。
设置 append
属性后,则在当前 (相对) 路径前添加基路径。例如,我们从 /a
导航到一个相对路径 b
,如果没有配置 append
,则路径为 /b
,如果配了,则为 /a/b
<!-- 使用 v-bind 的 JS 表达式-对象 加斜线和不加斜线的区别:不加斜线从当前路径后面添加字符-->
<router-link :to="{ path: 'login' }"
append>登陆-v-bind-对象|</router-link>
案例效果:我们先进到根目录,然后点点击,路径就在当前路径下拼接,多次拼接路径就会匹配不到了
3.4、tag属性的使用
默认值: "a"。
有时候想要 <router-link>
(默认a标签)渲染成某种标签,例如 <span>
。 于是我们使用 tag
prop 类指定何种标签,同样它还是会监听点击,触发导航。
<router-link :to="{ name: 'myabout' }"
tag="span">关于-v-bind-命名路由</router-link>
案例效果:
3.5、active-class 属性的使用-模糊匹配
默认值: "router-link-active"。
设置链接激活时使用的 CSS 类名。默认值可以通过路由的构造选项 linkActiveClass
来全局配置。(可以在路由构造中更改默认值linkActiveClass:'is-active')
<!--设置链接激活时使用的 CSS 类名-->
<router-link to="/login"
active-class="active">登陆-字符串|</router-link>
<style>
.router-link-active {
background: burlywood;
}
.active {
background: green;
}
</style>
<!--router/index.js中可更改全局默认值-->
export default new Router({
...
linkActiveClass:'is-active',
...
})
案例效果:
给第二个设置链接激活时使用的 CSS 类名为active,active-class选择样式时根据路由中的路径去匹配,发现我们没有点的另外一个链接也触发了激活样式。这是因为active-class默认是模糊匹配的,假如:/a/b上加active-class,则/a/b、/a、/都会被激活,这种模糊匹配有时候并不是我们想要的效果。vue给我们提供了另外一个属性,exact精确匹配。
3.6、exact 属性的使用-精确匹配
“是否激活”默认类名的依据是包含匹配。 举个例子,如果当前的路径是 /a
开头的,那么 <router-link to="/a">
也会被设置 CSS 类名。按照这个规则,每个路由都会激活 <router-link to="/">
!想要链接使用“精确匹配模式”,则使用 exact
属性。
<router-link to="/"
exact>默认根路径(exact)</router-link>
<router-link to="/">默认根路径</router-link>
<router-link to="/login">/login"|</router-link>
<router-link :to="{ path: '/login' }"
active-class="active">login-active-class|</router-link>
案例效果:
当我们在根目录下的时候,第一个和第二个处于激活状态,当我们点击最后一个链接的时候,第二、第三个也处于激活状态,而第一个加了exact属性,只有在精确匹配时才激活。
3.7、event属性的使用
默认值: 'click'。
声明可以用来触发导航的事件(event默认是click事件触发)。可以是一个字符串或是一个包含字符串的数组。
<nav>
<!-- event="click" 默认是click事件触发-->
<router-link :to="{ path: '/login' }">默认|</router-link>
<router-link :to="{ path: '/register' }"
event="mouseover">mouseover事件触发|</router-link>
<!-- @click.native="myclick" .native监听组件元素的原生事件-->
<router-link :to="{ path: '/login' }"
@click.native="myclick">默认@click.native|</router-link>
</nav>
<script>
export default {
name: 'App',
data () {
return {
mylogin: 'login',
myregister: 'register'
}
},
methods: {
myclick () {
alert('原生事件')
}
}
}
</script>
案例效果:
案例中将导航默认的click事件换成mouseover事件。 要支持原生事件可以使用.native修饰符。
3.8、exact-active-class属性的使用
默认值: "router-link-exact-active"。
配置当链接被精确匹配的时候应该激活的 class。注意默认值也是可以通过路由构造函数选项 linkExactActiveClass
进行全局配置的。
...
<router-link :to="{ path: '/' }">默认|</router-link>
<router-link :to="{ path: '/login' }">默认|</router-link>
<router-link :to="{ path: '/register' }"
event="mouseover">mouseover事件触发|</router-link>
...
<style>
.router-link-exact-active {
background: blue;
}
</style>
案例效果:
加上激活时样式效果,.router-link-exact-active设置背景色为蓝色。
3.9、aria-current-value属性的使用
当链接根据精确匹配规则激活时配置的 aria-current
的值。这个值应该是 ARIA 规范中允许的 aria-current 的值。
扩展:WAI-ARIA 1.1引入了一个有用的新属性:aria-current,aria的全称Accessible Rich Internet Application 无障碍富互联网应用程序,主要针对于屏幕阅读设备,更快更好地理解网页,一般是为不方便的人士提供的功能。
4、<router-view>的使用
<router-view>
组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view>
渲染的组件还可以内嵌自己的 <router-view>
,根据嵌套路径,渲染嵌套组件。其他属性 (非 router-view 使用的属性) 都直接传给渲染的组件, 很多时候,每个路由的数据都是包含在路由参数中。因为它也是个组件,所以可以配合 <transition>
和 <keep-alive>
使用。
4.1、name属性的使用
<router-view>
Props属性有一个name。如果 <router-view>
设置了名称,则会渲染对应的路由配置中 components
下的相应组件。
<router-view>作为
路由出口,如果我想实现一个经典布局(头部+侧边栏+内容区),一个出口比较难,那我想使用三个<router-view>,那怎么对应呢?这不禁让我想起了之前的具名插槽,是的,我们可以指定一个名字,叫命名视图。
4.2、命名视图的应用
我们新建header.vue、left.vue、main.vue三个组件,格式如下:
<template>
<div>
<h2>{{ msg }}</h2>
</div>
</template>
<script>
export default {
data: function () {
return { msg: '我是头部组件!' }
}
}
</script>
并且在router/index.js中配好路由
import Vue from 'vue'
import Router from 'vue-router'
import header from '../components/header'
import left from '../components/left'
import main from '../components/main'
// 1 引入vue-router插件并注册插件
Vue.use(Router)
export default new Router({
routes: [
{ path: '/', component: header },
{ path: '/', component: left },
{ path: '/', component: main }
]
})
app.vue中引用
<template>
<div id="app">
<nav>
<router-link :to="{ path: '/' }">经典布局</router-link>
</nav>
<hr>
<!-- 视图位置加载 =路由出口:路由匹配到的组件将渲染在这里-->
<router-view></router-view>
<router-view></router-view>
<router-view></router-view>
<hr>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
案例效果:
这并不是我们想要的效果,当浏览器地址栏的 URL 变化时,如果它匹配上了路径就会加载对应的组件,所以我们始终匹配的是第一个。
于是修改一下:
//router/index.js
export default new Router({
routes: [
{
path: '/',
components: {
'default': header,
'left': left,
'main': main
}
}
]
})
//App.vue
<router-view></router-view>
<router-view name="left"></router-view>
<router-view name="main"></router-view>
修改后效果:
5、Router 构建选项
当我们在router/index.js中创建路由实例的时候,我们使用了路由构建选项routes,并且也使用了一些路由配置path,component,name,components等。当然路由构建选项还有很多,除了routes,还有mode、base、linkActiveClass、LinExactActiveClass、scrollBehavior、parseQuery/stringifyQuery、fallback。
5.1、routes-选项介绍
类型: Array<RouteConfig>。RouteConfig
的类型定义:
interface RouteConfig = {
path: string,
component?: Component,
name?: string, // 命名路由
components?: { [name: string]: Component }, // 命名视图组件
redirect?: string | Location | Function,
props?: boolean | Object | Function,
alias?: string | Array<string>,
children?: Array<RouteConfig>, // 嵌套路由
beforeEnter?: (to: Route, from: Route, next: Function) => void,
meta?: any,
// 2.6.0+
caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
pathToRegexpOptions?: Object // 编译正则的选项
}
5.2、routes-name命名路由
在前面的案例中我们有使用到命名路由。在创建 Router 实例的时候,在 routes
配置中给某个路由设置名称。
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
<router-link :to="{ name: 'user'}">User</router-link>
components?: { [name: string]: Component }, // 命名视图组件,在介绍<router-view>时也介绍过了。
5.3、
routes-redirect重定向
当我们进入一个后台管理系统时,往往会有一个登陆界面。于是就可能这么配置,当我们一进页面就展示登陆。
export default new Router({
routes: [
{ path: '/', component: login },
{ path: '/login', component: login },
{ path: '/register', component: register }
]
})
案例效果:
这根路径/和/login路径都是登陆组件,这似乎看着有点怪异。这个时候我们就可以使用重定向。我们只要将根路径重定向一下。
{ path: '/', component: login, redirect: '/login' }
案例效果:
扩展:路由传参
在官网上有句话:在组件中使用 $route
会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。使用 props
将组件和路由解耦。在介绍props
之前有必要先了解下路由传参的两种方式。
使用query方式传递参数:<router-link :to="{ path: '/login?id=001&name=张三' }">登陆</router-link>
使用params方式传递参数:{ path: '/login/:id', component: login } <router-link :to="{ path: '/login/001' }">登陆</router-link>
<!-- 使用查询字符串给路由传递参数:id和name -->
<router-link :to="{ path: '/login?id=001&name=张三' }">登陆</router-link>
/* 在实例中输出一下this.$route对象 */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
created () {
console.log(this.$route)
console.log(this.$route.query.id + '==' + this.$route.query.name)
}
})
我们可以看到在 this.$route对象的query对象下保存了我们传递给路由的参数。
// router/index.js
export default new Router({
routes: [
{ path: '/login/:id/:name', component: login },
{ path: '/register', component: register }
]
})
<router-link :to="{ path: '/login/001/张三' }">登陆</router-link>
/* 在实例中输出一下this.$route对象 */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
created () {
console.log(this.$route)
console.log(this.$route.params.id)
console.log(this.$route.params.name)
}
})
案例效果:
我们打印this.$route路由信息对象,可以看到:id和:name被作为params属性,vue通过正则表达式去解析的。使用query和params方式传参,都依赖于url,在组件中使用 $route
会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。使用 props
将组件和路由解耦。
5.4、routes-props传参
RouteConfig-props支持三种类型
props?: boolean | Object | Function
布尔模式:如果 props 被设置为 true,route.params 将会被设置为组件属性。
对象模式:如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
函数模式:创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
5.4.1、 布尔模式
// router/index.js
export default new Router({
routes: [
{
path: '/login/:id/:name',
component: login,
props: true // 布尔模式 定义为 true 之后,它就会把 :id :name作为 props 传递到当前路由配置的组件里面去
},
{
path: '/register',
component: register,
props: {
default: true,
register: false // 对象模式可以指定组件设置
}
}
]
})
// login.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
{{id}} | {{name}}
</div>
</template>
<script>
export default {
props: ['id', 'name'],
data: function () {
return {
msg: '欢迎来到登陆界面!'
}
},
mounted () {
console.log(this)
}
}
</script>
案例效果:
在组件里面不需要去写 this.$route 这种写法。因为使用了 this.$route,就相当于是跟我们 vue-router 代码进行了一个耦合。
5.4.2、 对象模式
export default new Router({
routes: [
{
path: '/login/:id/:name',
component: login,
props: true // 定义为 true 之后,它就会把 :id :name作为 props 传递到当前路由配置的组件里面去
},
{
path: '/register',
component: register,
props: {
default: true,
reName: 'dxms', //对象模式 可以设置静态值,而不使用url
age: 18
}
}
]
})
// register.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
{{reName}} | {{age}}
</div>
</template>
<script>
export default {
props: ['reName', 'age'],
data: function () {
return {
msg: '欢迎来到注册界面!'
}
}
}
</script>
案例效果:
5.4.3、 函数模式
如果 props 要根据 query 进行传递呢?那么你可以声明的是一个方法,这个方法返回的是一个对象。
export default new Router({
routes: [
{
path: '/login',
component: login,
props: route => ({
id: route.query.id,
name: route.query.name
})
},
{
path: '/register',
component: register,
props: {
default: true,
reName: 'dxms',
age: 18
}
}
]
})
<router-link :to="{ path: '/login?id=001&name=张三'}">登陆</router-link>
案例效果:
5.5、routes-alias别名
/a
的别名是 /b
,意味着,当用户访问 /b
时,URL 会保持为 /b
,但是路由匹配则为 /a
,就像用户访问 /a
一样。“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
5.6、routes-children嵌套路由
实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件。比如很多软件学习网站分、购物网站,因类别多样而出现多层导航,借助 vue-router
,使用嵌套路由配置,就可以很简单地表达这种关系。新建如下组件:
router/index.js配置
import Vue from 'vue'
import Router from 'vue-router'
import FistPage from '../components/qtly/FistPage'
import Course from '../components/qtly/course'
import Java from '../components/qtly/page/java'
import Web from '../components/qtly/page/web'
// 1 引入vue-router插件并注册插件
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
component: FistPage // 首页组件
},
{
path: '/course',
component: Course, // 课程组件
redirect: '/course/java', // 加载课程组件时,默认加载出一个子组件
children: [{
path: 'java',
name: 'java',
component: Java
}, {
path: 'web',
name: 'web',
component: Web
}]
}
]
})
course.vue
<template>
<div>
<Nav></Nav>
<div class="content">
<div class="left">
课程页面目录
<ul>
<li>
<router-link to="/course/java">java</router-link>
</li>
<li>
<router-link to="/course/web">前端</router-link>
</li>
<li>android</li>
</ul>
</div>
<div class="right">
<!-- 视图区域 -->
<transition name='fade'
mode="out-in">
<router-view></router-view>
</transition>
</div>
</div>
</div>
</template>
<script>
import Nav from './Nav'
export default {
components: {
Nav
}
}
</script>
<style scoped>
.left,
.right {
float: left;
}
.left {
border: blue 1px solid;
width: 100px;
height: 180px;
}
.right {
margin-left: 10px;
}
ul {
list-style-type: none;
padding: 10px;
border: blue 1px solid;
}
ul li {
padding: 10px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
Nav.vue
<template>
<div>
<nav>
<ul>
<li>
<router-link to="/"
exact>首页</router-link>
</li>
<li>
<router-link :to="{path:'/course'}">课程</router-link>
</li>
</ul>
</nav>
</div>
</template>
<script>
export default {
name: 'Nav'
}
</script>
<style scoped>
div {
width: 100%;
height: 50px;
background: #f1f1f1;
line-height: 50px;
}
ul {
list-style: none;
}
li {
float: left;
margin: 0 20px;
}
</style>
其他
<!--FistPage.vue-->
<template>
<div>
<Nav></Nav>
主页
</div>
</template>
<script>
import Nav from './Nav'
export default {
name: 'fistPage',
components: {
Nav
}
}
</script>
<!--java.vue-->
<template>
<div>
欢迎来到Java页面
</div>
</template>
<script>
export default {
name: 'java'
}
</script>
<style scoped>
</style>
案例效果:
5.7、routes-其他
5.7.1、beforeEnter
beforeEnter?: (to: Route, from: Route, next: Function) => void进入路由之前执行的函数,比如可以做一些预判工作。
...
beforeEnter (to, from, next) {
if (from.name === 'java') {
console.log('我进来了')
}
next()
}
...
5.7.2、meta?: any
路由元信息 使用$route.meta.属性可以获取
5.7.3、caseSensitive?: boolean-版本要求2.6.0+
匹配规则是否大小写敏感?(默认值:false)
{
path: '/course',
name: 'cse',
component: Course, // 课程组件
redirect: '/course/Java', // 加载课程组件时,默认加载出一个子组件
children: [{
path: 'java',
name: 'java',
component: Java
}]
}
案例效果:
5.7.4、pathToRegexpOptions?: Object-版本要求2.6.0+
编译正则的选项
扩展:hash和history模式的由来
在了解mode选项之前,我们有必要先了解一下hash和history两种模式的由来。在浏览器中,所有的BOM和DOM对象都是宿主对象。(关于这部分,可以看我的这篇文章:https://blog.csdn.net/xiaoxianer321/article/details/108627681)BOM即浏览器对象模型,提供了与浏览器交互的方法和接口。BOM的核心对象,就是window对象,其中就有两个属性对象location和history,就是我们今天要介绍的主角。hash 模式和 history 模式都属于浏览器自身的特性。
扩展1:location对象
W3C中对location对象的描述:Location 对象存储在 Window 对象的 Location 属性中,表示那个窗口中当前显示的文档的 Web 地址,它还能控制浏览器显示的文档的位置。如果把一个含有 URL 的字符串赋予 Location 对象或它的 href 属性,浏览器就会把新的 URL 所指的文档装载进来,并显示出来。
window.location对象包含的属性和方法:
属性 | 描述 |
---|---|
hash | 设置或返回从井号 (#) 开始的 URL(锚)。 |
host | 设置或返回主机名和当前 URL 的端口号。 |
hostname | 设置或返回当前 URL 的主机名。 |
href | 设置或返回完整的 URL。 |
pathname | 设置或返回当前 URL 的路径部分。 |
port | 设置或返回当前 URL 的端口号。 |
protocol | 设置或返回当前 URL 的协议。 |
search | 设置或返回从问号 (?) 开始的 URL(查询部分)注:设置search 会直接刷新页面 |
方法 | 描述 |
---|---|
assign() | 加载新的文档。 |
reload() | 重新加载当前文档。 |
replace() | 用新的文档替换当前文档。 |
url
的组成部分有很多,如协议、主机名、资源路径、查询字段等等,其中就包含一个称之为片段的部分,以#
为标识。例如我们前面的案例:http://localhost:8080/#/course/Java中,#号后面的#/course/Java就是url的hash部分。
我们在单页应用中需要做到的是,在改变url时不刷新页面,location提供以下两种方式可以做到:
1)location.href
赋值时只改变 url 的 hash
2)
直接使用location.hash
赋值
打开控制台,输入location.href并改变url的hash部分,在浏览器未刷新的情况下切换页面内容。hash值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的进后退也能对其进行控制,所以人们在html5的history出现前,基本都是使用hash来实现前端路由的。他的特点在于:hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,改变hash不会重新加载页面。扩展2:
history对象
W3C中对location对象的描述:History 对象包含用户(在浏览器窗口中)访问过的 URL。History 对象最初设计来表示窗口的浏览历史。但出于隐私方面的原因,History 对象不再允许脚本访问已经访问过的实际 URL。唯一保持使用的功能只有 back()、forward() 和 go() 方法。
属性 | 描述 |
---|---|
length | 返回浏览器历史列表中的 URL 数量。 |
方法 | 描述 |
---|---|
back() | 加载 history 列表中的前一个 URL。 |
forward() | 加载 history 列表中的下一个 URL。 |
go() | 加载 history 列表中的某个具体页面。 |
在HTML4的时代,我们只能使用它的这几个属性和方法,而HTML5为其又添加了这2个方法:
方法 | 描述 |
---|---|
pushState(data, title [, url]) | 往历史堆栈的顶部添加一条记录。data为一个对象或null,它会在触发window的popstate事件(window.onpopstate)时,作为参数的state属性传递过去;title为页面的标题,但当前所有浏览器都忽略这个参数;url为页面的URL,不写则为当前页。 |
replaceState(data, title [, url]) | 更改当前页面的历史记录。参数同上。这种更改并不会去访问该URL。 |
查询了一下,浏览器对pushState的支持:
毕竟是html5新进的成员,如Chrome 8.0+、Firefox 4.0+...版本支持pushState,低版本的浏览器对于history API的兼容性不好,IE到10才支持这两个方法,如果想兼容老浏览器的话,可以试试History.js。
history有5个方法可以改变 url 而不刷新页面:
1)
history.pushState()
2)
history.replaceState()
3)
history.go()
4)
history.back() = history.go(-1)
5)
history.forward() = history.go(1)
案例执行过程:先清除浏览记录,第一步,使用history.pushState({},'','/course/Java'),只改变了url,页面并未发生改变;第二步,使用history.pushState({},'','/')再次改变了url;第三次使用history.replaceState({},'','/course/Java')替换url;第三步,使用history.go(-1),因为第二步使用的是替换并未产生历史记录,所以回退的是/course/Java界面;第四步,使用history.back()则回到了最初的记录,并且浏览器的后退按钮也变灰了;第五步。使用history.forward()前进一次。
案例分析:上诉案例中使用pushState和
replaceState并未使页面发生任何改变。pushState(state, title, url)
: 无刷新的向浏览器 历史最前方 加入一条记录,参数state
表示要跳转到的URL对应的状态信息(状态对象的序列化施加了640k个字符的大小限制)。history.pushState()和history.replaceState()的区别就是是否会留下痕迹,replaceState是替换当前url而不产生历史记录。
在MDN Web Docs中有这么一句话:每当处于激活状态的历史记录条目发生变化时,popstate
事件就会在对应window
对象上触发。如果当前处于激活状态的历史记录条目是由
history.pushState()
方法创建,或者由history.replaceState()方法修改过
的,则popstate事件对象的
state
属性包含了这个历史记录压缩的状态对象的一个拷贝。
当网页加载时,各浏览器对popstate
事件是否触发有不同的表现。以Chrome浏览器来测试一下:
新建:one.html、two.html、three.html
<!--one.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
我是one页面
<button onclick="changeUrl()">pushState-page1</button>
<button onclick="changeUrl2()">replaceState-page2</button>
<button onclick="changeUrl3()">back</button>
<button onclick="changeUrl4()">go(-1)</button>
</body>
</html>
<script>
function changeUrl(){
history.pushState({page: 1},'','./two.html')
}
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state))
};
function changeUrl2(){
history.replaceState({page: 2},'','./three.html')
}
function changeUrl3(){
history.back()
}
function changeUrl4(){
history.go(-1)
}
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state))
};
</script>
<!--two/three.html页面都类似如下-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
我是two页面
</body>
</html>
案例效果:
调用history.pushState()
或者history.replaceState()
不会触发popstate事件. popstate
事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()
方法),此外,a 标签的锚点也会触发该事件。你也可能也会想到替换URL可能会是很危险的,这里做了跨域限制。
5.8、mode选项
-
类型:
string
-
默认值:
"hash" (浏览器环境) | "abstract" (Node.js 环境)
-
可选值:
"hash" | "history" | "abstract"
配置路由模式:
-
hash
: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。 -
history
: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。 -
abstract
: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
-
路由的默认模式是hash,要配置成history,只要加上在路由实例中加上mode选项。
export default new Router({
mode: 'history',
...
})
5.8.1、hash和history的最直观区别
hash模式url中会带有#号。例如:http://localhost:8080/#/course/Java
而history模式url中不会带有#号。例如:http://localhost:8080/course/Java
5.8.2、hash和history访问路由匹配不上时的错误页面处理不同
hash模式下,仅hash符号之前的内容会被包含在请求中,如 http://localhost:8080/ 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;
history模式下,前端的url必须和实际后端发起请求的url一致,如http://localhost:8080/course/Java 。如果后端缺少对/course/Java的路由处理,将返回404错误。
正如官网所说:不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问就会返回 404。同时官网也给出了很多后端配置例子,如Apache、nginx、Node.js等等的配置。我们这测试项目因为是使用webpack的配置,访问不到默认访问的是跟路径,所以没有出现404。
5.9、base选项
-
类型:
string
-
默认值:
"/"
应用的基路径。例如,如果整个单页应用服务在
/app/
下,然后base
就应该设为"/app/"
。
export default new Router({
mode: 'history',
base: '/app/', // 配置单页应用的基路径
...
}
这样我们访问:http://localhost:8080/app/ 和 http://localhost:8080/ 访问的效果是一样的。
5.10、linkActiveClass选项
-
类型:
string
-
默认值:
"router-link-active"
全局配置
<router-link>
默认的激活的 class。
5.11、linkExactActiveClass选项
-
类型:
string
-
默认值:
"router-link-exact-active"
全局配置
<router-link>
默认的精确激活的 class。
前面我们在介绍<router-link>组件时,链接激活时使用的 CSS 类名,我们可以在路由选项中更改他的默认值。vue给我们的默认值是router-link-active/router-link-exact-active,同时也提供了选项给我们重命名的功能。
5.12、scrollBehavior选项
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router
能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState
的浏览器中可用。
案例:
//给路由加上scrollBehavior滚动行为选项
export default new Router({
mode: 'history',
scrollBehavior (to, from, savedPosition) {
console.log(to) // 要进入的目标路由对象,到哪里去
console.log(from) // 离开的路由对象,从哪儿来
console.log(savedPosition) // 会记录滚动条的坐标,点击"后退/前进" 时的记录值
if (savedPosition) {
return savedPosition // 期望滚动到哪个的位置
} else {
return { x: 0, y: 0 } // 期望滚动到哪个的位置
}
}
...
})
//给course.vue的<div class="content">加上一个超长的高度,出现滚动条
.content {
height: 2500px;
border: blue 1px solid;
}
案例效果:
如果:savedPosition捕获到了坐标就会记住之前前进或者后退时的位置。关于异步滚动、平滑滚动可以参考官网的说明。
5.13、parseQuery / stringifyQuery选项
-
类型:
Function
提供自定义查询字符串的解析/反解析函数。覆盖默认行为。如果有特定需求,可以借助这两个配置项,定制 query 信息。
5.14、fallback
-
类型:
boolean
-
默认值:
true
当浏览器不支持
history.pushState
控制路由是否应该回退到hash
模式。默认值为true
。在 IE9 中,设置为
false
会使得每个router-link
导航都触发整页刷新。它可用于工作在 IE9 下的服务端渲染应用,因为一个 hash 模式的 URL 并不支持服务端渲染。
6、Router 实例属性
6.1、router.app
-
类型:
Vue instance
配置了
router
的 Vue 根实例。
6.2、router.mode
-
类型:
string
路由使用的模式。
6.3、router.currentRoute
-
类型:
Route
当前路由对应的路由信息对象。
6.4、router.START_LOCATION
-
类型:
Route
以路由对象的格式展示初始路由地址,即路由开始的地方。可用在导航守卫中以区分初始导航。
7、Router 实例方法
7.1、全局前置守卫-router.beforeEach(to, from, next)
跳转之前,可以做的事情。每个守卫方法接收三个参数:
-
to: Route
: 即将要进入的目标 路由对象 -
from: Route
: 当前导航正要离开的路由 -
next: Function
: 一定要调用该方法来 resolve 这个钩子。执行效果依赖next
方法的调用参数。-
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 -
next(false)
: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。 -
next('/')
或者next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在router-link
的to
prop 或router.push
中的选项。 -
next(error)
: (2.4.0+) 如果传入next
的参数是一个Error
实例,则导航会被终止且该错误会被传递给router.onError()
注册过的回调。
-
7.2、全局解析守卫-router.beforeResolve(to, from, next)
和全局前置守卫类似,区别是在跳转被确认之前,同时在所有组件内守卫和异步路由组件都被解析之后,解析守卫才调用。
7.3、全局后置守卫-router.afterEach(to, from)
跳转之后,可以做的事情。
扩展:导航守卫
路由呢主要就是用来解决url跳转问题的,vue-router也实现了类似拦截器的一个功能实时监控路由跳转时的过程,在路由跳转的各个过程执行相应的操作,类似于生命周期函数。说白了就是在地址栏跳转之前、之后、跳转的瞬间我们可以干点什么。这就是官方说的:导航守卫。
导航守卫可以分为:全局守卫(在路由实例对象上注册使用)、路由独享守卫(在路由配置项中项定义)、组件内守卫(在组件属性中定义)。全局守卫即上面提供的三个实例方法:router.beforeEach、router.beforeResolve、router.afterEach。以一张图来展示导航守卫也许更清晰:
案例:
//在main.js中对路由对象加上
router.beforeEach((to, from, next) => {
console.log('全局前置守卫:beforeEach==')
next()
})
router.beforeResolve((to, from, next) => {
console.log('全局解析守卫:beforeResolve==')
next()
})
router.afterEach((to, from) => {
console.log('全局后置守卫:beforeResolve==')
})
//在router/index.js中对course导航加上
beforeEnter: (to, from, next) => {
console.log('路由独享守卫:beforeEnter==')
next()
}
//在course.vue组件中加上
beforeRouteEnter (to, from, next) {
console.log('组件内守卫:beforeRouteEnter==')
next()
},
beforeRouteUpdate (to, from, next) {
console.log('组件内守卫:beforeRouteUpdate==')
next()
},
beforeRouteLeave (to, from, next) {
console.log('组件内守卫:beforeRouteLeave==')
next()
}
案例效果:
我们可以看到:依次执行的是:全局前置守卫:beforeEach-》 路由独享守卫:beforeEnter-》 组件内守卫:beforeRouteEnter-》全局解析守卫:beforeResolve-》全局后置守卫:beforeResolve当离开课程组件时,组件内守卫:beforeRouteLeave。而首页组件,会加载全局守卫。整个执行过程大致可以总结为:
7.4、router.push
router.push(location, onComplete?, onAbort?) / router.push(location).then(onComplete).catch(onAbort)
用来导向特殊页面。
//语法:
1、字符串形式,值为路径
router.push('home')
2、对象形式:含有path的对象形式、
router.push({ path: 'home' })
3、对象形式:命名的路由(含有name和params属性传递参数)--注:由于动态路由也是传递params,所以path不能和params一起使用,否则params将无效。
router.push({ name: 'user', params: { userId: '123' }})
4、对象形式:带查询参数 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
7.5、router.replace
router.replace(location, onComplete?, onAbort?) / router.replace(location).then(onComplete).catch(onAbort)
跟 router.push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
7.6、router.go
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
。
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
7.7、router.back
router.back()
回退一步。类似 window.history.
back()
。
7.8、router.forward
router.forward()
前进一步。类似 window.history.
forward()
。
你也许注意到 router.push
、 router.replace
和 router.go
跟 window.history.pushState
、 window.history.replaceState
和 window.history.go
好像, 实际上它们确实是效仿 window.history
API 的。
扩展:编程式的导航
通过VueRouter实例的push()操作,通过编写代码来实现路由跳转,前面我们使用 <router-link>
组件创建 a 标签来定义导航链接,它绑定的是click事件,最后也是通过执行push()方法来进行路由跳转的。使用<router-link>导航,官方称之为声明式导航,而使用
push()等方法的导航称之为编程式导航——就是通过写js代码来实现页面的跳转。这样的好处是使得导航更为灵活,但是我们使用已有组件<router-link>实现导航则更为方便。
假如我们需要动态去加载页面。就像我们去一个购物网站,看中喜欢的商品,点击购买的时候,如果是没有登陆,会直接跳转到登陆页面一样。这里编程式导航则更为灵活。
案例:
login.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
<button @click="lg">登陆</button>
</div>
</template>
<script>
export default {
data: function () {
return {
msg: '欢迎来到登陆界面!'
}
},
methods: {
lg () {
// 默认登陆成功
// this.$router.push({ name: 'prd', params: { id: '123', name: '买家' } })
this.$router.push({ name: 'prd', query: { id: 123, name: '买家' } })
}
}
}
</script>
<style scoped>
</style>
product.vue
<template>
<div class="hm">
<h2>{{ msg }}</h2>
<img src="/static/avatar.jpg">
<div @click="buyit">购买</div>
</div>
</template>
<script>
export default {
data: function () {
return {
msg: '蜡笔小新联名衣服'
}
},
methods: {
buyit () {
console.log('params.id:' + this.$route.params.id)
console.log('query.id:' + this.$route.query.id)
if (this.$route.params.id === undefined && this.$route.query.id === undefined) {
this.$router.push({ name: 'lg' })
} else {
alert('购买成功')
}
}
}
}
</script>
<style scoped>
</style>
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import product from '../components/product'
import login from '../components/login'
// 1 引入vue-router插件并注册插件
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/', // 默认页面
redirect: '/product'
},
{
path: '/product',
component: product, // 产品页面
name: 'prd'
},
{
path: '/login',
component: login, // 登陆页面
name: 'lg'
}
]
})
App.vue
<template>
<div id="app">
<!-- 视图区域 -->
<div>我是购物网站主页</div>
<router-view></router-view>
</div>
</template>
<script>
import product from './components/product'
export default {
name: 'App',
components: {
product
}
}
</script>
<style>
</style>
案例效果:
7.9、router.getMatchedComponents
const matchedComponents: Array<Component> = router.getMatchedComponents(location?)
返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。
7.10、router.resolve
解析目标位置 (格式和 <router-link>
的 to
prop 一样)。
router.resolve(location, current?, append?)
current 是当前默认的路由 (通常你不需要改变它)
append 允许你在 current 路由上附加路径 (如同 router-link)
案例
let routeData = router.resolve({
path: '/product',
query: { id: 123, name: '买家' }
})
console.log('router.resolve()的返回值:', routeData)
案例效果:
7.11、router.addRoute
1、addRoute(route: RouteConfig): () => void
添加一条新路由规则。如果该路由规则有 name,并且已经存在一个与之相同的名字,则会覆盖它。
2、addRoute(parentName: string, route: RouteConfig): () => void
添加一条新的路由规则记录作为现有路由的子路由。如果该路由规则有 name,并且已经存在一个与之相同的名字,则会覆盖它。
案例:
//main.js
const about = { // 接口返回路由信息
path: '/about',
name: 'About',
component: () => import('./components/about.vue')
}
router.addRoute(about) // 添加到路由
//product.vue
<span @click="xq">详情</span><br><br>
...
methods: {
...
xq () {
this.$router.push({ name: 'About' })
}
}
案例效果:
7.12、router.getRoutes
getRoutes(): RouteRecord[]
获取所有活跃的路由记录列表。注意只有文档中记录下来的 property 才被视为公共 API,避免使用任何其它 property,例如 regex,因为它在 Vue Router 4 中不存在。
7.13、router.onReady
router.onReady(callback, [errorCallback])
该方法把一个回调排队,在路由完成初始导航时调用,这意味着它可以解析所有的异步进入钩子和路由初始化相关联的异步组件。
这可以有效确保服务端渲染时服务端和客户端输出的一致。
第二个参数 errorCallback 只在 2.4+ 支持。它会在初始化路由解析运行出错 (比如解析一个异步组件失败) 时被调用。
7.14、router.onError
router.onError(callback)
注册一个回调,该回调会在路由导航过程中出错时被调用。注意被调用的错误必须是下列情形中的一种:
错误在一个路由守卫函数中被同步抛出;
错误在一个路由守卫函数中通过调用 next(err) 的方式异步捕获并处理;
渲染一个路由的过程中,需要尝试解析一个异步组件时发生错误。
8、路由对象($route)
前面我们说到了$router路由器对象, 它包含一些操作路由的功能函数, 来实现编程式导航。而$route是当前路由对象,保存了一些当前的路由信息。后面我们都称之为当前路由对象。
官网对它的描述是:$router是一个路由对象 (route object) 表示当前激活的路由的状态信息,包含了当前 URL 解析得到的信息,还有 URL 匹配到的路由记录 (route records)。路由对象是不可变 (immutable) 的,每次成功的导航后都会产生一个新的对象。
8.1、$route.path
类型: string
字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"
。
8.2、$route.params
类型: Object
一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。
8.3、$route.query
类型: Object
一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1
,则有 $route.query.user == 1
,如果没有查询参数,则是个空对象。
8.4、$route.hash
类型: string
当前路由的 hash 值 (带 #
) ,如果没有 hash 值,则为空字符串。
8.5、$route.fullPath
类型: string
完成解析后的 URL,包含查询参数和 hash 的完整路径。
8.6、$route.matched
类型: Array<RouteRecord>
一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes
配置数组中的对象副本 (还有在 children
数组)。
8.7、$route.name
当前路由的名称,如果有的话。
8.8、$route.redirectedFrom
如果存在重定向,即为重定向来源的路由的名字。
案例:
// product.vue
...
mounted () {
console.log(this.$route.path)
console.log(this.$route.name)
console.log(this.$route.fullPath)
console.log(this.$route.matched)
}
...
案例效果:
9、Axios在Vue中的应用
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。Vue.js 2.0 版本推荐使用 axios 来完成 ajax 请求。
9.1、什么是Ajax
说到Ajax,可能即使你不是一个前端开发者,都应该有所耳闻了。因为它实在是太受欢迎了。
那么到底什么是Ajax呢?可能我们已经有了答案:异步请求,局部刷新。就这么简单?是的。
在 AJAX 被发明之前,浏览器是怎么发起请求的?
1)url请求:例如,用户在地址栏输入 http://baidu.com ,按回车,就向 http://baidu.com 发起了一个请求。(同时页面刷新)
2)a 标签:用户点击页面中的 a 链接,也会发起一个请求。(同时页面刷新)
这些请求都导致页面刷新,每当用户向服务器发送请求,哪怕只是需要更新一点点的局部内容,服务器都会将整个页面进行刷新。这样用户的操作页面会中断,对应用户体验也是一个极大的缺陷。
后来微软极具创新意识地提供了一个私有接口 ActiveXObject("Microsoft.XMLHTTP"),并在 IE 5.0 中开放给开发者用。Gmail 的开发者发现这个接口之后,2004年开始在Gmail和Google Maps引入这个概念时,它获得了更广泛的认可。直到2005年2月Jesse James Garrett在一篇文章中提出来AJAX,是Asynchronous JavaScript XML(异步JavaScript 和XML)的简称,Ajax提供与服务器异步通信的能力,一个最简单的应用是无需刷新整个页面而在网页中更新一部分数据。并且,直到今天它依然很受欢迎。
XMLHttpRequest对象是Ajax中最重要的一个对象。使用Ajax更多的是编写客户端代码。传统的web前端与后端的交互中,浏览器直接访问Tomcat的Servlet来获取数据。Servlet通过转发把数据发送给浏览器。当我们使用AJAX之后,浏览器是先把请求发送到XMLHttpRequest异步对象之中,异步对象对请求进行封装,然后再与发送给服务器。服务器并不是以转发的方式响应,而是以流的方式把数据返回给浏览器XMLHttpRequest异步对象会不停监听服务器状态的变化,得到服务器返回的数据,就展示到浏览器上。
编写一个Ajax程序:
1)创建XMLHttpRequest对象
if(window.XMLHttpRequest) {
//在IE6以上的版本以及其他内核的浏览器(Mozilla)等
httpRequest = new XMLHttpRequest();
}else if(window.ActiveXObject) {
//在IE6以下的版本
httpRequest = new ActiveXObject();
}
2)创建http请求
httpRequest.open("GET/POST", "Servlet1", true);
3)指定回调函数
httpRequest.onreadystatechange = myresponse
4)发送http请求
httpRequest.send("info...");
5)回调函数得到http返回的内容,并展示内容
function myresponse() {
//判断请求状态码是否是4【数据接收完成】
if(httpRequest.readyState==4) {
//再判断状态码是否为200【200是成功的】
if(httpRequest.status==200) {
//得到服务端返回的文本数据
var text = httpRequest.responseText;
//把服务端返回的数据写在div上
var div = document.getElementById("result");
div.innerText = text;
}
}
}
9.2、Axios的使用
中文文档地址:https://www.kancloud.cn/yunye/axios/234845或http://www.axios-js.com/zh-cn/docs/
GitHub地址:https://github.com/axios/axios
使用步骤:
1、安装 npm install axios
2、引入加载
import Axios from 'axios'
Vue.prototype.$axios = Axios // Axios挂载在原型上,Axios不支持Vue.use(axios)
3、使用
this.$axios.get(url).then(res => {
...
}).catch(error => {
...
})
开发环境中直接使用会报跨域问题:
什么是跨域?指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略:是指协议(protocol),域名(主机host),端口(port)都要相同,其中有一个不同都会产生跨域,在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
例如:
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js | 同一域名下 | 允许 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js | 同一域名下不同文件夹 | 允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名,不同端口 | 不允许 |
http://www.a.com/a.js https://www.a.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.a.com/a.js http://70.32.92.74/b.js | 域名和域名对应ip | 不允许 |
http://www.a.com/a.js http://script.a.com/b.js | 主域相同,子域不同 | 不允许 |
http://www.a.com/a.js http://a.com/b.js | 同一域名,不同二级域名(同上) | 不允许(cookie这种情况下也不允许访问) |
http://www.cnblogs.com/a.js http://www.a.com/b.js | 不同域名 | 不允许 |
解决方案:跨域的解决办法有很多,比如script标签
、jsonp
、后端设置cros
等等。axios不支持jsonp,我们需要使用http-proxy-middleware中间件做代理,使用文档地址:https://codechina.csdn.net/mirrors/chimurai/http-proxy-middleware
1)安装 http-proxy-middleware
npm install --save-dev http-proxy-middleware
安装完成后:在config/index.js中找到proxyTable,加上如下配置
proxyTable: {
'/api': {
target: 'https://www.baidu.com/', // 目标接口域名
changeOrigin: true, // 是否跨域
pathRewrite: { // /api开头的地址,通过node服务(服务端与服务端不存在跨域),转发到https://www.baidu.com/
'^/api': ''
}
}
}
在App.vue中使用axios
<template>
<div id="app">
<div>Axios获取参数</div>
<div v-if="isshow">{{getInfo}}</div>
<div v-else>{{getError}}</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
getInfo: '',
getError: '',
isshow: true
}
},
created () {
const url = '/api'
this.$axios.get(url).then(res => {
console.log(res)
this.getInfo = res.data
this.isshow = true
}).catch(error => {
console.log(error)
this.getError = error
this.isshow = false
})
}
}
</script>
案例效果:
实现了跨域请求获取百度主页信息。
二、VueX详解
前面我们在学习组件间通信的时候,当父组件向子组件传递数据,使用 props 传递数据。子组件向父组件传递数据,通过 events(自定义事件-回调参数) 传递数据。两个同级组件(兄弟组件)之间传递数据,可以共享父类或者通过 EventBus (事件总线-只适用于极小的项目)实现。当然官方,还提供了一个数据共享的方式——Vuex,它是一个专为 Vue.js 应用程序开发的状态管理模式。
官网对Vuex的介绍也十分详细:https://vuex.vuejs.org/zh/
1、什么是Vuex
官方的定义:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
这个状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
整个过程可以描述为:view->(dispatch)Action->(Comit)Mutations->(Mutate)State->View 注意:Action不是必需品,如果有异步操作才需要Action。
概念看多了总感觉有点茫茫然。我们可以把vuex 比作是一个“前端的数据库“,State 就是数据库。Mutations 就是我们把数据存入数据库的 API,用来修改state的。Actions是做数据处理的,拿到了数据,处理完了再存到数据库中当然你也可以不做处理,直接丢到数据库。如果需要从State 中取数据则使用Getters从数据库(state)中取。
什么情况下使用Vuex?Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。
2、Vuex的使用
1、安装
npm install vuex --save
或者
yarn add vuex
2、引用
import Vuex from 'vuex'
Vue.use(Vuex)
3、创建一个store
const store = new Vuex.Store({
state: {
count: 0
}
...
})
案例:
1)新建一个store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// Vuex 应用的核心就是 store(仓库)Vuex 和单纯的全局对象有以下两点不同:
// 1、Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
// 2、你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
export default new Vuex.Store({
state: {
count: 10
},
mutations: { // 一组方法,是改变store中状态的执行者。
increment (state) {
state.count++
}
}
})
2)在实例中注入store,在main.js中注入
import Vue from 'vue'
import App from './App'
import router from './router/index'
import store from './store/index'
Vue.config.productionTip = false // 关闭提示
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store, // 注入到实例中
components: { App },
template: '<App/>'
})
3)新建vuexstore/parent.vue
<template>
<div class="bt">
<h2>
我是父亲组件
</h2>
Count={{getCount}}
<button @click="addCount">addCount</button>
<child></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'father',
data () {
return {
}
},
components: {
child
},
computed: {
getCount () {
return this.$store.state.count
}
},
methods: {
addCount () {
this.$store.commit('increment') // store.commit 方法触发状态变更
}
}
}
</script>
<style scoped>
.bt {
width: 300px;
height: 500px;
border: blue 1px solid;
float: left;
}
</style>
4)新建vuexstore/child.vue
<template>
<div class="bt">
<h2>
我是儿子组件
</h2>
Count = {{getCount}}
</div>
</template>
<script>
export default {
name: 'child',
data () {
return {
}
},
computed: {
getCount () {
return this.$store.state.count
}
}
}
</script>
<style scoped>
.bt {
width: 300px;
height: 300px;
border: green 1px solid;
margin-top: 100px;
}
</style>
5)新建vuexstore/brother.vue
<template>
<div class="bt">
<h2>
我是兄弟组件
</h2>
Count = {{getCount}}
</div>
</template>
<script>
export default {
name: 'borther',
data () {
return {
}
},
computed: {
getCount () {
return this.$store.state.count
}
}
}
</script>
<style scoped>
.bt {
width: 300px;
height: 500px;
border: red 1px solid;
float: right;
}
</style>
案例效果:
Vuex 和单纯的全局对象不同,Vuex 的状态存储是响应式的。通过提交 (commit) mutation改变 store 中的状态,会立刻在视图中展示出来。
3、Vuex核心概念
3.1、State
Vuex 使用单一状态树(State)。用一个对象就包含了全部的应用层级状态。state存储了应用中需要共享的状态,为了能让Vue组件在State更改后也随之改变,需要基于state创建计算属性。State可以比作是一个状态仓库(数据库)。
3.2、Getters
从state中获取数据。类似于Vue中的计算属性,可以在所依赖的其他state或者getter改变后自动改变,每个getter方法接受state和其他getters作为前两个参数。可以比作是从State状态仓库(数据库)中取数据的方法。
// store/index.js中新增decreate方法
export default new Vuex.Store({
state: {
count: 10
},
mutations: { // 一组方法,是改变store中状态的执行者。
increment (state) {
state.count++
},
decreate (state) {
state.count--
}
}
})
// parent.vue-methods中新增decCount方法
<button @click="decCount">addCount</button><br>
decCount () {
this.$store.commit('decreate')
}
案例效果:
我们发现count被减成了负数。
//store/index.js 中新增getters选项,并且新增一个getState方法
export default new Vuex.Store({
state: {
count: 10
},
mutations: { // 一组方法,是改变store中状态的执行者。
increment (state) {
state.count++
},
decreate (state) {
state.count--
}
},
getters: {
getState (state) {
return state.count > 0 ? state.count : 0
}
}
})
//parent.vue将getCount换成this.$store.getters.getState
...
computed: {
getCount () {
return this.$store.getters.getState
}
}
...
案例效果:
父组件从state中获取数据,并且增加了一个大于0的判断。
3.3、Mutations
一组方法,是改变store中状态的执行者。mutations用于同步更改状态。第一个参数是state,后面其他参数是发起mutation时传入的参数。可以比作是把数据存入数据库的API。
使用常量替代 Mutation 事件类型
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
3.4、Action
一组方法,其中可以包含异步操作。想要异步的更改状态,需要使用action。action并不直接改变state,而是发起mutation。mutation必须同步执行。Action 通过 store.dispatch
方法触发。
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// Vuex 应用的核心就是 store(仓库)Vuex 和单纯的全局对象有以下两点不同:
// 1、Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
// 2、你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
export default new Vuex.Store({
state: {
count: 10
},
mutations: { // 一组方法,是改变store中状态的执行者。
increment (state) {
state.count++
},
decreate (state) {
state.count--
}
},
getters: {
getState (state) {
return state.count > 0 ? state.count : 0
}
},
actions: { // 在 action 内部执行异步 setTimeout 1秒后执行
// context:承上启下
increment (context) {
setTimeout(function () {
context.commit('increment')
}, 1000)
},
decreate (context) {
setTimeout(function () {
context.commit('decreate')
}, 1000)
}
}
})
//parent.vue
...
methods: {
addCount () {
// this.$store.commit('increment') // store.commit 方法触发mutations状态变更
this.$store.dispatch('increment') // action 使用dispatch触发action 的处理函数
},
decCount () {
// this.$store.commit('decreate')
this.$store.dispatch('decreate')
}
}
...
案例效果:
addCount/decCount没有立即执行,而是间隔一秒后再执行。
3.5、Modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得复杂是,store对象可能变得相当臃肿。为了解决这个问题,Vuex允许我们将store分割成模块。每个模块拥有自己的state、motation、action、getter。甚至是嵌套子模块。
//语法:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
1)store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// Vuex 应用的核心就是 store(仓库)Vuex 和单纯的全局对象有以下两点不同:
// 1、Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
// 2、你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
const modeleA = {
state () { // 类似Vue中的data,使用对象
return {
count: 10
}
},
mutations: { // 一组方法,是改变store中状态的执行者。
increment (state) {
state.count++
},
decreate (state) {
state.count--
}
},
getters: {
getState (state) {
return state.count > 0 ? state.count : 0
}
},
actions: { // 在 action 内部执行异步 setTimeout 1秒后执行
// context:承上启下
increment (context) {
setTimeout(function () {
context.commit('increment')
}, 1000)
},
decreate (context) {
setTimeout(function () {
context.commit('decreate')
}, 1000)
}
}
}
export default new Vuex.Store({
modules: {
a: modeleA
}
})
2)child.vue/brother.vue
...
<script>
export default {
name: 'child',
data () {
return {
}
},
computed: {
getCount () {
return this.$store.state.a.count
}
}
}
</script>
...
案例效果:
Vuex允许我们将store分割成模块。每个模块拥有自己的state、motation、action、getter。甚至是嵌套子模块。
4、辅助函数的使用
引入vuex 以后,我们需要在state中定义变量,类似于vue中的data,通过state来存放状态,然后我们在组件中定义方法来触发状态变更,虽然引入的时候挺方便了,但是组件中定义的代码还是很多而这时候vuex又给我们提供了更简便的方法,即辅助函数mapState、mapGetters、mapMutations、mapActions来简化组件中的冗余代码。
4.1、mapState
使用mapState辅助函数可以帮助我们生成计算属性。
4.2、mapGetters
使用mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性。
4.3、mapMutations
使用mapMutations辅助函数将组件中的methods映射为store.commit调用(需要在根节点注入store)。
4.4、mapActions
使用mapActions辅助函数将组件的methods映射为store.dispath调用(需要在根节点注入store)。
案例:
1)parent.vue中引入mapActions并使用
<template>
<div class="bt">
<h2>
我是父亲组件
</h2>
Count={{getCount}}<br>
<button @click="increment">addCount</button><br>
<button @click="decreate">decCount</button><br>
<child></child>
</div>
</template>
<script>
import child from './child'
import { mapActions } from 'vuex' // 引入mapActions
export default {
name: 'father',
data () {
return {
}
},
components: {
child
},
computed: {
getCount () {
return this.$store.getters.getState
}
},
methods: {
// addCount () {
// // this.$store.commit('increment') // store.commit 方法触发mutations状态变更
// this.$store.dispatch('increment') // action 使用dispatch触发action 的处理函数
// },
// decCount () {
// // this.$store.commit('decreate')
// this.$store.dispatch('decreate')
// }
...mapActions(['increment', 'decreate'])
// 相当于以下代码
// increment(){
// this.$store.commit('increment')
// }
// decreate(){
// this.$store.commit('decreate')
// }
}
}
</script>
<style scoped>
.bt {
width: 300px;
height: 500px;
border: blue 1px solid;
float: left;
}
</style>
案例效果:
...mapActions(['increment', 'decreate']) 简化了methods中的方法。