⚠️ Vue 3 TSX
本篇基于 Vue 3.0.7, vite 2.1.2 编写,由于 Vue 与 vite 改动较大,以最新版本为准,本文仅供参考。
写本篇文章主要是为了记录在正式使用 Vue 3 + vite 2 投入开发中遇到的一些问题,不适合没有任何 Vue 开发经验的同学阅读。本文中将会运用到 Vue 3 的 Composition api,vue-router@next。
首先,我的项目是基于 vite 2 架基的,同时使用了 PostCSS + Tailwind 2 CSS。UI Framework 使用了国外的 PrimeVue。初始化的过程不再讲述了。
Router 与 TSX
首先是,Vue Router 的使用。和 Vue 2 的 Router 并没有什么比较大的区别。
不同的是,Vue Router 3 使用 VueRouter 的默认导出来创建一个实例,而 Vue Router 4 使用 createRouter
来创建实例。与 Vue 一致,Vue Router 也摒弃了 class 的写法,全面转向函数式编程(Functional Programming)。(注:Vue 2 使用 Vue Router 3, Vue 3 使用 Vue Router 4)
// Vue router 3
import VueRouter from 'vue-router'
const router = new VueRouter({
routes,
})
// Vue router 4
import {
createRouter,
createWebHashHistory,
} from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes,
})
Vue Router 支持给每个路由(Route)添加一个 Meta 对象,存储在 Meta 对象中的值会在当前 Router 实例中取得。同时,Vue 3 原生支持了 JSX(大概只是比上代好一点点???),为此,我们也可以像 React 那样操作。
比如,我使用 Routes 来构建一个侧边菜单,当然为了简单,侧边菜单至多只有两层。
interface MenuModel {
title: string
path: string
icon: any
subItems?: Array<MenuModel>
hasParent: boolean
fullPath: string
query?: any
}
以上是我的菜单需要的结构,而如何将 icon 存储下来,在 render 函数直接使用呢。如果是 React,我们可以这样写。
icon: JSX.Element
然后直接使用 {menu.icon}
就行了。在 Vue3 中,如果使用 JSX,同样可以这样操作。在 Routes 中稍加修改。在 Meta 中,直接传入一个 JSX.Element
。
import dashboardFilled from '@iconify-icons/ant-design/dashboard-filled'
import { InlineIcon as Icon } from '@iconify/vue'
const routeForMenu: Array<RouteRecordNormalized> = [
{
path: '/dashboard',
component: DashBoardView,
name: 'dashboard',
meta: { title: '仪表盘', icon: <Icon icon={dashboardFilled} /> },
},
// ...
]
同样的在 Sidebar.tsx 中。
export const Sidebar = defineComponent({
setup() {
return () => <div>
// ....
{menu.icon}
</div>
}
})
当然这种用法只限于 JSX/TSX。使用 Vue 的模板的话,就会渲染一个 VNode 对象了。
因为 JSX.Element 只是一个 Object,在 Vue 模板中只会判断 components 注册了没有,而不会关心这个 Object 是不是 VNode。而 JSX 中则会去判断是 VNode 则 render。
如果想在 Vue 模板中使用外部的 JSX,那么就需要去 components 注册一下就行了。
// icon.tsx
export const Icon = <FontAwesomeIcon icon={faAlignJustify} />
<template>
<Icon />
</template>
<script lang="ts">
import { Icon } from './icon'
import { defineComponent } from 'vue'
export default defineComponent({
components: { Icon },
})
</script>
Setup 与 TSX
在 Vue 2 中,data 中的属性以 _
和 $
打头的会被忽略,从而无法使用响应式流。在 Vue 3 中,data 还是和 Vue 2 一样无法使用,在 setup 函数中亦如此。但是官网文档没写不让用。
<template>
<div class="w-10 m-auto">
<p>
<p>{{ $$a ?? 0}}</p>
<Button @click="$$a++">$$a++</Button>
</p>
<p>
<p>{{ a }}</p>
<Button @click="a++">a++</Button>
</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Button from 'primevue/button'
export default defineComponent({
components: { Button },
setup() {
const $$a = ref(0)
const $a = ref(0)
const a = ref(0)
return {
$$a,
a,
}
},
})
</script>
但在 TSX 中,你完全可以这样写。
export const PlaceHolderView = defineComponent({
setup() {
const router = useRouter()
const $$a = ref(0)
return () => (
<p>
{$$a.value}
<p>
<Button onClick={() => $$a.value++}>$$a++</Button>
</p>
</p>
)
},
})
Slot 与 TSX
在 Vue 中有个 v-slot 的东西,在 React 中则有个 Children,当然 Vue 的 slots 做的比 React 多得多。而在 React 中除了传递 Children,还可以通过 props 传递 React.reactElement。React 更加灵活。
在 Vue 3 TSX 写法中,v-slots.default
等于 React 的 children。
const Children = defineComponent({
setup() {
return () => <p>Children</p>
},
})
const Parent = defineComponent({
setup({}, { slots }) {
return () => (
<div class="">
<p>Parent</p>
{slots.default?.()}
</div>
)
},
})
const RootView = defineComponent({
setup() {
return () => (
<p>
<Parent>
<Children></Children>
</Parent>
</p>
)
},
})
slots 位于 setup 的第二个参数中,获取当前组件所有的 slots,并且是一个函数,需要 call 一下。
<Parent v-slots={{default: () => <Children />}}></Parent>
也可以用上面的方式传入。
v-slots 对 TSX 的方式不太友好,建议还是使用 React 的方式编写。通过传递 Props 来渲染子组件。
Emit 与 TSX
在 Vue 模板中,我们会用 @
去监听一个事件。在 React 的 JSX 中用 on 前缀来监听一个事件,如果是自定义事件,一般会定义一个新的函数。而在 Vue 中使用 emit
函数去发起一个事件。但是在 TSX 如何去监听呢。答案也是 on,你甚至可以不用手写一个函数。
const Parent = defineComponent({
setup({}, { slots, emit }) {
onMounted(() => {
emit('mount', 1)
})
return () => (
null
)
},
})
const RootView = defineComponent({
setup({}, ctx) {
return () => (
<Parent
onMount={(val) => {
console.log('mount', val)
}}
>
</Parent>
)
},
})
显然,onMount 这个 Props 是不存在的,我们也没有定义,但是在 Parent 中 emit 的事件为 mount
。就得到了这个 Props。这个过程是发生在编译阶段的,所以在不同的架手架行为可能不同。甚至说随时可能 breaking change,对 ts 的支持也很不友好,充满了红线。所以不建议使用。
以上就是近几天在开发过程中遇到的全部问题了,但是肯定远远不止这些。那么就先告一段落了。
Reference: vue-jsx-next