JSX
按照 React 官方的解释,JSX 是一个 JavaScript 的语法扩展,类似于模板语法,或者说是一个类似于 XML 的 ECMAScript 语法扩展,并且具备 JavaScript 的全部功能。
🔔 虽然 Vue 对 SFC 进行了炒鸡优化,但 JSX 语法依然是一种值得可选的拓展方式。
Vue3 编写组件的几种方式
开始之前,先回顾和了解一下 Vue3 中几种组件编写方式。
选项式写法(丢弃)
在 Vue 3.0 的 .vue 组件里,遵循 SFC 规范要求(注:SFC,即 Single-File Component,.vue 单组件),标准的 setup
用法是,在 setup
里面定义的数据如果需要在 template
使用,都需要 return
出来。
<template>
<h1>页面组件</h1>
<h1 v-text="num"></h1>
<button @click="add">自增</button>
<button @click="sub">自减</button>
</template>
<script>
import { ref } from 'vue'
export default {
props: {}, // 接收自定义属性
setup(props, context) {
console.log('----setup', props)
console.log('----setup', context)
const num = ref(1)
const add = () => {
num.value++
}
return { num, add }
},
methods: {
sub() {
this.num--
},
},
mounted() {
console.log('---mounted')
},
}
</script>
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
👀 详细使用请回顾 单组件的编写 内容板块。
组合式写法(推荐)
Vue 3.2 语法糖 <script-setup>
推出是为了让熟悉 3.0 的用户可以更高效率的开发组件,减少一些心智负担,只需要给 script
标签添加一个 setup
属性,那么整个 script
就直接会变成 setup
函数,所有顶级变量、函数,均会自动暴露给模板使用(无需再一个个 return
了),代码量大幅度减少。
<script setup>
import { ref } from 'vue'
const num = ref(1)
const add = () => {
num.value++
}
</script>
<template>
<h1>页面组件</h1>
<h1 v-text="num"></h1>
<button @click="add">自增</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
👀 详细使用请回顾 Vue3.2 语法糖 内容板块。
JSX 写法(扩展)
JSX 使自定义 Vue 组件更容易导入和管理,灵活扩展性更强,可以使用 JSX 来单独维护数据交互层,比
render
更清晰直观。
先看个简单栗子:
import { defineComponent, ref, computed, watch } from 'vue'
export default defineComponent((props) => {
const num = ref(1)
const add = () => {
num.value++
}
// 监听器
watch(num, () => {
console.log('---num changed', num.value)
})
// 计算属性
const total = computed(() => num.value * 1000)
return () => (
<div>
<h1>页面组件</h1>
<h1>{num.value}</h1>
<h1>{total.value}</h1>
<button onClick={add}>自增</button>
</div>
)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在 Vue3 中使用 JSX 语法
🔔 着重介绍 JSX 使用方法和使用场景,下文 示离 JSX,TSX 一致,统称为 JSX。
下面主要通过对比 jsx 和 template 不同语法,来实现同样的功能。
普通内容
TIP
普通内容常量,写法是一样的。
//jsx 写法
setup() {
return ()=><p type="email">hello</p>
}
//tempalte 写法
<tempalte>
<p type="email">hello</p>
</template>
2
3
4
5
6
7
8
9
动态变量
{}
是 jsx
的万能的用法,里面可以写 js
的表达式,循环逻辑操作等等。
TIP
jsx
统一使用大括号包裹变量,没有引号,比如<div age={age}>{age}</div>
;tempalte
内容使用双大括号包裹,属性变量使用冒号开头 如<div :age="age"></div>
。
//jsx 写法
setup() {
let age = 1
// 没有" "包裹,统一都是 {}
return ()=> <div age={age}>{age}</div>
}
//tempalte 写法
<tempalte>
<div :age="age" >{{age}}</div>
</template>
2
3
4
5
6
7
8
9
10
11
函数事件
基本用法
jsx
使用on
+大驼峰形式(首字母大写)
,template
使用@
+短横线形式
;jsx
方法名需要使用{}
包裹起来,tempalte 使用" "
包裹起来。
//jsx 写法
setup() {
const age= ref(0)
let inc = ()=> {
age.value++
}
return ()=> <div onClick={inc}>{age.value}</div>
}
//tempalte 写法
<tempalte>
<div @click="inc" >{{age}}</div>
</tempalte>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
参数进阶
jsx
和tempalte
都一样:无自定义参数的函数不需要带 () 调用结尾;jsx
在使用带参数的函数,则需要使用箭头函数包裹 :{()=>fn(args)}
;jsx
需要借助withModifiers
,实现.self
,.stop
等修饰符的效果。
//jsx 写法
import { withModifiers } from "vue"
...
setup() {
const age = ref(0)
let inc = ()=> {
age.value++
}
return ()=> (
// 根路径必须有节点,或者用<>代表fragment空节点
<>
// 无自定义参数,不需要 ()
<div onClick={inc}>{age.value}</div>
// 有参数,需要 ()=> 包裹
<div onClick={()=>inc('test')}>{age.value}</div>
<div onClick={withModifiers(inc, ["stop"])}>{age.value}</div>
// withModifiers 包裹 vue 修饰符
<input onKeyup =={(ev) => { //键盘事件enter事件
//逻辑部分也可以写入js
if (ev.key === 'Enter') {
inc ()
}
}/>
</>
)
}
//tempalte 写法
<tempalte>
<div @click="inc" >{{age}}</div>
<div @click="inc('test')" >{{age}}</div>
<div @click.stop="inc" >{{age}}</div> //stop修饰符
<input @keyup.enter="inc" >{{age}}</input> //键盘事件enter事件
</template>
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
ref 绑定
Vue3 中 有两种 ref
, 一种是指的 ref()
定义的双向绑定变量,另外则是绑定在 DOM 标签的 ref 引用
。
TIP
- 对于
ref()
双向绑定变量,jsx
不会向template
自动处理.value
问题,需要手动加.value
; - 对于
DOM
标签的ref
引用,jsx
是直接用ref(null)
变量,不需要加.value
,tempalte
则是用同名字符串。
//jsx 写法
setup() {
const divRef = ref(null)
const age = ref(0)
return ()=>
(
<div ref={divRef} > // DOM 标签的 ref 引用
<span>{age.value}</span> // ref() 双向绑定变量
</div>
)
}
//tempalte 写法
<tempalte>
<div ref='divRef'> // DOM 标签的 ref,使用同名字符串
<span>{{age}}</span> // ref() 双向绑定变量,不需要 .value
</div>
</templalte>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v-model 语法
在 jsx
中使用 v-model
或 v-models
代替 template
的 v-model
。
TIP
- 组件只有一个
v-model
时,使用v-model
语法; - 组件只有多个
v-model
时,可以使用v-model:xx
语法。
//jsx 写法
setup() {
const age = ref(0)
const gender = ref('')
return () =>
(
<input v-model={age} />
// v-model 带修饰,推荐 (v-model:修饰符)
<input v-model:foo={age} />
// 多个v-model
<input
//推荐(v-model:修饰符)
v-model:foo={age}
v-model:bar={gender}
/>
<input
//使用v-models,传递二维数组 (新版不推荐)
v-models={[
[age, "foo"],
[gender, "bar"],
]}
/>
)
}
//tempalte 写法
<tempalte>
<input v-model="age" />
<input v-model:foo="age" /> //v-model带修饰
<input
v-model:foo="age" //多个v-model
v-model:bar="gender"
/>
</template>
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
v-slots 语法
jsx
中使用 v-slots
代替 v-slot(简写#)
。
//jsx 写法
//方法一
const App = {
setup() {
const slots = {
default: () => <div>A</div>, // 默认插槽
bar: () => <span>B</span>, // 具名插槽
}
return () => <A v-slots={slots} />
}
}
//方法二
const App = {
setup() {
return () => (
<>
<A>
{{
default: () => <div>A</div>
bar: () => <span>B</span> // 具名插槽
}}
</A>
</>
)
}
}
//tempalte 写法
<tempalte>
<tempalte v-slot:bar> // 具名插槽,也可以 #bar 简写
<A />
</template>
<tempalte v-slot:default> // 默认插槽 也可以撒的不写
<A />
</template>
</template>
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
v-for 语法
jsx
中可使用 js
中的 map
循环来实现 tempalte
的 v-for
逻辑。
//jsx 写法
setup() {
const arr = ref([{label:'1'},{label:'2'},{label:'3'}])
return () => (
<>
{ arr.value.map( item => <span key={item.label}> {item.label} </span> )}
</>
)
}
//tempalte 写法
<tempalte>
<span v-for="item in arr" :key="item.label">{{item.label}}</span>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v-if 语法
jsx
中可使用 js
中的 if
逻辑,三目运算,&&,||
等来实现 tempalte
的 v-if
逻辑。
//jsx 写法
// 场景一:
setup() {
const show= ref(false)
return ()=>(
<>
{show && <span>test vif</span>} // 使用 && 运算
{!show && <span>test velse</span>}
</>
)
}
// 场景二:
setup() {
const show= ref(false)
return ()=>(
<>
<span>{show.value ? 'test vif' : 'test velse'}</span> //三目运算
</>
)
}
// 场景三:
setup() {
const show= ref(false)
const vif = () => {
if(show) {
return <span>test vif</span>
}
return <span>test velse</span>
}
return () => (
<>
vif() // if条件函数
</>
)
}
//tempalte 写法
<tempalte>
<span v-if="show">test vif</span>
<span v-else>test velse</span>
</template>
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
指令用法
指令使用中划线,比如 v-loading
,jsx 和 template
很相似。
//jsx 写法
setup() {
const isLoading= ref(true);
return () => (
<>
<span v-loading-fullscreen-lock={isLoading}> loading </span>
</>
)
}
//tempalte 写法
<tempalte>
<span v-loading-fullscreen-lock="isLoading"> loading </span>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
类名绑定
比较简单,不做赘述,Vue 绑定方式如果不了解,参考 动态绑定 CSS 内容板块。
// 直接使用JS模板字符串
const element = (
<div
className={`devui-accordion-item-title ${disabled ? 'disabled' : ''}`}
></div>
)
// 使用数组
const element = (
<div class={['devui-accordion-item-title', disabled && 'disabled']}>Item</div>
)
2
3
4
5
6
7
8
9
10
11
style 样式绑定
比较简单,直接看栗子,Vue 绑定方式如果不了解,参考 动态绑定 CSS 内容板块。
const width = '100px'
const element = <button style={{ width, fontSize: '16px' }}></button>
2
JSX 两个典型使用场景
组件方式使用
作为类同于 .vue
一样的组件,或者对于组件进行 .jsx
的二次封装,本质上依然是个组件。 下面我们看一个封装 C_Menu
组件拆出来柯厘化的 .jsx
组件,用来处理无限嵌套菜单。
// 不做过多注释,根据 JSX 语法,自行阅读理解
import { t } from '@/utils/d_i18n'
import { ElMenu, ElMenuItem, ElSubMenu } from 'element-plus'
import { defineComponent } from 'vue'
import C_Icon from '_c/C_Icon/index.vue' // 这类别名后续会统一优化,无需过多关注
import { VUE_OPTIONS } from './options'
import { I_MenuItem } from './types' // 这类别名后续会统一优化,无需过多关注
// TODO: 处理要渲染的菜单
function useRenderMenuEffect(menuData: I_MenuItem[]) {
return menuData.map((item: I_MenuItem) => {
const { path, children, meta, name } = item
//TODO: 处理submenu的插槽 插槽实现 #title层级的 DOM
const slots = {
title: () => {
return (
<>
<C_Icon v-show={meta?.icon} iconName={meta?.icon} />
<span>{name ? t(`route.${name}`) : meta.title}</span>
</>
)
},
}
//TODO: 递归选择children
if (children && children.length && !meta?.hidden) {
return (
<ElSubMenu index={path} v-slots={slots}>
{useRenderMenuEffect(children)}
</ElSubMenu>
)
}
//TODO: 正常渲染普通的菜单
if (!meta?.hidden) {
return (
<ElMenuItem index={path}>
<C_Icon v-show={meta?.icon} iconName={meta?.icon} />
<span>{name ? t(`route.${name}`) : meta.title}</span>
</ElMenuItem>
)
}
})
}
export default defineComponent({
...VUE_OPTIONS,
setup(props) {
return () => (
<ElMenu defaultActive={props.defaultActive} router={props.router}>
{useRenderMenuEffect(props.MENU_DATA!)}
</ElMenu>
)
},
})
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
53
顺便附上关于 JSX 组件使用过程中关于 props
的处理,这里把它单独剥到一个文件了,重点关注的是关于 JSX 中对于 props
传值的语法。
// ./options.ts
import { PropType } from 'vue'
import { I_MenuItem } from './types'
export const VUE_OPTIONS = {
props: {
MENU_DATA: {
type: Array as PropType<I_MenuItem[]>,
required: true,
},
defaultActive: {
type: String,
default: '',
},
// 是否是路由模式
router: {
type: Boolean,
default: false,
},
// 键名 - 菜单标题的键名
name: {
type: String,
default: 'name',
},
// 键名 - 菜单标识的键名
index: {
type: String,
default: 'index',
},
// 键名 - 菜单图标的键名
icon: {
type: String,
default: 'icon',
},
// 键名 - 菜单子菜单的键名
children: {
type: String,
default: 'children',
},
},
}
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
数据交互方式使用
很多时候,我们希望把一些静态数据,或者涉及到视图层渲染的数据跟组件逻辑解耦,这里示栗一个 .jsx 文件处理 table 组件,相对复杂的演示。
🔔 此示栗仅作为相对综合一些,用 jsx 数据交互方式,应用复杂场景的语法参考
- 模拟异步数据渲染 Tabel data;
- 列表项涉及较复杂的交互,行内及单元格编辑切换 Table columns;
- ui 组件的使用;
- 属性绑定,插槽,事件处理等较为综合的 JSX 语法应用。
import { I_RenderParams, I_TableColumns } from '@/components/C_Table/types'
import { ref } from 'vue'
import './index.scss'
// TODO: 模拟要渲染的数据源 tableData,从后台获取
const TABLE_DATA = ref()
export const exposeTableData = (callback: any) => {
setTimeout(() => {
TABLE_DATA.value = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
// 回调的时候为了强调格式,使异步数据源管理使用asnyc await, so 返回promise
callback(Promise.resolve(TABLE_DATA.value))
}, 1000)
}
// TODO: 要渲染的列表项
export const COLUMNS = (tableData: any): I_TableColumns[] => {
console.log('tableData ==>', tableData)
return [
{
type: 'index',
label: '序号',
width: 60,
},
{
type: 'selection',
label: '',
width: 60,
},
{
type: 'expand',
label: '',
},
{
//表头
label: '日期',
// 对齐方式
// TODO: 这里需要注意,响应式数据,必须传递对应的row,不能是具体的值
render: (params: I_RenderParams) =>
HTML_LINE_EDIT(params, 'date', tableData),
},
{
//表头
label: '姓名',
// 字段名称
render: ({ row }: any) => (
<div>
<el-popover
v-slots={{ reference: () => <el-tag>{row.name}</el-tag> }}
effect='light'
trigger='hover'
placement='top'
width='auto'>
<div>name: {row.name}</div>
<div>address: {row.address}</div>
</el-popover>
</div>
),
},
{
//表头
label: '地址',
// 字段名称
// 对齐方式
render: (params: I_RenderParams) =>
HTML_LINE_EDIT(params, 'address', tableData),
},
{
label: '操作',
fixed: 'right',
width: 160,
render: ({ row, index }: any) => (
<div>
<div v-show={activeLineEdit.value !== index || !isEditLine.value}>
<el-button
size='small'
type='warning'
onClick={() => handleEditClick(row, index)}>
<el-icon-edit />
</el-button>
<el-button size='small' type='danger'>
<el-icon-delete />
</el-button>
</div>
{/* TODO: 点击了行内编辑按钮的话 */}
<div v-show={activeLineEdit.value === index && isEditLine.value}>
<el-button
size='small'
type='primary'
onClick={() => clickConfirmOrCancel(tableData, row, index)}>
确定
</el-button>
<el-button
size='small'
onClick={() => clickConfirmOrCancel(tableData, row, index)}>
取消
</el-button>
</div>
</div>
),
},
]
}
const activeLineEdit = ref()
const isEditLine = ref(false)
const tempRowIndex = ref<number>()
const handleEditClick = (row: any, index: number) => {
clickConfirmOrCancel(null, null, tempRowIndex.value)
//整行编辑
activeLineEdit.value = index
isEditLine.value = true
// TODO: 这个坑要注意,因为是个引用类型,必须要给它深拷贝下
tempRow.value = JSON.parse(JSON.stringify(row))
// 重置当前的交叉index值
tempRowIndex.value = index
}
// TODO: 实时编辑相关的代码片段和对应逻辑
const HTML_LINE_EDIT = (
params: I_RenderParams,
attr: string,
tableData: any
) => {
const { index, column, row } = params
return (
<div class='html-line-edit'>
<div
v-show={index + column.id !== currentEdit.value && !isEditLine.value}>
<span> {row[attr]}</span>
<el-icon-edit
v-pointer
color='#e6a23c'
onClick={() => clickTempEdit(params)}
/>
</div>
{/* <span v-show={active.value === index}> */}
<span
v-show={index + column.id === currentEdit.value && !isEditLine.value}>
<el-input v-model={row[attr]} style='width:200px' size='small' />
<span>
<el-icon-check
v-pointer
color='#67c23a'
onClick={() => clickConfirmOrCancel(tableData, row)}
/>
<el-icon-close
v-pointer
color='#f56c6c'
onClick={() => clickConfirmOrCancel(tableData, row, index)}
/>
</span>
</span>
{/* 处理编辑行需要的元素 */}
<span v-show={activeLineEdit.value === index && isEditLine.value}>
<el-input v-model={row[attr]} style='width:200px' size='small' />
</span>
<span v-show={isEditLine.value && activeLineEdit.value !== index}>
{row[attr]}
<el-icon-edit
v-pointer
color='#e6a23c'
onClick={() => clickTempEdit(params)}
/>
</span>
</div>
)
}
// TODO: 当前点击的哪一行的哪一列
const tempRow = ref()
const currentEdit = ref('')
// 点击编辑按钮的时候,要对点击的数据进行临时存储,便于取消操作后恢复默认值
const clickTempEdit = (params: I_RenderParams) => {
clickConfirmOrCancel(null, null, tempRowIndex.value)
isEditLine.value = false
const { row, index, column } = params
currentEdit.value = index + column.id
// TODO: 这个坑要注意,因为是个引用类型,必须要给它深拷贝下
tempRow.value = JSON.parse(JSON.stringify(row))
// 重置当前的交叉index值
tempRowIndex.value = index
}
// 点击行内编辑保存或取消操作
const clickConfirmOrCancel = (tableData: any, row: any, index?: number) => {
console.log(tableData, row, index)
if (index || index === 0) {
// FIXME: 这里后面换成传入的tableData
TABLE_DATA.value[index] = tempRow.value
} else {
/* 将 rowid传递给后台即可 */
console.log('row')
}
currentEdit.value = ''
// 编辑行的也在这里复用处理
isEditLine.value = false
}
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
更多深入的细节,参考组件板块或后台项目代码。