大涛子客栈

基于 Ant Design 进行测试右键菜单组件、数据加载组件的使用体验

React 右键菜单组件方案调研

基于 Ant Design 进行测试,涉及到的两个组件库分别是:

React Contexify

官方文档:REACT-CONTEXIFY

Getting Started

下载:

1
2
3
yarn add react-contexify
# or
npm install --save react-contexify

引入:

1
2
import { Menu, Item, Separator, Submenu, MenuProvider } from 'react-contexify'
import 'react-contexify/dist/ReactContexify.min.css'

API

  • Menu, Item 两者组成了右键菜单的整体内容
  • Menu 可以通过 theme, anmation 设置主题和动画,还可以监听菜单显示、消失事件
  • Submenu 子菜单栏
  • Item 可监听点击事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Menu
id="menu_id"
theme={theme.dark}
animation={animation.zoom}
onShown={() => console.log('SHOWN')}
onHidden={() => console.log('HIDDEN')}
>
<Item onClick={onClick}>Lorem</Item>
<Item disabled>Disabled</Item>
<Separator />
<Submenu label="Foobar" arrow="&gt;">
<Item onClick={onClick}>Foo</Item>
</Submenu>
</Menu>
  • MenuProvider 展示可右击区域,通过 id 与 Menu 绑定
1
2
3
<MenuProvider id="menu_id" component="span" event="onClick">
<MyComponent />
</MenuProvider>
  • contextMenu 自定义可点击区域时使用,有 show 和 hideAll 两种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const handleEvent = e => {
e.preventDefault()
contextMenu.show({
id: 'menu_id',
event: e,
props: {
foo: 'bar'
}
})
}

const App = () => (
<div>
<div onContextMenu={handleEvent}>Right click me...</div>
<Menu />
</div>
)
  • Separator 分割线
  • IconFont 在 Item 中添加 Icon
  • theme 可选 dark, light
  • animation 可选 zoom, pop, flip, fade

Demo

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
import React from 'react'
import { Button } from 'antd'
import {
Menu,
Item,
Separator,
Submenu,
MenuProvider,
contextMenu,
IconFont,
theme,
animation
} from 'react-contexify'
import 'react-contexify/dist/ReactContexify.min.css'

const menuId = 'menuId'
const myMenuId = 'myMenuId'

// 禁止点击Item或者Submenu的方式
const isDisabled = ({ event, props }) => true

// 菜单点击事件
const onClick = ({ event, props }) => console.log(event, props)

const onClickMenu = ({ event, props }) => {
alert(
JSON.stringify(
{
x: event.clientX,
msg: props.msg
},
null,
2
)
)
}

// 创建菜单内容
const MyAwesomeMenu = () => (
<Menu
id={menuId}
theme={theme.dark}
animation={animation.zoom}
onShown={() => console.log('SHOWN')}
onHidden={() => console.log('HIDDEN')}
>
<Item onClick={onClick}>Lorem</Item>
<Item>
<IconFont className="fa fa-trash" />
Delete
</Item>
<Separator />
<Item disabled>Dolor1</Item>
<Item disabled={isDisabled}>Dolor2</Item>
<Separator />
<Submenu label="Foobar" arrow="&gt;">
<Item onClick={onClick}>Foo</Item>
</Submenu>
</Menu>
)

const MyMenu = () => (
<Menu id={myMenuId}>
<Item onClick={onClickMenu}>Click Me</Item>
</Menu>
)

// 自定义菜单内容
function handleEvent(e) {
e.preventDefault()
contextMenu.show({
id: myMenuId,
event: e,
props: {
msg: 'hello'
}
})
}

class ContextMenu extends React.Component {
render() {
return (
<div>
<MenuProvider id={menuId}>
<Button type="primary" size="large">
使用 MenuProvider
</Button>
</MenuProvider>
<MyAwesomeMenu />
<br />
<Button onContextMenu={handleEvent} type="primary" size="large">
未使用 MenuProvider
</Button>
<MyMenu />
</div>
)
}
}

export default ContextMenu

React Contextmenu

Installation

Using npm

1
npm install --save react-contextmenu

Using yarn

1
yarn add react-contextmenu

API

完整文档:API

The module exports the following:

  • ContextMenu, Base Contextmenu Component.
  • ContextMenuTrigger, Contextmenu Trigger Component.
  • MenuItem, A Simple Component for menu items.
  • SubMenu, A component for using submenus within the contextmenu.

Usage

地址:Usage

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
import React from 'react'
import ReactDOM from 'react-dom'
import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu'

function handleClick(e, data) {
console.log(data.foo)
}

