⚠️ Vue 3 TSX

360 阅读4分钟

本篇基于 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 对象了。

!Vue Template

因为 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