像 angular 一样使用 react 组件

前言

写过angularjs的应该对ng-if, ng-show, ng-repeat等内置指令比较熟悉, 在react里面一般都是用js来实现这些功能, 但是这种组件化(指令化)的书写方式比较简单易懂和便于维护, 比一个接一个的if,map,&&,||嵌套简洁得多, 所以可以自己实现几个指令, 方便使用. 需要更多的时候也可以自己再扩展.

ng-if

ng-if可以说是很简单的指令了, 当if条件为真时, 展示组件, 否则不展示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const NgCmd = ({ if: _if = true, children } = props) => {
if (!_if || !children) return null;
return children;
};
NgCmd.propTypes = {
if: React.PropTypes.any,
};
//使用
ReactDOM.render(
<div>
<NgCmd if={true}>
<div>this is true</div>
<span>true</span>
</NgCmd>
<NgCmd if={false}>
<div>this is false</div>
<span>false</span>
</NgCmd>
</div>,
document.querySelector('#container'),
);

注意, 该指令在react16 以上版本可用, 因为render函数可以返回数组. 低版本不可以, 需要修改返回值.

ng-show

ng-showng-hide还有ng-class可以放在一起说, 因为它们本质上都是改变元素的class. 在react里面不需要特殊的支持, 可以使用classnames插件, 比ng-class更方便. 具体使用方法可以参考文档.

1
2
3
4
5
<div>
<div className={classNames({ hide: this.state.hide })}>ng-hide</div>
<div className={classNames({ hide: !this.state.show })}>ng-show</div>
<div className={classNames({ class-one: this.state.needClassOne })}>ng-class</div>
</div>,

ng-cmd

为了提供控制式组件, 实现一个ng-cmd, 包括ng-ifng-show的功能.

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
import React from 'react';
import { PropTypes as P } from 'prop-types';
import _ from 'lodash';
const NgCmd = ({ if: _if = true, show = true, hide = false, children } = props) => {
if (!_if || !children) return null;
let classHideName = '';
if (!show || hide) {
classHideName = 'hide';
}
return React.Children.map(children, (child, index) => {
if (_.isString(child) || _.isNumber(child)) {
return classHideName ? null : child;
}
if (!React.isValidElement(child)) {
throw `ng-cmd only has React Element`;
}
let childProps = child.props;
let className = '';
if (child.props.className && classHideName) {
className = child.props.className + ' ' + classHideName;
} else if (child.props.className || classHideName) {
className = child.props.className || classHideName;
}
return React.cloneElement(child, {
...childProps,
key: index,
className: className,
});
});
};
NgCmd.propTypes = {
hide: P.any,
show: P.any,
if: P.any,
};
//使用
const Test = () => {
return (
<div>
<NgCmd show={false}>
<div>show - false</div>
</NgCmd>
<NgCmd show={true}>
<div>show - true</div>
</NgCmd>
<NgCmd hide={false}>
<div>hide - false</div>
</NgCmd>
<NgCmd hide={true}>
<div>hide - true</div>
</NgCmd>
<NgCmd if={false}>
<div>if - false</div>
</NgCmd>
<NgCmd if={true}>
<div>if - true</div>
</NgCmd>
</div>
);
};

