UI 框架-代码优化

基本完工了,但是代码还是很丑,重复的地方过多


汇总全局设定

首先,先让我们建立一个全局设定文件,方便后续查阅、管理

出于个人爱好,我喜欢管这样的文件叫 Global,并且我决定放在 src 目录下

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
// Global.ts
export const components = {
'Button': {
name: 'Button', title: 'Button 按钮'
},
'Card': {
name: 'Card', title: 'Card 卡片'
},
'Dialog': {
name: 'Dialog', title: 'Dialog 对话框'
},
'Switch': {
name: 'Switch', title: 'Switch 开关'
},
'Table': {
name: 'Table', title: 'Table 表格'
},
'Tabs': {
name: 'Tabs', title: 'Tabs 标签页'
},
}
export const guidances = {
'introduction': { path: "introduction", title: "介绍" },
'install': { path: "install", title: "安装" },
'start': { path: "start", title: "快速上手" }
}

该文件告诉了外部引用者,本项目的几个主要文档页的信息

化简样例

显然每个在 src/component 下的组件文档页都是引用 ./example 下对应的例子

那么我们应该把引用收束到一起,再暴露给组件文档页

首先,新建 src/component/contents 文件夹,为每个组件新建对应的样例管理文件

显然,该文件需要包含

  1. 所有的样例引入
  2. 该样例专属的参数列表

例如为 Button 组件新建 src/component/contents/Button.ts 文件

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
import LabyComponent1 from "../example/Button/Button1.example.vue";
import LabyComponent2 from "../example/Button/Button2.example.vue";
import LabyComponent3 from "../example/Button/Button3.example.vue";
import LabyComponent4 from "../example/Button/Button4.example.vue";
import LabyComponent5 from "../example/Button/Button5.example.vue";
import LabyComponent6 from "../example/Button/Button6.example.vue";

export default {
components: [
LabyComponent1,
LabyComponent2,
LabyComponent3,
LabyComponent4,
LabyComponent5,
LabyComponent6,
],
attributes: [
{ attr: 'level', desp: '默认类型', type: 'string', values: 'default / plain / primary / success / info / warning / danger', default: 'default' },
{ attr: 'disabled', desp: '是否禁用', type: 'boolean', values: 'false / true', default: 'false' },
{ attr: 'theme', desp: '式样', type: 'string', values: 'button / link / text', default: 'button' },
{ attr: 'loding', desp: '是否加载中', type: 'boolean', values: 'false / true', default: 'false' },
{ attr: 'size', desp: '尺寸', type: 'string', values: 'middle / small / large', default: 'middle' },
{ attr: 'color', desp: '颜色', type: 'string', values: '任意合法颜色值', default: '#f3678e' },
]
};

我为 Button 创建了 6 个样例,所以有 6 条静态引入

注意,对 vue 文件的引入,只能在代码中静态引入,不可以像 md 一样动态引入,否则必须要先配置对 vue 文件的额外解析

然后在 Button 组件文档页中引入该 ts

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
import LabyCard from "../lib/Card.vue";
import LabyTable from "../lib/Table.vue";

import Example from "./contents/Button";
import { ref } from "vue";
export default {
components: {
LabyCard,
LabyTable,
},
setup(props) {
const heads = [
{ name: "参数", identifier: "attr" },
{ name: "含义", identifier: "desp" },
{ name: "类型", identifier: "type" },
{ name: "可选值", identifier: "values" },
{ name: "默认值", identifier: "default" },
];
const keys = heads.map((item: any) => item.identifier);

const { components, attributes } = Example;
const visibility = ref(components.map((item) => false));
const toggle = (index) => {
visibility.value[index] = !visibility.value[index];
};
return {
Prism,
heads,
keys,
components,
attributes,
visibility,
toggle,
};
},
};

