React + Redux 组件化方案

作者:何方舟

在介绍组件化方案之前,先对 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'

接入组件

  1. 申明存储中心组件。
  2. 申明合适的高阶组件。
  3. 如果没有对应的高阶组件,则申明展示组件和数据组件,创建为新的高阶组件。
  4. 如果没有对应的展示组件,则创建一个需要的展示组件。回到step2
  5. 如果没有对应的数据组件,则创建一个需要的数据组件。回到step3
  6. 编写入口文件,引入各个高阶组件。

实际开发时我们的样子可能是这样的

  1. 我们接到了一个新的需求,其中大致布局和之前的项目完全一致,改变的点有,这个业务只在 手q 中执行,而且视频的数据源由一个新的 CGI 提供。
  2. 确认我们需要的组件在这个例子中,需要用的组件有:
  3. Header 头部
  4. Video 视频
  5. Message 消息
  6. Bubble 点赞
  7. ToolPanel 工具面板
  8. 在 tnpm 上查找高阶组件,发现以下高阶组件
  9. now-highorder-bubble
  10. now-highorder-message
  11. now-highorder-toolpanel
  12. now-highorder-header
  13. 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 数据 ,也不能使用。

  1. 在项目中自定义一个新的 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)
  1. 在项目中自定义一个新的 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)
  1. 编写入口文件 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 即可。

架构的优势

  1. 组件的引用简单。
  2. 展示组件和数据组件之间的分离实现了低耦合,而连接两者的高阶组件实现了高内聚。
  3. 全部由 tnpm 管理,模块管理方便。
  4. 即使使用了不同了数据管理架构,也可以直接使用展示组件。

一些待解决的问题

  1. 公用的 css 无法管理,需要引入新的构建工具
  2. 开发调试不方便,无法单独独立的开发一个组件
  3. 组件文档缺失。
  4. 缺乏测试用例,组件迭代后不能保证可靠性。

原文链接:http://ivweb.io/topic/57c531bc6227a4f55a8872c2

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

我的VS2010+VAssistX

最近越来越觉得VAssistX好用,可能是以前没有去仔细研究过吧,也可能是因为我是个快捷键控吧,不管怎样,用或不用,方便或不方便,它就是那里,一动也不动,进入...

1371
来自专栏hrscy

Unity 基础 - Input 类

任何一款游戏都必须和用户进行交互才行,最常用的就是通过键盘和鼠标进行交互,在 Unity 中想要获取用户的键盘或鼠标的事件的话,就必须使用 Input 类来获取...

1253
来自专栏五毛程序员

五毛的cocos2d-x学习笔记06-处理用户交互

1462
来自专栏智能算法

微信小程序,开发大起底

作者简介:张智超,北京微函工坊开发工程师,CSDN微信开发知识库特邀编辑。微信小程序爱好者。 感谢@翟东平 @qq_31383345 @nigelyq 等热情参...

53614
来自专栏深度学习自然语言处理

爬虫基础入门

为什么要学习爬虫 其实我们身边到处都是爬虫的产物,比如我们经常用的Google,百度,bing等,这些搜索引擎就是根据你的需求在网上爬去相关的网页;比如...

3758
来自专栏HTML5学堂

JavaScript | 选中并获取多行文本框内容的效果

HTML5学堂(码匠):文本操作一直是开发中不可避免的存在,用户选中的文本内容,是否可以进行获取并处理到需要的位置当中?如果可以,这样的操作到底需要使用到哪些方...

4356
来自专栏腾讯IVWEB团队的专栏

React V16 给我们带来了那些东西 ?

在如今越来越复杂的前端环境下,往往可能需要加载且渲染大量的 DOM 节点,那么在渲染的过程中,即使我们使用了 React virtualDom 进行维护,但是,...

5920
来自专栏程序员的知识天地

web前端开发规范总结

Web前端作为开发团队中不可或缺的一部分,需要按照相关规定进行合理编写(一部分不良习惯可能给自己和他人造成不必要的麻烦)。不同公司不同团队具有不同的规范和文档。...

3232
来自专栏技术墨客

React Web组件

从概念上说,React 和 Web组件 分别用于解决不同的问题。Web组件提供了强大的封装特性来支持其可重复使用性,而React提供了一系列声明性(declar...

802
来自专栏技术墨客

React学习(11)—— 高阶应用:Web组件

从概念上说,React 和 Web组件 分别用于解决不同的问题。Web组件提供了强大的封装特性来支持其可重复使用性,而React提供了一系列声明性(declar...

822

扫码关注云+社区

领取腾讯云代金券