6.Vue动画处理
vue有个内置组件 transition
基本使用:使用name定义过渡类名,然后常常与v-if、v-show、动态组件进行搭配
官方:
可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用 
v-if) - 条件展示 (使用 
v-show) - 动态组件
 - 组件根节点
 
基本使用(CSS)
<transition name="why">
    <h2 v-if="isShow">Hello World</h2>
</transition>
.why-enter-from,
.why-leave-to {
    opacity: 0;
}
.why-enter-to, 
.why-leave-from {
    opacity: 1;
}
.why-enter-active,
.why-leave-active {
    transition: opacity 2s ease;
}
class几个transition的类(我们可以通过name来定义前缀,比如name=”allen”,则类为allen-enter-from)
v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。(插入时样式)v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。(插入ing样式,常存放动画)v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时v-enter-from被移除),在过渡/动画完成之后移除。(插入后样式)v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。(移除前样式)v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。(移除ing样式,常存放动画)v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时v-leave-from被删除),在过渡/动画完成之后移除。(移除后样式)
transition的几个props
name-string用于自动生成 CSS 过渡类名。例如:name: 'fade'将自动拓展为.fade-enter,.fade-enter-active等。appear-boolean,是否在初始渲染时使用过渡。默认为false。mode-string控制离开/进入过渡的时间序列。有效的模式有"out-in"(先移除,再插入)和"in-out"(先插入,再移除);默认同时进行。duration-number | { enter: number, leave: number }。指定过渡的持续时间,指定之后css动画里原来写好的时间则无效css,指定:css='false'可以让css过渡属性失效,提高性能(用于只执行js动画)
第三方动画库
animate.css
npm i animate.css --save
然后直接在main.js里面引入
import "animate.css";
在官网搜寻自己想要的动画名称,并且在transition类中添加,比如
 .allen-enter-active {
     animation: bounceInUp 1s linear
 }
  .allen-leave-active {
      animation: bounceInUp 1s linear reverse
  }
gsap库
一个JS动画库(vue官方有在用),官网:https://greensock.com/
- 通过JavaScript为CSS属性,SVG、Canvas等设置动画,并且都是浏览器兼容的
 
npm i gsap
使用gsap库配合js钩子使用
methods: {
    enter(el, done) {
        console.log("enter");
        gsap.from(el, {
            scale: 0,
            x: 200,
            onComplete: done
        })
    },
        leave(el, done) {
            console.log("leave");
            gsap.to(el, {
                scale: 0,
                x: 200,
                onComplete: done
            })
        }
}
gsap除了对元素(element)进行JS动画处理,也可以对某个普通对象,进行按照时间间隔逐步改变属性值
  
    
    当前计数: {{showNumber.toFixed(0)}}
  
JS钩子
transition内置组件除了可以使用CSS动画,还可以配合上JS动画
此时使用里面定义好的JS钩子即可(钩子作用分别查看字面意思即可, enter-cancelled和 leave-cancelled对应的是取消操作 )
用的最多是enter和leave
<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>
钩子函数可以选择带入参数:
- el:当前元素
 - done:当只用 JavaScript 过渡的时候,在 
