vue 的响应式数据 + jsx 开发体验简直不要太好,心智负担确实小,简直完爆 react(纯属个人暴论),不足的地方就是生态了,这点确实比不过 react。
本文更侧重于 TS 类型的写法,毕竟初次接触 vue jsx 时,实在对其 TS 类型声明很不顺手。要说 vue 模板语法哪些 API 不能在 jsx 中使用,也就是一些 define 宏函数了,其它都正常使用。
v-model 双向绑定
注意:在 jsx 场景下,仍然可以使用 v-model
,但是不能使用 v-model.lazy
等修饰符
网上有使用
value
+event
事件的写法,都可以,论简单还是v-model
template 写法
<script setup lang="ts">
const inputVal = ref('');
</script>
<template>
<input v-model="inputVal" />
<div>{{ inputVal }}</div>
</template>
JSX 写法:ref 响应式值必须使用 .value
访问,这点和模板语法的 script 标签中使用规则一致
export default defineComponent({
name: 'ChildJSXComp',
setup() {
const inputVal = ref('');
return () => (
<>
<input v-model={inputVal.value} />
<div>{inputVal.value}</div>
</>
);
},
});
Props
基础例子
最基础的例子,子组件接收一个 message
属性,并限制类型为 string | number
template 写法
<script setup lang="ts">
interface MyProps {
message?: string | number;
}
const props = defineProps<MyProps>();
</script>
<template>
{{ props.message }}
</template>
jsx 写法:使用 PropType
可以对入参类型做进一步的类型约束
export default defineComponent({
name: 'ChildJSXComp',
props: {
message: { type: [String, Number] as PropType<string | number> },
},
setup(props) {
return () => <>{props.message}</>;
},
});
PropType
- 在 vue 模板的 setup 语法糖写法下,props 接收直接使用
const props = defineProps<MyProps>()
定义即可 - 在 jsx 中,不能直接定义 TS 类型,需要使用 类型标注 +
PropType
实现。
PropType
在 JSX 中可以对 props 的接收类型做更细致的类型约束
const selectorProps = {
// 细化 Array 类型为 SelectorOption[]
options: { type: Array as PropType<SelectorOption[]>, default: () => [] },
// 细化 Object 为 { label: string; value: string }
fieldNames: {
type: Object as PropType<{ label: string; value: string }>,
default: () => ({ label: 'label', value: 'value' }),
},
// 细化 Fuction 类型为 (value: string) => void
onSearch: { type: Function as PropType<(value: string) => void> },
}
export default defineComponent({
// ...
props: selectorProps,
setup(props) {
// ...
},
});
根据 props 的类型标注生成 TS 类型
之前使用的 props 标注是属于参数校验,是 Schema 数据,而非 TS。
如果其它组件基于此组件进行二次封装,需要该组件的 TS 类型,此时可以使用 ExtractPublicPropTypes
来将 props 类型标注转为 TS 类型,这样其它组件就可以使用了。
对于 vue 版本低于 3.3 的,可以使用 ExtractPropTypes
,个人感觉两者的关系就是 type ExtractPublicPropTypes = Patrial<ExtractPropTypes>
props 传递事件
个人更推荐使用 props 来传递事件,而非 emits
属性接收,原因如下:
- emits 接收事件在 eslint 下表现并不友好(不禁用某些规则的前提下)
- emits 接收事件,最终也可以合并到 props 上,通过 props 使用
对于第一点,下一小节会讲 emits 的类型定义,对于第二点,如图所示:
虽然是在 emits
属性上定义的事件,但是 props 依然可以访问,那为什么不直接在 props 上定义呢? ant-design-vue
就是把事件写在了 props 中,但是也有不少组件库依然写在 emits
里,看个人习惯吧。
可能是为了区分,事件是事件(emits),属性是属性(props),不让属性和事件混在一起,从代码的结构上来说更严谨。
emits 事件接收
template 写法,使用 defineEmits
<script setup lang="ts">
interface SelectorEmits {
(e: 'cancel'): void;
(e: 'search', value: string): void;
}
// 3.3 + 提供的事件类型简写
// interface SelectorEmits {
// cancel: [];
// select: [value: string];
// }
const emit = defineEmits<SelectorEmits>();
const clickButton = () => {
emit('search', '1');
};
</script>
<template>
<button @click="clickButton"></button>
</template>
jsx 写法,使用 emits
emits
以字符串数组定义事件,则无法对事件进行详细的类型约束
export default defineComponent({
// ...
emits: ['search', 'cancel'],
setup(props, { emit }) {
const clickButton = () => {
emit('search', "1");
};
return () => <button onClick={clickButton}>Test</button>;
},
});
emits
标注 TS 类型,将emits
换成对象写法
emits: {
search: (value: string) => true,
cancel: () => true,
},
这种写法在 eslint 中会出现一些问题,例如 search
事件,会报 “声明值未使用” 的问题,所以我推荐在 props 中定义事件,当然也可以全局禁用该规则,就不会出现这个报错问题。
slot 插槽
在 jsx 中写插槽时,没有类型提示,想要存在类型提示,只能手动标注类型
子组件接收插槽(默认插槽和传参插槽)
template 写法,使用 defineSlots
对插槽的类型进行标注,同时 IDE 也有友好的类型提示。
<script setup lang="ts">
import type { VNode } from 'vue';
interface MySlots {
default?(): VNode;
list?(scoped: { list: string[] }): VNode;
}
defineSlots<MySlots>();
</script>
<template>
<header>Header</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="list" :list="['1', '2', '3', '4']"></slot>
</footer>
</template>
jsx 写法,使用 slots
属性接收,所有插槽都是以函数调用方式渲染
slots 的 TS 写法和 props 类似,都是通过 as
来进一步约束类型
export default defineComponent({
name: 'ChildComp',
slots: Object as SlotsType<MySlots>,
setup(props, { slots }) {
return () => (
<>
<header>Header</header>
<main>{slots.default?.()}</main>
<footer>{slots.list?.({ list: ['1', '2', '3', '4'] })}</footer>
</>
);
},
});
父组件使用插槽(默认插槽和传参插槽)
template 写法
<ChildComp>
<template #default> Parnt Content </template>
<template #list="scoped">
<p v-for="item in scoped.list" :key="item">{{ item }}</p>
</template>
</ChildComp>
使用 defineSlots
的好处就是拥有更完整、更友好的 TS 类型提示。
jsx 有两种使用插槽的写法
写法一: 使用 v-slots
export default defineComponent({
name: 'ParentComp',
setup() {
return () => (
<ChildJSXComp
v-slots={{
default: () => <div>Parnt Content</div>,
list: (scoped: { list: string[] }) => {
return scoped.list.map((item) => <p key={item}>{item}</p>);
},
}}
/>
);
},
});
v-slots
接收的是一个对象,对象的 key 就是插槽名,value 就是一个函数。
这也就是为什么在 jsx 子组件使用插槽是slots.default?.()
的原因:因为传入的插槽就是函数,调用函数才能得到 VNode
写法二: 组件标签内写插槽,用闭合标签的内容来代替 v-slots
,其实就是把 v-slots
的内容粘贴到闭合标签中
<ChildJSXComp>
{{
default: () => <div>Parnt Content</div>,
list: (scoped: { list: string[] }) => {
return scoped.list.map((item) => <p key={item}>{item}</p>);
},
}}
</ChildJSXComp>
如果只传递默认插槽,也可以用最常规的写法,闭合标签内写 dom 元素
<ChildJSXComp>
<div>Parnt Content</div>,
</ChildJSXComp>
v-bind 动态绑定多个值
先说结论:多个值的绑定情况下,vue 的 v-bind="myObject"
= jsx 的 {...myObject}
这里就以 useuse 的 useVirtualList
为例
以上的 v-bind
写法换成 jsx 就如下所示
由于 wrapperProps
是一个 computed
计算结果,所以在使用时加 .value
其它
组件别名命名
注意:该命名是在 vue devtool 中查看使用的,而非代码中组件的引用别名
在 vue 3.3+ 之后,官方提供了 defineOptions
来进行组件的别名命名(默认文件名为组件名),方便 vue devtools 中调试查看,毕竟没人愿意打开调试工具,一看组件标签名都是 Index
。
template 写法
<script setup lang="ts">
defineOptions({ name: 'MyCompentName' });
</script>
// vue 3.3 之前需要额外的 script 标签定义
<script>
export default {
name: "MyCompentName"
}
</script>
jsx 写法
export default defineComponent({
name: 'MyCompentName',
});
标签:vue,插槽,jsx,TS,props,写法,emits
From: https://www.cnblogs.com/jsonq/p/18686890