function MyApp() {
return (
<div>
<ContextMenuTrigger id="some_unique_identifier">
<div className="well">Right click to see the menu</div>
</ContextMenuTrigger>

<ContextMenu id="some_unique_identifier">
<MenuItem data={{ foo: 'bar' }} onClick={this.handleClick}>
ContextMenu Item 1
</MenuItem>
<MenuItem data={{ foo: 'bar' }} onClick={this.handleClick}>
ContextMenu Item 2
</MenuItem>
<MenuItem divider />
<MenuItem data={{ foo: 'bar' }} onClick={this.handleClick}>
ContextMenu Item 3
</MenuItem>
</ContextMenu>
</div>
)
}

ReactDOM.render(<MyApp myProp={12} />, document.getElementById('main'))

对比异同

特性react-contexifyreact-contexmenu
版本@4.1.1@2.14.0
大小524 kB154 kB
周下载量8,92844,355
文件数8330
Coverage97%
Dependencies22
Dev Dependencies3736
Last publisha year ago3 months ago
Issues33 Open 71 Closed1 Open 213 Closed
更新版本数3254
浏览器支持latestFireFox38+ Chrome47+ Opera34+ Safari8+
IE11+
主题、动画、Icon
文档Demo 操作演示,API 文档API 文档,example 示例
API较多(8)较少(4)
API PropTypes较少较多
上手友好度良好一般

React 数据加载组件对比

基于 Ant Design 进行测试,涉及到的两个组件库分别是:
react-virtualized
react-infinite-scroller

对比异同

特性react-virtualizedreact-infinite-scroller
版本@9.22.2@1.2.4
大小2.27 MB211 kB
Star20k2.5k
周下载量715,678225,036
文件数25214
Coverage95%unknown
Contributors1931
Last publish2 months ago2 years ago
Issues314 Open 691 Closed57 Open 110 Closed
更新版本数2009
浏览器支持iOS and Android,IE 9+(user-defined)unknown
文档API 文档,example 示例API 文档,example 示例
特点大量的列表以及表格式数据,只加载可见区域的组件滚动加载,一直到无数据

react-virtualized

Getting started

Install react-virtualized using npm.

1
npm install react-virtualized --save

For example

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
155
156
157
158
159
160
161
162
163
164
165
166
import { List, message, Avatar, Spin } from 'antd'

import reqwest from 'reqwest'

import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller'
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'
import VList from 'react-virtualized/dist/commonjs/List'
import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader'

const fakeDataUrl =
'https://randomuser.me/api/?results=5&inc=name,gender,email,nat&noinfo'

const loadingStyle = {
position: 'absolute',
bottom: '-40px',
left: '30%'
}

class VirtualizedExample extends React.Component {
state = {
data: [],
loading: false
}

loadedRowsMap = {}

componentDidMount() {
this.fetchData(res => {
this.setState({
data: res.results
})
})
}

fetchData = callback => {
reqwest({
url: fakeDataUrl,
type: 'json',
method: 'get',
contentType: 'application/json',
success: res => {
callback(res)
}
})
}

handleInfiniteOnLoad = ({ startIndex, stopIndex }) => {
let { data } = this.state
this.setState({
loading: true
})
for (let i = startIndex; i <= stopIndex; i++) {
// 1 means loading
this.loadedRowsMap[i] = 1
}
if (data.length > 19) {
message.warning('Virtualized List loaded all')
this.setState({
loading: false
})
return
}
this.fetchData(res => {
data = data.concat(res.results)
this.setState({
data,
loading: false
})
})
}

isRowLoaded = ({ index }) => !!this.loadedRowsMap[index]

renderItem = ({ index, key, style }) => {
const { data } = this.state
const item = data[index]
return (
<List.Item key={key} style={style}>
<List.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
}
title={<a href="https://ant.design">{item.name.last}</a>}
description={item.email}
/>
<div>Content</div>
</List.Item>
)
}

render() {
const { data } = this.state
const vlist = ({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered,
width
}) => (
<VList
autoHeight
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
overscanRowCount={2}
rowCount={data.length}
rowHeight={73}
rowRenderer={this.renderItem}
onRowsRendered={onRowsRendered}
scrollTop={scrollTop}
width={width}
/>
)
const autoSize = ({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered
}) => (
<AutoSizer disableHeight>
{({ width }) =>
vlist({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered,
width
})
}
</AutoSizer>
)
const infiniteLoader = ({
height,
isScrolling,
onChildScroll,
scrollTop
}) => (
<InfiniteLoader
isRowLoaded={this.isRowLoaded}
loadMoreRows={this.handleInfiniteOnLoad}
rowCount={data.length}
>
{({ onRowsRendered }) =>
autoSize({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered
})
}
</InfiniteLoader>
)
return (
<List>
{data.length > 0 && <WindowScroller>{infiniteLoader}</WindowScroller>}
{this.state.loading && <Spin style={loadingStyle} />}
</List>
)
}
}

