Vue3 + setup + TypeScript: 构建现代、类型安全的Vue应用的关键技巧总结-CSDN博客

1. 组件引入

当使用 setup 的时候,组件直接引入就可以了,不需要再自己手动注册

html 复制代码
<template>
  <Child />
</template>

<script setup lang="ts">
import Child from "./Child.vue";
</script>

2. ref 和 reactive

ref 一般用于基本的数据类型,比如 stringboolean ,reactive 一般用于对象 ref 的地方其实也是调用的 reactive 实现的。

html 复制代码
<template>
  <h1>{{ title }}</h1>
  <div>
    {{ data }}
  </div>
</template>

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

const title = ref("title");

const data = reactive({
  userName: "xiaoming",
  age: 18,
});
</script>

3. defineEmits 和 defineProps 获取父组件传过来值和事件

javascript 复制代码
// 第一种不带默认值props
const props = defineProps<{
  foo: string
  bar?: number
}>()
// 第二种带默认值props

export interface ChildProps {
  foo: string
  bar?: number
}

const props = withDefaults(defineProps<ChildProps>(), {
   foo: "1qsd"
  bar?: 3
})

// 第一种获取事件
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 第二种获取事件

const emit = defineEmits(["dosth"])

4. 使用 useAttrs 和 useSlots

useAttrs 可以获取父组件传过来的 idclass 等值。useSlots 可以获得插槽的内容。例子中,我们使用 useAttrs 获取父组件传过来的 idclassuseSlots 获取插槽的内容。

父组件:

html 复制代码
<template>
  <div class="father">{{ fatherRef }}</div>
  <Child :fatherRef="fatherRef" @changeVal="changeVal" class="btn" id="111">
    <template #test1>
      <div>1223</div>
    </template>
  </Child>
</template>

<script setup lang="ts">
import { ref } from "vue";

import Child from "./Child.vue";

const fatherRef = ref("1");

function changeVal(val: string) {
  fatherRef.value = val;
}
</script>

<style lang="scss" scoped>
.father {
  margin-top: 40px;
  margin-bottom: 40px;
}
.btn {
  font-size: 20px;
  color: red;
}
</style>

子组件:

html 复制代码
<template>
  <!-- <div class="child">{{ props.fatherRef }}</div> -->
  <div v-bind="attrs">
    <slot name="test1">11</slot>
    <input type="text" v-model="inputVal" />
  </div>
</template>

<script setup lang="ts">
import { computed, useAttrs, useSlots } from "vue";

const props = defineProps<{
  fatherRef: string;
}>();

const emits = defineEmits(["changeVal"]);

const slots = useSlots();

const attrs = useAttrs();

console.log(122, attrs, slots);

const inputVal = computed({
  get() {
    return props.fatherRef;
  },

  set(val: string) {
    emits("changeVal", val);
  },
});
</script>

使用自定义指令

setup 里边自定义指令的时候,只需要遵循vNameOfDirective 这样的命名规范就可以了

比如如下自定义 focus 指令,命名就是 vMyFocus,使用的就是 v-my-focus

自定义指令

html 复制代码
<script setup lang="ts">
const vMyFocus = {
  onMounted: (el: HTMLInputElement) => {
    el.focus();
    // 在元素上做些操作
  },
};
</script>
<template>
  <input v-my-focus value="111" />
</template>

5. 使用 defineExpose 子组件传父组件

子组件

html 复制代码
<template>
  <div class="child"></div>
</template>

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

function doSth() {
  console.log(333);
}

defineExpose({ doSth });
</script>

父组件

html 复制代码
<template>
  <div class="father" @click="doSth1">222</div>
  <Child ref="childRef"></Child>
</template>

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

import Child from "./Child.vue";

const childRef = ref();

function doSth1() {
  childRef.value.doSth();
}
</script>

6. 父组件传子组件

父组件

html 复制代码
<template>
  <div class="father"></div>
  <Child @doSth="doSth"></Child>
</template>

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

import Child from "./Child.vue";

function doSth() {
  console.log(112);
}
</script>

子组件

html 复制代码
<template>
  <div class="child">2222</div>
</template>

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

const emits = defineEmits(["doSth"]);

onMounted(() => {
  emits("doSth");
});
</script>

7. toRefs

当从父组件向子组件传 props 的时候,必须使用 toRefs 或者 toRef 进行转一下,这是为什么呢?

这里是因为如果不使用 toRefs 转一次的话,当父组件中的 props 改变的时候,子组件如果使用了 Es6 的解析,会失去响应性。

可以看下如下例子

父组件

html 复制代码
<template>
  <div class="father" @click="changeVal">{{ fatherRef }}</div>
  <Child :fatherRef="fatherRef"></Child>
</template>

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

import Child from "./Child.vue";

const fatherRef = ref(1);

