React 实现全局组件
有一个这样的需求:用户进入首页时可能会有不同类型的对话框弹出,默认的情况下所有对话框都是打开的,这很影响用户体验。在无法减少对话框的前提下,需要实现一种机制,能够让对话框依次弹出。
在React-Native
中,一种不那么好的实现就是使用DeviceEventEmitter
(一种类似Node
中的事件机制),并且创建一个容器组件用于管理对话框弹出,关闭。最终把容器组件挂载到页面中即可。
为了实现这样的需求,除了消息的收发管理,难点还在于如何把容器组件挂载到应用中,由于React-Native
中并不存在DOM
,所以要采用其它的方式,确保该容器挂载后,整个App
使用阶段不会被卸载即可。
就此打住。
React中实现全局组件的思想与之类似,下面就来实现一个
message
全局组件。
最终的实现应该是这样使用的:1
2
3
4
5
6
7
8
9
10
11...
import message from './message'
class App extends React.Component {
...
handleClick = () => {
message('这是一条message')
}
render() {
return <Button onClick={this.handleClick}>点击</Button>
}
}
容器组件
容器组件用于管理message
(提供一系列增删接口),并且渲染到DOM
。
一个简易的容器组件可以是下面这样: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
28import React, { Component } from 'react';
import './style.css';
class Message extends Component {
state = {
messageList: []
}
add = (content) => {
const { messageList } = this.state
this.setState({ messageList: [content, ...messageList]})
}
remove = (key) => {
const { messageList } = this.state
const result = messageList.filter(item => item.key !== key)
this.setState({ messageList: result })
}
clear() {
this.setState({ messageList: [] })
}
render() {
const { messageList } = this.state
const nodes = messageList.map(item => item.component ? item.component : <div className="message-item">{item.content}</div>)
return (
<div className="message-container">
{ nodes }
</div>
)
}
}
上面实现的容器组件提供了add
/remove
/clear
三个方法来对message
进行管理,并最终渲染当前messageList
中所有的消息。
接下来需要考虑如何把容器组件渲染到DOM
,也可以说是把容器组件插入到DOM
中。有多种方式可以选择:
ReactDOM.render(element, container[, callback])
,这个方法的作用是把React
元素渲染到指定的容器中,也是我们最常用的一种渲染到DOM
的方法。ReactDOM.createPortal(child, container)
,该方法的作用是将子节点渲染到存在于父组件以外的 DOM 节点中,该方法是这样使用的:1
2
3
4
5
6
7
8
9// 见:[https://zh-hans.reactjs.org/docs/portals.html]
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
既然是全局message
组件,那么其一个理想的挂载的地方可以是body
或者指定的DOM
节点。下面,分别实现以上两种方式。
1 | Message.init = container => { |
在Message
上新增了一个静态函数init
,调用该函数就会把Message
组件渲染到指定的DOM
元素或者是document.body
上。
这里有一个疑问,Message
组件中提供了一系列接口来管理message
,但通过以上的方式地区把Message
组件挂载到DOM
上了,却没法暴露接口供外部方法。这里有多种方式,第一种是ReactDOM.render(element, container[, callback])
的返回值是其实是组件实例的引用,有了这个引用,外部就可以调用这些接口。这种方式已经不推荐使用,而推荐的方式也就是第二种方式callback ref
,我们可以给组件绑定一个回调类型的ref
,而这个callback
以组件实例作为参数,我们可以通过这种方式来向外部暴露组件实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Message.init = (container, callback) => {
// 创建内容容器
let root = document.createElement('div')
if (container) {
container.appendChild(root)
} else {
document.body.appendChild(root)
}
// callback
function ref(message) {
callback({
add(content){
message.add(content)
},
remove(key) {
message.remove(key)
},
clear() {
message.clear()
}
})
}
render(<Message ref={ref} />, root)
}
我们给init
方法增加了一个callback参数,通过这个callback就可以把组件实例暴露到外部中去。在callback ref
中,我们调用init
传入的callback
参数,并把组件实例作为callback
的参数,实际上,暴露整个组件实例是非常危险的,因此这里只是暴露了实例的几个外部调用接口。
外部接口
接下来实现外部接口,也就是我们外部调用的方法message(content)
。实现容器组件的时候,我们实现了一个静态方法Message.init
,该方法初始化了一个容器并挂载到DOM
,并通过callback
的方式对外暴露接口来管理messageList
。因此,这个外部调用的方式只需要去实现实现这个callback
。例如,我们实现的这个message(content)
调用后三秒后就会消失。
1 | import Message from './message' |
上面的代码通过callback
拿到了组件对外的接口并对外缓存messageInstance
,针对每条消息生成了一个UNIQUE_KEY
用于后续的消息移除。这里需要注意的是,Message
只能实例化一次。
总结
至此,一个全局组件message
已经完成了。当然,如果你想要看到更好的效果,还需要对容器和消息本身添加样式。我们还可以自定义消息组件,将接口通过props
传入到组件中,更好的管理消息。
React
也提供了ReactDOM.unmountComponentAtNode(container)
方法来卸载一个组件,当message
组件不再需要的时候,最好将它从DOM
中移除。
前面提到过ReactDOM.createPortal(child, container)
同样可以将节点渲染到DOM
。不多想要这种方式并不是很适合这种场景,因为对外暴露接口是一个难题,这里就不在演示了。
完。