Skip to content
This repository was archived by the owner on Apr 1, 2022. It is now read-only.

Commit 772acf5

Browse files
author
Ken Berkeley
committed
add README, refactor example/
1 parent 3ebdd29 commit 772acf5

25 files changed

+903
-566
lines changed

README.md

Lines changed: 363 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,372 @@
11
# Vue2 Datatable
22
> 做 Vue.js 下最好的 Datatable 组件
33
4-
任何开源的 datatable 都未必能满足所有的业务需求,因此本 README 致力于让您能在理解设计以及源码的基础上,可以自行定制出适合您业务需求的 datatable
4+
## § 前言
5+
任何开源的 Datatable 都未必能满足所有的业务需求(否则也不会有这个项目了)
6+
本 README 致力于让您在理解组件设计以及源码的基础上,可自行定制出适合您业务需求的 Datatable
57

6-
## 优势
7-
极其简单
8-
源码模块化,可读性好
9-
依赖少
10-
可移植性好(换成其他 UI 框架基本也就是换 class)
11-
特色:支持表头设置(支持保存本地) 无限嵌套组件
12-
扁平化组件
8+
## § 快速体验
9+
我们以 [`example/Basic/index.vue`](example/Basic/index.vue) 为例,效果见 [demo](https://kenberkeley.github.io/vue2-datatable/example-dist)
10+
11+
```html
12+
<template>
13+
<div>
14+
<code>query: {{ query }}</code>
15+
<datatable v-bind="$data" />
16+
<!-- 上面的写法比下面的要优雅,来源见 https://github.com/vuejs/vue/issues/4962
17+
<datatable
18+
:columns="columns"
19+
:data="data"
20+
:total="total"
21+
:query="query">
22+
</datatable>
23+
-->
24+
</div>
25+
</template>
26+
<script>
27+
import Datatable from 'vue2-datatable'
28+
import mockData from '../_mockData'
29+
30+
export default {
31+
components: { Datatable },
32+
data: () => ({
33+
columns: [
34+
{ title: 'User ID', field: 'uid', sort: true },
35+
{ title: 'Username', field: 'name' },
36+
{ title: 'Age', field: 'age', sort: true },
37+
{ title: 'Email', field: 'email' },
38+
{ title: 'Country', field: 'country' }
39+
],
40+
data: [],
41+
total: 0,
42+
query: {}
43+
}),
44+
watch: {
45+
query: {
46+
handler (query) {
47+
mockData(query).then(({ rows, total }) => {
48+
this.data = rows
49+
this.total = total
50+
})
51+
},
52+
deep: true
53+
}
54+
}
55+
}
56+
</script>
57+
```
58+
59+
## § 依赖
60+
* BootStrap 3.x + Font Awesome 4.x(全局引入)
61+
* [lodash / orderBy](https://lodash.com/docs/4.17.4#orderBy)
62+
* [replace-with](https://github.com/kenberkeley/replace-with)
63+
64+
注:BootStrap 以及 Font Awesome 的可替换性极强,您完全可以使用其他库替代(一般就是改一下类名即可)
65+
66+
## § 详解
67+
68+
### ⊙ 整体构造
69+
本 Datatable 的源码目录树 [`lib/`](lib/) 如下:
70+
71+
```
72+
lib/
73+
├─ HeaderSettings/ # 表头设置
74+
│   ├─ ColumnGroup.vue # 表头设置分栏组件
75+
│   └─ index.vue # 表头设置主体
76+
├─ HeadSort.vue # 排序
77+
├─ LimitSelect.vue # 每页显示记录数下拉选择框
78+
├─ MultiSelect.vue # 行首多选框
79+
├─ Pagination.vue # 分页
80+
└─ index.vue # Datatable 主体
81+
```
82+
83+
[`example/Advanced/index.vue`](example/Advanced/index.vue)[demo](https://kenberkeley.github.io/vue2-datatable/example-dist/#advanced) 为例,标出对应的组件如下图所示:
84+
85+
![Datatable Structure](structure.png)
86+
87+
### ⊙ 配置项
88+
> 【 Vue.js 小技巧 】
89+
> `HelloWorld` 组件中定义 `props: { hi: Boolean }`
90+
> `<hello-world hi />` 等同于 `<hello-world :hi="true" />`
91+
> 显然地,前者在写法上更加优雅
92+
93+
[`lib/index.vue`](lib/index.vue) 中的 `props` 如下:
94+
95+
```js
96+
props: {
97+
columns: { type: Array, required: true }, // 列定义
98+
data: { type: Array, required: true }, // 当页纪录 (rows)
99+
total: { type: Number, required: true }, // 记录总数
100+
query: { type: Object, required: true }, // 查询对象
101+
selection: Array, // 多项选择的容器
102+
summary: Object, // 汇总行数据 (summary row)
103+
HeaderSettings: { type: Boolean, default: true }, // 是否显示表头设置组件
104+
Pagination: { type: Boolean, default: true }, // 是否显示分页相关组件
105+
xprops: Object, // 额外传给动态组件的东东
106+
supportBackup: Boolean, // 是否支持使用 LocalStorage 保存表头设置
107+
supportNested: Boolean, // 是否支持内嵌组件 (nested component)
108+
tableBordered: Boolean // 是否添加 .table-bordered 类到 <table> 元素
109+
}
110+
```
111+
112+
下面仅讲解 `columns` / `data` / `query` / `selection` / `xprops` 以及三种动态组件(`thComp` / `tdComp` / `nested component`
113+
114+
***
115+
116+
#### `:columns`
117+
我们来对比一下 [`example/`](example/) 中的 [`Basic`](example/Basic/index.vue)[`Advanced`](example/Advanced/index.vue)`columns` 定义:
118+
119+
```js
120+
// example/Basic - 简单写法
121+
columns: [
122+
{ title: 'User ID', field: 'uid', sort: true },
123+
{ title: 'Username', field: 'name' },
124+
{ title: 'Age', field: 'age', sort: true },
125+
{ title: 'Email', field: 'email' },
126+
{ title: 'Country', field: 'country' }
127+
]
128+
129+
// example/Advanced - 标准写法
130+
columns: [{
131+
groupName: 'Normal',
132+
columns: [
133+
{ title: 'Email', field: 'email', visible: false, thComp: 'FilterTh', tdComp: 'Email' },
134+
{ title: 'Username', field: 'name', thComp: 'FilterTh' },
135+
{ title: 'Country', field: 'country', thComp: 'FilterTh' },
136+
{ title: 'IP', field: 'ip', visible: false, tdComp: 'IP' }
137+
]
138+
}, {
139+
groupName: 'Sortable',
140+
columns: [
141+
{ title: 'User ID', field: 'uid', sort: true, visible: 'true', weight: 1 },
142+
{ title: 'Age', field: 'age', sort: true },
143+
{ title: 'Create time', field: 'createTime', sort: true,
144+
thClass: 'w-240', tdClass: 'w-240', thComp: 'CreatetimeTh', tdComp: 'CreatetimeTd' }
145+
]
146+
}, {
147+
groupName: 'Extra (radio)',
148+
type: 'radio',
149+
columns: [
150+
{ title: 'Operation', tdComp: 'Opt' },
151+
// don't forget to set the columns below `visible: false`, since the `type` is `radio`
152+
{ title: 'Color', field: 'color', explain: 'Favorite color', visible: false, tdComp: 'Color' },
153+
{ title: 'Language', field: 'lang', visible: false, thComp: 'FilterTh' },
154+
{ title: 'PL', field: 'programLang', explain: 'Programming Language', visible: false, thComp: 'FilterTh' }
155+
]
156+
}]
157+
```
158+
159+
实际上 `Basic` 的这种简写最终都会被转为 `Advanced` 的标准形式(见源码 [`lib/index.vue`](lib/index.vue) 中的 `computed.columns$`
160+
161+
下面列出 `columns[i]` 中的可配置项:
162+
163+
| 参数 | 说明 | 类型 | 可选项 | 默认值 | 是否必须 |
164+
|---------|--------------------------------------------------|----------------|---------------------------|--------|----------|
165+
| title | 显示名称 | String | - | - ||
166+
| field | 字段名称 | String | - | - ||
167+
| explain | 说明文字 | String | - | - ||
168+
| sort | 是否支持排序 | Boolean | - | false ||
169+
| weight | 显示排名权重 | Number | - | 0 ||
170+
| visible | 是否显示(若为字符串类型则禁止设置该列显隐状态) | Boolean / String | true / false / 'true' / 'false' | true ||
171+
| thClass | 用于 `<th>` 的类名 | String | - | - ||
172+
| thStyle | 用于 `<th>` 的内联样式 | String | - | - ||
173+
| thComp | 用于 `<th>` 的动态组件名 | String | - | - ||
174+
| tdClass | 用于 `<td>` 的类名 | String | - | - ||
175+
| tdStyle | 用于 `<td>` 的内联样式 | String | - | - ||
176+
| tdComp | 用于 `<td>` 的动态组件名 | String | - | - ||
177+
178+
>【 JS 小技巧 】
179+
>
180+
```js
181+
cols.map(col => {
182+
if (!col.weight) col.weight = 0
183+
return col
184+
})
185+
// 利用逗号运算符,可以把上面的代码缩写为一行
186+
cols.map(col => ((col.weight = col.weight || 0), col))
187+
```
188+
***
189+
190+
#### `:data`
191+
实际上该项应该叫 `rows` 才合理,但主流的 Datatable 都是这样称呼,我也不能免俗
192+
本身该项是没啥好讲的,但本 Datatable 支持**无限递归内嵌组件**,靠的就是在这里做文章
193+
在此把源码 `lib/index.vue` 中的 `computed.data$` 直接搬出来:
194+
195+
```js
196+
data$ () {
197+
const { data } = this
198+
if (this.supportNested) {
199+
// support nested components with extra magic
200+
data.forEach(item => {
201+
if (!item.__nested__) {
202+
this.$set(item, '__nested__', {
203+
comp: '', // name of nested component
204+
visible: false,
205+
$toggle (comp, visible) {
206+
switch (arguments.length) {
207+
case 0:
208+
this.visible = !this.visible
209+
break
210+
case 1:
211+
switch (typeof comp) {
212+
case 'boolean':
213+
this.visible = comp
214+
break
215+
case 'string':
216+
this.comp = comp
217+
this.visible = !this.visible
218+
break
219+
}
220+
break
221+
case 2:
222+
this.comp = comp
223+
this.visible = visible
224+
break
225+
}
226+
}
227+
})
228+
Object.defineProperty(item, '__nested__', { enumerable: false })
229+
}
230+
})
231+
}
232+
return data
233+
}
234+
```
235+
236+
由源码可知,我们对 `data (rows)` 内的各个元素 `item (row)` 设置了一个不可遍历属性 `__nested__`,包含以下三个属性:
237+
238+
| 参数 | 说明 | 类型 | 可选项 | 默认值 |
239+
|---------|--------------|----------|-------------------------------------------------------------|--------|
240+
| comp | 内嵌组件名 | String | - | '' |
241+
| visible | 是否显示 | Boolean | true / false | false |
242+
| $toggle | 便捷操作函数 | Function | $toggle(comp) / $toggle(visible) / $toggle(comp, visible) | - |
243+
244+
`__nested__` 作为 `props.nested` 传入到对应的 `tdComp``nested component`
245+
由此,在对应的动态组件内部即可通过 `nested.$toggle` 实现对 `nested component` 的控制
246+
(当然,您要直接操作 `row.__nested__.$toggle` 也是没问题的,都是同一个东西)
247+
248+
***
249+
250+
#### `:query`
251+
让我们来看看 Datatable 是如何初始化 `query` 的(见源码 [`lib/index.vue`](lib/index.vue) 中的 `created` 钩子函数):
252+
253+
```js
254+
created () { // init query
255+
const { query } = this
256+
const q = { limit: 10, offset: 0, sort: '', order: '', ...query }
257+
Object.keys(q).forEach(key => this.$set(query, key, q[key]))
258+
}
259+
```
260+
261+
一般情况下,您只需要传入一个空对象 `{}` 即可。若还有其他查询条件(例如 `search`),则以下两种方式二选一:
262+
1. 在初始化时就传入 `{ search: '' }`(推荐)
263+
2. 自行使用 [`Vue.set / $vm.$set`](https://vuejs.org/v2/api/#Vue-set) 设置:`this.$set(this.query, 'search', '')`
264+
265+
上述两种方式均在 `example/Advanced` 中有所体现,最终目的都是让额外的查询属性变成[响应式](https://vuejs.org/v2/guide/reactivity.html)
266+
(其中第 2 种见 [`example/Advanced/comps/th-Filter.vue`](example/Advanced/comps/th-Filter.vue) 中的 `methods.search`
267+
268+
在此提一个常见的需求:实现刷新后保持查询条件
269+
最常见的解决方案是**同步查询条件到 URL**。拿 `example/Basic` 来说:
270+
271+
```html
272+
<template>
273+
<div>
274+
<code>query: {{ query }}</code>
275+
<datatable v-bind="$data" />
276+
</div>
277+
</template>
278+
<script>
279+
import Datatable from 'vue2-datatable'
280+
import mockData from '../_mockData'
281+
282+
export default {
283+
components: { Datatable },
284+
data () {
285+
return {
286+
columns: [
287+
{ title: 'User ID', field: 'uid', sort: true },
288+
{ title: 'Username', field: 'name' },
289+
{ title: 'Age', field: 'age', sort: true },
290+
{ title: 'Email', field: 'email' },
291+
{ title: 'Country', field: 'country' }
292+
],
293+
data: [],
294+
total: 0,
295+
query: this.$route.query // 初始化时传入 URL query(在业务中请注意安全性)
296+
}
297+
},
298+
watch: {
299+
// 同步本地 query 到 URL query
300+
query: {
301+
handler (query) {
302+
this.$router.push({ path: this.$route.path, query })
303+
},
304+
deep: true
305+
},
306+
// 通过监听 URL query 的变化来重新获取数据
307+
'$route.query' (query) {
308+
mockData(query).then(({ rows, total }) => {
309+
this.data = rows
310+
this.total = total
311+
})
312+
}
313+
}
314+
}
315+
</script>
316+
```
317+
318+
***
319+
320+
#### `:selection`
321+
一般情况下,您只需要传入一个空数组 `[]` 即可
322+
若有行被勾选,则对应的 `row` 将会进入到 `selection`
323+
假如您的产品经理要求默认就是全部勾选,也是没问题的。就以 `example/Advanced` 为例:
324+
325+
```js
326+
methods: {
327+
handleQueryChange () {
328+
mockData(this.query).then(({ rows, total, summary }) => {
329+
this.data = rows
330+
this.total = total
331+
this.summary = summary
332+
333+
// 就是这么简单!
334+
this.$nextTick(() => this.selection = [...this.data])
335+
})
336+
}
337+
}
338+
```
339+
340+
***
341+
342+
#### `:xprops`
343+
由于 `thComp / tdComp / nested component` 都是通过动态组件实现
344+
而业务需求又是不固定的,很可能需要传入很多额外的 props
345+
那么,源码 [`lib/index.vue`](lib/index.vue) 中的模板,很有可能会演变成这样子:
346+
347+
```html
348+
<component
349+
...
350+
:XXX="XXX"
351+
:YYY="YYY"
352+
:ZZZ="ZZZ"
353+
:XYZ="XYZ"
354+
:ZYX="ZYX"
355+
...><!-- 100+ extra props -->
356+
</component>
357+
```
358+
359+
再三斟酌下,我引入了 `xprops`,用于承载这些额外的 props,以避免污染源码
360+
361+
最常见的用处是,传入一个仅限于当前 Datatable 内部使用的 [eventbus](https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication),这样的话就不需要区分命名空间了
362+
(可以实现无限递归嵌套的 `example/Advanced` 就是通过这种方式来避免不必要的麻烦)
363+
364+
## 深入
13365

14366
## 设计理念
15367
full ES5
16-
关键:动态组件(全局化 局部化)
368+
关键:扁平的动态组件(全局化 局部化)
17369
反范式:允许子组件修改状态,毋须引入鸡肋的状态管理
18370
xprops 传递其余属性
19-
20-
## 源码技巧
21-
缩短 .map
371+
源码技巧 缩短 .map
372+
同步 URL 技巧

example-dist/client.73be7c4a.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

example-dist/client.96937642.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

example-dist/client.ca6aaf73.css

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)