react+koa2+mongodb实现留言功能(可体验)

本文涉及的产品
云数据库 MongoDB,通用型 2核4GB
简介: 留言功能在社交中占据很重要的作用。

image.png


留言功能在社交中占据很重要的作用。这里实现的留言功能,参考微信朋友圈的方式:


用户发送一个TOPIC话题,读者可以在该话题下面进行评论,也可以对该话题下的留言进行评论。但是始终只会展示两层树的评论。


当然,也可以像掘金这样进行嵌套多层树的结构展示。臣妾觉得嵌套得太深~


实际完成的效果如下:


image.png


体验站点请戳 jimmyarea.com


前端实现


使用技术


  • react
  • ant design
  • typescript


在上面的截图中,很明显,就是一个表单的设计,外加一个列表的展示。


表单的设计使用了ant design框架自带的form组件:


<Form
  {...layout}
  form={form}
  name="basic"
  onFinish={onFinish}
  onFinishFailed={onFinishFailed}
>
  <Form.Item
    label="主题"
    name="subject"
    rules={[
      { required: true, message: '请输入你的主题' },
      { whitespace: true, message: '输入不能为空' },
      { min: 6, message: '主题不能小于6个字符' },
      { max: 30, message: '主题不能大于30个字符' },
    ]}
  >
    <Input maxLength={30} placeholder="请输入你的主题(最少6字符,最多30字符)" />
  </Form.Item>
  <Form.Item
    label="内容"
    name="content"
    rules={[
      { required: true, message: '请输入你的内容' },
      { whitespace: true, message: '输入不能为空' },
      { min: 30, message: '内容不能小于30个字符' },
    ]}
  >
    <Input.TextArea
      placeholder="请输入你的内容(最少30字符)"
      autoSize={{
        minRows: 6,
        maxRows: 12,
      }}
      showCount
      maxLength={300}
    />
  </Form.Item>
  <Form.Item {...tailLayout}>
    <Button
      type="primary"
      htmlType="submit"
      style={{ width: '100%' }}
      loading={loading}
      disabled={loading}
    >
      <CloudUploadOutlined />
      &nbsp;Submit
    </Button>
  </Form.Item>
</Form>
复制代码


这里限制了输入的主题名称的长度为6-30;内容是30-300字符


针对留言的展示,这里使用的是ant design自带的ListComment组件:


