过渡与动画
Vue 封装了过渡与动画效果
作用:在插入、更新、移除 Dom 元素时,自动在合适的时刻在过渡标签上添加或移除不同的类名,只需写动画和过渡效果并应用到对应时刻的类名上,而无需手动操作 Dom
过渡标签:
(1) <transition> 包裹需要动画效果的单个元素
(2) <transition-group> 包裹需要动画效果的多个元素,每个元素都需要配置 Key 属性
不同时刻样式类名: (1) 控制元素进入的样式
- v-enter 进入的起点
- v-enter-active 进入过程中
- v-enter-to 进入的终点
(2) 控制元素离开的样式
- v-leave 离开的起点
- v-leave-active 离开过程中
- v-leave-to 离开的终点
过渡标签添加 name 属性来配置类名的前缀,默认是 v,如进入起点的类名是 .v-enter 或 <前缀>-enter
离开和进入:即元素的插入、更新、移除,通常配合 v-show、v-if 来控制
添加 appear 属性让动画在网页首次加载时、元素首次插入时也触发
元素飞入飞出案例
分别用动画属性 animate 和过渡属性 transition 配合不同的类名实现相同的动画效果
模板结构:
<template> <div> <button @click="isShow=!isShow">显示隐藏</button> <transition name="hello" appear> <h1 v-show="isShow" class="title">你好</h1> </transition> </div></template>交互:
export default { name: 'Test-', data(){ return{ isShow: true, } }}使用动画属性 animate 配合 .v-enter-active 和 .v-leave-active
.title { background: rgb(114, 196, 219);}/* 进入过程的动画 */.hello-enter-active{ animation: titleShow 1s;}/* 离开过程的动画 */.hello-leave-active{ animation: titleShow 1s reverse;}/* 动画 */@keyframes titleShow{ from { transform: translateX(-100%); } to{ transform: translateX(0px); }}过渡属性 transition 配合进入和离开的起点、终点的样式实现动画
.title { background: rgb(114, 196, 219);}/* 设置进入起点的样式 */.hello-enter { transform: translateX(-100%);}/* 设置进入终点的样式 */.hello-enter-to { transform: translateX(0);}/* 设置离开起点的样式 */.hello-leave { transform: translateX(0);}/* 设置离开终点的样式 */.hello-leave-to { transform: translateX(-100%);}/* 设置 transition 属性 */.hello-enter-active, .hello-leave-active { transition: 0.5s linear;}第三方动画库
有许多优秀的动画库,引入即可使用,以 animate.css 为例
安装:npm install animate.css --save
使用第三方库时,通常要在过渡标签上通过几个属性来自定义样式类名的后缀,name属性设置前缀 (1) enter-active-class 属性:设置进入过程中的样式类名 (2) leave-active-class 属性:设置离开过程中的样式类名
使用:引入动画库后,在过渡标签上设置name前缀以使用该动画库,然后在animate.css文档中挑选动画效果,将进入动画的类名设置到 enter-active-class 属性中,离开动画的类名设置到 leave-active-class 属性中,通过设置样式类名后缀来使用具体的动画效果
案例:
// 引入动画库import 'animate.css'export default { name: 'Test-', data() { return { isShow: true, } }}模板结构:
<template> <div> <button @click="isShow = !isShow">显示隐藏</button> <!-- 设置动画 --> <!-- name设置前缀表示使用animate.css动画库 --> <!-- 通过设置样式类名后缀来使用具体的动画效果 --> <transition name="animate__animated animate__bounce" enter-active-class="animate__swing" leave-active-class="animate__backOutUp" appear > <h1 v-show="isShow" class="title" key="1">你好</h1> </transition> </div></template>CSS 样式,因为使用了动画库,就无需自己写动画了
.title { background: rgb(114, 196, 219);}ToDoList添加动画
给 ToDoList 案例中每个todo 的新增和删除添加动画
效果:
修改 ToDoList.vue 组件
<template> <div class="todo-list"> <ul> <transition-group name="todo"> <ListItem v-for="item in todos" :key="item.id" :todo="item"></ListItem> </transition-group> </ul> </div></template>
<!-- ...... -->
<style>/* ........ *//* 动画 */.todo-enter,.todo-leave-to { opacity: 0; transform: translateX(-50%);}
.todo-enter-to,.todo-leave { opacity: 1; transform: translateX(0);}.todo-enter-active, .todo-leave-active { transition: all 0.3s;}</style>Vue中发请求
在 Vue 中发请求通常用 Axios
安装: npm i axios
Axios 笔记: AJAX请求相关-Axios
Vue-cli配置代理
当后端没有配置 cors 时,本地开发环境请求后端API就会产生跨域问题
服务器之间通信没有跨域问题,可以在本地配置一个代理服务器去请求API
在 vue.config.js 中添加 devServer 配置项,以在开发环境下将 API 请求代理到 API 服务器
const { defineConfig } = require('@vue/cli-service')module.exports = defineConfig({ transpileDependencies: true, devServer: { proxy: '<转发的url>' }})配置代理后,将请求的 baseURL 改为本地ip+端口,如 120.0.0.1:8080,代理服务会将请求转发,自己去请求实际的API地址,然后返回数据
有些时候并不想所有发往自身获取资源的请求都被代理转发,实际想获取的是本地public文件夹下的资源,而不是远程API的
可以写完整版 devServer 配置,匹配路径去选择性代理
**作用:**可以配置多个代理,且可以灵活的控制发往本地的请求是否走代理转发
const { defineConfig } = require('@vue/cli-service')module.exports = defineConfig({ transpileDependencies: true, devServer: { proxy: { // 匹配路径代理 '/api': { target: '<url>', // 代理目标的baseURL ws: true, // 用于支持websocket changeOrigin: true, // 伪造Host为代理目标baseURL pathRewrite: {'<正则匹配路径>', '<替换内容>'} // 重写路径 }, '/foo': { target: '<other_url>' } } }})还是让后端配cors好
获取所有账单
在 NodeJS 中写过一个记账本案例,里面写了一套完整的 API,刚好拿来用
**功能:**点击登陆提示登陆中,登陆成功提示成功登陆,点击获取账单,提示加载中,请求成功后展示所有账单的列表
<template> <div> <button @click="login">登录获取token</button> <button @click="getData">获取所有账单</button> <!-- 提示 --> <div v-show="tip !== ''" class="tip">{{ tip }}</div> <ul> <li v-for="(item, index) in kpb" :key="item.id"> {{ index + 1 }}: {{ item.matter }} | {{ item.date }} | {{ item.type }} | {{ item.account }} </li> </ul> </div></template>
<script>import axios from "axios";axios.defaults.baseURL = 'https://kpb.qcqx.cn';export default { name: "App", data() { return { // 登陆返回的token token: "", // 账单数据 kpb: "", // 提示 tip: "欢迎,请登陆", } }, methods: { // 登陆 login() { this.kpb = ""; this.tip = "登陆中...."; axios({ url: '/api/login', method: 'post', data: { username: 'qx', password: '123' }, // 请求超时 timeout: 5000 }).then(res => { // 响应体对象 console.log(res.data); this.token = res.data.data.token; this.tip = "登陆成功"; }).catch(err => { console.log(err); this.tip = "登陆失败"; }); }, // 获取账单 getData() { this.kpb = ""; this.tip = "账单加载中...."; axios({ url: '/api', method: 'get', headers: { token: this.token }, // 请求超时 timeout: 5000 }).then(res => { // 响应体对象 console.log(res.data); if (res.data.data === null) { this.tip = res.data.msg; return; } this.kpb = res.data.data; this.tip = ""; }).catch(err => { console.log(err); this.tip = "获取账单失败"; }); } }};</script>插槽
插槽是由子组件在模板的合适位置通过 <slot> 定义的
作用:让父组件可以向子组件定义的插槽处插入 html 结构
插槽的种类:
(1) **默认插槽:**简单地将组件标签中所有html结构放到插槽处
(2) **具名插槽:**通过 <slot> 上的 name 属性区分插槽,通过 v-slot:<插槽名> 将html插入到指定插槽
(3) 作用域插槽:用于数据在子组件中,且不将数据外传,根据数据生成的结构由父组件决定的情况
注意: (1) 父组件中要插入插槽的html结构也是在父组件中完成模板解析的,可以访问父组件中的数据,而不能直接访问到自组件的数据 (2) 三种插槽可以混合使用
默认插槽
简单地将组件标签中所有html结构放到插槽处
<ListBox> <ul> <li v-for="(item, index) in arr" :key="index">{{ item }}</li> </ul></ListBox><!-- 定义插槽 --><slot></slot>具名插槽
通过 <slot> 上的 name 属性区分插槽,通过 v-slot:<插槽名> 将html插入到指定插槽
v-slot: 可以简写为 #
<ListBox> <template v-slot:list> <ul><li v-for="(item, index) in arr" :key="index">{{ item }}</li></ul> </template> <!-- v-slot: 简写为 # --> <template #other> <h2>chuckle</h2> </template></ListBox><slot name="list"></slot><slot name="other"></slot>作用域插槽
用于数据在子组件中,且不将数据外传,根据数据生成的结构由父组件决定的情况
子组件通过 <slot> 的标签属性传数据给父组件(只能在组件标签中使用)
父组件通过 #<插槽名>="obj" 的 obj 接收一个对象,对象中有传过来的属性名和属性值(数据)
<ListBox> <!-- default代表无名插槽 --> <template #default="{arr}"> <ul><li v-for="(item, index) in arr" :key="index">{{ item }}</li></ul> </template> <!-- v-slot接收到的是一个对象 --> <template #other="obj"> <div>{{ obj.x }} | {{ obj.y }}</div> </template></ListBox><!-- 将子组件的数据传给父组件内的组件标签中使用 --><slot :arr="arr"></slot><!-- 传给父组件多个数据 --><slot name="other" :x="x" :y="y"></slot>Vuex
在实现组件间通信时,无论是全局事件总线还是消息订阅与发布,大量的声明事件或消息名,会让代码变得臃肿、难以维护
Vuex 是实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理(读/写)
安装: npm install vuex --save Vue2 只能使用 Vuex3
**功能:**将原本某一组件管理的数据拿出来给 Vuex 管理,所有组件都能看到 Vuex 中管理数据的仓库 Store,以及使用仓库所提供的,对数据读和写的方法
何时用 Vuex 管理状态(数据): (1) 多个组件依赖于同一状态 (2) 来自不同组件的行为需要变更同一状态
Vuex工作原理
Vuex 工作原理图:
Vuex 有三个重要组成部分,这三个部分都在 Store 实例上,本质都是对象
- **Actions:**进行一些业务逻辑(异步)的操作,如发送 AJAX 请求,不直接对数据进行操作
- **Mutations:**对数据进行直接的维护,有许多对数据进行操作的自定义方法
- **State:**状态(数据),存放着多个组件共享的数据
Vuex的工作循环:
- 组件通过
dispatch()调用 Actions 中的方法,触发一些业务逻辑 - Actions 中业务逻辑处理完后,在合适时机调用
commit()触发 Mutations 中对数据进行直接操作的方法 - 然后 State 中数据响应式地变化,触发对应视图更新
**注意:**如果对数据的操作不带有更多的业务逻辑,组件也可以直接调用 commit() 方法
**Store:**由 Vuex.Store() 构造函数创建,提供了三个重要组成部分以及 dispatch() 、commit() 等各种API
搭建Vuex环境
新建一个JS文件,用于配置和创建 Store 实例,通常是 src/store/index.js
import Vue from 'vue';import Vuex from 'vuex';// 使用vuex插件Vue.use(Vuex);
// 创建三个重要组成部分const actions = {}const mutations = {}const state = {}
// 创建store实例,并暴露出去export default new Vuex.Store({ // 将三个重要部分都交给store管理 actions, mutations, state});然后在 new Vue() 时将 Store 实例作为配置项传入
**目的:**让 vm 和 vc 身上新增一个 $store 属性,让所有组件都能访问到数据仓库 store
import Vue from "vue";import App from "./App.vue";// 引入store实例import store from "./store";
Vue.config.productionTip = false;
new Vue({ el: "#app", // 使用store store, render: h => h(App),})纯Vue求和案例
<template> <div> <CountNum></CountNum> </div></template>
<script>import CountNum from "./components/CountNum.vue";export default { name: "App", components: { CountNum, }};</script><template> <div> <h3>{{ sum }}</h3> <select v-model.number="num"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">+</button> <button @click="reduce">-</button> <button @click="addOdd">当前总和是奇数再加</button> <button @click="addWait">延迟1s再加</button> </div></template>
<script>export default { name: "CountNum", data() { return { // 总和 sum: 0, // 选择的数字 num: 0, } }, methods: { // 加 add() { this.sum += this.num; }, // 减 reduce(){ this.sum -= this.num; }, // 当前总和是奇数再加 addOdd(){ if(this.sum%2){ this.sum += this.num; } }, // 延迟1s再加 addWait(){ setTimeout(()=>{ this.sum += this.num; }, 1000) } },}</script>Vuex版求和案例
对纯 Vue 版进行修改,将 sum 交给 vuex 的 store 管理,将直接对数据的操作放到 Mutations 中,将带有业务逻辑的操作放到 Actions 中
一些总结与注意:
- 虽然
$store.state.xxx = xxx直接修改数据,也能响应式更新视图,但这样做无法被 vuex-tools 所监视,所以不要直接修改数据 commit()和dispatch()都只能传入两个参数,第一个是要触发的函数名,第二个是传入的数据- actions 中的函数收到两个参数,第一个 context 指上下文,是一个 mini 的 store,有部分 store 上的属性和方法,第二个 value 是
dispatch()传来的数据 - mutations 中的函数也收到两个参数,第一个 state 是 store 中的数据,第二个 value 是
commit()传来的数据
<template> <div> <!-- 通过vc身上的$store.state获取数据 --> <h3>{{ $store.state.sum }}</h3> <select v-model.number="num"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="add">+</button> <button @click="reduce">-</button> <button @click="addOdd">当前总和是奇数再加</button> <button @click="addWait">延迟1s再加</button> <button @click="$store.state.sum = 1">测试直接修改数据</button> </div></template><script>export default { name: "CountNum", data() { return { // 选择的数字 num: 0, } }, methods: { // 加 add() { // 传入函数名去匹配对数据的操作和选择的数字 this.$store.commit('add', this.num) }, // 减 reduce(){ this.$store.commit('reduce', this.num) }, // 当前总和是奇数再加 addOdd(){ this.$store.dispatch('addOdd', this.num) }, // 延迟1s再加 addWait(){ this.$store.dispatch('addWait', this.num) } },}</script>import Vue from 'vue';import Vuex from 'vuex';// 使用vuex插件Vue.use(Vuex);
// 有业务逻辑的操作放在actions中const actions = { // context:上下文,一个mini都store,有store中的部分属性和方法 // value,组件中dispatch()传过来的值 addOdd(context, value) { if (context.state.sum % 2) { // 业务逻辑处理完后调用上下文中的commit() context.commit('add', value); } }, addWait(context, value) { setTimeout(() => { context.commit('add', value); }, 1000) }}
// 没有业务逻辑的操作放在mutations中const mutations = { // mutations中的函数收到两个参数 // state:store中的数据,value:commit()传过来的值 add(state, value) { state.sum += value; }, reduce(state, value) { state.sum -= value; },}
const state = { // 数据 sum: 0,}
// 创建store实例export default new Vuex.Store({ actions, mutations, state});getters配置项
除了三个重要组成部分必须作为 Store 的配置项之外,还有其它配置项
getters 和计算属性差不多,对数据进行加工后再返回,其中的函数收到一个参数 state
// 准备gettersconst getters = { // 对sum进行放大10倍 bigSum(state){ return state.sum * 10; }}export default new Vuex.Store({ actions, mutations, state, // 配置getters getters});通过 $store.getters 获取加工后的数据
<h3>{{ $store.state.sum }}</h3><h3>{{ $store.getters.bigSum }}</h3>mapState和mapGetters
当一个组件需要获取多个状态时,总在模板中用 $store.state.xxx 获取状态,代码重复且冗余
即使是手动写成计算属性,也要重复去 return $store.state.xxx
mapState() 和 mapGetters() 可以帮我们生成状态的计算属性,以此简洁地使用状态
需要引入后才能使用:
import { mapState, mapGetters} from 'vuex'使用注意:
- 传入一个对象,key-value 是
<生成的计算属性名>:<状态名或getter名> - 返回值是包含多个计算属性的对象,需要用
...对象展开运算符,将其与组件的计算属性对象合并
//import { mapState, mapGetters} from 'vuex'export default { /* .... */ computed: { test(){ return 'test' }, // 对象展开运算符 ...mapState({ sum: 'sum' }), ...mapGetters({ bigSum: 'bigSum' }) }, /* .... */}当要生成的计算属性名和状态名或getter名一样时,可以使用数组简写形式,传入数组,元素是状态名或getter名
mapState(['sum'])mapActions和mapMutations
在 methods 中也写了很多看起来重复的代码
// 加add() { // 传入函数名去匹配对数据的操作和选择的数字 this.$store.commit('add', this.num)},// 减reduce(){ this.$store.commit('reduce', this.num)},// 当前总和是奇数再加addOdd(){ this.$store.dispatch('addOdd', this.num)},// 延迟1s再加addWait(){ this.$store.dispatch('addWait', this.num)}mapActions() 和 mapMutations() 能够生成调用 commit 和 dispatch 方法的方法
用法和 mapState 和 mapGetters 差不多
methods: { ...mapMutations(['add', 'reduce']), ...mapActions({ addOdd: 'addOdd', addWait: 'addWait' }),},生成的方法长这样,需要传入一个value
add(value){ this.$store.commit('add', value)}所以调用时就需要手动传入 value
<!-- 手动传入加数num --><button @click="add(num)">+</button><button @click="reduce(num)">-</button><button @click="addOdd(num)">当前总和是奇数再加</button><button @click="addWait(num)">延迟1s再加</button>Vuex模块化
不同的功能会用到不同的数据,当很多实现不同功能的数据都写在一个 index.js 中或一个配置项中时,会导致命名冲突且代码不易维护
**作用:**让代码更好维护,让多种数据分类更加明确
将不同数据的 store 配置项单独拿出,每个模块的函数获取的数据或上下文都是模块自己的,无需再指定,即模块的局部状态
export default { // 开启命名空间 namespaced: true,
actions: { // context:上下文,一个mini都store,有store中的部分属性和方法 // value,组件中dispatch()传过来的值 addOdd(context, value) { if (context.state.sum % 2) { context.commit('add', value); } }, addWait(context, value) { setTimeout(() => { context.commit('add', value); }, 1000) } },
mutations: { // mutations中的函数收到两个参数 // state:store中的数据,value:commit()传过来的值 add(state, value) { state.sum += value; }, reduce(state, value) { state.sum -= value; }, },
state: { sum: 0, },
getters: { bigSum(state) { return state.sum * 10; } }}通过 Store() 的 modules 配置项使用模块
import Vue from 'vue';import Vuex from 'vuex';// 使用vuex插件Vue.use(Vuex);
// 导入求和案例配置import count from "./count.js";
// 创建store实例export default new Vuex.Store({ modules: { // 使用模块 count, }});调整 mapState()、mapGetters() 等的参数,指定是触发哪个模块的 actions 或 mutations
computed: { ...mapState('count',['sum']), ...mapGetters('count',['bigSum'])},methods: { ...mapMutations('count',['add', 'reduce']), ...mapActions('count',['addOdd', 'addWait']),},总结: (1) 命名空间:默认情况下,模块内部的 action、mutation 和 getter 注册在全局命名空间,这样使得多个模块能够对同一个 mutation 或 action 作出响应。如果希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
(2) 开启命名空间后:
- 组件中读取 state 数据
// 方式一:直接读取this.$store.count.sum// 方式二:借助mapState...mapState('count',['sum'])- 组件中读取 getters 数据
// 方式一:直接读取this.$store.getters['count/bigSum']// 方式二:借助mapGetters...mapGetters('count',['bigSum'])- 组件中调用 dispatch
// 方式一:直接读取this.$store.dispatch('count/addOdd', value)// 方式二:借助mapActions...mapActions('count',['addOdd', 'addWait']),- 组件中调用 commit
// 方式一:直接读取this.$store.commit('count/add', value)// 方式二:借助mapActions...mapMutations('count',['add', 'reduce']),
评论