简介
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 create vite@latest my-vue-app --template vue yarn create vite my-vue-app --template vue pnpm create vite my-vue-app --template vue
执行过程
在项目搭建完成后我们可以使用
命令来运行项目
该命令首先回去寻找项目目录下的package.json
文件中的scripts
标签中的dev
标签
在Vite项目中dev
标签会被映射为vite
该命令首先会从本地的node_module
中查找bin
目录下的可执行vite
如果没有则会去全局目录中查找
Vue3补充
Vue3支持三种书写风格:
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中,我们则需要使用ref
或reactive
来注册响应式对象:
isRef
可以判断一个对象是否为ref
对象
shallowRef
创建浅层次的响应式对象,即引用对象中的数值变化不会引起它的视图变化,需要地址发生变化才会引起视图变化
需要注意的是如果在同一作用域对ref
和shallowRef
对象同时进行修改,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
的区别参考shallowRef
和ref
的区别:
shallowReactive
的响应式只会添加到对象的第一层属性,即shallowObj.attr
,之后层级的属性没有响应式,例如shallowObj.attr.attr2
同样的当shallowReactive
和reactive
的域相同时,也会受到reactive
的影响而发生改变
to全家桶
toRef
接收两个参数(obj, key)
其中obj为一个响应式对象(传入非响应式对象将不会产生影响)
原因是使用ref
或reactive
创建的响应式对象中的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 interface Options { schedule?: Function } let activeEffect;export const effect = (fn:Function , option: Options ) => { const _effect = function ( ) { activeEffect = _effect let res = fn () return res } _effect.options = option _effect () return _effect } const targetMap = new WeakMap ()export const track = (target, key ) => { let depsMap = targetMap.get (target) if (!depsMap) { depsMap = new Map () targetMap.set (target, depsMap) } let deps = depsMap.get (key) if (!deps) { deps = new Set () depsMap.set (key, deps) } deps.add (activeEffect) } export const trigger = (target, key ) => { let depsMap = targetMap.get (target) if (!depsMap) { console .error ("target is not a effect object" ) return } let deps = depsMap.get (key) if (!deps) { console .error ("target.key is not a effect value" ) return } deps.forEach ( effect => { 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 import { track, trigger } from "./effect" const isObject = (target:any ) => { return target!=null && typeof target == 'object' } export const reactive = <T extends object>(target:T ) => { return new Proxy (target, { get (target, propKey, receiver ) { let res = Reflect .get (target, propKey, receiver) as object track (target, propKey) if (isObject (res)) { return reactive (res) } return res }, set (target, propKey, value, receiver ) { let res :boolean = Reflect .set (target, propKey, value, receiver) 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" export const computed = (getter :Function ) => { let _value = effect (getter, { schedule : () => { _dirty = false } }) let _dirty = true let cacheValue class ComputedRefImpl { get value () { if (_dirty) { cacheValue = _value () _dirty = false } return cacheValue } } 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可以传入一个响应式对象数组 ,此时将会监听数组中的每一个对象的变化
第三个参接收一个对象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
中的组件没有beforeCreate
和created
两个生命周期,而使用setup
代替
Vue3组件生命周期 :
setup
代码块外面的部分
onBeforeMount
获取不到DOM
onMounted
能获取到DOM
onBeforeUpdate
获取更新前的DOM
onUpdated
获取更新后的DOM
onBeforeUnmount
onUnmounte
onRenderTracked((e)=>{})
在依赖收集完成后触发,即访问响应式对象的属性时obj.attr
onRenderTriggered((e) => {})
在触发更新时触发,即修改响应式对象的属性时obj.attr=value
开发时可以使用const instance = getCurrentInstance()
来获取当前组件的window对象,而生命周期函数实际上就是挂载到window对象上的函数,因此可以通过这种方式来检测生命周期函数是否挂载上
函数柯里化
生命周期函数的实现使用了函数柯里化 技术,因为生命函数创建时都由createHook
方法创建,并由名称区别,而其他的参数在同一项目中始终保持不变,因此需要进行函数柯里化 来保存部分参数,而这一技术实际上就是使用了函数闭包 的特性将函数和其外部变量一起保存:
柯里化(Currying)又称部分求值 ,一个柯里化的函数首先会接收一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
简单来说就是:柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©或者f(a, b)©或者f(a)(b, c)
例如下面这个日志函数:
1 2 3 4 5 6 7 const log = (date, project, message ) => { return `${date} ${project} ${message} ` } const logMsg = log ('2022-07-29' , 'xxx后台管理系统' , 'mm接口异常' );console .log (logMsg)
但通常当天日期是不变的,同一个项目的项目名也是不变的(不过不同的项目名是变化的),唯有信息是时刻变化
如果每次都把所有参数全部传入进去会有很多重复
因此可以对它进行柯里化:
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} ` } } } const logMsg1 = log ('2022-07-29' )('A项目' )('接口报错' );console .log (logMsg1); const logMsg2 = log ('2022-08-01' )('B项目' )('接口成功' );console .log (logMsg2); const sameDateLog = log ('2022-07-29' );const logMsg3 = sameDateLog ('A项目' )('接口异常' );console .log (logMsg3); const logMsg4 = sameDateLog ('B项目' )('接口超时' );console .log (logMsg4); const sameDateProjectNameLog = log ('2022-07-29' )('A项目' );const logMsg5 = sameDateProjectNameLog ('网络异常' )console .log (logMsg5);
可以利用递归来封装一个把任意函数变为柯里化函数的函数:
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 ; return function t ( ) { const innerLength = arguments .length ; const args = Array .prototype .slice .call (arguments ); if (innerLength >= len) { return fn.apply (undefined , args) } else { return function ( ) { const innerArgs = Array .prototype .slice .call (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);
柯里化经典面试题
请实现一个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
函数柯里化应用场景
参数复用:即如果函数有重复使用到的参数,可以利用柯里化,将复用的参数存储起来,不需要每次都传相同的参数
延迟执行:传入参数个数没有满足原函数入参个数,都不会立即返回结果,而是返回一个函数。(bind方法就是柯里化的一个示例)
函数式编程中,作为compose, functor, monad 等实现的基础
优点:
柯里化之后,我们没有丢失任何参数:log 依然可以被正常调用。
我们可以轻松地生成偏函数,例如用于生成今天的日志的偏函数。
入口单一。
易于测试和复用。
缺点:
函数嵌套多
占内存,有可能导致内存泄漏(因为本质是配合闭包实现的)
效率差(因为使用递归)
变量存取慢,访问性很差(因为使用了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 )const props = withDefaults (defineProps<{ title : String , arr : number [] }>(), { arr :()=> [6 ] title : "默认值" })
子传父
emit自定义事件方式
子组件声明自定义事件
在自定义事件中访问子组件自己的变量
父组件将自己的函数fun
以回调函数的方式 传递给子组件的事件
子组件将需要传递的数据data
作为回调函数的参数,调用回调函数fun
父组件即可在函数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) } 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>
兄弟传参
兄弟间传参有两种方式:
父组件介入
事件总线BUS
父组件介入传参理解起来相对容易,但写起来很复杂:
父组件使用emit
为子A派发事件
子A利用父组件派发的事件将数据data传递给父组件
父组件使用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 type BusClass = { emit :(name : string ) => void on :(name : string , callback : Function ) => void } type ParamKey = string | number | symbol type eventList = { [key : ParamKey ]: Array <Function > } class Bus implements BusClass { list : eventList constructor ( ) { this .list = {} } emit (name :string , ...args :Array <any > ) { let eventList : Array <Function > = this .list [name] eventList.forEach (fn => { fn.apply (this , ...args) }) } 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 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可以给以下情况添加过度动画:
基本用法
transition提供一个name属性,该属性用于将组件与css样式相对应,该组件提供6个默认对应的样式命名:
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from
被移除),在过渡/动画完成之后移除。
v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
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 ="enter" @after-enter ="afterEnter" @enter-cancelled ="enterCancelled" @before-leave ="beforeLeave" @leave ="leave" @after-leave ="afterLeave" @leave-cancelled ="leaveCancelled"
当只用 JavaScript 过渡的时候,在 enter
和 leave
钩子中必须使用 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-for
,v-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 => { 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
接着使用如下命令生成ts配置文件
然后创建一个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" function useResize (el : HTMLElement , callBack : Function ) { let resize = new ResizeObserver ((entries ) => { callBack (entries[0 ].contentRect ) }) resize.observe (el) } const install = (app : App ) => { app.directive ("resize" , { mounted (el, binding ) { useResize (el, binding.value ) } }) } useResize.install = install 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" export default defineConfig ({ build : { lib : { entry : "src/index.ts" , name : "useResize" , }, rollupOptions : { external : ['vue' ], output : { globals : { useResize : "useResize" } } } } })
接下来在package.json中添加一个打包的命令:
1 2 3 4 5 6 { "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" , "build" : "vite 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中配置一些发布时需要设计到的信息:
配置库作为全局变量被引入的js文件入口“main”
配置库作为es module被引入时的js文件入口“module”
配置上传到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 Vue .Prototype .$http = () => {}app = createApp (App ) app.config .globalProperties .$env = "dev" app.config .globalProperties .$filter = { format<T> (str : T) { return `Ender-${str} ` } } 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编写插件时需要经过以下步骤:
编写用作插件的vue组件,并将方法通过defineExpose暴露
编写ts以对象或函数的形式将vue组件导出
以对象形式导出时要求在对象中实现install函数
install函数主要完成以下几个步骤:
使用createVNode方法,根据vue组件创建虚拟DOM节点VNode
使用render方法将虚拟DOM挂在到指定DOM节点
如果组件有暴露方法则将方法注册为Vue全局函数或全局变量
在main.ts中引入组件,并使用app.use安装编写的插件
为了使用方便,需要在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" export default { install (app : App ) { const vNode : VNode = createVNode (Loading ) console .log (vNode) 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 )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 } 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组件库
ElementUI
样例使用ts,setup语法糖编写
表单需要自己写分页
Ant Design
样例使用ts,setup函数编写
表单包含分页
View Design
样例使用js,选项式API
Vant
移动端UI,支持小程序、vue2、vue3、react
样例使用setup函数模式
包含很多业务组件(地址,联系人编辑等等
提供了很多自定义hook
属性透传
在使用ElementUI时,由于Vue中Scoped的存在,阻止了css样式的向外传播。
由于单页应用会将所有Vue组件合并为一个HTML,因此为了保证css之间互相不影响。vue可以为vue组件中的style标签加scope
scope通过为DOM结构以及css样式上添加唯一不重复的标识:data-v-hash(通过PostCSS转译实现),以保证唯一性
总结scoped的三条渲染规则:
给HTML中的DOM节点加一个不重复的data属性:(data-v-123)来进行区分
给每个css选择器的末尾加上当前组件的属性选择器[data-v-123]
如果组件内部包含其他组件,则只会给其他组件的最外层标签加上当前组件的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 >() const send = ( ) => { chatList.push ({ name : "B" , message : ipt.value }) box.value .scrollup = 9999999 }
因此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 >() const send = ( ) => { chatList.push ({ name : "B" , message : ipt.value }) nextTick (() => { box.value .scrollup = 9999999 }) }
也可以使用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 >() const send = ( ) => { chatList.push ({ name : "B" , message : ipt.value }) 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, 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事实上包含三种编写风格:
template风格
JSX风格
h函数风格
Vue3中已经很少使用了,偶尔会出现在,需要定义一个小组件单又不需要复用,不想为其创建文件夹的场景下
出现的原因是Vue单文件组件编译是需要过程,他会经过
parser编译为AST树 -> transform转化为JS API -> generate生成render函数
render函数则会返回一个h函数包裹的VNode虚拟DOM节点
而h函数直接跳过这三个阶段,所以性能上有很大的帮助。其底层实际上就是调用的createVNode()
方法直接创建虚拟DOM节点,因此实际上使用h函数编写组件就像直接编写虚拟DOM节点。
h函数接收三个参数:
type节点类型
propsOrChildren对象,主要用来表示props, attrs, dom props, class, style
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' }) h ('div' , { class : 'bar' , innerHTML : 'hello' }) h ('div' , { '.name' : 'some-name' , '^width' : '100' }) h ('div' , { class : [foo, { bar }], style : { color : 'red' } }) h ('div' , { onClick : () => {} }) h ('div' , { id : 'foo' }, 'hello' ) h ('div' , 'hello' )h ('div' , [h ('span' , 'hello' )]) 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 --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 ) => { console .log (loadEnv (mode, process.cwd ())) return defineConfig ({ }) }
Vue3性能优化
在开发环境中通常页面性能会使用Chrome浏览器中的LightningHouse进行性能测试
其中包含几个常用指标:
FCP
First Contentful Paint
首屏加载时间
Speed Index
页面各个可见部分的显示平均时间
当有数据需要从后台获取时将影响这一数值
LCP
Largest Contentful Paint
最大内容绘制时间
页面最大的DOM元素绘制所需的时间
TTL
Time To Interactive
从页面开始渲染到用户可以操作的时间间隔
即内容必需渲染完成,且交互元素绑定的事件已经注册完成
TBT
Total Blocking Time
主进程被阻塞的时间
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 ({ 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 , sourcemap : false , minify : "terser" , assetsInlineLimit : 4000 } })
此外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也提供了封装组件的能力,由三部分组成:
Custom Element:自定义元素
Shadow DOM:(微前端也是基于该技术,隔离JS和CSS)用于将组件的css与js和外部缓解隔离
HTML template:允许我们使用模板字符串定义组件中的HTML内容
Web Components包含四个生命周期:
connectedCallback:挂载
disconnectedCallback:断开
adoptedCallback:移动
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" ); s.src = "接口地址xxxxx" + JSON .stringify (obj) document .body .appendChild (s) } 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" : "*" }
Proxy代理
实际上是用node代替前端对后端进行请求(因为服务端没有跨域限制)
假设有后端接口:
1 2 3 4 5 6 7 8 9 10 11 12 const express = require ('express' )const app = express ()app.get ('/user' ,(req,res )=> { res.json ({ code :200 , message :"请求成功" }) }) 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 export default defineConfig ({ plugins : [vue ()], server :{ proxy :{ '/api' :{ target :"http://localhost:9001/" , changeOrigin :true , rewrite :(path ) => path.replace (/^\/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 import httpProxy from 'http-proxy' export function proxyMiddleware ( httpServer : http.Server | null , options : NonNullable <CommonServerOptions ['proxy' ]>, config : ResolvedConfig ): Connect .NextHandleFunction { const proxy = httpProxy.createProxyServer (opts) as HttpProxy .Server const http = require ('http' )const httpProxy = require ('http-proxy' )const proxy = httpProxy.createProxyServer ({})http.createServer ((req,res )=> { proxy.web (req,res,{ target :"http://localhost:9001/xm" , changeOrigin :true , ws :true }) }).listen (8888 )
虚拟DOM与diff算法
虚拟DOM即通过JS生成的AST节点树
虚拟DOM的产生是因为:
而Diff算法则是为了让对虚拟DOM的操作变得更快而产生的。
例如v-for
中不使用key属性时,更新DOM需要经过以下几个阶段:
按从左到右的顺序,新元素替换旧元素
最后如果元素少了,则卸载最后几个
如果元素多了,则新增最后几个
如果使用了key,更新DOM就变为了如下几个步骤:
前序对比算法(从左到右对比新旧元素的key和type,直到第一个不匹配
后序对比算法(从右到左对比新旧元素的key和type,直到第一个不匹配
如果多出新节点,则挂载
如果多出旧节点,则卸载
乱序情况特殊处理
乱序时,先计算得到key与index的映射
然后应用了贪心+二分查找的思想计算最长上升子序列
那么之后只需要将不在最长上升子序列中的元素进行移动即可
如果在其中就不需要移动,这样能够保证移动的次数最少
Tailwind CSS
Tailwind CSS是一个由JS编写的CSS框架,基于PostCSS解析。
PostCSS插件在使用时需要进行一些配置:
PostCSS配置文件postcss.config.js,新增tailwind插件
TailwindCSS配置文件tailwind.config.js
PostCSS处理CSS的大致流程如下:
将CSS解析为抽象语法树(AST树)
读取配置文件,根据该文件生成新的抽象语法树
将AST树“传递”给一系列数据转换操作处理(变量数据循环生成,嵌套类名循环等)
清除一系列操作留下的数据
将处理完毕的AST树重新转化为字符串
安装
1 npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
其中autoprefixer可以 自动为样式添加–web-kit-等等前缀以增强页面的兼容性
生成配置文件
修改配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module .exports = { purge : ['./index.html' , './src/**/*.{vue,js,ts,jsx,tsx}' ], theme : { extend : {}, }, plugins : [], } 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中引入样式文件
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 )declare module 'vue' { export interface ComponentCustomProperties { $Bus : typeof 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端预览项目
安卓预览
如果想在安卓端预览项目,那么首先要对项目打包:
接下来使用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的小插件即可
在根目录下创建plugins目录
为tsconfig.node.json中的include配置扫包目录,以及开启允许隐式Any的设置
1 2 3 4 5 6 7 8 { "compilerOptions" : { "noImplicitAny" : false } , "include" : [ "plugins/**/*" , ] }
在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" , Declaration (node) { if (node.value .includes ("px" )){ const num = parseFloat (node.value ) node.value = `${((num / opt.viewportWidth) * 100 ).toFixed(2 )} vw` } } } }
然后再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" export default defineConfig ({ plugins : [ ], 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
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 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vite/plugin-vue-jsx' export default defineConfig ({ plugins : [vue (), vueJsx ()], resolve : { alias : { '@' : fileURLToPath (new URL ("./src" , import .meta .url )) } } })
TSX的使用
使用TSX创建组件时,先新建一个.tsx
文件后,使用如下方式即可:
export default function ( ) {
return (<div > Ender</div > )
}
<!--code130 -->
- 利用`defineComponent` 方法创建OptionAPI 式组件,需要实现render方法
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 >
</>
)
}
})
<!--code131 -->
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' import AutoImport from 'unplugin-auto-import/vite' export default defineConfig ({ plugins : [vue (), vueJsx (), AutoImport ({ 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 import type { Plugin } from 'vite' import * as babel from '@babel/core' import jsx from '@vue/babel-plugin-jsx' export default function ( ):Plugin { return { name : "vite-plugin-vue-tsx" , transform (code, id ) { 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 } , "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 tsx from './plugin/index' 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等等插件就能直接使用。
构建工具
构架工具具备的功能:
遇到ts文件使用tsc将ts文件转化为js文件
遇到.vue或.tsx/jsx使用react-compiler
和vue-compiler
将其转化为render
函数
遇到less/sass/postcss/windtail使用less-loader
,sass-loader
等将其编译为原生css
遇到老版本浏览器使用babel将es6以上的语法转化为旧版语法
利用uglify/terser等对代码进行压缩优化体积
…
构建工具就是将以上功能集成到一起并自动执行的工具
一个构建工具实际上还会复杂其他多种工作,那我们将他的功能进行归类:
模块化支持:支持es module
或commonJS
等多种模块化的模块引入语法from ... import ..
或const ... = require(...)
处理代码兼容性:babel、less、ts语法转换(构建工具利用其他处理工具自动化的完成)
提高项目性能:压缩文件,代码分割
优化开发体验:
构建工具自动监听文件变化,重新打包并在浏览器重新运行(热更新HMR )
开发服务器:解决跨域问题
目前市面上的构建工具:
webpack
vite
parcel
esbuild
rollup
grunt
gulp
Vite相对于Webpack的优势
Vite为什么笔Webpack快那么都?
Webpack考虑到网页可以跑在服务端,也能跑在客户端,于是支持多种模块化方法混用,例如:
1 2 import Vue from "vue" const _ = require ("lodash" )
那么为了避免这种情况导致的编译错误,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规范的各个模块进行统一集成。
预构建解决了下面三个问题:
不同的第三方包有不同的导出格式
在路径的处理上可以直接使用.vite/deps
,方便路径重写
解决网络多包传输的性能问题(是导致原生esmodule规范不敢支持node_module的原因之一),无论多少额外的export和import,最终都会尽可能进行集成最后生成一个或者几个模块
在vite.config.js
中可以通过optimizeDeps
设置针对某些包是否进行依赖预构建
1 2 3 4 5 export default defineConfig ({ optimizeDeps : { exclude : ['lodash-es' ], } }
Vite运行在node端为什么可以是用esmodule的包管理
node端只支持commonJS规范,但是vite的配置文件vite.config.js
可以使用esmodule进行包的导入导出
这是因为vite在读取配置文件的时候会率先解析文件语法,遇到esmodule规范的语法时直接将其替换为commonJS
Vite配置文件处理细节
Vite配置文件语法提示
是用webStorm会有良好的语法提示
为了获得良好的语法提示,建议从vite中导入defineConfig,然后是用defineConfig对配置对象进行导出,defineConfig是一个函数,该函数的参数是用ts定义了interface,因此会有良好的代码提示:
import { defineConfig } from 'vite'
export default defineConfig ({
}
<!--code141 -->
关于环境的处理
使用webpack的时候想要区分环境,需要针对不同环境编写config,比如开发环境要在webpack.dev.config
中配置,生产环境要在webpack.prod.config
中配置
而vite中则可以是用defineConfig函数进行区分,然后根据不同的环境返回不同的config对象:
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'
const envResolver = {
"build" : () => {
console .log ("生产环境" );
return {...viteBaseConfig, ...viteProbConfig}
},
"serve" : () => {
console .log ("开发环境" );
return {...viteBaseConfig, ...viteDevConfig}
}
}
export default defineConfig (({command} ) => {
return envResolver[command]()
})
<!--code142 -->
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文件的处理
vite读到main.js中引用了index.css文件
是用fs模块读取index.css中的文件内容
直接创建一个style标签,将index.css中文件内容粘贴到<style>
标签中
将<style>
标签插入index.html
的head中
将css文件内容直接替换为js脚本(便于热更新,或模块化css),让浏览器以js的方式解析css文件
但如果我们进行协同开发,不同的组件是用了不同的css文件,且这两个文件中都有名字为.footer
的样式,此时后导入的组件的样式将覆盖先导入的组件的样式
此时可以是用cssmodule来解决这一问题
cssmodule即css模块化,是一个基于node的css处理模块:
他读取${style}.moduel.css
的文件module
是一种约定,表示开启模块化
将其中的所有类目进行替换,即在原有类名的末尾添加一串hash字符串
同时创建一个对象让旧类名作为key,新类名作为value
将替换完成的内容放入head标签中
将module.css
文件中的内容替换为js
将3中创建的对象进行默认导出
同时less、sass等都可以使用同样的方法进行模块化
Vite中配置css的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 css : { modules : { localsConvention : "camelCase" , scopeBehaviour : "global" , generateScopedName : "[name]_[local]_[hash:5]" , hashPrefix : "ender" , globalModulePaths : [], }, },
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 : { less : { math : "always" , global : { mainColor : "red" }, }, scss : { } }
SourceMap
我们的项目上线时被压缩或者被编译过,那么当程序出错后,报错的位置信息将不再正确
此时我们可以开启vite的devSourcemap
配置:
这样在启动项目之后,即使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 const postcssPresetEnv = require ("post-preset-env" )module .export = { plugins : [postPresetEnv ()] }
使用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-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 { 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" console .log ("name: " , name)
在导入资源时,我们还可以定义其导入的方式
1 2 3 4 5 6 7 8 9 import imgUrl from "./src/assets/images/a.png?url" console .log ("imgUrl: " , imgUrl)import imgRaw from "./src/assets/images/a.png?raw" 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, "./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 = svgRawconst 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 : { output : { assetFileNames : "[hash].[name].[ext]" } }, 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 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 ) { server.middlewares .use ((req, res, next ) => {}) }, transformIndexHtml (html ){ }, configResolved (options ) { console .log ("options: " , options) }, configurePreviewServer (server ){ }, handleHotUpdate ( ) { }, options (rollupOptions ) { }, buildStart (fullRollupOptions ) { }, } ] })
下面介绍一些常用插件
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 import fs from 'fs' import path from 'path' const diffDirAndFile = (dirFilesArray = [], basePath = '' ) => { const result = { dirs : [], files : [], } dirFilesArray.forEach ((name ) => { const currentFileStates = fs.statSync ( path.resolve (__dirname, `${basePath} /${name} ` ) ) const isDir = currentFileStates.isDirectory () if (isDir) result.dirs .push (name) else result.files .push (name) }) return result } const getAllFills = (prefix = '@' ) => { const result = fs.readdirSync (path.resolve (__dirname, '../src' )) const diffResult = diffDirAndFile (result, '../src' ) 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 { config (config, env ) { const resolveAliasesObj = getAllFills () console .log (resolveAliasesObj) return { resolve : { alias : resolveAliasesObj, }, } }, } }
Vite-plugin-html
赋予开发者动态控制整个html文件中内容的能力
可以通过ejs
语法向html中注入内容,它在服务端用的比较多,因为服务端经常会动态地修改index.html
而Vite实际上运行在服务端,因此选用该语法进行注入
1 2 3 4 5 6 7 createHtmlPlugin ({ inject : { data : { title : "主页" } } })
其原理是通过Vite的transformIndexHtml
钩子实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 module .exports = (options ) => { return { transformIndexHtml : { enforce : "pre" transform : (html, ctx ) => { 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数据来模拟从后端请求到的数据进行开发。一般有两种方法:
简单方式: 直接写死
优点
缺点
无法进行海量数据的测试
无法获得标准数据
无法感知http的异常
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 import mockJS from "mockjs" const userList = mockJS.mock ({ "data|100" : [{ name : "@cname" , ename : "@first @last" , "id|+1" : 1 , avatar : mockJS.Random .image (), time : "@time" }] }) export default [ { method : "post" , url : "/api/users" , response : ({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 ({ 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) { mockResult = require (path.resolve (process.cwd (), "mock/index.js" )) } server.middlewares .use ((req, res, next ) => { const mockMatch = mockResult.find (mockDescriptor => mockDescriptor.url === req.url ) if (req.url === "/api/users" ) { } if (mockMatch) { const responseData = mockDescriptor.response (req) 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 : [ 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" , "strict" : true , "jsx" : "preserve" , "resolveJsonModule" : true , "isolatedModules" : false , "esModuleInterop" : true , "lib" : [ "ESNext" , "DOM" ] , "skipLibCheck" : true , "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 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 const [timer, setTimer] = useState (null );useEffect (() => { setTimer (setTimeout (() => {})) return () => clearTimeout (timer); })
1 2 3 4 const arr = [1 , 2 , 3 ]for (let i = 0 , len = arr.length ; i < len; 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 : [ checker ({typescript : true }), ] build : { rollupConfig : { output : { manualChunks : (id : string ) => { 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 : [ checker ({typescript : true }), ] build : { rollupConfig : { input : { main : path.resolve (__dirname, "./index.html" ), product : path.resolve (__dirname, "./product.html" ) }, output : { manualChunks : (id : string ) => { 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 : [ checker ({typescript : true }), viteCompression () ] build : { rollupConfig : { input : { main : path.resolve (__dirname, "./index.html" ), product : path.resolve (__dirname, "./product.html" ) }, output : { manualChunks : (id : string ) => { 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 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 : [ { name : "lodash" , var : "_" , 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 ()app.get ('/user' ,(req,res )=> { res.json ({ code :200 , message :"请求成功" }) }) 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 export default defineConfig ({ plugins : [vue ()], server :{ proxy :{ '/api' :{ target :"http://localhost:9001/" , changeOrigin :true , rewrite :(path ) => path.replace (/^\/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 import httpProxy from 'http-proxy' export function proxyMiddleware ( httpServer : http.Server | null , options : NonNullable <CommonServerOptions ['proxy' ]>, config : ResolvedConfig ): Connect .NextHandleFunction { const proxy = httpProxy.createProxyServer (opts) as HttpProxy .Server const http = require ('http' )const httpProxy = require ('http-proxy' )const proxy = httpProxy.createProxyServer ({})http.createServer ((req,res )=> { proxy.web (req,res,{ target :"http://localhost:9001/xm" , changeOrigin :true , ws :true }) }).listen (8888 )
Vite总结
Vite相关配置
Pinia
Pinia是一款状态管理工具,是Vuex的替代品,相较于Vuex,Pinia.js具有以下特性:
完整的ts支持
轻量化,压缩后体积只有1kb
去除mutations,只有state、getters、actions
actions支持同步和异步
代码扁平化没有模块嵌套,只有store的概念,store之间可以自由使用,每一个store都是独立的
无需手动添加store,store一旦创建便会自动添加
支持Vue3和Vue2
安装
引入
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' , pinia, })
修改state的方法
直接修改
在使用const store = useMyStore()
方法得到store可以直接访问store中的state并对其中的数据进行修改:
store.val = 2
使用patch批量修改
使用patch函数参数形式
store.$patch((state) => { val: 2 })
这种方法可以在函数中加一些业务逻辑
直接重新构造state
store.$state = {val:2}
这种方法必需修改state中的所有值
使用actions中定义的方法修改值
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进行修改都会同时反应到val
和store.val
getter和actions
getter中定义一些函数,类似于计算属性,可以修饰一些值:
actions中定义一些函数用于对state中的值进行操作,同时支持异步和同步,并且可以相互调用。
Pinia API
$reset()
不接受任何参数,没有返回值
调用后将state设置为初始值store.$reset()
$subscribe(Function, object | null)
接收一个工厂函数,函数有两个参数arg,state
,分别表示影响(包含新值、旧值等等)和新的state
当state中的值发生变化就会自动调用传入的工厂函数
$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 ) => { 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 )); }) return { ...data } } } const pinia = createPinia () pinia.use (piniaPlugin ({ key : "pinia" }))