<List
  loading={loadingMsg}
  itemLayout="horizontal"
  pagination={{
    size: 'small',
    total: count,
    showTotal: () => `共 ${count} 条`,
    pageSize,
    current: activePage,
    onChange: changePage,
  }}
  dataSource={list}
  renderItem={(item: any, index: any) => (
    <List.Item actions={[]} key={index}>
      <List.Item.Meta
        avatar={
          <Avatar style={{ backgroundColor: '#1890ff' }}>
            {item.userId?.username?.slice(0, 1)?.toUpperCase()}
          </Avatar>
        }
        title={<b>{item.subject}</b>}
        description={
          <>
            {item.content}
            {/* 子留言 */}
            <div
              style={{
                fontSize: '12px',
                marginTop: '8px',
                marginBottom: '16px',
                alignItems: 'center',
                display: 'flex',
                flexWrap: 'wrap',
                justifyContent: 'space-between',
              }}
            >
              <span>
                用户&nbsp;{item.userId?.username}&nbsp;&nbsp;发表于&nbsp;
                {moment(item.meta?.createAt).format('YYYY-MM-DD HH:mm:ss')}
              </span>
              <span>
                {item.canDel ? (
                  <a
                    style={{ color: 'red', fontSize: '12px', marginRight: '12px' }}
                    onClick={() => removeMsg(item)}
                  >
                    <DeleteOutlined />
                    &nbsp; Delete
                  </a>
                ) : null}
                <a
                  style={{ fontSize: '12px', marginRight: '12px' }}
                  onClick={() => replyMsg(item)}
                >
                  <MessageOutlined />
                  &nbsp; Reply
                </a>
              </span>
            </div>
            {/* 回复的内容 */}
            {item.children && item.children.length ? (
              <>
                {item.children.map((innerItem: any, innerIndex: any) => (
                  <Comment
                    key={innerIndex}
                    author={<span>{innerItem.subject}</span>}
                    avatar={
                      <Avatar style={{ backgroundColor: '#1890ff' }}>
                        {innerItem.userId?.username?.slice(0, 1)?.toUpperCase()}
                      </Avatar>
                    }
                    content={<p>{innerItem.content}</p>}
                    datetime={
                      <Tooltip
                        title={moment(innerItem.meta?.createAt).format(
                          'YYYY-MM-DD HH:mm:ss',
                        )}
                      >
                        <span>{moment(innerItem.meta?.createAt).fromNow()}</span>
                      </Tooltip>
                    }
                    actions={[
                      <>
                        {innerItem.canDel ? (
                          <a
                            style={{
                              color: 'red',
                              fontSize: '12px',
                              marginRight: '12px',
                            }}
                            onClick={() => removeMsg(innerItem)}
                          >
                            <DeleteOutlined />
                            &nbsp; Delete
                          </a>
                        ) : null}
                      </>,
                      <a
                        style={{ fontSize: '12px', marginRight: '12px' }}
                        onClick={() => replyMsg(innerItem)}
                      >
                        <MessageOutlined />
                        &nbsp; Reply
                      </a>,
                    ]}
                  />
                ))}
              </>
            ) : null}
            {/* 回复的表单 */}
            {replyObj._id === item._id || replyObj.pid === item._id ? (
              <div style={{ marginTop: '12px' }} ref={replyArea}>
                <Form
                  form={replyForm}
                  name="reply"
                  onFinish={onFinishReply}
                  onFinishFailed={onFinishFailed}
                >
                  <Form.Item
                    name="reply"
                    rules={[
                      { required: true, message: '请输入你的内容' },
                      { whitespace: true, message: '输入不能为空' },
                      { min: 2, message: '内容不能小于2个字符' },
                    ]}
                  >
                    <Input.TextArea
                      placeholder={replyPlaceholder}
                      autoSize={{
                        minRows: 6,
                        maxRows: 12,
                      }}
                      showCount
                      maxLength={300}
                    />
                  </Form.Item>
                  <Form.Item>
                    <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
                      <Button
                        style={{ marginRight: '12px' }}
                        onClick={() => cancelReply()}
                      >
                        Dismiss
                      </Button>
                      <Button
                        type="primary"
                        htmlType="submit"
                        loading={innerLoading}
                        disabled={innerLoading}
                      >
                        Submit
                      </Button>
                    </div>
                  </Form.Item>
                </Form>
              </div>
            ) : null}
          </>
        }
      />
    </List.Item>
  )}
/>
复制代码


当然,如果是多级地树结构嵌套,你完全可以只是使用Comment组件进行递归调用


列表是对用户发表的主题,留言以及子留言的展示。如果你纵览上面的代码片段,你会发现里面有一个Form表单。


是的,其Form表单就是给留言使用的,其结构仅仅是剔除了主题留言中的subject字段输入框,但是实际传参我还是会使用到。


完整的前端代码可前往jimmyarea 留言(前端)查看。


后端


使用的技术:


  • mongodb 数据库,这里我使用到了其ODM mongoose
  • koa2 一个Node框架
  • pm2 进程守卫
  • apidoc 用来生成接口文档(如果你留意体验站点,右上角有一个"文档"的链接,链接的内容就是生成的文档内容)


这里的搭建就不进行介绍了,可以参考koa2官网配合百度解决~


其实,本质上还是增删改查的操作。


首先,我们对自己要存储的数据结构schema进行相关的定义:


const mongoose = require('mongoose')
const Schema = mongoose.Schema
// 定义留言字段
let MessageSchema = new Schema({
  // 关联字段 -- 用户的id
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  type: Number, // 1是留言,2是回复
  subject: String, // 留言主题 
  content: String, //  留言内容
  pid: { // 父id
    type: String,
    default: '-1'
  },
  replyTargetId: { // 回复目标记录id, 和父pid有所不同
    type: String,
    default: '-1'
  },
  meta: {
    createAt: {
      type: Date,
      default: Date.now()
    },
    updateAt: {
      type: Date,
      default: Date.now()
    }
  }
})
mongoose.model('Message', MessageSchema)
复制代码