function changeVal() {
  fatherRef.value = 2;
}
</script>

<style lang="scss" scoped>
.father {
  margin-bottom: 40px;
}
</style>

子组件

html 复制代码
<template>
  <div class="child" @click="changeVal">{{ fatherRef }}</div>
</template>

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

const props = defineProps<{
  fatherRef: any;
}>();

const { fatherRef } = props;

function changeVal() {
  fatherRef.value = 34;
}
</script>

可以看到当父组件如果点击之后,因为使用 const { fatherRef } = props;进行解析,就失去了响应性

所以当父组件变成 2 的时候,子组件还是 1。

这里有两种解决办法

  1. 使用 const { fatherRef } = toRefs(props);
  2. 在模版中中使用 props.fatherRef

8. 子组件使用 v-model

8.1 可以在子组件中使用 computed,实现双向绑定

父组件

html 复制代码
<template>
  <div class="father">{{ fatherRef }}</div>
  <Child :fatherRef="fatherRef" @changeVal="changeVal"></Child>
</template>

<script setup lang="ts">
import { ref } from "vue";

import Child from "./Child.vue";

const fatherRef = ref("1");

function changeVal(val: string) {
  fatherRef.value = val;
}
</script>

<style lang="scss" scoped>
.father {
  margin-top: 40px;

  margin-bottom: 40px;
}
</style>

子组件

html 复制代码
<template>
  <!-- <div class="child">{{ props.fatherRef }}</div> -->
  <input type="text" v-model="inputVal" />
</template>

<script setup lang="ts">
import { computed } from "vue";

const props = defineProps<{
  fatherRef: string;
}>();

const emits = defineEmits(["changeVal"]);

const inputVal = computed({
  get() {
    return props.fatherRef;
  },

  set(val: string) {
    emits("changeVal", val);
  },
});
</script>

8.2 可以从父组件传递值和改变值的方法,然后子组件也可以使用 v-model

例子中父组件传递 modelValueupdate:modelValue 方法 父组件:

html 复制代码
<template>
  <Child :modelValue="searchText" @update:modelValue="changeVal"> </Child>
</template>

<script setup lang="ts">
import { ref } from "vue";

import Child from "./Child.vue";

const searchText = ref(1);

function changeVal(val: number) {
  searchText.value = val;
}
</script>

<style lang="scss" scoped>
.father {
  margin-top: 40px;

  margin-bottom: 40px;
}

.btn {
  font-size: 20px;

  color: red;
}
</style>

子组件:

html 复制代码
<template>
  <!-- <div class="child">{{ props.fatherRef }}</div> -->
  <!-- <div v-bind="attrs">
        <slot name="test1">11</slot>
        <input type="text" v-model="inputVal" />
    </div> -->
  <input v-model="modelValue" />
</template>

<script setup lang="ts">
import { computed, useAttrs, useSlots } from "vue";

const props = defineProps<{
  modelValue: number;
}>();

// const emits = defineEmits(["changeVal"]);
</script>

9. 递归组件

组件本身是可以调用组件自身的,也就是递归。vue3 中使用文件名称自动注册为组件的名称,比如名为 Child.vue 的组件可以在其模板中用 <Child/> 引用它自己。这里需要注意的是需要设置条件语句,用来中断递归,不然递归会无限递归下去。

父组件

html 复制代码
<template>
  <Child :modelValue="searchText" @update:modelValue="changeVal"> </Child>
</template>

<script setup lang="ts">
import { ref } from "vue";
import Child from "./Child.vue";
const searchText = ref(1);
function changeVal(val: number) {
  searchText.value = val;
}
</script>

<style lang="scss" scoped>
.father {
  margin-top: 40px;
  margin-bottom: 40px;
}
.btn {
  font-size: 20px;
  color: red;
}
</style>

子组件

html 复制代码
<template>
  <input v-model="modelValue" />
  <Child
    :modelValue="test"
    @update:modelValue="changeTest"
    v-if="modelValue > 2"
  ></Child>
</template>

<script setup lang="ts">
import { computed, useAttrs, useSlots, ref } from "vue";
const props = defineProps<{
  modelValue: number;
}>();
const test = ref(0);
function changeTest(val: number) {
  test.value = val;
}

// const emits = defineEmits(["changeVal"]);
</script>

<style lang="scss" scoped>
.child {
  position: relative;
}
</style>

10. vue3 ts 获取组件 ref 实例

  • 通过ref直接拿到dom引用
html 复制代码
<template>
    <div class="demo1-container">
        <div ref="sectionRef" class="ref-section"></div>
    </div>
</template>

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

通过对div元素添加了ref属性,为了获取到这个元素,我们声明了一个与ref属性名称相同的变量sectionRef,然后我们通过 sectionRef.value 的形式即可获取该div元素

  • 通过父容器的ref遍历拿到dom引用
