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