一般而言,我们在用Vue的时候,都是使用模板进行开发,但其实Vue 中也是支持使用jsx 或 tsx的。 最近我研究了一下如何在项目中混合使用二者,并且探索出了一些模式, 本文就是我在这种开发模式下的一些总结和思考,希望能帮助到大家。
由于tsx 就是ts版本的 jsx,二者基本上可以认作一个东西,所以下文使用tsx的地方,一般而言对于jsx也同样适用。
通常我们都是通过模板来定义我们的ui,这是官方推荐的编写方式,模板开发有很多优点,比如:
模板开发提供了指令、事件修饰符等功能,方便我们复用一些逻辑。 特别是 css scoped 方案,个人觉得比 css module 和 css in js 方案更直观好用。
然而纯模板开发也有一些缺点,比如:
上面我们说到模板不够灵活,而由于这方面恰好是 tsx 的长处,借助一些工具如vite-plugin-jsx 的帮助,我们可以很方便地在 vue3 中使用 tsx 来开发,下面是一个在vue3中使用tsx开发的例子。
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "TsxDemo",
props: {
msg: {
type: String,
required: true,
default: "hello wrold",
},
},
setup(props) {
const count = ref(0);
return () => (
<div>
<header>{props.msg}</header>
<main>{count.value}</main>
</div>
);
},
});
vue 中使用 tsx 开发还有一些需要注意的点,但由于本文重点不在于教大家tsx基础,所以我们这里按下不表,这里贴一个链接方便大家学习(Vue3 + TSX 最佳实践?不存在的 - 掘金 (juejin.cn))
tsx的优点:
tsx 的缺点
由于 tsx 开发模式不是vue官方推荐的开发模式,没法使用一些内置的功能,但我觉得最遗憾的是没法使用.vue单文件组件提供的 css scoped 这种css方案,个人觉得该方案相较于css modules 和 css in js方案更加简单易用。 另外一个比较大的问题还在于,没法使用defineProps defineEmits这些setup script 语法糖,导致在写 ts 类型时,只能使用基于运行时的推导方案,我们看下面个例子。
export default defineComponent({
props: {
count: {
type: Number,
required: true,
},
person: {
type: Object as PropType<{ name: string }>,
},
color: {
type: String as PropType<"success" | "error" | "primary">,
required: true,
},
},
setup() {
return () => <div>demo</div>;
},
});
在这里,我们写props的定义的时,很多情况下需要依赖 as PropType<xxx> 来帮我们推断出更精确的类型,而在setup script中我们可以使用基于ts的类型方案,这种方式显然会更加地友好。
<script setup lang="tsx">
// 基于ts的类型推断
const props = defineProps<{
count: number;
person?: {
name: string;
};
color?: "success" | "error" | "primary";
}>();
</script>
既然模板开发和tsx开发都有各有各的优缺点,那么有没有什么办法可以将他们的优点组合一下呢,答案即是我们今天要讨论的setup script + tsx 混合编程模式。
那么如何开启 setup script + tsx 混合编程模式呢?
很简单将lang改为tsx 就可以
<script lang="tsx" setup>
</script>
首先我们按最常规的方法,定义一个子组件,并且渲染到父组件中:
<template>
<div>
<Demo msg="hello world" />
</div>
</template>
<script lang="tsx" setup>
import { defineComponent } from "vue";
const Demo = defineComponent({
props: {
msg: String,
},
setup(props) {
return () => (
<div>
<div>msg is {props.msg}</div>
</div>
);
},
});
</script>
这就是最初始的状态,我们将在.tsx 中写组件的方式搬到了 setup script 中, Demo组件接受一个类型为string的属性msg,这段代码在浏览器中最终会渲染成
现在我们它加上样式,由于我们是在.vue文件中编写 Demo 组件,所以我们可以运用单文件内置的css方案。 .vue 文件中支持 css module 和 css scoped 方案,这两种方式都可以用,我们先看 css module方案
<template>
<div>
<Demo msg="hello world" />
</div>
</template>
<script lang="tsx" setup>
import { defineComponent, useCssModule } from "vue";
const styles = useCssModule();
const Demo = defineComponent({
props: {
msg: String,
},
setup(props) {
return () => (
<div class={styles.wrapper}>
<div class={styles.inner}>msg is {props.msg}</div>
</div>
);
},
});
</script>
<style lang="less" module>
.wrapper {
.inner {
color: red;
}
}
</style>
可以看到,完美生效,我们再来看一下 css scoped方案:
<template>
<div>
<Demo msg="hello world" />
</div>
</template>
<script lang="tsx" setup>
import { defineComponent } from "vue";
const Demo = defineComponent({
props: {
msg: String,
},
setup(props) {
return () => (
<div class="wrapper">
<div class="inner">msg is {props.msg}</div>
</div>
);
},
});
</script>
<style lang="less" scoped>
.wrapper {
.inner {
color: red;
}
}
</style>
可以看到,并没有生效,这是因为Demo是一个子组件,而scoped方案不会透传到子组件中dom中,所以这里我们得用:deep处理下:
<style lang="less" scoped>
:deep(.wrapper) {
.inner {
color: red;
}
}
</style>
再刷新下浏览器就可以看到css 生效了。
到这一步,通过用:deep做一下特殊处理,我们可以实现在 vue 中使用css scoped方案了。 那,能不能连 :deep都不写呢? 我们继续研究下。
这里之所以需要:deep 特殊处理的原因在于Demo 是一个组件,而css scoped默认不会透传到子组件中,那么如何去规避这个问题呢?
其实,Demo组件本质上是要实现一个生成一棵vnode 树,而这可以通过函数去生成:
const renderDemo = (props:{msg:string}) => {
<div class="wrapper">
<div class="inner">msg is {props.msg}</div>
</div>
}
现在我们需要将这个函数生成的 vnode 插入到模板中。
在tsx 中,我们经常可以看到这样的写法
setup() {
const header = <header>this is header</header>;
const renderMain = () => <main>this is main</main>;
const renderFooter = () => <footer>this is footer</footer>;
return () => (
<div>
{header}
{renderMain()}
{Math.random() > 0.5 ? renderFooter() : null}
</div>
);
},
我们将组件的各个部分通过拆分成了一个个子单元,它可以是一个单独的vnode,也可以是一个渲染函数,然后通过 {} 表达式嵌入主单元。那么,在混编模式下我们能不能这么做呢?
模板中有{{}}表达式,也是接受动态的内容,顺着这个思路,我们可以写出这样的代码:
<template>
<div>
{{ header }}
{{ renderMain() }}
{{ Math.random() > 0.5 ? renderFooter() : null }}
</div>
</template>
<script lang="tsx" setup>
const header = <header>this is header</header>;
const renderMain = () => <main>this is main</main>;
const renderFooter = () => <footer>this is footer</footer>;
</script>
然而,渲染出来的结果却是会报错.
原因是{{}} 与 {} 不同,其实是用来渲染动态字符串的,这里传vnode肯定是不行的,那么这里该怎么做呢?
答案是<component :is='xxx'>
<component :is='xxx'> 这里传的is既支持传组件,也支持传vnode,所以我们可以这样写。
<template>
<div>
<component :is="header"></component>
<component :is="renderMain()"></component>
<component :is="Math.random() > 0.5 ? renderFooter() : null"></component>
</div>
</template>
<script lang="tsx" setup>
const header = <header>this is header</header>;
const renderMain = () => <main>this is main</main>;
const renderFooter = () => <footer>this is footer</footer>;
</script>
渲染也是正常的
所以,之前的Demo也可以这样写
<template>
<div>
<component :is="renderDemo('hello world')"></component>
</div>
</template>
<script lang="tsx" setup>
const renderDemo = (msg: string) => (
<div>
<div> msg is {msg}</div>
</div>
);
</script>
之前我们的写法是通过一个将这个片段拆分为一个组件,导致我们在用css scoped 方案的时候,必须要用 :deep() 去透传一下,而这种写法支持渲染一个vnode,所以没有这个限制,也就是说我们写css 可以按照正常的写法去写,即:
<template>
<div>
<component :is="renderDemo('hello world')"></component>
</div>
</template>
<script lang="tsx" setup>
const renderDemo = (msg: string) => (
<div class="wrapper">
<div class="inner"> msg is {msg}</div>
</div>
);
</script>
<style lang="less" scoped>
.wrapper {
.inner {
color: red;
}
}
</style>
而渲染出的组件也是正常的
到此为止我们已经能将 tsx 和 css scoped 完美结合起来了,那还能更优化吗?
我们来看下面这个计数器 demo:
// Counter.vue
<template>
<component :is="render()"></component>
</template>
<script setup lang="tsx">
const props = defineProps<{
count: number;
}>();
const emits = defineEmits(["update:count"]);
const render = () => {
return (
<div>
<div class="content">count is {props.count}</div>
<div>
<button
onClick={() => {
emits("update:count", props.count + 1);
}}
>
add
</button>
</div>
</div>
);
};
</script>
<style lang="less" scoped>
.content {
color: red;
}
</style>
// Parent.vue
<template>
<Counter v-model:count="count" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import Counter from "./views/Counter.vue";
const count = ref(0);
</script>
我们直接将所有的渲染逻辑抽象为一个 render 函数,然后在模板部分,只用
<template>
<component :is="render()"></component>
</template>
这样写, 这就相当于我们完全实现了在template setup 模式中,完全使用tsx 去开发, 使得tsx 可以使用 defineProps defineEmits 等宏语法的支持;从渲染结果可以看出,点击逻辑可以生效,在我们设置的样式也不需要 :deep 加持就能作用到dom中;
这就是终极黑魔法!
上面啰啰嗦嗦说了一大堆,其实总结起来就是
这是我们常用的混合使用setup template 和tsx 的方式,既能实现 vnode 的复用,又可以完美地和 css scoped结合,推荐大家在一些需要灵活的场景下使用这种模。
原文链接:
https://juejin.cn/post/7282692088016437307