这里有个注意的点userId字段,这里我直接关联了注册的用户。


完成了字段的设定之后,下面就可以进行增删改查了。


详细的crud代码可以到jimmyarea 留言(后端) 查看。


本篇的重点是,对评论的话题和留言,如何转换成两层的树型结构呢?


这就是涉及到了pid这个字段,也就是父节点的id: 话题的pid-1,话题下留言的pid为话题的记录值。如下代码:


let count = await Message.count({pid: '-1'})
let data = await Message.find({pid: '-1'})
                      .skip((current-1) * pageSize)
                      .limit(pageSize)
                      .sort({ 'meta.createAt': -1})
                      .populate({
                        path: 'userId',
                        select: 'username _id' // select: 'username -_id' -_id 是排除_id
                      })
                      .lean(true) // 添加lean变成js的json字符串
const pids = Array.isArray(data) ? data.map(i => i._id) : [];
let resReply = []
if(pids.length) {
resReply = await Message.find({pid: {$in: pids}})
                               .sort({ 'meta.createAt': 1})
                               .populate({
                                path: 'userId',
                                select: 'username _id' // select: 'username -_id' -_id 是排除_id
                              })
}
const list = data.map(item => {
const children = JSON.parse(JSON.stringify(resReply.filter(i => i.pid === item._id.toString()))) // 引用问题
const tranformChildren = children.map(innerItem => ({
  ...innerItem,
  canDel: innerItem.userId && innerItem.userId._id.toString() === (user._id&&user._id.toString()) ? 1 : 0
}))
return {
  ...item,
  children: tranformChildren,
  canDel: item.userId && item.userId._id.toString() === (user._id&&user._id.toString()) ? 1 : 0
}
})
if(list) {
  ctx.body = {
    results: list,
    current: 1,
    count
  }
  return
}
ctx.body = {
  code: 10002,
  msg: '获取留言失败!'
}
复制代码


至此,可以愉快地进行留言~


相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
2月前
|
前端开发
React查询、搜索类功能的实现
React查询、搜索类功能的实现
17 0
|
3月前
|
前端开发
React 仿淘宝图片放大镜功能
React 仿淘宝图片放大镜功能
|
2月前
|
JavaScript 前端开发
在React和Vue中实现锚点定位功能
在React和Vue中实现锚点定位功能
25 1
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之Dialog表单提交,ICon样式事件,删除功能5
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之Dialog表单提交,ICon样式事件,删除功能5
28 0
|
4月前
|
存储 人工智能 NoSQL
MongoDB推出高级数据管理功能,实现随处可运行应用程序
借助MongoDB Atlas for the Edge,企业不仅可以安全地存储数据,还可以跨越不同数据源和目的地实时同步数据,从而提供具有高可用性、高弹性和高可靠性的应用程序
|
5月前
|
NoSQL 测试技术 API
Eolink Apikit 版本更新:「数据字典」功能上线、支持 MongoDB 数据库操作...
Eolink Apikit 版本更新: 1. 搭建自定义接口协议架构,支持快速适配金融行业各类型私有协议的导入、编辑和展示。 2. 数据字典功能上线,支持以数据字典的形式管理参数枚举值。 3. 数据库连接支持 MongoDB 数据库操作。 4. 基于 Apikit 类型导入 API 数据支持增量更新。
33 0
|
8月前
|
前端开发
|
8月前
|
前端开发
React/Umi中实现移动端滑动图片验证功能
React/Umi中实现移动端滑动图片验证功能
169 0
|
8月前
|
前端开发 JavaScript Java
React+后端实现导出Excle表格的功能
React+后端实现导出Excle表格的功能
208 0
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之Dialog表单提交,ICon样式事件,删除功能5
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之Dialog表单提交,ICon样式事件,删除功能5
32 0