【前端应用】- Vue3 Composition API 如何结合 TypeScript 进行使用

前置说明

Vue 官方文档具体有对 CompositionAPI + TypeScript 的使用说明,请参考:
TypeScript 与组合式 API | Vue.js

Vue3 的源码是使用 TypeScript 进行编写的,所以 compositionAPI 和 TypeScript 本身就十分"丝滑"。

本文是针对 Vue 的 <script setup lang="ts">语法进行阐述的。

一切要从宏函数说起:

所谓的宏函数 ,也叫编译宏编译宏函数 :就是在一个 ES 编写作用域内本身并没有定义,而是在编译过程中注入的工具函数。(类比绝地求生中,虽然系统本身没有瞄准功能,但是鼠标给予瞄准功能添加的鼠标宏
<script setup lang="ts">本身是 defineComponent + setup 函数的语法糖

举个例子,考虑下面两段代码:

vue 复制代码
<script setup lang="ts">
  const props = defineProps({
    a: {
      type: String,
      default: ''
    }
  });

  const emit = defineEmits(['change']);
</script>

<template>
  <div>
    <!-- sub views -->
  </div>
</template>
vue 复制代码
<script lang="ts">
  import { defineComponent as _defineComponent } from 'vue';

  export default _defineComponent({
    name: 'Comp',
    props: {
      a: {
        type: String,
        default: ''
      }
    },
    emits: ['change'],
    setup(props, ctx) {
      return {
        
      };
    }
  });
</script>

<template>
  <div>
    <!-- sub views -->
  </div>
</template>

在上述的第一段代码中,definePropsdefineEmits本身在 script 中都没有被定义,而是最终会在编译的过程中定义这两个函数并执行传递给 defineComponents 中,所以definePropsdefineEmits这两个函数就是宏函数。

简单实现一下吧

javascript 复制代码
export default function (source) {
  const importSentense = source.matches(/^import/gm);
  const commonSentense = source.matches(/^[^import]/gm);

  return `
    import { defineComponent as _defineComponent } from 'vue';
    ${ importSentense.join('\n') }
  
    let obj = {
        setup(props, ctx) {
            let scope = {};
            ${commonSentense}
            ${
                commonSentense.replace(/^(let|const|var)/gm, (source, key) => {
                    const varName = key.matches(/?=\=/).trim();
                    scope[\`${{varName}}\`] = varName;
                    return source;
                })
            }
            return scope;
        }
    };
  
    function defineProps(propsOptions) {
        obj.props = propsOptions;
    }
    
    function defineEmits(emitOptions) {
        obj.emits = emitOptions;
    }
    
    function defineExpose(exposeOptions) {
      const setup = obj.setup;

      obj.setup = function (props, ctx) {
        // ...
      }
    }

    return _defineComponent(obj);`;
}

在 Vue 的 <script setup> 语法中,主要有以下几个宏函数:

  • defineProps : 用来定义组件的 props 的
  • defineEmits: 用来定义组件的自定义传出事件 emits 的
  • defineExpose: 用来定义组件的自定义暴露属性或者方法的 (在函数调用组件时可能会非常有用, 比如封装一个 <MessageBox>
  • ......

从宏函数到宏函数定义:

宏函数定义主要可以分为编译时定义和运行时定义

  • 编译时定义:可以确定属性或者方法的 TypeScript 定义,但是无法直接定义默认值
  • 运行时定义:可以定义默认值和根据默认值进行推断式定义,但是无法进行精确的 TypeScript 定义

defineProps

defineProps 主要用于声明 Vue 组件中的 props
defineProps 的定义方式主要有三种:

运行时定义:

使用运行时定义 props 时,定义原始值和 JavaScript写法没有什么太大的区别;定义引用值需要使用 PropType 进行断言 (T 指代需要定义的类型)

vue 复制代码
<script setup lang="ts">
	import { PropType } from 'vue';

  interface IObject {
    a: number;
    b: number;
    c: number;
  }

  
  defineProps({
    num: Number,
    name: {
      type: String,
      default: '',
      required: true,
    },
  	isOk: Boolean,
    
    list: {
      type: Array as PropType<string[]>,
      default: () => []
    },
    obj: {
      type: Object as PropType<IObject>,
      default: () => ({ a: 1, b: 2, c: 3 })
    },
    map: {
      type: Map as PropType<Map<String, Function>>
    }
  });
</script>

注意

  1. 使用 PropType 进行断言声明值时,无法限制将 object 的默认值限制为对应的接口 IObject
  1. 使用 PropType 进行断言声明值时,无法限制组件调用时传入 IObject 传入额外的属性值

编译时定义:

  • 在 defineProps 方法的泛型中直接给入TS类型(interface, type 均可)并且 defineProps 不要传入运行时定义的配置对象
typescript 复制代码
export interface IDemoProps {
  num: number;
  name: string;
  isOk: boolean;
  list: string[];
  obj: IObject;
  map: Map<String, Function>;
}
vue 复制代码
<script setup lang="ts">
  import { IDemoProps } from '@/types';

  defineProps<IDemo1Props>();
</script>

注意:

  1. 使用编译时定义可以让外界严格地按照 TypeScript 的类型检查进行调用
  1. 使用编译时定义无法直接给组件定义默认值

编译时默认值定义:

如果需要使用编译时具有类型检查,并且还能给 props 添加默认值,需要使用 withDefaults编译宏包裹 defineProps 进行定义

vue 复制代码
<script setup lang="ts">
  withDefaults(defineProps<IDemo1Props>(), {
    num: 1,
    name: 'Demo',
    isOk: true,
    list: () => [],
    obj: () => ({
      a: 1,
      b: 2,
      c: 3,
      d: 4
    }),
    map: () => new Map<string, Function>()
  });
</script>

注意:

  1. withDefaults 可以给 defineProps 设置对应的,可选的默认值;被设置后的默认值外界渲染组件的时候可以不用传递这些值
  2. withDefaults 设置 props 默认值时,原始默认值直接传递值的本身,引用默认值传递一个返回对应引用的函数(e.g () => []) (类比 options API 定义 props 时的规则)

defineEmits

defineEmits 主要是告诉注册的组件内部到底有什么自定义事件。

同样地,defineEmits 也可以分成:

运行时定义

运行时定义的方式和 options API 定义 emits 选项的方式是一样的

vue 复制代码
<script setup lang="ts">
  const emit = defineEmits(['update', 'init', 'change']);
</script>

小提示:

  • 使用运行时定义 defineEmit 可以定义 emit 的事件名称,但是不够精确

编译时定义

编译时定义(基于类型定义)可以告诉 TypeScript 需要检查 emit 调用方法时需要检查参数。

  • 直接定义
vue 复制代码
export interface IDemoEmit {
  (e: 'change', id: number): void;
  (e: 'update', value: string): void;
  (e: 'init'): void;
}
vue 复制代码
<script setup lang="ts">
  import { IDemoEmit } from '@/types';

  const emit = defineEmits<IDemoEmit>();
</script>
  • v3.3 版本以上更简明的定义方式
typescript 复制代码
const emit = defineEmits<{
  change: [id: number];
  update: [value: string];
  init: [];
}>();

总结:

  • 添加参数设置能够使自定义事件调用时更加严谨,更推荐使用第二种方式进行声明

defineExpose

defineExpose 用于组件抛出公共 API。defineExpose 可以直接定义、添加泛型后定义。

直接定义

vue 复制代码
<script>
  defineExpose({
    show() {},

    hide() {},
  });
</script>

添加泛型后定义

添加泛型后定义能够让组件外部能够更精细的进行抛出方法的调用。

typescript 复制代码
export interface IDemoExpose {
  show(): void;

  hide(): void;
}
vue 复制代码
<script>
	import { IDemoExpose } from '@/types';

  defineExpose<IDemoExpose>({
    show() {},

    hide() {},
  });
</script>
vue 复制代码
<template>
  <div class="demo-container">
		<demo3 ref="demo3Ref" />
  </div>
</template>

<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  import Demo3 from './components/Demo3.vue';
  import { IDemoExpose } from '@/types';

  const demo3Ref = ref<IDemoExpose>();

  onMounted(() => {
    demo3Ref.value?.show();
  });
</script>

总结:

  • 添加泛型后定义能新增组件的严谨性和可扩展性,更推荐使用第二种方式进行声明

非 setup 语法下的定义:

非 setup 语法需要在 defineComponent 定义组件时依次传入泛型参数:

  • 第一个参数 Props
  • 第二个参数 Emit
  • 第三个参数 Expose
  • 第四个参数 Slot

响应式 API 的定义:

ref

ref 会根据初始化时的值推导其类型:

typescript 复制代码
import { ref } from 'vue';

// 推导出的类型:Ref<number>
const year = ref(2020);

// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020';

有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref 这个类型:

typescript 复制代码
import { ref } from 'vue'
import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // 成功!

或者,在调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为:

typescript 复制代码
// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // 成功!

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:

typescript 复制代码
// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()

reactive

reactive() 也会隐式地从它的参数中推导类型:

typescript 复制代码
import { reactive } from 'vue'

// 推导得到的类型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })

要显式地标注一个 reactive 变量的类型,我们可以使用接口:

typescript 复制代码
import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 指引' })

注意:

  1. 不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
  2. 限制参数类型请在 reactive 外面定义,而不是在泛型中定义。

computed

typescript 复制代码
import { ref, computed, ComputedRef } from 'vue';

const count = ref(0);

// 类型推导
const doubleCount = computed(() => count.value * 2);

// 泛型定义
const trippleCount = computed<number>(() => count.value * 3);

// 直接定义 computed
const fourTimeCount: ComputedRef<number> = computed(() => count.value *4);

provide & inject

provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:

typescript 复制代码
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // 若提供的是非字符串值会导致错误

const foo = inject(key) // foo 的类型:string | undefined

建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。

当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:

typescript 复制代码
const foo = inject<string>('foo') // 类型:string | undefined

注意注入的值仍然可以是 undefined,因为无法保证提供者一定会在运行时 provide 这个值。

当提供了一个默认值后,这个 undefined 类型就可以被移除:

typescript 复制代码
const foo = inject<string>('foo', 'bar') // 类型:string

如果你确定该值将始终被提供,则还可以强制转换该值:

typescript 复制代码
const foo = inject('foo') as string

为模板引用标注类型

模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建:

vue 复制代码
<script setup lang="ts">
  import { ref, onMounted } from 'vue'

  const el = ref<HTMLInputElement | null>(null)

  onMounted(() => {
    el.value?.focus()
  })
</script>

<template>
  <input ref="el" />
</template>

注意

为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。

事件处理函数的定义

在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型。让我们看一下这个例子:

vue 复制代码
<script setup lang="ts">
  function handleChange(event) {
    // `event` 隐式地标注为 `any` 类型
    console.log(event.target.value)
  }
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

没有类型标注时,这个 event 参数会隐式地标注为 any 类型。这也会在 tsconfig.json 中配置了 "strict": true 或 "noImplicitAny": true 时报出一个 TS 错误。因此,建议显式地为事件处理函数的参数标注类型。此外,你在访问 event 上的属性时可能需要使用类型断言:

typescript 复制代码
function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}
相关推荐
王哲晓32 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v37 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
我血条子呢3 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
Amd7943 小时前
Nuxt.js 应用中的 prepare:types 事件钩子详解
typescript·自定义·配置·nuxt·构建·钩子·类型
麦麦大数据3 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea