基于React与Redux的留言墙的实现

背景

由于某事业群需要留言墙用于年会,同时需要调用大象公众号服务器接口,所以在今年年初开发了留言墙用于活动现场交流。

设计

本次留言墙分为两部分。一部分为活动展示部分,另一部分为后台审批部分。

活动展示部分为匿名留言墙形式(后改为实名制),需要根据收到的留言墙进行向上平滑滚动,如果没有消息接收则停止在最后一条消息上。

后台审批部分为管理人员对用户像某个特定公众号发送的特定格式消息进行审核,符合上墙要求的消息则通过审核,通过活动展示页面进行展示。

技术方案

React

该项目采用了React作为View层的渲染框架。关于React的简单介绍,大家可以戳阮一峰的博客React 入门实例教程, 需要系统学习的同学可以戳React的官方网站React英文版,React中文版。建议大家阅读React英文版网站,中文版网站相对于英文版网站来说缺少了一部分内容,例如React的children部分,可能是由于英文文档更新导致的翻译不太及时的原因。

Redux

Redux的学习可以通过Redux中文文档来进行。里面有很多的示例能够辅助进行学习。具体使用方法会通过后面的步骤进行介绍。

实现

React

在View层中,有两个组件。

  • Message <div className="message"> {this.props.flag === false ? <img src={this.props.photo} alt="头像"/> : ''} <div className="message-content"> {this.props.uname} [{this.parseDate(this.props.sendTime)}] {this.props.text} {this.props.flag === true ? this.props.approve === 0 ? <button onClick={this.props.approveMsg}>通过</button> : '已通过' : ''} </div> </div>
  • MsgList <div> <button onClick={(e)=>{this.handleClick(e)}}>获取消息</button> <div id="wall"> <ul> {this.props.msgs.map((message, index) => <Message {...message} key={index} flag={this.props.flag} approveMsg={()=>{this.props.approveMsg(index, message.id)}}/> )} </ul> </div> </div>

其中Message为每条消息的组件,MsgList为整个消息列表的组件。

Redux

Action

Action主要为处理数据的数据层。大部分的数据操作都放在Action中,通过dispatch(Action)的方法来通知readucer进行数据更新,从而通过react-redux来通知组件更新。 首先,会定义一些Action常量,用于操作命名。

代码语言:javascript
复制
export const WALL_REQUEST = 'WALL_REQUEST';
export const WALL_REQUEST_SUCCESS = 'WALL_REQUEST_SUCCESS';
export const WALL_REQUEST_FAIL = 'WALL_REQUEST_FAIL';

export const CHECK_MSGS = 'CHECK_MSGS';
export const CHECK_MSGS_SUCCESS = 'CHECK_MSGS_SUCCESS';
export const CHECK_MSGS_FAIL = 'CHECK_MSGS_FAIL';
export const MSG_REQUEST = 'MSG_REQUEST';
export const MSG_REFUSE = 'MSG_REFUSE';
export const MSG_PASS = 'MSG_PASS';

同时,会定义一些函数,用于View层中与Action部分进行通信,从而触发某些操作。

代码语言:javascript
复制
export function fetchMsgs(number) {
const path = '/nh/show/msg';
return dispatch=> {
dispatch(requestMsgs(number));
return window.fetch(URL + path).then(response=>response.json()).then(json=>dispatch(receiveMsgs(json.data)));
}
}

在reducer中使用了window变量中的fetch接口用于数据获取,有关于此接口的使用我后面会写另一篇文章来进行介绍,大家如果需要资料可以先戳此处,需要中文版的童鞋可以戳此处。

Reducer

在Reducer中,会对当前state中的所有数据进行处理,改变state中的全局数据从而驱动组件重新渲染。

代码语言:javascript
复制
function msgsReducer(state = [], action) {
switch(action.type) {
case WALL_REQUEST:
{
return state.slice(action.number)
}
case WALL_REQUEST_SUCCESS:
{
return [
...state, ...action.data
]
}
default:
{
return state;
}
}
}

在reducer中一般通过Action中声明的操作和action所带来的参数对state进行操作。每次都需要返回一个新的对象或者数组,而不能再原有数据上进行修改,从而避免数据更新后组件不更新的问题。

Server

server端返回的数据为一次性数据,即数据取过后就不会再返回,因此需要在前端Reducer里面对数据进行存储。由于数据为滚动显示,因此需要一个队列来进行控制。

难点

滚动问题

scrollTop+setInterval

最开始的滚动方案选择此方案。此方案在实现上也最为简单。但是,当消息数目到达1K量级时,能够明显的感觉到有卡顿的现象发生,滚动很不流畅,因此抛弃了此方法。

transform+setInterval

由于上一个方案中scrollTop在节点数过多的情况下会导致卡顿的问题,因此在滚动上采用了transform的方法,但是由于setInterval粒度不够小,因此对其进行了替换。

transform+window.requestAnimationFrame

window.requestAnimationFrame是浏览器接口,被调用的频率是每秒60次,但是一般会遵循W3C标准规定的频率。使用此接口可以保证调用频率,同时能够配合transform的变化数字来进行速度控制。因此采用了此方法。

节点删除功能

由于在留言墙的使用过程中,会有不断的新的节点产生并且滚动出视口,因此为了节省内存,需要将滚动出视口的节点删除,从而避免整个网页消耗的内存越来越大。
由于滚动方式确定为transform的滚动方式,因此选择了在请求调用返回数据后同时触发删除代码,对当前消息节点进行判断,对已经滚动到视口外的数据节点进行删除,并重置transform值,从而达到删除节点的目的。

代码语言:javascript
复制
componentWillReceiveProps(nextProps) {
if(this.props.flag === false && this.props.msgs.length !== nextProps.msgs.length) {
let number = Math.floor((-this.y) / this.msgHeight),
element = document.getElementById('wall').getElementsByTagName('ul')[0];
if(number > 0) {
this.y = this.y + number * this.msgHeight;
element.style.transform = 'translateY(' + (this.y) + 'px)';
return;
}
window.cancelAnimationFrame(this.animationId);
this.animationId = window.requestAnimationFrame(this.scroll.bind(this));
}
}

通过组件接收新数据渲染时来重置transform值,完成节点删除工作。

不足

如果消息并发数量较多,会导致消息堆积在视口下方等待向上滚动,由此可能消耗大量的内存,后续仍然需要优化,避免所有接受到的未展示的数据都渲染出来堆积在下方。

总结

在刚开始设计时至少考虑到了滚动的情况,并没有考虑到消息越来越多导致页面占用内存越来越大的问题。当完成最初版本的消息滚动时,在自己测试的过程中因为消息数量过大导致卡顿,所以考虑到了滚动方面的优化与节点删除的问题。transform的效率优于scrollTop,而window.requestAnimationFrame的性能又优于setInterval,但是在开发时间上不是特别充足,因此选择了性能最好的技术方案,但是舍弃了一部分优化的方案。