第一章:什么是Vue Macros ?
Vue Macros 是一个实现了一些尚未被 Vue 官方实现的提案或想法的库。这意味着它将探索更多的Features和语法糖以增强 Vue 的功能。
Vue Macros 的核心开发团队由 Kevin Deng 主导,团队成员还包括 Alex 和 zhiyuanzmj 等人 (Vue Macros) (GitHub),官方团队参与项目贡献并密切合作,可以说是 Vue 的前瞻版本。
比如 在vue3.3/3.4 + 版本加入**defineOptions()
和 defineModel()
** 的就最早诞生于Vue Macros
第二章 Vue-macros JSX/TSX 实验性新特性
在这一章节,主要介绍 Vue-macros 的三个实验性新特性:defineRender
、setupComponent
和 setupSFC
。
DefineRender
defineRender
是一个让我们可以更简洁地定义组件渲染逻辑的特性。它为 JSX/TSX 语法提供了更好的支持,使得编写渲染函数变得更加直观和简洁。
传统的渲染函数定义方式:
ts
export default {
render() {
return <div>Hello, Vue!</div>;
},
};
使用 DefineRender 的方式:
ts
<script setup lang="tsx">
// JSX passed directly
defineRender(
<div>
<span>Hello</span>
</div>,
)
// Or using render function
defineRender(() => {
return (
<div>
<h1>Hello World</h1>
</div>
)
})
</script>
SetupComponent
setupComponent
是 Vue Macros 提供的一种实验性功能。该功能通过更简洁的语法,使开发者能够更加专注于业务逻辑而不是配置细节。以下是结合官方文档对 setupComponent
的详细介绍。
基本用法
使用 setupComponent
可以像下面这样定义组件:
ts
export const App = defineSetupComponent(() => {
defineProps<{
foo: string
}>()
defineEmits<{
(evt: 'change'): void
}>()
defineOptions({
name: 'App',
})
return <div />
})
特点:
根据github及官网的总结,大概有以下三个特点(→ 在我看来就是更像React了):
-
简化语法,专注业务逻辑:
setupComponent
通过简化组件setup
函数的定义,使开发者能够更加专注于业务逻辑而不是配置细节。例如,使用setupComponent
定义组件的方式更加简洁,使得代码更加易读和易维护 (Vue Macros) (Vue Macros)。
-
实验性功能,集成度高:
setupComponent
是一种实验性功能,使用时需要谨慎。它与defineProps
、defineEmits
和defineOptions
等功能结合使用,提供了一个统一的接口来定义组件的属性、事件和选项 (Vue Macros) (GitHub)。虽然目前 TypeScript 支持尚未完成,但这种集成度高的设计使得组件定义更加一致和规范。
-
高效的类型支持:
setupComponent
支持 TypeScript 类型注解,使开发者能够在开发过程中获得更好的类型检查和自动补全支持。这一特性在开发大型项目时尤为重要,可以有效减少因类型错误导致的 bug (GitHub) (Vue Macros)。不过需要注意的是,当前的 TypeScript 支持还在完善中,部分功能可能存在已知问题 (Vue Macros)。
SetupSFC
setupSFC
是一个专门为单文件组件(SFC)工具。更重要的是在setupSFC 的模式下,无需在包裹 defineSetupComponent 这个函数了,甚至可以直接写在 tsx 文件中,来编写一个 SFC。
安装指路:
ts
// vite.config.ts
import VueMacros from 'unplugin-vue-macros/vite'
import Vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
VueMacros({
plugins: {
vue: Vue({
include: [/\.vue$/, /\.setup\.[cm]?[jt]sx?$/],
// ⬆️ 需要添加 setup 模式
}),
},
}),
],
})
使用 SetupSFC 的方式:
ts
// Foo.setup.tsx
defineProps<{
foo: string
}>()
defineEmits<{
(evt: 'change'): void
}>()
export default () => (
<div>
<h1>Hello World</h1>
</div>
)
第三章:效率性新特性ChainCall
在这个章节,主要介绍 Vue-macros 的一个效率性新特性:chainCall
。这一特性旨在通过简化代码结构和提高开发效率,进一步增强 Vue 的功能,也可能在后几个版本加入Vue官方版。
官方调用方式
ts
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
使用 ChainCall 的方式
ts
<script setup lang="ts">
const props = defineProps<{
foo?: string
bar?: number[]
baz?: boolean
}>().withDefaults({
foo: '111',
bar: () => [1, 2, 3],
})
</script>
编译后:
ts
<script setup lang="ts">
const props = withDefaults(
defineProps<{
foo?: string
bar?: number[]
baz?: boolean
}>(),
{
foo: '111',
bar: () => [1, 2, 3],
},
)
</script>
优势
1. 代码简洁度
使用 chainCall
可以将定义属性和设置默认值的过程简化为一行代码,这比官方方式更加简洁:
- 官方调用方式 :需要显式调用
withDefaults
并传递defineProps
结果,显得冗长。 - ChainCall 方式 :通过链式调用,将
withDefaults
和defineProps
合并,减少代码量。
2. 可读性提高
链式调用的方式使代码结构更加直观,易于理解和维护:
- 官方调用方式:两步调用可能会让初学者感到困惑,需要额外的注释来解释。
- ChainCall 方式:一步到位,清晰明了。
3. 类型安全
chainCall
完全支持 TypeScript,可以在编译时检查类型,确保代码的可靠性:
- 官方调用方式:需要在两个函数调用间传递类型信息,可能导致类型丢失或错误。
- ChainCall 方式:在链式调用中保持类型信息的一致性,避免类型错误。
第四章:vue v3.4 / v3.3 和新特性
defineModel()
这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model
来使用。组件 v-model
指南中也讨论了示例用法。
在底层,这个宏声明了一个 model prop 和一个相应的值更新事件。如果第一个参数是一个字符串字面量,它将被用作 prop 名称;否则,prop 名称将默认为 "modelValue"
。在这两种情况下,你都可以再传递一个额外的对象,它可以包含 prop 的选项和 model ref 的值转换选项。
ts
// 声明 "modelValue" prop,由父组件通过 v-model 使用
const model = defineModel()
// 或者:声明带选项的 "modelValue" prop
const model = defineModel({ type: String })
// 在被修改时,触发 "update:modelValue" 事件
model.value = "hello"
// 声明 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel("count")
// 或者:声明带选项的 "count" prop
const count = defineModel("count", { type: Number, default: 0 })
function inc() {
// 在被修改时,触发 "update:count" 事件
count.value++
}
defineModel()
宏简化了组件中的双向数据绑定,使代码更加简洁高效。在之前的版本中,实现双向绑定需要定义传入数据的 props (modelValue
) 并发出事件 (update:modelValue
) 来更新父组件,这种方式既冗长又容易出错。
可以通过这个例子对比:
在 Vue 3.4 之前,实现双向数据绑定通常需要以下步骤:
ts
<!-- 父组件 -->
<template>
<ChildComponent v-model="username" />
</template>
<script setup>
import { ref } from 'vue';
const username = ref('');
</script>
<!-- 子组件 -->
<template>
<input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue']);
</script>
使用 defineModel()
的简化版本
ts
<!-- 父组件 -->
<template>
<ChildComponent v-model="username" />
</template>
<script setup>
import { ref } from 'vue';
const username = ref('');
</script>
<!-- 子组件 -->
<template>
<input type="text" v-model="model" />
</template>
<script setup>
import { defineModel } from 'vue';
const model = defineModel('modelValue', { required: true });
</script>
defineOptions()
以前的方式:混合使用 <script>
和 <script setup>
defineOptions()
在 Vue 3.3 中被引入,目的是为了解决在使用 <script setup>
时定义组件选项(如 name
、inheritAttrs
等)的繁琐问题。以前,这些选项只能在常规的 <script>
块中定义,这导致了在使用 <script setup>
时的割裂体验
在 Vue 3.3 之前,如果你想使用 <script setup>
并定义组件的选项(如 name
和 inheritAttrs
),你必须在常规的 <script>
块中进行定义。这导致了在同一个文件中混合使用两种脚本标签,造成代码分裂和管理上的不便。
ts
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<!-- 常规的 <script> 块 -->
<script>
export default {
name: 'ExampleComponent',
inheritAttrs: false,
};
</script>
<!-- <script setup> 块 -->
<script setup>
const message = 'Hello, Vue 3!';
</script>
在这个示例中,我们需要使用两个 <script>
块来分别定义组件选项和组件的逻辑,这使得代码显得分裂且不直观。
用 defineOptions()
的新方式
在 Vue 3.3 中,引入了 defineOptions()
,使得我们可以在 <script setup>
中直接定义组件的选项,从而避免了这种分裂:
ts
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<!-- 统一的 <script setup> 块 -->
<script setup>
defineOptions({
name: 'ExampleComponent',
inheritAttrs: false
});
const message = 'Hello, Vue 3.3!';
</script>
这个宏可以用来直接在 <script setup>
中声明组件选项,而不必使用单独的 <script>
块:
ts
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
}
})
</script>
- 仅支持 Vue 3.3+。
- 这是一个宏定义,选项将会被提升到模块作用域中,无法访问
<script setup>
中不是字面常数的局部变量。
Vue 3.4 的 JSX/TSX 相关更新
Vue 3.4 在 JSX/TSX 使用方面进行了显著更新,主要包括:
- 全局 JSX 命名空间的移除 :为了避免与 React 的全局命名空间冲突,Vue 不再默认注册全局
JSX
命名空间。开发者需要在tsconfig.json
中明确指定jsxImportSource
为vue
(Vue.js Blog) (Vue.js)。 - 更高效的编译和性能 :Vue 3.4 引入了新的模板解析器,显著提升了 SFC(单文件组件)的编译性能,这也包括对 JSX 的处理 (Vue.js Blog)。
- 定义模型的稳定性 :
defineModel
宏在 3.4 中变得稳定,使得使用v-model
的组件实现更加简洁 (Vue.js Blog)。
JSX/TSX 使用指南
对于使用 JSX/TSX 的开发者,Vue 提供了详细的配置指南:
- JSX 支持配置 :
create-vue
和 Vue CLI 都提供了预配置 JSX 支持的选项。手动配置时,可以参考@vue/babel-plugin-jsx
的文档 (Vue.js)。 - 类型推断和配置 :确保在
tsconfig.json
中设置"jsx": "preserve"
,并在需要的文件顶部添加/* @jsxImportSource vue */
注释,以启用 Vue 的 JSX 类型定义 (Vue.js)。
第五章:总结 & 对 Vue3 发展趋势的影响
Vue3 的发展趋势:JSX/TSX 是未来吗 🤔️?
随着 Vue3 的不断发展,特别是在 3.3 和 3.4 版本的更新中,JSX 和 TSX 的使用已经变得越来越普遍。这两种语法为开发者提供了更灵活、更高效的方式来编写 Vue 组件。Vue 3.4 尤其在性能和开发体验上做了大量优化,显著提升了模板解析器和响应性系统的效率,也对 JSX/TSX 这种写法支持度更高。
现在的模板语法与 JSX/TSX 互有优劣
模板语法:
- 简单易懂:模板语法直观且易于上手,开箱即用,可以直观地描述UI结构,减少了JavaScript代码的混杂。
- 快速开发:由于其直观的语法和框架内置的功能,模板语法适用于快速原型开发
- 丰富的工具链支持:Vue 提供了强大的模板编译和优化工具,可以在编译时进行很多优化。
JSX/TSX :
- 强大的灵活性:JSX/TSX允许在JavaScript中直接嵌入HTML样式的语法,提供了极大的灵活性来构建动态UI ,但由于JSX/TSX的高度灵活性,可能导致代码组织不当,增加维护难度 。
- 更好的类型支持:TSX 结合 TypeScript 使用,可以在编译时进行类型检查,减少运行时错误,虽然模板语法也可以使用TypeScript,但由于其设计上的限制,静态分析和类型检查的效果可能不如TSX来得全面和高效 。
总结 & 未来趋势
根据 Vue 3.4 和 Vue Macros的更新,可以看出 Vue 官方和社区都在大力支持和优化 JSX/TSX 的使用。比如3.4 全局 JSX 命名空间的移除是为了避免与 React 的命名空间冲突,使得 Vue 和 React 可以在同一个项目中共存,表示 Vue 在逐渐走向与 JSX/TSX 更加兼容和优化的方向。新的模板解析器和优化的响应性系统也使得使用 JSX/TSX 编写的组件在性能上得到了很大提升。
但是Vue未来真的会全面拥抱JSX/TSX吗?我个人看法其实是不会🤔️,虽然Vue 3.4 的更新已经为 JSX/TSX 提供了更好的支持,但总体上Vue官方对于JSX/TSX的进一步适配更新还是保持克制的,并且JSX/TSX本身也存在 学习曲线陡峭/复杂度增加/依赖构建工具 的问题,这些问题与 Vue 的开箱即用理念有所不同,所以个人还是更倾向于 Vue 长期会保持JSX/TSX与模板语法的共存状态。但无论未来如何发展,持续关注行业动向还是非常重要的~
参考链接
- vue-macros.dev/guide/getti...
- vuejs.org/
- github.com/vue-macros/...
- juejin.cn/post/735870...
- Vue 3.4 发布公告 (Vue.js Blog)
- Vue 3.3 发布公告 (Vue.js Blog)
- Vue 官方文档 JSX 使用指南 (Vue.js)