有个问题需要注意的是, 如果NgCmd组件的子元素是一个自定义组件, 那么需要自己接收props.className并使用到顶级节点上, 否则会造成showhide失效. 顺便提供一个ngCmd装饰器, 用来装饰组件, 即可在组件中直接使用show,hide,if, 不需要使用NgCmd组件.

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
//NgCmd for Decorators and function Component -- function Component dont need NgCmd
function ngCmd(arg) {
if (_.isFunction(arg)) {
const Wrapper = arg;
return class WrapperComponent extends React.Component {
render() {
return (
<NgCmd {...this.props}>
<Wrapper {...this.props} />
</NgCmd>
);
}
};
} else if (_.isArray(arg)) {
let i = 0;
let args = [];
if (arg.length == 0) {
args = ['if', 'show', 'hide'];
} else {
['if', 'show', 'hide'].forEach((value, index) => {
if (arg.indexOf(value) > -1) {
i++;
}
});
if (i == 0) {
throw 'need at least one ng-cmd';
}
args = arg;
}
return (Wrapper) => {
return class WrapperComponent extends React.Component {
render() {
let cmdsObj = {};
['if', 'show', 'hide'].forEach((v, i) => {
if (args.indexOf(v) > -1 && _.has(this.props, v)) {
cmdsObj[v] = this.props[v];
}
});
return (
<NgCmd {...cmdsObj}>
<Wrapper {...this.props} />
</NgCmd>
);
}
};
};
if (_.has(cmdsObj, 'if') && cmdsObj['if'] == false) {
return null;
}
}
}
const Test = () => {
return (
<div>
<NgCmd show={false}>
<Test2 />
</NgCmd>
<NgCmd show={true}>
<Test2 />
</NgCmd>
<NgCmd show={false}>
<Test3 />
</NgCmd>
<NgCmd show={true}>
<Test3 />
</NgCmd>
<NgCmd show={false}>
<Test4 />
</NgCmd>
<NgCmd show={true}>
<Test4 />
</NgCmd>
<NgCmd show={false}>
<Test5 />
</NgCmd>
<NgCmd show={true}>
<Test5 />
</NgCmd>
<NgCmd if={false}>
<Test2 />
</NgCmd>
<NgCmd if={true}>
<Test2 />
</NgCmd>
<NgCmd if={false}>
<Test3 />
</NgCmd>
<NgCmd if={true}>
<Test3 />
</NgCmd>
<NgCmd if={false}>
<Test4 />
</NgCmd>
<NgCmd if={true}>
<Test4 />
</NgCmd>
<Test4 if={true} />
<Test4 if={false} />
<Test4 show={true} />
<Test4 show={false} />
<Test4 hide={true} />
<Test4 hide={false} />
<Test5 if={true} />
<Test5 if={false} />
<Test5 show={true} />
<Test5 show={false} />
<Test5 hide={true} />
<Test5 hide={false} />
<Test6 if={true} />
<Test6 if={false} />
<Test6 show={true} />
<Test6 show={false} />
<Test6 hide={true} />
<Test6 hide={false} />
</div>
);
};
const Test2 = () => {
return 'Test2';
};
const Test3 = (props) => {
return <div>Test3</div>;
};
@ngCmd
class Test4 extends React.Component {
render() {
return <div className={this.props.className}>Test4-{JSON.stringify(this.props)}</div>;
}
}
@ngCmd(['hide', 'if'])
class Test5 extends React.Component {
render() {
return <div>Test5-{JSON.stringify(this.props)}</div>;
}
}
const Test6 = ngCmd((props) => {
return <div className={props.className}>Test6-{JSON.stringify(props)}</div>;
});

ng-switch

另一个比较有用的控件为ng-switch, 否则在react中使用switch只能使用立即执行函数.

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
var Test = (props) => {
return (<div className="test">
//不使用NgSwitch
{(function(){
switch(props.value){
case '1':return <div>one</div>;
case '2':return <div>two</div>;
case '3':return <div>threee</div>;
default : return <div>default</div>;
}
})()}
//使用NgSwitch
<NgSwitch when={props} get={(v) => (v.a)}>
<NgDefault>default</NgDefault>
<NgCase is="1"><div>1</div></NgCase>
<NgCase is="2">2</NgCase>
<NgCase is="3">3</NgCase>
</NgSwitch>
</div>)
<Test value="1"/>
<Test value="2"/>
<Test value="3"/>
<Test value="4"/>

//NgSwitch.js
import React from 'react';
import { PropTypes as P } from 'prop-types';
import _ from 'lodash';
import Immutable from 'immutable';

const NgSwitch = ({ when, get, children, } = props) => {
if (_.isNil(when) || _.isNil(children)) {
return null;
}
let child = null;
let defaultChild = null;
React.Children.forEach(children, function (element) {
if (!React.isValidElement(element)) return;
if (element.type != NgCase && element.type != NgDefault) return;
if (element.type == NgDefault && !defaultChild) {
defaultChild = element;
return;
} else if (element.type == NgDefault && defaultChild) {
throw 'ng-switch can not has two default'
}
if (child == null && element.props.is == (_.isFunction(get) ? get(when) : when)) {
child = element;
}
});
return child || defaultChild;
};
NgSwitch.propTypes = {
when: P.any.isRequired,
get: P.func,
}

const NgCase = ({ is, children } = props) => {
if (!is) {
return null;
}
return children;
};
NgCase.propTypes = {
is: P.any.isRequired,
}

const NgDefault = ({ children, } = props) => {
return children;
};
}

NgSwitch组件中, 可以自定义获取函数get, 如果使用的是immutable, 只需要设置get={(v) => (v.get('value'))}即可.

