# JSX
提示
本节为可选内容。
警告
不建议在库中使用 .vue
单文件来开发组件,这会缺少类型推断。除非有很特殊的需求。
目前编写节点的方式有两种:简单、笨拙的 模板语法 (opens new window),灵活、繁琐的 渲染函数 (opens new window)。如果嫌模板语法太笨拙,而渲染函数太繁琐,我想 JSX (opens new window) 是一个不错的选择。可以点击查看 Vue3 的 JSX 语法 (opens new window)。
思考:
- 单文件(
.vue
) 与 脚本文件(.js
或.ts
)文件的区别 - 模板语法、渲染函数(Function)、渲染函数(JSX)三者的区别
以下有几个简单的例子:
// .vue + template
<template>
<button :class="["ui-button", `size-${size}`]" @click="onClick">
<slot></slot>
<span class="ui-icon">icon</span>
</button>
</template>
<script>
import { h } from "vue";
export default {
emits: ["click"],
name: "ui-button",
props: {
size: String,
},
methods: {
onClick() {
this.$emit("click");
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// .js + template
import { defineComponent, h } from "vue";
export default defineComponent({
emits: ["click"],
name: "ui-button",
props: {
size: String,
},
methods: {
onClick() {
this.$emit("click");
},
},
template: `<button :class="["ui-button", \`size-${size}\`]" @click="onClick">
<slot></slot>
<span class="ui-icon">icon</span>
</button>`,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .jsx
import { defineComponent } from "vue";
export default defineComponent({
emits: ["click"],
name: "ui-button",
props: {
size: String,
},
methods: {
onClick() {
this.$emit("click");
},
},
render() {
return (
<button class={["ui-button", `size-${this.size}`]}>
{this.$slots.default?.()}
<span class="ui-icon">icon</span>
</button>
);
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// .js
import { defineComponent, h } from "vue";
export default defineComponent({
emits: ["click"],
name: "ui-button",
props: {
size: String,
},
methods: {
onClick() {
this.$emit("click");
},
},
render() {
return h(
"button",
{
class: ["ui-button", `size-${this.size}`],
onClick: this.onClick,
},
[this.$slots.default?.(), h("span", { class: "ui-icon" }, "icon")]
);
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
提示
可以自己亲手书写以上例子,感受一下,确保你能理解不同写法的利与弊。
# 改造
将之前 Vue 章节中写的 src/vue-button.tsx
进行改造:
提示
TypeScript + JSX 的文件后缀名为 .tsx
。
// src/vue-button.tsx
import "./style/vue-button.scss";
import { defineComponent, PropType } from "vue";
const Button = defineComponent({
emits: ["click"],
name: "ui-button",
props: {
size: {
type: String as PropType<"small" | "large" | "default">,
default: "default",
},
color: String,
},
methods: {
onClick(e: Event) {
this.$emit("click");
},
},
render() {
return (
<button class={["ui-button", `size-${this.size}`]} onClick={this.onClick}>
{this.$slots.default?.()}
</button>
);
},
});
export default Button;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
注意
在 .ts
使用 jsx
语法,VSCode 编辑器一般会抛出 无法使用 JSX,除非提供了 "--jsx" 标志。ts(17004)
错误。那么就需要在 tsconfig.json
打开对 jsx
支持的选项。
// tsconfig.json
{
"include": ["src/**/*"], // 只对 src 目录下做检查
"compilerOptions": {
// ...
"jsx": "preserve" /* Specify what JSX code is generated. */
// ...
}
}
2
3
4
5
6
7
8
9
# 使用
如何在 Vue3 上使用 JSX,可以查看 官方文档 (opens new window)。我们需要 @vue/babel-plugin-jsx (opens new window) 一个 Babel 插件 。
注意
这里使用的是 Vue3 的 JSX。
# 安装
npm install @vue/babel-plugin-jsx --save-dev
2
# 整体编译
// build.js
webpack(
{
// ...
// 顺序解析后缀名
resolve: { extensions: [".tsx", ".ts", ".js", ".json"] },
module: {
rules: [
// ...
{
test: /\.(tsx?)|(m?js)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
// 传入配置方式 或者 创建配置文件
options: {
presets: [
["@babel/preset-env", { debug: true, targets: "ie >= 11" }],
"@babel/preset-typescript",
],
plugins: [
"@vue/babel-plugin-jsx",
["@babel/plugin-transform-runtime", { corejs: 3 }],
],
},
},
},
],
},
plugins: [new MiniCssExtractPlugin()],
}
// ...
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
运行 node build.js
,打开 index.html
,正常运行。
# 单独编译
// compile.js
// 相当于 run-babel.js 的升级版
// ...
function compileTS(modules) {
// babel 配置
var config = {
presets: [
["@babel/preset-env", { modules, debug: true, targets: "IE >= 11" }],
"@babel/preset-typescript",
],
plugins: [
"@vue/babel-plugin-jsx",
["@babel/plugin-transform-runtime", { corejs: 3 }],
],
};
// Commonjs 输出至 ./lib
// ES6 Module 输出至 ./es
var path = modules === false ? "./es" : "./lib";
// 读取文件
src(["./src/**/*.ts", "./src/**/*.tsx"])
.pipe(
// 可以使用 through2 库,会更方便
// 创建转化流,类似于双工流,但其输出是其输入的转换的转换流。
new stream.Transform({
objectMode: true,
transform: function(chunk, encoding, next) {
// 转化逻辑
babel.transform(
chunk.contents.toString(encoding), // 文件内容
// 需要额外添加 filename
{ ...config, filename: chunk.basename },
(err, res) => {
// 文件中的 style/xxx.scss -> style/xxx.css
const content = res.code.replace(
/([\\/]style[\\/](?:.+)).scss/g,
"$1.css"
);
// 文件内容修改成转化后的代码
chunk.contents = Buffer.from(content);
// 后缀名文件 .ts -> .js
chunk.extname = ".js";
next(null, chunk);
}
);
},
})
)
.pipe(dest(path)); // 输出到某文件中
}
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
执行 node compile.js
单独编译文件,然后执行 node example/serve.js
,打开 http://localhost:3000/
,运行正常。
警告
还需要对编译声明文件 declare.js
进行调整,还需对 .tsx
文件编译。
// declare.js
// ...
// 简单实现返回文件夹下所有 .ts (除.d.ts) 文件函数
// 一般会直接使用第三方插件
function getFilesName(dirPath) {
dirPath = path.resolve(dirPath);
const collect = [];
const files = fs.readdirSync(dirPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
const childDirPath = path.resolve(dirPath, file.name);
collect.push(...getFilesName(childDirPath));
} else {
if (file.name.match(/[^(\.d)].tsx?$/)) {
const filePath = path.resolve(dirPath, file.name);
collect.push(filePath);
}
}
}
return collect;
}
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
执行 node declare.js
,输出声明文件。