# UI框架对比
# 背景
为了提示公司软件效能,特升级技术栈为typescript+vue3,所以调研以下UI框架包括但不限于Element-plus(饿了么前端团队)、 ant-design(vue版社区维护,但得到官方认可)、arco-design(字节跳动前端团队)、idux(深信服前端团队)。
# 简单对比
ant-design-vue、arco-design、iduxUI 样式差不多,但是idux和vue、在表单中没有使用v-model双向绑定(使用成本可能会高一点),下面是个简单例子
<template>
<IxForm class="demo-form" :control="formGroup" labelCol="6">
<IxFormItem label="Method" required message="Please select your contact method!">
<IxSelect control="method">
<IxSelectOption key="email" label="E-mail"></IxSelectOption>
<IxSelectOption key="mobilePhone" label="MobilePhone"></IxSelectOption>
</IxSelect>
</IxFormItem>
<IxFormItem label="Contact" required :message="getContactMessage">
<IxInput control="contact"></IxInput>
</IxFormItem>
<IxFormItem :controlCol="{ offset: 6 }">
<IxCheckbox control="subscribe">Subscribe notifications</IxCheckbox>
</IxFormItem>
<IxFormItem :controlCol="{ offset: 6 }">
<IxButton mode="primary" @click="onSubmit">Submit</IxButton>
</IxFormItem>
</IxForm>
</template>
<script setup lang="ts">
import { AbstractControl, Validators, useFormGroup } from '@idux/cdk/forms'
const mobilePhoneValidator = (value: string) => {
if (!value || /(^1\d{10}$)/.test(value)) {
return undefined
}
return { mobilePhone: { message: 'Mobile phone number is not valid!' } }
}
const { required, email } = Validators
const formGroup = useFormGroup({
method: ['email', required],
contact: ['', [required, email]],
subscribe: [true],
})
const methodControl = formGroup.get('method')
const contactControl = formGroup.get('contact')
const subscribeControl = formGroup.get('subscribe')
methodControl.watchValue(value => {
if (value === 'mobilePhone') {
subscribeControl.disable()
subscribeControl.setValue(false)
contactControl.setValidators([required, mobilePhoneValidator])
} else {
subscribeControl.enable()
subscribeControl.setValue(true)
contactControl.setValidators([required, email])
}
contactControl.reset()
})
const getContactMessage = (control: AbstractControl) => {
if (control.hasError('required')) {
return 'Please input your contact number!'
}
if (control.hasError('email')) {
return 'The input is not valid email!'
}
if (control.hasError('mobilePhone')) {
return control.getError('mobilePhone')!.message as string
}
return ''
}
const onSubmit = () => {
if (formGroup.valid.value) {
console.log('submit', formGroup.getValue())
} else {
formGroup.markAsDirty()
}
}
</script>
<style lang="less" scoped>
.demo-form {
max-width: 300px;
}
</style>
1
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
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
其中对比了 ant-design、arco-design,组件,使用方式上差别不大,基本组件也都齐全(但是ant-design的table的虚拟滚动是收费的),业务功能上都支持定制主体,国际化等功能。 ant-design-vue是社区维护的,提供的pro版本是收费的,arco-design提供的pro版提供路由菜单功能、权限控制等功能,值得一提的是 状态管理由之前的Vuex换成的Pinia (opens new window)。
# 深度对比 arco-design、element-plus
由于我门用的最多的就是table表格,所以就对表格做了深度比对,现列出重要功能点
功能 | arco-design | element-plus |
---|---|---|
虚拟滚动 | ✅ | ✅ |
表格边框 | 支持设置表格边框,单元格边框,表头边框 | 仅支持设置显示或者无 |
列渲染 | 支持传入数组以及template写法 | 仅支持template写法 |
列超出显示 | 可配置tooltip 组件属性 | 仅支持 |
可拖拽表格 | ✅ | ❌ |
分页器 | 支持配置分页器 | 需要另外组件 |
table性能对比
加载统一行列表格,都没使用虚拟列表
element-plus占用内存200+M
arco-design 占用内存80+M
table用法
arco-design
<template>
<div class="hello">
<a-table
:columns="columns"
:data="data"
:pagination="false"
:sticky-header="0"
:bordered="{
wrapper:true,
cell:true,
headerCell:true,
bodyCell:true
}"
>
<template #name>
<author></author>
</template>
</a-table>
</div>
</template>
<script>
import {nextTick, onMounted, reactive,ref} from 'vue';
import author from './author';
export default {
name: 'HelloWorld',
components:{author},
setup(props){
const loading = ref(false);
const columns = reactive([
{
title: 'Name',
dataIndex: 'name',
render(){
return <author></author>
}
},
{
title: 'Name',
dataIndex: 'name',
slotName:"name",
},
{
title: 'Name',
dataIndex: 'name',
render(){
return <author></author>
}
},
{
title: 'Name',
dataIndex: 'name',
slotName:"name",
},
{
title: 'Salary',
dataIndex: 'salary',
},
{
title: 'Address',
dataIndex: 'address',
},
{
title: 'Email',
dataIndex: 'email',
width:'100',
ellipsis: true,
tooltip:{position: 'left'},
},
{
title: 'Email',
dataIndex: 'email',
width:'100',
ellipsis: true,
tooltip:{position: 'left'},
},
{
title: 'Email',
dataIndex: 'email',
width:'100',
ellipsis: true,
tooltip:{position: 'left'},
},
{
title: 'Email',
dataIndex: 'email',
width:'100',
ellipsis: true,
tooltip:{position: 'left'},
},
{
title: 'Email',
dataIndex: 'email',
width:'100',
ellipsis: true,
tooltip:{position: 'left'},
},
{
title: "op",
dataIndex: "op",
render(){
return <a-button type="primary">1111111</a-button>
}
}
]);
const data = reactive([]);
onMounted(()=>{
console.time("start")
loading.value = true;
for (let i=0; i<100;i++){
if(i%2===0){
data.push({
email:"email",
address:"address",
salary:"salary",
name:"name"
})
}else {
data.push({
email:"emailemailemailemailemailemailemailemailemailem",
address:"address",
salary:"salary",
name:"name"
})
}
}
nextTick(()=>{
// loading.value = false;
console.timeEnd("start");
})
})
return{
columns,
data
}
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
1
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
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
element-plus
<template>
<div class="hello">
<el-table :data="data" style="width: 100%" :loading="loading">
<el-table-column prop="name" label="Name" width="180" >
<template #default>
<authorBox></authorBox>
</template>
</el-table-column>
<el-table-column prop="name" label="Name" width="180" >
<template #default>
<authorBox></authorBox>
</template>
</el-table-column>
<el-table-column prop="name" label="Name" width="180" >
<template #default>
<authorBox></authorBox>
</template>
</el-table-column>
<el-table-column prop="name" label="Name" width="180" >
<template #default>
<authorBox></authorBox>
</template>
</el-table-column>
<el-table-column prop="address" label="Address" />
<el-table-column prop="address" label="Address" />
<el-table-column prop="email" label="email" width="100" show-overflow-tooltip/>
<el-table-column prop="email" label="email" width="100" show-overflow-tooltip/>
<el-table-column prop="email" label="email" width="100" show-overflow-tooltip/>
<el-table-column prop="email" label="email" width="100" show-overflow-tooltip/>
<el-table-column prop="email" label="email" width="100" show-overflow-tooltip/>
<el-table-column prop="op" label="op" >
<el-button>1111111</el-button>
</el-table-column>
</el-table>
</div>
</template>
<script>
import {onMounted,reactive,nextTick,onBeforeMount,ref} from 'vue'
import authorBox from './author.vue'
export default {
name: 'HelloWorld',
props: {
msg: String
},
components:{ authorBox },
setup(){
const data = reactive([]);
const loading = ref(false);
onBeforeMount(()=>{
console.time("start")
loading.value = true;
})
onMounted(()=>{
for (let i=0; i<100;i++){
if(i%2===0){
data.push({
email:"email",
address:"address",
salary:"salary",
name:"name"
})
}else {
data.push({
email:"emailemailemailemailemailemailemailemailemailem",
address:"address",
salary:"salary",
name:"name"
})
}
}
nextTick(()=>{
loading.value = false;
console.timeEnd("start");
})
})
return{
data
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
1
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
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
ps:@金星果如果有更好的想法欢迎一起讨论