“工作室招新页面项目”实践中遇到的问题(一)———— React组件通信

Author Avatar
GeniusFunny 2月 13, 2018
  • 在其它设备中阅读本文章

最近学了React基础知识,所以有打算做一个项目来踩坑,恰好适逢大二寒假并且工作室恰好需要在开学后招新,所以天时地利人和,我就用React作为这个项目的技术栈。于是项目开发中的第一个问题就诞生了————React组件通信

问题

来源

material-ui中的组件AppBar和Drawer之间的通信,二者为兄弟组件。

AppBar组件

Drawer组件

功能需求:

  1. 我需要点击AppBar的左侧的icon调出Drawer。
  2. 点击Drawer中的MenuItem跳转到其他页面
  3. 调出Drawer后点击非Drawer组件后收起Drawer。

解决方案

通讯是单向的,数据必须是由一方传到另一方。

父组件与子组件的通信

在 React 中,父组件可以向子组件通过传 props 的方式,向子组件进行通讯。

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
class Parent extends Component {
constructor() {
this.state = {
msg: '今天是情人节耶'
};
}

render() {
return (
<div>
<Child msg={this.state.msg} />
</div>
);
}
}

class Child extends Component {
constructor(props) {
super(props);
}
render() {
return (
<p>this.props.msg</p>
)
}
}

子组件与父组件的通信

子组件向父组件通信,同样需要父组件向子组件传递props,不过这次是传递的是以父组件自身为作用域的函数,子组件负责调用,将要传递的信息传入函数中,作为参数,传递到父组件的作用域中。

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
class Parent extends Component {
constructor() {
this.state = {
msg: '今天是情人节耶'
};
this.transferMsg = this.transferMsg.bind(this);
}

componentDidMount() {
console.log(this.state.msg);
}

componentDidUpdate() {
console.log(this.state.msg);
}

transferMsg(msg) {
this.setState({
msg: msg
});
}

render() {
return (
<div>
<Child transferMsg={this.transferMsg} />
</div>
);
}
}

class Child extends Component {
constructor(props) {
super(props);
this.msg = '我也喜欢你';
}
componentDidMount() {
this.props.transferMsg(this.msg);
}
render() {
return (
<p>情人节啊</p>
)
}
}

兄弟组件之间的通信

因为AppBar和Drawer为兄弟组件,它们的共同点是拥有一个相同的父组件。
先看看组件结构:

1
2
3
4
<Nav>
<AppBar />
<Drawer />
</Nav>

所以我们可以将父组件Nav作为中转站,AppBar传递信息给Nav,Nav再将信息传递给Drawer;Drawe传递信息给AppBar与此类似。解决方案如下,我们将Drawer的开关状态放在了Nav的state中,声明了更改open值的transferMsg函数,然后将这个函数分别传递个NavBar和Drawer,这样以来真正控制open的值就是NavBar和Drawer组件。具体实现如下:

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
class Nav extends Component {
constructor(props) {
super(props);
this.state = {
open: false
}
this.transferMsg = this.transferMsg.bind(this);
}

transferMsg() {
this.setState({
open: !this.state.open
});
}

render() {
return (
<nav>
<NavBar transferMsg={this.transferMsg}/>
<NavDrawer
msg={this.state.open}
transferMsg={this.transferMsg}
/>
</nav>
);
}
}

class NavBar extends Component {
constructor(props) {
super(props);
this.handleMenuClick = this.handleMenuClick.bind(this);
}

handleMenuClick(event) {
this.props.transferMsg();
}

render() {
return (
<AppBar
title="CTG Club"
iconClassNameRight="mudiocs-icon-navigation-expand-more"
onLeftIconButtonClick={this.handleMenuClick}
/>
);
}
}

class NavDrawer extends Component {
constructor(props) {
super(props);
this.handleToggle= this.handleToggle.bind(this);
this.handleClose = this.handleClose.bind(this);
}

handleToggle() {
this.props.transferMsg();
}

handleClose() {
this.props.transferMsg();
}

render() {
return (
<Drawer
docked={false}
width={200}
open={this.props.msg}
onRequestChange={this.handleToggle}
>
</Drawer>
)
}
}

这个项目中的问题迎刃而解。这代码结构,仿佛还有一点东西。

发布者-订阅者模式

定义

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。

####运用
1.发布-订阅者模式可以广泛应用于异步编程,只是一种替代回调函数的方案。
2。发布-订阅者模式可以取代对象之间硬编码的通知机制,一个对象不再显式调用另一个对象的某个接口,让两个对象松耦合地联系在一起。(这一点就可以上述的解决方案的不足的地方)

实例

