Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

简介

Vite是由Vue公司开发的新型前端构建工具,由两部分组成:

  • 一个开发服务器,基于原生ES模块提供了丰富的内建功能,例如模块热更新(HMR)
  • 一套构建指,使用Rollup打包代码,

Vite可通过JS API和插件API进行拓展

搭建项目

1
2
3
4
npm init vite@latest
npm create vite@latest
yarn create vite
pnpm create vite

以上均可

这样创建会询问你项目名称和模板

同时也可以使用参数直接指定

1
2
3
4
npm create vite@latest my-vue-app -- --template vue # npm 7+
npm create vite@latest my-vue-app --template vue # npm 6.x
yarn create vite my-vue-app --template vue
pnpm create vite my-vue-app --template vue

执行过程

在项目搭建完成后我们可以使用

1
npm run dev

命令来运行项目

该命令首先回去寻找项目目录下的package.json文件中的scripts标签中的dev标签

在Vite项目中dev标签会被映射为vite

该命令首先会从本地的node_module中查找bin目录下的可执行vite

如果没有则会去全局目录中查找

Vue3补充

Vue3支持三种书写风格:

  • option API

  • setup函数

    • setup函数需要将数据返回:

      • <template>
            <div>
                {{ a }}
            </div>
        </template>
        
        <script>
            export default {
                setup() {
                    const a = 1
                    return {
                        a
                    }
                }
            }
        </script>
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15

        - 使用双花括号进行双向绑定

        - setup语法糖:

        - ```vue
        <template>
        <div>
        {{ a }}
        </div>
        </template>

        <script steup lan="ts">
        const a:number = 1
        </script>
    • 直接就能进行双向绑定,需要注意的是setup在单页组件(SFC)中只能有一个

v-once,即之渲染依次

v-memo="[]"list中接收一个值,配合v-for进行性能优化

ref全家桶

在Vue2的选项是API中我们定义响应式对象会这样创建:

1
2
3
4
5
6
7
8
9
<script lang="ts">
export default {
data() {
return {
age:18
}
}
}
</script>

但在组合式API中,我们则需要使用refreactive来注册响应式对象:

isRef可以判断一个对象是否为ref对象

shallowRef创建浅层次的响应式对象,即引用对象中的数值变化不会引起它的视图变化,需要地址发生变化才会引起视图变化

  • 需要注意的是如果在同一作用域对refshallowRef对象同时进行修改,shallowRef会受到ref的影响导致视图改变

triggerRef用于强制更新收集的依赖

  • 上面提到的使用ref影响shallowRef的原因是在更新ref对象时调用了triggerRef

customRef自定义Ref,接收一个回调函数作为参数,回调函数有两个参数track和trigger,分别用于收集和触发更新,该回调函数必须实现get和set方法

注意:

可以在浏览器的控制台设置中 > 首选项 > 控制台 > 启用自定义格式化程序

可以让Ref对象在输出时更简洁

此外在Vue2中介绍过使用ref获取DOM元素的功能,Vue3中同样可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<div ref="dom">
我是DOM
</div>
<button @click="change">
获取DOM
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dom = ref<HTMLDivElement>()

const change = () => {
// 此处使用?.断言来辅助TS推断
// 注意由于setUp标签渲染比DOM快,因此函数外读取将为undefined
// 因此可以放在点击事件中
console.log(dom.value?.innerText)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<div>
Ref: {{ Man }}
</div>
<div>
shallowRef: {{ Man3 }}
</div>
<button @click="change">
修改
</button>
</template>
<script lang="ts">
import { ref, isRef, shallowRef, triggerRef, customRef } from 'vue'
import type { Ref } from 'vue' // Ref的泛型

// 自定义Ref
function MyRef<T>(value:T) {
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newVal) {
value=newVal
trigger()
}
}
})
}


type M = {
name:string
}
// 直接使用ref
const Man = ref<M>({ name: 'Ender' })
// 使用Ref接口
const Man:Ref<M> = ref({ name: 'Ender' })
const Man2 = { name: 'Ender3' }
const Man3 = shallowRef({ name: 'Ender4' })
const customRefMan = MyRef<string>('customEnder')
const change = () => {
// 修改和取值时需要通过value访问其中的值
Man.value.name = 'Ender2'
// shallowRef使用这样的方式修改视图将不会同步改变
Man2.value.name = 'Ender2' // 但此处仍然会发生改变,因为收到了ref的影响
// shallowRef需要使用这样的方式修改
Man2.value = {
name: "Ender2"
}
// 但如果调用了triggerRef,即使是shallowRef也一样会被更新
triggerRef()
// 自定义Ref同样通过value的方式进行值修改
customRefMan.value = 'customEnder修改了'
console.log(isRef(Man2))
console.log(Man)
console.log(Man)
}
</script>

reactive全家桶

与ref一样,目的是将变量变为响应式对象,但和ref还是有区别的:

  • ref支持所有类型,但reactive只支持引用类型(Array, Object,Map,Set)(reactive同样支持传递泛型
  • reactive取值和赋值不需要使用.value进行访问,直接使用赋值语句进行修改即可
  • 但需要注意的是reactive对象不能直接赋值,原因是reactive对象是由proxy代理的,直接赋值将破坏响应式对象

readonly用来创建一个reactive对象的只读视图:

1
2
let obj = reactive({name:'Ender'})
const read = readonly(obj)

其中read对象的属性是无法进行赋值操作的,但read对象会随着obj对象的改变而改变

shallowReactive用于创建一个浅层响应式对象,与reactive的区别参考shallowRefref的区别:

  • shallowReactive的响应式只会添加到对象的第一层属性,即shallowObj.attr,之后层级的属性没有响应式,例如shallowObj.attr.attr2
  • 同样的当shallowReactivereactive的域相同时,也会受到reactive的影响而发生改变

to全家桶

  • toRef
    • 接收两个参数(obj, key)
    • 其中obj为一个响应式对象(传入非响应式对象将不会产生影响)
      • 原因是使用refreactive创建的响应式对象中的get和set方法会调用track收集依赖和trigar更新依赖
      • 因此在toRef返回的响应式对象中并没有做这两个操作,目的是防止传入响应式对象时收集和更新了两次
      • 所以非响应式对象即使传入toRef函数,返回的对象中也并不会调用收集和更新函数
    • key为一个属性key
    • 作用是可以让响应式obj中的某个属性变为响应式
    • 使用场景为:
      • 将对象中的某个属性包装为一个响应式对象提供给外部使用,而不暴露整个对象
      • 有时需要将响应式对象中的某些属性解构出来传递给某个函数,由于解构操作使得属性失去响应式,此时需要使用它将这些属性变为响应式的
  • toRefs
    • 指定了泛型为Object
    • 接收一个Object类型的参数
    • 即将这个对象中的所有属性均调用toRef变为响应式属性后,将整个新对象返回
    • 使用场景:
      • 需要对整个对象进行解构取值时
      • let { name, age, like } = toRefs(obj)此时name、age、like均为响应式
      • 如果此处不调用toRefs函数,得到的变量将不是响应式的,即修改将不会被更新
  • toRaw
    • 接收一个Object类型的参数
    • 将传入的响应式Object转化为普通对象,即取消响应式
    • 源码中就是返回了响应式对象中的__v_raw属性的值

Vue3响应式原理

Vue2中使用Object.defineProperty实现响应式

Vue3中则使用Proxy来实现

  • vue2的不足
    • Vue2中的方法只能劫持设置好的数据,新增的数据需要使用Vue.set()
    • 数组只能操作种方式,修改某一项值无法劫持

下面是Vue3的响应式实现:

effect.ts用于处理数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

// TODO: effect函数用于处理DOM渲染与数据的绑定
// TODO:effect函数接收一个用于DOM渲染的函数
// TODO: 定义一个全局变量来存储effect,便于函数外进行收集
// TODO: 定义一个脏数据标记方法
interface Options {
schedule?: Function
}
let activeEffect;
export const effect = (fn:Function, option: Options) => {
const _effect = function() {
// TODO: 将fn的作用域提升使得外界可以访问
activeEffect = _effect
// TODO: 渲染DOM
let res = fn()
return res
}
// 在effect中记录是否需要清除脏数据标记位的操作
_effect.options = option
_effect()
// TODO: 将函数返回用于在Computed对象中渲染
return _effect
}

// TODO: 实现track收集依赖
// TODO:接收一个对象以及对象中的一个属性值
// TODO: 一个全局的Map用于构建Object到Map的映射
const targetMap = new WeakMap()
export const track = (target, key) => {
// TODO: 取出target对象所对应的属性列表
let depsMap = targetMap.get(target)
if(!depsMap) {
// TODO: 第一次调用如果没有则添加
depsMap = new Map()
targetMap.set(target, depsMap)
}
// TODO: 如果有则将其中的key属性名对应的effect函数set取出
let deps = depsMap.get(key)
if(!deps) {
// 如果没有就添加
deps = new Set()
depsMap.set(key, deps)
}

// TODO: 如果有则将effect函数加入到deps中,收集结束
deps.add(activeEffect)
}

// TODO: 实现trigger触发更新函数
// TODO: 接收一个对象以及对象中的一个属性值
export const trigger = (target, key) => {
// TODO: 根据对象找到属性MAP
let depsMap = targetMap.get(target)
// TODO: 没有则报错
if(!depsMap) {
console.error("target is not a effect object")
return
}
// TODO: 存在则取出key对应的effect函数SET
let deps = depsMap.get(key)
// TODO: 如果没有则报错
if(!deps) {
console.error("target.key is not a effect value")
return
}
// TODO: 存在则重新调用其中的所有effect函数
deps.forEach( effect => {
// TODO: 在触发更新时判断这一步更新是否有数据因此变为脏数据,有则重置其脏数据标记位
if(effect?.options?.schedule) {
effect?.options?.schedule?.()
} else {
effect()
}
})
}

reactive.ts用于处理响应式对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// TODO: 引入收集和触发机制
import { track, trigger } from "./effect"

// TODO: 一个用于判断属性是否为对象的函数
const isObject = (target:any) => {
return target!=null && typeof target == 'object'
}


// TODO: reactive实现
// TODO: reactive创建一个函数,接收一个对象,并使用泛型限制参数为对象
export const reactive = <T extends object>(target:T) => {
// TODO: 创建一个proxy对象来代理原有对象
return new Proxy(target, {
// TODO: 拦截get
get(target, propKey, receiver) {
// TODO: 返回对象中的参数,此处使用Reflect中的参数来保证上下文的正确性
let res = Reflect.get(target, propKey, receiver) as object
// TODO: 访问对象属性 后 需要进行资源回收
track(target, propKey)
// TODO: 如果属性是对象,怎需要递归创建
if(isObject(res)) {
return reactive(res)
}
return res
},
set(target, propKey, value, receiver) {
// TODO: 对对象属性进行设置,此处使用Reflect中的参数来保证上下文的正确性
let res:boolean = Reflect.set(target, propKey, value, receiver)
// TODO: 设置对象属性 后 需要进行依赖更新
trigger(target, propKey)
return res
}
})
}

computed.ts用于实现计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { effect } from "./effect"

// TODO: 实现计算属性,接收一个getter函数
export const computed = (getter:Function) => {
// TODO: 记录getter方法的响应式函数
let _value = effect(getter, {
schedule: () => {
// TODO: 回调函数中将数据标记为脏数据
_dirty = false
}
})
// TODO: 设置缓存机制
let _dirty = true
let cacheValue
class ComputedRefImpl {

// TODO: 拦截计算属性的访问属性请求
get value() {
// TODO: 如果为脏数据,则重新计算结果并更新缓存
if(_dirty) {
cacheValue = _value()
_dirty = false
}
// TODO: 如果不为脏数据,则直接返回缓存结果
return cacheValue
}
}

// TODO: 返回
return new ComputedRefImpl
}

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head></head>

<body>
<div id="app"></div>
<script type="module">
import { reactive } from './reactive.ts'
import { effect } from "./effect.ts"
import { computed } from "./computed.ts"

const user = reactive({
name: "Ender",
age: "23",
foo: {
bar: {
sss: 123
}
}
})

effect(() => {
document.querySelector('#app').innerText = `${user.name} -- ${user.age} -- ${user.foo.bar.sss}`
})

window.a = reactive({
name: 'a',
age: 18
})
// 计算属性
window.b = computed(() => {
console.log('重新计算')
return a.age + 10
})
</script>
</body>
</html>

computed计算属性

计算属性即当依赖发生改变时,就会触发他的更新,如果依赖值不变,则使用缓存中的属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div>
<div>
姓:<input v-model="firstName" type="text">
</div>
<div>
名:<input v-model="lastName" type="text">
</div>
<div>
全名: {{ name }}
</div>
<button @click="changeName">
changeName
</button>
</div>
</template>
<script setup lang='ts'>
import { ref, computed } from 'vue'
let firstName = ref('张')
let lastName = ref('三')
// 1. 选项式写法 支持一个对象传入get、set方法
// name会随着firstName和lastName的变化而变化
let name = computed<String>({
get () {
return firstName.value + '-' + lastName.value
},
set (newVal) {
// 将得到的值解构赋值给firstName和lastName
[firstName.value, lastName.value] = newVal.split('-')
}
})
const changeName = () => {
// name变量可以直接赋值,赋值时会被computed中的set拦截
name.value = 'Ender-Xiao'
}
// 函数写法 只能支持一个gat函数,不允许修改
// 使用computed创建的对象是readonly的
let name = computed(()=> firstName.value + '-' + lastName.value)

</script>

注意:

需要注意的是使用computed函数创建的计算属性是readonly的,无法进行修改

watch监听器

  • watch(obj, (newVal, oldVal) => {}, {deep:true})
    • obj为一个响应式对象
    • 第二个参数接收一个回调函数,当obj发生变化时调用该函数
      • 其中newVal为改变后的值,oldVal为改变前的值
    • obj可以传入一个响应式对象数组,此时将会监听数组中的每一个对象的变化
      • 并且此时的newVal和oldVal都将变为数组
    • 第三个参接收一个对象options,其中包含一些可选项
      • deep
        • 当obj为一个嵌套多层的对象且,为ref对象时,需要在options中将深度监听deep开启
        • 如果obj是一个reactive对象,那么则不需要开启deep
        • 但处理引用对象时,newVal和oldVal中的值将会相同
      • immediate
        • 默认为false
        • 为true时立即调用传入的第二个参数回调函数
      • flash
        • 默认为"pre",表示组件更新之前执行回调
        • "sync"表示与组件更新同步执行回调
        • "post"表示组件更新之后执行回调
    • 当需要监听响应式对象中的某一个属性时则需要使用get函数的形式传递:
      • watch(()=>obj.attr1.attr2.name, (newVal, oldVal)=>{})
      • 此时newVal为新值,oldVal为旧值
  • watchEffect高级监听器
    • const stop = watchEffect((oninvailidate)=>{oninvailidate(() => {console.log("before")})console.log(message.value)}, {})
    • 该函数接收一个回调函数
      • 该回调函数中直接使用某一变量,那么该变量值的改变就将会被监听
      • 回调函数接收一个oninvailidate函数
        • 该回调函数接收另一个回调函数
        • 在这个回调函数中的代码将始终在其监听的变量值发生变化之前运行
      • 接收一个option对象
        • flash
          • 默认为"pre",表示组件更新之前执行回调
          • "sync"表示与组件更新同步执行回调
          • "post"表示组件更新之后执行回调
        • onTrigger
          • onTrigger(e){debugger}可以提供调试时使用

组件与生命周期

Vue3与2的区别在于,Vue3中在setup中引用组件后无需注册

而Vue3组件的生命周期区别在于Vue3的setup中的组件没有beforeCreatecreated两个生命周期,而使用setup代替

Vue3组件生命周期

  1. setup代码块外面的部分
  2. onBeforeMount获取不到DOM
  3. onMounted能获取到DOM
  4. onBeforeUpdate获取更新前的DOM
  5. onUpdated获取更新后的DOM
  6. onBeforeUnmount
  7. onUnmounte
  8. onRenderTracked((e)=>{})在依赖收集完成后触发,即访问响应式对象的属性时obj.attr
  9. onRenderTriggered((e) => {})在触发更新时触发,即修改响应式对象的属性时obj.attr=value

开发时可以使用const instance = getCurrentInstance()来获取当前组件的window对象,而生命周期函数实际上就是挂载到window对象上的函数,因此可以通过这种方式来检测生命周期函数是否挂载上

函数柯里化

生命周期函数的实现使用了函数柯里化技术,因为生命函数创建时都由createHook方法创建,并由名称区别,而其他的参数在同一项目中始终保持不变,因此需要进行函数柯里化来保存部分参数,而这一技术实际上就是使用了函数闭包的特性将函数和其外部变量一起保存:

柯里化(Currying)又称部分求值,一个柯里化的函数首先会接收一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

简单来说就是:柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)或者f(a, b)(c)或者f(a)(b, c)

例如下面这个日志函数:

1
2
3
4
5
6
7
// 参数:date, project, message
const log = (date, project, message) => {
return `${date} ${project} ${message}`
}

const logMsg = log('2022-07-29', 'xxx后台管理系统', 'mm接口异常');
console.log(logMsg) // 输出 2022-07-29 xxx后台管理系统 mm接口异常

但通常当天日期是不变的,同一个项目的项目名也是不变的(不过不同的项目名是变化的),唯有信息是时刻变化

如果每次都把所有参数全部传入进去会有很多重复

因此可以对它进行柯里化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const log = (date) => {

return (projectName) => {

return (message) => {

return `${date} ${projectName} ${message}`

}

}

}


/* 如果日期、项目名、信息都不同的情况下输出日志 */
// 日期为“2022-07-29”,项目名为“A项目”,输出日志
const logMsg1 = log('2022-07-29')('A项目')('接口报错');
console.log(logMsg1); // 打印 2022-07-29 A项目 接口报错
// 日期为“2022-07-29”,项目名为“A项目”,输出日志
const logMsg2 = log('2022-08-01')('B项目')('接口成功');
console.log(logMsg2); // 打印 2022-08-01 B项目 接口成功


/* 如果日期相同,项目名、信息不同的情况下输出日志 */
const sameDateLog = log('2022-07-29');
// 项目名为“A项目”,输出日志
const logMsg3 = sameDateLog('A项目')('接口异常');
console.log(logMsg3); // 打印 2022-07-29 B项目 接口异常
// 项目名为“B项目”,输出日志
const logMsg4 = sameDateLog('B项目')('接口超时');
console.log(logMsg4); // 打印 2022-07-29 B项目 接口超时


/* 如果日期、项目名相同,信息不同的情况下输出日志 */
const sameDateProjectNameLog = log('2022-07-29')('A项目');
// 输出日志
const logMsg5 = sameDateProjectNameLog('网络异常')
console.log(logMsg5); // 打印 2022-07-29 A项目 网络异常

可以利用递归来封装一个把任意函数变为柯里化函数的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 函数柯里化,利用递归和闭包实现
const curry = function(fn) {
const len = fn.length; // 获取初始函数fn的形参个数

// curry返回改造后的函数
return function t() {
const innerLength = arguments.length; // 获取t的实参个数
const args = Array.prototype.slice.call(arguments); // 将类数组arguments对象转为真正的数组(类数组arguments对象是函数传入的实际参数,类似数组,拥有数组属性,但不是数组)

if (innerLength >= len) { // 递归出口,如果t实参个数已经大于fn形参个数,则终止递归
return fn.apply(undefined, args) // 执行改造后的函数

} else { // 如果t的实参个数少于fn的形参个数,说明柯里化并没有完成,则继续执行柯里化
return function () {
const innerArgs = Array.prototype.slice.call(arguments); // 将类数组arguments对象转为真正的数组(类数组arguments对象是函数传入的实际参数,类似数组,拥有数组属性,但不是数组)
const allArgs = args.concat(innerArgs);
return t.apply(undefined, allArgs)
}
}
}
}

// 测试
function add (num1, num2, num3, num4, num5) {
return num1 + num2 + num3 + num4 + num5;
}


const finalFun = curry(add);
const result1 = finalFun(1)(2)(3)(4)(5);
const result2 = finalFun(1, 2)(3)(4)(5);
const result3 = finalFun(1,2,3)(4)(5);
const result4 = finalFun(1,2,3)(4, 5);

console.log(result1, result2, result3, result4); // 15 15 15 15
柯里化经典面试题

请实现一个add函数实现以下功能 :

1
2
3
4
5
6
7
add(1) // 1
add(1)(2) // 3
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
add(1)(2,3) // 6
add(1,2)(3) // 6
add(1,2,3) // 6
函数柯里化应用场景
  1. 参数复用:即如果函数有重复使用到的参数,可以利用柯里化,将复用的参数存储起来,不需要每次都传相同的参数
  2. 延迟执行:传入参数个数没有满足原函数入参个数,都不会立即返回结果,而是返回一个函数。(bind方法就是柯里化的一个示例)
  3. 函数式编程中,作为compose, functor, monad 等实现的基础

优点:

  1. 柯里化之后,我们没有丢失任何参数:log 依然可以被正常调用。
  2. 我们可以轻松地生成偏函数,例如用于生成今天的日志的偏函数。
  3. 入口单一。
  4. 易于测试和复用。

缺点:

  1. 函数嵌套多
  2. 占内存,有可能导致内存泄漏(因为本质是配合闭包实现的)
  3. 效率差(因为使用递归)
  4. 变量存取慢,访问性很差(因为使用了arguments)

组件间通信

父传子props

vue3中使用props传值

父组件通过v-bind(:)将要传递的参数进行绑定

1
2
3
4
<!-- template -->
<waterFallVue :title="name"></waterFallVue>
<!-- script -->
let name="ender"

而子组件中如果处于setup模式下,则需要使用defineProps()宏来声明,其中声明的变量是可以直接在模板中使用的,但在宏外的js作用域内,需要通过其返回值props来访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
{{ title }}
</div>
</template>
<!-- script setup -->
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
default: "默认值"
}
})
console.log(props.title)
</script>

在非setup模式下,则需要在setup中接收props参数:

1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
props: {
title: String
},
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.title)
}
}
</script>
TS泛型

值得一提的是,结合TS泛型机制,可以更方便,更整体化的去声明参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const props = defineProps<{
title: String
}>()
console.log(props.title)

//TS 定义默认值需要使用withDefaults,该函数第二个参数接收一个对象来定义默认值
const props = withDefaults(defineProps<{
title: String,
arr: number[]
}>(), {
// 引用数据类型需要通过函数防止引用
arr:()=>[6]
title: "默认值"
})

子传父

emit自定义事件方式
  1. 子组件声明自定义事件
  2. 在自定义事件中访问子组件自己的变量
  3. 父组件将自己的函数fun回调函数的方式传递给子组件的事件
  4. 子组件将需要传递的数据data作为回调函数的参数,调用回调函数fun
  5. 父组件即可在函数fun中接收到子组件的值

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const emit = defineEmits(['on-trans-data'])
const transData = "我是被传递的参数"
const send = () => {
emit('on-trans-data', transData)
}

// ts中的泛型写法如下
const emit = defineEmits<{
(e:"on-trans-data",name:string):void
}>()
const transData = "我是被传递的参数"
const send = () => {
emit('on-trans-data', transData)
}

父组件

1
2
3
4
5
6
7
8
9
10
<template>
<waterFallVue @on-trans-data="getData"></waterFallVue>
</template>

<script setup lang="ts">
const getData = (data:String) => {
// data为子组件中的数据
console.log(data)
}
</script>
defiineExpose方法暴露方法or属性

子组件

1
2
3
4
defineExpose({
data: "Hello",
open: () => console.log(1)
})

而父组件则要使用ref来获取暴露的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<waterFallVue ref="waterFall"></waterFallVue>
</template>

<script setup lang="ts">
import waterFallVue from './components/water-fall.vue'
import { onMounted } from 'vue'
onMounted(()=>{
// 此处需要利用TS的InstanceType来推断类型
const waterFall = ref<InstanceType<typeof waterFallVue>>()
console.log(waterFall.value.data)
})
</script>

使用defineProps实现瀑布流组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<template>
<div id="waterFall" class="ender-waterfall">
<div :style="{height: item.height + 'px', background: item.background, left: item.left + 'px',top: item.top + 'px'}" v-for="(item, index) in waterFallList" :key="index" class="ender-waterfall__items"></div>
</div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'

// TODO: 接收父组件传来的数据
const props = withDefaults(defineProps<{
list: any[]
}>(),{
list: ()=>[{height: 300, background: 'red'}]
})
// TODO: 维护需要渲染的元素
const waterFallList = reactive<any[]>([])
// TODO: 维护高度
const heightList:number[] = []

//TODO: 初始化父组件传来的参数
const init = () => {
// TODO: 清空数据
waterFallList.length = 0
heightList.length = 0
const width = 130
// TODO: 获取可是区域的宽度
const x = document.getElementById("waterFall")?.clientWidth
const column =Math.floor(x===undefined? 0: x / width)
console.log(column)
// TODO: 根据列数从props中拿出第一行
for (let i = 0; i < props.list.length; i++) {
if(i<column){
// TODO: 第一列元素按顺序放
props.list[i].left = i * width
props.list[i].top = 10
waterFallList.push(props.list[i])
heightList.push(props.list[i].height + 10)
}else {
// TODO: 之后的元素放在高度最小的列
let minHeightIndex = 0;
let minHeight = heightList[0];
heightList.forEach((h, index) => {
if(h < minHeight){
minHeightIndex = index
minHeight = h
}
})
// TODO: 计算top和left
props.list[i].left = minHeightIndex * width
props.list[i].top = minHeight + 10
// TODO: 更新需要渲染的元素
waterFallList.push(props.list[i])
// TODO: 更新高度
heightList[minHeightIndex] = minHeight + props.list[i].height + 10
}
}
}

const resizeHandler = () => {
init()
}

onMounted(()=>{
init()
// TODO: 监听窗口大小改变事件
// 防抖函数
const debounce = (fn:Function, delay:number) => {
let timer:any = null
return function() {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn()
}, delay)
}
}
// 触发事件
const cancelDebounce = debounce(resizeHandler, 100)
window.addEventListener("resize", cancelDebounce)
})
</script>

<style lang="scss" scoped>
@include b(waterfall){
position: relative;
width: 100%;
@include e(items){
position: absolute;
width: 120px;
}
}
</style>

兄弟传参

兄弟间传参有两种方式:

  1. 父组件介入
  2. 事件总线BUS

父组件介入传参理解起来相对容易,但写起来很复杂:

  1. 父组件使用emit为子A派发事件
  2. 子A利用父组件派发的事件将数据data传递给父组件
  3. 父组件使用props为子B传递数据data

这样组件A就能通过父组件派发的事件修改数据data,组件B也能监听到数据data的变化

父组件介入

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<A @trans-data-to-b="getMessageFromA"></A>
<B :message="message"></B>
</div>
</template>

<script setup lang="ts">
import A from "./A.vue"
import B from "./B.vue"
import { ref } from "vue";

let message = ref("")
const getMessageFromA = (params: string) => {
message.value = params
}
</script>
<style>
</style>

子组件A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<button @click="emitToB">给B传递一些信息</button>
</div>
</template>

<script setup lang="ts">
const emit = defineEmits(['trans-data-to-B'])
let message = ""
const emitToB = () => {
message = "来自A的信息"
emit('trans-data-to-B', message)
}
</script>
<style>
</style>

子组件B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{ messageFromA }}
</div>
</template>

<script setup lang="ts">

type Props = {
messageFromA: string
}

defineProps<Props>()
</script>
<style>
</style>
事件总线BUS

利用发布订阅设计模式设计一个用于事件管理的类:

Bus.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// TODO: 事件总线接口
type BusClass = {
emit:(name: string) => void
on:(name: string, callback: Function) => void
}

// TODO: 事件名称类型
type ParamKey = string | number | symbol

// TODO: 将事件名称与回调函数对应
type eventList = {
[key: ParamKey]: Array<Function>
}

class Bus implements BusClass {
list: eventList
constructor() {
// 初始化事件列表
this.list = {}
}
// TODO: 事件调用时,找到事件名称对应的回调函数,分别执行事件名对应的所有回调函数
emit(name:string, ...args:Array<any>) {
let eventList: Array<Function> = this.list[name]
eventList.forEach(fn => {
fn.apply(this, ...args)
})
}
// TODO: 将事件名与回调函数相对应,即事件的实际处理操作
on(name:string, callback:Function) {
let eventList:Array<Function> = this.list[name] || []
eventList.push(callback)
this.list[name] = eventList
}
}
export default new Bus()

子组件A(A.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<button @click="emitToB">给B传递一些信息</button>
</div>
</template>

<script setup lang="ts">
import Bus from "../../Bus"
let message = ""
const emitToB = () => {
message = "来自A的信息"
// 注册事件
Bus.emit('trans-data-to-B', message)
}
</script>
<style>
</style>

子组件B(B.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
{{ messageFromA }}
</div>
</template>

<script setup lang="ts">
import Bus from '../../Bus';
import { ref } from 'vue';
let messageFromA = ref("")
Bus.on('trans-data-to-B', (message:string) => {
messageFromA.value = message
})
type Props = {
messageFromA: string
}

defineProps<Props>()
</script>
<style>
</style>

全局组件与递归组件

全局组件

定义全局组件需要在main.ts中对组件进行引入完成后即可在项目中的任何组件进行不引入的使用

此外还可以批量注册全局组件:

1
2
3
4
5
6
7
// 借鉴ElementUI中的全局注册Icon组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}

递归组件

对于树形结构的数据,我们可能需要使用递归的组件来对其进行渲染,例如如下形式的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
const data = reactive<TreeList[]>([
{
name: "no.1",
children: [
{
name: "no.1-1",
children: [
{
name: "no.1-1-1",
},
],
},
],
},
{
name: "no.2",
children: [
{
name: "no.2-1",
},
],
},
{
name: "no.3",
},
]);

在VUE3中,可以直接使用文件名作为递归组件自己的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Tree.vue -->
<templat>
<div class="tree" v-for="item in data">
<input type="checkbox"> <span>{{ item.name }}</span>
<Tree v-if="item?.children?.length" :data="item.children"></Tree>
</div>
</templat>

<script setip lang='ts'>
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
</script>

此外还可以使用一个额外的script来将组件变为具名组件,然后通过组件名来调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- Tree.vue -->
<templat>
<div class="tree" v-for="item in data">
<input type="checkbox"> <span>{{ item.name }}</span>
<!-- 使用name调用组件自身 -->
<tree v-if="item?.children?.length" :data="item.children"></tree>
</div>
</templat>

<!-- 定义组件name -->
<script lang='ts'>
export default {
name: "tree"
}
</script>

<script setip lang='ts'>
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
</script>

第三种方式在Vue3.3之前需要使用插件unplugin-vue-define-options,之后的版本可以直接使用defineOptions编译宏来定义组件的name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- Tree.vue -->
<templat>
<div class="tree" v-for="item in data">
<input type="checkbox"> <span>{{ item.name }}</span>
<!-- 使用name调用组件自身 -->
<tree v-if="item?.children?.length" :data="item.children"></tree>
</div>
</templat>

<script setip lang='ts'>
<!-- 使用defineOptions定义组件name -->
defineOptions({
name: "tree"
})
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
</script>
递归组件事件

可以对递归组件定义事件,此时需要注意事件冒泡的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- Tree.vue -->
<templat>
<!-- 此处注意阻止组件事件冒泡,或者在父组件使用代理来处理组件点击事件 -->
<div @click.stop='clickTap(item, $event)' class="tree" v-for="item in data">
<input type="checkbox"> <span>{{ item.name }}</span>
<!-- 使用name调用组件自身 -->
<tree v-if="item?.children?.length" :data="item.children"></tree>
</div>
</templat>

<script setip lang='ts'>
<!-- 使用defineOptions定义组件name -->
defineOptions({
name: "tree"
})
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
const clickTap = (item: Tree, e) => {
console.log(item)
console.log(e.target)
}
</script>

传送组件Teleport

Teleport组件可以将其内部的组件通过一个css选择器,传送到所选择的元素内:

to属性接收一个css选择器字符串

disable接收一个boolean值控制传送的开启和关闭

1
2
3
<Teleport :disable="false" :to="body">
<A></A>
</Teleport>

动画组件Transition

transition可以给以下情况添加过度动画:

  • v-if
  • v-show
  • 动态组件
  • 组件根节点

基本用法

transition提供一个name属性,该属性用于将组件与css样式相对应,该组件提供6个默认对应的样式命名:

  1. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
  4. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

例如定义一个组件:

1
2
3
4
<button @click='flag = !flag'>切换</button>
<transition name='fade'>
<div v-if='flag' class="box"></div>
</transition>

则对应的css动画就可以这样设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//开始过度
.fade-enter-from{
background:red;
width:0px;
height:0px;
transform:rotate(360deg)
}
//开始过度了
.fade-enter-active{
transition: all 2.5s linear;
}
//过度完成
.fade-enter-to{
background:yellow;
width:200px;
height:200px;
}
//离开的过度
.fade-leave-from{
width:200px;
height:200px;
transform:rotate(360deg)
}
//离开中过度
.fade-leave-active{
transition: all 1s linear;
}
//离开完成
.fade-leave-to{
width:0px;
height:0px;
}

结合第三方动画库

除了默认的样式匹配方式,还可以通过props来自定义动画和css样式的对应关系,这样可以方便我们使用第三方动画库:

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class
1
2
3
4
5
6
7
8
<template>
<transition enter-active-class="animate__animated animate__bounceInLeft" name='fade'>
<div v-if='flag' class="box"></div>
</transition>
</template>
<script setup lang="ts">
import 'animate.css'
</script>

也可以设置动画进入和离开的持续时间:

1
2
3
4
<transition :duration="1000">...</transition>


<transition :duration="{ enter: 500, leave: 800 }">...</transition>

transition包含8个生命周期

1
2
3
4
5
6
7
8
@before-enter="beforeEnter" //对应enter-from
@enter="enter"//对应enter-active
@after-enter="afterEnter"//对应enter-to
@enter-cancelled="enterCancelled"//显示过度打断
@before-leave="beforeLeave"//对应leave-from
@leave="leave"//对应enter-active
@after-leave="afterLeave"//对应leave-to
@leave-cancelled="leaveCancelled"//离开过度打断

当只用 JavaScript 过渡的时候,在 enterleave 钩子中必须使用 done 进行回调

回调函数将会在这一生命结束前被调用

结合gsap 动画库使用 GreenSock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup lang="ts">
import {ref} from 'vue'
import gsap from 'gsap'
const BeforeEnter = (el: Element) => {
console.log('进入之前from', el);
gsap.set(el, {
width: 0,
height: 0
})
}
const Enter = (el: Element,done: gsap.Callback) => {
console.log('过度曲线');
gsap.to(el, {
width: 200px,
height: 200px,
onComplete: done
})
}
const AfterEnter = (el: Element) => {
console.log('to');
}
const EnterCanceller = (el: Element) => {
console.log('过度效果被打断');
}
</script>
<template>
<transition @before-enter="BeforeEnter" @enter="Enter" @after-enter="AfterEnter" @enter-cancelled="EnterCancelled">
</transition>
</template>

apper属性

通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态

1
2
<transition appear appear-active-class="from" appear-from-class="active" appear-to-class="to">
</transition>

过度列表transition-group

用法和transition几乎相同,用于渲染整个列表

  • 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag attribute 指定渲染一个元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素总是需要提供唯一的 key attribute 值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

使用move-class制作的洗牌动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<button @click="shuffleMatrix">shuffle</button>
<transition-group move-class="ender-shuffleWraps--moveAnimate" class="ender-shuffleWraps" tag="div">
<div class="ender-shuffleWraps__items" v-for="item in list" :key="item.id">
{{item.number}}
</div>
</transition-group>
</div>
</template>

<script setup lang="ts">
import { ref, reactive} from 'vue'
import _ from 'lodash'

// 利用Array.apply生成一个长度为81,且值初始化为undefined的数组
const list = ref(Array.apply(null, {length: 81} as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1
}
}))

const shuffleMatrix = () => {
list.value = _.shuffle(list.value)
}

</script>

<style lang="scss">
@include b(shuffleWraps) {
display: flex;
flex-wrap: wrap;
width: calc(20px * 9 + 18px);
@include e(items) {
width: 20px;
height: 20px;
background: #ccc;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
margin: 1px;
}
@include m(moveAnimate) {
transition: all 0.3s;
}
}
</style>

自定义指令

Vue中提供了v-forv-if等等指令同时也支持自定义指令,自定义指令属于破坏性更新

Vue3的自定义指令也包含一些钩子函数

Vue3指令钩子函数

Vue3的自定义指令钩子函数与Vue3组件生命周期相同

  • created 元素初始化的时候
  • beforeMount 指令绑定到元素后调用 只调用一次
  • mounted 元素插入父级dom调用
  • beforeUpdate 元素(虚拟DOM)被更新之前调用
  • update 这个周期方法被移除 改用updated
  • beforeUnmount 在元素被移除前调用
  • unmounted 指令被移除后调用 只调用一次

而Vue2中指令生命周期为:bind,inserted,update,componentUpdate,unbind

自定义指令钩子函数

首先可以在setup语法糖中声明局部自定义指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<tmplate>
<div>
<button @click="flg = !flg">
切换状态
</button>
<!-- 使用自定义钩子 -->
<!-- 自定义钩子同样允许自定义参数,以及自定义修饰符 -->
<A v-if="flg" v-move:aaa.ender="{background: 'red'}"></A>
</div>
</tmplate>

<script setup lang="ts">
import A from "./components/A.vue"
import {ref, Directive, DirectiveBinding} from "vue"
let flg = ref<boolean>(true)
type Dir ={
background: string
}
// 自定义指令的命名必须以v开头
// 类型为Directive
const vMove:Directive = {
// 传入自定义钩子的参数在所有生命周期函数中都能作为参数接收到
created() {
console.log("========>created")
},
beforeMount() {
console.log("========>beforeMount")
},
// 接收到的参数以及修饰符都包含在DirectiveBinding类型中,该类型接收一个泛型辅助推导
mounted(el:HTMLElement, dir:DirectiveBinding<Dir>) {
console.log("========>mounted")
el.style.background = dir.value.background
},
beforeUpdate() {
console.log("========>beforeUpdate")
},
updated() {
console.log("========>updated")
},
beforeUnmount() {
console.log("========>beforeUnmount")
},
unmount() {
console.log("========>unmount")
}
}
</script>

<style scoped>
.A {
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
</style>

函数简写

当并不需要自定义指令的全部钩子函数时,可以使用函数简写使用部分钩子函数

例如下面这个使用自定义指令实现按钮级的鉴权功能的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<tmplate>
<div class="btns">
<button v-has-show="shope:edit">创建</button>
<button v-has-show="shope:create">编辑</button>
<button v-has-show="shope:delete">删除</button>
</div>
</tmplate>

<script setip lang="ts">
import {ref, reactive} from "vue"
import type {Directive} from "vue"

// 模拟用户ID存储
localStorage.setItem('userId', 'ender')
// mock后台返回的数据
// 此处使用京东标准,ID:页面:权限
const permission = [
'ender:shope:edit',
'ender:shope:create',
'ender:shope:delete'
]
// 取出用户ID
const userId = localStorage.getItem('userId') as string
// 创建自定义指令
// 函数式自定义指令,接收两个参数el和binding,分别代表被指令绑定的DOM元素,以及指令绑定的值
// 可以通过Directive接收的两个泛型指定类型
const vHasShow:Directive<HTMLElement, string>(el, binding) {
// 拼接得到用户权限信息字符串
if(!permission.includes(userId + ":" + binding.value)) {
el.style.display = 'none'
}
}
</script>

<style scope lang='less'>
.btns{
button {
margin: 10px
}
}
</style>

自定义指令实现图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<tmplate>
<div>
<div>
<img v-lazy="item" v-for="item in arr" width="360" height="500" alt="">
</div>
</div>
</tmplate>

<script setup lang="ts">
import {ref, reactive} from "vue"
import type { Directive } from "vue"
// 使用vue提供的函数批量引入图片
// meta找有两个方法:glob和globEager
// glob是懒加载的,返回的是一个函数,import语句用于函数会触发代码分包
// let modules = {"xxx": ()=> import("xxx")}
// globEager为静态加载
// import xxx from 'xxx'
// 使用TS中的Record工具定义一个对象类型,接收两个泛型,分别表示key和value的类型
let imgList:Record<string, { default: string }> = import.meta.glob('./assets/images/*.*')
let arr = Object.values(imageList).map((v) => v.default)

// 自定义指令
const vLazy:Directive<HTMLImgElement, string> = async (el, binding) => {
// 加载默认显示的图片
const def = await import('./assets/vue.svg')
el.src = def.default
// 借助js新增的IntersectionObserver方法监听元素是否出现在可视窗内
const observer = new IntersectionObserver((enr) => {
// 回调函数返回一个entrece数组,取其第一个元素
// inersectionRatio为一个number变量,用于记录元素在可视窗口中的比例
if(enr[0].inersectionRatio > 0) {
// 出现在视窗中则需要加载真正的图片
el.src = binding.value
// 赋值完成后停止监听避免反复加载
observer.unobserver(el)
}
})
// 开启事件监听
observer.observer(el)
}
</script>

<style scoped lang="less">
</style>

自定义hooks

hook即将文件的一些单独功能的js代码抽离出来进行封装的方式,是函数的一种写法。

vue2中使用mixin来实现类似的功能但不同的是hook是函数,mixin返回的是一个对象

hook的经典应用案例是Vue3 hook 库Get Started | VueUse

下面以一个实现图片转base64的功能演示hook的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { onMounted } from "vue";

type Options = {
el: string // 用与检索图片标签的字符串
}

type Return = {
baseURL: string | null
}

export default function(options: Options): Promise<Return> {
const toBase64 = (el: HTMLImageElement):string => {
// TODO: 创建canvas对象将图像画进去后利用toDataURL方法转base64编码
const canvas: HTMLCanvasElement = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = el.width
canvas.height = el.height
ctx.drawImage(el, 0, 0, canvas.width, canvas.height)

return canvas.toDataURL('image/png')
}
return new Promise((resolve) => {
onMounted(() => {
const file: HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
file.onload = (): void => {
resolve({
baseURL: toBase64(file)
})
}
})
})
}

接下来只需要在组件中引入并使用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<img id="img" width="300" height="300" src="./img/test.png" />
</template>

<script setup lang="ts">
import useImageToBase64 from "./hooks"
useImageToBase64({el: "#img"}).then((res) => {
console.loh(res.baseURL)
})
</script>

<style>
</style>

自定义hook与自定义指令组合

下面通过一个案例来熟悉vite打包,自定义hook自定义指令以及如何发布到npm

项目构建

首先构建项目,创建src文件,在其下方创建index.ts入口,随后创建README

然后使用如下命令生成package.json

1
pnpm init

接着使用如下命令生成ts配置文件

1
tsc --init

然后创建一个vite.config.ts文件作为vite的配置文件

创建一个index.d.ts文件用来编写声明文件

编写库功能

随后就可以开始在index.ts中编写内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import type {App} from "vue"

// mutationObserver 主要侦听子集变化,包括属性变化以及增删改查
// interSectionObserver 主要侦听元素是否在视口内
// ResizeObserver 主要侦听元素宽高变化
function useResize(el: HTMLElement, callBack: Function) {
let resize = new ResizeObserver((entries) => {
// 由于可以监听多个元素,所以entries是一个数组,此处我们只监听一个元素
// contentRect包含变化前后的宽高
callBack(entries[0].contentRect)
})
// 添加侦听元素
resize.observe(el)
}

// TODO: vue插件规范,需要实现install方法
// 改方法接收app作为参数
// 这样app实例就可以通过app.use(router)这样的形式使用
const install = (app: App) => {
// 通过directive添加自定义指令
app.directive("resize", {
mounted(el, binding) {
useResize(el, binding.value)
}
})
}

useResize.install = install

// 将函数导出,就实现了自定义hook的效果
export default useResize
使用vite打包

接着在vite中配置该库打包时要用到的一些信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { defineConfig } from "vite"

// 默认打包一个UMD和一个es module
// umd支持amd cmd cjs 全局变量模式
export default defineConfig({
build: {
lib: {
// 库入口文件地址
entry: "src/index.ts",
// 库名称
name: "useResize",
},
// 属性透传
rollupOptions: {
// 确保外部化处理不想打包进库的依赖
external: ['vue'],
output: {
// 在UMD模式下为这些外部化的依赖提供一个全局变量
globals: {
useResize: "useResize"
}
}
}
}
})

接下来在package.json中添加一个打包的命令:

1
2
3
4
5
6
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "vite build"
}
}

然后呢使用如下命令进行打包:

1
npm run build

就会看到多了一个dist文件夹,其中ts代码被编译为了mjs以及umd形式的两个js文件

发布到npm

在将库发布到npm之前我们需要对我们的库做一些声明,在index.d.ts中编写声明:

1
2
3
4
5
6
7
// 声明文件
declare const useResize: {
(el: HTMLElement, callBack: Function): void;
install: (app: App) => void;
}

export default useResize

最后,在发布之前还需要再package.json中配置一些发布时需要设计到的信息:

  1. 配置库作为全局变量被引入的js文件入口“main”
  2. 配置库作为es module被引入时的js文件入口“module”
  3. 配置上传到npm时要上传的文件“file”
1
2
3
4
5
6
7
8
9
10
11
{

"main": "dist/ender-vue-resize.umd.js",
"module": "dist/ender-vue-resize.mjs",

"files": [
"dist",
"index.d.ts"
],
}

之后使用如下命令进行发布:

1
2
3
npm adduser # 注册一个npm账号
npm login # 登录npm账号
npm publish # 发布

全局函数以及全局变量

在vue2中使用Prototype来增加全局函数或全局变量,但vue3中已经不再使用这一方法了

vue3中通过createApp生成vueApp实例对象,则可以通过以下方式添加全局函数以及全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vue2.x
Vue.Prototype.$http = () => {}

// vue3.x
app = createApp(App)
app.config.globalProperties.$env = "dev"
app.config.globalProperties.$filter = {
format<T> (str: T) {
return `Ender-${str}`
}
}

// 定义好之后需要扩充vue的声明,防止编辑器报错
type Filter = {
format<T> (str: T):string
}
declare module "vue" {
export interface ComponentCustomProperties {
$filter: Filter,
$env: string
}
}

使用时直接在html中或在script中调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{$env}}
</div>
<div>
{{$filter.format("World")}}
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance} from "vue"
// js中需要先获取当前app实例,再从当前实例的proxy中得到全局变量/方法
const app = getCurrentInstance()
console.log(app?.proxy?.$env)
</script>

Vue编写插件

使用vue编写插件时需要经过以下步骤:

  1. 编写用作插件的vue组件,并将方法通过defineExpose暴露
  2. 编写ts以对象或函数的形式将vue组件导出
    1. 以对象形式导出时要求在对象中实现install函数
    2. install函数主要完成以下几个步骤:
      1. 使用createVNode方法,根据vue组件创建虚拟DOM节点VNode
      2. 使用render方法将虚拟DOM挂在到指定DOM节点
      3. 如果组件有暴露方法则将方法注册为Vue全局函数或全局变量
  3. 在main.ts中引入组件,并使用app.use安装编写的插件
  4. 为了使用方便,需要在main.ts中拓展vue的声明

index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div v-if="isShow" class="ender-loading">
<div class="ender-loading__content">Loading...</div>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(false) // 控制显示
console.log(isShow)

const show = () => {
isShow.value = true
}

const hide = () => {
isShow.value = false
}
// 暴露组件方法
defineExpose({
isShow,
show,
hide
})
</script>

<style lang="scss" scoped>
@include b(loading) {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
@include e(content) {
font-size: 30px;
color: #fff;
}
}
</style>

index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createVNode, App, VNode, render } from "vue";
import Loading from "./index.vue"

// 以对象形式导出作为插件时需要实现install方法
export default {
// install方法接收app实例对象
install(app: App) {
// 将组件创建为虚拟DOM节点
const vNode: VNode = createVNode(Loading)
console.log(vNode)
// 使用render方法将虚拟DOM挂在到指定元素中
// 此处由于是全局loading则挂在到body上
render(vNode, document.body)
// 将组件方法配置为全局方法
app.config.globalProperties.$loading = {
show: () => vNode.component?.exposed?.show(),
hide: () => vNode.component?.exposed?.hide()
}
}
}

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import Loading from './components/Loading'


let app = createApp(App)

// 使用use方法安装插件
app.use(Loading)

// 自定义插件需要进行拓展声明才能有代码提示
type Lod = {
show: () => void,
hide: () => void
}

declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$loading: Lod
}
}

app.mount('#app')

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

<template>
<Layout></Layout>
</template>

<script setup lang="ts">
import { ref, reactive, getCurrentInstance } from 'vue'
import Layout from "./Layout/index.vue"

const instance = getCurrentInstance()
instance?.proxy?.$loading.show()

setTimeout(() => {
instance?.proxy?.$loading.hide()
}, 2000)

</script>

<style lang="scss">
* {
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
}
#app {
@include bfc;
}
</style>

app.use手撕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { App } from "vue";
import app from "../main";
import { isFunction } from "lodash";

interface Use {
install: (app: App, ...options: any[]) => void
}

// set防止重复注册
const installList = new Set()
export function MyUseM<T extends Use>(plugin: T, ...options: any[]) {
if(installList.has(plugin)) {
console.error("插件已注册过")
} else if(plugin && isFunction(plugin.install)) {
installList.add(plugin)
plugin.install(app, ...options)
} else if(isFunction(plugin)) {
installList.add(plugin)
plugin(app, ...options)
}
// 链式调用
return app
}

UI组件库

  1. ElementUI
    1. 样例使用ts,setup语法糖编写
    2. 表单需要自己写分页
  2. Ant Design
    1. 样例使用ts,setup函数编写
    2. 表单包含分页
  3. View Design
    1. 样例使用js,选项式API
  4. Vant
    1. 移动端UI,支持小程序、vue2、vue3、react
    2. 样例使用setup函数模式
    3. 包含很多业务组件(地址,联系人编辑等等
    4. 提供了很多自定义hook

属性透传

在使用ElementUI时,由于Vue中Scoped的存在,阻止了css样式的向外传播。

由于单页应用会将所有Vue组件合并为一个HTML,因此为了保证css之间互相不影响。vue可以为vue组件中的style标签加scope

scope通过为DOM结构以及css样式上添加唯一不重复的标识:data-v-hash(通过PostCSS转译实现),以保证唯一性

总结scoped的三条渲染规则:

  1. 给HTML中的DOM节点加一个不重复的data属性:(data-v-123)来进行区分
  2. 给每个css选择器的末尾加上当前组件的属性选择器[data-v-123]
  3. 如果组件内部包含其他组件,则只会给其他组件的最外层标签加上当前组件的data属性

但使用ElementUI时,通常会希望自定义一些样式,因此vue提供了:deep,他的作用就是用来改变属性选择器的位置

以下代码会将[data-v-123]属性选择器加在最外层属性ender-content__input上,如果不使用deep则会加在 ender-content__input input上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div class="ender-content">
<div class="ender-content__item">
<el-input class="ender-content__input"></el-input>
</div>
</div>
</template>

<style scoped lang="scss">
@include b(content) {
flex: 1;
background-color: #fff;
overflow: auto;
overflow-y: hidden;
padding: 0 10px;
display: flex;
flex-direction: column;
@include e(item) {
flex: 1;
overflow: auto;
border-bottom: 1px solid #ccc;
padding: 10px;
}
@include e(input) {
// vue2 中使用 /deep/
// vue3 中也可以使用,但是会报错
:deep(input){
background: red;
}
}
}
</style>

Vue3 Style新特性

插槽选择器

当我们使用插槽时,想要为插入的内容添加样式:

A组件接收一个插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
我是插槽
<slot></slot>
</div>
</template>

<script>
export default {}
</script>

<style scoped>

</style>

在App.vue中引入,并向插槽中插入div

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<A>
<div class="a">私人定制div</div>
</A>
</div>
</template>

<script setup>
import A from "@/components/A.vue"
</script>

<style lang="less" scoped>
</style>

接着使用:slotted来修饰类选择器

1
2
3
4
5
6

<style scoped>
:slotted(.a) {
color:red
}
</style>

全局选择器

如果想要添加全局样式,在vue2中则需要去掉style标签中的scoped

vue3中只需要使用global修饰即可

1
2
3
4
5
<style lang="less" scoped>
:global(div){
color:red
}
</style>

动态css

单文件组件的style标签可以通过v-bind将响应式对象绑定为css属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="div">
小满是个弟弟
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
const red = ref<string>('red')
</script>

<style lang="less" scoped>
.div{
color:v-bind(red)
}

</style>

如果绑定响应式对象中的值为引用类型,则需要使用引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <template>
<div class="div">
小满是个弟弟
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue"
const red = ref({
color:'pink'
})
</script>

<style lang="less" scoped>
.div {
color: v-bind('red.color');
}
</style>

css module

<style module>标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件

并且支持自定义注入名称<style module="ender">

之后可以直接通过:class指令使用(多值可以使用数组接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div :class="[ender.red,ender.border]">
Hello World
</div>
</template>

<style module="ender">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>

不具名module则通过style使用

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div :class="$style.red">
Hello World
</div>
</template>

<style module>
.red {
color: red;
font-size: 20px;
}
</style>

注入的类可以通过 useCssModule API 在 setup()<script setup> 中使用。对于使用了自定义注入名称的 <style module> 模块,useCssModule 接收一个对应的 module attribute 值作为第一个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div :class="[ender.red,ender.border]">
Hello World
</div>
</template>


<script setup lang="ts">
import { useCssModule } from 'vue'
const css = useCssModule('ender')
</script>

<style module="ender">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>

使用场景一般用于TSX 和 render 函数 居多

NextTick

在Vue中数据的更新是同步的,但是DOM的更新是异步的

因此当我们实现一个自动滚动到最低部的聊天框这种需要在send按钮点击后操作DOM的场景时,由于send处理函数是同步的,而更新DOM是异步的,因此并不会滚动到我们希望的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref, reactive } from "vue"
let chatList = reactive([
{name: "A", message: "xxxxx"}
])
let ipt = ref("")
let box = ref<HTMLDivElement>() // 使用ref绑定的DOM元素
const send = () => {
chatList.push({
name: "B",
message: ipt.value
})
box.value.scrollup = 9999999 // 当这一步执行时同步的,因此会先滑动到底部,在将新增加的DOM更新到页面上
}

因此Vue提供了NextTick API用来解决这样的问题,第一种方式就是回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, reactive } from "vue"
let chatList = reactive([
{name: "A", message: "xxxxx"}
])
let ipt = ref("")
let box = ref<HTMLDivElement>() // 使用ref绑定的DOM元素
const send = () => {
chatList.push({
name: "B",
message: ipt.value
})
nextTick(() => {
box.value.scrollup = 9999999 // 将语句放在nextTick的回调函数中就能解决问题
})
}

也可以使用async/await来实现上述功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, reactive } from "vue"
let chatList = reactive([
{name: "A", message: "xxxxx"}
])
let ipt = ref("")
let box = ref<HTMLDivElement>() // 使用ref绑定的DOM元素
const send = () => {
chatList.push({
name: "B",
message: ipt.value
})
// 这样在await之后的语句都变为异步执行了
await nextTick()
box.value.scrollup = 9999999
}

实际上nextTick函数就是将其中的代码变为了异步任务,其源码是通过Promise将代码变为微任务实现的:

源码地址:core\packages\runtime-core\src\scheduler.ts

1
2
3
4
5
6
7
8
9
10
const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

export function nextTick<T = void>(
this: T, // ts语法用于指定this类型
fn?: (this:T) => void
): Promise<void> {
const p = currrentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this), fn) : p
}

事实上nextTick中的tick指的就是浏览器渲染过程中的一帧,那浏览器这一帧率做了什么

1.处理用户的事件,就是event 例如 click,input change 等。

2.执行定时器任务

3.执行 requestAnimationFrame

4.执行dom 的回流与重绘

5.计算更新图层的绘制指令

6.绘制指令合并主线程 如果有空余时间会执行 requestidlecallback

所以 一个Tick 就是去做了这些事

h函数

Vue事实上包含三种编写风格:

  1. template风格
  2. JSX风格
  3. h函数风格

Vue3中已经很少使用了,偶尔会出现在,需要定义一个小组件单又不需要复用,不想为其创建文件夹的场景下

出现的原因是Vue单文件组件编译是需要过程,他会经过

parser编译为AST树 -> transform转化为JS API -> generate生成render函数

render函数则会返回一个h函数包裹的VNode虚拟DOM节点

而h函数直接跳过这三个阶段,所以性能上有很大的帮助。其底层实际上就是调用的createVNode()方法直接创建虚拟DOM节点,因此实际上使用h函数编写组件就像直接编写虚拟DOM节点。

h函数接收三个参数:

  1. type节点类型
  2. propsOrChildren对象,主要用来表示props, attrs, dom props, class, style
  3. children子节点

h函数有多种重载形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' })

//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div', { class: 'bar', innerHTML: 'hello' })

// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })

// class 和 style 可以是对象或者数组
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 定义事件需要加on 如 onXxx
h('div', { onClick: () => {} })

// 子集可以字符串
h('div', { id: 'foo' }, 'hello')

//如果没有props是可以省略props 的
h('div', 'hello')
h('div', [h('span', 'hello')])

// 子数组可以包含混合的VNode和字符串
h('div', ['hello', h('span', 'hello')])

props传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<Btn text="按钮"></Btn>
</template>

<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
text: string
}
const Btn = (props: Props, ctx: any) => {
return h('div', {
class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',

}, props.text)
}
</script>

派发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<Btn @on-click="getNum" text="按钮"></Btn>
</template>

<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
text: string
}
const Btn = (props: Props, ctx: any) => {
return h('div', {
class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
onClick: () => {
ctx.emit('on-click', 123)
}
}, props.text)
}

const getNum = (num: number) => {
console.log(num);
}
</script>

定义插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<Btn @on-click="getNum">
<template #default>
按钮slots
</template>
</Btn>
</template>

<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
text?: string
}
const Btn = (props: Props, ctx: any) => {
return h('div', {
class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
onClick: () => {
ctx.emit('on-click', 123)
}
}, ctx.slots.default())
}

const getNum = (num: number) => {
console.log(num);
}
</script>

可以看到实际上和编写setUphan’shu的形式是一样的。

Vue3.3编译宏

需要使用Vue3.3及以上版本

defineProps

使用普通方式defineProps接收组件参数时,无法设置赞数类型,因此使得使用prop访问参数时没有代码提示

为了解决这个问题Vue提供了PropType,可以使用这个API将参数断言为指定的数据类型

同时defineProps函数也可接收一个泛型来定义接收参数的类型:defineProps<{name:string[]}>()

Vue3.3对defineProps进行了改进,新增了泛型的支持,需要在script标签上增加generic="T"熟悉来定义泛型

父向子组件传参

1
2
3
4
5
6
7
8
9
10
<template>
<div>
<Child :name="['ender']"></Child>
</div>
</template>
<script lang='ts' setup>
import Child from './views/child.vue'
</script>
<style></style>

子组件接收

1
2
3
4
5
6
7
8
9
10
11
12
 <template>
<div>
{{ name }}
</div>
</template>
<script generic="T" lang='ts' setup>
// 通过generic属性定义泛型
defineProps<{
name:T[] // name属性就可以使用泛型构造任意类型数组的类型
}>()
</script>

这也如果有的父组件要向子组件传递string[],但有的又需要传递number[]时,就可以使用泛型来定义类型

defineEmits

defineEmits编译宏用来进行事件派发,在TS中为了获得更友好的类型提示,通常我们需要利用该函数接收一个泛型来定义事件接收函数的类型:

1
2
3
const emit = defineEmits<{
(event: string, name: string): void
}>()

3.3中对这一方式进行了优化,可以直接通过数组的形式为事件指定参数类型:

1
2
3
const emit = defineEmits<{
'send': [name: string]
}>()

defineOptions

vue3.3内置了defineOptions()用于操作OptionsAPI中的内容,需要注意的是已经有编译宏的Options,比如emit,props, expose, slots无法在其中使用

通常用来定义name:

1
2
3
4
defineOptions({
name:"Child",
inheritAttrs:false,
})

defineSlots

defineSlots是3.3新增的编译宏,只做声明不做实现

作用是约束slot类型

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<Child :data="list">
<template #default="{item}">
<div>{{ item.name }}</div>
</template>
</Child>
</div>
</template>
<script lang='ts' setup>
import Child from './views/child.vue'
const list = [
{
name: "张三"
},
{
name: "李四"
},
{
name: "王五"
}
]
</script>
<style></style>

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<ul>
<li v-for="(item,index) in data">
<slot :index="index" :item="item"></slot>
</li>
</ul>
</div>
</template>
<script generic="T" lang='ts' setup>
defineProps<{
data: T[]
}>()
defineSlots<{
default(props:{item:T,index:number}):void
}>()
</script>

环境变量

Vue提供的用于区分开发环境,测试环境,生产环境的方法

默认的环境变量保存在import.meta.env,需要注意的是尽量不要对环境变量做动态的修改,因为在生成环境中环境变量采用硬编码的方式,因此在生产环境下动态修改环境变量会报错

如果需要自定义环境变量,则需要在根目录下创建文件名为:

.env.development-开发环境

.env.production-生产环境

的文件

如果希望上述两个环节变量生效,需要子package.json中是script中指定启动命令的模式(目前可以默认读取无需配置了:

1
2
3
4
"scripts": {
// "dev": "vite"
"dev": "vite --mode development"
}

但如果想在Vue组件以外的ts文件中获取环境变量,比如在vist.config.ts中获取环境变量则需要使用nodejs的API以及Vite的APi:

1
2
3
4
5
6
7
8
9
10
import { defineConfig, loadEnv } from "vite"

export default({mode}:any) => {
// loadEnv接收两个参数,第一个参数代表当前允许的模式,此处通过export default的回调函数获得
// 第二个参数接收一个目录代表环境变量文件所在的路径,此处使用nodeJS中的process.cwd()获取根目录路径
console.log(loadEnv(mode, process.cwd()))
return defineConfig({
// Vite配置
})
}

Vue3性能优化

在开发环境中通常页面性能会使用Chrome浏览器中的LightningHouse进行性能测试

其中包含几个常用指标:

  1. FCP
    • First Contentful Paint
    • 首屏加载时间
  2. Speed Index
    • 页面各个可见部分的显示平均时间
    • 当有数据需要从后台获取时将影响这一数值
  3. LCP
    • Largest Contentful Paint
    • 最大内容绘制时间
    • 页面最大的DOM元素绘制所需的时间
  4. TTL
    • Time To Interactive
    • 从页面开始渲染到用户可以操作的时间间隔
    • 即内容必需渲染完成,且交互元素绑定的事件已经注册完成
  5. TBT
    • Total Blocking Time
    • 主进程被阻塞的时间
  6. CLS
    • Cumulative Layout Shift
    • 计算布局偏移值
    • 偏移值不为0可能导致用户想点A但下一帧A按钮被挤到旁边导致实际点了B

而在生产环境中Vite可以使用rollup-plugin-visualizer对打包后的项目进行性能分析

需要在vite.config.ts中进行注册:

1
2
3
4
5
import { visualizer } from "rollup-plugin-visualizer"
defineConfig({
// 主要要将open属性设为true
plugins: [Vue(),vueJSX(), visualizer({open:true})]
})

然后使用npm run build命令进行打包后,打包完成时会生成一个html显示打包过程中每个模块占用的时间,模块面积越大代表打包耗时越多

对于打包的优化,Vite提供了一些配置项对打包的过程进行优化,可以在build属性中设置:

1
2
3
4
5
6
7
8
9
defineConfig({
build: {
chunkSizeWarningLimit: 2000, // 包大小警告阈值
cssCodeSplit: true, // css 拆分
sourcemap: false, // 是否生成sourcemap
minify: "terser", // 是否使用最小化混淆,支持esbuild和terser,前者打包更快,后者打包后体积更小
assetsInlineLimit: 4000 // 小于该值的图片将大包为base64
}
})

此外vite还有许多优化插件,比如:

  • vite-plugin-pwa:让项目支持离线缓存技术,使web应用无线接近于原生应用
    • 可以添加到桌面,使用manifest实现
    • 可以实现离线缓存,使用service worker实现(新技术
    • 可以发送通知,使用service worker实现

另外还可以通过一些vue的插件进行性能优化比如图像懒加载可以使用vue3-lazy实现懒加载

对于数据量很多的列表可以使用虚拟列表中技术(Element-UI已经集成),即只有可视区域的DOM,其余部分没有DOM

对于主进程频繁被阻塞的问题可以使用多线程来解决,通过new Worker创建子线程,然后通过postMessage和onMessage分别进行发送信息和接收信息,使用terminate方法关闭,该方法遵守同源限制,且不允许在子线程中操作DOM

此外还可以使用防抖节流来优化用户体验,这些在VueUse中都已经实现

Web Components

原生JS也提供了封装组件的能力,由三部分组成:

  1. Custom Element:自定义元素
  2. Shadow DOM:(微前端也是基于该技术,隔离JS和CSS)用于将组件的css与js和外部缓解隔离
  3. HTML template:允许我们使用模板字符串定义组件中的HTML内容

Web Components包含四个生命周期:

  1. connectedCallback:挂载
  2. disconnectedCallback:断开
  3. adoptedCallback:移动
  4. attributeChangeCallback:改变

原生Web Component也可以配合Vue一起使用

注意:vue中使用原生组件需要在vite配置中指定某某前缀的组件跳过组件检测

然后就可以在js中使用defineCustomElement(customVueCeVue)创建自定义组件,其中函数可以从vue中导入,customVueCeVue为我们定义好的组件

然后使用window.customElements.define('prefix-component')将组件和他的标签挂载道window对象上

这种模式下传参时只能支持字符串参数,因此如果要传递引用类型则需要使用JSON.stringify()转化为JSON字符串

proxy跨域

跨域是存在于浏览器端的问题,主要是由于浏览器最核心最基础的安全功能——同源策略限制

当一个请求url的协议,域名,端口三者中任意一个与当前页面的URL不同则为跨域

解决跨域问题通常有以下几个方法

JsonP方法

百度使用的方法

原理:由于HTML中的script标签不受同源策略的限制,因此我们可以通过动态创建script标签,将src作为服务器地址,然后服务器返回一个callback接受返回的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function clickBtn() {
let s, obj
obj = {"table": "products", "limit": 10}; // 构建参数
s = document.createElement("script"); // 创建script元素
s.src = "接口地址xxxxx" + JSON.stringify(obj) // 传递参数
document.body.appendChild(s) // 将script挂载
}

// 与服务端返回函数同名
function myFunc(myObj) {
// 接收后端返回数据
document.getElementById("demo").innerHTML = myObj
}

但该方法无法发送post请求

CORS设置

后端可以通过设置CORS来允许跨域资源共享

1
2
3
4
5
6
7
8
{
"Access-Control-Allow-Origin": "http://web.xxx.com" // 可指定地址
}

// 也可以设为*允许任意站点
{
"Access-Control-Allow-Origin": "*" // 使用通配符允许任意站点访问,不推荐,安全性不高,且在使用session时无法在前端种植cookie
}

Proxy代理

实际上是用node代替前端对后端进行请求(因为服务端没有跨域限制)

假设有后端接口:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express')
const app = express()

//创建get请求
app.get('/user',(req,res)=>{
res.json({
code:200,
message:"请求成功"
})
})
//端口号9001
app.listen(9001)

我们直接在前端使用fetch会报跨域错误:

1
2
3
4
<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('http://localhost:9001/user')
</script>

此时可以开启vite的代理

vite中在配置文件中设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
// vite.config.ts
export default defineConfig({
plugins: [vue()],
server:{
proxy:{
'/api':{
target:"http://localhost:9001/", //跨域地址
changeOrigin:true, //支持跨域
rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
}
}
}
})

然后将前端请求url修改为/api/user即可

webpack proxy 和 node proxy 用法都类似

需要注意的是proxy只在开发环境中生效,生产环境由于网页会上服务器,前后端分离可以使用nginx,apache等等服务器的代理实现

vite的proxy底层实现实际上就是监听前端页面端口的请求后,向服务器端口发送请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// proxyMiddleware 中的代码
import httpProxy from 'http-proxy'
export function proxyMiddleware(
httpServer: http.Server | null,
options: NonNullable<CommonServerOptions['proxy']>,
config: ResolvedConfig
): Connect.NextHandleFunction {
// lazy require only when proxy is used
const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server

// http-proxy 模块用于转发 http 请求
// 其实现的大致原理为使用 http 或 https 模块搭建 node 代理服务器,将客户端发送的请求数据转发到目标服务器,再将响应输送到客户端
const http = require('http')

const httpProxy = require('http-proxy')

const proxy = httpProxy.createProxyServer({})

//创建一个代理服务 代理到9001
http.createServer((req,res)=>{
proxy.web(req,res,{
target:"http://localhost:9001/xm", //代理的地址
changeOrigin:true, //是否有跨域
ws:true //webSocetk
})
}).listen(8888)

虚拟DOM与diff算法

虚拟DOM即通过JS生成的AST节点树

虚拟DOM的产生是因为:

  • 直接操作DOM性能低下
  • 但操作JS速度更快

而Diff算法则是为了让对虚拟DOM的操作变得更快而产生的。

例如v-for中不使用key属性时,更新DOM需要经过以下几个阶段:

  • 按从左到右的顺序,新元素替换旧元素
  • 最后如果元素少了,则卸载最后几个
  • 如果元素多了,则新增最后几个

如果使用了key,更新DOM就变为了如下几个步骤:

  • 前序对比算法(从左到右对比新旧元素的key和type,直到第一个不匹配
  • 后序对比算法(从右到左对比新旧元素的key和type,直到第一个不匹配
  • 如果多出新节点,则挂载
  • 如果多出旧节点,则卸载
  • 乱序情况特殊处理
    • 乱序时,先计算得到key与index的映射
    • 然后应用了贪心+二分查找的思想计算最长上升子序列
    • 那么之后只需要将不在最长上升子序列中的元素进行移动即可
    • 如果在其中就不需要移动,这样能够保证移动的次数最少

Tailwind CSS

Tailwind CSS是一个由JS编写的CSS框架,基于PostCSS解析。

PostCSS插件在使用时需要进行一些配置:

  1. PostCSS配置文件postcss.config.js,新增tailwind插件
  2. TailwindCSS配置文件tailwind.config.js

PostCSS处理CSS的大致流程如下:

  1. 将CSS解析为抽象语法树(AST树)
  2. 读取配置文件,根据该文件生成新的抽象语法树
  3. 将AST树“传递”给一系列数据转换操作处理(变量数据循环生成,嵌套类名循环等)
  4. 清除一系列操作留下的数据
  5. 将处理完毕的AST树重新转化为字符串

安装

1
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

其中autoprefixer可以 自动为样式添加–web-kit-等等前缀以增强页面的兼容性

生成配置文件

1
npx tailwindcss init -p

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 2.0
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
// 3.0
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}

打包时会根据tailwind的配置,将没有使用到的类排除,只将使用到的类打包

创建一个index.css

引入

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

在main.ts中引入样式文件

1
import './index.css'

Mitt

Mitt是一个利用发布订阅设计模式解决组件间通讯操作发杂问题的JS库,它提供了emit,on,off,all四种方法

配置

全局使用Mitt首先需要将其挂载到根组件上,如果使用ts并且想要类型推断功能,则需要获取Mitt中的所有类型:

在项目的main.ts中进行如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'
import mitt from 'mitt'
import './style.css'
import App from './App.vue'

const Mitt = mitt()

const app = createApp(App)

//TODO: 支持TS的类型推断代码提示
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof Mitt
}
}

// TODO: 全局挂载Mitt
app.config.globalProperties.$Bus = Mitt

app.mount('#app')

emit

接着在组件A中就可以直接注册事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import { getCurrentInstance } from 'vue';

// TODO: 获取组件实例
const instance = getCurrentInstance()

const emitEvent = () => {
// TODO: 从实例的代理处获得Bus
instance?.proxy?.$Bus.emit('trans-data-to-b', '来自A的数据')
}

</script>

<template>
<div>
<h1>组件A</h1>
<button @click="emitEvent">emit</button>
</div>
</template>

<style>

</style>

on

在组件B中可以通过on来订阅事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup lang="ts">
import { getCurrentInstance, ref } from 'vue';

const instance = getCurrentInstance()

const messageFromA = ref("")

instance?.proxy?.$Bus.on('trans-data-to-b', (message: string) => {
console.log(message)
messageFromA.value = message
})

</script>

<template>
<div>
<h1>组件B</h1>
<span>{{ messageFromA }}</span>
</div>
</template>

<style>

</style>

on函数还可以监听所有事件:

1
2
3
4
5
6
instance?.proxy?.$Bus.on('*', (eventName:string,message: string) => {
// 事件名与参数
console.log(eventName)
console.log(message)
messageFromA.value = message
})

Off

使用off函数可以将之前为事件名称绑定的函数删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script setup lang="ts">
import { getCurrentInstance, ref } from 'vue';

const instance = getCurrentInstance()

const messageFromA = ref("")

const Bus = (message: any) => {
console.log(message)
messageFromA.value = message
}

// TODO: 为事件名绑定事件函数
instance?.proxy?.$Bus.on('trans-data-to-b', Bus)

// TODO: 删除事件函数
instance?.proxy?.$Bus.off('trans-data-to-b', Bus)

</script>

<template>
<div>
<h1>组件B</h1>
<span>{{ messageFromA }}</span>
</div>
</template>

<style>

</style>

all

all用来获取一个所有事件名称到事件处理函数的Map

可以使用all.clear来清除所有事件的绑定:

1
instance?.proxy?.$Bus.all.clear()

Ionic

ionic是一个支持Angular,react,vue的移动端开发框架,其转化的方式时基于webview的,属于Hybrid App开发,底层时基于cordva和capacitor(二选一)

安装

Java环境以及Android Studio(包含安卓SDK)

项目创建

可以使用以下命令使用ionic命令构建项目:

1
ionic start projectName templateName --type vue

接下来可以直接使用npm run dev命令在web端预览项目

安卓预览

如果想在安卓端预览项目,那么首先要对项目打包:

1
npm run build

接下来使用ionic命令将包编译为安卓项目

如果是ios项目则只需要把android替换为ios

1
ionic capacitor copy android

编译完成后会生成一个android文件夹,其中应该包含一个capacitor.setting.gradle文件

接下来可以使用ionic相关命令开启预览

1
ionic capacitor open android

ionic会帮忙在android studio中打开项目,创建虚拟机后运行即可预览

但这样的预览无法实现热更新

热更新

ionic热更新只能在使用USB调试的时候可以实现,在安卓SDK中安装USB Driver后,连接安卓手机,使用如下命令开启热更新:

1
ionic capacitor run android -l --external

H5适配

分辨率适配

在开发移动端页面时常因为设备宽高比不同导致页面显示不全,才是可以通过meta标签来告诉浏览器使用什么大小渲染页面(默认为标准视口大小)

1
<meta name="viewport" content="width=device-width", initial-scale="1.0">

将标签添加在index.html既可

清除默认样式

web页面偶尔会带有默认的margin或padding,会影响开发次数需要清除默认样式,并将根容器进行BFC设置

1
2
3
4
5
6
7
8
html,body,#app {
height: 100%;
overflow: hidden;
}
* {
padding: 0;
margin: 0;
}

postCSS

postCSS相当于CSS的babel,可以对CSS代码进行一些预编译。

比如如果想要实现px自动转vw的小工具,只需要编写一个postCSS的小插件即可

  1. 在根目录下创建plugins目录

  2. 为tsconfig.node.json中的include配置扫包目录,以及开启允许隐式Any的设置

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"noImplicitAny": false
},
"include": [
"plugins/**/*",
]
}
  1. 在plugin目录下创建插件文件:postcss-px-to-viewport.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Plugin } from "postcss";

// 默认参数
const Options = {
viewportWidth: 375,
}

interface Options {
viewportWidth?: number
}

export const PostCsspxToViewport = (options: Options = Options): Plugin => {
// 接收到参数后将参数与默认值合并
const opt = Object.assign({}, Options, options)
return {
// 定义插件名称
postcssPlugin: "postcss-px-to-viewport",
// 钩子函数
// 该钩子函数用于获取css节点
Declaration(node) {
// 删选带px单位的属性值,node.key和node.value分别表示所有属性与其值
// 这里还可以使用自定义单位,例如ED
if(node.value.includes("px")){
const num = parseFloat(node.value) // 获取属性值的数值部分
node.value = `${((num / opt.viewportWidth) * 100).toFixed(2)}vw`
}
}
}
}
  1. 然后再vite.config.ts中引入并注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {PostCsspxToViewport} from "./plugins/postcss-px-to-viewport"

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
],

// 配置postcss
css: {
postcss: {
plugins: [PostCsspxToViewport()]
}
}
})

切换主题/切换字体大小

可以使用VueUse插件封装好的APIuseCssVar改变css中定义的变量来实现:

1
2
const font = useCssVar('--size')
font.value = num + 'px'

其底层原理是通过以下两个API获取和设置css变量:

1
2
document.documentElement.style.getPropertyValue('--size')
document.documentElement.style.setProperty('--size', num + 'px')

CSS原子化

定义:

原子化CSS是一种CSS架构方式,其支持小型、单一用途的类,其名称基于视觉功能。

更加通俗的来讲,原子化CSS是一种新的CSS编程思路,它倾向于创建小巧且单一用途的class,并且以视觉效果进行命名。举个简单的例子:

1
2
3
4
5
6
7
8
9
<!-- 原子化类定义 -->
<style>
.text-white { color: white; }
.bg-black { background-color: black; }
.text-center { text-align: center; }
</style>

<!-- 原子化类使用 -->
<div class="text-white bg-black text-center">hello Atomic CSS</div>

css原子化的优缺点:

  • 优点:
    • 减少了css体积,提高css可复用性
    • 减少起名复杂度
  • 缺点
    • 增加了记忆成本,比如background需要记住其缩写bg

原子化插件unocss

1
npm i -D unocss

unocss最好使用在vite中,因为webpack的版本功能被阉割

小满Vue3第三十七章(unocss原子化)_unocss解决了什么问题-CSDN博客

TSX/JSX

Vue中同样支持使用JSX的方式来编写组件,需要安装和使用官方插件:

1
npm install @vitejs/plugin-vue-jsx -D

安装完成后需要在vite中进行一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vite/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL("./src", import.meta.url))
}
}
})

TSX的使用

使用TSX创建组件时,先新建一个.tsx文件后,使用如下方式即可:

  1. export default function() {
        return (<div>Ender</div>)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    - 即返回一个渲染函数

    2. ```tsx
    import { defineComponent } from "vue";

    export default defineComponent({
    data() {
    return {
    age: 25
    }
    },

    render() {
    return (<dev>
    { this.age }
    </dev>)
    }
    })
    - 利用`defineComponent`方法创建OptionAPI式组件,需要实现render方法
  2. import { defineComponent } from "vue";
    import { ref } from "vue";
    
    interface Props {
        name?: string
    }
    export default defineComponent({
        props: {
            name:String
        },
        emits: ['my-click'],
        setup(props:Props, {emit} ) {
            const age = ref(25)
            const fn = (str:string) => {
                console.log("点击了: ", str)
                emit("my-click", str)
            }
            console.log(props)
            return () => (
                <>
                    <div>props: {props.name}</div>
                    <div onClick={() => fn("Hello")}>
                        { age.value }
                    </div>
                </>
            )
        }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    - 利用`defineComponent`方法创建组合式组件,需要实现setup方法并返回render函数

    定义完成后的TSX/JSX组件可以直接通过引入TS/JS文件来使用:

    父组件:

    ```vue
    <script setup lang="ts">
    import A from './components/A.vue';
    import B from './components/B.vue';
    import cmp from './components/cmp';

    const tsxCmpClick = (str:String) => {
    console.log("父组件收到了: ", str)
    }
    </script>

    <template>
    <div>
    <A></A>
    <B></B>
    <cmp name="TSX Component" @my-click="tsxCmpClick"></cmp>
    </div>
    </template>

    <style scoped>
    </style>

unplugin-auto-import

一个能够使项目全局引入vue函数的组件,可以引入ref、reactive等vue包中的函数,也支持axios等其他包

安装:

1
npm install unplugin-auto-import

vite.config.ts中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// 自动引入ref, reactive等插件
import AutoImport from 'unplugin-auto-import/vite'
// import tsx from './plugin/index'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx(), AutoImport({
// 全局引入vue包中的函数
imports:['vue'],
// 定义挂载位置
dts: "src/auto-import.d.ts"
})],
resolve: {
alias: {
'@': fileURLToPath(new URL("./src", import.meta.url))
}
}
})

Babel与Vite插件编写

一个将ES6+语法编译为ES5从而实现低版本浏览器适配的插件,其原理是:

源代码 ->(编译器(parse))抽象语法树 AST->(转换过程(transform))修改后的AST ->(生成器(generator))转换后的代码

可以利用Babel实现Vite项目中的TSX解析,可以利用以下插件实现:

1
2
3
4
5
npm install @vue/babel-plugin-jsx # vue的babel支持
npm install @babel/core # babel核心组件
npm install @babel/plugin-transform-typescript # babel TS编译器
npm install @babel/plugin-syntax-import-meta # import 语法编译
npm install @types/babel__core # babel core声明文件

首先我们在根目录下创建一个插件目录plugin并在目录下创建文件index.tx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// plugin/index.ts
import type { Plugin } from 'vite'
import * as babel from '@babel/core'
import jsx from '@vue/babel-plugin-jsx'

export default function():Plugin {
return {
// 插件命名需要以vite-plugin开头,vue用来区分是vue使用还是react
name: "vite-plugin-vue-tsx",
// id为文件路径
transform(code, id) {

// 如果是以.tex结尾的路径
if(/.tsx$/.test(id)) {
console.log(code, id)
}
}
}
}

其次将该目录添加进tsconfig.node.json文件中进行引入

1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
// 添加plugin目录
"include": ["vite.config.ts", "plugin"]
}

然后就可以和其他插件一样在vite的配置文件中使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
// import vueJsx from '@vitejs/plugin-vue-jsx'
// 自定义插件
import tsx from './plugin/index'

// https://vitejs.dev/config/
export default defineConfig({
// 插件使用
plugins: [vue(), tsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL("./src", import.meta.url))
}
}
})

Vite

Vite提现了Vue团队的一贯作风:减少开发者的心智负担。

相比于webpack,他内置了更多功能从而减少了开发者对插件等外部依赖的管理,降低了心智负担

例如在Vite中不需要配置css-loader、less-loader、ts-loader等等插件就能直接使用。

构建工具

构架工具具备的功能:

  1. 遇到ts文件使用tsc将ts文件转化为js文件
  2. 遇到.vue或.tsx/jsx使用react-compilervue-compiler将其转化为render函数
  3. 遇到less/sass/postcss/windtail使用less-loadersass-loader等将其编译为原生css
  4. 遇到老版本浏览器使用babel将es6以上的语法转化为旧版语法
  5. 利用uglify/terser等对代码进行压缩优化体积

构建工具就是将以上功能集成到一起并自动执行的工具

一个构建工具实际上还会复杂其他多种工作,那我们将他的功能进行归类:

  1. 模块化支持:支持es modulecommonJS等多种模块化的模块引入语法from ... import ..const ... = require(...)
  2. 处理代码兼容性:babel、less、ts语法转换(构建工具利用其他处理工具自动化的完成)
  3. 提高项目性能:压缩文件,代码分割
  4. 优化开发体验:
    1. 构建工具自动监听文件变化,重新打包并在浏览器重新运行(热更新HMR
    2. 开发服务器:解决跨域问题

目前市面上的构建工具:

  • webpack
  • vite
  • parcel
  • esbuild
  • rollup
  • grunt
  • gulp

Vite相对于Webpack的优势

Vite为什么笔Webpack快那么都?

Webpack考虑到网页可以跑在服务端,也能跑在客户端,于是支持多种模块化方法混用,例如:

1
2
import Vue from "vue" // es6 module
const _ = require("lodash") // commonJs

那么为了避免这种情况导致的编译错误,Webpack在启动服务之前,需要对每一个moduel都进行一边编译,以确保把所有模块化方法都转化为了一般形式,因此当项目使用的module越来越多时,启动和热更新(HMR)的速度就会变慢

而Vite要求模块化必需使用es6 module的方式,则可直接启动服务,当module被需要时,在编译module,实现按需导入,减少编译和热更新等待的时间

Vite脚手架和Vite

在Vite官网使用给出的构建项目时使用的命令:

yarn create vite

实际上是使用了create-vite这个脚手架的功能,脚手架包含Vite但脚手架的主要更能还是根据模板创建项目文件以及目录

Vite

Vite以index.html文件作为入口

它解析<script type="module" src="..."> ,这个标签指向你的 JavaScript 源码。内联引入 JavaScript 的 <script type="module"> 和引用 CSS 的 <link href> 也能利用 Vite 特有的功能被解析。

index.html 中的 URL 将被自动转换,因此不再需要 %PUBLIC_URL% 占位符了。

根目录

vite 以当前工作目录作为根目录启动开发服务器。你也可以通过 vite serve some/sub/dir 来指定一个替代的根目录。注意 Vite 同时会解析项目根目录下的 配置文件(即 vite.config.js,因此如果根目录被改变了,你需要将配置文件移动到新的根目录下。

Vite运行在node环境中,在后续的代码中经常会使用到path.resolve()函数来构造路径,原因是当我们在使用相对路径访问某个包时,node环境下会将相对路径与process.pwd()拼接,但这一函数返回值始终是当前node的运行目录,也就是说如果我们不再vite的项目根目录下使用,相对路径的拼接就会出错,因此我们需要使用__dirname这一变量来得到当前文件所在的目录,再与相对路径拼接得到正确的绝对路径。

实际上这与node的编译有关,node在对js文件进行编译时,会将每个js文件封装为一个立即执行函数来隔绝作用于,在这个函数中第五个参数就是__dirname,用来保存当前js文件的绝对路径。因此在node环境下的js文件可以访问这一变量。

此外该立即执行函数打字包含以下几个参数:

1
2
3
(function(exports, require, module, __filename, __dirname) {

}())

其中

  • exports === module.exports是commonjs的导出对象
  • require为commonjs的导入方法
  • module为module对象
  • __filename为文件绝对路径+文件名
  • __dirname为文件绝对路径

Vite依赖构建

Vite遇到非绝对路径会对其进行补全,依赖预构建只发生在开发环境,生产环境全权交给rollup,对于commonJS的导出方式进行依赖预构建。

Vite的依赖构建特别之处在于会对以来进行依赖预构建

Vite会找到对应的依赖,然后使用esbuild(go语言编写)对JS语法进行一些处理,将其他模块化规范都转化为es6 module的规范,然后放到当前目录下的node_module/.vite/deps中,同时对esmodule规范的各个模块进行统一集成。

预构建解决了下面三个问题:

  1. 不同的第三方包有不同的导出格式
  2. 在路径的处理上可以直接使用.vite/deps,方便路径重写
  3. 解决网络多包传输的性能问题(是导致原生esmodule规范不敢支持node_module的原因之一),无论多少额外的export和import,最终都会尽可能进行集成最后生成一个或者几个模块

vite.config.js中可以通过optimizeDeps设置针对某些包是否进行依赖预构建

1
2
3
4
5
export default defineConfig({
optimizeDeps: {
exclude: ['lodash-es'], //不对lodash-es进行依赖预构建,此时会根据加载所有lodash依赖的包
}
}

Vite运行在node端为什么可以是用esmodule的包管理

node端只支持commonJS规范,但是vite的配置文件vite.config.js可以使用esmodule进行包的导入导出

这是因为vite在读取配置文件的时候会率先解析文件语法,遇到esmodule规范的语法时直接将其替换为commonJS

Vite配置文件处理细节

  1. Vite配置文件语法提示

    1. 是用webStorm会有良好的语法提示

    2. 为了获得良好的语法提示,建议从vite中导入defineConfig,然后是用defineConfig对配置对象进行导出,defineConfig是一个函数,该函数的参数是用ts定义了interface,因此会有良好的代码提示:

    3. import { defineConfig } from 'vite'
      export default defineConfig({
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      4. 或者可以是用ts装饰器定义类型:

      5. ```ts
      /**
      * @type import ('vite').UserConfig
      */
      const viteConfig = {
      }
  2. 关于环境的处理

    1. 使用webpack的时候想要区分环境,需要针对不同环境编写config,比如开发环境要在webpack.dev.config中配置,生产环境要在webpack.prod.config中配置

    2. 而vite中则可以是用defineConfig函数进行区分,然后根据不同的环境返回不同的config对象:

    3. import { defineConfig } from 'vite'
      
      // 导入不同环境的配置
      import viteBaseConfig from './vite.base.config.ts'
      import viteDevConfig from './vite.base.config.ts'
      import viteProbConfig from './vite.base.config.ts'
      
      // 文件压缩
      // import viteCompression from 'vite-plugin-compression'
      
      
      // 策略模式,减少if else的使用
      const envResolver = {
        "build": () => {
          console.log("生产环境");
          return {...viteBaseConfig, ...viteProbConfig}
        },
        "serve": () => {
          console.log("开发环境");
          return {...viteBaseConfig, ...viteDevConfig}
        }
      }
      
      export default defineConfig(({command}) => {
          // command根据是用的命令vite build/vite区分环境
          return envResolver[command]()
      })
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54



      ### Vite环境变量配置

      > 会根据当前代码环境变化的变量成为环境变量
      >
      > 代码环境包括:
      >
      > 1. 开发环境
      > 2. 测试环境
      > 3. 预发布环境
      > 4. 灰度环境
      > 5. 生产环境

      例如是用第三方SDK的时候(比如腾讯TRTC、百度地图,小程序)时,尝尝需要设置`APP_KEY`

      通常在生产环境、开发环境中需要是用不一样的key

      并且通常在前后端分离的项目中,前端在生产环境和测试环境调用的API地址是不同的

      #### 服务端(node)是用环境变量

      vite和webpack均使用dotenv第三方库来处理环境变量,在项目开始时会读取`.env`文件并解析文件中的对应环境变量,将其注入到process对象下,但是vite考虑到和其他配置的冲突问题,不会直接注入到process对象下,这涉及到配置文件中的一些配置项(例如`root,envDir`后者用来配置当前环境变量的文件地址),此时可以通过`loadEnv`的方法,改方法接收两个参数:

      1. `mode`,来自`defineConfig`的回调函数中的参数,代表运行模式,默认为`development | production`根据命令变化,即:
      1. `vite --mode development`
      2. `envDir`一个字符串表示当前env文件所在目录,可以是用process.pwd() ,该方法返回当前node进程的工作目录
      3. `prefixes`环境变量前缀名,默认为`VITE_`

      当我们调用`loadEnv`会进行一下操作:

      1. 创建一个空对象
      2. 是用`.env`字符串与mode进行拼接并与`.env`,`.env.${mode}`,`.env.local`,`.env.local.${mode}`形成数组,是用`fromEntries`方法根据这些文件名到`envDir`中读取配置文件,是用`dotenv`进行解析,形成一个对象。在第二部解析产生对象时,由于数组的先后顺序,后面的配置文件的属性值将替换前面配置文件中的同名属性
      3. 检查`process.env`中的`NODE_ENV,BROWSER,BROWSER_ARGS`是否有值,如果没有,则是用第二部产生的对象中的同名属性值替换
      4. 从2中的对象中,挑选属性名以`prefixes`开头的属性加入1中生成的空对象,只有这些属性可以暴露给客户端
      5. 重`process.env`中,挑选属性名以`prefixes`开头的属性加入1中生成的空对象,只有这些属性可以暴露给客户端
      6. 返回空对象

      `dotenv`根据文件名来区不同环境对应的环境变量文件:

      - `.env`为所有环境共用的变量
      - `.env.development`为默认情况下开发环境需要用到的环境变量
      - `.env.production`为默认情况下生产环境需要用到的环境变量

      如果我们希望配置更多不同环境的配置文件,可以通过`package.json`中配置不同的命令实现:

      ```json
      "scripts": {
      "dev": "vite",
      "build": "vue-tsc && vite build",
      "test": "vite --mode test",
      "preview": "vite preview"
      },

process是node端关于进程的对象

客户端是用环境变量

vite会将环境变量同时注入import.meta.env中,以便在客户端是用环境变量(即组件,axios,等文件内)

由上文对loadEnv的源码分析可知,vite对环境变量进行了拦截,避免将不必要的隐私数据注入到import.meta.env中,只有以prefixes开头的环境变量才会被注入

也可以直接在配置文件中通过envPrefix进行配置

1
2
3
export default defineConfig({
envPrefix: "ENDER_",
})

Vite如何让浏览器识别.vue

Vite实际上在开发环境中运行整个项目签会使用node开启一个服务,并监听特定端口的请求(默认未5173

当我们在浏览器中输入url时,Vite服务会根据URL找到对应的文件,以response的形式应答浏览器,并以Content-Type字段告诉浏览器应该以什么方式解析返回的结果

当请求的文件时.vue文件时,Vite服务读到文件后会进行AST语法分析(这一过程就是vue-loader的功能),最终生成JS语法文件,然后以text/javascript格式相应浏览器,浏览器就会以JS格式解析.vue文件

Vite如何处理css

Vite原生支持css文件的处理

  1. vite读到main.js中引用了index.css文件
  2. 是用fs模块读取index.css中的文件内容
  3. 直接创建一个style标签,将index.css中文件内容粘贴到<style>标签中
  4. <style>标签插入index.html的head中
  5. 将css文件内容直接替换为js脚本(便于热更新,或模块化css),让浏览器以js的方式解析css文件

但如果我们进行协同开发,不同的组件是用了不同的css文件,且这两个文件中都有名字为.footer的样式,此时后导入的组件的样式将覆盖先导入的组件的样式

此时可以是用cssmodule来解决这一问题

cssmodule即css模块化,是一个基于node的css处理模块:

  1. 他读取${style}.moduel.css的文件module是一种约定,表示开启模块化
  2. 将其中的所有类目进行替换,即在原有类名的末尾添加一串hash字符串
  3. 同时创建一个对象让旧类名作为key,新类名作为value
  4. 将替换完成的内容放入head标签中
  5. module.css文件中的内容替换为js
  6. 将3中创建的对象进行默认导出

同时less、sass等都可以使用同样的方法进行模块化

Vite中配置css的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
css: { // TODO: 对css的行为进行配置, modules的配置会传给postcss处理
modules: { // TODO: 对css模块化的默认行为进行覆盖
localsConvention: "camelCase", // 配置类名展示形式,camelCase表示驼峰命名支持驼峰式和连接符式
scopeBehaviour: "global", // 配置当前的模块化行为是模块化还是全局化global表示全局,local表示模块化
// generateScopedName: (name, filename, css) =>{return `${name}_${Math.random().toString(36).substr(3, 8)}`}
// name代表css文件中原本的类名
// filename代表css的绝对路径
// css代表当前样式
generateScopedName: "[name]_[local]_[hash:5]", // 生成的类名规则,其中格式通配符定义在postcss文档中给出,可以配置为函数
hashPrefix: "ender", // 生成hash时,hashPrefix会被加到类名中来生成hash以降低hash冲突
globalModulePaths: [], // 代表不想参与css模块化的路径,建议使用Path.resolve生成绝对路径,通常如果自己的css中使用'@import'引入了第三方css时,需要为第三方css配置
},
},

Vite配置css预处理器

在没有构建工具的情况下可以直接安装less或sass,然后使用对应的编译程序编译less或sass代码:

1
2
3
4
5
yarn add less // 安装less

npx lessc style.module.less // 编译less文件

npx lessc --math="always" style.module.less // 编译less文件

这些lessc命令使用到的参数都可以在vite中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
preprocessorOptions: {  // TODO: css预处理器配置采用key + config的形式处理
less: {
// 在webpack中是通过配置less-loader实现的
math: "always", // 配置对数学表达式的编译,
global: {
// 配置全局变量
mainColor: "red" // 此时在less文件中即可读取该变量
},
},
scss: {
}
}

SourceMap

我们的项目上线时被压缩或者被编译过,那么当程序出错后,报错的位置信息将不再正确

此时我们可以开启vite的devSourcemap配置:

1
devSourcemap: true  // 开启css的sourceMap,即文件索引,开启后,项目启动时在浏览器中可以直接索引到样式所在的文件

这样在启动项目之后,即使less或者sass被编译成了其他文件,我们依然可以在浏览器的开发者中直接看到样式所在的源文件

PostCSS

与babel相对于js一样,postCSS是对css进行兼容性处理的软件

我们思考预处理器无法完成的事情:

  • 对未来css属性的一些使用降级问题
  • 前缀补全: –webkit(为了兼容低版本浏览器)

使用

首先需要安装postcss

1
yarn add postcss-cli postcss -D

随后我们可以通过描述文件控制postcss
postcss支持多种格式的配置文件,但是最常用的还是js

在配置文件中我们可以对postcss用到的插件进行统一的整理

例如经常使用的postcss-preset-env预设环境,其中包含了一些默认的插件,例如语法降级,编译等插件

1
2
3
4
5
6
7
8
9
10
//postcss.config.js

const postcssPresetEnv = require("post-preset-env")
module.export = {
// 使用预设环境添加语法降级和编译功能
// 使用less,sass进行语法编译,但目前postcss的less和sass插件已经停止维护了
// 因此less和sass目前是独立于postcss进行编译的,编译完成后在交由postcss,但实际上postcss在功能上是可以包含less和sass的
// 此后postcss也被称为后处理器,即less和sass处理后交由postcss处理
plugins: [postPresetEnv(/* pluginsOptions */)]
}

使用postcss可以对指定文件进行编译:

1
npx postcss style.css -o result.css

Vite中配置postCSS

vite支持postCSS因此只需要在vite.config.js中进行配置即可:

1
2
3
4
5
css: {
postcss: {
plugins: [postcssPresetEnv()] // 配置postcss中使用plugins
}
},
  • postcss-preset-env: 支持css变量、未来语法,甚至自动补全(auto-prefixer插件的功能)

vite同样支持直接读取postcss.config.js中的配置,但在vite.config.js中进行的配置优先级更高。

Vite加载静态资源

除了动态API以外,绝大多数资源都会被视为静态资源

vite对于静态资源基本上是开箱即用的

对于assets中的图片、Json文件,使用import引入即可,并且json通过import引入之后直接就为对象,但在其他的构建工具中导入的json文件将作为json字符串

1
2
3
4
5
// people.json
{
name: "小王",
age: 18
}
1
2
3
4
5
6
7
import jsonFile from "./src/assets/json/people.json"
console.log("jsonFile: ", jsonFile)

// 也可以使用展开运算符部分导入
import { name } from "./src/assets/json/people.json"
// 这样构建生产环境时可以触发Tree Shacking机制避免不必要的导入,提升性能
console.log("name: ", name)

在导入资源时,我们还可以定义其导入的方式

1
2
3
4
5
6
7
8
9
// 以url的形式导入(默认形式)
import imgUrl from "./src/assets/images/a.png?url"
// 输出为图像绝对路径
console.log("imgUrl: ", imgUrl)

// 以raw的形式导入
import imgRaw from "./src/assets/images/a.png?raw"
// 输出为图像Buffer,即二进制字符串
console.log("imgRaw: ", imgRaw)"

但有时候我们需要在组件中引入assets中的某张图片,但是由于组件目录过深导致使用相对路径导入时需要使用很多次../来退回到src目录下,此时就可以利用Vite的resolve.alias别名配置来定义绝对路径的别名:

1
2
3
4
5
6
7
8
9
10
11
import path from "path";

export default defineConfig({
resolve: {
alias: {
// 使用path.resolve和__dirname拼接绝对路径
'@': path.resolve(__dirname, "./src")
'@assets': path.resolve(__dirname, "./src/assets")
}
}
})

这样引入时只需要使用别名引入静态资源

1
import imgUrl from "@assets/images/a.png"

关于SVG

SVG的优缺点:

  • 优点:
    • 体积小
    • 不会失真
  • 缺点:
    • 无法表示层次复杂的图片

在导入图像时我们讨论过使用不同的格式加载图片得到的结果,加载SVG文件时同理,但使用RAW格式加载SVG得到的结果这不再是buffer,而是文件内容的字符串,因此,我们可以直接将RAW格式的SVG添加到页面中,还能进行修改颜色、绑定事件等等操作:

1
2
3
4
5
6
7
8
9
10
import svgRaw from "./assets/svgs/svgFile.svg?raw";
// 结果为字符串
console.log(svgRaw)
document.body.innerHTML = svgRaw

// 绑定事件与修改颜色
const svgElement = document.getElementByTagName("svg")[0];
svgElement.onmouseenter = function() {
this.style.fill = "red"
}

Vite生产环境对静态资源的处理

Vite使用rollup打包后,会在/dist文件夹下生成最终的文件,但其中静态资源的路径使用的是以/dist为根目录的相对路径,因此如果我们在项目文件夹下启动Live Server等工具进行预览时,会出现找不到资源的问题,此时只需要切换到/dist目录下启动服务即可。(Webpack中可以通过配置baseURL来解决)

打包后的静态资源会在文件名后添加hash字符串,这是由于浏览器自身具有缓存机制,对于同名资源浏览器不会重新加载,而是直接读取缓存。因此需要hash字符串来避免文件名一致,保证浏览器会重新请求静态资源。

Vite中使用的hash算法是基于文件名和文件内容的,因此当我们对文件内容进行修改时产生的hash值就会改变,从而引起重新加载。

在构建生产包时的配置可以在build中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
build: {
// 取消文件压缩
minify: false,
rollupOptions: { // TODO: 配置rollup的构建策略
output: { // 控制输出
// 占位符控制生成静态资源文件的名称
// hash为文件名和文件内容组合生产的字符串
assetFileNames: "[hash].[name].[ext]"
}
},
// 静态资源阈值。4000KB,小于该阈值的图片将被打包为base64图片
assetsInlineLimit: 4096000,
// 导出文件夹名称
outDir: "outDist",
// 导出静态资源文件夹名称
assetsDir: "static",
// 清楚输出目录中的所有文件
emptyOutDir: true,
}

resolve.alias原理

在我们对alias进行配置时,实际上Vite在开启服务后,会读取vite.config.hs中的内容,当我们输入URL请求文件时,Vite服务会拿到我们请求的文件名,然后使用Object.entries()得到resolve.alias中的键值对,然后将文件中import...from中的@替换为配置的path.

Vite插件

Vite会在生命周期的不同阶段调用不同的插件已达成某些目的

事实上中间件、插件,都具有类似的功能,例如Redux中间件就是在Redux dispatch一个action之后,到达reducer之前进行的一些操作

实际上如果想自己开发一个Vite插件,Vite为我们提供了许多生命周期钩子函数,我们可以利用这些钩子函数,在不同的钩子函数中实现我们希望的功能,那么一个属于我们的插件就完成了。

Vite在构建生产环境时使用的是rollup,那么Vite会提供一些与rollup生命周期相同的生命周期通用钩子,使用这些钩子,Vite会在开发环境以及生产环境中都调用这些钩子。另外Vite还提供了一些Vite特有的钩子

下面列举一些Vite常用的特有钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (filePath) => {
if(filePath.includes("node_modules")) {
return "vendor"
}
}
}
}
},
plugins: [
{
config(options) {
// 处理配置文件
},
configureServer(server) {
// 处理vite服务器事业
server.middlewares.use((req, res, next) =>{})
},
transformIndexHtml(html){
// 处理index.html文件
},
configResolved(options) {
// 整个配置文件的解析流程完成后调用该钩子函数,用于读和存储解析完成的配置文件
// vite内部有一个默认的配置文件
console.log("options: ", options)
},
configurePreviewServer(server){
// vite打包完成后,可以使用vite preview命令预览生产环境下的项目
// 功能与configureServer基本相同只不过是配置preview阶段开启的服务
},
handleHotUpdate() {
// 热更新相关钩子,用于自定义热更新行为
},
// vite和rollup通用钩子
options(rollupOptions) {
// 处理rollup配置,即build.rollupOptions对象
// vite会调用该钩子,rollup也会
},
buildStart(fullRollupOptions) {
// 与configResolved功能相同,拿到编译完后的rollup配置
},
}
]
})

下面介绍一些常用插件

Vite-aliases

该插件可以帮助项目自动生成别名

他会检查目录下包括src在内的所有目录,并帮助框架自动生成别名

该插件能自动生成配置:

1
2
3
4
5
6
{
"@": "/**/src",
"@assets": "/**/src/assets",
"@components": "/**/src/components",
//...
}

该插件有许多配置,例如prefix用来修改别名的前缀,默认为@

具体参考:Vite-Aliases Github

下面尝试手写实现一个vite-aliases插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Vite的插件需要返回一个配置对象

// const fs = require('fs')
// const path = require('path')

import fs from 'fs'
import path from 'path'

/**
*
* @param {Array} dirFilesArray 包含所有文件名的数组
* @param {string} basePath 跟目录
* @returns {object} 包含两个数组属性的对象:dirs表示目录下所有目录名,files表示目录下所有文件名
*/
const diffDirAndFile = (dirFilesArray = [], basePath = '') => {
const result = {
dirs: [],
files: [],
}
dirFilesArray.forEach((name) => {
// TODO: 读取每个文件的文件状态
const currentFileStates = fs.statSync(
path.resolve(__dirname, `${basePath}/${name}`)
)
// TODO: 判断是否为目录
const isDir = currentFileStates.isDirectory()

if (isDir) result.dirs.push(name)
else result.files.push(name)
})
return result
}

/**
*
* @param {string} prefix 自定义前缀,默认为@
* @returns {object} key代表别名,value代表绝对路径
*/
const getAllFills = (prefix = '@') => {
// TODO: 读取src目录下的所有文件文件名
const result = fs.readdirSync(path.resolve(__dirname, '../src'))
// TODO: 区分目录名和文件名
const diffResult = diffDirAndFile(result, '../src')
// TODO: 对于每个目录名,产生一组别名到绝对路径的键值对
const resolveAliasesObj = {}
diffResult.dirs.forEach((name) => {
const key = `${prefix}${name}`
const absPath = path.resolve(__dirname, `../src/${name}`)
resolveAliasesObj[key] = absPath
})

return resolveAliasesObj
}

export default () => {
return {
/**
*
* @param config UserConfig 当前Vite配置文件
* @param env { mode: string, command: string} mode为当前所处环境,command为serve或build
*
*/
config(config, env) {
// TODO: 对src目录下的所有文件进行处理
const resolveAliasesObj = getAllFills()
console.log(resolveAliasesObj)
// 该函数返回的对象是部分的ViteConfig,返回的对象将于与iteConfig进行Deep Merge
return {
resolve: {
alias: resolveAliasesObj,
},
}
},
}
}

Vite-plugin-html

赋予开发者动态控制整个html文件中内容的能力

可以通过ejs语法向html中注入内容,它在服务端用的比较多,因为服务端经常会动态地修改index.html

而Vite实际上运行在服务端,因此选用该语法进行注入

1
2
3
4
5
6
7
createHtmlPlugin({
inject: {
data: {
title: "主页" // 修改index.html中的title标签
}
}
})

其原理是通过Vite的transformIndexHtml钩子实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = (options) => {
return {
// 用于转换html
transformIndexHtml: {
// 将插件执行时机提前
enforce: "pre"
transform: (html, ctx) => {
// ctx表示执行上下文,包含api, /index.html, get, post, headers
return html.replace(/<%= title %>/g, options.inject.data.title);
}
}
}
}

在该插件实现的过程中我们使用了enforce:"pre"配置,让插件的运行周期提前,实际上vite在运行各种插件时是按照一定顺序进行的:

  • Alias
  • 带有enforce: "pre"的用户插件
  • Vite核心插件
  • 没有enforce属性的插件
  • Vite构建用的插件
  • 带有enforce: "post"的用户插件
  • Vite后置构建插件(最小化、manifest,报告)

因此当我们不设置enforce属性时,会先运行Vite核心插件,导致核心插件先读取了index.html的内容,然后直接报错,因此需要在其他组件执行之前,将ejs替换为真实的内容。

Vite-plugin-mock

mock数据指模拟数据,该插件依赖于mockJS

在前后端并行开发时,我们通常需要先完成接口文档的编写,后端同学根据接口文档编写接口以保证返回的数据结构相同。

有了接口文档后,就可以利用mock数据来模拟从后端请求到的数据进行开发。一般有两种方法:

  1. 简单方式: 直接写死
    • 优点
      • 书写简单方便调试
    • 缺点
      • 无法进行海量数据的测试
      • 无法获得标准数据
      • 无法感知http的异常
  2. mockJS:模拟海量数据

首先我们在根目录下创建一个mock目录,并添加index.js来编写mock脚本

相关语法可以在mockJS中查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// /mock/index.js
import mockJS from "mockjs"

const userList = mockJS.mock({
"data|100": [{
name: "@cname", // 生成不同中文名
ename: "@first @last", // 生成不同英文名
"id|+1": 1, // 从1开始每次自增1的id
avatar: mockJS.Random.image(), // 生成不同图片地址
time: "@time" // 生成不同的时间
}]
})

export default [
{
method: "post",
url: "/api/users",
response: ({body}) => {
// body -> 请求体
return {
code: 200,
msg: "success",
data: userList
}
}
}
]

随后在Vite中进行配置后,我们就可以在项目中像使用真正的API一样,用Axios请求得到结果了:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineConfig } from "vite"
import { ViteMockServe } from "vite-plugin-mock"

export default defineConfig({
plugins: [
ViteMockServe({
// mock脚本目录
mockPath: "mock",
// 配置只有开发环境才会拦截请求
localEnabled: command === 'serve'
})
]
})

其余配置可以在vite-plugins-mock插件中查看。

通过实际使用我们可以感受到该插件是通过拦截了我们的请求实现的,随后我们也知道Vite在开发环境中会为前端开启一个服务,以此来实现热更新、跨域等等功能,该插件就是利用了Vite启动的服务器为我们拦截了请求,实际上该插件是通过Vite的configureServer钩子来得到Vite服务器相关配置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import fs from "fs"
import path from "path"

export default(config) => {
return {
config(config, env) {
// 处理配置
}
configureServer(server) {
// 服务器相关配置
// 真实情况下使用异步
const mockState = fs.stateSync("mock")
const isDir = mockState.isDirectory()
let mockResult = []
if(isDir) {
// 使用process.cwd()获取当前执行根目录
mockResult = require(path.resolve(process.cwd(), "mock/index.js"))

}
// server.middlewares获得服务器的中间件
// node服务器对请求的处理是按照顺序交给一个又一个中间件进行处理的
// req: 请求对象, 用户发送的请求,包括请求头、请求体
// res: 响应对象, 包括响应头res.header
// next: 是否交给下一个中间件进行处理,调用next方法则会交给下一个中间件
server.middlewares.use((req, res, next) => {
// 查找mock脚本文件中是否包含请求的url
const mockMatch = mockResult.find(mockDescriptor => mockDescriptor.url === req.url)
if(req.url === "/api/users") {
}
if(mockMatch) {
// 调用mock脚本的响应函数得到响应内容
const responseData = mockDescriptor.response(req)
// 设置响应头中的响应类型为json避免中文乱码
res.setHeader("Content-Type", "application/json")
// 返回请求
res.end(JSON.stringify(responseData))
}
next()
})
}
}
}

Vite-plugin-federation

模块联邦插件

Webpack5提出联邦(federation)的概念,允许两个不同项目访问彼此的方法与组件。
Vite也具备实现类似功能的插件

Vite + Ts

Vite原生支持TS

但Vite只对ts进行编译,不会执行ts中的类型检查,Vite假设ts类型检查已经被编辑器执行了。但可以通过tsc --noEmit在打包阶段进行ts类型检查,如果检查不通过则不会生成打包文件,可以通过修改package.json -> scripts.build脚本,先运行tsc --noEmit进行检测再进行build:

1
2
3
"scripts": {
"build": "tsc --noEmit && vite build"
}

但是在Vite中,我们如果想让ts的报错直接输出在控制台,该如何做呢?

可以使用vite-plugin-checker插件

1
2
3
4
5
6
7
8
import checker from "vite-plugin-checker"

export default defineConfig({
plugins: [
// 开启ts语法检测
checker({typescript: true}),
]
})

ts包含一些配置,可以在tsconfig.json中编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node", // 配置模块解析方法为Node, 规定ts找包是从node_modules中找
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": false,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"], // 声明当前环境,让ts在该环境下进行语法检测
"skipLibCheck": true, // 是否跳过node_modules目录的检查
"noEmit": true,
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

此外,如果我们希望向环境变量中引入一些提示,这可以在vite-dev.d.ts中进行配置:

1
2
3
4
5
6
7
// ts三斜线指令,读到该配置时Vite自动引入"vite/client"
/// <reference types="vite/client" />

// 此处编写的借口将与ts默认类型进行合并,此后调用import.meta.env就能获得关于VITE_PROXY_TARGET的语法提示
interface ImportMetaEnv {
readonly VITE_PROXY_TARGET: string;
}

Vite性能优化

性能优化说的是在哪些方面进行优化?

  • 开发时态的构建速度优化:npx run dev呈现结果要占用很长时间、以及热更新需要很长时间才能反应到页面上
    • webpack4可以通过cache-loader,webpack5可以通过cache来缓存上一次loader的结果,如果两次构建源代码产生的结果没有变化这不调用loader,另外thread-loader还能开启多线程构建
    • 然而Vite是按需加载的形式,不太需要关注这些
  • 页面性能指标:与写代码的方式有关
    • fcp(first content paint):首个元素染时间
      • 懒加载优化
      • http优化:
        • 强缓存:服务端给响应头追加一些字段(expires),客户端会记住这些字段,在expires(截止失效时间)没有到达之前,无论如何刷新页面,客户端都不会重新请求,而是从缓存中取
        • 协商缓存:是否使用缓存需要和后端协商,当服务端将客户端标记为协商缓存后,客户端下次刷新页面需要重新请求资源时会先向服务端发送一个协商请求,服务端如果认为资源发生了变化则响应具体的内容,如果服务端认为没有变化则响应304
    • lcp(largest content paint):页面中最大元素的渲染时长
  • js逻辑
    • 要注意副作用的清除:
      • 例如一个组件中包含一个计时器,由于项目运行过程中组件会发生频繁的挂载与卸载,如果不清楚计时器,每次挂载时又会创建新的计时器。所以在React的函数组件中,我们如果要在声明周期中使用计时器,通常会在useEffect的返回函数中清除定时器
    • 写法上的注意事项: requestAnimationFrame, requestIdleCallback
      • 浏览器的帧率为:16.6ms一帧(包括执行js逻辑以及回流、重绘),当js逻辑、回流、重绘这些任务的处理时长超过16.6ms,则会出现明显卡顿。
      • requestIdleCallback接收一个回调函数当浏览器一帧的任务进行到尾声时如果执行时间不足16.6ms则会执行传入的回调
      • react也提供了concurrency(concurrent mode)可中断渲染来处理卡顿的问题
    • 防抖 节流建议使用lodash提供的,避免自己书写时造成的性能问题
    • 另外对于保存了海量数据的Array,使用lodash中提供的forEach方法代替Array原型上的forEach,lodash的forEach引入了额外的算法平衡性能
  • 对作用域的控制
    • 在写for循环的时候,为了避免作用域嵌套层级过深,需要将for块级作用域中用到的变量保存在该作用域中,避免多次通过作用域链向外部作用域查找数据。
  • css
    • 关注继承属性,能继承的不要重复写
    • 尽量避免过深的css嵌套
  • 构建优化(rollup)🌟
    • 优化体积
      • 压缩
      • treeShacking
      • 图片资源压缩
      • CDN加载
      • 分包策略
1
2
3
4
5
6
7
// react中处理计时器
const [timer, setTimer] = useState(null);
useEffect(() => {
setTimer(setTimeout(() => {}))
// 组件卸载时会调用useEffect的返回值
return () => clearTimeout(timer);
})
1
2
3
4
const arr = [1, 2, 3]
// 在for块级作用域中创建变量len,避免比较大小时每次向外询问arr.length
for(let i = 0, len = arr.length; i < len; i++) {}
// 较低性能写法:for(let i = 0; i < arr.length; i++){}

分包策略

浏览器对于静态资源的缓存策略是,文件名未发生变化,则不会重新请求

当我们在业务代码中引用了lodash等包时,打包的过程中会将这些第三方包也和业务代码打包到同一个js文件中,此时会为业务代码引入大量代码,而第三方包却是长期不会发生变化的。

当我们对业务代码进行修改时,打包生成的文件会加入hash字符串,从而改变文件名,让浏览器重新请求,但这个过程中第三方包也被加入了进去,因此第三方包也会和业务代码一起被重新请求,导致需要请求大量没有变化的代码。

分包就是把一些不会更新的代码进行单独打包

例如我们可以通过build.rollupConfig.output.manualChunks配置分包策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import checker from "vite-plugin-checker"

export default defineConfig({
plugins: [
// 开启ts语法检测
checker({typescript: true}),
]
build: {
rollupConfig: {
output: {
manualChunks: (id: string) => {
// id为包路径名
// 对于所有路径名中包含node_module的包,都打包到vender.js中
if(id.includes("node_module")) {
return "vender"
}
}
}
}
}
})

此外对于多出口的项目,首先我们可以通过配置build.rollupConfig.input来处理不同出口文件的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import checker from "vite-plugin-checker"
import path from "path"

export default defineConfig({
plugins: [
// 开启ts语法检测
checker({typescript: true}),
]
build: {
rollupConfig: {
input: {
main: path.resolve(__dirname, "./index.html"),
product: path.resolve(__dirname, "./product.html")
},
output: {
manualChunks: (id: string) => {
// id为包路径名
// 对于所有路径名中包含node_module的包,都打包到vender.js中
if(id.includes("node_module")) {
return "vender"
}
}
}
}
}
})

vite为我们进行了优化,当index.html依赖的main.ts文件引用了product.html依赖的product.ts文件,而两个ts文件均引用了lodash等第三方包时,vite会将main.ts中的对lodash的引用去除,并直接通过product.ts引用lodash以缩小包体积,当然当我们设置了lodash分包后,两个文件都会直接充lodash包中引用

gzip压缩

将静态文件进行压缩,已达到减少体积的目的

Vite服务将会对静态文件进行压缩,客户端收到压缩包后进行解压

在工程化领域内存在chunk的概念,也可以叫块

块最后会被映射为js文件,但是块并不是js文件,在vite中这一概念不如webpack中明显

当我们单个chunk的大小超过500Kb后Vite会给出警告⚠️,建议我们采用分包策略,动态导入,或者调整警告阈值。

另外我们还可以用gzip来压缩chunk的体积

我们可以通过vite-plugin-compression插件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import checker from "vite-plugin-checker"
import path from "path"
import viteCompression from "vite-plugin-compression"

export default defineConfig({
plugins: [
// 开启ts语法检测
checker({typescript: true}),
//
viteCompression()
]
build: {
rollupConfig: {
input: {
main: path.resolve(__dirname, "./index.html"),
product: path.resolve(__dirname, "./product.html")
},
output: {
manualChunks: (id: string) => {
// id为包路径名
// 对于所有路径名中包含node_module的包,都打包到vender.js中
if(id.includes("node_module")) {
return "vender"
}
}
}
}
}
})

此时再进行打包,dist目录下除了会生成index.js以外,还会生成index.js.gz文件

此时服务端拿到打包后的代码后,如果收到请求index.js则服务端会读取index.js.gz,然后设置响应头为content-encoding: gzip此时浏览器检查收到的响应中是否包含gzip,如果有这进行解压操作,得到原本的js文件

由上面的流程可以发现解压操作需要浏览器来做,因此需要浏览器承担一部分解压的消耗。对于较小的文件进行压缩,就可能会适得其反,降低性能。

动态导入

动态导入的思想与vite的按需加载是异曲同工的

动态导入是es6的一个新特性

对于一个需要导入的包,我们只需要以函数的方式使用import就可以实现动态导入:

1
import("./src/imgLoader")

这种方式的主要应用场景是在进行路由时,实现动态加载组件:

1
2
3
4
5
6
7
8
9
10
11
// 以react中的路由为例
const routes = [
{
path: "/home",
component: import("./src/components/Home")
},
{
path: "/login",
component: import("./src/components/Login")
}
]

import函数返回一个promise对象,他的原理实际上是拿到组件文件后先对组件进行编译为js,随后并不会使用<script src="./src/components/login.js">来引入js文件,而是在没有进入某个页面时将promise的状态设为pending,当进入该页面后,将promise的状态设为fulfilled,将<script src="./src/components/login.js">插入body中实现引入。

CDN加速

CDN: content delivery network内容分发网络

利用这一技术我们可以充CDN服务器上请求资源或第三方包,而不需要将第三方包打包进我们的项目中去。

使用CDN请求包的好处是,CDN服务器会根据地理位置的远近选择距离最近的可用服务器进行响应,从而加速请求。

以前在原生项目中我们使用JQuery时经常会用到cdn技术,例如充百度CDN中引入JQuery:

1
2
3
<head>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>

当Vite在开发环境并不能那么轻松的修改html,因此Vite可以通过vite-plugin-cdn-import插件配置cdn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import viteCDNPlugin from "vite-plugin-cdn-import"

export default defineConfig({
plugins: [
viteCDNPlugin({
modules: [
{
// 需要使用CDN引入的包名称
name: "lodash",
// 引入后的变量名
var: "_",
// 引入的CDN地址
path: "https://unpkg.com/lodash@4.17.21/lodash.min.js"
}
]
})
]
})

随后在组件中直接正常引入lodash即可在开发环境中使用,打包的时候rollup和vite将不会把通过cdn引入的包打包进我们的工程

需要注意⚠️的是,使用CDN引入包后,我们需要项目时刻保持联网状态才能想CDN服务器请求我们需要的包。

Vite跨域

跨域是存在于浏览器端的问题,主要是由于浏览器最核心最基础的安全功能——同源策略限制

当一个请求url的协议,域名,端口三者中任意一个与当前页面的URL不同则为跨域

发生跨域时候,服务器实际上已经响应了请求,只是浏览器对响应的请求进行了拦截,因为发送请求时浏览器是无法进行拦截的,浏览器并不知道请求的服务器是否运行跨域

Vite支持采用Proxy的方式解决跨域问题

实际上是用node代替前端对后端进行请求(因为服务端没有跨域限制)

假设有后端接口:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express')
const app = express()

//创建get请求
app.get('/user',(req,res)=>{
res.json({
code:200,
message:"请求成功"
})
})
//端口号9001
app.listen(9001)

我们直接在前端使用fetch会报跨域错误:

1
2
3
4
<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('http://localhost:9001/user')
</script>

此时可以开启vite的代理

vite中在配置文件中设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vite.config.ts
export default defineConfig({
plugins: [vue()],
server:{
proxy:{
// key + 描述对象的形式
'/api':{
// 遇到/api开头的请求,将其代理到targe表示的地址
target:"http://localhost:9001/", //跨域地址
changeOrigin:true, //支持跨域
rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
}
}
}
})

然后将前端请求url修改为/api/user即可

webpack proxy 和 node proxy 用法都类似

需要注意的是proxy只在开发环境中生效,生产环境由于网页会上服务器,前后端分离可以使用nginx,apache等等服务器的代理实现

vite的proxy底层实现实际上就是监听前端页面端口的请求后,向服务器端口发送请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// proxyMiddleware 中的代码
import httpProxy from 'http-proxy'
export function proxyMiddleware(
httpServer: http.Server | null,
options: NonNullable<CommonServerOptions['proxy']>,
config: ResolvedConfig
): Connect.NextHandleFunction {
// lazy require only when proxy is used
const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server

// http-proxy 模块用于转发 http 请求
// 其实现的大致原理为使用 http 或 https 模块搭建 node 代理服务器,将客户端发送的请求数据转发到目标服务器,再将响应输送到客户端
const http = require('http')

const httpProxy = require('http-proxy')

const proxy = httpProxy.createProxyServer({})

//创建一个代理服务 代理到9001
http.createServer((req,res)=>{
proxy.web(req,res,{
target:"http://localhost:9001/xm", //代理的地址
changeOrigin:true, //是否有跨域
ws:true //webSocetk
})
}).listen(8888)

Vite总结

Vite相关配置

Pinia

Pinia是一款状态管理工具,是Vuex的替代品,相较于Vuex,Pinia.js具有以下特性:

  • 完整的ts支持
  • 轻量化,压缩后体积只有1kb
  • 去除mutations,只有state、getters、actions
  • actions支持同步和异步
  • 代码扁平化没有模块嵌套,只有store的概念,store之间可以自由使用,每一个store都是独立的
  • 无需手动添加store,store一旦创建便会自动添加
  • 支持Vue3和Vue2

安装

1
npm install pinia

引入

Vue3

在main.ts中进行如下引入

1
2
3
4
5
6
7
8
9
10
11
import { createApp } from 'vue'
import App from './App.vue'
import {createPinia} from 'pinia'

const store = createPinia()
let app = createApp(App)


app.use(store)

app.mount('#app')

Vue2

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
el: '#app',
// other options...
// ...
// note the same `pinia` instance can be used across multiple Vue apps on
// the same page
pinia,
})

修改state的方法

  1. 直接修改
    • 在使用const store = useMyStore()方法得到store可以直接访问store中的state并对其中的数据进行修改:
    • store.val = 2
  2. 使用patch批量修改
    • store.$patch({ val: 2 })
  3. 使用patch函数参数形式
    • store.$patch((state) => { val: 2 })
    • 这种方法可以在函数中加一些业务逻辑
  4. 直接重新构造state
    • store.$state = {val:2}
    • 这种方法必需修改state中的所有值
  5. 使用actions中定义的方法修改值
    • store.updateVal(num)

Store

解构store

store中的state属性可以通过解构的方式得到,但是结构出来的数据并不具有响应式

1
2
const store = useMyStore()
const {val} = store

因此对store中的值进行修改不会反应到结构出来的val

为了解决这个问题pinia提供了一个hook可以将普通变量变为响应式

1
2
3
import { storeToRefs } from "pinia"
const store = useMyStore()
const {val} = storeToRefs(store)

这也无论是对val进行修改或是对store.val进行修改,或是使用actions进行修改都会同时反应到valstore.val

getter和actions

getter中定义一些函数,类似于计算属性,可以修饰一些值:

actions中定义一些函数用于对state中的值进行操作,同时支持异步和同步,并且可以相互调用。

Pinia API

  1. $reset()
    • 不接受任何参数,没有返回值
    • 调用后将state设置为初始值store.$reset()
  2. $subscribe(Function, object | null)
    • 接收一个工厂函数,函数有两个参数arg,state,分别表示影响(包含新值、旧值等等)和新的state
    • 当state中的值发生变化就会自动调用传入的工厂函数
  3. $onAction(Function, boolean | null)
    • 调用actions中的方法时出发Function
    • 接收一个工厂函数,接收一个对象arg,包含一个after回调,store实例等等
    • 第二个参数为true时,则即使调用onAction的组件被销毁,该函数依然会继续监听

Pinia插件

持久化

pinia和vuex都不具备持久化的功能,页面刷新后其中的数据就会变为初始状态

pinia支持自定义插件,可以使用插件来实现pinia中数据的持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//定义兜底变量
const __piniaKey = '__PINIAKEY__'

//定义入参类型
type Options = {
key?:string
}

//将数据存在本地
const setStorage = (key: string, value: any): void => {

localStorage.setItem(key, JSON.stringify(value))

}


//存缓存中读取
const getStorage = (key: string) => {
return (localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {})
}

//利用函数柯丽华接受用户入参
const piniaPlugin = (options: Options) => {

//将函数返回给pinia 让pinia 调用 注入 context
return (context: PiniaPluginContext) => {
const { store } = context;
const data = getStorage(`${options?.key ?? __piniaKey}-${store.$id}`)
store.$subscribe(() => {
setStorage(`${options?.key ?? __piniaKey}-${store.$id}`, toRaw(store.$state));
})
//返回值覆盖pinia 原始值
return {
...data
}
}
}

//初始化pinia
const pinia = createPinia()


//注册pinia 插件
pinia.use(piniaPlugin({
key: "pinia"
}))

评论