我使用 React 和 GraphQL 构建了一个聊天应用程序简介免责声明第一步构建应用程序技术堆栈难点我不引以为豪的事情最终想法

2025-05-25

我使用 React 和 GraphQL 构建了一个聊天应用程序

简介

免责声明

第一步

构建应用程序

技术栈

困难的部分

我不引以为豪的事情

最后的想法

TLDR

您可以在此处尝试该应用程序:chat.abdou.dev

您可以在此github repo中查看源代码

简介

大约四个月前,我决定开始学习 Web 开发。我认为,要知道自己是否具备就业资格,最好的办法就是从零开始构建一个复杂的、实际的应用。我的第一个选择是开发一个聊天应用。对我来说,这似乎足够复杂,而且我知道在这个过程中我会学到很多东西。

免责声明

公平地说,虽然我没有 Web 开发经验,但我在编程世界里也算不上完全的新手。我做过一些 C 语言编程,也用过一点 Flutter,我觉得这让我更容易掌握 React 等新技术。否则,我可能要花远不止 4 个月的时间 :)。另外,这是我事后在 GitHub 上发布的图。
Github 图

第一步

我知道我必须学习基础知识。在对 HTML、CSS 和 JavaScript 有扎实(嗯)的理解之前,先学那些疯狂的东西(React)是没有意义的(剧透:我最后学的是 Typescript)。

所以,我的第一个目的地是 FreeCodeCamp。它提供了丰富的课程,涵盖了 Web 开发入门所需的一切。我完成了前三个认证,然后就准备好踏入“荒野”,开始自学了。

构建应用程序

现在我已经掌握了所需的所有基础知识,是时候开始构建聊天应用程序了。

技术栈

我浏览了一些招聘信息,看看市场上流行什么,最后我决定在我的下一个项目中使用这些技术(非详尽列表):

前端

  • 反应
  • Redux:毕竟我正在开发一个功能齐全的聊天应用。我需要一个可靠的状态管理解决方案。
  • 用于组件/样式的MaterialUI
  • GraphQL 的Apollo 客户端

后端

  • Node.jsExpress
  • 用于 GraphQL 的Apollo 服务器
  • TypeGraphQL:用于构建 GraphQL API
  • PostgreSQL与Prisma(下一代 ORM,非常酷的东西)结合用于数据库。

我有没有提过我在整个项目中都使用了 Typescript?我发现没有类型安全真的很难工作。

设置

当时,我听说了Next.js,觉得它像是我想学的东西,所以决定用它。我还计划用 TDD(测试驱动开发)完成所有代码编写,所以搭建测试环境是必须的。经过一番折腾,我终于搞定了JestReact Testing Library,可以和 Typescript 配合使用了。现在,我准备开始写代码了。

应用程序功能

验证

为了简单起见,我使用 Google 进行身份验证。用户使用 Google 登录后,我获取令牌并将其发送到后端。如果没有用户与该 Google 帐户关联,我会创建该帐户,然后继续。

我花了一些时间研究不同的身份验证方法,最简单的方法是使用jwt。其他解决方案(例如 auth0)似乎非常复杂。我尝试以最佳方式实现 jwt 身份验证。我没有使用本地存储来存储任何令牌(尽管它在很多教程中被广泛使用),而是使用了刷新/访问令牌策略:

  • 刷新令牌:生成的令牌的有效期很长(基本上永不过期),并且它被设置为仅限 http 的 cookie,因此客户端永远无法访问它。它用于在访问令牌过期时生成新的访问令牌。
  • 访问令牌:生成的访问令牌有效期较短(30分钟),并且仅存储在前端的内存(变量)中。它会随所有后续的 http 请求一起发送,用于执行经过身份验证的操作。

我还在前端实现了自动刷新功能,这样当访问令牌过期时,系统会自动生成一个新的访问令牌,用户根本感觉不到。虽然不是很复杂,但我对结果很满意。

用户配置文件

每个用户都有一个唯一的用户名、姓名和头像。这些都是公开的,任何人都可以查看。用户可以更改自己的用户名、姓名,并上传新的头像。值得注意的是,图片会调整为不同的尺寸(小、中和原始尺寸)。因此,我们不需要为 48x48 的用户头像获取 1280x1024 的图片。

朋友们

每个用户都可以向其他用户发送好友请求,并且可以接受或拒绝,就像 Facebook 一样。用户还可以屏蔽其他用户,阻止他们继续发送好友请求和消息。与某人成为好友后,您可以向他们发送短信,并查看他们的活跃状态(上次上线日期)(如果他们没有隐藏状态)。

