一种更加方便的组件开发方式

0. 配置 Vite 对 JSX 的支持

1
2
3
npm create vite@latest
cd vite-project
npm install

将项目跑起来之后进入项目中配置 vite 对于 jsx 的支持,根据官方文档的说明,下载@vitejs/plugin-vue-jsx插件。

1
npm install --save-dev @vitejs/plugin-vue-jsx

打开项目根目录下的 vite.config.js,引入并使用组件

1
2
3
4
5
6
7
8
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx"; // 引入插件

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()], // 使用插件
});

至此项目配置 JSX 支持完毕。

1. 调整开发方式

1.1 调整 SFC 为 JSX

配置了 JSX 之后,我们编写组件不再使用.vue 结尾,即不再编写 SFC 组件。以下是一个 Vue3 函数式组件的示例

1
2
3
4
5
6
7
8
/* App.jsx */
import { defineComponent } from "vue";

const App = defineComponent(() => {
return () => <div>App</div>;
});

export default App;

其中 defineComponent 接受一个函数作为组件初始化函数,具体详看文档defineComponent

1.2 调整 css 的引入方式

原来在 SFC 中,项目的样式文件仅需写在 SFC 的 style 标签中即可。但是在 JSX 中,样式文件作为独立的文件,可以 import 到任何组件中,即以下面的方式引入样式文件

1
2
3
4
/* App.css */
.container {
color: rgba(0, 0, 0, 0.6);
}
1
2
3
4
5
6
7
8
9
/* App.jsx */
import { defineComponent } from "vue";
import "./App.css";

const App = defineComponent(() => {
return () => <div class="container">App</div>;
});

export default App;

但是上述的使用方式,App.css 中的样式是影响全局的,我们需要配置 CSS Module 实现 SFC 中的 scoped,让组件样式的影响范围保持在组件中。

在配置 CSS Modules 之前,我们先引入 less,让 css 编写更加方便。

1.3 引入 less

在 vite 中,仅需安装 less 依赖即可添加 less 支持

1
npm install --save-dev less

然后把.css 后缀改为.less 即可

1
2
3
4
5
6
7
/* App.less */
.container {
color: rgba(0, 0, 0, 0.6);
}
.red {
color: red !important;
}
1
2
3
4
5
6
7
8
9
/* App.jsx */
import { defineComponent } from "vue";
import "./App.less";

const App = defineComponent(() => {
return () => <div class="container">App</div>;
});

export default App;

1.4 调整 less 的引入方式

在 vite 中,*.module.less 命名的 less 文件,vite 会对其进行 CSS Module 处理,导出一个 JS 对象,对象的 key 为 css 样式类名,对象的 value 为 css module 后的样式类名。即组件调整为以下写法。