最近一年重庆房价蹭蹭往上涨,小明迫于家里的压力,打算在解放碑买置一套总价200万的三居室。小明到了售楼处才发现心仪的房子已经被别人买了,不过后续还有一些尾房推出,但是开发商也不清楚什么时候推出这些尾房。
于是售楼MM记下了小明的电话,答应他尾房一旦推出就打电话给他。后续小码也留了号码,每天等着售楼MM的电话。
这就是一个典型的发布-订阅者模式的例子,逻辑代码实现如下:

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
//把发布—订阅的功能提取出来,放在一个单独的对象内:
const event = {
clientList: [],
listen(key, fn) {
if (!this.clientList[key]) { //如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
},

trigger(...message) {
let key = Array.prototype.shift.call(message), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) { //如果没有订阅该消息,则返回
return false;
}

for (let i = 0, fn = fns[i++]; ) {
fn.apply(this, message); //message是发布消息时附送的参数
}
},

remove(key, fn) {
let fns = this.clientList[key];

if (!fns) { // 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) { //// 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (let i = fns.length - 1; i >= 0; i--) { // 反向遍历订阅的回调函数列表
let _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1); // 删除订阅者的回调函数
}
}
};
}
};

//再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
const installEvent = function (obj) {
for (let i in event) {
obj[i] = event[i];
}
};
const salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', fn1 = function (price) {
console.log('价格= ' + price);
});
salesOffices.listen('squareMeter88', fn2 = function (price) {
console.log('价格= ' + price);
});
salesOffices.remove('squareMeter88', fn1); // 删除小明的订阅
// 小明订阅消息
// 小红订阅消息
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000

对于此项目中问题的解决方案:

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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/*
on、one:on 与 one 函数用于订阅者监听相应的事件,并将事件响应时的函数作为参数,on 与 one 的唯一区别就是,使用 one 进行订阅的函数,只会触发一次,而 使用 on 进行订阅的函数,每次事件发生相应时都会被触发。
trigger:trigger 用于发布者发布事件,将除第一参数(事件名)的其他参数,作为新的参数,触发使用 one 与 on 进行订阅的函数。
off:用于解除所有订阅了某个事件的所有函数。
*/
const eventProxy = {
onObj: {},
oneObj: {},
on: function(key, fn) {
if (this.onObj[key] === undefined) {
this.onObj[key] = [];
}
this.onObj[key].push(fn);
},
one: function(key, fn) {
if (this.oneObj[key] === undefined) {
this.oneObj[key] = [];
}

this.oneObj[key].push(fn);
},
off: function(key) {
this.onObj[key] = [];
this.oneObj[key] = [];
},

trigger: function(...args) {
let key;
if (args.length === 0) {
return false;
}
key = args[0];
args = [].concat(Array.prototype.slice.call(args, 1));

if (this.onObj[key] !== undefined && this.onObj[key].length > 0) {
for (let i in this.onObj[key]) {
this.onObj[key][i].apply(null, args);
}
}

if (this.oneObj[key] !== undefined && this.oneObj[key].length > 0) {
for (let i in this.oneObj[key]) {
this.oneObj[key][i].apply(null, args);
this.oneObj[key][i] = undefined;
}
this.oneObj[key] = [];
}
}
};

const BranchItemDatas = [
{
name: 'Web研发部',
imgUrl: null,
imgTitle: null,
intro: null,
key:1
},
{
name: '移动开发部',
imgUrl: null,
imgTitle: null,
intro: null,
key: 2
},
{
name: '视觉设计部',
imgUrl: null,
imgTitle: null,
intro: null,
key: 3
},
{
name: '产品运营部',
imgUrl: null,
imgTitle: null,
intro: null,
key: 4
},
{
name: '运维安全部',
imgUrl: null,
imgTitle: null,
intro: null,
key: 5
}
];

class Nav extends Component {
render() {
return (
<nav>
<NavBar />
<NavDrawer />
</nav>
);
}
}

class NavBar extends Component {
constructor(props) {
super(props);
this.handleMenuClick = this.handleMenuClick.bind(this);
}
handleMenuClick() {
eventProxy.trigger('open', true);
}
render() {
return (
<AppBar
title="CTG Club"
onLeftIconButtonClick={this.handleMenuClick}
/>
);
}
}

class NavDrawer extends Component {
constructor(props) {
super(props);
this.handleToggle = this.handleToggle.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false
}
}

handleToggle() {
this.setState({
open: !this.state.open
})
}

handleClose() {
this.setState({
open: false
})
}

componentDidMount() {
eventProxy.on('open', () => {
this.setState({
open: true
});
});
eventProxy.on('close', () => {
this.setState({
open: false
});
});
}

render() {
return (
<Drawer
docked={false}
width={200}
open={this.state.open}
onRequestChange={this.handleToggle}
>
<MenuItems />
</Drawer>
)
}
}

class MenuItems extends Component {
handleMenuItemClick() {
eventProxy.trigger('close', false);
}
render() {
return (
<ul className='normal-list'>
<MenuItem onClick={this.handleMenuItemClick} key={0}>首页</MenuItem>
{BranchItemDatas.map((item) => {
return (
<MenuItem onClick={this.handleMenuItemClick} key={item.key}>{item.name}</MenuItem>
);
})}
<MenuItem onClick={this.handleMenuItemClick} key={6}>关于CTG</MenuItem>
<MenuItem onClick={this.handleMenuItemClick} key={7}>联系我们</MenuItem>
</ul>
)
}
}

关于

参考书籍或博文:
《JavaScript设计模式与开发实践》
“React 组件间通讯”————淘宝前端团队

情人节快乐啊
情人节快乐啊
情人节快乐啊
情人节快乐啊