html 复制代码
<template>
    <div class="demo2-container">
        <div ref="listRef" class="list-section">
            <div @click="higherAction(index)" class="list-item" v-for="(item, index) in state.list" :key="index">
                <span>{{item}}</span>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
const listRef = ref()
</script>

通过对父元素添加了ref属性,并声明了一个与ref属性名称相同的变量listRef,此时通过listRef.value会获得包含子元素的dom对象 此时可以通过listRef.value.children[index]的形式获取子元素dom

  • 通过:ref将dom引用放到数组中

    html 复制代码
    <template>
      <div class="demo2-container">
          <div class="list-section">
              <div :ref="setRefAction" @click="higherAction(index)" class="list-item" v-for="(item, index) in state.list" :key="index">
                  <span>{{item}}</span>
              </div>
          </div>
      </div>
    
      </template>
    
      <script setup lang="ts">
      import { reactive } from 'vue'
    
      const state = reactive({
          list: [1, 2, 3, 4, 5, 6, 7],
          refList: [] as Array<any>
      })
    
      const setRefAction = (el: any) => {
          state.refList.push(el);
      }
      </script>

    通过:ref循环调用setRefAction方法,该方法会默认接收一个el参数,这个参数就是我们需要获取的div元素 此时可以通过state.refList[index]的形式获取子元素dom

  • 通过子组件emit传递ref

html 复制代码
<template>
    <div ref="cellRef" @click="cellAction" class="cell-item">
        <span>{{item}}</span>
    </div>
</template>

<script setup lang="ts">
import {ref} from 'vue';

const props = defineProps({
    item: Number
})
const emit = defineEmits(['cellTap']);
const cellRef = ref();
const cellAction = () => {
    emit('cellTap', cellRef.value);
}
</script>

通过对子组件添加了ref属性,并声明了一个与ref属性名称相同的变量cellRef,此时可以通过emit将cellRef.value作为一个dom引用传递出去

  • tsx 等 render 组件中获取的方式更简单
javascript 复制代码
import { defineComponent, ref, onMounted } from "@vue/runtime-core";
import { ElForm } from "element-plus";

export default defineComponent({
  setup() {
    const $form = ref<InstanceType<typeof ElForm>>(null);

    onMounted(() => {
      $form.value?.validate; // 类型正确
    });

    return () => <ElForm ref={$form}></ElForm>;
  },
});

需要注意的是,如果使用 expose 暴露方法出去,无法获取到对应的类型,您需要自定义类型 github.com/vuejs/rfcs/...[1]

javascript 复制代码
// 组件 MyForm
import { defineComponent, ref, onMounted } from "@vue/runtime-core";
import { ElForm } from "element-plus";

type ELEForm = InstanceType<typeof ElForm>;

// 在外界通过 ref 获取组件实例 请使用这个类型
export interface MyFormExpose {
  validate: ELEForm["validate"];
}

export default defineComponent({
  name: "MyForm",

  setup(props, { expose }) {
    const $form = ref<InstanceType<typeof ElForm>>(null);

    expose({
      validate: (callback) => $form.value?.validate(callback),
    } as MyFormExpose);

    return () => <ElForm ref={$form}></ElForm>;
  },
});
html 复制代码
<!-- Home.vue -->
<template>
  <MyForm :ref="$form" />
</template>

<script>
import { defineComponent, ref, onMounted } from '@vue/runtime-core'
import MyForm, { MyFormExpose } from '@/components/MyForm'
export default defineComponent({
  components: { MyForm }

  setup(){
    const $form = ref<InstanceType<typeof MyForm> & MyFormExpose>(null)

    onMounted(() => {
       $form.value?.validate // 类型正确
    })
  }
})
</script>
相关推荐
liuweni1 小时前
Next.js 独立开发教程(三):CSS 样式的完整指南
开发语言·前端·javascript·css·react.js·职场和发展·前端框架
沉默璇年4 小时前
react中的useCallback 有什么作用?
前端·react.js·前端框架
爱学习的执念4 小时前
如何使用Jest测试你的React组件
前端·react.js·前端框架
乐容4 小时前
react 中解决 类型“never”上不存在属性“value”。
前端·react.js·前端框架
大梦百万秋6 小时前
React前端框架基础知识详解
前端·react.js·前端框架
Dragon Wu11 小时前
Taro React小程序开发框架 总结
前端·react.js·前端框架·taro
bin915311 小时前
【热门主题】000067 React前端框架:探索高效Web开发之路
前端框架
sususugaa11 小时前
前端框架Vue3——响应式数据,v-on,v-show和v-if,v-for,v-bind
开发语言·前端·vue.js·前端框架
啵咿傲18 小时前
在React中实践一些软件设计思想 ✅
前端·react.js·前端框架
Dragon Wu21 小时前
前端框架 Redux tool RTK 总结
前端·javascript·前端框架