修改模板为 v-for 遍历

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
<template>
<div>Button 文档</div>
<div
class="container"
v-for="({ ...component }, index) in components"
:key="index"
>
<laby-card class="example">
<h2>{{ component.__sourceCodeTitle }}</h2>
<br />
<component :is="component" />
<br />
<br />
<code class="markdown-body">
<pre
v-if="visibility[index]"
v-html="
Prism.highlight(
component.__sourceCode,
Prism.languages.html,
'html'
)
"
></pre>
</code>

<button class="toggle" @click="toggle(index)">
<span class="close" v-if="visibility[index]">

<span class="desp">隐藏代码</span>
</span>
<span class="open" v-else>

<span class="desp">显示代码</span>
</span>
</button>
</laby-card>
<br />
</div>
<laby-table bordered>
<thead>
<tr>
<th v-for="(head, index) in heads" :key="index">{{ head.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(attribute, index) in attributes" :key="index">
<td v-for="key in keys" :key="key" v-html="attribute[key]"></td>
</tr>
</tbody>
</laby-table>
</template>

效果如图

效果图

抽取公用显示

显然,每个组件文档页,都具有与 Button 组件文档页类似的结构

那么我们可以把这个结构抽取出来,然后按照需要进行引入

先抽取结构,我选择在 src/views 下新建 Content.vue 来承载这个结构

组件列表和参数表通过 src/components/contents 下的汇总来引入

引入后,根据字段名的不同,制作哈希表 LabyMap

再要求用户传入参数 props,通过 name 来指定选择要显示哪个组件文档页,通过 title 来指示现在的组件文档页的标题

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
145
146
147
148
149
150
151
152
153
154
// Content.vue
<template>
<h1>{{ title }}</h1>
<br />
<div
class="container"
v-for="({ ...component }, index) in components"
:key="index"
>
<laby-card class="example">
<h2>{{ component.__sourceCodeTitle }}</h2>
<br />
<component :is="component" />
<br />
<br />
<code class="markdown-body">
<pre
v-if="visibility[index]"
v-html="
Prism.highlight(
component.__sourceCode,
Prism.languages.html,
'html'
)
"
></pre>
</code>

<button class="toggle" @click="toggle(index)">
<span class="close" v-if="visibility[index]">

<span class="desp">隐藏代码</span>
</span>
<span class="open" v-else>

<span class="desp">显示代码</span>
</span>
</button>
</laby-card>
<br />
</div>
<laby-table bordered>
<thead>
<tr>
<th v-for="(head, index) in heads" :key="index">{{ head.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(attribute, index) in attributes" :key="index">
<td v-for="key in keys" :key="key" v-html="attribute[key]"></td>
</tr>
</tbody>
</laby-table>
</template>
<script lang="ts">
import LabyButtons from "../components/contents/Button";
import LabyCards from "../components/contents/Card";
import LabyDialogs from "../components/contents/Dialog";
import LabySwitchs from "../components/contents/Switch";
import LabyTables from "../components/contents/Table";
import LabyTabss from "../components/contents/Tabs";

import { ref } from "vue";
import LabyCard from "../lib/Card.vue";
import LabyTable from "../lib/Table.vue";
import "prismjs";
import "prismjs/themes/prism.css";

const Prism = (window as any).Prism;

const LabyMap = {
Button: LabyButtons,
Card: LabyCards,
Dialog: LabyDialogs,
Switch: LabySwitchs,
Table: LabyTables,
Tabs: LabyTabss,
};

export default {
props: {
name: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
components: {
LabyCard,
LabyTable,
},
setup(props) {
const { name, title } = props;
const heads = [
{ name: "参数", identifier: "attr" },
{ name: "含义", identifier: "desp" },
{ name: "类型", identifier: "type" },
{ name: "可选值", identifier: "values" },
{ name: "默认值", identifier: "default" },
];
const keys = heads.map((item: any) => item.identifier);

const { components, attributes } = LabyMap[name];
const visibility = ref(components.map((item) => false));
const toggle = (index) => {
visibility.value[index] = !visibility.value[index];
};
return {
title,
Prism,
heads,
keys,
components,
attributes,
visibility,
toggle,
};
},
};
</script>
<style lang="scss" scoped>
$theme-color: #f3678e;
.container {
&:hover {
> .example > .toggle > * > .desp {
display: inline;
}
}
> .example {
> .toggle {
display: block;
width: 100%;
height: 32px;
border: none;
transition: background-color 250ms;
outline: none;
&:focus {
outline: none;
}
background: white;
cursor: pointer;
&:hover {
background: fade-out($theme-color, 0.95);
}
> * > .desp {
display: none;
}
}
}
}
</style>

同理可以抽取指南文档页的结构到 src/views/Guidance.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Guidance.vue
<template>
<article class="markdown-body" v-html="md"></article>
</template>
<script>
import { ref } from "vue";
export default {
props: {
path: {
type: String,
required: true,
},
},
setup(props) {
const md = ref(null);
import(`../markdown/${props.path}.md`).then(
(res) => (md.value = res.default)
);
return { md };
},
};
</script>

然后修改 router.ts,通过路由来传递参数

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
// router.ts
import { createWebHistory, createRouter } from 'vue-router'
import Home from './views/Home.vue'
import Document from './views/Document.vue'
import Content from './views/Content.vue'
import Guidance from './views/Guidance.vue'
import { guidances, components } from './Global'

const history = createWebHistory()
const router = createRouter({
history,
routes: [
{ path: '/', component: Home },
{
path: '/document', component: Document, children: [
{ path: '', redirect: '/document/introduction' },
{ path: 'introduction', component: Guidance, props: guidances['introduction'] },
{ path: 'install', component: Guidance, props: guidances['install'] },
{ path: 'start', component: Guidance, props: guidances['start'] },
{ path: 'button', component: Content, props: components['Button'] },
{ path: 'card', component: Content, props: components['Card'] },
{ path: 'dialog', component: Content, props: components['Dialog'] },
{ path: 'switch', component: Content, props: components['Switch'] },
{ path: 'table', component: Content, props: components['Table'] },
{ path: 'tabs', component: Content, props: components['Tabs'] },
]
}
]
})
export default router

现在,就可以删除 src/component 下,没有被引用的组件了

如无意外,现在该目录下,应该只有

  • contents,文件夹
  • example,文件夹
  • Topnav.vue

精简 router

这个 router.ts 看起来也还是很糟心,再优化一下吧

注意到重复的部分出现在 children 字段下,而该字段的值是个数组,每个数组项都是一个包含 3 个字段的对象

那么我们可以先定义这个对象

1
2
3
4
5
6
// router.ts
function Route(path, component, props) {
this.path = path
this.component = component
this.props = props
}

再观察指南的路由,和组件的路由,有如下规律

  1. 指南的路由
    • path 值集合,与 Global.ts 中的 guidances 对象的 keys 是一致的
    • props 中传入的键集合,与 Global.ts 中的 guidances 对象的 keys 是一致的
  2. 组件的路由
    • path 值集合,与 Global.ts 中的 components 对象的 keys.toLowerCase 是一致的
    • props 中传入的键集合,与 Global.ts 中的 components 对象的 keys 是一致的

那么容易得到指南路由的数组和组件路由的数组

1
2
3
4
5
6
7
// router.ts
const guidancesRoutes = Object.keys(guidances).map(item => {
return new Route(item, Guidance, guidances[item])
})
const componentsRoutes = Object.keys(components).map(item => {
return new Route(item.toLowerCase(), Content, components[item])
})

然后在路由配置的 children 字段下,使用 spread 语法展开这两个数组即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// router.ts
const router = createRouter({
history,
routes: [
{ path: '/', component: Home },
{
path: '/document', component: Document, children: [
{ path: '', redirect: '/document/introduction' },
...guidancesRoutes,
...componentsRoutes
]
}
]
})

好了,现在的代码,就几乎没有冗余,非常漂亮了


感谢阅读

--It's the end.Thanks for your read.--