从零搭建个人博客(4)-留言评论区

效果图

代码:https://github.com/MSChuan/Blog_Comment
Demo:https://mschuan.github.io/Blog_Comment/dist_prod/index.html

本文将实现一个简单的留言评论区,评论分为两种,如上图,这里使用parent comment称呼”test”和”沙发”这两条评论,”你个2货”是对”沙发”的回复,这里称为child comment。最上面的部分是添加parent comment的区域,每一条留言都有一个回复和赞按钮,用于回复产生child comment以及点赞,对应数目需要在点击后+1。

State

由于react+redux会维护一个全局状态树,所以我们首先要定义它,才能继续实现后续的代码编写。首先当然是评论的内容存储,每一条parent comment+related child comments看作一个package,于是我们可以用packages这样一个数组就能存储整个内容,每个package也是一个数组,每个元素都是一个object,包括评论的内容,回复数,点赞数,id,创建时间等。Besides,点击回复后需要弹出回复框,回复框出现的位置也需要在state中定义。

const initialState = {
    CommentState: {
        packages: [],
        replyBoxIndex: {
            packageIndex: -1,
            commentIndex: -1
        }
    }
};

这里定义了初始状态,回复框的出现位置需要两个索引,一个是package的索引,另一个就是package中哪条评论下方出现回复框,初始的-1表示没有回复框。

Action

从功能上看只有四个action,添加parent comment,添加child comment,点击回复按钮,点赞,于是定义这四个action,首先新建一个types.js文件,用于定义所有的action。

const types = {
    AddParentComment: 'AddParentComment',
    AddReplyBox: 'AddReplyBox',
    PraiseComment: 'PraiseComment',
    AddChildComment: 'AddChildComment',
};

export default types;

定义actionFactory,用于产生所有的action。

import types from './Types';

const actionFactory = {
    AddParentComment: (text, articleId) => ({
        type: types.AddParentComment,
        text: text,
        articleId: articleId
    }),
    AddReplyBox: (packageIndex, commentIndex) => ({
        type: types.AddReplyBox,
        packageIndex: packageIndex,
        commentIndex: commentIndex
    }),
    PraiseComment: (packageIndex, commentIndex, id) => ({
        type: types.PraiseComment,
        packageIndex: packageIndex,
        commentIndex: commentIndex,
        id: id
    }),
    AddChildComment: (packageIndex, id, text) => ({
        type: types.AddChildComment,
        packageIndex: packageIndex,
        text: text,
        id: id
    }),
};

export default actionFactory;

AddParentComment需要两个参数,一个是内容,另一个是文章id,当文章id为0时,我们认为是在留言板的留言。其余action的参数就不一一解释了,都较为简单。

Reducer

有了state和action,就能很清晰的写出reducer。

const packages = (state = initialState.CommentState.packages, action) => {
    switch(action.type) {
        case types.AddChildComment:
            if(action.id !== state[action.packageIndex][0].id) {
                return state;
            }
            return ([
                ...state.slice(0, action.packageIndex),
                [Object.assign({},state[action.packageIndex][0],{replyCount: state[action.packageIndex][0].replyCount + 1}),
                    ...state[action.packageIndex].slice(1), {text: action.text, created_at: '', praiseCount: 0, replyCount: 0, id: 0 }],
                ...state.slice(action.packageIndex + 1)
            ]);
        case types.AddParentComment:
            return ([[{text: action.text, created_at: '', praiseCount: 0, replyCount: 0, id: 0}], ...state]);
        case types.PraiseComment:
            return ([
                ...state.slice(0, action.packageIndex),
                [...state[action.packageIndex].slice(0, action.commentIndex),
                    Object.assign({}, state[action.packageIndex][action.commentIndex], {praiseCount: state[action.packageIndex][action.commentIndex].praiseCount + 1}),
                    ...state[action.packageIndex].slice(action.commentIndex + 1)],
                ...state.slice(action.packageIndex + 1)
            ]);
        default:
            return state;
    }
};

首先是packages,这里写的比较丑,比较好的做法是把那些很长很复杂的逻辑封装成函数,可以复用,也会让switch case更加清晰。

const replyBoxIndex = (state = initialState.CommentState.replyBoxIndex, action) => {
    switch(action.type) {
        case types.AddReplyBox:
            if(state.packageIndex === action.packageIndex && state.commentIndex === action.commentIndex) {
                return initialState.CommentState.replyBoxIndex;
            }
            return ({
                packageIndex: action.packageIndex,
                commentIndex: action.commentIndex
            });
        default:
            return state;
    }
};

对于回复框,这里实现了toggle,即点击同一个回复按钮可以开关对应的回复框。

React实现

整个结构也比较清楚,用一个Container把评论区包起来,下面定义两个Component,一个是添加parent comment,一个显示packages中的内容。

Container