通知

目前,它们只会显示是否有人接受了你的好友请求。我暂时想不出其他用途。

消息传递

这款应用的核心功能,也是上述所有功能之后最后实现的。我尝试克隆 Facebook Messenger 的行为。由于我当时已经熟悉了所有内容,所以这并不难,但我仍然遇到了一些非常恼人的问题:

  • 递送状态:如果你查看 Facebook Messenger,你会注意到每条消息都有一个递送状态,可以是SENDINGSENTRECEIVEDDELIVEREDSEEN。尝试完全按照 Messenger 的方式实现它非常棘手。我从一开始就没有考虑过这个问题,所以最终我修改了很多地方才让它正常工作。
  • 竞争条件:通常情况下,你期望事情按特定顺序发生,例如,一条消息的状态可能从 变为SENDINGSENT然后变为DELIVERED,但有时并非如此。例如,如果网速较慢,你可能会在客户端收到确认消息已发送的响应之前就收到消息已送达的通知,因此状态会从 变为SENDINGDELIVERED然后变为SENT,这完全不是我们想要的,而且可能会导致恼人的错误,例如消息显示两次,或者根本不显示。处理这些竞争条件并不简单,而且我认为之后的代码会变得有点混乱。

用户还可以通过每条消息发送多张图片/视频。

即时的

在开始使用消息传递功能之前,我一直以为我会使用套接字来实现实时通信。后来我发现 GraphQL 或许可以解决这个问题,结果证明我的猜想是正确的。GraphQL 支持
订阅功能,它(引用Apollo 文档)“有助于实时通知客户端后端数据的变更,例如新对象的创建或重要字段的更新”。订阅功能非常适合这种用例,而且实现起来也相当简单。

Next.js 和 Vite.js

项目进行到一半时,我意识到我并没有真正从 Next.js 的大多数功能中受益(或者也许我根本不知道如何使用?)。与此同时,我发现Vite.js使用esbuild,它是目前最快的打包工具,所以我就切换到了它。它确实非常快,我可以修改代码并立即在浏览器中看到效果,所以我现在会继续使用它。我并没有完全放弃 Next.js,以后在构建合适的项目(例如博客/作品集)时,我一定会学习它。毕竟,像服务器端渲染这样的概念有点太高级了,而我对这些都还很陌生。

托管

我使用 digitalocean 托管前端和后端。我以每月 5 美元的价格购买了一个 Droplet,把所有东西都放了进去。部署过程很有趣,我一直不喜欢所有与 IP 地址、DNS、防火墙相关的东西……但结果发现其实还不错,所有东西都有文章/教程解释所有细节,你只需要照做就行了。

对于文件托管,你通常会想用一些云存储解决方案,比如 Amazon S3 bucket,但单独付费不太合理,毕竟这只是一个个人项目。所以我决定直接用 Droplet 的内存。

困难的部分

这些是我觉得最困难的事情。它们有时会让我坐下来思考,是放弃这一切,成为一名全职面包师。

配置

我不知道该怎么称呼它,所以我称它为配置,但是你知道...当你试图让 jest 工作时,即使你遵循了所有的说明,它就是不工作。最终偶然发现,在 github 对话深处的一个评论中,你必须从 tsconfig.json 中删除某一行...或者那次我想在 Next.js 项目中使用装饰器,但它不起作用,所以我不得不痛苦地尝试 Babel 配置,我对它一无所知,直到它开始工作,但后来我发现 Next.js HMR 坏了,所以在浪费了那么多时间之后,我不得不放弃使用装饰器。有趣的是,我尝试在后端项目中使用它们,它们从第一次尝试就起作用了。

我总是偶然遇到像 webpack、bundler、构建工具之类的术语,但它们对我来说仍然很陌生,我只是使用它们,却对它们的工作原理一无所知,这让我感到很不好意思。也许我应该开始认真学习它们,也许这会帮助我解决将来那些耗时的配置问题。

造型

样式是最难的事情之一,我不知道该如何设置组件的样式。我应该使用纯 CSS 文件吗?应该使用 CSS 模块吗?应该使用 Bootstrap 还是像 Tailwind 这样更现代的库?这是一个非常艰难的决定。我最初使用 CSS 模块,然后切换到 Styled Components,最后决定使用 MaterialUI。最后,我不得不迁移所有内容以使用 MaterialUI 及其 JSS 解决方案。这真是一团糟,重构非常耗时,而且我仍然觉得我没有按照预期去做。现在,如果不修改代码库中的每个组件,我甚至无法添加暗黑模式。