enter和leave钩中必须使用done进行回调。否则,它们将被同步调用,过渡会立即完成。 
// ...
methods: {
  beforeEnter(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  enter(el, done) {
    // ...
    done()
  },
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  },
  // --------
  // 离开时
  // --------
  beforeLeave(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  leave(el, done) {
    // ...
    done()
  },
  afterLeave(el) {
    // ...
  },
  // leaveCancelled 只用于 v-show 中
  leaveCancelled(el) {
    // ...
  }
}
列表插入
列表新增 / 删除元素的时候,可以模拟成v-if时,新增/ 删除 元素
只不过通过 transition-group 进行包裹
props:
- tag:将 
transition-group变成 xx标签 
不过,列表多出了其他元素,而添加 / 删除元素时影响到其他列表元素位置时,调整的动画该如何设置?
- 我们可以使用 v-move的class来完成动画的设置,它会在元素改变位置的时候应用,和之前的名字一样,我们可以通过name定义前缀
 
添加 / 删除列表元素小案例
  
    
    
    
      
        {{item}}
      
     
  
7.其他
自定义指令
代码的复用主要还是通过组件,但是在某些情况下,你需要对DOM元素进行底层操作,就可以用到自定义指令
- 自定义局部指令:组件通过directives选项
 - 自定义全局指令:app的directives选项
 
案例:为组件自定义 v-focus 指令
  
    
  
若为全局指令则在main.js中定义(更常用)
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app');
// 全局指令
app.directive("focus", {
  mounted(el, bindings, vnode, preVnode) {
    el.focus();
  },
})
自定义指令内部的生命周期属性:
| 名称 | 
|---|
| created | 
| beforeMount | 
| mounted | 
| beforeUpdate | 
| updated | 
| beforeUnmount | 
| unmounted | 
生命周期函数携带的参数
- el:元素
 - bindings:一个对象,里面存储传入的参数、修饰符等
 - vnode虚拟节点
 - preVnode,指向上一个虚拟节点
 
teleport
在某些情况下,我们并不想要将组建挂载在组件树上,而是移动到Vue app 以外的地方(我们有其他div#app的元素)
- teleport是Vue内置组件,类似于React的Portals
 - 它有两个属性
- to:指定将其中的内容移动到目标元素,可以使用选择器
 - disabled:是否禁用teleport的功能
 
 
自定义插件
全局插件有个约定俗成的小规范:命名都加$, $开头
添加完毕之后,可以通过 this.命名来获取
但是setup比较麻烦,因为setup不能通过this拿到组件实例
export default {
  install(app) {
      app.config.globalProperties.$name = "Allen"
  }
}
在main.js中导入
import { createApp } from 'vue'
import App from './App.vue'
import plugins_object from './plugins/plugins_object'
const app = createApp(App)
app.use(plugins_object); // 使用插件
/*
内部执行plugins_object.install(app)
*/
app.mount('#app');
在Vue3中,use方法实际上是把插件添加进Set中,然后调用其install方法
如果没有install方法,而plugin本身就是一个方法,则调用plugin方法
use(plugin: Plugin, ...options: any[]) {
    if (installedPlugins.has(plugin)) {
        __DEV__ && warn(`Plugin has already been applied to target app.`)
    } else if (plugin && isFunction(plugin.install)) {
        installedPlugins.add(plugin)
        plugin.install(app, ...options)
    } else if (isFunction(plugin)) {
        installedPlugins.add(plugin)
        plugin(app, ...options)
    } else if (__DEV__) {
        warn(
            `A plugin must either be a function or an object with an "install" ` +
            `function.`
        )
    }
    return app
},
vue3源码相关
三大核心
- Complier模块,编译模板系统(tempplate -> h函数可识别的形式)
 - Runtime模块,称之为Renderer模块,真正的渲染模块(h函数可识别的形式 -> 虚拟DOM -> 真实DOM)
 - Reactivity模块,响应式系统
 
element-plus
https://element-plus.gitee.io/zh-CN/
Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3 的桌面端组件库(Element是针对vue2.x的)
如果是移动端的推荐使用 VantUI 
使用:
方案一:直接全局引用 - > 所有组件全部集成,组件全部进行了全局注册
优点:集成比较简单,可以直接使用
缺点:全部打包‘
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
方案二:按需引用
优点:包会比较小
缺点:引用麻烦
方案三:官网推荐的自动导入
8.双向绑定
Proxy VS Object.defineProperty
Vue2.x版本中的双向绑定不能检测到下标的变化,不存在的属性不能被拦截(其实也是因为
Object.defineProperty方法必须传入对象、属性参数),属性中还有属性嵌套还得递归;proxy可以劫持整个对象,并返回一个新对象,管你属性存不存在都可以拦截响应式方面 性能得到很大提升 不用初始化的时候就递归遍历属性;响应式不区分数组和对象
修改对象:
- 使用 
Object.defineProperty我们修改原来的obj对象就触发拦截 - 使用proxy要修改代理对象,才触发拦截(即使在getter方法中 
return target[key],也不会触发循环调用 ) 
- 使用 
 Proxy能观察的类型比defineProperty更加丰富,比如has、deleteProperty等
Proxy作为新标准将受到浏览器厂商的重点优化
more:
- 这里Proxy相对与原来Vue2.0的响应式的  “不管三七二十一,上来就给你判断对象  + 递归 ”  不一样,做了小优化,他在修改数据时并不是一上来就递归,而是先通过get方法获取,如果获取到的结果是一个对象,则再做一层代理,进行递归,然后再通过
set修改 - 既然Proxy可以改属性 + 新增属性,那他怎么识别?通过 if 语句 + 
对象.hasOwnProperty(属性)判断,原来是否有这个属性 
为什么以前不用Proxy?兼容性差。。。ie11就不兼容
代理模式
代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式。
当一个复杂对象的多份副本须存在时,代理模式可以结合享元模式以减少内存用量。典型作法是创建一个复杂对象及多个代理者,每个代理者会引用到原本的复杂对象。而作用在代理者的运算会转送到原本对象。一旦所有的代理者都不存在时,复杂对象会被移除。
Proxy
Proxy代理Object 控制和修改Object的基本行为(覆盖对象的基本操作),比如属性调用、属性赋值、删除属性、方法调用等
target是被代理的对象,handler是一个对象,属性是各种控制或修改target基本行为的方法
你可以想象成proxy就是一个壳
const p = new Proxy(target, handler);
handler里面的get()、set()和Object.defineProperty()的get方法、set方法有点像,重新定义了属性的读取、设置行为
get()拦截某个属性的读取操作。可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
set()拦截某个属性的赋值操作。可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
其实handler有13种方法
handler.getPrototypeOf()
// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。
handler.setPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。
handler.isExtensible()
// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。
handler.preventExtensions()
// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。
handler.getOwnPropertyDescriptor()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。
handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。
handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。
handler.get()
// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。
handler.set()
// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。
handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。
handler.ownKeys()
// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。
handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。
handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。
里面的方法一般都需要返回值
var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(...arguments);
  }
});
甚至你可以用proxy做缓存
服务端和客户端同步一个状态可能会出现问题,这很常见,在整个操作周期内,数据都有可能被改变,并且很难去掌握需要重新同步的时机。
proxy提供了一种新的办法,可以让属性在必要的时候失效,所有的访问操作,都会被检查判断,是否返回缓存还是进行其他行为的响应。
Copyconst timeExpired = (target, ttl = 60) => {
  const created_at = Date.now();
  const isExpired = () => (Date.now - created_at) > ttl * 1000;
  return new Proxy(tarvet, {
    get: (target, k) => isExpired() ? undefined : Reflect.get(target, k);
  })
}
上面的功能很简单,他在一定时间内正常返回访问的属性,当超出ttl时间后,会返回undefined。
Copylet timeExpired = ephemeral({
  balance: 14.93
}, 10)
console.log(bankAccount.balance)    // 14.93
setTimeout(() => {
  console.log(bankAccount.balance)  // undefined
}, 10 * 1000)
上面的例子会输出undefined在十秒后,更多的骚操作还请自行斟酌。
Reflect
通过Proxy的捕获器(handler),我们可以完成基于自己的参数重建原始操作,但是并非所有捕获器都像 get 一样那么简单,直接返回就行了,所以通过手动写码如法炮制的想法是不现实的,所以我们使用到了Reflect完成轻松重建,确保完成原有的行为,然后再部署额外的功能。
Reflect将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上,也就是说,从Reflect对象上可以拿到语言内部的方法。Reflect修改某些Object方法的返回结果,让其变得更合理。比如,
Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
上面两点表明 Reflect可能将成为日后替代 Object对象方法的新的王!
让
Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // trueReflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } });上面代码中,
Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。个人认为:这样就形成了Proxy和Reflect的最佳搭配,既保证了原来(默认)的行为,又可以自己添加额外的行为
9.Vue3 + TS 项目搭建规范
集成editorconfig配置
editorconfig有助于为不同IDE编辑器上同一项目多人开发的编码风格(比如控制tab缩进空格数量等)
在根目录下新建一个 .editorconfig 文件
注意,默认情况下vscode不会读取这个文件,需要安装一个插件 EditorConfig for VScode
# https://editorconfig.org
root = true
[*]                                      #所有文件适用
charset = utf-8                          #文字字符集啊uft-8
indent_style = space                     #缩进风格space
indent_size = 2                          #缩进大小
end_of_line = lf                         #控制换行类型lf、cr、crlf
insert_final_newline = true              #去除行首任意空白字符
trim_trailing_whitespace = true          #始终在文件末尾插入一个新行
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
配置eslint
推荐先在vscode安装一下eslint插件
然后可以在根目录下的.eslintrc.js文件查看eslint的规则配置
但是有时候出现我们自定义prettier配置(格式化)和我们的eslint并不兼容的场面,此时我们要安装两个库(如果在创建脚手架的时候有配置过eslint + prettier选项的话,则已经帮我们安装过了)
npm i eslint-plugin-prettier eslint-config-prettier -D
然后在配置文件 .eslintrc.js 中的extends设置最后一行
extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/typescript/recommended',
    '@vue/prettier',
    '@vue/prettier/@typescript-eslint',
    'plugin:prettier/recomended'
]
若想关掉一些eslint检查,可以
rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'typescirpt-eslint/no-var-require': 'off' // 关闭对require引用的禁令
}
不同的开发环境
在开发中,有时候我们需要根据不同的环境设置不同的环境变量,常见的有三种环境
- 开发环境:development
 - 生产环境:production
 - 测试环境:test
 
