专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
商务河北  ·  经开区“美•强•优”三重奏 ·  7 小时前  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  昨天  
前端早读课  ·  【第3451期】前端 TypeError ... ·  2 天前  
51好读  ›  专栏  ›  前端外刊评论

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

前端外刊评论  · 公众号  · 前端  · 2020-01-19 09:00

正文

前言

我日常工作都是使用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都有一些不同。

例如这样一段代码:

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

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

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

在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');



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>

)
}
});



这个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






请到「今天看啥」查看全文