ng-repeat

ng-repeat作为angular里面的一大利器, 使得循环一个dom结构再也不是很麻烦的事情, 结合ng-if以及ng-class, 可以方便的定义重复元素. 这里也仿造ng-repeat实现一个循环组件, 而且不会有像ng-repeat造成性能不好的问题, 和原始的map写法相比性能可能还有提升(有提供测试 demo, 可以自行测试). 同时为了方便项目组使用, 兼容immutable数据结构和原生数据结构, 提供自定义key(track by), filter函数, sortBy函数. 内置提供$$index, $$isOdd, $$total等相关属性.

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
import React from 'react';
import { PropTypes as P } from 'prop-types';
import _ from 'lodash';
import Immutable from 'immutable';

const NgFor = ({ for: _for, of: _of = 'item', filter = null, sortBy = null, trackBy = null, render } = props) => {
if (!_for || !render) {
return;
}
const _obj = {};
if (Immutable.isCollection(_for)) {
if (filter) {
_for = _for.filter(filter);
}
const _size = _for.count();
if (_size == 0) {
return null;
}
return _for
.sortBy((v) => v[sortBy])
.map((value, index) => {
_obj[_of] = value;
const key = Immutable.fromJS(value).get(trackBy) || index;
var ele = render({
..._obj,
$$index: index,
$$isEnd: index + 1 === _size,
$$isStart: index === 0,
$$isOdd: index % 2 != 0,
$$isEven: index % 2 == 0,
$$total: _size,
$$key: key,
key: key,
});
//when you need High performance, you need set the 'key' props by yourself ?? not sure
return ele.key ? ele : React.cloneElement(ele, { key });
});
} else {
_for = _.filter(_for, filter);
const _size = _.size(_for);
if (_size == 0) {
return null;
}
return _.map(_.sortBy(_for, sortBy), (value, index) => {
_obj[_of] = value;
const key = value[trackBy] || index;
var ele = render({
..._obj,
$$index: index,
$$isEnd: index + 1 === _size,
$$isStart: index === 0,
$$isOdd: index % 2 != 0,
$$isEven: index % 2 == 0,
$$total: _size,
$$key: key,
key: key,
});
return ele.key ? ele : React.cloneElement(ele, { key });
});
}
};
NgFor.propTypes = {
for: P.any.isRequired,
of: P.string.isRequired,
filter: P.func,
sort: P.func,
trackBy: P.string,
};
NgFor.defaultProps = {
of: 'item',
};

var Test = (props) => {
var start = [
{ id: 'a1', value: 'av1' },
{ id: 'a2', value: 'av2' },
{ id: 'a3', value: 'av3' },
];
var arrays = [];
for (var i = 0; i < 5000; i++) {
//for (var i = 0; i < 20000; i++) {
arrays.push({
id: 'a1',
value: 'av1',
});
}
var arraysIm = Immutable.fromJS(arrays);
return (
<div className='test'>
{console.log('NgFor no set self key')}
{console.time('NgFor no set self key')}
<NgFor
for={arrays}
of='item'
render={function (_props) {
return (
<div id={_props.item.id}>
{_props.item.value}
&nbsp;length:
{arrays.length}
</div>
);
}}
></NgFor>
{console.timeEnd('NgFor no set self key')}
{console.log('NgFor set self key')}
{console.time('NgFor set self key')}
<NgFor
for={arrays}
of='item'
render={function (_props) {
return (
<div id={_props.item.id} key={_props.key}>
{_props.item.value}
&nbsp;length:
{arrays.length}
</div>
);
}}
></NgFor>
{console.timeEnd('NgFor set self key')}
{console.log('NgFor Immutable set self key')}
{console.time('NgFor Immutable set self key')}
<NgFor
for={arraysIm}
of='item'
render={function (_props) {
return (
<div id={_props.item.get('id')} key={_props.key}>
{_props.item.get('value')}
&nbsp;length:
{arraysIm.count()}
</div>
);
}}
></NgFor>
{console.timeEnd('NgFor Immutable set self key')}
{console.log('js Map set self key')}
{console.time('js Map set self key')}
{arrays.map((value, index) => {
return (
<div id={value.id} key={index}>
{value.value}
&nbsp;length:
{arrays.length}
</div>
);
})}
{console.timeEnd('js Map set self key')}
</div>
);
};
export { Test };

export default NgFor;

性能测试截图(5000):

作者

Mosby

发布于

2017-08-23

许可协议

评论