方法一:
为了让不同环境使用不同的变量,我们可以使用 process.env.NODE_ENV
这个值实际上是我们使用webapack的时候,通过它的DefinePlugin全局变量,根据环境注入不同的值,使得在不同环境下 process.env.NODE_ENV 的值分别是 ‘development’ 、’production’、’test’
此时我们全局注入的变量通过 if-else 或者 switch 进行变化即可(比如 if(process.env.NODE_ENV === 'development') )
方法二:
注意:此方法创建的变量名不能乱起,只有起固定格式的名字,才会被全局注入
官网注明:请注意,只有
NODE_ENV,BASE_URL和以VUE_APP_开头的变量将通过webpack.DefinePlugin静态地嵌入到客户端侧的代码中。这是为了避免意外公开机器上可能具有相同名称的私钥。
在项目根目录下新建三个文件,文件名分别是 .env.devlopment、.env.production、.env.test

// .env.test
VUE_APP_BASE_URL = ''
VUE_APP_BASE_NAME = 'allen'方案二此时在客户端侧代码中使用环境变量
只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:
console.log(process.env.VUE_APP_SECRET)
Vuex配合TS
createStore可以传入一个泛型 变成 createStore<T> 这个泛型 规定了 state的类型
import { createStore } from 'vuex'
interface IRootState {
  name: string
  age: number
}
export default createStore<IRootState>({
  state: () => {
    return {
      name: 'allen',
      age: 18
    }
  },
  mutations: {},
  actions: {},
  modules: {}
})
// 使用
import { useStore } from 'vuex';
import {IRootState} from '@/store/types';
export default defineComponent({
  setup() {
    const store = useStore<IRootState>()
    return {}
  }
})
但是此时我们即使里面已经定义了一些模块,ts还是会给我们报错,比如我们的store里面有个login模块
但是使用的时候ts会报错
store.state.login.name
此时就表现了vuex对TS的支持非常差(4.x版本)
不过pinia对TS的支持就相对好一点,用法也和vuex相似
当然coderwhy老师还是有解决的方法,比如导出vuex的时候,写多一个函数进行导出
vuex模块使用(new)
接口定义
import { ILoginState } from './login/types'
export interface IRootState {
  name: string
  age: number
}
export interface IStoreType extends IRootState {
  login: ILoginState
}
在store/index.ts文件中导出一个函数
import { createStore, Store, useStore } from 'vuex'
import { IRootState, IStoreType } from './types'
//....
//负责导出store(coderwhy老师的特殊处理)
export function useMyStore(): Store<IStoreType> {
  return useStore()
}
export default store
此时使用
import { defineComponent } from 'vue'
import { useMyStore } from '@/store'
export default defineComponent({
  setup() {
    const store = useMyStore()
    console.log(store.state.login.name) //此时使用起来没问题!
    return {}
  }
})
vuex的modules模块封装
在ts使用vuex中module来封装模块的时候,需要导入的Module类型声明也有讲究
Module类型类型声明需要传入两个泛型 Module<S, R>  S 为 模块中state的类型,R为根模块的state类型
遇到繁琐的接口定义
百度一下:json to ts
会自动转换,把json转换成ts语法,直接帮你写好
但是遇到复杂的数据类型,嵌套,它的转换仍然是有问题的。此时就只能用any了
TS结合Vue3的props
有时面对props传参的时候,我们需要特定类型来规范我们传入的参数,而单单靠props自身的type我们还不满足,此时我们可以使用 PropType
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
  props: {
    formItems: {
      type: Array as PropType<string[]>
    }
  },
  setup() {
    return {}
  }
})
</script>
此时我们可以在泛型里面定义接口,也是可以的
但是众所周知,在定义默认值的时候,复杂数据类型要写成一个函数的形式(《Vue(上)》篇章有提及)
但是如果是vue3 + ts的话,规定要写成一个箭头函数(使用this时)