1
2
3
4
5
6
7
/* App.module.less */
.container {
color: rgba(0, 0, 0, 0.6);
}
.red {
color: red !important;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* App.jsx */
import { defineComponent } from "vue";
import styles from "./App.module.less";

/*
styles的值为(不固定)
{"container":"_container_xpseh_1","red":"_red_xpseh_4"}
*/

const App = defineComponent(() => {
/* 下面的JSX将会被渲染成如下dom */
/* <div class="_container_xpseh_1">App</div> */
return () => <div class={styles.container}>App</div>;
});

export default App;

通过生成添加哈希的类名,保证了每个组件引用的样式都是独一无二的,避免了全局污染。

1.5 插槽

使用 JSX 后插槽和编写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// App.jsx
import { defineComponent } from "vue";
import Foo from "./Foo";

const App = defineComponent(() => {
return () => (
<Foo>
{{
default: () => <div>默认插槽</div>,
name: () => <div>具名插槽</div>,
}}
</Foo>
);
});

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
// Foo.jsx
import { defineComponent } from "vue";

const Foo = defineComponent((props, context) => {
return () => (
<div>
<div>Default Slot {context.slots.default()}</div>
<div>Name Slot {context.slots.name()}</div>
</div>
);
});

export default Foo;

在 Foo 组件中编写体验还好,但是在 App 组件中,插槽的使用方式不如 React 中的方便

1.6 HOC

现在我们可以编写 HOC 了,代码示例如下

1
2
3
4
5
6
7
8
9
// App.jsx
import { defineComponent } from "vue";
import Foo from "./Foo";

const App = defineComponent(() => {
return () => <Foo msg="msg" msg2="msg2" />;
});

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Foo.jsx
import { defineComponent } from "vue";
import HOC from "./HOC";

const Foo = defineComponent(
(props) => {
return () => (
<div>
<div>{props.msg}</div>
<div>{props.msg2}</div>
</div>
);
},
{
props: {
msg: String,
msg2: String,
},
}
);

export default HOC(Foo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HOC.js
import { defineComponent, h, useAttrs } from "vue";

const HOC = (Element) => {
const Wrapper = defineComponent((props) => {
const attrs = useAttrs();
return () =>
h(Element, {
...attrs,
msg: "劫持msg",
});
});
// 【关键】设置不允许继承,不然props还是会挂到劫持组件的attr中
Wrapper.inheritAttrs = false;
return Wrapper;
};

export default HOC;

如上,我在 APP 组件中引入 Foo 组件,Foo 组件受到 HOC.js 的劫持,其中 props 中的 msg 会被篡改成”劫持 msg”。
HOC可以实现很多功能,在开发中很方便。

2. 函数式组件

函数式组件就是使用函数编写的一种组件,组件由一个函数构成,接收 props,返回 vnode。在 React 中通过 React Hook 可以编写出很多功能强大的组件。但是在 Vue 中,函数式组件没有生命周期,无法使用 Vue 的 Hooks。

以下是一个函数式组件的例子

1
2
3
4
// App.jsx
const App = () => <div>App</div>;

export default App;

仅仅编写一个函数,就能成为一个组件。

但如果是 React 的话,可以添加 Hook 在不同的时机触发不同的钩子,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
// App.jsx
import React, { useEffect } from "react";

const App = () => {
useEffect(() => {
consol.log("load");
}, []);

return <div>App</div>;
};

export default App;

但是在 Vue 中,函数式组件不具备任何生命周期,无法使用 Hooks,如果想实现上述方式,得在 setup 中注册钩子,代码如下

1
2
3
4
5
6
7
8
9
10
11
// App.jsx
import { defineComponent, onMounted } from "vue";

const App = defineComponent(() => {
onMounted(() => {
console.log("load");
});
return () => <div>App</div>;
});

export default App;

3. JSX 的父子通讯方式

在 SFC 中,父子之间的通讯方式主要是父组件传递 props 给子组件,子组件 emit 事件给父组件的方式进行通讯。
但是在 JSX 中,有一种 JSX 特色的通讯方式,这一点在 React 中经常使用

下面编写两个组件 Foo 和 Bar,通过点击 Bar 接受父组件的传参,并能通知父组件更变自身状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Foo.jsx
import { defineComponent, ref } from "vue";
import Bar from "./Bar";

const Foo = defineComponent(() => {
const fooText = ref("Foo");
const barText = ref("Bar");

return () => (
<div>
{fooText.value}
<Bar
text={barText.value}
textClick={() => {
fooText.value = barText.value;
}}
/>
</div>
);
});

export default Foo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Bar.jsx
import { defineComponent } from "vue";

const Bar = defineComponent(
(props) => {
return () => <div onClick={props.textClick}>{props.text}</div>;
},
{
props: {
text: String,
textClick: Function,
},
}
);

export default Bar;

以上组件中,点击 Bar 中的文字,会改变 Foo 中的状态。
JSX 中其实不存在子传父,JSX 中更像子组件永远接受参数,只负责展示或者调用 props 中的数据或者函数,自己没有传递出去什么,自己只是调用了一个具有父元素作用域的函数而已。

4. 结束语

尽管 Vue 没有像 React 那样那么方便的使用 JSX,但是也在支持 JSX 的道路上走了很好的一步,而且相比 React Vue 对函数组件有着自己独特的 Hook 优势,使用函数组件能更方便的设计组件,以此记录。