虚拟化

我已经使用了分页功能,如果一个对话有 1000 条消息,我不会一次性全部抓取。我只会抓取前 30 条,当用户滚动到顶部时,我再抓取另外 30 条,以此类推。

这还不够,因为获取所有这 1000 条消息后,DOM 中就会有 1000 个消息元素,这对性能不利。虚拟化通过仅渲染可见元素来解决这个问题。假设用户的视口可以容纳 20 条消息,那么 DOM 中只会显示 20 个(通常略多于 20 个)元素,当用户滚动时,不可见的消息将被可见消息替换,但 DOM 中的元素数量始终保持不变。(
下图来自这条推文)
虚拟化解释

一些库,例如 Virtuoso 和 React Window,在实现虚拟化方面做得很好,但就我而言,它们两个都表现不佳。消息元素的高度是可变的,必须在渲染之前计算这些高度,以及许多其他计算。当消息列表变得很大时,这些计算会变得非常繁重,应用程序将变得无法使用。我花了很多时间尝试让虚拟化发挥作用,但最终,我决定最好停止尝试,直接将所有内容渲染到 DOM 中。到目前为止,我还没有发现任何性能问题,我检查了 Facebook Messenger 和 Whatsapp Web,它们都没有使用虚拟化,知道 Facebook 和我做同样的事情,我感到很欣慰 :)。

在移动开发中(或者至少在 Flutter 中),你拥有开箱即用的内置虚拟化功能。到目前为止,我一直认为它是标准配置,你可以在所有其他 SDK 中找到它,我把它视为理所当然。我仍然不明白为什么 React 没有这样的东西,难道虚拟化在 Web 开发中不那么重要吗?

保持一切测试

由于我从事测试驱动开发,几乎每一段生产代码都要写测试。毫无疑问,保持代码库的良好测试非常重要,但这个过程却极其繁琐。有些测试比其他测试更难,有时你会花 90% 的时间来编写测试的模拟对象,而只有 10% 的时间用于编写实际的测试。而且,如果你修改了测试过的代码,就不得不重新进行一遍测试。

总而言之,这总是值得的。拥有一套强大的测试套件能让你确信你的代码确实有效。每当你想要提交新的代码时,只需运行这些测试,如果一切通过,就可以了。

前端测试套件

我不引以为豪的事情

主题

我知道我已经抱怨过样式了,但主题是我做得不好的地方之一,我为此深感自责。我知道不应该在每个组件里硬编码颜色、字体大小或任何与样式相关的内容。我不得不使用全局主题,Material UI 也提供了一种便捷的方法。但我仍然急于在屏幕上看到我的工作成果,我只想尽快构建这些组件,告诉自己以后再重构,但我几乎什么都不知道。有一次,事情变得太过繁琐,我懒得重构所有东西,所以我干脆放弃了,让一切保持原样。

下次,我将先写下我的全局主题,然后再编写任何组件代码。

路由

我确信我本可以实现比现在更好的路由。直到我完成了整个身份验证功能后,我才发现 React Router。我之前使用的是条件渲染,如果用户已登录,则显示主屏幕;如果没有登录,则显示登录屏幕。之后,我开始使用 React Router,但我仍然不确定是否充分利用了它的强大功能。我不得不使用一些 hack 技巧才能让一切按我的意愿运行
(我需要一个自定义的返回按钮,它并不总是像浏览器中的按钮那样工作),在我看来,这不太简洁。在下一个项目中,我一定会在路由方面投入更多时间和精力。

应用程序大小

最后,该应用程序的大小超过 1.4 MB,我不确定,但我认为应该比这小得多。我稍后会花些时间分析这个问题。

最后的想法

这段小小的旅程很有趣,我对最终的结果也很满意。我想我终于可以称自己为“全栈开发者”了。我知道这个领域还有很多东西我还没有探索和掌握,但这只是一个开始。

我不知道接下来该怎么做,所以我会慢慢思考,决定下一步该怎么做。希望您读得愉快,欢迎随时试用这款应用并给我一些反馈。再见!

再见

文章来源:https://dev.to/aouahib/i-built-a-chat-app-using-react-and-graphql-1ejm
PREV
我为 DevTo 构建了一个 MCP 服务器(100% 开源)🎉
NEXT
浏览器如何呈现网页?