<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>React + NestJS + Socket.io 项目实践【3】:消息发送 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS + Socket.io 全栈项目实践的第三篇,核心是实现用户间的私聊消息发送与实时显示。后端部分,教程讲解了如何通过 SocketGateway 接收消息并调用服务将其持久化到 MongoDB。前端部分,则演示了如何构建私聊界面,通过 Socket.io emit 发送消息,并利用 React Context 实现消息的即时渲染。此外,文章还解决了多个常见开发问题:聊天窗口的自动滚动、浏览器刷新后登录状态的恢复,以及通过 Axios 拦截器处理过期的 JWT Token,从而构建出一个体验完善的实时聊天核心功能。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/b5ac.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/b5ac.html">永久链接</a><div class="p-summary visually-hidden"><p>近期在期末考,所以更新会比较慢,这篇文章主要讲解如何实现消息发送功能。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/%E5%85%A8%E6%A0%88%E5%AE%9E%E8%B7%B5/">全栈实践</a><a class="p-category" href="../tags/Node-js/">Node.js</a><a class="p-category" href="../tags/React-js/">React.js</a><a class="p-category" href="../tags/TypeScript/">TypeScript</a><a class="p-category" href="../tags/NestJS/">NestJS</a></div><h1 class="post-title p-name">React + NestJS + Socket.io 项目实践【3】:消息发送</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-05-14T07:41:50.000Z">5/14/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.993Z"></time></div><div class="post-content e-content"><html><head></head><body><p>近期在期末考,所以更新会比较慢,这篇文章主要讲解如何实现消息发送功能。</p>
<span id="more"></span>
<h1 id="客户端的私聊界面"><a class="markdownIt-Anchor" href="#客户端的私聊界面"></a> 客户端的私聊界面</h1>
<p>之前的文章中我都没有去讲解客户端的代码。在讲解消息发送之前,我先介绍一下客户端的代码。</p>
<p>我的客户端中有一个 <code>PrivateMessageChatPage</code> 组件,用来显示和某个用户的私聊界面。这是最终的效果:</p>
<video id="video" controls="" height="420">
<source id="mp4" src="b5ac/Test.mp4" type="video/mp4">
</video>
<p>而 <code>PrivateMessageChatPage</code> 的布局是这样的:</p>
<p><img src="/posts/b5ac/1.png" alt="PrivateMessageChatPage"></p>
<p>目前只有 <code>PrivateMessageMessagesWrapper</code> 和 <code>PrivateMessageTextBox</code> 组件是有内容的,其他的组件都是空的。不过这不影响我们的消息显示。</p>
<p>首先导入这些组件和一些类型:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, { useState } <span class="keyword">from</span> <span class="string">'react'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageTabBar</span> <span class="keyword">from</span> <span class="string">'./Private_Message_Tab_Bar'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageMessagesWrapper</span> <span class="keyword">from</span> <span class="string">'./Private_Message_Messages_Wrapper'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageProfilePanel</span> <span class="keyword">from</span> <span class="string">'./Private_Message_Profile_Panel'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageTextBox</span> <span class="keyword">from</span> <span class="string">'./Private_Message_Text_Box'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">FriendsListSideBar</span> <span class="keyword">from</span> <span class="string">'../private_message_common/Friends_List_Sidebar'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Message</span> } <span class="keyword">from</span> <span class="string">'../../types/interfaces'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MessageContext</span> } <span class="keyword">from</span> <span class="string">'../context/Message_Context'</span></span><br></pre></td></tr></tbody></table></figure>
<p><code>Message</code> 类型:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">Message</span> {</span><br><span class="line"> <span class="attr">id</span>?: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">senderId</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">receiverId</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">text</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">timestamp</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>每条消息都有一个 <code>senderId</code>、<code>receiverId</code>、<code>text</code> 和 <code>timestamp</code>。<code>id</code> 是消息的唯一标识符。</p>
<p>我们之后会根据 <code>senderId</code> 和 <code>receiverId</code> 来从服务端获取消息。</p>
<p><code>PrivateMessageChatPage</code> 组件:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">PrivateMessageChatPage</span> = (<span class="params"></span>) => {</span><br><span class="line"> <span class="keyword">const</span> [receiverName, setReceiverName] = useState<<span class="built_in">string</span>>(<span class="string">'Dummy'</span>)</span><br><span class="line"> <span class="keyword">const</span> [newMessage, setNewMessage] = useState<<span class="title class_">Message</span> | <span class="literal">null</span>>(<span class="literal">null</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">addMessage</span> = (<span class="params"><span class="attr">message</span>: <span class="title class_">Message</span></span>) => {</span><br><span class="line"> <span class="title function_">setNewMessage</span>(message)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">MessageContext.Provider</span> <span class="attr">value</span>=<span class="string">{{</span> <span class="attr">newMessage</span>, <span class="attr">addMessage</span> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">FriendsListSideBar</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"d-flex flex-column mx-0 h-100 w-100"</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"d-flex flex-row"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">height:</span> '<span class="attr">48px</span>', <span class="attr">padding:</span> '<span class="attr">8px</span>', <span class="attr">fontSize:</span> '<span class="attr">16px</span>', <span class="attr">borderBottom:</span> '<span class="attr">solid</span> <span class="attr">3px</span> <span class="attr">rgba</span>(<span class="attr">45</span>, <span class="attr">47</span>, <span class="attr">52</span>)' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">PrivateMessageTabBar</span> <span class="attr">receiverUsername</span>=<span class="string">{receiverName}</span> <span class="attr">setReceiverUsername</span>=<span class="string">{setReceiverName}</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"d-flex flex-row flex-fill align-items-stretch p-0"</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"d-flex flex-column h-100 position-relative"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">minWidth:</span> <span class="attr">0</span>, <span class="attr">minHeight:</span> <span class="attr">0</span>, <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"position-relative"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>', <span class="attr">minHeight:</span> <span class="attr">0</span>, <span class="attr">minWidth:</span> <span class="attr">0</span>, <span class="attr">zIndex:</span> <span class="attr">0</span> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">PrivateMessageMessagesWrapper</span> <span class="attr">receiverUsername</span>=<span class="string">{receiverName}</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"position-sticky bottom-0 w-100"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">backgroundColor:</span> '<span class="attr">rgba</span>(<span class="attr">49</span>, <span class="attr">51</span>, <span class="attr">56</span>)' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">PrivateMessageTextBox</span> <span class="attr">receiverUsername</span>=<span class="string">{receiverName}</span> <span class="attr">addMessage</span>=<span class="string">{addMessage}</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">PrivateMessageProfilePanel</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">MessageContext.Provider</span>></span></span></span><br><span class="line"> )</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">PrivateMessageChatPage</span></span><br></pre></td></tr></tbody></table></figure>
<p><code>MessageContext</code> 是一个 React 上下文,用来传递消息。</p>
<blockquote>
<p>React 的 Context API 是一种在组件之间共享数据的方法,而不必通过组件树的逐层传递 <code>props</code>。</p>
<p>通过创建一个 Context 对象,然后使用 <code><MyContext.Provider></code> 组件将值传递给后代组件,可以在组件树中传递数据。</p>
</blockquote>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">'react'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Message</span> } <span class="keyword">from</span> <span class="string">'../../types/interfaces'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">MessageContextType</span> {</span><br><span class="line"> <span class="attr">newMessage</span>: <span class="title class_">Message</span> | <span class="literal">null</span></span><br><span class="line"> <span class="attr">addMessage</span>: <span class="function">(<span class="params"><span class="attr">message</span>: <span class="title class_">Message</span></span>) =></span> <span class="built_in">void</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">MessageContext</span> = <span class="title class_">React</span>.<span class="property">createContext</span><<span class="title class_">MessageContextType</span> | <span class="literal">undefined</span>>(<span class="literal">undefined</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>在这个例子中,<code>MessageContext</code> 被用来在组件树中共享 <code>newMessage</code> 和 <code>addMessage</code>。</p>
<ul>
<li><code>newMessage</code> 是最新的消息。</li>
<li><code>addMessage</code> 是一个函数,用来添加消息。</li>
</ul>
<p>我们的 <code>PrivateMessageTextBox</code> 组件是用来发送消息的,当用户在输入框中输入新的消息并发送时,组件会调用 <code>addMessage</code> 方法,将新消息添加到 <code>MessageContext</code> 中。而 <code>PrivateMessageMessagesWrapper</code> 组件会监听 <code>newMessage</code> 的改变,一旦 <code>newMessage</code> 出现了变化,新消息就会被添加到该组件的状态中用于显示。</p>
<p>这意味着,用户只要发送了消息,消息就会立即显示在界面上。</p>
<p><code>PrivateMessageTextBox</code> 组件的 <code>handleSendMessage</code> 方法:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">PrivateMessageTextBox</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">PrivateMessageTextBoxProps</span>> = <span class="function">(<span class="params">{ receiverUsername }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> [message, setMessage] = useState<<span class="built_in">string</span>>(<span class="string">''</span>)</span><br><span class="line"> <span class="keyword">const</span> context = <span class="title function_">useContext</span>(<span class="title class_">MessageContext</span>)</span><br><span class="line"> <span class="keyword">if</span> (!context) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">'MessageContext is undefined'</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> {addMessage} = context</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">ChangeEvent</span><<span class="title class_">HTMLTextAreaElement</span>></span>) => {</span><br><span class="line"> <span class="title function_">setMessage</span>(e.<span class="property">target</span>.<span class="property">value</span>)</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">handleSendMessage</span> = <span class="keyword">async</span> (<span class="params"><span class="attr">e</span>?: <span class="title class_">FormEvent</span></span>) => {</span><br><span class="line"> e && e.<span class="title function_">preventDefault</span>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> jwtToken = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'jwtToken'</span>)</span><br><span class="line"> <span class="keyword">const</span> senderId = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'userId'</span>)</span><br><span class="line"> <span class="keyword">if</span> (!senderId) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">'User ID not found in local storage'</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UserService</span>.<span class="title function_">getUserByUsername</span>(jwtToken, receiverUsername)</span><br><span class="line"> <span class="keyword">const</span> receiver = response.<span class="property">data</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> privateMessage = {</span><br><span class="line"> <span class="attr">id</span>: <span class="string">`<span class="subst">${socket.id}</span><span class="subst">${<span class="built_in">Math</span>.random()}</span>`</span>,</span><br><span class="line"> <span class="attr">senderId</span>: senderId,</span><br><span class="line"> <span class="attr">receiverId</span>: receiver.<span class="property">_id</span>,</span><br><span class="line"> <span class="attr">text</span>: message,</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toISOString</span>(),</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> socket.<span class="title function_">emit</span>(<span class="string">'privateMessageSent'</span>, privateMessage)</span><br><span class="line"> <span class="title function_">addMessage</span>(privateMessage)</span><br><span class="line"> <span class="title function_">setMessage</span>(<span class="string">''</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>这里的 <code>UserService.getUserByUsername</code> 方法进行了更改,需要多传入一个 <code>jwtToken</code> 参数。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UserService</span> = {</span><br><span class="line"> <span class="attr">getUserByUsername</span>: <span class="function">(<span class="params"><span class="attr">token</span>: <span class="built_in">string</span> | <span class="literal">null</span>, <span class="attr">username</span>: <span class="built_in">string</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> api.<span class="title function_">get</span>(<span class="string">`/users/username/<span class="subst">${username}</span>`</span>, { <span class="attr">headers</span>: { <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">${token}</span>`</span> } })</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="attr">getUserByUserId</span>: <span class="function">(<span class="params"><span class="attr">token</span>: <span class="built_in">string</span> | <span class="literal">null</span>, <span class="attr">userId</span>: <span class="built_in">string</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> api.<span class="title function_">get</span>(<span class="string">`/users/userid/<span class="subst">${userId}</span>`</span>, { <span class="attr">headers</span>: { <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">${token}</span>`</span> } })</span><br><span class="line"> },</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这是因为在上个文章中我实现了 <code>@Public</code> 装饰器,用来标记哪些路由是公开的。</p>
<p>根据用户的用户名或者 ID 来获取用户信息,都应当是私有的,所以需要传入 <code>jwtToken</code>。</p>
<blockquote>
<p>OAuth 2.0 授权框架规范中定义了 <code>Bearer</code> 令牌类型,它是一种用于 OAuth 2.0 的访问令牌,用于对资源进行身份验证。任何持有 <code>Bearer</code> 令牌的人都可以访问与该令牌相关联的资源。</p>
</blockquote>
</blockquote>
<p>向服务端发送 <code>privateMessageSent</code> 事件后,立即调用 <code>addMessage</code> 方法,将消息添加到 <code>MessageContext</code> 中。</p>
<h1 id="消息传递"><a class="markdownIt-Anchor" href="#消息传递"></a> 消息传递</h1>
<p>如果只是写了客户端的代码,那么消息只是在客户端显示,而不会真正的发送到服务端。要实现消息发送,我们需要在服务端中接收消息。</p>
<p>在 <a href="/posts/40b4.html">上个文章</a> 的最后一个分段中,我实现了 Socket 模块。</p>
<p>Socket 用白话来说就是一个通道,客户端和服务端可以通过这个通道进行双向通信。双向通信和传统的 HTTP 请求不同,HTTP 请求是单向的,客户端向服务端发送请求、服务端返回响应。而 Socket 是双向的,客户端和服务端可以随时向对方发送消息。</p>
<p>上个文章中 <code>SocketGateway</code>(网关)订阅了 <code>privateMessageSent</code> 事件,但我只是简单的打印了一下消息,并没有去更进一步的处理。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SubscribeMessage</span>(<span class="string">'privateMessageSent'</span>)</span><br><span class="line"><span class="title function_">handlePrivateMessage</span>(<span class="params"><span class="meta">@MessageBody</span>() <span class="attr">data</span>: <span class="built_in">any</span>, <span class="attr">client</span>: <span class="title class_">Socket</span></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Received private message:'</span>, data, <span class="string">'from'</span>, client)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这次我要详细地讲解如何实现消息传递。首先消息传递在技术上的流程是这样的:</p>
<ol>
<li>客户端使用 Socket 向服务端发送 <code>privateMessageSent</code> 事件。</li>
<li>服务端在 <code>SocketGateway</code> 中接收到 <code>privateMessageSent</code> 事件后,依靠 <code>MessagesService</code> 将消息存储到数据库中。</li>
</ol>
<p>为什么要细分出两个模块呢?因为我认为这两个模块的职责不同,<code>Socket</code> 模块负责处理 Socket 相关的逻辑,<code>Messages</code> 模块则负责处理消息的存储与获取。</p>
<p>上个文章中并没有细写 <code>Messages</code> 模块,我现在来写一下:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongooseModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Message</span>, <span class="title class_">MessageSchema</span> } <span class="keyword">from</span> <span class="string">'./message.schema'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MessagesService</span> } <span class="keyword">from</span> <span class="string">'./messages.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MessagesController</span> } <span class="keyword">from</span> <span class="string">'./messages.controller'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [<span class="title class_">MongooseModule</span>.<span class="title function_">forFeature</span>([{ <span class="attr">name</span>: <span class="title class_">Message</span>.<span class="property">name</span>, <span class="attr">schema</span>: <span class="title class_">MessageSchema</span> }])],</span><br><span class="line"> <span class="attr">providers</span>: [<span class="title class_">MessagesService</span>],</span><br><span class="line"> <span class="attr">controllers</span>: [<span class="title class_">MessagesController</span>],</span><br><span class="line"> <span class="attr">exports</span>: [<span class="title class_">MessagesService</span>],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">MessagesModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>exports</code> 是用来导出 <code>MessagesService</code> 的,这样其他模块就可以使用 <code>MessagesService</code> 了。刚才也说过,<code>SocketGateway</code> 需要使用 <code>MessagesService</code> 来存储消息。</p>
</blockquote>
<p>我在这里做了一些修改,将 <code>MessagesSchema</code> 更改为了 <code>MessageSchema</code>。因为这个模型实际上是用来存储单一的消息,所以我认为它的名字应该是单数形式。同时,我将原先的 <code>sender</code> 和 <code>receiver</code> 更改为了 <code>senderId</code> 和 <code>receiverId</code>,因为我想通过用户的 ID 来查找用户,而不是直接使用用户对象。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Prop</span>, <span class="title class_">Schema</span>, <span class="title class_">SchemaFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Document</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Schema</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Message</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Document</span> {</span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">senderId</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">receiverId</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">text</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">default</span>: <span class="title class_">Date</span>.<span class="property">now</span> })</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="title class_">Date</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">MessageSchema</span> = <span class="title class_">SchemaFactory</span>.<span class="title function_">createForClass</span>(<span class="title class_">Message</span>)</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>在这里,我添加了一个 <code>timestamp</code> 字段,用来记录每条消息的发送时间。</p>
</blockquote>
<div class="danger">
<p>在客户端里,我将发送给服务端的消息数据结构设计为以下形式:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">id</span>: <span class="string">`<span class="subst">${socket.id}</span><span class="subst">${<span class="built_in">Math</span>.random()}</span>`</span>,</span><br><span class="line"> <span class="attr">senderId</span>: senderId,</span><br><span class="line"> <span class="attr">receiverId</span>: receiver.<span class="property">_id</span>,</span><br><span class="line"> <span class="attr">text</span>: message,</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toISOString</span>(),</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这里,我也添加了一个 <code>timestamp</code> 字段。这是因为我希望客户端在发送消息后,能立即在界面上显示这条消息,而不需要等待服务端的响应。</p>
<p>值得注意的是,我选择让客户端直接显示消息,而没有等待服务端存储消息并返回。这样做的结果是,客户端显示的时间戳实际上是客户端发送消息的时间,而不是服务端存储消息的时间,除非用户刷新了页面,让客户端向服务端请求实际的数据。</p>
</div>
<p>接着我们要在 <code>MessagesService</code> 中添加一个方法,用来存储消息。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">InjectModel</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Model</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Message</span> } <span class="keyword">from</span> <span class="string">'./message.schema'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">CreateMessageDto</span> } <span class="keyword">from</span> <span class="string">'./dto/create-message.dto'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">MessagesService</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="meta">@InjectModel</span>(Message.name) <span class="keyword">private</span> <span class="attr">messageModel</span>: <span class="title class_">Model</span><<span class="title class_">Message</span>></span>) {}</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">create</span>(<span class="attr">createMessageDto</span>: <span class="title class_">CreateMessageDto</span>): <span class="title class_">Promise</span><<span class="title class_">Message</span>> {</span><br><span class="line"> <span class="keyword">const</span> createdMessage = <span class="keyword">new</span> <span class="variable language_">this</span>.<span class="title function_">messageModel</span>(createMessageDto)</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">await</span> createdMessage</span><br><span class="line"> .<span class="title function_">save</span>()</span><br><span class="line"> .<span class="title function_">then</span>(<span class="title function_">async</span> (message) => {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Message saved:'</span>, message)</span><br><span class="line"> <span class="keyword">return</span> message</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function">(<span class="params"><span class="attr">error</span>: <span class="built_in">any</span></span>) =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Error saving message:'</span>, error)</span><br><span class="line"> <span class="keyword">throw</span> error</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>CreateMessageDto</code> 是一个数据传输对象,用来传输消息的数据。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">CreateMessageDto</span> {</span><br><span class="line"> <span class="keyword">readonly</span> <span class="attr">text</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="keyword">readonly</span> <span class="attr">senderId</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="keyword">readonly</span> <span class="attr">receiverId</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在这个方法中,我只需要 <code>text</code>、<code>senderId</code> 和 <code>receiverId</code> 这三个字段。</p>
<p>接着使用 <code>save</code> 方法来保存消息,如果保存成功则返回消息,否则抛出错误。</p>
</blockquote>
<p>最后就是在 <code>SocketGateway</code> 中调用 <code>MessagesService</code> 的 <code>create</code> 方法:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">MessagesService</span> } <span class="keyword">from</span> <span class="string">'../messages/messages.service'</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SocketGateway</span> <span class="keyword">implements</span> <span class="title class_">OnGatewayInit</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="comment">// ...</span></span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="attr">messagesService</span>: <span class="title class_">MessagesService</span>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@SubscribeMessage</span>(<span class="string">'privateMessageSent'</span>)</span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">handlePrivateMessage</span>(<span class="meta">@MessageBody</span>() <span class="attr">data</span>: <span class="built_in">any</span>): <span class="title class_">Promise</span><<span class="built_in">void</span>> {</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">messagesService</span>.<span class="title function_">create</span>({ <span class="attr">senderId</span>: data.<span class="property">senderId</span>, <span class="attr">receiverId</span>: data.<span class="property">receiverId</span>, <span class="attr">text</span>: data.<span class="property">text</span> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>别忘了 <code>SocketModule</code> 中要导入 <code>MessagesModule</code>,才能让 <code>SocketGateway</code> 使用 <code>MessagesService</code>。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketService</span> } <span class="keyword">from</span> <span class="string">'./socket.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketGateway</span> } <span class="keyword">from</span> <span class="string">'./socket.gateway'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MessagesModule</span> } <span class="keyword">from</span> <span class="string">'../messages/messages.module'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [<span class="title class_">MessagesModule</span>],</span><br><span class="line"> <span class="attr">providers</span>: [<span class="title class_">SocketGateway</span>, <span class="title class_">SocketService</span>],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SocketModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<p>这样服务端就可以接收到客户端发送的消息,并将消息存储到数据库中。</p>
<h1 id="消息显示"><a class="markdownIt-Anchor" href="#消息显示"></a> 消息显示</h1>
<p>用户点击他们和其他用户的私聊界面时,我们需要从服务端获取他们之间的所有消息。</p>
<p>在我的应用中,因为是仿照 Discord 的,所有的私聊路由都是 <code>/channel/@me/:id</code> 这种形式的。<code>id</code> 是接收者的 ID。</p>
<p>也就是说我们可以让服务端有一个 GET 路由,当客户端访问这个路由时,服务端会返回当前用户和 <code>id</code> 用户之间的所有消息。</p>
<p>在 <code>MessagesController</code> 中添加 GET 路由:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Controller</span>, <span class="title class_">Get</span>, <span class="title class_">Param</span>, <span class="title class_">Request</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span> <span class="keyword">as</span> <span class="title class_">ExpressRequest</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MessagesService</span> } <span class="keyword">from</span> <span class="string">'./messages.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'messages'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">MessagesController</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">messagesService</span>: <span class="title class_">MessagesService</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Get</span>(<span class="string">':senderId/:receiverId'</span>)</span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">getMessages</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="meta">@Request</span>() <span class="attr">req</span>: <span class="title class_">ExpressRequest</span>,</span></span><br><span class="line"><span class="params"> <span class="meta">@Param</span>(<span class="string">'senderId'</span>) <span class="attr">senderId</span>: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> <span class="meta">@Param</span>(<span class="string">'receiverId'</span>) <span class="attr">receiverId</span>: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">messagesService</span>.<span class="title function_">getMessages</span>(senderId, receiverId)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>当客户端访问 <code>/api/messages/:senderId/:receiverId</code> 时,服务端会返回当前用户(<code>senderId</code>)和 <code>receiverId</code> 用户之间的所有消息。</p>
<p><code>MessagesService</code> 中添加 <code>getMessages</code> 方法:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">getMessages</span>(<span class="params"><span class="attr">senderId</span>: <span class="built_in">string</span>, <span class="attr">receiverId</span>: <span class="built_in">string</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">messageModel</span></span><br><span class="line"> .<span class="title function_">find</span>({</span><br><span class="line"> <span class="attr">senderId</span>: senderId,</span><br><span class="line"> <span class="attr">receiverId</span>: receiverId,</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">exec</span>()</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function">(<span class="params">messages</span>) =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Messages found:'</span>, messages)</span><br><span class="line"> <span class="keyword">return</span> messages</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function">(<span class="params"><span class="attr">error</span>: <span class="built_in">any</span></span>) =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Error finding messages:'</span>, error)</span><br><span class="line"> <span class="keyword">throw</span> error</span><br><span class="line"> })</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>客户端里也添加一个方法,专门访问 <code>/api/messages/:receiverId</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">MessageService</span> = {</span><br><span class="line"> <span class="attr">getMessagesByUserId</span>: <span class="function">(<span class="params"><span class="attr">token</span>: <span class="built_in">string</span> | <span class="literal">null</span>, <span class="attr">senderId</span>: <span class="built_in">string</span> | <span class="literal">null</span>, <span class="attr">receiverId</span>: <span class="built_in">string</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> api.<span class="title function_">get</span>(<span class="string">`/messages/<span class="subst">${senderId}</span>/<span class="subst">${receiverId}</span>`</span>, { <span class="attr">headers</span>: { <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">${token}</span>`</span> } })</span><br><span class="line"> },</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>现在回到客户端的 <code>PrivateMessageMessagesWrapper</code> 组件。我们需要明白这个组件的职责是什么:</p>
<ol>
<li>当用户点击某个用户的私聊界面时,组件会向服务端请求 <code>senderId</code> 用户和 <code>receiverId</code> 用户之间的所有消息。</li>
<li>当用户发送消息时,组件会将新消息添加到消息列表中来立即显示。</li>
</ol>
<p>首先以最基础的形式来写这个组件:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">PrivateMessageMessagesWrapper</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">PrivateMessageMessagesWrapperProps</span>> = <span class="function">(<span class="params">{ receiverUsername }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> [messages, setMessages] = useState<<span class="title class_">Message</span>[]>([])</span><br><span class="line"> <span class="keyword">const</span> currentUser = <span class="title function_">useSelector</span>(<span class="function">(<span class="params"><span class="attr">state</span>: { auth: { user: UserProfile } }</span>) =></span> state.<span class="property">auth</span>.<span class="property">user</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"d-flex flex-column position-absolute top-0 bottom-0 overflow-y-scroll overflow-x-hidden"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">left:</span> <span class="attr">0</span>, <span class="attr">right:</span> <span class="attr">0</span>, <span class="attr">overflowAnchor:</span> '<span class="attr">none</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">ol</span> <span class="attr">className</span>=<span class="string">"p-0 m-0"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> <span class="attr">1</span>, <span class="attr">minHeight:</span> '<span class="attr">0</span>', <span class="attr">listStyle:</span> '<span class="attr">none</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> {messages.map((message, index) => (</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">li</span> <span class="attr">key</span>=<span class="string">{message.id</span> || <span class="attr">index</span>} <span class="attr">className</span>=<span class="string">"position-relative mx-2"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">outline:</span> '<span class="attr">none</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"position-relative"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">marginTop:</span> '<span class="attr">1.0625rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">minHeight:</span> '<span class="attr">2.75rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingTop:</span> '<span class="attr">0.125rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingBottom:</span> '<span class="attr">0.125rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingLeft:</span> '<span class="attr">72px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingRight:</span> '<span class="attr">48px</span>!<span class="attr">important</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">wordWrap:</span> '<span class="attr">break-word</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">userSelect:</span> '<span class="attr">text</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"position-static ms-0 ps-0"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">textIndent:</span> '<span class="attr">none</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> {/* User's avatar */}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">img</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">src</span>=<span class="string">{imgURL}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"position-absolute overflow-hidden"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">pointerEvents:</span> '<span class="attr">auto</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">textIndent:</span> '<span class="attr">-9999px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">left:</span> '<span class="attr">16px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">marginTop:</span> '<span class="attr">calc</span>(<span class="attr">4px-0.125rem</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">width:</span> '<span class="attr">40px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">height:</span> '<span class="attr">40px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">borderRadius:</span> '<span class="attr">50</span>%',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">cursor:</span> '<span class="attr">pointer</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">userSelect:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">alt</span>=<span class="string">""</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"> {/* Username and message time */}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">h3</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"overflow-hidden position-relative p-0 m-0 d-flex flex-row align-items-center"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">display:</span> '<span class="attr">block</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">lineHeight:</span> '<span class="attr">1.375rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">minHeight:</span> '<span class="attr">1.375rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">whiteSpace:</span> '<span class="attr">break-spaces</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">span</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"me-1 fs-6 position-relative overflow-hidden text-white"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">fontWeight:</span> '<span class="attr">500</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">display:</span> '<span class="attr">inline</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">verticalAlign:</span> '<span class="attr">baseline</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">outline:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}></span></span></span><br><span class="line"><span class="language-xml"> {message.senderId === currentUser._id ? currentUser.username : receiverUsername}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">span</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"ms-1"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">fontSize:</span> '<span class="attr">0.75rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">height:</span> '<span class="attr">1.25rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">verticalAlign:</span> '<span class="attr">baseline</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">display:</span> '<span class="attr">inline-block</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">cursor:</span> '<span class="attr">default</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">pointerEvents:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">outline:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">fontWeight:</span> '<span class="attr">500</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">148</span>, <span class="attr">154</span>, <span class="attr">158</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">time</span> <span class="attr">dateTime</span>=<span class="string">{message.timestamp.toString()}</span>></span>{formatDate(message.timestamp)}<span class="tag"></<span class="name">time</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">h3</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"> {/* Message Content */}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"overflow-hidden position-relative fs-6 p-0 m-0"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">userSelect:</span> '<span class="attr">text</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">whiteSpace:</span> '<span class="attr">break-spaces</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">wordWrap:</span> '<span class="attr">break-word</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">marginLeft:</span> '<span class="attr">calc</span>(<span class="attr">-1</span> * <span class="attr">72px</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingLeft:</span> '<span class="attr">72px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">textIndent:</span> '<span class="attr">0</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">lineHeight:</span> '<span class="attr">1.375rem</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">span</span>></span>{message.text}<span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">li</span>></span></span></span><br><span class="line"><span class="language-xml"> ))}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">ol</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"> )</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>messages</code> 是该组件的状态,用来存储所有的需要被显示的消息。消息的数据结构是 <code>Message</code>,上面已经定义过了。</li>
<li><code>currentUser</code> 是从 Redux 中提取出的当前用户的信息,包括了用户的 ID 和用户名。</li>
</ul>
<p>这个组件的底层逻辑是这样的:</p>
<ol>
<li><code>map</code> 遍历 <code>messages</code> 数组,对每一条消息都生成一个 <code>li</code> 元素。</li>
<li>每一条消息都包含了用户的头像(这里写死了)、用户名(如果消息的 <code>senderId</code> 和当前用户的 ID 相同,那么消息的用户名就是当前用户的用户名,否则就是接收者的用户名)、消息发送时间和消息内容。</li>
</ol>
<blockquote>
<p>消息发送时间是通过 <code>formatDate</code> 函数格式化的:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">formatDate</span> = (<span class="params"><span class="attr">timestamp</span>: <span class="built_in">string</span></span>) => {</span><br><span class="line"> <span class="keyword">const</span> date = <span class="keyword">new</span> <span class="title class_">Date</span>(timestamp)</span><br><span class="line"> <span class="keyword">const</span> year = date.<span class="title function_">getFullYear</span>()</span><br><span class="line"> <span class="keyword">const</span> month = <span class="title class_">String</span>(date.<span class="title function_">getMonth</span>() + <span class="number">1</span>).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">'0'</span>)</span><br><span class="line"> <span class="keyword">const</span> day = <span class="title class_">String</span>(date.<span class="title function_">getDate</span>()).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">'0'</span>)</span><br><span class="line"> <span class="keyword">const</span> hours = <span class="title class_">String</span>(date.<span class="title function_">getHours</span>()).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">'0'</span>)</span><br><span class="line"> <span class="keyword">const</span> minutes = <span class="title class_">String</span>(date.<span class="title function_">getMinutes</span>()).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">'0'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${year}</span>/<span class="subst">${month}</span>/<span class="subst">${day}</span> <span class="subst">${hours}</span>:<span class="subst">${minutes}</span>`</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<p>那么我们该如何获取消息、并将消息添加到 <code>messages</code> 中呢?</p>
<p>当 <code>PrivateMessageMessagesWrapper</code> 组件被挂载时,以及 <code>receiverUsername</code> 发生变化时,我们都需要向服务端请求消息:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">fetchMessages</span> = <span class="keyword">async</span> (<span class="params"></span>) => {</span><br><span class="line"> <span class="keyword">const</span> jwtToken = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'jwtToken'</span>)</span><br><span class="line"> <span class="keyword">const</span> senderId = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'userId'</span>)</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UserService</span>.<span class="title function_">getUserByUsername</span>(jwtToken, receiverUsername)</span><br><span class="line"> <span class="keyword">const</span> receiver = response.<span class="property">data</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title class_">MessageService</span>.<span class="title function_">getMessagesByUserId</span>(jwtToken, senderId, receiver.<span class="property">_id</span>)</span><br><span class="line"> <span class="title function_">setMessages</span>(res.<span class="property">data</span>)</span><br><span class="line"> <span class="keyword">return</span> res.<span class="property">data</span></span><br><span class="line"> } <span class="keyword">catch</span> (err) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(err)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">fetchMessages</span>().<span class="title function_">then</span>(<span class="function">(<span class="params">r</span>) =></span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Messages fetched:'</span>, r))</span><br><span class="line">}, [receiverUsername])</span><br></pre></td></tr></tbody></table></figure>
<p>这里我使用了 <code>useEffect</code> 钩子,当 <code>receiverUsername</code> 发生变化时,就会调用 <code>fetchMessages</code> 方法。</p>
<p><code>fetchMessages</code> 方法会向服务端请求 <code>senderId</code> 用户和 <code>receiverId</code> 用户之间的所有消息,并将消息存储到 <code>messages</code> 中。</p>
<p>不只是如此,先前我们在 <code>PrivateMessageTextBox</code> 组件中发送消息时,会将用户自身发送的消息添加到 <code>MessageContext</code> 中。<code>PrivateMessageMessagesWrapper</code> 组件同样也需要去监听用户自身发送的消息,并将这些消息添加到 <code>messages</code> 中:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> context = <span class="title function_">useContext</span>(<span class="title class_">MessageContext</span>)</span><br><span class="line"><span class="keyword">if</span> (!context) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">'MessageContext is undefined'</span>)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">const</span> { newMessage } = context</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (newMessage) {</span><br><span class="line"> <span class="title function_">setMessages</span>(<span class="function">(<span class="params">prevMessages</span>) =></span> [...prevMessages, newMessage])</span><br><span class="line"> }</span><br><span class="line">}, [newMessage])</span><br></pre></td></tr></tbody></table></figure>
<h1 id="滚动到底部"><a class="markdownIt-Anchor" href="#滚动到底部"></a> 滚动到底部</h1>
<p>当该被显示的消息超过了可视区域时,用户需要手动滚动到底部才能看到最新的消息。这是不友好的,我们应当让用户看到最新的消息。</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> endOfMessagesRef = useRef<<span class="literal">null</span> | <span class="title class_">HTMLSpanElement</span>>(<span class="literal">null</span>)</span><br><span class="line"><span class="keyword">const</span> prevMessagesLength = useRef<<span class="built_in">number</span>>(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (endOfMessagesRef.<span class="property">current</span> && messages.<span class="property">length</span> > prevMessagesLength.<span class="property">current</span>) {</span><br><span class="line"> endOfMessagesRef.<span class="property">current</span>.<span class="title function_">scrollIntoView</span>({ <span class="attr">behavior</span>: <span class="string">'smooth'</span>, <span class="attr">block</span>: <span class="string">'nearest'</span>, <span class="attr">inline</span>: <span class="string">'start'</span> })</span><br><span class="line"> }</span><br><span class="line"> prevMessagesLength.<span class="property">current</span> = messages.<span class="property">length</span></span><br><span class="line">}, [messages])</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">ol</span>></span></span></span><br><span class="line"><span class="language-xml"> {messages.map((message, index) => (</span></span><br><span class="line"><span class="language-xml"> // ...</span></span><br><span class="line"><span class="language-xml"> ))}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">span</span> <span class="attr">ref</span>=<span class="string">{endOfMessagesRef}</span> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">ol</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line">)</span><br></pre></td></tr></tbody></table></figure>
<p>我使用了 <code>useRef</code> 来创建一个 <code>endOfMessagesRef</code> 引用,用来指向消息列表的最底部。当 <code>messages</code> 数组的长度发生变化时,我就将 <code>endOfMessagesRef</code> 滚动到可视区域。</p>
<p><code>prevMessagesLength</code> 也是一个 <code>useRef</code>,用来存储上一次 <code>messages</code> 的长度。这个长度在回调函数的最后会被更新为当前的 <code>messages</code> 的长度。</p>
<p><code>useEffect</code> 的回调函数会先检查 <code>endOfMessagesRef.current</code> 是否存在,以及 <code>messages</code> 的长度是否大于 <code>prevMessagesLength.current</code>。如果两个条件都满足,就将 <code>endOfMessagesRef.current</code> 滚动到可视区域。</p>
<blockquote>
<p><code>scrollIntoView</code> 方法是一个 DOM 方法,用来将元素滚动到可视区域。</p>
<ul>
<li><code>behavior</code> 决定了滚动的动画效果,<code>smooth</code> 表示平滑滚动。</li>
<li><code>block</code> 决定了元素在垂直方向上的对齐方式,<code>nearest</code> 表示将元素对齐到最接近的边缘。</li>
<li><code>inline</code> 决定了元素在水平方向上的对齐方式,<code>start</code> 表示将元素对齐到起始边缘。</li>
</ul>
</blockquote>
<p><code>span</code> 标签是一个空元素,用来占位,需要放在 <code>ol</code> 标签的最后一个子元素后面。</p>
<h1 id="浏览器刷新后状态丢失"><a class="markdownIt-Anchor" href="#浏览器刷新后状态丢失"></a> 浏览器刷新后状态丢失</h1>
<p>在用户刷新浏览器后,Redux 的状态会丢失。这是因为 Redux 的状态是存储在内存中的,刷新浏览器后内存被清空,状态也就丢失了。</p>
<p>由于我们已经在 <code>localStorage</code> 中存储了用户的 <code>jwtToken</code> 和 <code>userId</code>,我们可以从 <code>localStorage</code> 中获取这些信息,并重新设置 Redux 的状态。</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useAppDispatch } <span class="keyword">from</span> <span class="string">'./redux/store'</span></span><br><span class="line"><span class="keyword">import</span> { setUserDetails } <span class="keyword">from</span> <span class="string">'./redux/actions/authActions'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> dispatch = <span class="title function_">useAppDispatch</span>()</span><br><span class="line"></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'jwtToken'</span>)</span><br><span class="line"> <span class="keyword">const</span> userId = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'userId'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (token && userId) {</span><br><span class="line"> <span class="title function_">dispatch</span>(<span class="title function_">setUserDetails</span>(userId))</span><br><span class="line"> }</span><br><span class="line"> }, [dispatch])</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>setUserDetails</code> 是一个 Redux 的 action,用来设置用户的 ID。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">setUserDetails</span> = (<span class="params"><span class="attr">userId</span>: <span class="built_in">string</span></span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">async</span> (<span class="attr">dispatch</span>: <span class="title class_">Dispatch</span>) => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'jwtToken'</span>)</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UserService</span>.<span class="title function_">getUserByUserId</span>(token, userId)</span><br><span class="line"> <span class="keyword">if</span> (response.<span class="property">status</span> === <span class="number">200</span>) {</span><br><span class="line"> <span class="title function_">dispatch</span>(<span class="title function_">setCurrentUser</span>(response.<span class="property">data</span>))</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(response.<span class="property">data</span>.<span class="property">message</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>此处使用了 <code>localStorage</code> 中的 <code>userId</code> 来向服务端请求了用户的信息,并将用户信息存储到 Redux 的状态中。</p>
<h1 id="令牌过期"><a class="markdownIt-Anchor" href="#令牌过期"></a> 令牌过期</h1>
<p>服务端向客户端返回的 <code>jwtToken</code> 是有过期时间的。当 <code>jwtToken</code> 过期后,客户端再向服务端发送请求时,服务端会返回 <code>401 Unauthorized</code> 错误。</p>
<p>在这种情况下,我们应当让用户重新登录。我在 <code>axios</code> 的拦截器中添加了一个拦截器,用来处理 <code>401</code> 错误。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">api.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(</span><br><span class="line"> <span class="function">(<span class="params">response</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> response</span><br><span class="line"> },</span><br><span class="line"> <span class="function">(<span class="params">error</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (error.<span class="property">response</span>) {</span><br><span class="line"> <span class="keyword">if</span> (error.<span class="property">response</span>.<span class="property">status</span> === <span class="number">401</span> && error.<span class="property">response</span>.<span class="property">statusText</span> === <span class="string">'Unauthorized'</span>) {</span><br><span class="line"> <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="string">'jwtToken'</span>)</span><br><span class="line"> <span class="variable language_">window</span>.<span class="property">location</span>.<span class="title function_">reload</span>()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error)</span><br><span class="line"> },</span><br><span class="line">)</span><br></pre></td></tr></tbody></table></figure>
<p><code>window.location.reload()</code> 会重新加载页面,没有 <code>jwtToken</code> 的情况下,用户会因为路由守卫而被重定向到登录页面。</p>
<h1 id="其他的小改动"><a class="markdownIt-Anchor" href="#其他的小改动"></a> 其他的小改动</h1>
<h2 id="user接口"><a class="markdownIt-Anchor" href="#user接口"></a> <code>User</code> 接口</h2>
<p><code>User</code> 接口的 <code>id</code> 字段改为 <code>_id</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">User</span> {</span><br><span class="line"> <span class="attr">_id</span>?: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">emailAddress</span>?: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">username</span>: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">password</span>?: <span class="built_in">string</span></span><br><span class="line"> <span class="attr">access_token</span>?: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这是因为在 MongoDB 中,每个文档都有一个 <code>_id</code> 字段,用来唯一标识文档。为了可以直接将 MongoDB 中的文档映射到 <code>User</code> 接口,我将 <code>id</code> 改为了 <code>_id</code>。</p>
<h2 id="自定义滚动条"><a class="markdownIt-Anchor" href="#自定义滚动条"></a> 自定义滚动条</h2>
<p>我使用的是 Edge 浏览器,它的滚动条不能说难看,只能说和好看不搭边。所以我在 <code>index.css</code> 中自定义了滚动条的样式:</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">::-webkit-scrollbar {</span><br><span class="line"> <span class="attribute">width</span>: <span class="number">16px</span>;</span><br><span class="line"> <span class="attribute">height</span>: <span class="number">16px</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">::-webkit-scrollbar-track {</span><br><span class="line"> <span class="attribute">background</span>: <span class="built_in">hsl</span>( <span class="number">220</span> <span class="built_in">calc</span>( <span class="number">1</span> * <span class="number">6.5%</span>) <span class="number">18%</span> / <span class="number">1</span>);</span><br><span class="line"> <span class="attribute">margin-bottom</span>: <span class="number">8px</span>;</span><br><span class="line"> <span class="attribute">border</span>: <span class="number">4px</span> solid transparent;</span><br><span class="line"> <span class="attribute">background-clip</span>: padding-box;</span><br><span class="line"> <span class="attribute">border-radius</span>: <span class="number">8px</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">::-webkit-scrollbar-thumb {</span><br><span class="line"> <span class="attribute">background</span>: <span class="built_in">hsl</span>( <span class="number">225</span> <span class="built_in">calc</span>( <span class="number">1</span> * <span class="number">7.1%</span>) <span class="number">11%</span> / <span class="number">1</span>);</span><br><span class="line"> <span class="attribute">background-clip</span>: padding-box;</span><br><span class="line"> <span class="attribute">border</span>: <span class="number">4px</span> solid transparent;</span><br><span class="line"> <span class="attribute">border-radius</span>: <span class="number">8px</span>;</span><br><span class="line"> <span class="attribute">min-height</span>: <span class="number">40px</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">::-webkit-scrollbar-corner {</span><br><span class="line"> <span class="attribute">background</span>: transparent;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这些样式是根据 Discord 的滚动条样式来写的。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="b1c6.html">上一篇</a><a class="next" href="40b4.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/b5ac.html" data-full-url="https://cytrogen.icu/posts/b5ac.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>