export default VirtualizedExample

使用 React hooks 方式:

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
import { useState } from 'react'
import { List, message, Avatar, Spin } from 'antd'

import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller'
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'
import VList from 'react-virtualized/dist/commonjs/List'
import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader'

function Virtualized({ FileItem, data }) {
console.log(data)
const [loading, setLoading] = useState(false)

const loadedRowsMap = {}

const handleInfiniteOnLoad = ({ startIndex, stopIndex }) => {
let { data } = data
setLoading(true)
for (let i = startIndex; i <= stopIndex; i++) {
// 1 means loading
oadedRowsMap[i] = 1
}
if (data.length > 19) {
message.warning('Virtualized List loaded all')
setLoading(false)
return
}
// this.fetchData((res) => {
// data = data.concat(res.results)
// this.setState({
// data,
// loading: false,
// })
// })
}
const isRowLoaded = ({ index }) => !!loadedRowsMap[index]
const renderItem = ({ index, key, style }) => {
const item = data[index]
return <FileItem key={key} listData={item} style={style} />
}

const vlist = ({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered,
width
}) => (
<VList
autoHeight
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
overscanRowCount={2}
rowCount={data.length}
rowHeight={73}
rowRenderer={renderItem}
onRowsRendered={onRowsRendered}
scrollTop={scrollTop}
width={width}
/>
)
const autoSize = ({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered
}) => (
<AutoSizer disableHeight>
{({ width }) =>
vlist({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered,
width
})
}
</AutoSizer>
)
const infiniteLoader = ({
height,
isScrolling,
onChildScroll,
scrollTop
}) => (
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={handleInfiniteOnLoad}
rowCount={data.data.length}
>
{({ onRowsRendered }) =>
autoSize({
height,
isScrolling,
onChildScroll,
scrollTop,
onRowsRendered
})
}
</InfiniteLoader>
)
return (
<>
{data?.data && data.data.length > 0 && (
<WindowScroller>{infiniteLoader}</WindowScroller>
)}
{loading && <Spin className="loading" />}
<style jsx>
{`
.loading {
position: absolute;
bottom: -40px;
left: 50%;
}
`}
</style>
</>
)
}

export default Virtualized

react-infinite-scroller

Getting started

1
npm install react-infinite-scroller --save

How to use:

1
import InfiniteScroll from 'react-infinite-scroller'

For example

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
import React from 'react'
import { List, message, Avatar, Spin } from 'antd'
import reqwest from 'reqwest'
import InfiniteScroll from 'react-infinite-scroller'

import style from './list.css'

const fakeDataUrl =
'https://randomuser.me/api/?results=5&inc=name,gender,email,nat&noinfo'

class InfiniteListExample extends React.Component {
state = {
data: [],
loading: false,
hasMore: true
}

componentDidMount() {
this.fetchData(res => {
this.setState({
data: res.results
})
})
}

fetchData = callback => {
reqwest({
url: fakeDataUrl,
type: 'json',
method: 'get',
contentType: 'application/json',
success: res => {
callback(res)
}
})
}

handleInfiniteOnLoad = () => {
let { data } = this.state
this.setState({
loading: true
})
if (data.length > 14) {
message.warning('Infinite List loaded all')
this.setState({
hasMore: false,
loading: false
})
return
}
this.fetchData(res => {
data = data.concat(res.results)
this.setState({
data,
loading: false
})
})
}

render() {
return (
<div className={style['demo-infinite-container']}>
<InfiniteScroll
initialLoad={false}
pageStart={0}
loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore}
useWindow={false}
>
<List
dataSource={this.state.data}
renderItem={item => (
<List.Item key={item.id}>
<List.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
}
title={<a href="https://ant.design">{item.name.last}</a>}
description={item.email}
/>
<div>Content</div>
</List.Item>
)}
>
{this.state.loading && this.state.hasMore && (
<div className={style['demo-loading-container']}>
<Spin />
</div>
)}
</List>
</InfiniteScroll>
</div>
)
}
}

export default InfiniteListExample

list.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
.demo-infinite-container {
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: auto;
padding: 8px 24px;
height: 300px;
}
.demo-loading-container {
position: absolute;
bottom: 40px;
width: 100%;
text-align: center;
}

 评论