一. 前言
在日常开发中,Vue3.0已经用的很常见了。但是在最近接手一个多人迭代的“code hell”项目中,我发现一些非常匪夷所思的写法。在这里总结一下,我个人感觉比较好用的Vue3.0的特性,以及一些在ts方面更加合理的规范吧。仅作为个人日常总结。
注意: 以下默认setup的语法糖写法
二. defineProps
1. 定义
定义组件的属性。通过调用该函数,可以声明组件的属性,并且将属性的类型、默认值等信息进行声明。 在子组件内部通过props属性来接收父组件传递的属性值。
这里注意是defineProps是个函数
可以看一下在Vue源码中的定义:
export declare function defineProps<TypeProps>(): Readonly<TypeProps>;
这里面牵涉到runtime以及一些宏函数
的问题(之后有空再聊一下吧)
2. 写法
通过使用范型参数定义
const props = defineProps<{T}>()
如下例子:
const props = defineProps<{
setUpPropsConfig: {
title: string,
value: number,
isOK: boolean,
list: string | number [],
myObject: { name: string, age: number},
func?: () => void
}
}>()
还可以把当前的范型参数抽出去编写成一个接口
interface ISetUpProps{
····
}
const props = defineProps<ISetUpProps>()
这里要注意一个问题,这个接口文件不能通过import从外部文件引入,不然会报错
。
[@vue/compiler-sfc] type argument passed to defineProps() must be a literal type,
or a reference to an interface or literal type.
加入默认值:
withDefaults(defineProps, {})
const props = withDefaults(defineProps<{
title: string,
value: number
}>(), {
title: 'name',
value: 1
})
三. defineEmits
用于定义组件的事件。通过调用该函数,可以声明组件所触发的事件,并且将事件的名称、参数等信息进行声明。
export declare function defineEmits<TypeEmit>(): TypeEmit;
类似于defineProps也是一个函数,可以通过范型参数定义
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', array: number[]): void }
>()
其实这两个方法本质上来说就是函数function() , 跟范型函数的写法是一致的:
// 范型函数
const emitFunc = <T>(args: T): void => {
}
function emitFunc2<T>(args: T): void {
}
// defineProps
function defineProps<TypeProps>(): Readonly<TypeProps>
// defineEmits
function defineEmits<TypeEmit>(): TypeEmit
四. defineExpose
在setup语法糖下,显式暴露组件的公共属性和方法。简单来说,就是允许父组件访问子组件的特定属性或方法
。
这个方法之前我用的不多,最近在项目中发现还真的挺好用的,挺香 !!!
举个例子: 项目中有很多表单弹窗的封装。怎么封装呢?
4.1 update:modelValue
这个方法就是通过传入一个 modelValue
的prop,在需要更新值的时候,通过触发 update:modelValue
, 完成组件值的更新。
简单封装一个弹窗:
代码如下:
// myDialog.vue
<template>
<div class="mask" v-if="visiable">
<div class="modal-content">
<div class="dialog-header">{{ title }}</div>
<div class="dialog-body">{{ content }}</div>
<div class="dialog-footer">
<el-button @click="onConfirm" type="primary">确定</el-button>
<el-button @click="onCancel">取消</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
visiable: boolean,
title: string,
content: string
}>(), {
visiable: false,
title: '我的标题',
content: 'xxxxxxx'
})
const emit = defineEmits<{
(e: 'update:visiable', val: boolean): void
}>()
const onConfirm = () => {
emit('update:visiable', false)
}
const onCancel = () => {
emit('update:visiable', false)
}
</script>
<style scoped lang="scss">
.mask {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
.modal-content {
width: 400px;
padding: 16px;
margin: 200px auto 0;
position: relative;
background: #fff;
}
.dialog-header {
padding-bottom: 16px;
color: #303133;
line-height: 24px;
}
.dialog-body {
color: 606266;
font-size: 14px;
}
.dialog-footer {
margin-top: 18px;
text-align: right;
}
}
</style>
在父组件里面引入一下,然后调用一下:
<template>
<myDialog v-model:visiable="visiable" />
<button @click="clickMe">点我</button>
</template>
<script lang="ts" setup>
import myDialog from '../src/myDialog/index.vue'
import { ref } from 'vue'
const visiable = ref(false)
const clickMe = () => {
visiable.value = true
}
</script>
4.2 defineExpose
简单对上的组件进行修改一下:
<template>
<div class="mask" v-if="visiable">
<div class="modal-content">
<div class="dialog-header">{{ modalContent.title }}</div>
<div class="dialog-body">{{ modalContent.content }}</div>
<div class="dialog-footer">
<el-button @click="onConfirm" type="primary">确定</el-button>
<el-button @click="onCancel">取消</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visiable = ref(false)
const modalContent = ref<{ title: string, content: string }>({title: '', content: ''})
const init = (item: { title: string, content: string }) => {
visiable.value = true
modalContent.value = item ?? {}
}
const closeModal = () => visiable.value = false
const onConfirm = () => {
closeModal()
}
const onCancel = () => {
closeModal()
}
defineExpose(
{
init
}
)
</script>
也比较容易理解,不再依靠外部传入的prop,通过内部定义的ref来控制显示,然后把控制显示的方法在这里是 init
,通过defineExpose暴露出去,在父组件中调用。
在父组件中:
<template>
<!-- <myDialog v-model:visiable="visiable" /> -->
<defineDialog ref="defineDialogRef" />
<button @click="clickMe">点我</button>
</template>
<script lang="ts" setup>
import defineDialog from '../src/myDialog/defineDialog.vue'
import { ref } from 'vue'
const defineDialogRef = ref()
const clickMe = () => {
defineDialogRef.value.init({title: '新的标题', content: '这是个内容'})
}
</script>
通过ref拿到子组件对象,然后调用init方法,同时可以传值。
可以试一下,没有什么问题。
两种方法没有什么好坏之分,就是一种写法上的不同。个人觉得第二种更好用一点,特别是只需要处理子组件上的值的时候。
五. 获取 DOM 节点
有个疑问,这么多年了,vue3.0项目中还会有人用getElementById等方法获取节点么?这不是兜兜转转还是拉了一裤子翔么?
除了一些特殊的情况外,都可以使用内置的templatereference(简称 ref
)获取、操作dom元素。
如下的例子:
<template>
<div ref="divRef" style="width: 200px; height: 400px;"></div>
<input ref="inputRef" value="value的内容" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
// 可以设置不同的元素类型
const divRef = ref<HTMLDivElement>()
const inputRef = ref<HTMLInputElement>()
onMounted(() => {
console.log(divRef.value)
// 为了严格的类型安全,这里最好通过可选链来限制。
console.log(inputRef.value?.value)
})
</script>
ref
如果用于普通 DOM 元素
,引用将是元素本身
;如果用于子组件
,引用得到的值将是子组件的实例对象
。
六. pinia
合理的使用一些pinia的api可以很好的简化代码、优化代码。
6.1 基本使用
新建一个store.ts
import { defineStore } from "pinia"
type TStore = {
counter: number
name: string
}
export const useStore = defineStore('myStore', {
state: (): TStore => {
return {
counter: 1,
name: '小美老师'
}
},
getters: {
addCount: (state) => {
return state.counter++
},
addCount2(): number {
return this.counter * 3;
}
},
actions: {
induce() {
this.counter--
}
}
})
要注意两点:
- state是一个函数,
所以类型推断的时候是需要按照函数的类型推断方式
,推荐使用箭头函数更方便。 - 有两种getters的写法,但是个人更推荐通过常规函数用this访问整个store的实例。
6.2 state的获取
- 通过store实例访问 state,可以直接
读取和写入
,store.counter++。 - 通过
store.$reset()
方法可以将 state 重置为初始值。 - 通过
store.$patch()
方法可以完成state值的批量修改。 - 通过
store.$subscribe()
订阅 state 的变化,在 patches 修改之后订阅只会触发一次。
这个2和3其实真的很好用,但是我在项目中看到用的不多。
如上代码,我们在项目中引入一下:
<template>
<div>{{ name }}</div>
<div> {{ counter }}</div>
<button @click="patch">patch</button>
<button @click="reset">reset</button>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useStore } from '../src/store'
const store = useStore()
const { name, counter } = storeToRefs(store)
const reset = () => {
store.$reset(); // 重置成功
};
const patch = () => {
// 直接修改state里面的值,不用在批次修改。
store.$patch({
counter: store.counter + 1,
name: "啊,小美老师",
});
}
store.$subscribe(
() => {
console.log("啊,小美老师发生了变化");
},
{ detached: true }
);
</script>
这里的detached
属性可能是用来指示这个监听器是否应该与组件的生命周期解绑。如果detached
为true
,那么即使组件被销毁,这个监听器也会继续监听状态变化。
这里面尤其是store.$subscribe
我在项目中很少看到,都是在通过watch监听store里面值的改变。这个是可以推广使用的。
六. 总结
简单介绍了一下项目中个人比较常用的几个特性,需要的同学配合项目食用。多谢大家了!
偏爱前端的晓羽:称作2024很强的前端面试场景题,成功帮助532人拿到offer
给你们推荐一篇我帮助500+名同学完成改造的文章,希望大家看完以后都可以领取到心仪的offer哦