六、创建更好的用户界面
过渡和动画是在我们的应用中创建更好的用户体验的好方法。由于有这么多不同的选项和用例,如果使用不足或过度,它们可能会影响应用的效果。我们将在本章中进一步研究这个概念。
我们还将查看第三方【验证表】。这将允许我们创建与应用大小相适应的表单。我们还可以根据表单状态更改 UI,并显示有用的验证消息以帮助用户。
最后,我们将看看如何使用render
函数和 JSX 来使用 Vue 构建用户界面。虽然这并非适用于所有场景,但有时您希望在模板中充分利用 JavaScript,并使用功能组件模型创建智能/表示组件。
在本章结束时,您将拥有:
- 了解 CSS 动画
- 创建自己的 CSS 动画
- 用于
Animate.css
创建交互 UI,工作量小 - 调查并创建了自己的 Vue 转换
- 利用
Vuelidate
在 Vue 内验证表单 - 使用
render
功能替代模板驱动的 UI - 使用 JSX 编写与 React 类似的 UI
让我们从理解为什么我们应该关心项目中的动画和过渡开始。
动画
动画可用于将焦点吸引到特定的 UI 元素,并通过将其带入生活来改善用户的整体体验。当没有清晰的开始状态和结束状态时,应使用动画。动画可以设置为自动播放,也可以由用户交互触发。
CSS 动画
CSS 动画不仅是一个强大的工具,而且它们也很容易维护,只需要很少的知识就可以在项目中使用它们。
将它们添加到界面可以是捕获用户注意力的直观方法,也可以轻松地将用户指向特定元素。动画可以定制,使其成为各种项目中大量用例的理想选择。
在深入研究 Vue 过渡和其他动画可能性之前,我们应该了解如何制作基本的 CSS3 动画。让我们创建一个简单的项目来更详细地了解这一点:
# Create a new Vue project
$ vue init webpack-simple vue-css-animations
# Navigate to directory
$ cd vue-css-animations
# Install dependencies
$ npm install
# Run application
$ npm run dev
在App.vue
内部,我们可以先创建以下样式:
<style>
button {
background-color: transparent;
padding: 5px;
border: 1px solid black;
}
h1 {
opacity: 0;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
.animated {
animation: fade 1s;
opacity: 1;
}
</style>
正如你所看到的,没有什么太不寻常的事情。我们用名为fade
的@keyframes
声明 CSS 动画,本质上给 CSS 两种状态,我们希望元素处于-opacity: 1
和opacity: 0
。它没有说明这些关键帧重复了多长时间或是否重复;这些都是在animated
课上完成的。每当我们将类添加到元素时,我们都会为1
s 应用fade
关键帧;同时,我们正在添加opacity: 1
以确保它不会在动画结束后消失。
我们可以利用v-bind:class
根据toggle
的值动态添加/删除该类,将其组合在一起:
<template>
<div id="app">
<h1 v-bind:class="{ animated: toggle }">I fade in!</h1>
<button @click="toggle = !toggle">Toggle Heading</button>
</div>
</template>
<script>
export default {
data () {
return {
toggle: false
}
}
}
</script>
凉的我们现在可以根据Boolean
值淡入航向。但如果我们能做得更好呢?在这种特殊情况下,我们本可以通过过渡来实现类似的结果。在更详细地研究转换之前,让我们先看看在项目中使用 CSS 动画的其他方法。
动画
Animate.css
是在项目中轻松实现不同类型动画的好方法。这是一个由 Daniel Eden(创建的开源 CSS 库 https://daneden.me/ ),它让我们可以访问“即插即用”CSS 动画。
在将其添加到任何项目之前,请前往https://daneden.github.io/animate.css/ 并预览不同的动画样式。有许多不同的动画可供选择,每种动画都提供不同的默认动画。这些可以进一步定制,我们将在本节稍后讨论更多。
通过在终端中运行以下命令,继续创建游乐场项目:
Create a new Vue project
$ vue init webpack-simple vue-animate-css
# Navigate to directory
$ cd vue-animate-css
# Install dependencies
$ npm install
# Run application
$ npm run dev
项目设置完成后,继续在您选择的编辑器中打开它,然后转到index.html
文件。在<head>
标记内,添加以下样式表:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">
这是Animate.css
处理项目所需的样式表参考。
使用 Animate.css
现在我们在项目中有了Animate.css
,我们可以将App.vue
改为template
,如下所示:
<template>
<h1 class="animated fadeIn">Hello Vue!</h1>
</template>
在添加任何动画之前,我们首先需要添加动画类。接下来,我们可以从Animate.css
库中选择任何动画;我们选择fadeIn
作为这个例子。然后可以将其切换到其他动画,如bounceInLeft
、shake
、rubberBand
等等!
我们可以以前面的示例为例,将其转换为基于布尔值的绑定类值,但转换可能更令人兴奋。
过渡
转换是从一个特定状态开始,然后转换到另一个状态,并在其间插入值。过渡不能在动画中包含多个步骤。想象一对窗帘从打开到关闭:第一种状态是打开位置,而第二种状态是关闭位置。
Vue 有自己的处理转换的标签,称为<transition>
和<transition-group>
。这些标签是可定制的,可以轻松地与 JavaScript 和 CSS 一起使用。转换工作不一定需要transition
标记,因为您只需将状态变量绑定到可见属性,但是标记通常提供更多的控制和可能更好的结果。
让我们以之前的toggle
为例,创建一个使用transition
的版本:
<template>
<div id="app">
<transition name="fadeIn"
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut">
<h1 v-if="toggle">I fade in and out!</h1>
</transition>
<button @click="toggle = !toggle">Toggle Heading</button>
</div>
</template>
<script>
export default {
data () {
return {
toggle: false
}
}
}
</script>
让我们更详细地看一下运动部件。
我们将元素包围在一个<transition>
标记中,每当<h1>
进入 DOM 时,该标记将应用于animated fadeIn
的enter-active-class
。这由v-if
指令触发,因为toggle
变量最初设置为false
。单击按钮切换布尔值,触发转换并应用适当的 CSS 类。
过渡态
每个进入/离开变换最多应用六个类,这些类由进入场景、进入场景和离开场景时的变换组成。第一套(v-enter-*)
为先入后出的过渡,第二套(v-leave-*)
为先入后出的结束过渡:
| 名称 | 说明 |
| v-enter
| 这是 enter 的最开始状态。插入图元一帧后,将删除该图元。 |
| v-enter-active
| enter-active
是enter
的活动状态。它在整个活动阶段处于活动状态,并且仅在过渡或动画结束后才被删除。此状态还管理其他指令,如延迟、持续时间等。 |
| v-enter-to
| 这是 enter 的最后一个状态,在插入元素后添加了一帧,这与删除v-enter
的时间相同。过渡/动画结束后,将删除Enter-to
。 |
| v-leave
| 这是休假的开始状态。触发离开转换后,在一帧后移除。 |
| v-leave-active
| leave-active
是leave
的活动状态。它在离开阶段的整个过程中都处于活动状态,并且仅在过渡或动画结束后才被删除。 |
| v-leave-to
| 请假的最后一个状态,在触发请假后增加一帧,这与v-leave
被删除的时间相同。当过渡/动画结束时,Leave-to
被移除。 |
每个enter
和leave
转换都有一个前缀,表中显示为v
的默认值,因为转换本身没有名称。在将 enter 或 leave 转换添加到项目中时,理想情况下应该应用适当的命名约定作为唯一标识符。如果您计划在一个项目中使用多个转换,并且可以通过简单的分配操作来完成,则这会有所帮助:
<transition name="my-transition">
表单验证
在整本书中,我们研究了各种不同的方式,我们可以通过v-model
之类的方式来捕获用户输入。我们将使用名为Vuelidate的第三方库根据特定规则集执行模型验证。让我们通过在终端中运行以下命令来创建游乐场项目:
# Create a new Vue project
$ vue init webpack-simple vue-validation
# Navigate to directory
$ cd vue-validation
# Install dependencies
$ npm install
# Install Vuelidate
$ npm install vuelidate
# Run application
$ npm run dev
什么是 Vuelidate?
Vuelidate
是一个开源的轻量级库,帮助我们使用各种验证上下文执行模型验证。验证可以在功能上进行组合,它还可以与其他库(如Moment
、Vuex
等)很好地配合使用。由于我们已经在我们的项目中使用npm install vuelidate
安装了它,我们现在需要将其注册为main.js
中的插件:
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import App from './App.vue';
Vue.use(Vuelidate);
new Vue({
el: '#app',
validations: {},
render: h => h(App),
});
将空的 validations 对象添加到我们的主 Vue 实例会在整个项目中引导 Vuelidate 的$v
。这样,我们就可以使用$v
对象在 Vue 实例中跨所有组件获取表单当前状态的信息。
使用 Vuelidate
让我们创建一个基本表单,允许我们输入一个firstName
、lastName
、email
和password
。这将允许我们使用Vuelidate
添加验证规则,并在屏幕上显示它们:
<template>
<div>
<form class="form" @submit.prevent="onSubmit">
<div class="input">
<label for="email">Email</label>
<input
type="email"
id="email"
v-model.trim="email">
</div>
<div class="input">
<label for="firstName">First Name</label>
<input
type="text"
id="firstName"
v-model.trim="firstName">
</div>
<div class="input">
<label for="lastName">Last Name</label>
<input
type="text"
id="lastName"
v-model.trim="lastName">
</div>
<div class="input">
<label for="password">Password</label>
<input
type="password"
id="password"
v-model.trim="password">
</div>
<button type="submit">Submit</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
email: '',
password: '',
firstName: '',
lastName: '',
};
},
methods: {
onSubmit(){
}
},
}
</script>
这里发生了很多事情,所以让我们一步一步地把它分解:
- 我们正在使用
@submit.prevent
指令创建一个新表单,以便在提交表单时页面不会重新加载,这与在此表单上调用 submit 并在事件上使用preventDefault
相同 -
接下来,我们将
v-model.trim
添加到每个表单输入元素中,以便修剪任何空白并将输入捕获为变量 -
我们在数据函数中定义这些变量,以便它们是被动的
submit
按钮是用type="submit"
定义的,因此当点击该按钮时,表单的submit
功能将运行- 我们正在剔除一个空白的
onSubmit
函数,我们很快就会创建它
现在我们需要添加@input
事件,并在我们的input
元素中的每一个上调用touch
事件,绑定到数据属性v-model
,并向字段提供验证,如下所示:
<div class="input">
<label for="email">Email</label>
<input
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email">
</div>
<div class="input">
<label for="firstName">First Name</label>
<input
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()">
</div>
<div class="input">
<label for="lastName">Last Name</label>
<input
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()">
</div>
<div class="input">
<label for="password">Password</label>
<input
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()">
</div>
然后,我们可以通过从Vuelidate
导入验证并添加与表单元素对应的validations
对象,将验证添加到我们的 Vue 实例中。
Vuelidate
将使用我们的data
变量绑定此处设置的相同名称,如下所示:
import { required, email } from 'vuelidate/lib/validators';
export default {
// Omitted
validations: {
email: {
required,
email,
},
firstName: {
required,
},
lastName: {
required,
},
password: {
required,
}
},
}
我们只是导入所需的电子邮件验证器并将其应用于每个模型项。这从本质上确保我们的所有项目都是必需的,并且电子邮件输入与电子邮件正则表达式匹配。然后,我们可以通过添加以下内容来可视化表单和每个字段的当前状态:
<div class="validators">
<pre>{{$v}}</pre>
</div>
然后,我们可以添加一些样式,在右侧显示验证,在左侧显示表单:
<style>
.form {
display: inline-block;
text-align: center;
width: 49%;
}
.validators {
display: inline-block;
width: 49%;
text-align: center;
vertical-align: top;
}
.input {
padding: 5px;
}
</style>
如果一切按计划进行,我们应该得到以下结果:
显示表单错误
我们可以使用$v.model_name
对象内部的$invalid
布尔值(其中model_name
等于email
、firstName
、lastName
或password
来显示消息或更改表单字段的外观。让我们首先添加一个名为error
的新类,该类在输入字段周围添加一个red``border
:
<style>
input:focus {
outline: none;
}
.error {
border: 1px solid red;
}
</style>
然后,只要该字段无效并使用v-bind:class
触碰,我们就可以有条件地应用该类:
<div class="input">
<label for="email">Email</label>
<input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email">
</div>
<div class="input">
<label for="firstName">First Name</label>
<input
:class="{ error: $v.firstName.$error }"
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()">
</div>
<div class="input">
<label for="lastName">Last Name</label>
<input
:class="{ error: $v.lastName.$error}"
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()">
</div>
<div class="input">
<label for="password">Password</label>
<input
:class="{ error: $v.password.$error }"
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()">
</div>
然后,每当字段无效或有效时,这将为我们提供以下结果:
如果出现这种情况,则可以显示错误消息。这可以通过多种方式完成,具体取决于您想要显示的消息类型。以email
输入为例,当email
字段的电子邮件地址无效时,显示一条错误消息:
<div class="input">
<label for="email">Email</label>
<input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email">
<p class="error-message" v-if="!$v.email.email">Please enter a valid email address</p>
</div>
// Omitted
<style>
.error-message {
color: red;
}
</style>
正如我们从$v
对象的表示中所看到的,当字段具有有效的电子邮件地址时,电子邮件布尔值为 true,如果没有,则为 false。虽然这会检查电子邮件是否正确,但不会检查字段是否为空。让我们添加另一条基于required
验证器检查此情况的错误消息:
<p class="error-message" v-if="!$v.email.email">Please enter a valid email address.</p>
<p class="error-message" v-if="!$v.email.required">Email must not be empty.</p>
如果我们愿意,我们甚至可以更进一步,创建我们自己的包装器组件来呈现每个字段的各种错误消息。让我们填写其余的错误消息,同时检查表单元素是否已被触动(是$dirty
:
<div class="input">
<label for="email">Email</label>
<input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email">
<div v-if="$v.email.$dirty">
<p class="error-message" v-if="!$v.email.email">Please enter a
valid email address.</p>
<p class="error-message" v-if="!$v.email.required">Email must not
be empty.</p>
</div>
</div>
<div class="input">
<label for="firstName">First Name</label>
<input
:class="{ error: $v.firstName.$error }"
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()">
<div v-if="$v.firstName.$dirty">
<p class="error-message" v-if="!$v.firstName.required">First Name
must not be empty.</p>
</div>
</div>
<div class="input">
<label for="lastName">Last Name</label>
<input
:class="{ error: $v.lastName.$error}"
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()">
<div v-if="$v.lastName.$dirty">
<p class="error-message" v-if="!$v.lastName.required">Last Name
must not be empty.</p>
</div>
</div>
<div class="input">
<label for="password">Password</label>
<input
:class="{ error: $v.password.$error }"
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()">
<div v-if="$v.password.$dirty">
<p class="error-message" v-if="!$v.password.required">Password must
not be empty.</p>
</div>
</div>
密码验证
在创建用户帐户时,密码通常会输入两次,并符合最小长度。让我们添加另一个字段和更多验证规则来强制执行此操作:
import { required, email, minLength, sameAs } from 'vuelidate/lib/validators';
export default {
// Omitted
data() {
return {
email: '',
password: '',
repeatPassword: '',
firstName: '',
lastName: '',
};
},
validations: {
email: {
required,
email,
},
firstName: {
required,
},
lastName: {
required,
},
password: {
required,
minLength: minLength(6),
},
repeatPassword: {
required,
minLength: minLength(6),
sameAsPassword: sameAs('password'),
},
},
}
我们已经做了以下工作:
- 将
repeatPassword
字段添加到我们的数据对象中,以便它可以保存重复的密码 - 从
Vuelidate
导入了minLength
和sameAs
验证器 - 将
6
字符的minLength
添加到password
验证器 - 增加了
sameAs
验证器,强制repeatPassword
遵循与password
相同的验证规则
由于我们现在有了适当的密码验证,我们可以添加新字段并显示任何错误消息:
<div class="input">
<label for="email">Email</label>
<input
:class="{ error: $v.email.$error }"
type="email"
id="email"
@input="$v.email.$touch()"
v-model.trim="email">
<div v-if="$v.email.$dirty">
<p class="error-message" v-if="!$v.email.email">Please enter a valid email address.</p>
<p class="error-message" v-if="!$v.email.required">Email must not be empty.</p>
</div>
</div>
<div class="input">
<label for="firstName">First Name</label>
<input
:class="{ error: $v.firstName.$error }"
type="text"
id="firstName"
v-model.trim="firstName"
@input="$v.firstName.$touch()">
<div v-if="$v.firstName.$dirty">
<p class="error-message" v-if="!$v.firstName.required">First Name must not be empty.</p>
</div>
</div>
<div class="input">
<label for="lastName">Last Name</label>
<input
:class="{ error: $v.lastName.$error}"
type="text"
id="lastName"
v-model.trim="lastName"
@input="$v.lastName.$touch()">
<div v-if="$v.lastName.$dirty">
<p class="error-message" v-if="!$v.lastName.required">Last Name must not be empty.</p>
</div>
</div>
<div class="input">
<label for="password">Password</label>
<input
:class="{ error: $v.password.$error }"
type="password"
id="password"
v-model.trim="password"
@input="$v.password.$touch()">
<div v-if="$v.password.$dirty">
<p class="error-message" v-if="!$v.password.required">Password must not be empty.</p>
</div>
</div>
<div class="input">
<label for="repeatPassword">Repeat Password</label>
<input
:class="{ error: $v.repeatPassword.$error }"
type="password"
id="repeatPassword"
v-model.trim="repeatPassword"
@input="$v.repeatPassword.$touch()">
<div v-if="$v.repeatPassword.$dirty">
<p class="error-message" v-if="!$v.repeatPassword.sameAsPassword">Passwords must be identical.</p>
<p class="error-message" v-if="!$v.repeatPassword.required">Password must not be empty.</p>
</div>
</div>
提交表格
接下来,如果表单无效,我们可以禁用Submit
按钮:
<button :disabled="$v.$invalid" type="submit">Submit</button>
我们还可以通过this.$v.$invalid
在 JavaScript 中获取该值。下面是一个示例,说明如何检查表单是否无效,然后基于表单元素创建用户对象:
methods: {
onSubmit() {
if(!this.$v.$invalid) {
const user = {
email: this.email,
firstName: this.firstName,
lastName: this.lastName,
password: this.password,
repeatPassword: this.repeatPassword
}
// Submit the object to an API of sorts
}
},
},
如果您希望以这种方式使用数据,您可能更愿意这样设置数据对象:
data() {
return {
user: {
email: '',
password: '',
repeatPassword: '',
firstName: '',
lastName: '',
}
};
},
我们现在已经创建了一个具有适当验证的表单!
渲染/功能组件
我们将绕过和绕开验证和动画,考虑使用功能组件和渲染功能来提高应用性能。您还可能听说这些组件被称为“表示组件”,因为它们是无状态的,只作为输入属性接收数据。
到目前为止,我们只为组件声明了带有template
标记的标记,但也可以使用render
函数(如src/main.js
所示):
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
h
来自 hyperscript,它允许我们使用 JavaScript 创建/描述 DOM 节点。在render
函数中,我们只是简单地呈现App
组件,将来,我们将更详细地研究它。Vue 创建一个虚拟 DOM,以使使用实际 DOM 变得更简单(以及在处理大量元素时提高性能)。
渲染元素
我们可以用以下对象替换我们的App.vue
组件,该对象接受render
对象和hyperscript
而不是使用template
:
<script>
export default {
render(h) {
return h('h1', 'Hello render!')
}
}
</script>
然后呈现一个新的带有文本节点'Hello render!'
的h1
标记,该标记被称为VNode(虚拟节点)和复数VNode(虚拟 DOM 节点),用于描述整个树。现在让我们看看如何在ul
中显示项目列表:
render(h){
h('ul', [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
}
必须认识到,我们只能使用 hyperscript 渲染一个根节点。此限制与我们的模板相同,因此我们希望将项目包装在一个div
中,如下所示:
render(h) {
return h('div', [
h('ul', [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
])
}
属性
我们还可以将样式元素和各种其他属性传递给渲染项。下面是一个使用style
对象更改每个项目red
颜色的示例:
h('div', [
h('ul', { style: { color: 'red' } }, [
h('li', 'Evan You'),
h('li', 'Edd Yerburgh'),
h('li', 'Paul Halliday')
])
])
您可以想象,我们可以添加任意数量的style
属性,以及我们期望的额外选项,例如props
、directives
、on
(单击处理程序)等等。让我们看看如何映射元素以使用props
渲染组件。
组件和道具
让我们用一个道具name
在components/ListItem.vue
下创建一个ListItem
组件。我们将渲染此组件来代替我们的li
,并映射到包含各种names
的数组上。注意我们如何将functional: true
选项添加到我们的 Vue 实例中;这告诉 Vue,这纯粹是一个表示组件,它不会有任何自己的状态:
<script>
export default {
props: ['name'],
functional: true
}
</script>
通过我们的render
函数,h
通常也被称为createElement
,因为我们处于 JavaScript 上下文中,所以我们能够利用数组操作符,例如map
、filter
、reduce
等等。让我们用map
替换动态生成组件的静态名称:
import ListItem from './components/ListItem.vue';
export default {
data() {
return {
names: ['Evan You', 'Edd Yerburgh', 'Paul Halliday']
}
},
render(createElement) {
return createElement('div', [
createElement('ul',
this.names.map(name =>
createElement(ListItem,
{props: { name: name } })
))
])
}
}
我们需要做的最后一件事是在组件中添加一个render
函数。作为第二个参数,我们能够访问上下文对象,这允许我们访问options
比如props
。在本例中,我们假设name
道具始终存在,而不是null
或undefined
:
export default {
props: ['name'],
functional: true,
render(createElement, context) {
return createElement('li', context.props.name)
}
}
同样,我们现在有一个元素列表,其中包括作为prop
传递的项目:
JSX
虽然这是一个很好的思考练习,但在大多数情况下模板都是优越的。有时您可能希望在组件内部使用 render 函数,在这些情况下,使用 JSX 可能更简单。
让我们通过在终端中运行以下命令,将用于 JSX 的 babel 插件添加到我们的项目中:
$ npm i -D babel-helper-vue-jsx-merge-props babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx
然后我们可以更新我们的.babelrc
以使用新插件:
{
"presets": [
["env", { "modules": false }],
"stage-3"
],
"plugins": ["transform-vue-jsx"]
}
这允许我们重写render
函数,以利用更简单的语法:
render(h) {
return (
<div>
<ul>
{ this.names.map(name => <ListItem name={name} />) }
</ul>
</div>
)
}
这更具声明性,也更易于维护。在引擎盖下,它正在被传输到以前的hyperscript
格式与巴贝尔。
总结
在本章中,我们学习了如何在 Vue 项目中利用 CSS 动画和转换。这允许我们让用户体验更流畅,并改善应用的外观和感觉。
我们还学习了如何使用render
方法构建 UI;这涉及到使用 HyperScript 创建 VNode,然后使用 JSX 进行更清晰的抽象。虽然您可能不想在项目中使用 JSX,但如果您来自 React 背景,您可能会发现它更适合您。
版权属于:月萌API www.moonapi.com,转载请注明出处