作者:何方舟
在介绍组件化方案之前,先对 react 和 redux 做一个简单介绍。
Why React
理想中的组件化,第一步应该就是组件的标签化, 例如有一个 Header 组件,如下图所示
无需关注组件内部的实现,我们只需要使用一个
标签就能调用它,通过设置属性的方式,来控制它的显示的内容,和对应的事件。
class Page extends Component {
render () {
<div>
<Header onAttend={click} anchorInfo={anchorInfo} members={members}/>
</div>
}
}
onAttend 决定点击关注时会触发的事件
anchorInfo 决定左侧展示的主播信息
members 坐定右侧展示的成员信息
借助 jsx 语法,React 已经实现上述想法。
Why Redux
在简单的应用中,上面的组件化方案是非常清晰的,因为
组件被任何其他组件使用,且没有任何副作用。
但是由于 React 的数据流向是单向的, 子组件的数据和方法只能由父级组件赋予,一旦组件嵌套层次变深,传递数据将会变得非常复杂。
拿上面的 Header 组件来说, 它的内部还使用了 Avatar 和 Members 两个组件,Header 把它接受到的数据和方法,又需要传递给了 Avatar 和 Members 。
//Header.js
class Header extends Component {
render () {
<Anchor avatarInfo={this.props.AnchorInfo} onClick={this.props.click} />
<Members members={this.props.members}/>
}
}
当然有人会认为直接在 Header 中申明所需要的数据和方法,不再从父级获得,这样不就解决了深层嵌套的问题吗,但是如此一来数据就和组件耦合到一起了,不同项目使用的 Header 的数据源一般是不同的,这意味着你需要为每个项目都要写一个 Header,提供不同的获取数据方式。
另一方面在假设另一个组件下载条 DownloadBar 中也有使用 anchorInfo 这个数据,
那么 DownloadBar 中也需要维护这个数据。
如果两个组件内部的 anchorInfo 发生变化,那么都需要通知另一个组件也发生变化,因为 anchorInfo 应该是唯一的。
大型应用中不同组件共享同一个数据源的情况是常见的,如果都让组件自身来维护一份的数据,很容易造成数据混乱。
redux 框架解决了这个问题,简单来说,它将 react 由父级传递数据,变为了由一个统一的数据源 store 单向地向各个组件传递数据。
- 原始的 React 架构
- 加入了 Redux 的架构之后的
所有数据都存放在 store 中,组件内部不维护任何数据。
store 提供了 dispatch 方法来触发改变 store 中数据。 dispatch 传入的值被称作 action。 dispatch(action) 之后,会进入到 store 中称为 reducer 的处理函数,这些 reducer 会依据不同的 action 的类型,进行不同的处理,reducer 返回的值就会作为 store 中新的数据,一个 reducer 对应的是 store 中一个数据字段,每多一个reducer, store 中就多一个数据字段。数据发生改变后, store 就会通知对应的组件重新渲染。
通过 redux 框架提供的 connect 高阶函数, 直接从 store 选取需要的数据和申明需要使用的方法传入组件中,这些申明的方法是组件事件具体的逻辑的实现,例如发送请求,上报逻辑等等,所以通常调用 dispatch(action) 的逻辑也会包含在里面。
在 React 作为 UI 组件库的基础上,以 redux 作为状态管理框架,我们定义了4种类型的组件。
展示组件
React 组件即为我们的展示组件。它内部不会维护任何动态的数据,除了部分只和组件本身有关的数据,例如 Video 组件中, playState(播放状态),就是它内部才会拥有的状态,而 src(播放源) 就必须从外部传入。它不会包含各种事件具体的实现,只提供对应的接口(如 onClick),具体的实现都由外部调用者去决定。
存储中心组件
存储中心组件即为上文提到的 redux 架构中的 store。 存储中心组件中默认定义了一些 reducer 处理函数和一些 middleware,还包含了连接 redux 和 react 的高阶函数和向 store 中注入新的 reducer 的方法。
数据组件
数据组件即为 redux 架构中某个action 和 对应的 reducer 的合集。数据组件提供了各种 action 可以去调用,并且定义了对应的 action 去处理,数据组件中必须引用存储中心组件,因为数据组件必须向 store 中注入对应的 reducer 处理函数。例如在 roomInfo 的数据组件中,提供了 enterRoom, loadRoomInfo, leaveRoom 这些 action 供调用者使用,且自动向 store 中添加了 roomInfo 这个数据。
数据组件中也会存在互相依赖的情况,例如 chatmessage 会例如 longpoll 这个数据组件,因为 chatmessage 的 reducer 中需要对 longpoll 的
action 也进行处理。
高阶组件
高阶组件即为经过 connect 高阶组件中申明使用的展示组件和数据组件。 函数处理后的展示组件。通常情况下,被使用的组件一般都是高阶组件。
高阶组件确定向该展示组件传入的属性和方法。高阶组件是和业务耦合的,复用性不强。高阶组件高度聚合,而展示组件和数据组件间又充分解耦。
一个高阶组件中可能包含多个数据组件,例如 Ranklist 这个展示组件,需要由提 roomInfo 和 rankList 这两个数据组件提供数据。
高阶组件可能不会引入任何数据组件的方法,只需 import 对应的数据组件,将reducer 注入进 store
import '@tencent/now-data-roomInfo'
接入组件
- 申明存储中心组件。
- 申明合适的高阶组件。
- 如果没有对应的高阶组件,则申明展示组件和数据组件,创建为新的高阶组件。
- 如果没有对应的展示组件,则创建一个需要的展示组件。回到step2
- 如果没有对应的数据组件,则创建一个需要的数据组件。回到step3
- 编写入口文件,引入各个高阶组件。
实际开发时我们的样子可能是这样的
- 我们接到了一个新的需求,其中大致布局和之前的项目完全一致,改变的点有,这个业务只在 手q 中执行,而且视频的数据源由一个新的 CGI 提供。
- 确认我们需要的组件在这个例子中,需要用的组件有:
- Header 头部
- Video 视频
- Message 消息
- Bubble 点赞
- ToolPanel 工具面板
- 在 tnpm 上查找高阶组件,发现以下高阶组件
- now-highorder-bubble
- now-highorder-message
- now-highorder-toolpanel
- now-highorder-header
- now-highorder-video
其中可以直接使用的组件有
- now-highorder-bubble
- now-highorder-message
- now-highorder-toolpanel
通过 tnpm 安装对应组件
tnpm install @tencent/now-highorder-message @tencent/now-highorder-toolpanel
@tencent/now-highorder-bubble
now-highorder-header 定义的 onClose 事件只能在 NOW APP 中才能执行,
所以不能使用。
now-highorder-video 中引用的数据组件使用的 CGI 数据是一个旧版 CGI 数据
,也不能使用。
- 在项目中自定义一个新的 header 高阶组件, 使用的展示组件和数据组件与 now-highorder-header 中的一样,任然是 now-display-header(展示组件) 和 now-data-header(数组组件), 只是通过 connect 链接的时候,onClose 传入的方法 为新的方法。
通过 tnpm 安装对应的展示组件和数据组件
tnpm install @tencent/now-data-roomInfo @tencent/now-display-header
创建新的 Header 高阶组件 now-highorder-header2
import Header from '@tencent/now-display-header' //引入展示组件
import roomInfo from '@tencent/now-data-roomInfo' //引入数据组件
import connect from 'react-redux'
export default connect((state) => {
const {
roomInfo
} = state
return {
roomInfo
}
}, (dispatch) => {
return {
onClose: () => {
_.mqq('close') //手q中改为调用 mqq 提供的 close 接口
}
}
})(Header)
- 在项目中自定义一个新的 video 的高阶组件,使用的展示组件为现有的 now-display-header, 因为使用了一个新的 CGI, 先新建一个的数据组件 now-data-videoinfo_v2,数据组件必须引用 now-store 中的 addReducer 方法,向store中注入新的字段。
now-data-videoinfo_v2
import {
addReducer
} from '@tencent/now-store';
export function loadVideo(roomId) { //定义action函数
...
}
function videoInfo (state = { // 定义 reducer处理函数
url: '',
}, action) {
...
}
addReducer({ // 向store中注入新的数据
videoInfo
})
在新的 video 高阶组件中引入,这个数据组件和 now-display-video
通过 tnpm 安装对应的展示组件
tnpm install @tencent/now-display-video
创建新的高阶组件 now-highorder-video2
import Video from '@tencent/now-display-video' //引入展示组件
import {loadVideo} from 'now-data-videoinfo_v2' //引入申明的数据组件
export default connect((state) => {
const {
url,
} = state.videoInfo
return {
src: url
}
}, (dispatch) => {
return {
onLoad: () => {
return dispatch(loadVideo())
}
}
})(Video)
- 编写入口文件 index.js
引入现有的和刚新建的组件,组装页面。
import React, { Component } from 'react' 引入基础框架
import { Provider, connect } from 'react-redux'
import Store from '@tencent/now-store'; //引入管理组件
import Header from './now-highorder-header2' //引用高阶组件
import Video from './now-highorder-video2'
import Message from '@tencent/now-highorder-message'
import Bubble from '@tencent/now-highorder-bubble'
import ToolPanel from '@tencent/now-highorder-toolpanel'
class PageContainer extends Component { //创建 react 根组件
render () {
return (
<div id="root"> //引用各个组件
<Header />
<Video />
<Message />
<Bubbles />
<ToolPanel />
</div>
)
}
}
const store = new Store() //实例化管理组件
const Root = connect(function(state) {
return state;
})(PageContainer);
ReactDOM.render(
<Provider store={store}>
<Root />
</Provider>,
document.getElementById('container')
) //渲染 React
例如上面代码,需要通过 import 组件 将reducer 注入进 store 即可。
架构的优势
- 组件的引用简单。
- 展示组件和数据组件之间的分离实现了低耦合,而连接两者的高阶组件实现了高内聚。
- 全部由 tnpm 管理,模块管理方便。
- 即使使用了不同了数据管理架构,也可以直接使用展示组件。
一些待解决的问题
- 公用的 css 无法管理,需要引入新的构建工具
- 开发调试不方便,无法单独独立的开发一个组件
- 组件文档缺失。
- 缺乏测试用例,组件迭代后不能保证可靠性。