9

⚠️ Vue 3 TSX

 4 years ago
source link: https://innei.ren/posts/programming/vue3-like-tsx
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
⚠️ 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)
1// Vue router 3
2import VueRouter from 'vue-router'
3const router = new VueRouter({
4  routes,
5})
6// Vue router 4
7import {
8  createRouter,
9  createWebHashHistory,
10} from 'vue-router'
11const router = createRouter({
12  history: createWebHashHistory(),
13  routes,
14})
Vue Router 支持给每个路由(Route)添加一个 Meta 对象,存储在 Meta 对象中的值会在当前 Router 实例中取得。同时,Vue 3 原生支持了 JSX(大概只是比上代好一点点???),为此,我们也可以像 React 那样操作。
比如,我使用 Routes 来构建一个侧边菜单,当然为了简单,侧边菜单至多只有两层。
1interface MenuModel {
2  title: string
3  path: string
4  icon: any
5  subItems?: Array<MenuModel>
6  hasParent: boolean
7  fullPath: string
8  query?: any
9}
以上是我的菜单需要的结构,而如何将 icon 存储下来,在 render 函数直接使用呢。如果是 React,我们可以这样写。
1icon: JSX.Element
然后直接使用 {menu.icon} 就行了。在 Vue3 中,如果使用 JSX,同样可以这样操作。在 Routes 中稍加修改。在 Meta 中,直接传入一个 JSX.Element
1import dashboardFilled from '@iconify-icons/ant-design/dashboard-filled'
2import { InlineIcon as Icon } from '@iconify/vue'
3const routeForMenu: Array<RouteRecordNormalized> = [
4  {
5    path: '/dashboard',
6    component: DashBoardView,
7    name: 'dashboard',
8    meta: { title: '仪表盘', icon: <Icon icon={dashboardFilled} /> },
9  },
10  // ...
11]
同样的在 Sidebar.tsx 中。
1export const Sidebar = defineComponent({
2  setup() {
3    return () => <div>
4      // ....
5    	{menu.icon}
6    </div>
7  }
8})
当然这种用法只限于 JSX/TSX。使用 Vue 的模板的话,就会渲染一个 VNode 对象了。
Vue Template
因为 JSX.Element 只是一个 Object,在 Vue 模板中只会判断 components 注册了没有,而不会关心这个 Object 是不是 VNode。而 JSX 中则会去判断是 VNode 则 render。
如果想在 Vue 模板中使用外部的 JSX,那么就需要去 components 注册一下就行了。
1// icon.tsx
2export const Icon = <FontAwesomeIcon icon={faAlignJustify} />
1<template>
2  <Icon />
3</template>
4
5<script lang="ts">
6import { Icon } from './icon'
7import { defineComponent } from 'vue'
8
9export default defineComponent({
10  components: { Icon },
11  
12})
13</script>
上面的是没注册的,下面的是注册的

Setup 与 TSX

在 Vue 2 中,data 中的属性以 _$ 打头的会被忽略,从而无法使用响应式流。在 Vue 3 中,data 还是和 Vue 2 一样无法使用,在 setup 函数中亦如此。但是官网文档没写不让用。
1<template>
2  <div class="w-10 m-auto">
3
4    <p>
5      <p>{{ $$a ?? 0}}</p>
6      <Button @click="$$a++">$$a++</Button>
7    </p>
8  
9    <p>
10      <p>{{ a }}</p>
11      <Button @click="a++">a++</Button>
12    </p>
13  </div>
14</template>
15
16<script lang="ts">
17import { defineComponent, ref } from 'vue'
18import Button from 'primevue/button'
19export default defineComponent({
20  components: { Button },
21  setup() {
22    const $$a = ref(0)
23    const $a = ref(0)
24    const a = ref(0)
25    return {
26      $$a,
27      a,
28    }
29  },
30})
31</script>
但在 TSX 中,你完全可以这样写。
1export const PlaceHolderView = defineComponent({
2  setup() {
3    const router = useRouter()
4
5    const $$a = ref(0)
6    return () => (
7        <p>
8          {$$a.value}
9          <p>
10            <Button onClick={() => $$a.value++}>$$a++</Button>
11          </p>
12        </p>
13    )
14  },
15})

Slot 与 TSX

在 Vue 中有个 v-slot 的东西,在 React 中则有个 Children,当然 Vue 的 slots 做的比 React 多得多。而在 React 中除了传递 Children,还可以通过 props 传递 React.reactElement。React 更加灵活。
在 Vue 3 TSX 写法中,v-slots.default 等于 React 的 children。
1const Children = defineComponent({
2  setup() {
3    return () => <p>Children</p>
4  },
5})
6
7const Parent = defineComponent({
8  setup({}, { slots }) {
9    return () => (
10      <div class="">
11        <p>Parent</p>
12        {slots.default?.()}
13      </div>
14    )
15  },
16})
17
18const RootView = defineComponent({
19  setup() {
20    return () => (
21        <p>
22          <Parent>
23            <Children></Children>
24          </Parent>
25        </p>
26    )
27  },
28})
slots 位于 setup 的第二个参数中,获取当前组件所有的 slots,并且是一个函数,需要 call 一下。
1<Parent v-slots={{default: () => <Children />}}></Parent>
也可以用上面的方式传入。
v-slots 对 TSX 的方式不太友好,建议还是使用 React 的方式编写。通过传递 Props 来渲染子组件。

Emit 与 TSX

在 Vue 模板中,我们会用 @ 去监听一个事件。在 React 的 JSX 中用 on 前缀来监听一个事件,如果是自定义事件,一般会定义一个新的函数。而在 Vue 中使用 emit 函数去发起一个事件。但是在 TSX 如何去监听呢。答案也是 on,你甚至可以不用手写一个函数。
1const Parent = defineComponent({
2  setup({}, { slots, emit }) {
3    onMounted(() => {
4      emit('mount', 1)
5    })
6    return () => (
7      null
8    )
9  },
10})
11
12const RootView = defineComponent({
13  setup({}, ctx) {
14    return () => (
15      <Parent
16        onMount={(val) => {
17          console.log('mount', val)
18        }}
19        >
20      </Parent>
21    )
22  },
23})
显然,onMount 这个 Props 是不存在的,我们也没有定义,但是在 Parent 中 emit 的事件为 mount。就得到了这个 Props。这个过程是发生在编译阶段的,所以在不同的架手架行为可能不同。甚至说随时可能 breaking change,对 ts 的支持也很不友好,充满了红线。所以不建议使用。
以上就是近几天在开发过程中遇到的全部问题了,但是肯定远远不止这些。那么就先告一段落了。
Reference:
vue-jsx-next
https://github.com/vuejs/jsx-next

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK