React&Nest.js社区平台(四)——✏️文章发布与管理实战

简介: React&Nest.js社区平台(四)——✏️文章发布与管理实战

前言

在上一期我们已经实现了个人信息模块,这一期来实现文章发布与管理。涉及到如下功能:

  • 草稿创建/修改
  • 文章发布
  • 文章删除
  • 获取我发布的文章

看起来像是文章的增删改查功能,其实还是有不少值得思考的地方,我们一点点往下看。

后端实现

我们写文章很多时候都不是一口气写完的,大多数都是写了一部分之后,下次再来接着写,所以草稿功能是很有必要的。

那么草稿跟正式发布的文章,我们应不应该保存在同一张表里面?

如果草稿有一张单独的表,会有如下的优缺点:

优点:

  • 将草稿和文章主表分开存储,可以使数据结构更加清晰,易于理解和维护,查询时心智负担小,数据的边界清晰,很难出现草稿出现在已发布中的文章这种 bug
  • 草稿和文章主表可以有独立的表和索引,提高查询效率。
  • 可以针对草稿和文章主表分别进行数据管理,例如数据备份、恢复、迁移等操作。

缺点:

  • 草稿发布时需同步两张表的数据,维护一致性。
  • 相对草稿跟已发布的文章在一张表里工作量增多

而如果草稿跟已发布的文章在同一张表中,也会有如下的优缺点:

优点:

  • 将草稿和文章主表保存在同一张表中,可以简化数据模型,减少表之间的关联,使用 status 状态来区分是草稿还是已发布的文章
  • 可以使用简单的 SQL 语句在同一张表中查询草稿和文章主表的数据。
  • 工作量较小

缺点:

  • 需要管理状态字段的取值和含义,可能会增加数据管理的复杂性。
  • 性能问题:如果草稿和文章主表的数据量较大,可能会影响查询性能。
  • 查询文章时需要额外关注查询条件,不然容易出现已发布的文章混入草稿文章

如果数据量较大,且需要频繁进行草稿和文章主表之间的查询和数据同步,那么分开存储可能是更好的选择。

如果数据量较小,且查询和数据同步的需求较简单,那么使用状态字段来区分可能更加简单和方便。

最终我们选择的是第二种方案:即草稿与已发布的文章存在同一张表里,使用状态字段区分,

表设计

文章表 articles 的建表 DDL 语句如下:

CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT NULL,
`content` mediumtext,
`introduction` varchar(100) DEFAULT NULL,
`views` int(11) DEFAULT '0',
`likes` int(11) DEFAULT '0',
`favorites` int(11) DEFAULT '0',
`creator_id` int(11) DEFAULT NULL,
`creator_name` varchar(100) DEFAULT NULL,
`category_id` int(4) DEFAULT NULL,
`is_deleted` tinyint(4) DEFAULT '0',
`status` int(4) DEFAULT '0',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `articles_creator_id_IDX` (`creator_id`,`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

介绍一下上面各个字段的含义:

  • id :主键 id
  • title :文章标题
  • content :文章内容
  • introduction :文章简介
  • views :阅读量
  • likes :点赞数
  • favorites :收藏数
  • creator_id :创建人 id
  • creator_name :创建人的用户名
  • is_deleted :是否删除
  • status :文章状态
  • category_id :文章标签 id
  • created_time :创建时间
  • updated_time :更新时间

这里建了一个 creator_id 的二级索引,旨在加速查询“我发布的文章”这个列表。

创建/修改文章

这里我把创建跟修改文章做成一个接口,如果带了 id 就是修改,不带 id 就是创建。

export class CreateArticleDto {
  id?: number;
  title: string;
  content: string;
}

然后新建一个 createOrUpdate 路由,做一下简单的参数校验之后,具体的业务逻辑会交给 service

  @Post('createOrUpdate')
  async createOrUpdate(
    @User('id') userId: number,
    @Body() createArticleDto: CreateArticleDto,
  ) {
    if (!createArticleDto.title && !createArticleDto.content) {
      throw Error('标题和内容不能同时为空');
    }
    const res = await this.articleService.createOrUpdate(
      createArticleDto,
      userId,
    );
    return res;
  }

service

  • 如果是修改文章:
  1. 先用 id 查询出文章的创建人信息,如果创建人 id 跟当前的登录人 id 不一致,则没有权限修改
  2. 执行更新逻辑
  • 如果是创建文章:
  1. 先使用当前登录人 id 查询出当前登录人的用户名,这里我在文章表冗余了创建人的用户名,好处是展示文章信息的时候不需要再去查询一遍用户表,需要注意的点是,当用户名修改时,需要同步修改这些冗余的字段
  2. 执行创建逻辑
//service
  async createOrUpdate(
    createArticleDto: CreateArticleDto,
    creatorId: number,
  ): Promise<number> {
    const userInfo = await this.userRepository.findOne({
      where: { id: creatorId },
      select: ['username', 'id'],
    });
    if (createArticleDto?.id) {
      if (creatorId !== userInfo.id) {
        throw Error('无权限修改');
      }
      await this.articleRepository.update(
        { id: createArticleDto.id },
        createArticleDto,
      );
      return createArticleDto.id;
    } else {
      const res = await this.articleRepository.save(
        Object.assign({}, createArticleDto, {
          creatorName: userInfo.username,
          creatorId: userInfo.id,
        }),
      );
      return res.id;
    }
  }

发布文章

由于文章表的 status 字段有默认值 0 ,所以当文章创建的时候,它本身就是草稿状态。在发布文章的时候我们需要做如下的交互:

  1. 选择文章分类
  2. 填写文章简介
  3. 修改文章状态

所以发布文章的 DTO 如下:

export class PublishArticleDto {
  @IsNotEmpty()
  id: number;
  @IsNotEmpty()
  introduction: string;
  @IsNotEmpty()
  categoryId: number;
}

删除文章

做删除的时候最好做软删除,就是在表里有一个 is_deleted 字段,默认值是 0 ,删除的时候就把这个字段值改成 1

这样做可以很轻松的拓展类似于回收站这样的功能,如果真的把某一条数据从表里删除了,指不定某一天某个用户想恢复这条数据,那就得找你们公司的DBA团队了,一般情况下最好不要做硬删除。

所以删除接口的逻辑其实是一个更新逻辑, service 的实现如下:

//service
  async deleteArticle(articleId: number, creatorId: number) {
    const article = await this.articleRepository.findOne({
      where: { id: articleId, creatorId },
    });
    if (!article) {
      throw Error('删除失败');
    }
    await this.articleRepository.update({ id: articleId }, { isDeleted: 1 });
    return true;
  }

我的文章列表

我们上面把文章分成了草稿和已发布两种状态,获取的时候可以把他们一起拿出来,但是不要把 is_deleted1 的数据也查出来了。

这里还有一个优化点,我们这个接口需要提供给前端,前端用来展示我的文章列表,既然是列表,那一般不会把文章的内容展示在列表上。而文章的内容大小是不可估量的,对于这种字段来说,完全没有必要取出来。

所以平时开发的时候多注意一下按需取字段,一方面是减少传输的体积,另一方面也可以走到覆盖索引。这个意识希望大家都有

  async getMyArticle(creatorId: number) {
    const list =
      (await this.articleRepository.find({
        where: { creatorId, isDeleted: 0 },
        select: ['createdTime', 'updatedTime', 'id', 'title', 'status'],
      })) || [];
    return {
      published: list.filter((item) => item.status === 1),
      draft: list.filter((item) => item.status === 0),
    };
  }

前端实现

接口实现了之后,就可以来实现前端的逻辑了。这里前端需要做两个页面:

  • 编辑页
  • 我的文章列表页

样式可能有些简陋,大家看的时候多担待。

编辑器接入

这里我使用的 markdown 编辑器用的是掘金开源的 markdown 编辑器。它也具有插件化的生态,所以需要用什么插件的话就自己选择。

import { Editor as BEditor } from "@bytemd/react";
 <BEditor
    uploadImages={handleUpload}
    mode="split"
    locale={zh}
    value={value}
    plugins={plugins}
    onChange={(v) => {
      setValue(v);
    }}
/>

接入的话相当简单,就像使用一个 input 组件一样简单,上传图片时需要实现一个 uploadImages 方法。

我们在之前已经实现了通用的上传逻辑接口,而且是支持多文件上传的,所以这里只需要调用一下就能完美契合。

  const handleUpload = async (files: File[]) => {
    const formData = new FormData();
    for (const file of files) {
      formData.append("files", file);
    }
    const res = await upload(formData);
    return res.data.map((item: any) => ({ url: item.url }));
  };

image.png

保存与发布

保存的话就是在标题或者内容发生变化的时候,调用一下更新内容的接口就好了,这里我做了一个 1s 的防抖处理。

  const updateArticle = useCallback(
    debounce((id: number, title: string, content: string) => {
      createOrUpdateArticle({
        id,
        title,
        content,
      });
    }, 1000),
    []
  );

发布的时候会弹出一个 popover 来继续填写发布后的字段,之后的一些拓展逻辑也可以在这个 popover 里面加,例如定时发布等等。

image.png

我的文章列表

image.png

文章列表这里用了 2tab ,来区分草稿与已经发布的文章。调用获取我的文章列表,根据对应的 tabKey 把列表渲染出来即可。

对于每一项都有一个编辑跟删除操作,编辑的话就是跳转到编辑页就行,删除的话可以做一个二次确认,然后调用软删除的接口,调用完之后刷新一下列表即可。

  <Popconfirm
    title="确认删除吗?"
    onConfirm={async () => {
      await deleteArticle({ id: item.id });
      await getData();
      message.success("删除成功");
    }}
  >
    <a
      style={{ marginRight: 12 }}
      onClick={() => navigate(`/editor?id=${item.id}`)}
    >
      编辑
    </a>
    <a>删除</a>
  </Popconfirm>

image.png

最后

以上就是本文的全部内容,主要介绍了社区平台中的文章发布与管理的实战运用。如果你觉得有意思的话,点点关注点点赞吧~

相关文章
|
5月前
|
前端开发 API 数据库
React Server Components 实战:解锁高效渲染新姿势
React Server Components 实战:解锁高效渲染新姿势
242 81
|
6月前
|
缓存 前端开发 安全
解锁下一代 React:Server Components 实战指南
解锁下一代 React:Server Components 实战指南
247 84
|
6月前
|
前端开发
React Hooks数据获取:避免内存泄漏的实战指南
React Hooks数据获取:避免内存泄漏的实战指南
|
7月前
|
人工智能 自然语言处理 JavaScript
通义灵码2.5实战评测:Vue.js贪吃蛇游戏一键生成
通义灵码基于自然语言需求,快速生成完整Vue组件。例如,用Vue 2和JavaScript实现贪吃蛇游戏:包含键盘控制、得分系统、游戏结束判定与Canvas动态渲染。AI生成的代码符合规范,支持响应式数据与事件监听,还能进阶优化(如增加启停按钮、速度随分数提升)。传统需1小时的工作量,使用通义灵码仅10分钟完成,大幅提升开发效率。操作简单:安装插件、输入需求、运行项目即可实现功能。
403 4
 通义灵码2.5实战评测:Vue.js贪吃蛇游戏一键生成
|
4月前
|
人工智能 自然语言处理 前端开发
让AI学会"边做边想":ReAct的实战指南
还在为AI的「知其然不知其所以然」而烦恼?ReAct技术让AI不仅会思考,更会行动!通过模拟人类的思考-行动-观察循环,让AI从书呆子变身为真正的问题解决专家。几行代码就能构建智能Agent,告别AI幻觉,拥抱可追溯的推理过程!
|
3月前
|
JavaScript 前端开发 安全
【逆向】Python 调用 JS 代码实战:使用 pyexecjs 与 Node.js 无缝衔接
本文介绍了如何使用 Python 的轻量级库 `pyexecjs` 调用 JavaScript 代码,并结合 Node.js 实现完整的执行流程。内容涵盖环境搭建、基本使用、常见问题解决方案及爬虫逆向分析中的实战技巧,帮助开发者在 Python 中高效处理 JS 逻辑。
|
5月前
|
JavaScript 前端开发 算法
流量分发代码实战|学会用JS控制用户访问路径
流量分发工具(Traffic Distributor),又称跳转器或负载均衡器,可通过JavaScript按预设规则将用户随机引导至不同网站,适用于SEO优化、广告投放、A/B测试等场景。本文分享一段不到百行的JS代码,实现智能、隐蔽的流量控制,并附完整示例与算法解析。
162 1
|
9月前
|
前端开发 数据可视化 测试技术
React音频播放列表组件开发实战:常见问题与避坑指南
本文介绍了构建React音频播放列表组件的核心架构与常见问题解决方案。通过管理播放状态、列表索引和音频进度,结合异步控制、状态清理、节流优化等技术,确保流畅的用户体验。针对移动端兼容性、内存泄漏、列表渲染性能等问题提供了具体修复方案,并分享了自定义Hook封装、可视化音频波形等进阶实践。最后,总结了性能优化法则和测试关键点,帮助开发者打造生产级可靠的音频播放组件。
275 18
|
9月前
|
Web App开发 移动开发 前端开发
React 视频播放器样式自定义实战指南
本文详细介绍了如何在React项目中实现视频播放器的样式自定义,涵盖HTML5 `&lt;video&gt;`标签的基础知识、CSS样式定制技巧及常见问题解决方案。针对全屏模式样式失效、移动端触摸事件冲突和进度条样式定制等问题提供了具体代码示例。同时,探讨了视频预加载策略和内存优化方法,并推荐了几款调试工具,帮助开发者提升用户体验和应用性能。
302 6
|
9月前
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例