添加模态对话框支持
用 Redux 描述并渲染 React 模态对话框,搭建 ModalManager 与 reducer
正如你可能已经注意到的,本课程的目的并不是构建一个功能完整且有意义的应用程序。它的主要目标是让我有机会展示一些实用的 React 和 Redux 技术。这意味着其中会有一些明显过度设计的地方。比如模态对话框——这个应用其实并不需要模态对话框,但没关系:我们还是要给 Project Mini-Mek 加上模态对话框 :)
在 React 中处理模态对话框并用 Redux 驱动它们的基本概念已经在其他地方被多次详细描述过。我的参考链接列表中包含许多相关文章,以下是精选阅读清单:
- Dave Ceddia 的文章《React 中的模态对话框》展示了渲染和控制模态对话框的基本方法。
- Dan Abramov 在 Stack Overflow 上的回答《如何使用 Redux 控制模态对话框》说明了如何编写包含模态描述信息的 reducer,并通过一个中心化的 React 组件来渲染它。
- David Gilbertson 的文章《React 中的模态对话框》讨论了将模态对话框置于应用其他内容之上的不同方法之间的权衡。
- Mike Vercoelen 的文章《使用 React 和 Redux 实现可扩展的模态对话框》实际演示了 Dan 提出的基于 Redux 的模态对话框技术。
- 最后,我自己的博客文章《从通用“选择器”模态对话框中获取返回值》也值得一看。
我们将把这些概念付诸实践,并在某些方面进一步拓展。
用 Redux 驱动 React 模态对话框
我们先来搭建显示单个模态对话框所需的各个组件。我们需要以下几个部分:
- 一个
ModalManager组件,它接收要显示的模态组件的描述、该模态应传入的 props,以及一个可用模态组件的查找表,并根据描述渲染对应的模态组件; - 用于存储和清除当前模态描述的动作(actions)和 reducers;
- 一个实际要显示的模态组件。
features/modals/modalsReducer.js
1import { 2 MODAL_CLOSE, 3 MODAL_OPEN 4} from "./modalConstants"; 5 6import {createReducer} from "common/utils/reducerUtils"; 7 8const initialState = null; 9 10export function openModal(state, payload) { 11 const {modalType, modalProps} = payload; 12 return {modalType, modalProps}; 13} 14 15export function closeModal(state, payload) { 16 return null; 17} 18 19export default createReducer(initialState, { 20 [MODAL_OPEN]: openModal, 21 [MODAL_CLOSE]: closeModal 22});
这个 reducer 的逻辑非常简单:我们只需存储一个包含模态类型名称和其应接收的 props 的对象,然后设置或清空该值。
1import React, {Component} from "react"; 2import {connect} from "react-redux"; 3import { 4 Modal, 5} from "semantic-ui-react"; 6 7import {closeModal} from "features/modals/modalActions"; 8 9const actions = {closeModal}; 10 11export class TestModal extends Component { 12 render() { 13 return ( 14 <Modal 15 closeIcon="close" 16 open={true} 17 onClose={this.props.closeModal} 18 > 19 <Modal.Header>模态 #1</Modal.Header> 20 <Modal.Content image> 21 <Modal.Description> 22 <p>这是一个模态对话框。很酷吧?</p> 23 </Modal.Description> 24 </Modal.Content> 25 <Modal.Actions> 26 </Modal.Actions> 27 </Modal> 28 ) 29 } 30} 31 32export default connect(null, actions)(TestModal);
Semantic-UI-React 已经提供了一个 Modal 类,可以方便地渲染标题、内容和操作按钮。由于本文的重点是展示如何通过 React 和 Redux 来操控模态对话框,而不是从零开始构建模态组件,因此我们将直接使用 SUI-React 内置的 Modal 组件,而不是自行实现。
注意我们向 <Modal> 传递了 open={true} 属性。这是因为 SUI-React 的 Modal 可以处于打开或关闭状态,而在我们的场景中,每当显示 <TestModal> 时,内部的 <Modal> 都应该被展示出来。
features/modals/ModalManager.jsx
1import React, {Component} from "react"; 2import {connect} from "react-redux"; 3 4import TestModal from "./TestModal"; 5 6const modalComponentLookupTable = { 7 TestModal 8}; 9 10const mapState = (state) => ({currentModal: state.modals}); 11 12export class ModalManager extends Component { 13 render() { 14 const {currentModal} = this.props; 15 16 let renderedModal; 17 18 if (currentModal) { 19 const {modalType, modalProps = {}} = currentModal; 20 const ModalComponent = modalComponentLookupTable[modalType]; 21 22 renderedModal = <ModalComponent {...modalProps} />; 23 } 24 25 return <span>{renderedModal}</span> 26 } 27} 28 29export default connect(mapState)(ModalManager);
现在我们进入了基本模态对话框架构的核心部分。不过,在深入之前,先回顾一下背后的理论。
在典型的面向对象 GUI 工具包中,你可能会写类似 const myModal = new MyModal(); myModal.show() 的代码,由模态类自己负责显示。jQuery 风格的 UI 插件也是如此。此时,你的应用程序中存在某种“隐式状态”:应用正在“显示一个模态”,但除了创建了 MyModal 实例,或者在网页中新增了元素之外,没有明确的方式来追踪这一状态。这种“是否正在显示模态”的状态本质上是隐式的,而并未被显式记录。
但在 React(尤其是结合 Redux 时),我们被鼓励尽可能让所有状态都变得显式。 这也是为什么你会经常看到像 requesting: true 这样的状态值——因为现在你可以明确地根据是否有正在进行的 AJAX 请求来决定 UI 行为(例如显示加载动画)。在我们的场景中,我们希望显式地跟踪当前是否正在显示模态,甚至更进一步,记录下当前模态的具体描述信息。
我们的 <ModalManager> 组件将位于应用组件树的较顶层。它从 Redux 中读取当前的模态描述。如果存在描述对象,我们就需要真正地显示一个模态。
我们建立一个查找表,键是某个字符串标识符,值是具体的模态组件。在这个例子中,我们直接使用变量名作为键(利用 ES6 对象字面量的简写语法),因此当 modalType 是 "TestModal" 时,就会取出对应的组件类。一旦获得了正确的模态组件类,就可以使用标准的 JSX 语法进行渲染。同时,我们还可以将描述中包含的任何 props 对象传递给该模态组件。
最终效果如下:

一个模态对话框!还有背景遮罩!是不是很激动? :)
从代码角度看,这个思路的核心就这些:使用一个描述性值来判断是否应该显示模态,如果要显示,则确定是哪一个,然后渲染它并传入所需 props。但我们可以在此基础上进行大量有用的扩展。