Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用Vue 3.0做JSX(TSX)风格的组件开发 #11

Open
hujiulong opened this issue Jan 14, 2020 · 24 comments
Open

使用Vue 3.0做JSX(TSX)风格的组件开发 #11

hujiulong opened this issue Jan 14, 2020 · 24 comments

Comments

@hujiulong
Copy link
Owner

hujiulong commented Jan 14, 2020

前言

我日常工作都是使用React来做开发,但是我对React一直不是很满意,特别是在推出React Hooks以后。

不可否认React Hooks极大地方便了开发者,但是它又有非常多反直觉的地方,让我难以接受。所以在很长一段时间,我都在尝试寻找React的替代品,我尝试过不少别的前端框架,但都有各种各样的问题或限制。

在看到了Vue 3.0 Composition-API的设计,确实有眼前一亮的感觉,它既保留了React Hooks的优点,又没有反复声明销毁的问题,而Vue一直都是支持JSX语法的,3.0对TypeScript的支持又非常好,所以我开始尝试用Vue + TSX来做开发。

Vue 3.0已经发布了alpha版本,可以通过以下命令来安装:

npm install vue@next --save

简单示例

先来看看用Vue3.0 + TSX写一个组件是什么什么样子的。

实现一个Input组件:

import { defineComponent } from 'vue';

interface InputProps {
  value: string;
  onChange: (value: string) => void;
}
const Input = defineComponent({
  setup(props: InputProps) {
    const handleChange = (event: KeyboardEvent) => {
      props.onChange(event.target.value);
    }

    return () => (
      <input value={props.value} onInput={handleChange} />
    )
  }
})

可以看到写法和React非常相似,和React不同的是,一些内部方法,例如handleChange,不会在每次渲染时重复定义,而是在setup这个准备阶段完成,最后返回一个“函数组件”。

这算是解决了React Hooks非常大的一个痛点,比React Hooks那种重复声明的方式要舒服多了。

Vue 3.0对TS做了一些增强,不需要像以前那样必须声明props,而是可以通过TS类型声明来完成。

这里的defineComponent没有太多实际用途,主要是为了实现让ts类型提示变得友好一点。

Babel插件

为了能让上面那段代码跑起来,还需要有一个Babel插件来转换上文中的JSX,Vue 3.0相比2.x有一些变化,不能再使用原来的vue-jsx插件。

我们都知道JSX(TSX)实际上是语法糖,例如在React中,这样一段代码:

const input = <input value="text" />

实际上会被babel插件转换为下面这行代码:

const input = React.createElement('input', { value: 'text' });

Vue 3.0也提供了一个对应React.createElement的方法h。但是这个h方法又和vue 2.0以及React都有一些不同。

例如这样一段代码:

<div class={['foo', 'bar']} style={{ margin: '10px' }} id="foo" onClick={foo} />

在vue2.0中会转换成这样:

h('div', {
  class: ['foo', 'bar'],
  style: { margin: '10px' }
  attrs: { id: 'foo' },
  on: { click: foo }
})

可以看到vue会将传入的属性做一个分类,会分为classstyleattrson等不同部分。这样做非常繁琐,也不好处理。

在vue 3.0中跟react更加相似,会转成这样:

h('div', {
  class: ['foo', 'bar'],
  style: { margin: '10px' }
  id: 'foo',
  onClick: foo
})

基本上是传入什么就是什么,没有做额外的处理。

当然和React.createElement相比也有一些区别:

  • 子节点不会作为以children这个名字在props中传入,而是通过slots去取,这个下文会做说明。
  • 多个子节点是以数组的形式传入,而不是像React那样作为分开的参数

所以只能自己动手来实现这个插件,我是在babel-plugin-transform-react-jsx的基础上修改的,并且自动注入了h方法。

实际使用

在上面的工作完成以后,我们可以真正开始做开发了。

渲染子节点

上文说到,子节点不会像React那样作为children这个prop传递,而是要通过slots去取:

例如实现一个Button组件

// button.tsx
import { defineComponent } from 'vue';
import './style.less';

interface ButtonProps {
  type: 'primary' | 'dashed' | 'link'
}
const Button = defineComponent({
  setup(props: ButtonProps, { slots }) {
    return () => (
      <button class={'btn', `btn-${props.type}`}>
        {slots.default()}
      </button>
    )
  }
})

export default Button;

然后我们就可以使用它了:

import { createApp } from 'vue';
import Button from './button';

// vue 3.0也支持函数组件
const App = () => <Button>Click Me!</Button>

createApp().mount(App, '#app');

渲染结果:
image

Reactive

配合vue 3.0提供的reactive,不需要主动通知Vue更新视图,直接更新数据即可。

例如一个点击计数的组件Counter:

import { defineComponent, reactive } from 'vue';

const Counter = defineComponent({
  setup() {
    const state = reactive({ count: 0 });
    const handleClick = () => state.count++;
    return () => (
      <button onClick={handleClick}>
        count: {state.count}
      </button>
    )
  }
});

