14.vue-router 3.x
路由安装的时候不是必须的,可以等到使用的时候再装,如果之前没有安装的话,可以再单独安装。之前的终端命令行不要关闭,再重新开一个,还需要再package.json文件的依赖中添加。
如果忘记之前是否有安装过,可以在package.json文件中查看依赖是否有router。
vue是单页面应用(SPA,在一个页面中切换)。
路由:简单来说就是路径跟视图的对应关系(一个路径对应一个视图)。vue中的切换都是基于路由去切换的。
安装
npm install vue-router
//安装指定版本
npm install vue-router@3.2.0
npm i jquery --save(缩写-S)生产依赖 --save-dev(缩写-D)开发依赖 将jquery安装并保存到生产依赖或开发依赖中
而现在不用这么麻烦了,直接npm install jquery 直接自动保存到生产依赖中,如果需要保存到开发依赖中还是需要像之前一样
卸载:npm uninstall jquery
使用步骤
在使用组件名之前都需要引入组件所在的文件。
在 router 文件夹下的 index.js 文件中:
1.引入vue、 vue-router import Vue from 'vue'
import VueRouter from 'vue-router'
2.注册组件VueRouter,Vue.use(VueRouter)
(因为这是为vue量身打造的,只要不是vue的核心并且是为vue量身定做的才可以注册。如vue-router和vuex)
3.定义路由配置项(哪个路径对应哪个视图)。------重点。①路由的核心就是路径与视图的对应关系:使用路由懒加载的方式引入。②路由代表的不是整个页面,而是需要变动的某一块页面,需要在最大的组件App.vue中给路由视图留出位置。<router-view></router-view>
。
//在router文件下的index.js文件中使用组件名之前是要引入组件
import MyHome from '../views/MyHome'
import MyAbout from '../views/MyAbout'
//定义路由配置项:路径和组件视图
const routes=[
{
path:'/',
component:'MyHome'
},
{
path:'/about',
component:'MyAbout'
}
]
<!-- 在App.vue文件中合适的位置留出路由视图的位置 -->
<router-view></router-view>
component: 组件名 在文件顶部引入组件文件
4.创建 VueRouter 实例并导出,然后 在main.js的根组件中导入,传入配置项,挂载到实例上。
实战中使用路由的步骤(只是需要自己修改的部分,每个路由文件都需要的步骤不包括,如果是面试题还是上面的步骤):
1、在路由文件夹下的index.js文件下引入需要的组件文件;
或者一开始不引入,使用懒加载(建议采用该方法)
2、定义路由配置项:路径和视图的对应关系。
3、在App.vue文件中留出合适的路由视图渲染的位置。
<router-view></router-view>
const routes=[
{
path:'/',
component:组件名1
//使用懒加载的方法
//component:()=>import("路径")
},
{
path:'/about',
component:组件名2
},....
]
//使用案例:router文件夹下的index.js文件
import Vue from 'vue' //引入vue
import VueRouter from 'vue-router' //引入vue-router
//import MyHome from '../views/MyHome' //1 引入组件或使用路由懒加载
//import MyAbout from '../views/MyAbout'
Vue.use(VueRouter)//注册组件,给vue定制的,只要不是vue本身的东西使用的时候都得注册
//2 定义路由配置项:路径和视图的对应关系
const routes = [
{
path: '/', //默认路径开始显示的页面
// component: MyHome //组件名,前提是引入组件
//使用路由懒加载
component:()=>import('../views/MyHome.vue')
},
{
path: '/about',//路径不可重复,以 / 开头
//component:MyAbout
//使用路由懒加载
component:()=>import('../views/MyAbout')
}
]
//创建一个路由实例,导出去,在根组件main.js中引入,根配置项中才能使用路由
const router = new VueRouter({
routes
})
export default router
<!-- app.vue文件 -->
<template>
<div>
<h1>{{msg}}</h1>
<my-info></my-info>
<router-view></router-view> <!-- 3 路由视图就渲染在此-->
</div>
</template>
<script>
import MyInfo from './views/MyInfo.vue' //引入的组件
export default {
data(){
return {
msg:"我是App根组件"
}
},
components:{MyInfo}
}
</script>
<style scoped>
h1{
color: blue;
}
</style>
路由的两个核心
(1)定义路由配置项
(2)定义路由视图的显示位置(router-view)
路径定义规则:路径以斜线打头
页面与路由一定是一一对应的,一个页面一定要对应路由,否则找不到
**路由的懒加载:**之前是先把组件导入,不管用不用都把组件加载了,那些可能用不上的组件就会发生性能浪费。使用路由的懒加载的话就先不加载组件,等访问哪个页面再去加载哪个页面对应的路由。
component: () => import('../views/About.vue')
//只有访问该页面时才会调用该函数,加载
关于路径的写法
import Abc from "./demo"
1.如果当前目录下有demo文件夹,那么是去引入的demo文件夹下的index.js
2.如果不存在demo文件夹,会优先去找当前目录下的demo.vue文件
3.如果不存在.vue文件,那么会优先查找当前目录下的demo.js文件
路径中@
代表的是src目录,本质上是因为webpack内部配置的
嵌套路由
在整个项目系统中一进来就已经分了两个一级路由了:登陆注册页面和主页面,如果是没有登录注册就显示登录注册页面,如果登陆过了就显示主页面,这两个页面中显示一个。
二级路由是点击两边的导航而切换的页面。
注意点:每一级路由都必须要有该级路由的router-view。
1.哪个组件还有子路由,那么需要在该组件内部定义router-view,代表该组件的子路由显示在该位置上
2.子路由的配置,定义在父路由的children属性下,配置内容同父路由
子路由如果不加斜线,那么默认在上级路由上叠加,如果加了斜线,那么前面必须带上 上级路由。
//配置路由项
const routes=[
{
path:'/home',
name:"Home",//name属性叫什么都可以,但是不能重名,在路由跳转的时候会用到
component:()=>import("@/views/MyHome"),
children:[{
path:'/home/about',
name:"About",
component:()=>import("@/views/MyAbout")
}]
},
{
path:'/login',
name:"Login",
component:()=>import("@views/MyLogin")
}
]
//2 在父组件MyHome中找到路由试图router-view的位置。
路由的跳转
需要路由配置项的name属性
1.js形式的跳转:当用户请求成功/事件时的自动跳转
如果看到了某个方法是以$打头的,基本可以断定,该方法是Vue根实例的方法。
this.$router.push('字符串路径 /home/about ') //this指向的是当前组件
this.$router.push({ path: '/home/about' })
this.$router.push({ name: 'About'})
2.标签形式的跳转:当用户点击时的跳转情况,类似html中的a标签
<router-link to="/home/about">跳转 </router-link>
// 1.to 可以是字符串路径 还可以是对象(但需要是js的执行环境,加':') 2.:to="{path:'路径'}" 3.{name:"名字"}
//路由嵌套少的话使用路径较好,但是如果是嵌套情况较多的话建议是使用name跳转
3.浏览器的跳转:前提是浏览器要有记录
// 在浏览器记录中前进一步,等同于 history.forward()
this.$router.go(1)
// 后退一步记录,等同于 history.back()
this.$router.go(-1)
// 前进 3 步记录
this.$router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
this.$router.go(-100)
this.$router.go(100)
动态路由
多个路由匹配同一个页面,动态不写死。例如教务系统中查询成绩时的 www.qinghua. search / 20220101和www. qinghua. search/20220102,前半部分一样,都是查询页面。
使用场景:多个不同的路径匹配同样的页面。(也能够实现跨组件传递数据,拿到动态路径的名字id,但是这样的话就会出现必须传值的情况。)
动态路由匹配 在路径后面加/:名字
。这样在输入路径的时候名字可以是任意的,他们都对应的是同一个页面。
如果想要拿到地址的名字内容,在另一个页面中使用的话,使用$route.params.名字
(至于前面加不加this,如果是在html中写的则不加,如果是在js的methods中写的则要加)即可。
【this.$router拿到的是根路由实例,this.$route拿到的是当前路由实例。
】
如:
{ path: '/users/:id', component: User },
//可以同时存在多个动态路由
{ path: '/users/:id/:no', component: User },
注意只改后面动态路径参数是不会重新触发组件生命周期的。
但是如果页面中用到了动态路由的数据,那么切换动态路径的时候,是会触发组件更新的生命周期beforeUpdate和updated的(视图需要重新渲染)。
当有动态路径发生改变,想要在动态路径改变时做事情,组件生命周期是不会发生变化的,(只有页面中用到了动态路由的数据时才会发生变化,这样就很不稳定),但是使用导航守卫中的组件内的守卫钩子beforeRouterUpdaete是能够检测到的,然后在里面写我们需要做的事情就行了。
动态路由的跳转:
1.this.$router.push({ name: 'user', params: { userId: '123' }}) //使用这种方法很方便,动态路径可以将params的值设置为变量
2.this.$router.push({ path: '/user/123' })//如果用这个的话还不如用下面的那个
2.this.$router.push( '/user/123')
//下面的写法是错误的,path和parmas不能一起写,一起写会导致不知道哪一个是动态路径了
router.push({ path: '/user', params: { userId: '123' }})//如果提供了 path,params 会被忽略
同样的规则适用于router-link标签
查询参数
不需要使用动态路径的。
使用场景:用于跨组件传递数据。(实现页面跳转并且把数据带过去在另一个页面中使用)
查询参数的跳转的时候直接在地址后面拼接?key=value
,不用像动态路由那样修改路由配置项。
router.push({ path: '/register', query: { plan: 'private' }})
router.push({ path: '/register?key=value'}})
router.push("/register?key=value&key2=value2")
接收数据的页面使用该数据时:this.$route.query.key
。
注意 name和params配套
path和query 配套(因为路径后面没有冲突,直接就是数据)
动态路由和查询参数的相同点:都能实现跨页面传递数据;
不同点:
1.跳转方式:动态路由 name params;查询参数 path query.
2.动态路由更多的用于多个路径匹配同一个页面中;查询参数更多用于跨组件传递数据。
而且使用查询参数不需要在路由中定义东西的,不需要改路由配置项,而动态路由需要先改路由表
/:id
。3.动态路由传递参数不方便,不管用不用都必须传值。
注意:query查询参数传递的数据不要是引用数据类型的数据,只能传基本类型的数据,如果非要想传对象,可以将其转为字符串类型的数据。
捕获所有路由
访问的路由不存在,匹配不上时,显示的是404页面时使用。
{
// 会匹配所有路径
path: '*',
component:()=>import("@/views/NotFound")
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。
新版本不限制 通配符的存放位置,也就是说就算是放在最开始,其他路由也是能匹配上的,不过不推荐
组件路由对象
在组件中通过this.$route拿到当前组件的路由信息
{name:"组件的名字",fullPath:"完整路径",path:"路径",matched:[匹配上的所有路由,一级路由匹配上的是一个,嵌套路由匹配上的是多个],meta:"路由元信息",params:"动态路径参数",query:"查询参数"}
重定向和别名
重定向:“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b。
在路由配置项中添加redirect:"要跳转的路径".
const routes =[
{
path:'/home',
name:"Home",
redirect:'/login', //重定向:加了这个一进入home页面就会自动跳转到login页面,URL发生了改变
alias:'/abc', //别名:URL没有发生改变,通过这两个名字都能访问到 使用的时候很少
component:()=>import ('@/views/MyHome'),
children:[
{
path:'about',
name:'About',
component:()=>import('@/views/MyAbout')
}
]
},
]
重定向使用场景:如淘宝在未登录时购买物品一直会跳转 到登录页面。// 或者 对于管理系统而言,一级路由是登录页面和主界面,在进入主界面之后只显示头部和侧导航栏,中间的内容是不显示的,这时如果使用重定向,在进入主界面之后就自动显示二级路由的一部分内容。 // 或者 输入网址后但进入的网站的网址是另一个的情况,网址有风险的情况。
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
别名:/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样. 使用的时候很少
在路由配置项中添加:alias:"/abc"
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
别名的使用场景:不用按照子父级关系定义路径了,之前子路由的路径得在父路由路径的基础上添加,使用别名后,可以直接写子路由的路径,不用再受父级路径的影响了,但是新版本修改后 不用别名就已经支持了。
路由组件传参(针对动态路由而言的)
可以将params直接映射到组件的属性中。
步骤:将路由组件中的路由配置项的属性props设置为true,这样动态路径就会作为参数传入到组件中,然后在组件中接收一下就可以直接用 。
使用动态路由this.$route.params.id
之前的做法,能拿但是很麻烦:
const User = {
template: '<div>User {{ $route.params.id }}</div>'
//2 在组件中要拿到id
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
//1 动态路由/:id
})
用了组件传参后:
const User = {
props: ['id',"num"],
//2 在相应的组件中的js中(不是放在data中,与data同级)接收一下传过来的props后可直接使用
template: '<div>User {{ id }} {{ num }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id/:num', component: User, props: true },
//1 将路由组件中路由配置项的属性props设置为true,这样动态路径就会作为属性参数传入到组件中,然后在组件中接收一下就可以直接用
]
})
props的来源:现在props可能是父-子传值中父组件传过来的,也有可能是路由中带的。
路由模式
默认的路由模式是hash模式,在创建项目的时候有问是否选择history模式,我们当时选择的是N。
hash模式的特点:URL地址栏是拼接在#后面的,#是一定会在的,即使是删掉也会在。hash模式丑,实际中都不使用。
改路由模式的方法:在路由文件创建路由对象时添加一个mode属性
const router=new VueRouter({
routes,
mode:"history" //将路由模式改为history模式
})
hash模式和history模式:
(面试题)history模式使用时存在的最大问题:是会导致刷新404的问题。
原因:hash模式是基于hash值来做的,#后面的hash值是不会发往后台的,那是前端自己定义的路由自己用的,发送请求查找服务器时是会忽略掉#后面的内容的,是按照#前面的内容查找的,是能成功查找到的;而history模式是按照完整的路径地址查找的,刷新时会按照完整的路径去后台服务器中查找,但是由于这个地址时前端定义的,在服务器是没有这个路径的,是无法在后台找到的,就会发生找不到页面404。(如hash模式的http://localhost:8080/#/home在向服务器发送请求时发送的地址只是#前面的http://localhost:8080/,是能够成功找到的;而history模式发送请求时使用的是完整的地址http://localhost:8080/home,该地址在服务器中是找不到的,出现404。要解决这个问题就需要后端配置nginx反向代理做重定向,当请求的路径不存在时重定向到首页。)
当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
要解决这种问题就需要后端做一个nginx
做一个反向代理,做重定向,如果请求的路径不存在,重定向到首页。----这个问题是由后端配置的,
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
如果面试官继续往下问是怎么做重定向的?那就回答是由后端做nginx代理做的,不是前端做的。
导航守卫/路由守卫
限制哪些能跳转,哪些不能跳转。
使用场景:如淘宝中未登录就支付是不可以的,有路由守卫拦下来。/ 或者权限管控。
全局前置守卫
全局:访问项目中的任何一个路由都会拦下来;前置:在还没跳转之前就拦下来。
使用场景:一般用来做整个项目的管控,如未登录时要求要先去登录。
beforeEach不用主动去调用,当有组件之间的跳转时就会自动的触发。
//在router文件夹下的index.js文件中创建的路由实例
const router = new VueRouter({ ... })
//beforeEach方法是我们创建的路由实例router的方法
router.beforeEach((to, from, next) => {
//根据判断条件能否进入到下一个页面
})
to: Route
: 即将要进入的目标 路由对象 (路由对象就是this.$route拿到当前组件的路由信息)from: Route
: 当前导航正要离开的路由next: Function
: 一定要调用该方法来 resolve 这个钩子。执行效果依赖next
方法的调用参数。—next方法需要主动调用。下面第一种和第三种是用的最多的。next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。主动调用next()表示放行,直接往下进入下一个组件。----------------使用较多的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()
注册过的回调。
注意避免死循环,下面的写法是死循环
let flag = false;//存储的是登录的状态
//全局前置守卫
router.beforeEach((to, from, next) => {
if(flag){
next();//直接放行
}else{
next("/login") 跳转到登录页面。在未登录状态时会发生死循环
}
})
先看是否登录,未登录要去登录页面,而想要去登录页面又要检查是否登录,一看未登录就要去登录页面,而主只要想去其他页面就要进行检查,这样就陷入了死循环。
解决方法:在状态是未登录,需要转向登陆页面时做一个判断,判断当前是否是要去登录页面,如果是的话放行,否则还是得要去登陆页面。加个验证就ok了。
let flag = false;//存储的是登录的状态
//全局前置守卫
router.beforeEach((to, from, next) => {
if (flag) {
next();//直接放行
} else {
//在是未登录状态需要转向登录页面时加一个验证:如果是要去登录页面的,就让他过去,否则如果是要去其他页面的话不让过,还是需要去登陆页面
if (to.path == '/login') {
next()
} else {
next('/login');//跳转到登录页面
}
}
})
全局解析守卫(用的很少)
在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
全局前置守卫和全局解析守卫只是守卫的时机不同。全局前置守卫是在进大门的时候检查,而全局解析守卫是在进入大门之后再进入每一个小门的时候检查。
全局后置钩子
钩子:在特定的情况下会自动运行的函数。
跳转到某个页面之后需要做事情,一般可以用组件的生命周期来写。全局后置钩子与组件自己的生命周期类似,一般就不用了。
//和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
路由独享的守卫
不需要给全局加守卫,只给其中某些重要的页面加守卫。beforeEnter:(to,from,next)=>{}
//在路由配置项中写的
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫
在对应的组件.vue中写。
export default {
mounted(){
console.log(this.$route)
},
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
//使用场景:当有动态路径发生改变,想要在动态路径改变时做事情,组件生命周期是不会发生不会的,但是使用组件内的守卫是会发生变化的。
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
//进入该路由时执行,该路由中参数改变时执行,离开该路由时执行。
完整的导航解析流程
全局守卫先于局部守卫的执行。
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由元信息
定义路由的时候可以配置 meta 字段。通过this.$route
拿到meta信息。
const routes = [
{
path: '/home',
name: "Home",
meta:{a:"123"},//meta路由元信息
component: () => import('@/views/MyHome'),
children: [
{
path: 'about',
name: 'About',
alias: '/abc',
meta:{auth:"boss"},//meta路由元信息,权限,判断当前组件的meta是否是boss
component: () => import('@/views/MyAbout'),
}
]
},
]
使用场景:meta可以设置一些跟当前路由相关的自定义信息,比如页面标题,面包屑导航,页面权限等。
滚动行为
由于页面内容较多出现了滚动条,上面的内容不是我们想要看的,下面才是,我们想要跳转页面过来后直接定位到想要看的部分,不想手动向下滑动。
有滚动条的前提下才生效,没有滚动条不生效
在路由实例中加:
const router = new VueRouter({
routes,
//更改路由模式
mode: "history",
//滚动行为
scrollBehavior(to, from) {
//to from是路由对象 我们不想所有的页面都滚动到相同的位置,to用于判断去到的是a页面滚到这里,如果去到的是b页面滚动到那里
console.log(to, from)
// return 期望滚动到哪个的位置 x是离左边距离 y是离顶部距离
//有滚动条的前提下才生效,没有滚动条不生效
return { x: 0, y: 1000 }
}
})
vue-router总结
//router文件夹下的index.js文件
import Vue from 'vue' //引入vue
import VueRouter from 'vue-router' //引入vue-router
Vue.use(VueRouter)//注册组件,给vue定制的,只要不是vue本身的东西使用的时候都得注册
//路由配置项
const routes = [
{
path: "/home",
name: "Home",//路由跳转的时候会用到
redirect: "/home/about",//重定向
alias: "/b",//别名
meta:{auth:"boss"},//路由元信息,像页面标题、面包屑导航、页面权限
component: () => import("@/views/MyHome"), //路由懒加载的方式引入
beforeEnter: (to, from, next) => {
// ...
console.log(to,from,next)
},//路由独享的守卫
children: [
{
path: "about",
name: "About",
component: () => import("@/views/MyAbout"),
//children:[]
//还需要在MyHome父组件中写出<router-view></router-view>的位置
}
]
},
{
//动态路由的内容
path: "/login/:id",//动态路由 如果想要在组件中拿到id的内容:1.this.$route.params.id 2.使用路由组件传参:在路由配置项的对应路由中加属性pross:true,然后再对应组件.vue的js中接受一下props:['id','no'],这样就可以直接使用id和no了
name: "Login",
component: () => import("@/views/MyLogin"),
//props:true
},
{
//捕获所有路由 404的处理
path: "*",
component: () => import("@/views/NotFound")
}
]
//创建路由实例,导出到main.js
const router = new VueRouter({
routes,
mode: "history",//更改路由模式
//滚动行为
scrollBehavior(to, from) {
//to from是路由对象 我们不想所有的页面都滚动到相同的位置,to用于判断去到的是a页面滚到这里,如果去到的是b页面滚动到那里
console.log(to, from)
// return 期望滚动到哪个的位置 x是离左边距离 y是离顶部距离
//有滚动条的前提下才生效,没有滚动条不生效
return { x: 0, y: 1000 }
}
})
// 全局前置守卫
let flag = false;//存储的是登录的状态
router.beforeEach((to, from, next) => {
if (flag) {
next()
// console.log(from)
} else {
//注意:发生死循环情况的解决,加一个验证就ok了
if (to.path == '/login') {
next()
} else {
next('/login')
}
}
})
//全局后置钩子
/* router.afterEach((to, from) => {
console.log(to, from)
}) */
export default router
关于路由跳转:
1.普通路由的跳转:
1.js形式的跳转:
this.$router.push("/login")
`this.$router.push({path:"/login"}) ` `this.$router.push({name:"Login"})`
2.标签形式的跳转:
<router-link to="/login"> 跳转 </router-link>
`<router-link :to="{path:'/login'}"> 跳转 </router-link>` `<router-link :to="{name:'Login'}> 跳转 </router-link> `
3.浏览器形式的跳转:
this.$router.go(1)
` this.$router.go(-1)`
2.动态路由的跳转:需要修改路由配置项
/:id/:no
name—parmas1.js形式的跳转:
this.$router.push("/login/123/456")
` this.$router.push({path:"/login/123/456"}) ` ` this.$router.push({name:"Login",params:{id:'123',no:'456'}})`
2.标签形式的跳转:与js形式类似
3.查询参数的跳转: 不需要修改路由配置项 path—query
1.js形式的跳转:
this.$router.push("/login?key=value&key2=vlaue2")
`this.$router.push({path:"/login?key=value&key2=vlaue2"}) ` ` this.$router.push({path:"/login",query:{key:value,key2:value2}})`
2.标签形式的跳转:与js形式类似
(面试题)实现跨页面的数据传递的方法有哪些:
- 动态路由:parmas;
this.$route.params.名字
- 查询参数:query;(更建议使用的)
this.$route.query.名字
- 本地存储:在一个页面存储数据,在另一个页面取数据(没有产生跨域)。(如果使用的话建议使用sessionStorage,这样页面关闭之后数据就会消失,不会产生日积月累使用导致存满的情况) sessionStorage.setItem()和sessionStorage.getItem().
- 路由组件传参:实际上还是动态路由的方法,只是接收取数据的方式不一样。将路由组件中的路由配置项的属性props设置为true,这样动态路径就会作为参数传入到组件中,然后在组件中的props(与data同级)接收一下就可以直接用 。
15.Axios
前后端交互使用ajax,但是原生的ajax很复杂麻烦不想用,后来有了jquery的ajax简单了许多,在vue中也可以使用jquery的ajax,但是jquery的 包很大,而且jq的ajax有回调地狱的问题(后来使用promise来解决异步的问题),因此基于这些在vue中就不适用jq的ajax了,而是使用axios。
axios并不是vue中的内容,也不是vue独有的,react也可以使用.
安装
- Axios是一个基于Promise封装的一个http库,可以用于浏览器和node.js中
- Axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样
安装:npm install axios
.
使用
1.引入:import axios from "axios"
。(想在那个页面中使用,就在哪个页面中引入即可,不是所有的页面都需要发请求的。)
但在实际项目中许多页面都需要发送请求,我们不想要每次用都要引入axios,可以将引入axios放在main.js文件中的根实例上,但是axios不是Vue的配置项不能route一样写在里面,我们可以将axios挂载在Vue的原型上,这样创建的每一个Vue实例都可以使用axios了。----------使用时this.$axios({})
import axios from "axios"
Vue.prototype.$axios=axios;//加$是为了区分是根实例的方法
【不需要像VueRouter一样注册,axios不仅在vue中使用,在react中也是可以使用的。】
2.发送get/post请求。
axios请求返回的结果是一个Promise对象,并将请求成功的数据直接resolve出来。如果请求失败,写在then的第二个参数中。
请求返回的结果拿到的数据就是JSON格式,不需要再转成JSON。
想要一进来就发请求,可以将请求写在生命周期creacted中。
发送get请求
axios({
method: "get", // (默认是get请求)
url: "/user/12345",
params: {//请求参数
firstName: "Fred",
lastName: "Flintstone"
}
})
.then((res)=>{
//res就是请求成功返回的结果
},(err)=>{
//err就是请求失败返回的结果
});
快捷发送get请求:
//axios.get(url,{params:{}})
axios.get("/user/12345",{
params:{
firstName: "Fred",
lastName: "Flintstone"
}
}).then((res)=>{
//res是请求返回的数据
})
发送post请求
axios({
method:"post",
url:"/user/12345",
data:{
firstName: "Fred",
lastName: "Flintstone"
}
}).then((res)=>{
//res就是请求成功返回的数据
})
快捷发送post请求:
//axios.get(url,{})
axios.get("/user/12345",{
firstName: "Fred",
lastName: "Flintstone"
}).then((res)=>{
//res是请求返回的数据
})
注意使用get请求和post请求时的请求方式中method不加s。(注意与单文件组件中的函数方法methds相区分)
get请求和post请求不同的地方:
1.method由"get"变为"post";
2.传送的数据由params变为data.
3.快捷发送请求中,发送请求参数时:get请求是个对象 {params:{key1:value1}};而post请求不用data直接写参数 {key1:value1}.
axios请求案例
高德地图开放平台的天气查询
<template>
<div>
<h1>我是axios组件</h1>
</div>
</template>
<script>
import axios from "axios"
export default {
created(){
axios({
method:'get',
url:"https://restapi.amap.com/v3/weather/weatherInfo",
params:{
key:"e17d487bab9ea0753d34762321979db4",
city:"南宫"
}
}).then((res)=>{
//res就是返回来的数据
console.log(res)
})
}
}
</script>
<style scoped>
</style>
自定义axios实例
不用上面现成的axios,自己创建一个.
axios.create({})返回的是一个axios实例,以后就发送请求都用这个,在所有页面中通用,不用发送一个请求创建一个。将自定义axios实例的方法单独放在一个文件夹中。
axios.create({
//配置项
})
关于配置项:
以下配置中,只有url是必须的,如果没有配置method,将默认使用get方法。
url和method每个页面都是不一样的,在封装axios的时候不将其放进去。
- url: ‘/user’: 用户请求服务器资源的 URL
- method: ‘post’: 创建请求时使用的方法
- baseURL: 'http://demo-domain:8080/api/':** 自动加在URL(非绝对路径时)前的路径,所有的请求在发送的时候会自动添加该前缀。**
- headers: { ‘x-Requested-With’: ‘XMLHttpRequest’}: 自定义请求头
- params: { ‘ID’: ‘12345’ }: 与请求一起发送的URL参数,当method指定为GET时使用
- data: { ‘name’: ‘zhangfs’ }: 请求主体参数,用于PUT,POST,PATCH方法
- timeout: 8000: 请求超时时间,0表示无超时;超过时间请求被中断
- withCredentials: false: //表示跨域请求时是否需要使用凭证,是true的时候,开启withCredentials后,服务器才能拿到你的cookie,当然后端服务器也要设置允许你获取你开启了才有用。
拦截器
请求拦截器
axios.interceptors.request
请求拦截器:在数据请求之前有一个函数给拦下来。只要发任何请求就会执行。
希望每次请求之前做什么事,在请求发送之前需要验证一些信息。例如在每次请求之前需要先拦下来看看是否登录了。
与导航守卫相似:导航守卫拦截的是导航跳转的情况, 拦截器拦截的是发送请求的情况。
拦截器是全局的,加了拦截器之后只要发送请求都会拦下来做验证。
// 注意,这里面有 2 个函数——分别是成功和失败时的回调函数
axios.interceptors.request.use(function(config) {
// 发起请求前执行一些处理任务
return config; // 返回配置信息
}, function (error) {
// 请求错误时的处理
return Promise.reject(error);
});
config是一个对象,就是请求的时候的一些配置信息,可以在拦截器中更改一些参数,像请求头、请求参数等。必须返回config配置信息,这个请求才能发出去。
响应拦截器
axios.interceptors.response
响应拦截器:数据回来之前有一个函数给拦下来。
例如:天气查询时返回的数据先要判断是否存在,再做后续的操作,这时可以使用响应拦截器。
axios.interceptors.response.use(function(response) {
// 处理响应数据
return response; // 返回响应数据
}, function (error) {
// 响应出错后所做的处理工作
return Promise.reject(error);
});
response就是请求返回来的数据。
关于本地存储
属于js的知识,但在vue的项目中使用很多,在实际工作中使用的价值还是很高的。
一般情况下数据是存放在数据库中的,但是在一些情况下还是需要存储在本地的,如记住登录时记住密码,如果密码是存在数据库中的话换一台电脑密码是会被记住的,而实际上是是在当前电脑上记住了。/ 或者网站浏览的历史记录。
三种浏览器存储数据的方式:localStroage
、sessionStorage
和cookie
.
任何本地存储的数据是不能跨域访问的。
本地存储的数据在浏览器中是可以看到的,在右键检查–Application/应用—左侧的Storage中有LocalStorage、SessionStorage、cookies点开之后有存储的key和value。
1.sessionStorage和localStorage
sessionStorage和localStorage存储的值只能是字符串类型的,如果想要存储其他类型的需要将其使用 JSON.stringfy()
转为字符串。
sessionStorage只要关闭页面存储的数据就会消失;localStorage的数据不删除是永久不会消失的。
//1.保存数据
sessionStorage.setItem('key','value')
localStorage.setItem('key','value')
//2.获取数据
sessionStorage.getItem('key')
localStorage.getItem('key')
//3.删除数据
sessionStorage.removeItem('key')
localStorage.removeItem('key')
//4.清空数据
sessionStorage.clear()
localStorage.clear()
2.cookie
// 设置cookie
document.cookie = "username=orochiz"
document.cookie = "age=20"
// 读取cookie
var msg = document.cookie
console.log(msg) // username=orochiz;age=20
// 添加过期时间(单位:天)
var d = new Date() // 当前时间
var days = 3 // 3天
d.setDate(d.getDate() + days)
document.cookie ="username=orochiz;"+"expires="+d
// 删除cookie (给某个键值对设置过期的时间)
d.setDate(d.getDate() - 1)
console.log(document.cookie)
cookie的创建比较麻烦,在项目中cookie的操作都是基于第三方插件来实现的,因为存储时只能存储字符串形式的"key=value",在取值时拿到的也是字符串类型的,想要拿到key和value很麻烦还需要以"=“和”;"切割等复杂程序。
cookie不是主动删除的,是在一段时间后自己过期删除的。
3. (高频面试题)sessionStroage、LocalStorage和cookie的异同点。
- 存储时间有效期不同:1、cookie的有效期是可以设置的,默认的情况下是关闭浏览器后失效;2、sessionStorage的有效期是仅保持在当前页面,关闭当前会话页或者浏览器后就会失效;3、localStorage的有效期是在不进行手动删除的情况下是一直有效的。sessionStroage:关闭页面就失效;LocalStorage:不删除永久不失效;cookie:超过规定时间就失效。
- 存储大小不同:1、cookie的存储是4kb左右,存储量较小,一般页面最多存储20条左右信息;2、localStorage和sessionStorage的存储容量是5Mb(官方介绍,可能和浏览器有部分差异性)cookie:小;sessionStorage和Local Storage更大一些;
- 与服务端的通信:1、cookie会参与到与服务端的通信中,一般会携带在http请求的头部中,例如一些关键密匙验证等。2、localStorage和sessionStorage是单纯的前端存储,不参与与服务端的通信cookie会在请求头中跟随请求一同发往服务器的。sessionStorage和Local Storage纯前端,不参与服务的的通信。
vuex不是永久存储,一刷新就没有了,只是临时过度的。如果想要刷新浏览器不丢失数据只有存储到本地存储LocalStorage、SessionStorage、cookies或数据库中。
vue跨域:请求代理配置
配置在vue.config.js文件中,vue的其他配置。
每次需要改的地方只有target的内容,在写代理的路径时要注意是几个页面前面共同的部分路径。
注意:只要改了请求代理的配置,必须重启服务(ctrl+c,Y), 并且更改axios的url为’/api’ ! !!!
devServer: {
proxy: { //配置跨域
"/api": {
target: "https://api.binstd.com/recipe", //每次需要更改的地方,填写请求目标的地址
//ws:true,
changeOrigin: true,//允许跨域
pathRewrite: {
'^/api': '' //请求的时候使用这个api就可以
}
}
}
}
// devServer:请求本地/开发服务器
// proxy:/ˈprɒksi/ 代理
// target:存放用来想请求的路径地址
// pathRewrite:路径重写
// '^/api' :^代表以什么开头,以/api开头,重写为空,因为在真正发送请求的时候路径中是不存在/api的
// 用 /api来代替想要请求的地址,/api/search相当于https://api.binstd.com/recipe/search
也可以不叫’/api’,随便都行,只是约定俗成而已。
15.综合案例:使用vue-router+axios实现美食项目
项目前的准备
使用现成的接口:极速数据,专门用来提供数据服务的。
直接使用脚手架创建项目,从vue create 项目名称
开始,
与之前不同的地方:
1.没选css预处理器
2.选择history模式
项目创建好之后需要做的步骤:
-
删除不要的文件:
1.components文件夹下的全部文件; 2.views文件夹下的全部文件; 3. App.vue只剩下router-view; 4.router文件夹下的index.js中的引入和路由配置项全部删掉。
2.创建分析项目时需要考虑的步骤:
1.先分析路由结构:一级路由--登录和主页;二级路由--主页中的点击导航栏中的内容。
美食项目的路由结构:不需要登陆,只需要一个主页面、一个详情页,点击菜之后进入详情页,这两个页面是同级路由,即两个一级路由(因为详情页不是在主页的基础上再显示,子父级关系是子级在父级的基础上显示内容),没有嵌套。
将页面转换为移动端的页面:右键检查页面左侧有一个标志,点击之后就变为了移动端的显示方式,在上方可以调节手机型号和尺寸100%。
3.安装axios.
项目实现过程
极速数据
appkey:34daa84d8da46182
还能用:299dbc7685b99190
url:https://api.jisuapi.com/recipe/
定义完了.vue组件之后下一步就要定义路由。
一、最上面是搜索框:只要使用了搜索框下一步一定是双向绑定。
1.调整搜索框的大小:在移动端中不要直接给定大小,要使用响应式布局。(最简单最常用的是百分比,还可以是flex布局和rem布局)
2.搜索框输入内容后,回车,显示结果。
(1)回车需要按键修饰符:@keyup.enter="search".
(2)发送请求需要安装axios:npm install axios
,由于不只是一个页面需要axios,将引入axios的语句放在main.js文件中,并将axios挂载到Vue的原型上,这样创建的所有的Vue实例都能使用了(父类原型上的所有属性,他的子类全部都可以使用)。(像router是vue特有的属性,可以放在根实例中的配置项,而axios不是vue的配置项不能直接写在根实例中)-------使用时this.$axios({})
import axios from "axios"
Vue.prototype.$axios=axios;//加$是为了区分是根实例的方法
(3)发送请求从"http://localhost:8080/"到https://api.binstd.com/recipe/search会产生跨域问题(浏览器与服务器之间才会产生跨域)。
在vue和react中最常用的**解决跨域问题**的方法:请求代理。先把浏览器将请求发送给本地服务器中(这二者之间不产生跨域),再由本地服务器发送给远程服务器(服务器与服务器之间不存在跨域)。
**请求代理配置**方法:在vue.config.js文件中加入以下配置,将axios中的url的内容改为'/api/search',并重启服务器。访问/api就当对于访问[https://api.binstd.com/recipe/search了。](https://api.binstd.com/recipe/search了。)
devServer: {
proxy: {
"/api": {
target: "https://api.binstd.com/recipe", //每次需要更改的地方
changeOrigin: true,//允许跨域
pathRewrite: {
'^/api': '' //请求的时候使用这个api就可以
}
}
}
}
3.在搜索框输入内容直接跳转到详情页展示内容。
搜索框回车后转到详情页,在详情页发送请求,由于在详情页可以通过搜索框发送的请求和点击分类发送的请求所需的接口参数不一样,因此需要判断需要进行哪一个的请求:我采用的方法是给搜索框跨页面传递参数query的时候加一个flag,在详情页接收的时候判断是否有flag属性,若有的话执行搜索框的请求,若没有执行分类的请求。(如果二者都传递过去flag的属性,会被覆盖)
二、清除浏览器自带的margin和padding,不能将这些样式写在某一个文件中的style中,由于scoped这样会只在该文件中起作用。
解决方法:在src下新建一个文件夹common,在commom文件夹下新建一个文件叫common.css,将项目中共用的样式写在里面;再将common.css文件引入到main.js中,import "./common/common.css"
直接引入整个文件。
项目中共用的样式有:清除margin和padding、清除浮动、类别li样式、去掉a标签的默认样式和颜色。
*{margin:0;padding:0;}
li{list-style:none;}
.fl{float:left;}
.fr{float:right;}
.clear{clear:both;}
a{color:#333;text-decoration:none;}
三、一进入到主页,在用户自己输入之前是会有一部分内容的推荐的。在页面一创建就推荐,使用生命周期created或者mounted。
1.请求的地址与前面的不同,封装的请求代理的地址是几个页面共同的前半部分,这一块在发送请求时的路径是 /api/class。所有请求的地址都有前缀/api,这就让我们想到了自定义axios实例,封装baseURL。(先不做,暂时不要求规范)
2.每个发送请求都需要使用参数appkey,想要使用请求拦截器。(先不做,暂时不要求规范)
3.使用v-for循环创建推荐内容。
4.页面一进去下面不是空白的,默认的推荐是按照功效来分的。
5.点击上面的分类推荐,显示不同的分类。
四、点击下面具体的分类,跳转到详情页,在详情页发送查询详情的请求,需要传递参数classsId.
1.注意是在主页发送请求,数据返回到主页,再由主页将数据带到详情页?还是在详情页再发送详情页请求?
一定是要在进入到详情页之后再发送请求,在主页发送请求的话数据又不在主页中用,还得带过去,麻烦死了,于是干脆直接在详情页发送请求。
2.点击分类时,需要跳转到详情页,并且把查询详情时所需要的参数数据传送到详情页,使用参数查询实现跨页面传递数据。
3.点击分类后,一进入详情页就发送请求显示详情的内容,将发送请求写在生命周期函数created中。
4.在实际工作中请求返回的数据在赋值前一定要先判断是否为空(就像之前天气查询返回来的数据为空的情况),一般后端都会给返回一个status表示是否有数据。(先不做,暂时不要求规范)
5.菜品名称、图片、简介、原材料、烹饪步骤
如果没有id当key,可以使用name做key,如果还是没有,也可以使用index.
6.使用 {{}} 在内容中会出现带标签的,不识别标签,我们可以使用识别标签的v-html
将内容渲染到页面中。
在整个项目中都会用到到,不用每次用每次都引入的:
1.common.css文件;
2.axios挂载到Vue原型上;
代码
common文件夹下的common.css
*{
padding: 0;
margin: 0;
}
li{
list-style: none;
}
.fl{
float: left;
}
.fr{
float: right;
}
.clear{
clear: both;
}
a{
color: #333;
text-decoration: none;
}
roouter文件夹下的index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: () => import("@/views/HomeView")
},
{
path:"/detail",
name:"detail",
component:()=>import("../views/TypeDetail")
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
views文件夹下的HomeView.vue
<template>
<div>
<input type="text" class="search" v-model="searchValue" @keyup.enter="search">
<div class="recommend">
<div class="tags" v-for="(item,index) in tagList" :key="item.classid" @click="select(index)">
{{item.name}}
</div>
</div>
<div class="type">
<div class="type-item" v-for="item in typeList" :key="item.classid" @click="toDetail(item.classid)">
{{item.name}}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
searchValue: "",
tagList: [],
typeList: [],
};
},
methods: {
//搜索框回车后转到详情页查询:通过传过去的是否有flag属性来判断做的是什么操作。
search() {
this.$router.push(`/detail?keyword=${this.searchValue}&flag=true`);
},
recommend() {
this.$axios({
url: "/api/class",
method: "get",
params: {
appkey: "34daa84d8da46182",
},
}).then((res) => {
this.tagList = res.data.result;
this.typeList = res.data.result[0].list.slice(0, 10); //截取前10条数据
//截取子数组的方法是slice
});
},
select(index) {
this.typeList = this.tagList[index].list.slice(0, 10);
},
toDetail(classid) {
this.$router.push(`/detail?classid=${classid}`);
},
},
created() {
this.recommend();
},
};
</script>
<style scoped>
.search {
width: 90%;
height: 30px;
margin-left: 5%;
margin-top: 10px;
}
.tags {
display: inline-block;
background-color: blanchedalmond;
padding: 0 5px; /* 由于每个标签的文字多少不同,不能直接给定宽度,使用padding来解决 */
height: 20px;
text-align: center;
line-height: 20px;
margin-left: 5px;
margin-top: 10px;
}
/* .wrap{
width: 90%;
margin-left: 5%;
} */
.type-item {
width: 45%;
height: 100px;
background-color: aqua;
margin-left: 3%;
display: inline-block;
text-align: center;
line-height: 100px;
margin-top: 10px;
}
</style>
views文件夹下的TypeDetail.vue
<template>
<div>
<div v-for="(item, index) in detailList" :key="item.id">
<h1>{{ index + 1 }}.{{ item.name }}</h1>
<img :src="item.pic" />
<div v-html="item.content"></div>
<h2>原材料</h2>
<ul>
<li v-for="m in item.material" :key="m.mname">
{{ m.mname }}:{{ m.amount }}
</li>
</ul>
<h2>烹饪步骤</h2>
<ul>
<li
v-for="(m, index) in item.process"
:key="index"
v-html="index + 1 + '.' + m.pcontent"
></li>
<!-- 注意:识别标签中的文本使用v-html,可以对其进行拼接 -->
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
detailList: [],
};
},
created() {
if ("flag" in this.$route.query) {
//按照输入框查询
this.$axios({
url: "/api/search",
method: "get",
params: {
keyword: this.$route.query.keyword,
num: 10,
start: 0,
appkey: "34daa84d8da46182",
},
}).then((res) => {
this.detailList = res.data.result.list;
});
} else {
this.$axios({
url: "/api/byclass",
method: "get",
params: {
classid: this.$route.query.classid, //注意这里通过查询参数传过来的数据拿去方法,要记得加.classid
start: 0,
num: 10,
appkey: "34daa84d8da46182",
},
}).then((res) => {
this.detailList = res.data.result.list;
});
}
},
};
</script>
<style scoped>
</style>
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<style>
</style>
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import "./common/common.css" //引入样式到所有文件
import axios from "axios"
Vue.config.productionTip = false
Vue.prototype.$axios = axios // 将axios挂载到Vue实例的原型上
new Vue({
router,
render: h => h(App)
}).$mount('#app')
vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: { //配置跨域
"/api": {
target: "https://api.jisuapi.com/recipe/", //每次需要更改的地方,填写请求目标的地址
//ws:true,
changeOrigin: true,//允许跨域
pathRewrite: {
'^/api': '' //请求的时候使用这个api就可以
}
}
}
}
})
16.Vuex 3.x
使用vuex来进实现子-父传值、子-子传值以及更复杂的传值,像父-子传值这种简单的传值就是用props即可。
注意:
Vuex 3.x版本是与Vue2.0版本配套
Vuex4.x版本是与Vue3.0版本配套
概念:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库(也就是公共数据管理模式)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
这么说吧,将vue想作是一个js文件、组件是函数,那么vuex就是一个全局变量,只是这个“全局变量”包含了一些特定的规则而已。
使用
npm install vuex
注意,安装的时候如果有以下报错,说明是安装版本过高导致,安装低版本即可解决
解决方案:
npm install vuex@3.6.2
再单独创建一个文件夹store,在里面创建一个index.js,里面的内容如下:(与vue-router各方面都很类似)
//store文件夹下的index.js文件内容 1引入 2注册 3创建vuex实例并默认导出 4main.js引入使用
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//store的意思是:商店,组件之间的传值直接都从store中拿,不管两个组件之间是什么关系
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
export default store
//main.js
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
})
五大核心概念
vuex中的state数据和getters映射到组件中的计算属性中,mutations映射到组件中的methods中时,尽量使用相同的名字,这样便于找到。
vuex中的getters、mutations都必须要有第一个参数state,actions的第一个默认参数是context,
state
getter 第一个默认参数是state,第二个默认参数是getters
mutation 第一个默认参数是state,第二个参数是载荷payload
action 第一个默认参数是context,第二个参数是载荷payload
state
状态:state里存放的其实就是希望共享的数据。
const store = new Vuex.Store({
state: {
count: 10,
like:["篮球","足球"],
movie:{"name":"速度与激情",actor:"保罗沃克"}
}
})
**vuex中state的数据在组件中使用的时候一般都放在计算属性computed中,**这样能够在数据发生变化时驱动视图响应式发生变化。
在其他组件取出共享的数据的方法:this.$store.state.count
getters
getters与计算属性不同的地方:在使用上面的数据的时候不同,computed是直接this.count,而getters必须通过第一个参数state.count。
类似于计算属性,不同的是在使用state里面的数据时与computed使用data里的数据this.gender不一样,getters里面的要接收参数,默认接受第一个参数,该参数是所有的state,其他的用法跟computed是一模一样的
第二个参数是所有的getters
1.基本用法
getters里面的属性不能与state里的重名。
//index.js文件中
const store = new Vuex.Store({
state: {
count: 10
},
getters:{
money(state){
return state.count+"元"
}
}
})
//组件中取出数据
computed:{
money(){
return this.$store.getters.money
}
}
在其他组件中取出getters的方法:this.$store.getters.money
2.getters也可以传参(和计算属性一样,返回一个函数即可)
使用场景:同一个数据在不同的页面中有不同的换算结果。
//index.js文件中
getters:{
change(state){
return (rate)=>{
return state.money*rate
}
}
}
//组件中取数据,如果在组件中使用该数据的时候还需要传参,则计算属性中也需要返回一个函数来传参
computed:{
change(){//注意这里计算属性中函数名不能传参,得在return函数中写参数
return (rate)=>{
return this.$store.getters.change(rate)
}
}
}
//组件中使用该数据
{{change(0.6)}}
3.getters的第二个参数:所有getters
计算属性computed中可以使用data中的数据,同时也可以使用其他的计算属性和props。同样getters中也可以除了使用state中的数据,也可以使用其他的getters.
getters:{
info(state){
return state.count*2
},
money(state,getters){
return getters.info+"元"
}
}
mutations
mutations这里定义一些改state数据的方法。
之前在组件中想要更改data中的数据直接this.count=新值即可,而想要修改vuex中state的数据是不能直接改this.$store.state.count=新值的,必须要通过mutations修改。
1.修改state数据的唯一办法就是提交mutation
2.mutations中修改的数据如果是引用类型,一定注意要完整的赋值(不能单独修改某一项)。如果觉得完整赋值很麻烦,可以通过深复制将数据复制给另一个变量,修改另一个变量的某一项,再将另一个变量赋值给该数据即可。
修改state中的对象和数组的不同方法:
1、数组:法一:完整替换;
法二:深复制.(更好)
let narr=[...arr]
narr[0]="w"
arr=narr
2、对象:法一:完整替换;
法二:深复制,相同属性名后面的属性值覆盖前面的属性值。(更好)
person={...person,salary:8000}
**1.基本用法 接收默认的第一个参数state **
就是你想做的事情的列表,mutation要通过commit提交.
//index.js文件
const store = new Vuex.Store({
state: {
count: 1,
person:{
name:"小明",
age:25,
salary:5000
},
arr:["h","e","l","l","o"],
},
mutations: {
addCount (state) {
// 变更状态
state.count++
},
changePerson(state){
//要修改的是引用数据类型的
//方法一:完整替换,很麻烦
//state.person={name:"小明",age:25,salary:8000}
//方法二:深复制,将要修改的属性值放在后面,将前面的属性值覆盖(更好一些)
state.person={...state.person,money:8000}
},
changeArr(state){
//方法一:完整替换,与对象一样
//方法二:深复制,不能像对象一样覆盖
let narr=[...state.arr]
narr[0]="w"
state.arr=narr
}
}
})
export default store
组件中调用:this.$store.commit("mutation名字")
//组件中
this.$store.commit(' mutation名字 ')
2.载荷(payload)
mutations中是可以传参数的,第一个参数是state,第二个参数是自定义的需要的参数,同样在组件中提交的时候也要有第二个参数this.$store.commit("函数名",第二个参数)
。
可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload):
//index.js文件
mutations: {
increment (state, n) {
state.count += n
}
}
//调用
this.$store.commit('increment', 10)
大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
//调用
this.$store.commit('increment', {operate:"add",amount:500})
3.有payload时以对象风格的提交方式
如果有payload的话更推荐用对象风格的提交方式。
mutations: {
increment (state, payload) { //此时payload是包含着type的
state.count += payload.amount
}
}
//调用
this.$store.commit({
type: 'increment',
operation:"add",
amount: 10,//参数
})
4.常量替代 Mutation 事件类型
在小型项目中会更麻烦,但在大型项目中可以避免出错,因为写字符串容易写错。至于用不用这种方法取决于公司之前的项目。
在store文件夹中新建一个文件mutations-type.js在里面单独定义一些名字。在mutations中使用的时候使用常量,在提交的时候也用常量。
使用过程:
1.mutations-type.js中定义名字,并导出(不是默认导出)。
2.在index.js文件中导入。由于不是默认导出的,导入时需要加{}。
3.在mutations中的使用:
[ADD](state){
}
4.在组件中提交mutation时:
先引入名字,在提交时函数名直接变为变量名。this.$store.commit(ADD)
//index.js文件中
import Vue from 'vue'
import Vuex from 'vuex'
import {ADD,MINUS} from "./mutation-type"
//2 导入
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 10
},
mutations:{
//这里要加[],因为不加[]是字符串,加[]才是变量
//3 使用
[ADD](state){
return state.count++
},
[MINUS](state){
return state.count--
}
}
})
export default store
//mutation-type.js文件中
//1 定义mutation的名字,并导出
export const ADD = 'add';
export const MINUS = 'minus';
//App.vue文件中
//4 引入
import {ADD} from "./store/mutation-type"
export default{
data(){
return{
msg:"我是App组件"
}
},
mounted(){
//5 使用ADD
this.$store.commit(ADD)
console.log(this.$store.state.count)//11
},
5.Mutation 必须是同步函数
比如个人信息需要在项目中通用,需要请求得到,
为何mutation不能包含异步操作?
使用层面:代码更高效易维护, 逻辑清晰(规范,而不是逻辑的不允许);
具体原因:为了让devtools 工具能够追踪数据变化;
具体原因详解
每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来(每次状态的改变都会生产一个全新的 state 对象),然后就可以实现 “time-travel” 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
注: vue-devtools 的时间旅行 - time travel
Vuex 借鉴 Flux 单向数据流思想,采用集中式统一管理保存状态。但是这些状态不会随时间改变而变化。为了使状态,可以被捕获、重播或重现。vue-devtools工具,可以帮助我们的 Vue 应用可以实现这种时间旅行!
每次状态的改变都会生产一个全新的 state 对象,当你想展现什么时间段的状态,只需要切换到那个时间段的 state 对象,所以vuex原则上只能通过mutation 并且非异步更改状态,否则无法实现state修改的记录保留或无法正确记录。
actions
actions用于写异步操作,异步操作之后提交mutation修改数据。
有一个默认参数context(上下文对象,实际上就是与store对象一模一样的对象).
在其他组件中派发action:this.$store.dispatch("actions中函数名")
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。只有mutation能修改state数据
- Action 可以包含任意异步操作。
演示代码:
const store = new Vuex.Store({
state: {
info:"我是共享的数据"
},
mutations: {
changeInfo (state,payload) {
state.info=payload.msg
}
},
actions: {
//可以使用es6的结构赋值来进行简化
getInfo (context,payload) {
//这里只是将异步操作做了,然后再将修改state数据的操作交还给mutation,只有mutation能修改state数据。
setTimeout(()=>{
context.commit("changeInfo",payload)
},3000)
}
}
})
//其他组件中派发actions
this.$store.dispatch("getInfo",{msg:"helloworld"})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
**其他组件分发action:**最终mutation可以接收到其他组件dispatch的参数。
this.$store.dispatch('increment')
//带参数的形式
// 以载荷形式分发
this.$store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
this.$store.dispatch({
type: 'incrementAsync',
amount: 10
})
如果只用commit操作常用es6的结构赋值来简化(也可以带载荷):
actions:{
getInfo({commit}){
setTimeout(()=>{
commit("changeInfo",{aa:"helloworld"})
},3000)
}
}
**组合action:**有两个action,先触发其中一个,等到这个有返回结果之后再去触发另一个action。????话不是很理解的
1.dispatch函数返回值是promise
如需要先拿到Id再用id去拿到info,最后根据info再去修改state的数据。
//index.js文件中 需要先执行getId,再执行getInfo,最后resolve的值传到了getInfo中的payload中,再去修改state的值
actions:{
getInfo(context,payload){
console.log(payload)
},
getId(){
return new Promise((resolve)=>{
setTimeout(() => {
resolve("我是id")
}, 2000);
})
}
}
//其他组件中 先分发getId,返回一个Promise,再分发getInfo
changeInfo(){
this.$store.dispatch("getId").then((res)=>{
this.$store.dispatch("getInfo",res)
})
}
//index.js文件中
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
//其他组件中
this.$store.dispatch('actionA').then(() => {
// ...
this.$store.dispatch("")
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await ,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
mutaions和actions流程
modules
在不开启命名空间的默认情况下,只有state数据是只属于模块的,在读取数据时默认情况下state读取的是根实例的,getters、mutations、actions默认是定义在全局的,若想把getters、mutations、actions也设置为私有的,给模块开启命名空间。
各个模块中state、mutation、actions中的变量名可以重复,getters中的方法名不能重复。
基本用法
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter
单独定义一个模块跟我们之前定义store是一样的
store文件夹下的moduleA.js
默认导出
export default{
state:{
text:"我是moduleA的数据"
},
getters:{},
mutations:{},
actions:{}
}
store文件夹下的moduleB.js
默认导出
export default{
state:{
text:"我是moduleB的数据"
},
getters:{},
mutations:{},
actions:{}
}
store文件夹下的index.js
导入store模块
import Vue from "vue";
import Vuex from "vuex";
import moduleA from "./moduleA"
import moduleB from "./moduleB"
Vue.use(Vuex);
const store=new Vuex.Store({
state:{
},
modules:{
// 模块名:模块对象
moduleA,moduleB
}
})
export default store
读取模块中的数据语法:默认情况下是根实例的state,想要读取子模块的state需要指明模块名this.$store.state.模块名.数据名
this.$store.state.模块名.数据名
//例如
this.$store.state.moduleB.text
多层嵌套
每一个子模块依旧可以有自己的子模块,比如moduleA里添加一个moduleC
moduleA.js
import moduleC from "./moduleC"
export default{
state:{
text:"我是moduleA的数据"
},
getters:{},
mutations:{},
actions:{},
modules:{
moduleC
}
}
moduleC.js
export default{
state:{
text:"我是moduleC的数据"
},
getters:{},
mutations:{},
actions:{}
}
访问:this.$store.state.模块名.子模块名.数据名
this.$store.state.模块名.子模块名.数据名
//例如
this.$store.state.moduleA.moduleC.text
命名空间
不开启命名空间,在默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的
比如:moduleA和moduleB都定义了一个叫changeText的mutation,提交mutation后所有模块的text数据都会发生更改。
moduleA.js
import moduleC from "./moduleC"
export default{
state:{
text:"我是moduleA的数据"
},
getters:{},
mutations:{
changeText(state){
state.text="moduleA的数据改了"
}
},
actions:{},
modules:{
moduleC
}
}
moduleB.js
export default{
state:{
text:"我是moduleB的数据"
},
getters:{},
mutations:{
changeText(state){
state.text="moduleB的数据改了"
}
},
actions:{}
}
MyHome.vue
computed:{
textA(){
console.log(this.$store.state)
return this.$store.state.moduleA.text
},
textB(){
console.log(this.$store.state)
return this.$store.state.moduleB.text
}
},
methods:{
//我们会发现我们直接提交mutation不需要加模块名字,A和B的mutation都能触发
changeText(){
this.$store.commit("changeText")
}
}
getters也是同理,多个模块都有同一个getter,默认读取的是根store的getters,如果根store中没有,则按照模块注册顺序读取
**开启命名空间后:**getters、mutations、actions只在当前模块内有效。
开启命名空间:
可以通过在模块内部添加
namespaced: true
的方式使其成为带命名空间的模块,表示这些只在当前模块生效。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
如果是只给moduleA开启了命名空间:像以前一样提交mutation时this.$store.commit("mutation方法名")
,只有muduleA的数据没有发生更改。
如果只想要提交moduleA的mutation:this.$store.commit("模块名/mutation方法名")
,这时只有modualA的数据发生了更改。
示例:
moduleA.js
import moduleC from "./moduleC"
export default{
namespaced: true, //开启命名空间
state:{
text:"我是moduleA的数据"
},
getters:{
info(){
return "哈哈"
}
},
mutations:{
changeText(state){
state.text="moduleA的数据改了"
}
},
actions:{},
modules:{
moduleC
}
}
// 其他组件中
this.$store.commit("changeText")//除了moduleA都发生了更改
this.$store.commit("moduleA/changeText") //只有moduleA的数据发生了更改
此时修改数据需要加上模块名:
this.$store.commit("moduleA/changeText")//代表提交moduleA里的changeText
//对于嵌套模块
this.$store.commit("moduleA/moduleC/changeText")//提交moduleA里的moduleC里的changeText
//对于getters,注意这里的不同,getters不是方法
this.$store.getters["moduleA/info"]
//对于actions
this.$store.dispatch("moduleA/actionA")
开启命名空间后访问全局内容
在开启了命名空间的模块内部,也是可以访问到全局数据的
getters中可以拿到全局的state和getters:getters中的第三四个参数 rootState rootGetters 拿到的是所有的数据,不只是根store中的数据
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
actions中可以拿到所有的state和getters:context里面就有全局的
actions:{
actionA(context){
console.log(context)
}
},
输出内容:
在开启了命名空间的模块内部也可以提交全局的action或者mutation,实现更改全局的而开启命名空间的不更改:
将
{ root: true }
作为第三个参数传给dispatch
或commit
即可,表示提交的是全局的,第二个参数是actions和mutations需要传的参数,没有就写null。
想要更改全局的action或者mutation,开启了命名空间的模块不做修改:
1、开启命名空间,在组件中使用时正常使用
this.$store.dispatch("actions方法名")
;2、在开启了命名空间的模块内部,在提交的时候传
{ root: true }
作为第三个参数。
dispatch('someOtherAction') // -> 'foo/someOtherAction' 调用的是当前模块的
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
关于命名空间,全局访问局部、局部访问局部、局部访问全局都是可以的。
带命名空间的辅助函数
...mapState("moduleA",{textA:"text"})
...mapMutations("moduleA",["changeText"])
...mapState("moduleA/moduleC",["text"]),
还可以通过使用 createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('moduleA/moduleC')
computed:{
...mapState(["text"]),//这样text默认就是在moduleA/moduleC下的
}
辅助函数
使用步骤:
1、引入mapState 把vuex中的数据映射到组件中
import { mapState,mapGetters,mapMutations,mapActions } from 'vuex'
mapState函数的返回值是一个对象
初步优化:(公司里肯定不会这么写)
//把this.$store.state做了封装
computed:mapState({
money:state=>state.money
a:state=>state.a
})
二次优化(99%不会这么写)但也有可能这么写是因为vuex数据的名字与组件中之前写的某些变量名重复了,需要改名,这时可以二次优化和最终优化结合起来写
computed:mapState({
//前面是组件中计算属性的名字 后面是vuex中的名字
money:"money",// 必须是字符串的形式
a:"a",
b:"b",
c:"c",
})
最终优化(工作中推荐)前提是计算属性中的名字与vuex中state的名字一样
computed:mapState(["money","a","b","c"])
//使用扩展运算符将辅助函数和其他计算属性混合到一起,组成一个对象
data(){
return{
count:100,
gender:1
}
},
//mapState
computed:{
...mapState(["money","a","b","c"]),//最终优化
...mapState({change:"d"}),//二次优化,需要将vuex映射过来的数据改名的情况
...mapGetters(['doubleA','doubleB']),//
sex(){
return this.gender==1?"男":"女"
}
},
注意:如果getters需要传参的话,在映射到computed中时也可以直接写数组,然后在html模板中直接传入参数即可,同理,mutation和action如果有载荷的话也是这样。
使用案例:
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 10
},
getters:{
doubleCount(state){
return (rate)=>{
return state.count*rate
}
}
},
mutations:{
changeCount(state,payload){
state.count*=payload.count
}
},
actions:{
asyncChangeCount({commit},payload){
setTimeout(() => {
commit("changeCount",payload)
}, 3000);
}
}
});
export default store
<!-- 组件中 -->
<template>
<div>
<!-- getters、mutations、actions如果有需要传参数的,直接在模板中调用的时候传参即可,在mapGetters中和mapState一样,直接写数组 -->
<h1>{{count}}----{{doubleCount(10)}}</h1>
<h1>{{num}}</h1>
<button @click="changeCount({count:5})">更改</button>
<button @click="asyncChangeCount({count:10})">异步更改</button>
</div>
</template>
<script>
import { mapActions, mapGetters, mapMutations, mapState } from 'vuex'
export default {
computed:{
...mapState(['count']),
...mapState({
num:"count"
}), // 使用对象格式更改名字
...mapGetters(['doubleCount']),
} ,
methods:{
...mapMutations(['changeCount']),
...mapActions(['asyncChangeCount'])
}
}
</script>
<style scoped>
</style>
vuex不是永久存储,一刷新就没有了,只是临时过度的。如果想要刷新浏览器不丢失数据只有存储到本地存储LocalStorage、SessionStorage、cookies或数据库中。