React 实现全局组件

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
28
import 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中。有多种方式可以选择:

  1. ReactDOM.render(element, container[, callback]),这个方法的作用是把React元素渲染到指定的容器中,也是我们最常用的一种渲染到DOM的方法。
  2. 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
2
3
4
5
6
7
8
9
10
Message.init = container => {
// 创建内容容器
let root = document.createElement('div')
if (container) {
container.appendChild(root)
} else {
document.body.appendChild(root)
}
render(<Message />, root)
}

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
24
Message.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
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
import Message from './message'

let messageInstance;

let index = 0;

function addMessage(content) {
const key = `${index++}_MESSAGE_UNIQ_KEY`
messageInstance.add({ key, content })
setTimeout(() => {
messageInstance.remove(key)
}, 3000)
}

function message(content) {
function callback(instance) {
messageInstance = instance
addMessage(content)
}
if(!messageInstance) {
Message.init(null, callback)
} else {
addMessage(content)
}
}

export default message

上面的代码通过callback拿到了组件对外的接口并对外缓存messageInstance,针对每条消息生成了一个UNIQUE_KEY用于后续的消息移除。这里需要注意的是,Message只能实例化一次。

总结

至此,一个全局组件message已经完成了。当然,如果你想要看到更好的效果,还需要对容器和消息本身添加样式。我们还可以自定义消息组件,将接口通过props传入到组件中,更好的管理消息。

React也提供了ReactDOM.unmountComponentAtNode(container)方法来卸载一个组件,当message组件不再需要的时候,最好将它从DOM中移除。

前面提到过ReactDOM.createPortal(child, container)同样可以将节点渲染到DOM。不多想要这种方式并不是很适合这种场景,因为对外暴露接口是一个难题,这里就不在演示了。

完。

分享到 评论