渲染结果:
Kapture 2020-01-14 at 13 15 22

这个Counter组件如果用React Hooks来写:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount(count + 1);
  return (
    <button onClick={handleClick}>
      count: {count}
    </button>
  )
}

对比之下可以发现Vue 3.0的优势:

在React中,useState和定义handleClick的代码会在每次渲染时都执行,而Vue定义的组件重新渲染时只会执行setup中最后返回的渲染方法,不会重复执行上面的那部分代码。

而且在Vue中,只需要更新对应的值即可触发视图更新,不需要像React那样调用setCount

当然Vue的这种定义组件的方式也带来了一些限制setup的参数props是一个reactive对象,不要对它进行解构赋值,使用时要格外注意这一点:

例如实现一个简单的展示内容的组件:

// 错误示例
import { defineComponent, reactive } from 'vue';

interface LabelProps {
  content: string;
}
const Label = defineComponent({
  setup({ content }: LabelProps) {
    return () => <span>{content}</span>
  }
})

这样写是有问题的,我们在setup的参数中直接对props做了解构赋值,写成了{ content }这样在后续外部更新传入的content时,组件是不会更新的,因为破坏了props的响应机制。以后可以通过eslint之类的工具来避免这种写法。

正确的写法是在返回的方法里再对props做解构赋值:

import { defineComponent, reactive } from 'vue';

interface LabelProps {
  content: string;
}
const Label = defineComponent({
  setup(props: LabelProps) {
    return () => {
      const { content } = props;  // 在这里对props做解构赋值
      return <span>{content}</span>;
    }
  }
})

生命周期方法

在Vue 3.0中使用生命周期方法也非常简单,直接将对应的方法import进来即可使用。

import { defineComponent, reactive, onMounted } from 'vue';

interface LabelProps {
  content: string;
}
const Label = defineComponent({
  setup(props: LabelProps) {
    
    onMounted(() => { console.log('mounted!'); });
  
    return () => {
      const { content } = props;
      return <span>{content}</span>;
    }
  }
})

vue 3.0对tree-shaking非常友好,所有API和内置组件都支持tree-shaking。

如果你所有地方都没有用到onMounted,支持tree-shaking的打包工具会自动将起去掉,不会打进最后的包里。

指令和过渡效果

Vue 3.0还提供了一系列组件和方法,来使JSX也能使用模板语法的指令和过渡效果。

使用Transition在显示/隐藏内容块时做过渡动画:

import { defineComponent, ref, Transition } from 'vue';
import './style.less';

const App = defineComponent({
  setup() {
    const count = ref(0);
    const handleClick = () => {
      count.value ++;
    }

    return () => (
      <div>
        <button onClick={handleClick}>click me!</button>
        <Transition name="slide-fade">
          {count.value % 2 === 0 ?
            <h1>count: {count.value}</h1>
          : null}
        </Transition>
      </div>
    )
  }
})
// style.less
.slide-fade-enter-active {
  transition: all .3s ease;
}
.slide-fade-leave-active {
  transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
  transform: translateX(10px);
  opacity: 0;
}

渲染结果:
Kapture 2020-01-14 at 12 25 41

也可以通过withDirectives来使用各种指令,例如实现模板语法v-show的效果:

import { defineComponent, ref, Transition, withDirectives, vShow } from 'vue';
import './style.less';

const App = defineComponent({
  setup() {
    const count = ref(0);
    const handleClick = () => {
      count.value ++;
    }

    return () => (
      <div >
        <button onClick={handleClick}>toggle</button>
        <Transition name="slide-fade">
          {withDirectives(<h1>Count: {count.value}</h1>, [[
            vShow, count.value % 2 === 0
          ]])}
        </Transition>
      </div>
    )
  }
})

这样写起来有点繁琐,应该可以通过babel-jsx插件来实现下面这种写法:

<h1 vShow={count.value % 2 === 0}>Count: {count.value}</h1>

优缺点

在我看来Vue 3.0 + TSX完全可以作为React的替代,它既保留了React Hooks的优点,又避开了React Hooks的种种问题。

但是这种用法也有一个难以忽视的问题:它没办法获得Vue 3.0编译阶段的优化。

Vue 3.0通过对模板的分析,可以做一些前期优化,而JSX语法是难以做到的。

例如“静态树提升”优化:

如下一段模板(这是模板,并非JSX):

<template>
 <div>
   <span>static</span>
   <span>{{ dynamic }}</span>
 </div>
</template>

如果不做任何优化,那么编译后得到的代码应该是这样子:

render() {
 return h('div', [
   h('span', 'static'),
   h('span', this.dynamic)
 ]);
}

那么每次重新渲染时,都会执行3次h方法,虽然未必会触发真正的DOM更新,但这也是一部分开销。

通过观察,我们知道h('span', 'static')这段代码传入的参数始终都不会有变化,它是静态的,而只有h('span', this.dynamic)这段才会根据dynamic的值变化。

