~cytrogen/blog-public

blog-public/posts/b5ac.html -rw-r--r-- 119.2 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 4 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
<!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>) =&gt; {</span><br><span class="line">  <span class="keyword">const</span> [receiverName, setReceiverName] = useState&lt;<span class="built_in">string</span>&gt;(<span class="string">'Dummy'</span>)</span><br><span class="line">  <span class="keyword">const</span> [newMessage, setNewMessage] = useState&lt;<span class="title class_">Message</span> | <span class="literal">null</span>&gt;(<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>) =&gt; {</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">&lt;<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> }}&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">FriendsListSideBar</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">"d-flex flex-column mx-0 h-100 w-100"</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<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>)' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<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> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<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>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<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>' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<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> }}&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">PrivateMessageMessagesWrapper</span> <span class="attr">receiverUsername</span>=<span class="string">{receiverName}</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<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>)' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<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> /&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">PrivateMessageProfilePanel</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">MessageContext.Provider</span>&gt;</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>&lt;MyContext.Provider&gt;</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>) =&gt;</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>&lt;<span class="title class_">MessageContextType</span> | <span class="literal">undefined</span>&gt;(<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>&lt;<span class="title class_">PrivateMessageTextBoxProps</span>&gt; = <span class="function">(<span class="params">{ receiverUsername }</span>) =&gt;</span> {</span><br><span class="line">  <span class="keyword">const</span> [message, setMessage] = useState&lt;<span class="built_in">string</span>&gt;(<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>&lt;<span class="title class_">HTMLTextAreaElement</span>&gt;</span>) =&gt; {</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>) =&gt; {</span><br><span class="line">    e &amp;&amp; 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>) =&gt;</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>) =&gt;</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>&lt;<span class="title class_">Message</span>&gt;</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>&lt;<span class="title class_">Message</span>&gt; {</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) =&gt; {</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>) =&gt;</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>&lt;<span class="built_in">void</span>&gt; {</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>) =&gt;</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>) =&gt;</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>) =&gt;</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>&lt;<span class="title class_">PrivateMessageMessagesWrapperProps</span>&gt; = <span class="function">(<span class="params">{ receiverUsername }</span>) =&gt;</span> {</span><br><span class="line">  <span class="keyword">const</span> [messages, setMessages] = useState&lt;<span class="title class_">Message</span>[]&gt;([])</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>) =&gt;</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">&lt;<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>' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<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>' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">        {messages.map((message, index) =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<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>' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<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">              }}&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<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>' }}&gt;</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">&lt;<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">                /&gt;</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">&lt;<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">                  }}&gt;</span></span></span><br><span class="line"><span class="language-xml">                  <span class="tag">&lt;<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">                    }}&gt;</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">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                  <span class="tag">&lt;<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">                    }}&gt;</span></span></span><br><span class="line"><span class="language-xml">                    <span class="tag">&lt;<span class="name">time</span> <span class="attr">dateTime</span>=<span class="string">{message.timestamp.toString()}</span>&gt;</span>{formatDate(message.timestamp)}<span class="tag">&lt;/<span class="name">time</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                  <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;/<span class="name">div</span>&gt;</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">&lt;<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">                }}&gt;</span></span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;<span class="name">span</span>&gt;</span>{message.text}<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ))}</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</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>) =&gt; {</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">() =&gt;</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>) =&gt; {</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>) =&gt;</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">() =&gt;</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>) =&gt;</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&lt;<span class="literal">null</span> | <span class="title class_">HTMLSpanElement</span>&gt;(<span class="literal">null</span>)</span><br><span class="line"><span class="keyword">const</span> prevMessagesLength = useRef&lt;<span class="built_in">number</span>&gt;(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> {</span><br><span class="line">  <span class="keyword">if</span> (endOfMessagesRef.<span class="property">current</span> &amp;&amp; messages.<span class="property">length</span> &gt; 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">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      {messages.map((message, index) =&gt; (</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">&lt;<span class="name">span</span> <span class="attr">ref</span>=<span class="string">{endOfMessagesRef}</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</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">() =&gt;</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 &amp;&amp; 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>) =&gt; {</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>) =&gt; {</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>) =&gt;</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>) =&gt;</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> &amp;&amp; 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>