class CommentContainer extends React.Component {
    constructor(props) {
        super(props);

        // 0 means this comment container is not related to any article
        this.articalId = props.articalId || 0;
    }
    componentDidMount() {
        // TODO: send request to fetch comments content
    }
    render() {
        const { state, actions } = this.props;
        let commentPackageList = state.packages.map((p, index) => {
            return (<div className="commentPackageBox" >
                        <CommentList
                            comments={p}
                            packageIndex={index}
                            replyBoxIndex={(index === state.replyBoxIndex.packageIndex) ? state.replyBoxIndex.commentIndex : -1}
                            actions={actions}
                        />
                    </div>);
        });

        return (<div>
                    <AddParentComment actions={actions} articalId={this.articalId} />
                    {commentPackageList}
                </div>);
    }
}

从store中拿到state,action,经过处理有传递给子组件,map函数在处理数组时特别好用。

Component

class AddParentComment extends React.Component {
    constructor(props) {
        super(props);
        this.editor = null;
    }
    componentDidMount() {
        const textbox = ReactDOM.findDOMNode(this.refs.AddParentCommentBox);
        this.editor = new Simditor({
            textarea: $(textbox),
            toolbar: ['title', 'bold', 'italic', 'underline', 'strikethrough', 'color', 'ol', 'ul', 'link', 'alignment', 'emoji'],
            emoji: {
                imagePath: config.emojiUrl
            }
        });
    }
    render() {
        const {actions, articalId} = this.props;
        return (<form>
                    <FormGroup controlId="AddParentCommentBox">
                        <FormControl componentClass="textarea" placeholder="留言" ref={'AddParentCommentBox'} />
                        <Button type="submit" onClick={(e) => {
                            e.preventDefault();
                            if(!!this.editor && this.editor.getValue() !== '') {
                                actions.AddParentComment(this.editor.getValue(), articalId);
                                this.editor.setValue('');
                            }
                        }}>提交</Button>
                    </FormGroup>
                </form>);
    }
}

组件的内容就是一个编辑框+提交按钮,使用react-bootstrap很容易就能实现。显示评论内容部分稍微复杂一点,首先是根据packages render出parent comment和child comment,若是需要显示回复框,则在对应的位置插入回复框,更好的做法是复用AddParentComment这个组件,因为回复框其实和AddParentComment是同构的。

class CommentList extends React.Component {
    constructor(props) {
        super(props);
        this.editor = null;
    }
    componentDidUpdate() {
        if(!!this.refs.commentReplyBox) {
            const textbox = ReactDOM.findDOMNode(this.refs.commentReplyBox);
            this.editor = new Simditor({
                textarea: textbox,
                toolbar: ['title', 'bold', 'italic', 'underline', 'strikethrough', 'color', 'ol', 'ul', 'link', 'alignment', 'emoji'],
                emoji: {
                    imagePath: config.emojiUrl
                }
            });
        }
    }
    render() {
        const {comments, packageIndex, replyBoxIndex, actions} = this.props;
        let list = comments.map((comment, index) =>{
            return (
                <ListGroupItem className="commentOutline">
                    <div className="commentContent" dangerouslySetInnerHTML={{__html: comment.text}}></div>
                    <Form horizontal>
                        <FormGroup>
                            <ControlLabel>{comment.created_at}</ControlLabel>
                            <Button bsStyle="link" eventKey={3} href="#" onClick={(e) => {
                                e.preventDefault();
                                actions.AddReplyBox(packageIndex, index);
                            }}>{'回复 ' + (index === 0 ? comment.replyCount : '')}</Button>
                            <Button bsStyle="link" eventKey={2} href="#" onClick={(e) => {
                                e.preventDefault();
                                actions.PraiseComment(packageIndex, index, comments[index].id);
                            }} >{'赞 ' + comment.praiseCount}</Button>
                        </FormGroup>
                    </Form>
                </ListGroupItem>
            );
        });

        if(replyBoxIndex >= 0) {
            list.splice(replyBoxIndex + 1, 0,
                <ListGroupItem id="commentReplyOutline">
                    <FormGroup controlId="commentReplyBox">
                        <FormControl componentClass="textarea" placeholder="回复" ref={'commentReplyBox'} />
                        <Button className="commentReplyBoxReplyButton" type="submit" onClick={(e) => {
                            e.preventDefault();
                            if(!!this.editor && this.editor.getValue() !== '') {
                                actions.AddChildComment(packageIndex, comments[0].id, this.editor.getValue());
                                actions.AddReplyBox(-1, -1);
                            }
                        }}>回复</Button>
                    </FormGroup>
                </ListGroupItem>
            );
        }

        return <ListGroup className="commentList">{list}</ListGroup>;
    }
}

写代码之前对于组件的封装考虑不是很到位,没有最大程度的复用组件,由于时间关系,就先mark一下。

最后加一下css,让child comment显示时能够有left margin,同时调整编辑框的高度。

.commentList {
  & > li:not(:first-child) {
      margin-left: 10%;
  }
}

.simditor-body {
    min-height: 100px !important;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值