vue3语法应用
vue3父子传值
父传子数据,父组件通过v-bind绑定一个数据,然后子组件通过difineProps接受传过来的值,
如以下代码
<template>
<div>
<Menu :data="list" title="我想穿个裤子"></Menu>
</div>
</template>
<script lang="ts" setup>
import Menu from './Menu/index.vue'
import {reactive} from 'vue'
const list = reactive<string[]>(['父组件数据'])
</script>
子组件接受值
<template>
<div class="menu">
{{ data }}
</div>
</template>
<script lang="ts" setup>
// 第一种
defineProps<{
title: string,
data: string[]
}>()
// 第二种
type obj = {
title?: string,
data?: string[]
}
defineProps<obj>()
// 第三种携带默认值
withDefaults(defineProps<obj>(), {
title: '我是默认值',
data:() => ['默认值']
})
</script>
子组件传递给父组件数据
<template>
<div class="menu">
<button @click="clickTap">转发派给父组件</button>
</div>
</template>
<script lang="ts" setup>
import {reactive} from 'vue'
const list = reactive<string[]>(['子组件传递数据'])
const emit = defineEmits(['on-click'])
const clickTap = () => {
emit('on-click', list)
}
</script>
父组件接受子组件的事件
<template>
<div>
<Menu @on-click="getList"></Menu>
</div>
</template>
<script lang="ts" setup>
import Menu from './Menu/index.vue'
import {reactive} from 'vue'
// reactive第一种接值
type obj = {
list: string[]
}
const arrlist = reactive<obj>({
list: []
})
const getList = (list: string[]) => {
arrlist.list = list
console.log(list, '父组件接受子组件')
}
// 第二种接值
const arr = reactive<string[]>([])
const getList = (list: string[]) => {
arr.push(...list)
console.log(list, '父组件接受子组件')
}
</script>
子组件暴露给父组件内部属性通过defineExpose,我们从父组件获取子组件实例通过ref
// 父组件
<template>
<div class="layout">
<Menu
ref="menus"
></Menu>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
const menus = ref<any>(null)
console.log(menus.value.ceshi)
</script>
// 子组件
<template>
<div class="menu">
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
const ceshi = ref<string>('给父组件')
defineExpose({
ceshi
})
</script>
vue3(全局组件,局部组件,递归组件)
全局组件在main.ts引入我们的组件跟随在createApp(App)后面 切记不能放到mount后面这是一个链式调用其次调用component第一个参数组件名称 第二个参数组件实例
import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset.less'
import card from './components/card.vue'
const app = createApp(App)
app.component('card', card)
app.mount('#app')
局部组件
就是在一个组件内(A)通过import去引入别的组件(B)称之为局部组件
应为B组件只能在A组件内使用 所以是局部组件
如果C组件想用B组件就需要C组件也手动import引入B组件
递归组件
原理跟我们写js递归是一样的 自己调用自己 通过一个条件来结束递归 否则导致内存泄漏
父组件传值
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'
}
])
子组件接受递归值
<template>
<div>
<div :key="index" v-for="(item, index) in data">
<div @click="itemClick(item)">{{ item.name }}</div>
<tree-list v-if="item?.children?.length" :data="item.children"></tree-list>
</div>
</div>
</template>
<script lang="ts" setup>
type TreeList = {
name: string,
icon?: string,
children?: TreeList[] | []
}
type Props<T> = {
data?: T[] | []
}
defineProps<Props<TreeList>>()
const itemClick = (item: TreeList) => {
console.log(item)
}
</script>
vue3动态组件
动态组件就是:让多个组件使用同一个挂载点,并动态切换组件。
再挂载点使用component标签,然后使用v-bind:is="组件名"
markRaw用来跳过proxy,转为普通对象。
Prick用来抽取定义的数据类型中的某一个。
<template>
<button :key="index" v-for="(item, index) in data" @click="itemClick(item)">{{ item.name }}</button>
<component :is="isName.couname"></component>
</template>
<script lang="ts" setup>
import A from "./A.vue"
import B from "./B.vue"
import C from "./C.vue"
import { reactive, markRaw } from 'vue'
type obj = {
name: string,
couname: any
}
type cou = Prick<obj, 'couname'>
const data = reactive<obj[]>([
{
name: 'A组件',
couname: markRaw(A)
},
{
name: 'B组件',
couname: markRaw(B)
},
{
name: 'C组件',
couname: markRaw(C)
}
])
const isName = reactive<cou>(
{
couname: data[0].couname
}
)
const itemClick = (item: obj) => {
isName.couname = item.couname
}
</script>
vue3插槽使用
子组件定义插槽
<template>
<div class="red">
我是匿名插槽
<slot></slot>
</div>
<div class="green">
我是具名插槽
<slot name="content"></slot>
</div>
<div class="blue">
我是作用域名插槽
<div :key="index" v-for="(item, index) in list">
<slot name="floter" :data="item"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
type obj = {
name: string,
age: number
}
const list = reactive<obj[]>([
{
name: '作用域查查',
age: 18
},
{
name: '作用域查查11',
age: 20
},
{
name: '作用域查查22',
age: 30
}
])
</script>
父组件接收
<template>
<div class="center">
内容区域
<dialog-slot>
<template v-slot>
<div>默认插槽111222</div>
</template>
<template #content>
<div>具名插槽了解一下</div>
</template>
<template #floter="{data}">
<div>
作用域插槽{{ data.name }} --- {{ data.age }}
</div>
</template>
<template #[name]>
<div>
自定义11222
</div>
</template>
</dialog-slot>
</div>
</template>
<script lang="ts" setup>
import dialogSlot from './dialogSlot.vue'
import { ref } from 'vue'
const name = ref('content')
</script>
vue3异步组件&suspense
异步组件在大型应用中,我们可能需要将应用分割成小一些的代码块 并且减少主包的体积这时候就可以使用异步组件。
顶层 await在setup语法糖里面 使用方法<script setup>中可以使用顶层await。结果代码会被编译成async setup()
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
父组件用子组件通过defineAsyncComponent加载异步配合import函数模式可以分包
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const Dialog = defineAsyncComponent(() => import('../../components/Dialog/index.vue'))
</script>
suspense组件有两个插槽。它们都只接受一个直接子节点。default插槽里的节点会尽可能展示出来。如果不能,则展示fallback插槽里的节点。
<Suspense>
<template #default>
<div>
成功返回展示
</div>
</template>
<template #fallback>
<div>
失败返回展示
</div>
</template>
<Suspense>
vue3(Teleport传送组件)
teleport vue3新特性之一。
teleport是一种能够将我们的模板渲染至指定DOM节点,不受父级style,v-show等属性影响,但data,prop数据依旧能够共用的技术;类似于react的portal。
主要解决的问题因为teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响
使用方法
通过to属性 插入指定元素位置to="body"便可以将teleport内容传送到指定位置
<Teleport to="body">
<div>插入body</div>
</Teleport>
也可以自定义传送位置支持class id等选择器 也能插入多个Teleport
<div id="app"></div>
<div class="modal"></div>
<Teleport to=".modal">
<div>插入classmodal</div>
</Teleport>
vue3keep-alive
有时候不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。
开启keep-alive生命周期的变化
初次进入时:onMounted>onActivated
推出后出发:deactivated
再次进入只会触发onActivated。事件挂载的方法等,只执行一次的放在onMounted中;组件每次进行执行的方法放在onActivated中
// 基本
<keep-alive>
<component :is="view"></component>
</keep-alive>
// 多个条件判断子组件
<keep-alive>
<login v-if="a > 1"></login>
<regin v-else></regin>
</keep-alive>
// 和transition一起使用
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
include和exclude,max
include可以传入单值和数组他是记录要缓存的组件。
exclude可以传入单值和数组他是记录不需要缓存的组件。
max设置最大缓存组件数量
<keep-alive :include="" :exclude="" :max=""></keep-alive>
vue3transition动画组件
vue提供了transition的封装组件,在下列情形中可以给任何元素和组件添加进入/离开过度;
条件渲染(v-if) ,条件展示(v-show),动态组件,组件根节点
自定义transition过度效果,需要在transition组件的name属性自定义。并在css中写入对应的样式
1,过度的类名
在进入/离开的过度中,会有6个class切换。
1, v-enter-from 过度开始
2, v-enter-active 执行过度状态
3, v-enter-to 过度结束
4, v-leave-from 离开过度
5, v-leave-active 离开的过度状态
6, v-leave-to 离开过度结束
<button @click="flag = !flage">切换</button>
<transition name="fade">
<div v-if="flag" class="box"></div>
</transition>
// 开始过度
.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;
}
2.自定义过度class类名
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
自定义过度时间 单位毫秒 也可以分别指定进入和离开的持续时间
<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
通过自定义class结合css动画库animate.css 安装npm install animate.css 引入import ‘animate.css’
<transition
leave-active-class="animate__animated animate__bounceInLeft"
enter-active-class="animate__animated animate__bounceInRight">
<div v-if="flag" class="box"></div>
</transition>
3.transition生命周期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"//离开过度打断
只用js过度的时候,在enter和leave钩子中必须使用done进行回调
const beforeEnter = (el: Element) => {
console.log('进入之前from', el);
}
const Enter = (el: Element,done:Function) => {
console.log('过度曲线');
setTimeout(()=>{
done()
},3000)
}
const AfterEnter = (el: Element) => {
console.log('to');
}
4.appear
通过这个属性可以设置初始节点过度,就是页面加载完成开始动画对应三个状态
appear
appear-from-class=""
appear-to-class=""
appear-active-class=""
vue3transition-group过度列表
transition-group特点
1,默认情况下,它不会渲染一个包裹元素,但是你可以通过tag attribute指定渲染一个元素。
2,过度模式不可用,因为我们不在相互切换特有的元素。
3,内部元素总是需要提供唯一的key attribute值。
4,css过度的类将会应用在内部的元素中,而不是这个组/容器本身。
<transition-group>
<div :key="item" v-for="item in list">{{ item }}</div>
</transition-group>
const list = reactive<number[]>([1,2,3,4,5])
const push = () => {
list.push(1)
}
const pop = () => {
list.pop()
}
2.列表的移动过渡
<transition-group>组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的v-move类就可以使用这个新功能,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过name attribule来定义,也可以通过move-class手动设置
<template>
<div>
<button @click="shuffle">Shuffle</button>
<transition-group class="wraps" name="mmm" tag="ul">
<li class="cell" v-for="item in items" :key="item.id">{{ item.number }}</li>
</transition-group>
</div>
</template>
<script setup lang='ts'>
import _ from 'lodash'
import { ref } from 'vue'
let items = ref(Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1
}
}))
const shuffle = () => {
items.value = _.shuffle(items.value)
}
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(25px * 10 + 9px);
.cell {
width: 25px;
height: 25px;
border: 1px solid #ccc;
list-style-type: none;
display: flex;
justify-content: center;
align-items: center;
}
}
.mmm-move {
transition: transform 0.8s ease;
}
</style>
3,状态过度
vue也同样可以给数字svg背景颜色等添加过度动画
<template>
<div>
<input step="20" v-model="num.current" type="number" />
<div>{{ num.tweenedNumber.toFixed(0) }}</div>
</div>
</template>
<script setup lang='ts'>
import { reactive, watch } from 'vue'
import gsap from 'gsap'
const num = reactive({
tweenedNumber: 0,
current:0
})
watch(()=>num.current, (newVal) => {
gsap.to(num, {
duration: 1,
tweenedNumber: newVal
})
})
</script>
<style>
</style>
vue3 provide/inject
当父组件有很多数据需要分发给其子代组件的时候,就可以使用provide和inject。
如果传递普通的值 是不具有响应式的 需要通过ref reactive 添加响应式
例子 父组件传递数据
<template>
<div>
<A>我是App</A>
</div>
</template>
<script lang="ts" setup>
import { provide, ref } from 'vue'
import A from './A.vue'
let flag = ref<number>(1)
provide('flag', flag)
</script>
子组件接收
<template>
<div>
<button @click="change">change flag</button>
<div>{{ flag }}</div>
</div>
</template>
<script lang="ts" setup>
import { inject, ref, Ref } from 'vue'
const flag = inject<Ref<number>>('flag', ref(1))
const change = () => {
flag.value = 2
}
</script>
vue3兄弟组件传参和bus
两种方案
1.借助父组件传参例如父组件为App子组件为A和B他两个是同级的
<template>
<div>
<A @on-click="getFalg"></A>
<B :flag="Flag"></B>
</div>
</template>
<script setup lang="ts">
import A from './A.vue'
import B from './B.vue'
import { ref } from 'vue'
let Flag = ref<boolean>(false)
const getFalg = (flag: boolean) => {
flag.value = flag
}
</script>
A组件派发事件通过App.vue接受A组件派发的事件然后在props传给B组件 也是可以实现的。缺点就是比较麻烦,无法直接通信,只能充当桥梁。
2.event bus
通过emit和on传递和接受事件参数,简易版例子
type BusClass<T> = {
emit: (name: T) => void
on: (name: T, callback: Function) => void
}
type BusParams = string | number | symbol
type List = {
[key: BusParams]: Array<Function>
}
class Bus<T extends BusParams> implements BusClass<T> {
list: List
constructor() {
this.list = {}
}
emit(name: T, ...args: Array<any>) {
let eventName: Array<Function> = this.list[name]
eventName.forEach(ev => {
ev.apply(this, args)
})
}
on(name: T, callback: Function) {
let fn: Array<Function> = this.list[name] || []
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus<number>()
3,mitt兄弟传参
mitt安装
npm install mitt -S
main.ts初始化
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt'
const Mit = mitt()
//TypeScript注册
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mit
}
}
const app = createApp(App)
//Vue3挂载全局API
app.config.globalProperties.$Bus = Mit
app.mount('#app')
使用方法通过emit派发,on方法添加事件,off方法移除,clear清空所有
A组件派发emit
<template>
<div>
<h1>我是A</h1>
<button @click="emit1">emit1</button>
<button @click="emit2">emit2</button>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance();
const emit1 = () => {
instance?.proxy?.$Bus.emit('on-num', 100)
}
const emit2 = () => {
instance?.proxy?.$Bus.emit('*****', 500)
}
</script>
<style>
</style>
B组件监听on
<template>
<div>
<h1>我是B</h1>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
instance?.proxy?.$Bus.on('on-num', (num) => {
console.log(num,'===========>B')
})
</script>
<style>
</style>
监听所有事件(on("*"))
instance?.proxy?.$Bus.on('*',(type,num)=>{
console.log(type,num,'===========>B')
})
移除监听事件(off)
const Fn = (num: any) => {
console.log(num, '===========>B')
}
instance?.proxy?.$Bus.on('on-num',Fn)//listen
instance?.proxy?.$Bus.off('on-num',Fn)//unListen
清空所有监听(clear)
instance?.proxy?.$Bus.all.clear()
vue3 TSX
我们之前是使用template去写我们模板。现在可以扩展另一种风格TSX风格,vue2的时候就已经支持jsx写法,只不过不是很友好,随着vue3对typescript的支持度,tsx写法越来越被接受
1,安装插件
npm install @vitejs/plugin-vue-jsx -D
vite.config.ts 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx()
]
})
2.修改tsconfig.json配置文件
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
3,使用tsx
在目录新建一个name.tsx文件 esx不会自动解包使用ref必须加value。常用的功能展示
v-model,v-show,v-if不支持需要改变风格三目运算,v-for转换为map, v-bind,v-on,props,emit
/*
* @Author: tly
* @Date: 2022-06-25 16:42:59
* @LastEditTime: 2022-06-25 16:55:16
* @FilePath: \vite-app\src\app.tsx
*/
import { ref } from 'vue'
let v = ref<string>('')
let flag = ref<boolean>(false)
let arr = [1,2,3,4,5]
type Props = {
title: string
}
const renderDom = (props: Props, content: any) => {
return (
<>
<div>{props.title}</div>
<input v-model={v.value} type="text" />
<div>{v.value}</div>
<div v-show={flag.value}>景天</div>
<div v-show={!flag.value}>雪见</div>
{
flag.value ? <div>雪见</div> : <div>景天</div>
}
{
arr.map(v => {
return <div onClick={clickTap.bind(this, content)} data-index={v}>${v}</div>
})
}
</>
)
}
const clickTap = (v:any) => {
console.log('click', v)
v.emit('on-click', 1)
}
export default renderDom
vue3自动引入插件
npm i -D unplugin-auto-import
vite.config.ts配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from 'path'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'~': resolve(__dirname, 'src')
}
},
plugins: [
vue(),
AutoImport({
imports:['vue'],
dts:"src/auto-import.d.ts"
})
]
})
配置完成后会自动在src目录下生成auto-import.d.ts文件,使用ref reactive watch 等无须import 导入 可以直接使用。具体配置可以点击链接查看详情,node版本必须是12版本以上,目前我使用的是14.19.3
GitHub - antfu/unplugin-auto-import: Auto import APIs on-demand for Vite, Webpack and Rollup
vue3v-model
1.默认值的改变
prop:value -> modelValue;
时间:input -> update:modelValue
v-bind的.sync修饰符和组件的model选项已移除
新增支持多个v-model
新增支持自定义修饰符
案例子组件
<template>
<div class="aaa" v-if="propData.modelValue">
<div>
<div>
标题--{{ propData.modelValue }} -- {{ propData.title }}
</div>
<div @click="clonse">×</div>
</div>
<div>内容--</div>
</div>
</template>
<script setup lang="ts">
type props = {
modelValue: boolean,
title: string,
modelModifiers?: {
xiaoyuan: boolean
},
titleModifiers?: {
xiao: boolean
}
}
const propData = defineProps<props>()
const emit = defineEmits(['update:modelValue', 'update:title'])
const clonse = () => {
console.log(propData.titleModifiers)
if (propData.titleModifiers?.xiao) {
emit('update:title', '我是一只狗狗!')
} else {
emit('update:title', '我是一只猫猫!!')
}
emit('update:modelValue', false)
}
</script>
<style lang="less" scoped>
.aaa{
margin-top: 20px;
width: 300px;
height: 300px;
border: 1px solid black;
>div:nth-of-type(1) {
height: 30px;
padding: 0 10px;
border-bottom: 1px solid black;
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>
案例父组件
<template>
<div class="center">
<button @click="changeFlag">chang {{ flag }}</button>
<div>{{ title }}</div>
<aa v-model="flag" v-model:title.xiao="title"></aa>
</div>
</template>
<script lang="ts" setup>
import aa from './AA.vue'
import { ref } from 'vue'
const flag = ref<boolean>(true)
const title = ref<string>('我是一只猫')
const changeFlag = () => {
flag.value = !flag.value
}
</script>
添加到组件v-model的修饰符将通过modelModifiers prop 提供给组件。在示例中,我们创建了一个组件,其中包含默认为空对象的modelModifiers prop
vue3自定义指令directive
1.vue3指令的钩子函数
created 元素初始化的时候
beforeMount 指令绑定到元素后调用 只调用一次
mounted 元素插入父级dom调用
beforeUpdate 元素被更新之前调用
updated 元素被更新之后调用
beforeUnmount 在元素被移除前调用
unmounted 在元素被移除后调用 只调用一次
vue2 指令 bind insered update componentUpdated unbind
这里需要注意的限制:必须以vNameDirective的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
<template>
<div class="center">
<button @click="flag = !flag">change</button>
<AA v-if="flag" v-move-directive:aa.xiao="{background: 'green'}"></AA>
</div>
</template>
<script lang="ts" setup>
import AA from './AA.vue'
import { Directive, DirectiveBinding } from 'vue'
const flag = ref<boolean>(true)
type value = {
background: string
}
const vMoveDirective: Directive = {
created() {
console.log('元素初始化---created')
},
beforeMount(...args: Array<any>) {
console.log('指令绑定到元素后调用 只调用一次-----beforeMount')
console.log(args)
},
mounted(el: HTMLElement, dir: DirectiveBinding<value>) {
console.log('元素插入父级dom调用----mounted')
console.log(el, dir)
el.style.background = dir.value.background
},
beforeUpdate() {
console.log('元素被更新之前调用-----beforeUpdate')
},
updated() {
console.log('元素被更新后调用----updated')
},
beforeUnmount() {
console.log('元素被移除前调用----beforeUnmount')
},
unmounted() {
console.log('元素被移除后调用----unmounted')
}
}
</script>
<style lang="less" scoped>
</style>
生命周期钩子参数详解
第一个el当前绑定的DOM元素
第二个binding
- instance:使用指令的组件实例。
- value:传递给指定的值。例如在v-name-directive="1+1"中,该值为2
- oldValue:先前的值,尽在beforeUpdate和updated中可用。无论值是否有更改都可用
- arg:传递给指令的参数(如果有的话)。例如在v-name-directive:foo中。arg为foo。
- modifiers:包含修饰符(如果有的话)的对象。例如在v-name-directive.foo.bar中,修饰符对象为{foo:true,bar:true}。
- dir:一个对象,在注册指令时作为参数传递
第三个 当前元素的虚拟DOM也就是Vnode
第四个prevNode 上一个虚拟节点,尽在beforeUpdate和updated钩子中 可用
2.函数简写
你可能想在mounted和updated时出发相同行为,而不关心其他的钩子函数,那么你可以通过将这个函数模式实现
<template>
<div class="center">
<input type="text" v-model="back">
<AA v-move-directive:aa.xiao="{background: back}"></AA>
</div>
</template>
<script lang="ts" setup>
import AA from './AA.vue'
import { Directive, DirectiveBinding } from 'vue'
const back = ref<string>('')
type value = {
background: string
}
const vMoveDirective: Directive = (el: HTMLElement, binding: DirectiveBinding<value>) => {
el.style.background = binding.value.background
}
</script>
案例自定义拖拽指令
<template>
<div v-move class="box">
<div class="header"></div>
<div>
内容
</div>
</div>
</template>
<script setup lang='ts'>
import { Directive } from "vue";
const vMove: Directive = {
mounted(el: HTMLElement) {
let moveEl = el.firstElementChild as HTMLElement;
const mouseDown = (e: MouseEvent) => {
//鼠标点击物体那一刻相对于物体左侧边框的距离=点击时的位置相对于浏览器最左边的距离-物体左边框相对于浏览器最左边的距离
console.log(e.clientX, e.clientY, "-----起始", el.offsetLeft);
let X = e.clientX - el.offsetLeft;
let Y = e.clientY - el.offsetTop;
const move = (e: MouseEvent) => {
el.style.left = e.clientX - X + "px";
el.style.top = e.clientY - Y + "px";
console.log(e.clientX, e.clientY, "---改变");
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", move);
});
};
moveEl.addEventListener("mousedown", mouseDown);
},
};
</script>
<style lang='less'>
.box {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 1px solid #ccc;
.header {
height: 20px;
background: black;
cursor: move;
}
}
</style>
vue3自定义hooks
vue3自定义hook主要用来处理复用代码逻辑的一些封装,这个在vue2就已经有一个东西是Mixins,mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现一次写代码,多组件受益的效果。弊端就是会涉及到覆盖的问题。
组件的data,methods,filters会覆盖mixins里的同名data,methods,filters。
第二点就是 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
vue3的自定义的hook
- vue3 的 hook函数相当于vue2 的 mixin,不用在与hooks是函数
- vue3 的 hook函数可以帮助我们提高代码的复用性,让我们能在不同的组件中都利用hooks函数
vue3 hook 库 Get Started | VueUse
自定义hook转base64案例
import { onMounted } from 'vue'
type Options = {
el: string
}
type Return = {
BaseUrl: string | null
}
export default function (option: Options): Promise<Return> {
return new Promise((resolve) => {
onMounted(() => {
console.log(option)
const file:HTMLImageElement = document.querySelector(option.el) as HTMLImageElement
console.log(file)
file.onload = ():void => {
resolve({
BaseUrl: toBase64(file)
})
}
})
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)
console.log(el.width)
return canvas.toDataURL('image/png')
}
})
}
vue3定义全局函数和变量
由于vue3没有Prototype属性 使用 app.config.globalProperties 代替 然后去定义变量和函数
vue2
// 之前 (vue 2.x)
Vue.prototype.$http = () => {}
vue3
// 之后 (vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}
在vue3移除了过滤器 我们正好可以使用全局函数代替Filters
案例
app.config.globalProperties.$filters ={
format<T extends any>(str: T): string {
return `$${str}`
}
}
生命文件不然ts无法正确类型推导
type Filter = {
format: <T extends any>(str: T) => T
}
// 声明要扩充@vue/runtime-core包的声明
// 这里扩充’ComponentCustomProperties‘接口,因为他是vue3中实例的属性的类型
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$filter: Filter
}
}
在setup读取全局函数的值
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
const { appContext } = <ComponentInternalInstance>getCurrentInstance()
console.log(appContext.config.globalProperties.$env);
vue3编写插件
插件是自包含的代码,通常向vue添加全局级功能。你如果是一个对象需要有install方法vue会帮你自动注入到install方法 你如果是function就直接当install方法去使用。
在使用插件时,在createApp()初始化vue应用程序后,你可以通过调用use()方法将插件添加到你的应用程序中。
实现一个Loading
loading.vue
<template>
<div v-if="isShow" class="loading">
<div class="loading-content">Loading...</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const isShow = ref(false)//定位loading 的开关
const show = () => {
isShow.value = true
}
const hide = () => {
isShow.value = false
}
//对外暴露 当前组件的属性和方法
defineExpose({
isShow,
show,
hide
})
</script>
<style scoped lang="less">
.loading {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
&-content {
font-size: 30px;
color: #fff;
}
}
</style>
loading.ts
import { createVNode, render, VNode, App } from 'vue';
import Loading from './index.vue'
export default {
install(app: App) {
//createVNode vue提供的底层方法 可以给我们组件创建一个虚拟DOM 也就是Vnode
const vnode: VNode = createVNode(Loading)
//render 把我们的Vnode 生成真实DOM 并且挂载到指定节点
render(vnode, document.body)
// Vue 提供的全局配置 可以自定义
app.config.globalProperties.$loading = {
show: () => vnode.component?.exposed?.show(),
hide: () => vnode.component?.exposed?.hide()
}
}
}
Main.ts
import Loading from './components/loading'
let app = createApp(App)
app.use(Loading)
type Lod = {
show: () => void,
hide: () => void
}
//编写ts loading 声明文件放置报错 和 智能提示
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$loading: Lod
}
}
app.mount('#app')
代码中使用
<script lang="ts" setup>
import {ComponentInternalInstance, getCurrentInstance} from 'vue'
const { appContext } = getCurrentInstance() as ComponentInternalInstance
const changClick = () => {
// 所有的自定义方法都会在appContext内置属性里
console.log(appContext)
appContext.config.globalProperties.$loading.show()
setTimeout(() => {
appContext.config.globalProperties.$loading.hide()
}, 3000)
}
</script>
vue3了解ui库elementUI,AntDesigin
1,element-plus安装方法
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
main.ts引入
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
volar插件支持
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
2,ant design vue安装
$ npm install ant-design-vue@next --save
$ yarn add ant-design-vue@next
main.ts引入
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import App from './App';
import 'ant-design-vue/dist/antd.css';
const app = createApp(App);
app.use(Antd).mount('#app');
Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js
vue3样式穿透scoped
主要用于修改很多vue常用的组件库(element,vant,antDesigin),虽然配好了样式但是还是需要更改其他的样式需要用到样式穿透。
scoped的原理
vue中的scoped通过在DOM结构以及css样式上加唯一不重复的标记data-v-hash的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。
scoped三条渲染规则:
- 给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
- 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如data-v-123)来私有化样式
- 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
PostCSS会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了'样式模块化'的效果. vue3使用穿透属性 :deep(.name)
vue3 css style完整新特性
1,插槽选择器
A组件定义一个插槽
<template>
<div>
我是插槽
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
</style>
在app.vue引入
<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>
在A组件修改class a的颜色需要使用(:slotted(.a))
<style scoped>
:slotted(.a) {
color:red
}
</style>
2,全局选择器
我们想加入全局样式通常都是新建一个style标签 不加scoped现在有更优雅的解决方案
<style>
div{
color:red
}
</style>
<style lang="less" scoped>
</style>
<style lang="less" scoped>
:global(div){
color: red;
}
</style>
3,动态css
单文件组件的<style>标签可以通过v-bind这一css函数将css的值关联到动态的组件状态上
<template>
<div class="div">
动态添加
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const red = ref<string>('red')
const blue = ref({
color: 'blue'
})
</script>
<style lang="less" scoped>
.div{
color:v-bind(red)
}
// 如果是对象需要添加引号
.div{
color:v-bind('blue.color')
}
</style>
4,css module
<style module>
标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style
对象的键暴露给组件
<template>
<div :class="$style.red">
module CSS
</div>
</template>
<style module>
.red {
color: red;
}
</style>
自定义注入名称(多个可以用数组)
你可以通过给module attribute 一个值来自定义注入的类对象的property键
<template>
<div :class="[zs.red,zs.border]">
module Name
</div>
</template>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>
与组合式API一同使用
注入的类可以通过 useCssModule API 在 setup()
和 <script setup>
中使用。对于使用了自定义注入名称的 <style module>
模块,useCssModule
接收一个对应的 module
attribute 值作为第一个参数。使用场景一般用于 TSX 和 render 函数居多
<template>
<div :class="[zs.red,zs.border]">
module useCssModule
</div>
</template>
<script setup lang="ts">
import { useCssModule } from 'vue'
const css = useCssModule('zs')
</script>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>
vue3集成Tailwind CSS
Tailwind CSS 是一个由js编写的CSS 框架 他是基于postCss 去解析的
官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。
对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:
- PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
- TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
PostCSS - 是一个用 JavaScript 工具和插件来转换 CSS 代码的工具 | PostCSS 中文网
postCss 功能介绍
1.增强代码的可读性 (利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。)
2.将未来的 CSS 特性带到今天!(PostCSS Preset Env 帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills,此功能基于 cssdb 实现。)
3.终结全局 CSS(CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。)
. 4.避免 CSS 代码中的错误(通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS 。)
postCss 处理 tailWind Css 大致流程
- 将CSS解析成抽象语法树(AST树)
- 读取插件配置,根据配置文件,生成新的抽象语法树
- 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
- 清除一系列操作留下的数据痕迹
- 将处理完毕的AST树重新转换成字符串
安装
1,初始化项目
npm init vue@latest
2,安装 tailwind 以及其他依赖
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
3,生成配置文件
npx tailwindcss init -p
4,修改配置文件 tailwind.config.js
2.6版本
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: [],
}
5,创建一个index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
最后npm run dev就可以使用啦
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div class="md:flex">
<div class="md:flex-shrink-0">
<img class="h-48 w-full object-cover md:w-48" src="http://n.sinaimg.cn/translate/20170815/OoVn-fyixtym5144510.jpg" alt="Man looking at item at a store">
</div>
<div class="p-8">
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">Case study</div>
<a href="#" class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">Finding customers
for your new business</a>
<p class="mt-2 text-gray-500">Getting a new business off the ground is a lot of hard work. Here are five ideas
you can use to find your first customers.</p>
</div>
</div>
</div>
vue3(Event Loop 和 nextTick)
js执行机制
在我们学js的时候都知道js是单线程的如果是多线程的话会引发一个问题在同一时间同时操作DOM一个增加一个删除js就不知道到底要干嘛了,所以这个语言是单线程的但是随着HTML5到来js也支持了多线程webWorker 但是也是不允许操作DOM
单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以js中就出现了异步的概念。
同步任务
代码从上到下按顺序执行
异步任务
1,宏任务
script(整体代码),setTimeout,setInterval,UI交互事件,postmessage,ajax
2,微任务
Promise.then.catch.finally,MutaionObserver,process.nextTick(node.js环境)
运行机制
所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个任务队列,异步任务执行队列中先执行宏任务,然后清空当次宏任务中所有的微任务,然后进行下一个tick如此形成循环。
nextTick就是创意一个异步任务,那么它自然要等到同步任务执行完成之后才执行
<template>
<div ref="xiaohuo">
{{ text }}
</div>
<button @click="change">change div</button>
</template>
<script setup lang='ts'>
import { ref,nextTick } from 'vue';
const text = ref('小伙子开飞机')
const xiaohuo= ref<HTMLElement>()
const change = async () => {
text.value = '小伙子不开飞机'
console.log(xiaohuo.value?.innerText) //小伙子开飞机
await nextTick();
console.log(xiaohuo.value?.innerText) //小伙子不开飞机
}
</script>
<style scoped>
</style>
vue3开发适配移动端
开发移动端主要就是适配各种手机,在之前我们用的是rem 根据html font-size 去做缩放 现在有了更好用的vw vh。
vw 视口的最大宽度,1vw等于视口宽度的百分之一
vh 视口的最大高度, 1vh等于视口高度的百分之一
1,安装依赖
npm install postcss-px-to-viewport -D
因为vite中内联了postcss,所以并不需要额外的创建 postcss.config.js文件
vite.config.ts
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import postcsspxtoviewport from 'postcss-px-to-viewport'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
css: {
postcss: {
plugins: [
postcsspxtoviewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 320, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
landscape: false // 是否处理横屏情况
})
]
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
如果用的vite是ts他这个插件并没有提供声明文件,所以需要自己创建一个文件postcss-px-to-viewport.d.ts和vite.config.ts平级
declare module 'postcss-px-to-viewport' {
type Options = {
unitToConvert: 'px' | 'rem' | 'cm' | 'em',
viewportWidth: number,
viewportHeight: number, // not now used; TODO: need for different units and math for different properties
unitPrecision: number,
viewportUnit: string,
fontViewportUnit: string, // vmin is more suitable.
selectorBlackList: string[],
propList: string[],
minPixelValue: number,
mediaQuery: boolean,
replace: boolean,
landscape: boolean,
landscapeUnit: string,
landscapeWidth: number
}
export default (options: Partial<Options>) => any
}
在tsconfig.json中引入声明文件postcss-px-to-viewport.d.ts
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "postcss-px-to-viewport.d.ts"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
vue3(unocss)原子化
重新构想原子化CSS - 知乎
什么是css原子化
1,减少了css体积,提高了css复用
2,减少起名的复杂度
3,增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头bg
接入unocss
tips:最好用于vite webpack属于阉割版功能很少
安装
npm i -D unocss
vite.config.ts
import unocss from 'unocss/vite'
plugins: [vue(), vueJsx(),unocss({
rules:[
]
})],
main.ts 引入
import 'uno.css'
配置静态css
rules: [
['flex', { display: "flex" }]
]
配置动态css(使用正则表达式)
m-参数*10 例如 m-10 就是margin: 100px;
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }]
]
shortcuts 可以自定义组合样式
plugins: [vue(), vueJsx(), unocss({
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }],
['pink', { color: 'pink' }]
],
shortcuts: {
btn: "pink flex"
}
})],
unocss 预设
import { presetIcons, presetAttributify, presetUno } from 'unocss'
presets: [presetIcons(),presetAttributify(),presetUno()],
1,presetIcons Icon图标预设
图标集合安装
npm i -D @iconify-json/ic
首先我们去icones官网(方便浏览和使用iconify)浏览我们需要的icon,比如这里我用到了Google Material Icons图标里面的baseline-add-circle图标
<div class="i-ic-baseline-backspace text-3xl bg-green-500" />
2.presetAttributify 属性化模式支持
属性语义化 无须class
<div font="black">
btn
</div>
3.presetUno工具类预设
默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。
例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效。
vue3环境变量
环境变量:他的主要作用就是让开发者区分不同的运行环境,来实现 兼容开发和生产
例如 npm run dev 就是开发环境 npm run build 就是生产环境等等等
vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量
{
"BASE_URL":"/", //部署时的URL前缀
"MODE":"development", //运行模式
"DEV":true," //是否在dev环境
PROD":false, //是否是build 环境
"SSR":false //是否是SSR 服务端渲染模式
}
需要注意的一点就是这个环境变量不能使用动态赋值import.meta.env[key] 因为这些环境变量在打包的时候是会被硬编码的通过JSON.stringify注入浏览器的
配置额外的环境变量
在根目录新建env文件 可以创建多个
如下 env.[name]
修改启动命令
在package.json 配置 --mode env文件名称
配置智能提示
interface ImportMetaEnv {
VITE_XIAOMAN:string
}
然后在app.vue输出console.log(import.meta.env) 就已经添加进去了
生产环境使用
创建 .env.production 在执行npm run build 的时候他会自己加载这个文件
如果想在vite.config.ts使用环境变量
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default ({mode}:any) => {
console.log(loadEnv(mode,process.cwd()))
return defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
}
我们就可以通过环境变量这个值 做一些事情比如 切换接口url等
vue3-router
第一章安装Router
router路由
应为vue是单页应用不会有那么多html 让我们跳转 所有要使用路由做页面的跳转。
vue路由允许我们通过不同的URL访问不同的内容。通过vue可以实现多视图的单页web应用。
1,安装
构建前端项目
npm init vue@latest
// 或者
npm init vite@latest
使用vue3 安装对应的router4版本
使用vue2 安装对应的router3版本
在src目录下面新建router文件然后在router文件夹下新建index.ts
//引入路由对象
import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
// vue2 mode history 对应 vue3 createWebHistory
// vue2 mode hash 对应 vue3 createWebHashHistory
// vue2 mode abstact 对应 vue3 createMemoryHistory
//路由数组的类型 RouteRecordRaw
// 定义一些路由
// 每个路由都需要映射到一个组件。
const routes: Array<RouteRecordRaw> = [{
path: '/',
component: () => import('../components/a.vue')
},{
path: '/register',
component: () => import('../components/b.vue')
}]
const router = createRouter({
history: createWebHistory(),
routes
})
//导出router
export default router
router-link
请注意,我们没有使用常规的a标签,而是使用一个自定义组件router-link来创建链接。这使得vue router 可以在不重新加载页面的情况下更改URL,处理URL的生成以及编码。我们将在后面看到如何从这些功能中获益。
router-view
router-view将显示与url对应的组件。你可以把它放在任何地方,以适应你的布局
<template>
<div>
<h1>年轻人可以</h1>
<div>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link tag="div" to="/">跳转a</router-link>
<router-link tag="div" style="margin-left:200px" to="/register">跳转b</router-link>
</div>
<hr />
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>
最后在main.ts挂载
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
第二章Router(命名路由编程式导航)
命名路由
除了path之外,你还可以为任何路由提供name。这有以下优点:
- 没有硬编码的URL
- params的自动编码/解码
- 防止你在url中出现打字错误
- 绕过路径排序
const routes:Array<RouteRecordRaw> = [
{
path:"/",
name:"Login",
component:()=> import('../components/login.vue')
},
{
path:"/reg",
name:"Reg",
component:()=> import('../components/reg.vue')
}
]
router-link跳转方式需要改变 变为对象并且有对应name
<h1>年轻人可以的</h1>
<div>
<router-link :to="{name:'Login'}">Login</router-link>
<router-link style="margin-left:10px" :to="{name:'Reg'}">Reg</router-link>
</div>
<hr />
编程式导航
除了使用<router-link>创建a标签来定义导航链接,我们还可以借助router的实例方法,通过编写代码来实现。
1.字符串模式
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = () => {
router.push('/reg')
}
2.对象模式
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = () => {
router.push({
path: '/reg'
})
}
3.命名式路由模式
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = () => {
router.push({
name: 'Reg'
})
}
a标签跳转
直接通过a href也可以跳转但是会刷新页面
<a href="/reg">rrr</a>
第三章Router (历史记录)
replace的使用
采用replace进行页面的跳转会同样也会创建渲染新的Vue组件,但是在history中其不会重复保存记录,而是替换原有的vue组件;
router-link使用方法
<router-link replace to="/">Login</router-link>
<router-link replace style="margin-left:10px" to="/reg">Reg</router-link>
编程式导航
<button @click="toPage('/')">Login</button>
<button @click="toPage('/reg')">Reg</button>
js
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = (url: string) => {
router.replace(url)
}
横跨历史
该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步
<button @click="next">前进</button>
<button @click="prev">后退</button>
const next = () => {
//前进 数量不限于1
router.go(1)
}
const prev = () => {
//后退
router.back()
}
第四章Router(路由传参)
query传参
编程式导航使用router push或者replace的时候改为对象形式新增query必须传入一个对象
const toDetail = (item: Item) => {
router.push({
path: '/reg',
query: item
})
}
接受参数使用useRoute的query
import { useRoute } from 'vue-router';
const route = useRoute()
<div>品牌:{{ route.query?.name }}</div>
<div>价格:{{ route.query?.price }}</div>
<div>ID:{{ route.query?.id }}</div>
params路由传参
编程式导航使用router push或者replace的时候改为对象形式并且只能使用name,path无效,然后传入parans
const toDetail = (item: Item) => {
router.push({
name: 'Reg',
params: item
})
}
接受参数使用useRoute的params
import { useRoute } from 'vue-router';
const route = useRoute()
<div>品牌:{{ route.params?.name }}</div>
<div>价格:{{ route.params?.price }}</div>
<div>ID:{{ route.params?.id }}</div>
动态路由传参
很多时候,我们需要将给定匹配模式的路由映射到同一个组件。例如,我们可能有一个user组件,它应该对所有用户进行渲染,但用户ID不同。在Vue Router中,我们可以在路径中使用一个动态字段来实现,我们称之为路径参数。
路径参数用冒号:表示。当一个路由被匹配时,它的params的值将在每个组件
const routes:Array<RouteRecordRaw> = [
{
path:"/",
name:"Login",
component:()=> import('../components/login.vue')
},
{
//动态路由参数
path:"/reg/:id",
name:"Reg",
component:()=> import('../components/reg.vue')
}
]
const toDetail = (item: Item) => {
router.push({
name: 'Reg',
params: {
id: item.id
}
})
}
import { useRoute } from 'vue-router';
import { data } from './list.json'
const route = useRoute()
const item = data.find(v => v.id === Number(route.params.id))
二者的区别
- query传参配置的时path,而params传参配置的时name,在params中配置path无效
- query在路由配置不需要设置参数,而params必须设置
- query传递的参数会显示在地址栏中
- params传参刷新会无效,但是query会保存传递过来的值,刷新不变
- 路由配置
第五章Router(嵌套路由)
嵌套路由一些应用程序的UI由多层嵌套的组件组件。在这种情况下,URL的片段通常对应于特定的嵌套组件结构,例如
const routes: Array<RouteRecordRaw> = [
{
path: "/user",
component: () => import('../components/footer.vue'),
children: [
{
path: "",
name: "Login",
component: () => import('../components/login.vue')
},
{
path: "reg",
name: "Reg",
component: () => import('../components/reg.vue')
}
]
},
]
children配置只是另一个路由数组,就像routes本身一样。因此,你可以根据自己的需要,不断地嵌套试图。不要忘记写router-view
<div>
<router-view></router-view>
<div>
<router-link to="/">login</router-link>
<router-link style="margin-left:10px;" to="/user/reg">reg</router-link>
</div>
</div>
第六章Router(命名视图)
命名视图可以在同一级(同一个组件)中展示更多的路由视图,而不是嵌套显示。命名视图可以让一个组件中具有多个路由渲染出口,这对于一些特定的布局组件非常有用。命名视图的概念非常类似于具名插槽,并且视图的默认名称也是default。
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components配置(带上s)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: "/",
components: {
default: () => import('../components/layout/menu.vue'),
header: () => import('../components/layout/header.vue'),
content: () => import('../components/layout/content.vue'),
}
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
对应Router-view 通过name 对应组件
<div>
<router-view></router-view>
<router-view name="header"></router-view>
<router-view name="content"></router-view>
</div>
第七章Router(重定向-别名)
重定向redirect
1,字符串形式配置,访问/重定向到/user (地址栏显示/内容为/user路由的内容)
const routes: Array<RouteRecordRaw> = [
{
path:'/',
component:()=> import('../components/root.vue'),
redirect:'/user1',
children:[
{
path:'/user1',
components:{
default:()=> import('../components/A.vue')
}
},
{
path:'/user2',
components:{
bbb:()=> import('../components/B.vue'),
ccc:()=> import('../components/C.vue')
}
}
]
}
]
2,对象形式配置
const routes: Array<RouteRecordRaw> = [
{
path:'/',
component:()=> import('../components/root.vue'),
redirect: { path: '/user1' },
children:[
{
path:'/user1',
components:{
default:()=> import('../components/A.vue')
}
},
{
path:'/user2',
components:{
bbb:()=> import('../components/B.vue'),
ccc:()=> import('../components/C.vue')
}
}
]
}
]
3,函数模式(可以传参)
const routes: Array<RouteRecordRaw> = [
{
path:'/',
component:()=> import('../components/root.vue'),
redirect: (to) => {
return {
path: '/user1',
query: to.query
}
},
children:[
{
path:'/user1',
components:{
default:()=> import('../components/A.vue')
}
},
{
path:'/user2',
components:{
bbb:()=> import('../components/B.vue'),
ccc:()=> import('../components/C.vue')
}
}
]
}
]
别名alias
将 / 别名为 /root, 意味着当用户访问 /root时, URL仍然是 /user, 但会被匹配为用户正在访问 /
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('../components/root.vue'),
alias:["/root","/root2","/root3"],
children: [
{
path: 'user1',
components: {
default: () => import('../components/A.vue')
}
},
{
path: 'user2',
components: {
bbb: () => import('../components/B.vue'),
ccc: () => import('../components/C.vue')
}
}
]
}
]
第八章Router(导航守卫)
全局前置守卫
router.beforeEach
router.beforeEach((to, form, next) => {
console.log(to, form)
next()
})
每个守卫方法接收三个参数:
to:Route,即将要进入的目标 路由对象
form:Route, 当前导航正要离开的路由
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed(确认的)。
next(false); 中断当前的导航,如果浏览器的URL改变了(可能是用户手动或浏览器后退按钮),那么URL地址会重置到form路由对应的地址。
next('/')或者next({ path: '/' }):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
案例权限判断
const whileList = ['/']
router.beforeEach((to, from, next) => {
let token = localStorage.getItem('token')
// 白名单 有值或者登录过存储了token信息可以跳转 否则就去登录页面
if(whileList.includes(to.path) || token){
next()
} else {
next({ path: '/' })
}
})
全局后置守卫
使用场景一般可以用来坐loadingBar
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受next函数也不会改变导航本身:
router.afterEach((to, from) => {
Vnode.component?.exposed?.endLoading()
})
loadingBar组件
<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>
<script setup lang='ts'>
import { ref, onMounted } from 'vue'
let speed = ref<number>(1)
let bar = ref<HTMLElement>()
let timer = ref<number>(0)
const startLoading = () => {
let dom = bar.value as HTMLElement;
speed.value = 1
timer.value = window.requestAnimationFrame(function fn() {
if (speed.value < 90) {
speed.value += 1;
dom.style.width = speed.value + '%'
timer.value = window.requestAnimationFrame(fn)
} else {
speed.value = 1;
window.cancelAnimationFrame(timer.value)
}
})
}
const endLoading = () => {
let dom = bar.value as HTMLElement;
setTimeout(() => {
window.requestAnimationFrame(() => {
speed.value = 100;
dom.style.width = speed.value + '%'
})
}, 500)
}
defineExpose({
startLoading,
endLoading
})
</script>
<style scoped lang="less">
.wraps {
position: fixed;
top: 0;
width: 100%;
height: 2px;
.bar {
height: inherit;
width: 0;
background: blue;
}
}
</style>
mian.ts
import loadingBar from './components/loadingBar.vue'
const Vnode = createVnode(loadingBar)
render(Vnode, document.body)
router.beforeEach((to, from, next) => {
Vnode.component?.exposed?.startLoading()
})
router.afterEach((to, from) => {
Vnode.component?.exposed?.endLoading()
})
第九章Router(路由元信息)
路由元信息
通过路由记录的meta属性可以定义路由的元信息。使用路由元信息可以在路由中附加自定义的数据,例如:
- 权限校验标识
- 路由组件的过度名称。
- 路由组件持久化缓存(keep-alive)的相关配置
- 标题名称
我们可以在导航守卫或者是路由对象中访问路由的元信息数据。
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta: {
title: "登录"
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta: {
title: "首页",
}
}
]
})
使用TS扩展
如果不使用扩展将会是unknown类型
declare module 'vue-router' {
interface RouteMeta {
title?: string
}
}
第十章Router(路由过度动效)
过渡动效
想要在你的路径组件上使用转场,并对导航进行动画处理,你需要使用v-slot API:
<router-view #default="{route,Component}">
<transition :enter-active-class="`animate__animated ${route.meta.transition}`">
<component :is="Component"></component>
</transition>
</router-view>
上面的用法会对所有的路由使用相同的过度,如果你想让每个路由组件有不同的过渡,你可以将元信息和动态的name结合在一起,放在transition上
declare module 'vue-router'{
interface RouteMeta {
title:string,
transition:string,
}
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta:{
title:"登录页面",
transition:"animate__fadeInUp",
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta:{
title:"首页!!!",
transition:"animate__bounceIn",
}
}
]
})
第十一章Router(滚动行为)
滚动行为
使用前端路由,当切换到新路由时,想要页面滚动到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router可以自定义路由切换时页面如何滚动。
当创建一个Router实例,你可以提供一个scrollBehavior方法
const router = createRouter({
history: createWebHistory(),
scrollBehavior: (to, from, savePosition) => {
console.log(to, '==============>', savePosition);
return new Promise((r) => {
setTimeout(() => {
r({
top: 10000
})
}, 2000);
})
},
scrollBehavior 方法接收to和from路由对象。第三个参数savedPosition当且仅当popstate导航(通过浏览器的 前进/后退 按钮触发)时才可用。
scrollBehavior 返回滚动位置的对象信息,长这样:
{ left:number,top:number }
const router = createRouter({
history: createWebHistory(),
scrollBehavior: (to, from, savePosition) => {
return {
top:200
}
},
第十二章Router(动态路由)
动态路由
我们一般使用动态路由都是后台返回一个路由表前端通过调接口拿到后处理(后端处理路由)
主要使用的方法就是router.addRoute
添加路由
动态路由主要通过两个函数实现。router.addRoute() 和 router.removeRoute(). 它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用router.push() 或 router.replace() 来手动导航,才能显示该新路由
router.addRoute({ path: '/about', component: About })
删除路由
有几个不同的方法来删除现有的路由:
通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,在添加路由;
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
通过调用router.addRoute() 返回的回调;
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
通过使用router.removeRoute() 按名称删除路由
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 symbol 作为名字。
当路由被删除时,所有的别名和子路由也会被同时删除
查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
- router.hasRoute():检查路由是否存在。
- router.getRoutes():获取一个包含所有路由记录的数组。
案例前端代码
注意一个事项vite在使用动态路由的时候无法使用别名@必须使用相对路径
const initRouter = async () => {
const result = await axios.get('http://localhost:9999/login', { params: formInline });
result.data.route.forEach((v: any) => {
router.addRoute({
path: v.path,
name: v.name,
//这儿不能使用@
component: () => import(`../views/${v.component}`)
})
router.push('/index')
})
console.log(router.getRoutes());
}
vue3-Pinia
第一章pinia
前端全局状态管理工具
- 完整的ts的支持
- 足够轻量,压缩后的体积只有1kb左右
- 去除 mutstions,只有state,getters,actions
- actions支持同步和异步
- 代码扁平化没有模块嵌套,只有store的概念,store之间可以自由使用,每一个store都是独立的
- 无需手动添加 store, store一旦创建便会自动添加
- 支持vue3和vue2
官方文档Pinia
1.起步和安装
yarn add pinia
npm install pinia
2. 引入注册vue3
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使用
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,
})
第二章pinia初始化store仓库
1.新建一个文件夹store
2.新建文件[name].ts
3.定义仓库store
import { defineStore } from 'pinia'
4,我们需要知道存储是使用定义的defineStore(),并且它需要一个唯一的名称,作为第一个参数传递我这里是新建一个ts文件把名称抽离出去了
新建文件store-name.ts
export const enum Names {
Test = 'TEST'
}
store引入
import { defineStore } from 'pinia'
import { Names } from './store-namespace'
export const useTestStore = defineStore(Names.Test, {
})
这个名称,也称为id,是必要的,pinia使用它来将商店链接到devtoots。将返回的函数命名为use...是可组合项之间的约定,以使其使用习惯。
5.定义值
state箭头函数返回一个对象 在对象里面定义值
import { defineStore } from 'pinia'
import { Names } from './store-namespce'
export const useTestStore = defineStore(Names.Test, {
state:()=>{
return {
current:1
}
},
//类似于computed 可以帮我们去修饰我们的值
getters:{
},
//可以操作异步 和 同步提交state
actions:{
}
})
第三章pinia修改state值的几种方法
1.state是允许直接修改值的例如current++
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.current}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.current++
}
</script>
<style>
</style>
2.批量修改state的值
在他的实例上有$path方法可以批量修改多个值
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.current}}
</div>
<div>
{{Test.age}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.$patch({
current:200,
age:300
})
}
</script>
<style>
</style>
3.批量修改函数形式
推荐使用函数形式 可以自定义修改逻辑
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.current}}
</div>
<div>
{{Test.age}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.$patch((state)=>{
state.current++;
state.age = 40
})
}
</script>
<style>
</style>
4.通过原始对象修改整个实例
$state您可以通过store的属性设置为新对象来替换store 的整个状态 缺点就是必须修改整个对象的所有属性
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.current}}
</div>
<div>
{{Test.age}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.$state = {
current:10,
age:30
}
}
</script>
<style>
</style>
5.通过actions修改
定义actions中直接使用this就可以知道state里面的值
import { defineStore } from 'pinia'
import { Names } from './store-naspace'
export const useTestStore = defineStore(Names.TEST, {
state:()=>{
return {
current:1,
age:30
}
},
actions:{
setCurrent () {
this.current++
}
}
})
使用方法直接在实例调用
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.current}}
</div>
<div>
{{Test.age}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.setCurrent()
}
</script>
<style>
</style>
第四章解构store
在pinia是不允许直接解构是会失去响应性的
const Test = useTestStore()
const { current,name } = Tst
console.log(current,name)
差异对比
修改Test current解构完之后的数据不会变
而源数据是会变的
<template>
<div>origin value {{Test.current}}</div>
<div>
pinia:{{ current }}--{{ name }}
change :
<button @click="change">change</button>
</div>
</template>
<script setup lang='ts'>
import { useTestStore } from './store'
const Test = useTestStore()
const change = () => {
Test.current++
}
const { current, name } = Test
console.log(current, name);
</script>
<style>
</style>
解决方案可以使用storeToRefs
import { storeToRefs } from 'pinia'
const Test = useTestStore()
const { current, name } = storeToRefs(Test)
其原理跟toRefs 一样的给里面的数据包裹一层toref
第五章 (Actions,getters)使用
actions(支持同步异步)
1,同步直接调用即可
import { defineStore } from 'pinia'
import { Names } from './store-naspace'
export const useTestStore = defineStore(Names.TEST, {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
<template>
<div>
<button @click="Add">+</button>
<div>
{{Test.counter}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.randomizeCounter()
}
</script>
<style>
</style>
2.异步 可以结合async await修饰
import { defineStore } from 'pinia'
import { Names } from './store-naspace'
type Result = {
name: string
isChu: boolean
}
const Login = (): Promise<Result> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: '小满',
isChu: true
})
}, 3000)
})
}
export const useTestStore = defineStore(Names.TEST, {
state: () => ({
user: <Result>{},
name: "123"
}),
actions: {
async getLoginInfo() {
const result = await Login()
this.user = result;
}
},
})
template.vue
<template>
<div>
<button @click="Add">test</button>
<div>
{{Test.user}}
</div>
</div>
</template>
<script setup lang='ts'>
import {useTestStore} from './store'
const Test = useTestStore()
const Add = () => {
Test.getLoginInfo()
}
</script>
<style>
</style>
3,多个action互相调用getLoginInfo setName
state: () => ({
user: <Result>{},
name: "default"
}),
actions: {
async getLoginInfo() {
const result = await Login()
this.user = result;
this.setName(result.name)
},
setName (name:string) {
this.name = name;
}
},
getters
1,使用箭头函数不能使用this this指向已经改变,指向undefined 修改值请用state
主要作用类似于computed 数据修饰并且有缓存
getters:{
newPrice:(state)=> `$${state.user.price}`
},
2,普通函数形式可以使用this
getters:{
newCurrent ():number {
return ++this.current
}
},
3,getters 互相调用
getters:{
newCurrent ():number | string {
return ++this.current + this.newName
},
newName ():string {
return `$-${this.name}`
}
},
第六章API
1,$reset
重置store到他的初始状态
state: () => ({
user: <Result>{},
name: "default",
current:1
}),
vue例如我把值改变到了10
const change = () => {
Test.current++
}
调用$reset(); 将会把state所有的值 重置回 原始状态
2.订阅state的改变
类似于vuex的abscribe。 只要有state的变化就会走这个函数
Test.$subscribe((args, state) => {
console.log(args, state)
})
返回值
第二个参数
如果你的组件卸载之后还想继续调用请设置第二个参数
Test.$subscribe((args,state)=>{
console.log(args,state);
},{
detached:true
})
3,订阅actions的调用
只要有actions被调用就会走这个函数
Test.$onAction((args)=>{
console.log(args);
})
第七章(pinia插件)
pinia和vuex都有一个通病 页面刷新状态会丢失
我们可以写一个pinia插件缓存他的值
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'
}))