在Vue 3.0中,编译器会自动分析出这种区别,对于静态的节点,会自动提升到render方法外部,避免重复执行。

Vue 3.0编译后的代码:

const __static1 = h('span', 'static');

render() {
   return h('div', [
       __static1,
       h('span', this.dynamic)
    ])     
}

这样每次渲染时就只会执行两次h。换言之,经过静态树提升后,Vue 3.0渲染成本将只会和动态节点的规模相关,静态节点将会被复用。

除了静态树提升,还有很多别的编译阶段的优化,这些都是JSX语法难以做到的,因为JSX语法本质上还是在写JS,它没有任何限制,强行提升它会破坏JS执行的上下文,所以很难做出这种优化(也许配合prepack可以做到)。

考虑到这一点,如果你是在实现一个对性能要求较高的基础组件库,那模板语法仍然是首选。

另外JSX也没办法做ref自动展开,使得refreactive在使用上没有太大区别。

后话

我个人对Vue 3.0是非常满意的,无论是对TS的支持,还是新的Composition API,如果不限制框架的话,那Vue以后肯定是我的首选。


更新:

本文中通过TS的interface声明props类型的依赖vue3的Optional props decalration,但后续版本中这个功能被废除了,原因可以查看#154, 在#1155中也有一些替代方案的讨论

@zouhangwithsweet
Copy link

hi,大佬。请问哪里可以看到 demo 源码呢?

@hujiulong
Copy link
Owner Author

hi,大佬。请问哪里可以看到 demo 源码呢?

这两天我整理一下放上来

@ZhengXiaowei
Copy link

希望可以放个demo,非常感谢

@jianwei
Copy link

jianwei commented Jan 27, 2020

q求 demo

@zouhangwithsweet
Copy link

zouhangwithsweet commented Jan 27, 2020

先来抛转引玉了,demo 和 babel-plugin 写的都比较简陋。vue3-tsx

@xialvjun
Copy link

@zouhangwithsweet 一定要 .vue 文件吗, 不能 .tsx 文件吗?

@ZhengXiaowei
Copy link

@xialvjun 可以看看我写的base-vue-tsx-template,基于vue-cli3做的tsx

@deepkolos
Copy link

preact-cli之前的版本用了一个babel插件实现了静态树提升 babel-plugin-transform-react-constant-elements, 不过后续版本没有集成这个plugin不知道为什么

@hujiulong
Copy link
Owner Author

preact-cli之前的版本用了一个babel插件实现了静态树提升 babel-plugin-transform-react-constant-elements, 不过后续版本没有集成这个plugin不知道为什么

@deepkolos 应该是转换后某些边界情况会出错。可以看这个讨论 facebook/react#3226

@wuhao000
Copy link

我现在用vue3.0开发,最麻烦的就是tsx会报 jsx element type does not have any constructor or call signatures

@lanshanmao
Copy link

@hujiulong 求解,为什么我这里拿不到
image

image
image

props打印出来是
image

@hujiulong
Copy link
Owner Author

@lanshanmao 看文章最后那段话

@zmj0920
Copy link

zmj0920 commented Sep 15, 2020

占个位置

@shipMatserJack
Copy link

貌似tsx组件内defineComponent里面不定义props是有问题的吧,这样的话setup函数内拿不到props的(ps:我的vue版本是3.0.4)

@hujiulong
Copy link
Owner Author

貌似tsx组件内defineComponent里面不定义props是有问题的吧,这样的话setup函数内拿不到props的(ps:我的vue版本是3.0.4)

@841440416 看文章最后那段文字。这个功能已经被移除了

@shipMatserJack
Copy link

貌似tsx组件内defineComponent里面不定义props是有问题的吧,这样的话setup函数内拿不到props的(ps:我的vue版本是3.0.4)

@841440416 看文章最后那段文字。这个功能已经被移除了

看到了,谢谢

@meikang123
Copy link

meikang123 commented Jan 13, 2021

tsx 会让热更新失效 遇到过吗 怎么解决

问题复现: vuejs/core#2778 (comment)

@Amour1688
Copy link

Amour1688 commented Jan 27, 2021

tsx 会让热更新失效 遇到过吗 怎么解决

问题复现: vuejs/vue-next#2778 (comment)

JSX 的语法需要单独的 loader 去支持,https://github.com/vueComponent/vue-jsx-hot-loader

@Niofh
Copy link

Niofh commented Apr 9, 2021

image
有人能解决这个问题吗?vscocde 没有报错,但是再webstrom tsx就不识别tsx语法,这个是什么问题呢?

@ishowman
Copy link

ishowman commented May 4, 2021

现在的这种写法,声明了ts类型还要声明 props 类型,写起来就像是脱裤子放屁一样难受。

@wenfangdu
Copy link

wenfangdu commented May 18, 2021

使用 .tsx 热加载失效,如何解决呢?

@PERSISTENC
Copy link

自定义函数里面如何写render??
image

@passioncsu
Copy link

这个文章非常棒

@leonyh7
Copy link

leonyh7 commented Jun 15, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests