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