携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
从Vue3发布到现在已经很久了,Vue相关的生态已逐渐形成。自从使用Vue3后写最多的就是SFC单文件组件。之前也总结过一篇关于SFC单文件组件的语法实践:Vite+Vue3.2+TS 相关API归纳总结
但是最近也是看了开源社区中许多组件库在搭建时使用的 Vue3 + TSX 这样一套语法去开发,这就让我对 TSX 的写法好奇起来。虽然再次之前很久以前使用过一段时间的 React 但写 JSX/TSX 语法依然还是有点不知所措。
所以也同样稍微体验了一下并且总结才输出着一篇文章,希望有帮助。
基本使用
JSX 是 JavaScript 的一个类似 XML 的扩展
通过Vite搭建的项目,在Vue中使用JSX语法,我们需要使用到一个插件来进行语法支持:@vitejs/plugin-vue-jsx
-
安装:
pnpm install @vitejs/plugin-vue-jsx -D -
在
vite.config.ts中配置文件
import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
export default defineConfig({
plugins: [vueJsx()],
});
- 在App.tsx中编写一个简单函数式组件
export default () => <div>Hello World</div>
使用 defineComponent 定义组件
使用defineComponent创建组件的好处在于:
- 在定义 Vue 组件时会提供类型提示的帮助函数
- 使用
defineComponent在插件@vitejs/plugin-vue-jsx则会提供HMR支持
import { defineComponent } from "vue"
export default defineComponent({
setup() {
// 需要在setup函数当中return一个渲染函数
return () => <div>使用defineComponent来定义组件</div>
}
})
语法概述
文本插值
在 JSX/TSX 中使用 {} 单括号进行包裹
export default defineComponent({
setup() {
const userName = ref("Vue3-TSX")
return () => (
<>
{/* 文本插值:在JSX中使用单括号 */}
<h3>{userName.value}</h3>
</>
)
}
})
条件渲染
可以使用 if/else 和 三目运算符来进行条件渲染
export default defineComponent({
setup() {
const randomNumber = Math.floor(Math.random() * 10)
return () => (
<>
{/* 条件渲染 */}
<h3>随机数{randomNumber}:{randomNumber > 6 ? '大于6' : '小于等于6'}</h3>
</>
)
}
})
列表渲染
列表渲染则是使用 array.map 方法
export default defineComponent({
setup() {
interface ListData {
id: string
name: string
age: number
}
const listData = ref<ListData[]>([{
id: '1',
name: '小红',
age: 20
}, {
id: '2',
name: '小王',
age: 22
}, {
id: '3',
name: '小华',
age: 25
}])
return () => (
<>
{/* 列表渲染 */}
{listData.value.map(item => {
return (
<div style="border: 1px solid #eeeeee; margin-bottom: 5px;">
<p>ID:{item.id}</p>
<p>用户名:{item.name}</p>
<p>年龄:{item.age}</p>
</div>
)
})}
</>
)
}
})
标签属性绑定
当我们给标签绑定属性,使用 {} 即可不需要给标签名添加 :
export default defineComponent({
setup() {
const githubLink = 'https://github.com/flingyp'
return () => (
<>
{/* 标签属性绑定 */}
<a href={githubLink} target="__blank">GitHub</a>
</>
)
}
})
事件绑定
绑定事件 onMethodName={}
export default defineComponent({
setup() {
const submitContent = (info: string) => {
console.log("提交的信息:", info);
}
return () => (
<>
{/* 事件绑定 */}
<button class={['custom-button']} onClick={() => submitContent('添加')}>时间绑定</button>
</>
)
}
})
v-model 数据双向绑定
第一种写法默认值 modelValue
当我们需要这样使用组件:<CustomInput v-model={inputContent.value} /> 则需要使用这种方法
export default defineComponent({
name: 'CustomInput',
props: {
modelValue: { // 默认Prop
type: String,
default: '默认内容'
}
},
emits: ['update:modelValue'], // 默认Emit
setup(props, { emit }) {
const updateInputValue = (e: any) => {
emit('update:modelValue', e.target.value)
}
return () => <input placeholder="请输入内容" value={props.modelValue} onInput={updateInputValue}></input>
}
})
第二种写法自定义名称
而当我们需要使用这种方式使用组件:<CustomInput v-model:inputContent={inputContent.value} />
export default defineComponent({
name: 'CustomInput',
props: {
inputContent: { // 自定义Props
type: String,
default: '默认内容'
}
},
// 对应的触发事件
emits: ['update:inputContent'],
setup(props, { emit }) {
const updateInputValue = (e: any) => {
emit('update:inputContent', e.target.value)
}
return () => <input placeholder="请输入内容" value={props.inputContent} onInput={updateInputValue}></input>
}
})
插槽
默认插槽
export default defineComponent({
name: "CustomSlot",
setup(props, { slots }) {
return () => (
<>
{/* 第一种写法 */}
<button>{slots.default?.() || '默认内容'}</button>
{/* 第一种写法 */}
<button>{renderSlot(slots, 'default')}</button>
</>
)
}
})
具名插槽
但我们使用具名插槽,我们的父组件可以由两种写法。
父组件的定义如下:
第一种
export default defineComponent({
name: 'App',
setup() {
return () => (
<>
<CustomSlot v-slots={{
default: () => '默认按钮',
prefix: () => '前缀',
suffix: () => '后缀'
}}>
</CustomSlot>
</>
)
}
})
第二种
export default defineComponent({
name: 'App',
setup() {
return () => (
<>
<CustomSlot>
{{
default: () => '默认按钮',
prefix: () => '前缀',
suffix: () => '后缀'
}}
</CustomSlot>
</>
)
}
})
子组件定义也有两种方式:
export default defineComponent({
name: "CustomSlot",
setup(props, { slots }) {
return () => (
<>
<button>{slots.prefix && slots.prefix()}-{slots.default && slots.default()}-{slots.suffix && slots.suffix()}</button>
</>
)
}
})
第二种写法使用 renderSlot(slot, 'slotName')
export default defineComponent({
name: "CustomSlot",
setup(props, { slots }) {
return () => (
<button>{renderSlot(slots, 'prefix')}{renderSlot(slots, 'default')}{renderSlot(slots, 'suffix')}</button>
)
}
})
作用域插槽
作用域插槽就是有时候可能需要通过子组件向父组件传递数据,则是我们就需要使用到我们的作用域插槽。其实也就是在具名插槽的基本上添加一个Prop的参数。具体使用方式如下:
// 第一种方式
export default defineComponent({
name: "CustomSlot",
setup(props, { slots }) {
return () => (
<>
<button>{slots.prefix && slots.prefix({ name: '默认前缀' })} - {slots.default && slots.default()} - {slots.suffix && slots.suffix({ name: '默认前缀' })}</button>
</>
)
}
})
// 第二种方式
export default defineComponent({
name: "CustomSlot",
setup(props, { slots }) {
return () => (
<>
<button>{renderSlot(slots, 'prefix', { name: '默认前缀' })}-{renderSlot(slots, 'default')}-{renderSlot(slots, 'suffix', { name: '默认后缀' })}</button>
</>
)
}
})
父组件
export default defineComponent({
name: 'App',
setup() {
return () => (
<CustomSlot
v-slots={{
default: () => '默认按钮',
prefix: (props: { name: string }) => <lablel>{props.name}</lablel>,
suffix: (props: { name: string }) => <lablel>{props.name}</lablel>,
}}>
</CustomSlot>
)
}
})
简单总结
从SFC单文件组件的写法再到JSX/TSX的写法需要一段时间的适应。使用 JSX/TSX 写法则更加倾向于编写原生JS,在操作DOM和渲染会变得更加灵活,使用门槛相对较高。
而SFC单文件组件使用起来则相对简单但可能才操作DOM和渲染方面没有其灵活性。