~cytrogen/blog-public

blog-public/posts/40b4.html -rw-r--r-- 137.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
<!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 项目实践【2】:JWT 验证 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS + Socket.io 全栈项目实践的第二篇,核心内容是实现基于 JWT 的用户身份验证。后端部分,教程详细讲解了如何配置 NestJS 的JWT模块、在登录成功后生成Token,并创建全局路由守卫 AccessTokenGuard 配合 @Public 装饰器来保护接口。前端部分,则演示了 React 应用在登录后如何存储Token,并利用自定义 useAuth Hook 和 ProtectedRoute 组件,根据认证状态实现动态的页面访问控制。本教程完整地展示了一个安全的全栈JWT认证流程,为后续功能开发奠定了权限基础。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/40b4.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/40b4.html">永久链接</a><div class="p-summary visually-hidden"><p>上篇文章我们迁移了整个项目到 NestJS,这篇文章我们将实现 JWT 验证。</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 项目实践【2】:JWT 验证</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-04-28T04:25:50.000Z">4/28/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>上篇文章我们迁移了整个项目到 NestJS,这篇文章我们将实现 JWT 验证。</p>
<span id="more"></span>
<h2 id="一些改动"><a class="markdownIt-Anchor" href="#一些改动"></a> 一些改动</h2>
<p>为了项目的可维护性,我对项目的目录结构进行了一些调整。</p>
<p>首先是把所有注册登录相关的代码都从 <code>users</code> 文件夹放到 <code>auth</code> 文件夹中,只给 <code>users</code> 文件夹留下用户信息的相关代码。</p>
<p><code>UsersModule</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></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_">User</span>, <span class="title class_">UserSchema</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersController</span> } <span class="keyword">from</span> <span class="string">'./users.controller'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'./users.service'</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><br><span class="line">    <span class="title class_">MongooseModule</span>.<span class="title function_">forFeature</span>([</span><br><span class="line">      {</span><br><span class="line">        <span class="attr">name</span>: <span class="title class_">User</span>.<span class="property">name</span>,</span><br><span class="line">        <span class="attr">schema</span>: <span class="title class_">UserSchema</span>,</span><br><span class="line">      },</span><br><span class="line">    ]),</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">controllers</span>: [<span class="title class_">UsersController</span>],</span><br><span class="line">  <span class="attr">providers</span>: [<span class="title class_">UsersService</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_">UsersModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<p><code>UsersService</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_">User</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</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_">UsersService</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="meta">@InjectModel</span>(User.name)</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="attr">usersModel</span>: <span class="title class_">Model</span>&lt;<span class="title class_">User</span>&gt;,</span></span><br><span class="line"><span class="params">  </span>) {}</span><br><span class="line">		</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">findByEmail</span>(<span class="attr">emailAddress</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">User</span> | <span class="literal">null</span>&gt; {</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({ emailAddress }).<span class="title function_">exec</span>()</span><br><span class="line">  }</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">findByUsername</span>(<span class="attr">username</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">User</span> | <span class="literal">null</span>&gt; {</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({ username }).<span class="title function_">exec</span>()</span><br><span class="line">  }</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">findAll</span>(): <span class="title class_">Promise</span>&lt;<span class="title class_">User</span>[]&gt; {</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">find</span>().<span class="title function_">exec</span>()</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>UsersController</code> 新添了一个 <code>GET</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></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="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'./users.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Public</span> } <span class="keyword">from</span> <span class="string">'../common/decorator/public.decorator'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersController</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">usersService</span>: <span class="title class_">UsersService</span></span>) {}</span><br><span class="line">		</span><br><span class="line">  <span class="meta">@Public</span>()</span><br><span class="line">  <span class="meta">@Get</span>(<span class="string">':username'</span>)</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">findOne</span>(<span class="params"><span class="meta">@Param</span>(<span class="string">'username'</span>) <span class="attr">username</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">usersService</span>.<span class="title function_">findByUsername</span>(username)</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>假设要查询的用户的用户名是 <code>dummy</code>,那么请求的 URL 就是 <code>/api/users/dummy</code></p>
</blockquote>
<p>客户端的文件改动则是将大部分文件从 <code>components</code> 目录中拿了出来,例如 Redux 相关的文件被统一放到了 <code>redux</code> 目录、和 <code>components</code> 同级。</p>
<p>同时我还删除了上一篇文章中写的 <code>Guard</code> 组件,因为在这篇文章中我们会要写 <a href="#%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%88%A4%E6%96%AD%E7%94%A8%E6%88%B7%E6%98%AF%E5%90%A6%E7%99%BB%E5%BD%95">功能更为复杂的升级版路由守卫</a></p>
<h2 id="jwt生成"><a class="markdownIt-Anchor" href="#jwt生成"></a> JWT 生成</h2>
<p>JWT 是一种用于在网络上传输信息的简洁方法。对比 Session 和 Cookie,JWT 的优势在于不需要在服务端存储用户信息,而是通过加密的方式将用户信息存储在 Token 中,然后在客户端存储这个 Token。</p>
<p>有关 JWT 的更多信息,可以自行查阅资料。</p>
<p>JWT 验证的实现很大程度上参照了稀土掘金上的一篇文章:<a target="_blank" rel="noopener" href="https://juejin.cn/post/7236593818594312229">NestJS 登录功能:基于 JWT 的身份验证</a></p>
<p><code>.env</code> 文件中配置:</p>
<figure class="highlight plaintext"><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">JWT_SECRET=secret</span><br><span class="line">JWT_TOKEN_AUDIENCE=localhost:4000</span><br><span class="line">JWT_TOKEN_ISSUER=localhost:4000</span><br><span class="line">JWT_ACCESS_TOKEN_TTL=3600</span><br></pre></td></tr></tbody></table></figure>
<p><code>AppModule</code> 中配置 <code>ConfigModule</code>,这里需要用到 <code>Joi</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">import</span> { <span class="title class_">ConfigModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Joi</span> <span class="keyword">from</span> <span class="string">'joi'</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><br><span class="line">    <span class="title class_">ConfigModule</span>.<span class="title function_">forRoot</span>({</span><br><span class="line">      <span class="attr">validationSchema</span>: <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line">        <span class="attr">JWT_SECRET</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line">        <span class="attr">JWT_TOKEN_AUDIENCE</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line">        <span class="attr">JWT_TOKEN_ISSUER</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line">        <span class="attr">JWT_ACCESS_TOKEN_TTL</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">3600</span>),</span><br><span class="line">      }),</span><br><span class="line">    }),</span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>使用 <code>Joi</code> 包的目的是为了验证环境变量是否存在,以及是否符合预期的类型。</p>
<p>假设环境变量 <code>JWT_SECRET</code> 不存在,那么 <code>Joi</code> 会抛出一个错误,阻止应用程序启动。</p>
</blockquote>
<p>新建一个 <code>jwt.config.ts</code> 文件,用于配置 <code>JwtModule</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> { registerAs } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">registerAs</span>(<span class="string">'jwt'</span>, <span class="function">() =&gt;</span> {</span><br><span class="line">  <span class="keyword">return</span> {</span><br><span class="line">    <span class="attr">secret</span>: process.<span class="property">env</span>.<span class="property">JWT_SECRET</span>,</span><br><span class="line">    <span class="attr">audience</span>: process.<span class="property">env</span>.<span class="property">JWT_TOKEN_AUDIENCE</span>,</span><br><span class="line">    <span class="attr">issuer</span>: process.<span class="property">env</span>.<span class="property">JWT_TOKEN_ISSUER</span>,</span><br><span class="line">    <span class="attr">accessTokenTtl</span>: <span class="built_in">parseInt</span>(process.<span class="property">env</span>.<span class="property">JWT_ACCESS_TOKEN_TTL</span> ?? <span class="string">'3600'</span>, <span class="number">10</span>),</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>接着在 <code>AuthModule</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_">JwtModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../common/config/jwt.config'</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><br><span class="line">    <span class="title class_">ConfigModule</span>.<span class="title function_">forFeature</span>(jwtConfig),</span><br><span class="line">    <span class="title class_">JwtModule</span>.<span class="title function_">registerAsync</span>(jwtConfig.<span class="title function_">asProvider</span>()),</span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>回到我们用户登陆的逻辑。当服务端验证用户信息后,我们需要生成一个 Token,然后返回给客户端。</p>
<p>新建一个 <code>active-user-data.interface.ts</code> 文件,用于定义 Token 中的负载:</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="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">ActiveUserData</span> {</span><br><span class="line">  <span class="attr">sub</span>: <span class="built_in">number</span></span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 JWT 中,<code>sub</code>(主题,通常是 ID)和 <code>name</code> 是常见的有效载荷字段,定义这些字段有助于应用进行身份验证和授权、帮助服务器识别发送请求的用户。</p>
<p><code>AuthService</code> 中添加两个方法,用于生成 Token 和验证 Token:</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><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="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ConfigType</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">'../users/user.schema'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../common/config/jwt.config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ActiveUserData</span> } <span class="keyword">from</span> <span class="string">'./interfaces/active-user-data.interface'</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_">AuthService</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="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params">    <span class="meta">@Inject</span>(jwtConfig.KEY)</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtConfiguration</span>: <span class="title class_">ConfigType</span>&lt;<span class="keyword">typeof</span> jwtConfig&gt;,</span></span><br><span class="line"><span class="params">  </span>) {}</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">generateTokens</span>(<span class="params"><span class="attr">user</span>: <span class="title class_">User</span></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">signToken</span>&lt;<span class="title class_">Partial</span>&lt;<span class="title class_">ActiveUserData</span>&gt;&gt;(user.<span class="property">_id</span>, {<span class="attr">name</span>: user.<span class="property">username</span>})</span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> signToken&lt;T&gt;(<span class="attr">userId</span>: <span class="built_in">number</span>, <span class="attr">payload</span>?: T) {</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">jwtService</span>.<span class="title function_">signAsync</span>(</span><br><span class="line">      {</span><br><span class="line">        <span class="attr">sub</span>: userId,</span><br><span class="line">        ...payload,</span><br><span class="line">      },</span><br><span class="line">      {</span><br><span class="line">        <span class="attr">secret</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">secret</span>,</span><br><span class="line">        <span class="attr">audience</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">audience</span>,</span><br><span class="line">        <span class="attr">issuer</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">issuer</span>,</span><br><span class="line">        <span class="attr">expiresIn</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">accessTokenTtl</span>,</span><br><span class="line">      },</span><br><span class="line">    )</span><br><span class="line">  }</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>signToken</code> 方法接收一个用户 ID 和一个负载对象,然后使用 <code>JwtService</code><code>signAsync</code> 方法生成一个 Token。这里使用的 <code>jwtConfig</code> 是我们之前配置的 JWT 配置。</p>
<p><code>generateTokens</code> 方法接收一个用户对象,然后调用 <code>signToken</code> 方法生成一个 Token。</p>
</blockquote>
<p><code>login</code> 方法中调用 <code>generateTokens</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"><span class="attr">loginUserDto</span>: <span class="title class_">LoginUserDto</span></span>) {</span><br><span class="line">  <span class="keyword">const</span> user = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({</span><br><span class="line">      <span class="attr">username</span>: loginUserDto.<span class="property">username</span>,</span><br><span class="line">  })</span><br><span class="line">  <span class="keyword">if</span> (!user) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>(<span class="string">'The username is invalid'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> passwordValid = <span class="keyword">await</span> bcrypt.<span class="title function_">compare</span>(loginUserDto.<span class="property">password</span>, user.<span class="property">password</span>)</span><br><span class="line">  <span class="keyword">if</span> (!passwordValid) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>(<span class="string">'The password is invalid'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 返回Token</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">generateTokens</span>(user)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>AuthController</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><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Post</span>(<span class="string">'login'</span>)</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"><span class="meta">@Body</span>() <span class="attr">loginUserDto</span>: <span class="title class_">LoginUserDto</span>, <span class="meta">@Res</span>() <span class="attr">res</span>: <span class="title class_">Response</span></span>) {</span><br><span class="line">  <span class="keyword">try</span> {</span><br><span class="line">    <span class="keyword">const</span> resultToken = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">authService</span>.<span class="title function_">login</span>(loginUserDto)</span><br><span class="line"></span><br><span class="line">    res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">OK</span>).<span class="title function_">json</span>({</span><br><span class="line">      <span class="attr">status</span>: <span class="string">'00000'</span>,</span><br><span class="line">      <span class="attr">message</span>: <span class="string">'User logged in successfully'</span>,</span><br><span class="line">      <span class="attr">token</span>: resultToken,  <span class="comment">// &lt;-- 向客户端返回Token</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 class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">UnauthorizedException</span>) {</span><br><span class="line">      res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">UNAUTHORIZED</span>).<span class="title function_">json</span>({</span><br><span class="line">        <span class="attr">status</span>: <span class="string">'U0202'</span>,</span><br><span class="line">        <span class="attr">message</span>: <span class="string">'The username or password is invalid'</span>,</span><br><span class="line">      })</span><br><span class="line">    } <span class="keyword">else</span> {</span><br><span class="line">      res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>).<span class="title function_">json</span>({</span><br><span class="line">        <span class="attr">status</span>: <span class="string">'U0200'</span>,</span><br><span class="line">        <span class="attr">message</span>: <span class="string">'Failed to log in user due to an unknown error'</span>,</span><br><span class="line">      })</span><br><span class="line">    }</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>客户端接收到 Token 后,可以将 Token 存储在 <code>localStorage</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">loginUser</span> = (<span class="params"><span class="attr">userData</span>: <span class="title class_">User</span>, <span class="attr">navigate</span>: (path: <span class="built_in">string</span>) =&gt; <span class="built_in">void</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> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">login</span>(userData);</span><br><span class="line">      <span class="keyword">const</span> data = response.<span class="property">data</span>;</span><br><span class="line">      <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">"00000"</span>) {</span><br><span class="line">        <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">"jwtToken"</span>, data.<span class="property">token</span>);</span><br><span class="line">        <span class="title function_">dispatch</span>(<span class="title function_">setCurrentUser</span>({</span><br><span class="line">          ...userData,</span><br><span class="line">          <span class="attr">access_token</span>: data.<span class="property">token</span>,</span><br><span class="line">        }));</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">log</span>(data);</span><br><span class="line">          <span class="title function_">navigate</span>(<span class="string">"/"</span>);</span><br><span class="line">      } <span class="keyword">else</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>);</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>
<h2 id="jwt验证"><a class="markdownIt-Anchor" href="#jwt验证"></a> JWT 验证</h2>
<p>生成 Token 后,我们需要在每次请求时验证 Token。</p>
<p>服务端新建一个 <code>access-token.guard.ts</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">CanActivate</span>, <span class="title class_">ExecutionContext</span>, <span class="title class_">Inject</span>, <span class="title class_">Injectable</span>, <span class="title class_">UnauthorizedException</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_">ConfigType</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Reflector</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">REQUEST_USER_KEY</span> } <span class="keyword">from</span> <span class="string">'../../common'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../../common/config/jwt.config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">IS_PUBLIC_KEY</span> } <span class="keyword">from</span> <span class="string">'../../common/decorator/public.decorator'</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_">AccessTokenGuard</span> <span class="keyword">implements</span> <span class="title class_">CanActivate</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="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">reflector</span>: <span class="title class_">Reflector</span>,</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params">    <span class="meta">@Inject</span>(jwtConfig.KEY)</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtConfiguration</span>: <span class="title class_">ConfigType</span>&lt;<span class="keyword">typeof</span> jwtConfig&gt;,</span></span><br><span class="line"><span class="params">  </span>) {}</span><br><span class="line">		</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">canActivate</span>(<span class="attr">context</span>: <span class="title class_">ExecutionContext</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">boolean</span>&gt; {</span><br><span class="line">    <span class="keyword">const</span> isPublic = <span class="variable language_">this</span>.<span class="property">reflector</span>.<span class="title function_">get</span>(<span class="variable constant_">IS_PUBLIC_KEY</span>, context.<span class="title function_">getHandler</span>())</span><br><span class="line">    <span class="keyword">if</span> (isPublic) <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">		  </span><br><span class="line">    <span class="keyword">const</span> request = context.<span class="title function_">switchToHttp</span>().<span class="title function_">getRequest</span>()</span><br><span class="line">    <span class="keyword">const</span> token = <span class="variable language_">this</span>.<span class="title function_">extractTokenFromHeader</span>(request)</span><br><span class="line">    <span class="keyword">if</span> (!token) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> {</span><br><span class="line">      request[<span class="variable constant_">REQUEST_USER_KEY</span>] = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">jwtService</span>.<span class="title function_">verifyAsync</span>(token, <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>)</span><br><span class="line">    } <span class="keyword">catch</span> (error) {</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>()</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">  }</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">extractTokenFromHeader</span>(<span class="attr">request</span>: <span class="title class_">Request</span>): <span class="built_in">string</span> | <span class="literal">undefined</span> {</span><br><span class="line">    <span class="keyword">const</span> authorization = request.<span class="property">headers</span>[<span class="string">'authorization'</span>]</span><br><span class="line">    <span class="keyword">const</span> [, token] = authorization?.<span class="title function_">split</span>(<span class="string">' '</span>) ?? []</span><br><span class="line">    <span class="keyword">return</span> token</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>AccessTokenGuard</code> 类实现了 <code>CanActivate</code> 接口,该接口用于验证请求是否可以通过。</p>
<p><code>extractTokenFromHeader</code> 方法用于从请求头中提取 Token。</p>
</blockquote>
<p>上述代码中导入的两个新文件分别是 <code>common/index.ts</code><code>common/decorator/public.decorator.ts</code></p>
<figure class="highlight typescript"><figcaption><span>common/index.ts</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="variable constant_">REQUEST_USER_KEY</span> = <span class="string">'user'</span></span><br></pre></td></tr></tbody></table></figure>
<p>这个常量在 <code>request</code> 对象中用作键、用于存储已验证的 JWT 的解码信息。具体来说,当一个请求到达并通过 <code>canActivate</code> 方法时,会从请求的 <code>Authorization</code> 头中提取 JWT。如果 JWT 存在且有效(也就是能够被 <code>JwtService</code><code>verifyAsync</code> 方法验证),那么 JWT 的解码信息就会被存储在 <code>request[REQUEST_USER_KEY]</code> 中。</p>
<p>这样做的目的是为了在后续的请求处理中,可以直接通过 <code>request[REQUEST_USER_KEY]</code> 来获取 JWT 的解码信息,无需再次验证。</p>
<figure class="highlight typescript"><figcaption><span>common/decorator/public.decorator.ts</span></figcaption><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="keyword">import</span> { <span class="title class_">SetMetadata</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="variable constant_">IS_PUBLIC_KEY</span> = <span class="string">'isPublic'</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">Public</span> = (<span class="params"></span>) =&gt; <span class="title class_">SetMetadata</span>(<span class="variable constant_">IS_PUBLIC_KEY</span>, <span class="literal">true</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>首先我们要清楚一个概念。在我们启用了 JWT 验证的情况下,会导致所有的请求都需要携带 JWT Token。但是有些请求我们并不想要求用户携带 Token,比如说注册和登录请求(因为用户还没有 Token)。这时我们就需要一个装饰器来标记这些请求是公开的。</p>
<p><code>Public</code> 装饰器标记了一个请求是公开的,这样在 <code>AccessTokenGuard</code> 中就可以根据这个标记来判断是否需要验证 Token。</p>
<p>有了 <code>Public</code> 装饰器后我们就可以在 <code>AuthController</code> 中标记 <code>register</code><code>login</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">import</span> { <span class="title class_">Public</span> } <span class="keyword">from</span> <span class="string">'../common/decorator/public.decorator'</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="meta">@Public</span>()</span><br><span class="line"><span class="meta">@Post</span>(<span class="string">'register'</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@Public</span>()</span><br><span class="line"><span class="meta">@Post</span>(<span class="string">'login'</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>NestJS 的项目每当新建一个东西时,都需要在 <code>app.module.ts</code> 中引入。这里我们导入 <code>AccessTokenGuard</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="variable constant_">APP_GUARD</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AccessTokenGuard</span> } <span class="keyword">from</span> <span class="string">'./guards/access-token.guard'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="attr">providers</span>: [</span><br><span class="line">    {</span><br><span class="line">      <span class="attr">provide</span>: <span class="variable constant_">APP_GUARD</span>,</span><br><span class="line">      <span class="attr">useClass</span>: <span class="title class_">AccessTokenGuard</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><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>APP_GUARD</code> 用于告诉 NestJS 我们要使用 <code>AccessTokenGuard</code> 这个守卫。</p>
</blockquote>
<p>有了这个守卫后,每次请求都会被验证 Token。如果请求中没有 Token 或者 Token 无效,那么请求就会被拒绝。</p>
<h2 id="客户端判断用户是否登录"><a class="markdownIt-Anchor" href="#客户端判断用户是否登录"></a> 客户端判断用户是否登录</h2>
<p>未登录的用户是不能访问某些页面的,同理,已登录的用户也不能访问登录和注册页面。</p>
<p>我们先自定义一个 Hook 来检查用户是否已经进行了身份验证。新建一个 <code>useAuth</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useEffect, useState } <span class="keyword">from</span> <span class="string">'react'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useAuth</span> = (<span class="params"></span>) =&gt; {</span><br><span class="line">  <span class="keyword">const</span> [isAuthenticated, setIsAuthenticated] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="keyword">const</span> [isAuthChecked, setIsAuthChecked] = <span class="title function_">useState</span>(<span class="literal">false</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">if</span> (token) {</span><br><span class="line">      <span class="title function_">setIsAuthenticated</span>(<span class="literal">true</span>);</span><br><span class="line">    } <span class="keyword">else</span> {</span><br><span class="line">      <span class="title function_">setIsAuthenticated</span>(<span class="literal">false</span>);</span><br><span class="line">    }</span><br><span class="line">    <span class="title function_">setIsAuthChecked</span>(<span class="literal">true</span>);</span><br><span class="line">  }, []);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> { isAuthenticated, isAuthChecked };</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这个 Hook 会检查 <code>localStorage</code> 中是否存在 <code>jwtToken</code>,如果存在则认为用户已经登录、设置 <code>isAuthenticated</code><code>true</code>,否则设置为 <code>false</code></p>
<p>为了不重复进行这个检查,我们还设置了一个 <code>isAuthChecked</code> 状态,用于标记用户是否已经进行了身份验证。</p>
<p>在我们当前的项目中,身份验证无非有两种必需导向用户到不同页面的情况:</p>
<ol>
<li>用户未登录,但是访问了首页,这时我们需要将用户导向登录页面。</li>
<li>用户已经登录,但是访问了登录或注册页面,这时我们需要将用户导向首页。</li>
</ol>
<p>我们先解决第一个问题。新建一个 <code>ProtectedRoute</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">ProtectedRoute</span>: <span class="title class_">React</span>.<span class="property">FC</span>&lt;<span class="title class_">RouteProps</span>&gt; = <span class="function">(<span class="params">{ children }</span>) =&gt;</span> {</span><br><span class="line">  <span class="keyword">const</span> { isAuthenticated, isAuthChecked } = <span class="title function_">useAuth</span>()</span><br><span class="line">  <span class="keyword">const</span> navigate = <span class="title function_">useNavigate</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> (!isAuthenticated &amp;&amp; isAuthChecked) {</span><br><span class="line">      <span class="keyword">if</span> (<span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">pathname</span> === <span class="string">'/channels/@me'</span>) {</span><br><span class="line">        <span class="title function_">navigate</span>(<span class="string">'/login'</span>, { <span class="attr">replace</span>: <span class="literal">true</span> })</span><br><span class="line">      }</span><br><span class="line">    }</span><br><span class="line">  }, [isAuthenticated, isAuthChecked, navigate])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!isAuthChecked) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;&gt;</span>{isAuthenticated ? children : <span class="tag">&lt;<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/login"</span> <span class="attr">replace</span> /&gt;</span>}<span class="tag">&lt;/&gt;</span></span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>App.tsx</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">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</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">BrowserRouter</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Routes</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">Navigate</span> <span class="attr">to</span>=<span class="string">"/channels/@me"</span> <span class="attr">replace</span> /&gt;</span> } /&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/channels/@me/*"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">ProtectedRoute</span>&gt;</span><span class="tag">&lt;<span class="name">Home</span> /&gt;</span><span class="tag">&lt;/<span class="name">ProtectedRoute</span>&gt;</span> } /&gt;  {/* &lt;--- 这里 */}</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">Routes</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">BrowserRouter</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  );</span></span><br><span class="line"><span class="language-xml">}</span></span><br></pre></td></tr></tbody></table></figure>
<p>当用户尝试访问 <code>/channels/@me</code> 页面或其子页面时,如果用户未登录,那么就会被导向登录页面。</p>
<p>第二个问题同理,只要写一个逻辑相反的 <code>UnauthenticatedRoute</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UnauthenticatedRoute</span>: <span class="title class_">React</span>.<span class="property">FC</span>&lt;<span class="title class_">RouteProps</span>&gt; = <span class="function">(<span class="params">{ children }</span>) =&gt;</span> {</span><br><span class="line">  <span class="keyword">const</span> { isAuthenticated, isAuthChecked } = <span class="title function_">useAuth</span>()</span><br><span class="line">  <span class="keyword">const</span> navigate = <span class="title function_">useNavigate</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> (isAuthenticated &amp;&amp; isAuthChecked) {</span><br><span class="line">      <span class="title function_">navigate</span>(<span class="string">'/channels/@me'</span>, { <span class="attr">replace</span>: <span class="literal">true</span> })</span><br><span class="line">    }</span><br><span class="line">  }, [isAuthenticated, isAuthChecked, navigate])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!isAuthChecked) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;&gt;</span>{!isAuthenticated ? children : <span class="tag">&lt;<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/channels/@me"</span> <span class="attr">replace</span> /&gt;</span>}<span class="tag">&lt;/&gt;</span></span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<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></pre></td><td class="code"><pre><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">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">BrowserRouter</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Routes</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">Route</span> <span class="attr">path</span>=<span class="string">"/login"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">UnauthenticatedRoute</span>&gt;</span><span class="tag">&lt;<span class="name">Login</span> /&gt;</span><span class="tag">&lt;/<span class="name">UnauthenticatedRoute</span>&gt;</span> } /&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/register"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">UnauthenticatedRoute</span>&gt;</span><span class="tag">&lt;<span class="name">Register</span> /&gt;</span><span class="tag">&lt;/<span class="name">UnauthenticatedRoute</span>&gt;</span> } /&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Routes</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">BrowserRouter</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h2 id="eslint和prettier配置"><a class="markdownIt-Anchor" href="#eslint和prettier配置"></a> ESLint 和 Prettier 配置</h2>
<p>我开发这个项目一直用的是 WebStorm。最近换设备后,一进入客户端的目录后都会弹出错误,说是 ESLint 配置冲突,就决定重新为两个目录配置 ESLint 和 Prettier。</p>
<p>服务端因为新建项目时就自带了 <code>.eslintrc.js</code><code>.prettierrc</code> 文件,所以只需要在客户端新建这两个文件即可。</p>
<p>客户端下安装必要的包:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev eslint-plugin-prettier eslint-config-prettier</span><br></pre></td></tr></tbody></table></figure>
<p>新建 <code>.eslintrc.js</code> 文件:</p>
<figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = {</span><br><span class="line">  <span class="attr">parser</span>: <span class="string">'@typescript-eslint/parser'</span>,</span><br><span class="line">  <span class="attr">parserOptions</span>: {</span><br><span class="line">    <span class="attr">project</span>: <span class="string">'tsconfig.json'</span>,  <span class="comment">// &lt;--- 这个文件是WebStorm新建项目时自动生成的</span></span><br><span class="line">    <span class="attr">tsconfigRootDir</span>: __dirname,</span><br><span class="line">    <span class="attr">sourceType</span>: <span class="string">'module'</span>,</span><br><span class="line">  },</span><br><span class="line">  <span class="attr">plugins</span>: [<span class="string">'@typescript-eslint/eslint-plugin'</span>, <span class="string">'react'</span>],</span><br><span class="line">  <span class="attr">extends</span>: [</span><br><span class="line">    <span class="string">'plugin:@typescript-eslint/recommended'</span>,</span><br><span class="line">    <span class="string">'plugin:react/recommended'</span>,</span><br><span class="line">    <span class="string">'plugin:prettier/recommended'</span>,</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">root</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">env</span>: {</span><br><span class="line">    <span class="attr">browser</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">jest</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">es6</span>: <span class="literal">true</span>,</span><br><span class="line">  },</span><br><span class="line">  <span class="attr">rules</span>: {</span><br><span class="line">    <span class="string">'@typescript-eslint/interface-name-prefix'</span>: <span class="string">'off'</span>,</span><br><span class="line">    <span class="string">'@typescript-eslint/explicit-function-return-type'</span>: <span class="string">'off'</span>,</span><br><span class="line">    <span class="string">'@typescript-eslint/explicit-module-boundary-types'</span>: <span class="string">'off'</span>,</span><br><span class="line">    <span class="string">'@typescript-eslint/no-explicit-any'</span>: <span class="string">'off'</span>,</span><br><span class="line">    <span class="string">'react/prop-types'</span>: <span class="string">'off'</span>,</span><br><span class="line">  },</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p><code>.prettierrc</code> 文件我直接搬的服务端的配置,如下:</p>
<figure class="highlight json"><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"singleQuote"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"trailingComma"</span><span class="punctuation">:</span> <span class="string">"all"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"endOfLine"</span><span class="punctuation">:</span> <span class="string">"lf"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"arrowParens"</span><span class="punctuation">:</span> <span class="string">"always"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"bracketSameLine"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"bracketSpacing"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"embeddedLanguageFormatting"</span><span class="punctuation">:</span> <span class="string">"auto"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"htmlWhitespaceSensitivity"</span><span class="punctuation">:</span> <span class="string">"css"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"insertPragma"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"jsxSingleQuote"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"printWidth"</span><span class="punctuation">:</span> <span class="number">120</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"proseWrap"</span><span class="punctuation">:</span> <span class="string">"never"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"quoteProps"</span><span class="punctuation">:</span> <span class="string">"as-needed"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"requirePragma"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"semi"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"tabWidth"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"useTabs"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"vueIndentScriptAndStyle"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"singleAttributePerLine"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
<p>重启 WebStorm 后,错误提示消失,一切正常。</p>
<h2 id="其他"><a class="markdownIt-Anchor" href="#其他"></a> 其他</h2>
<p>我长期没有更新的原因非常简单,我于三月底的时候遇到了一个不知道是什么 BUG 的 BUG。无论怎么修改代码,客户端都无法连接到服务端,而服务端也没有报告详细的错误信息。那时候直接摆烂了,也没有再继续开发。</p>
<p>最近重新看了一下代码,发现问题竟然是服务端的 <code>logger.middleware.ts</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_">Injectable</span>, <span class="title class_">NestMiddleware</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="title class_">Response</span>, <span class="title class_">NextFunction</span> } <span class="keyword">from</span> <span class="string">'express'</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_">LoggerMiddleware</span> <span class="keyword">implements</span> <span class="title class_">NestMiddleware</span> {</span><br><span class="line">  <span class="title function_">use</span>(<span class="params"><span class="attr">req</span>: <span class="title class_">Request</span>, <span class="attr">res</span>: <span class="title class_">Response</span>, <span class="attr">next</span>: <span class="title class_">NextFunction</span></span>) {  <span class="comment">// &lt;--- res参数被我删掉了</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Request...'</span>, req.<span class="property">method</span>, req.<span class="property">originalUrl</span>)</span><br><span class="line">    <span class="title function_">next</span>()</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>那么为什么这个异常没有被服务端捕获呢?因为我的全局异常过滤器也没有写好!</p>
<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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ExceptionFilter</span>, <span class="title class_">Catch</span>, <span class="title class_">ArgumentsHost</span>, <span class="title class_">HttpException</span>, <span class="title class_">HttpStatus</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Catch</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AnyExceptionFilter</span> <span class="keyword">implements</span> <span class="title class_">ExceptionFilter</span> {</span><br><span class="line">  <span class="keyword">catch</span>(<span class="attr">exception</span>: <span class="built_in">any</span>, <span class="attr">host</span>: <span class="title class_">ArgumentsHost</span>) {</span><br><span class="line">    <span class="keyword">const</span> ctx = host.<span class="title function_">switchToHttp</span>()</span><br><span class="line">    <span class="keyword">const</span> response = ctx.<span class="title function_">getResponse</span>()</span><br><span class="line">    <span class="keyword">const</span> request = ctx.<span class="title function_">getRequest</span>()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> status = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getStatus</span>() : <span class="title class_">HttpStatus</span>.<span class="property">INTERNAL_SERVER_ERROR</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> message = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getResponse</span>() : <span class="string">'Internal server error'</span></span><br><span class="line"></span><br><span class="line">    response.<span class="title function_">status</span>(status).<span class="title function_">json</span>({</span><br><span class="line">      <span class="attr">statusCode</span>: status,</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 class="attr">path</span>: request.<span class="property">url</span>,</span><br><span class="line">      <span class="attr">message</span>: message,</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>status</code><code>message</code> 都被我写死了!如果异常不是 <code>HttpException</code>,那么这两个值就都只会是 <code>500</code><code>Internal server error</code></p>
<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><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_">ExceptionFilter</span>, <span class="title class_">Catch</span>, <span class="title class_">ArgumentsHost</span>, <span class="title class_">HttpException</span>, <span class="title class_">HttpStatus</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="title class_">Response</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Catch</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AnyExceptionFilter</span> <span class="keyword">implements</span> <span class="title class_">ExceptionFilter</span> {</span><br><span class="line">  <span class="keyword">catch</span>(<span class="attr">exception</span>: <span class="built_in">any</span>, <span class="attr">host</span>: <span class="title class_">ArgumentsHost</span>) {</span><br><span class="line">    <span class="keyword">const</span> ctx = host.<span class="title function_">switchToHttp</span>()</span><br><span class="line">    <span class="keyword">const</span> response = ctx.<span class="property">getResponse</span>&lt;<span class="title class_">Response</span>&gt;()</span><br><span class="line">    <span class="keyword">const</span> request = ctx.<span class="property">getRequest</span>&lt;<span class="title class_">Request</span>&gt;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> status = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getStatus</span>() : <span class="title class_">HttpStatus</span>.<span class="property">INTERNAL_SERVER_ERROR</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> message = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getResponse</span>() : exception</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(exception)</span><br><span class="line"></span><br><span class="line">    response.<span class="title function_">status</span>(status).<span class="title function_">json</span>({</span><br><span class="line">      <span class="attr">statusCode</span>: status,</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 class="attr">path</span>: request.<span class="property">url</span>,</span><br><span class="line">      <span class="attr">message</span>: message,</span><br><span class="line">    })</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>现在,无论异常是什么,都会被正确地捕获并返回给客户端。</p>
<p>希望大家以我为戒,不要犯我这样的错误。</p>
<h4 id="新添加但没有完成的东西"><a class="markdownIt-Anchor" href="#新添加但没有完成的东西"></a> 新添加但没有完成的东西</h4>
<p>服务端新增了 Socket 模块,毕竟完成了注册登录、JWT 生成验证等功能后,下一个就是老生常态的消息接收发送。既然我们使用的是 Socket.IO,那就要新建一个 Socket 模块。</p>
<p><code>SocketModule</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></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><br><span class="line"><span class="meta">@Module</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><code>SocketService</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_">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_">Server</span> } <span class="keyword">from</span> <span class="string">'socket.io'</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_">SocketService</span> {</span><br><span class="line">  <span class="keyword">private</span> <span class="attr">server</span>: <span class="title class_">Server</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">initialize</span>(<span class="params"><span class="attr">server</span>: <span class="title class_">Server</span></span>) {</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">server</span> = server</span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  <span class="title function_">sendMessage</span>(<span class="params"><span class="attr">event</span>: <span class="built_in">string</span>, <span class="attr">message</span>: <span class="built_in">any</span></span>) {</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">server</span>.<span class="title function_">emit</span>(event, message)</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>initialize</code> 方法用于初始化 Socket 服务器。</p>
<p><code>sendMessage</code> 方法用于向所有连接的客户端发送消息。</p>
</blockquote>
<p>这里讲一下 Controller 和 Gateway 的区别。</p>
<p>在传统的 HTTP 请求 / 响应模型中,Controller 负责处理来自客户端的请求并返回响应。然而,我们在使用 WebSocket 时,不再有请求和响应的概念,而是有事件和消息的概念。这时,Gateway(网关)就是用来处理这些事件和消息的。</p>
<p><code>SocketGateway</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><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">WebSocketGateway</span>, <span class="title class_">WebSocketServer</span>, <span class="title class_">SubscribeMessage</span>, <span class="title class_">MessageBody</span>, <span class="title class_">OnGatewayInit</span> } <span class="keyword">from</span> <span class="string">'@nestjs/websockets'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Server</span>, <span class="title class_">Socket</span> } <span class="keyword">from</span> <span class="string">'socket.io'</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><br><span class="line"><span class="meta">@WebSocketGateway</span>(<span class="number">3001</span>, {</span><br><span class="line">  <span class="attr">allowEIO3</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">cors</span>: {</span><br><span class="line">    <span class="attr">origin</span>: process.<span class="property">env</span>.<span class="property">CLIENT_ORIGIN</span> || <span class="string">'http://localhost:3000'</span>,</span><br><span class="line">    <span class="attr">credentials</span>: <span class="literal">true</span>,</span><br><span class="line">  },</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="meta">@WebSocketServer</span>()</span><br><span class="line">  <span class="attr">server</span>: <span class="title class_">Server</span></span><br><span class="line"></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">socketService</span>: <span class="title class_">SocketService</span></span>) {}</span><br><span class="line"></span><br><span class="line">  <span class="title function_">afterInit</span>(<span class="params"><span class="attr">server</span>: <span class="title class_">Server</span></span>) {</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">socketService</span>.<span class="title function_">initialize</span>(server)</span><br><span class="line">  }</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="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><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>SocketGateway</code> 中,我们使用 <code>this.socketService.initialize(server)</code> 初始化了 Socket 服务器,然后使用 <code>@SubscribeMessage</code> 装饰器来监听客户端发送的消息。</p>
<p>我们可以在客户端发送消息时调用 <code>socket.emit('privateMessageSent', message)</code>。来看一下我的输入框 <code>PrivateMessageTextBox</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, { useState, <span class="title class_">KeyboardEvent</span>, <span class="title class_">ChangeEvent</span>, <span class="title class_">FormEvent</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_">Icon</span> } <span class="keyword">from</span> <span class="string">"@iconify/react"</span>;</span><br><span class="line"><span class="keyword">import</span> socket <span class="keyword">from</span> <span class="string">"../../redux/actions/messageActions"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'../../redux/actions/serverConnection'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">PrivateMessageTextBoxProps</span> {</span><br><span class="line">  <span class="attr">receiverUsername</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><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><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleKeyDown</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">KeyboardEvent</span></span>) =&gt; {</span><br><span class="line">    <span class="keyword">if</span> (e.<span class="property">key</span> === <span class="string">"Enter"</span> &amp;&amp; !e.<span class="property">shiftKey</span>) {</span><br><span class="line">      e.<span class="title function_">preventDefault</span>();</span><br><span class="line">      <span class="title function_">handleSendMessage</span>();</span><br><span class="line">    }</span><br><span class="line">  }</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> 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_">UsersService</span>.<span class="title function_">getUserByUsername</span>(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">    socket.<span class="title function_">emit</span>(<span class="string">"privateMessageSent"</span>, {</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><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><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">className</span>=<span class="string">"px-2 m-3"</span> <span class="attr">onSubmit</span>=<span class="string">{</span> <span class="attr">handleSendMessage</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">"w-100 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 class="attr">marginBottom:</span> '<span class="attr">24px</span>', <span class="attr">backgroundColor:</span> '<span class="attr">rgba</span>(<span class="attr">56</span>, <span class="attr">58</span>, <span class="attr">64</span>)', <span class="attr">textIndent:</span> '<span class="attr">0</span>', <span class="attr">borderRadius:</span> '<span class="attr">8px</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></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">className</span>=<span class="string">"overflow-x-hidden overflow-y-scroll"</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">borderRadius:</span> '<span class="attr">8px</span>', <span class="attr">backfaceVisibility:</span> '<span class="attr">hidden</span>', <span class="attr">scrollbarWidth:</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">          <span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">"d-flex position-relative"</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">className</span>=<span class="string">"position-sticky"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">0</span> <span class="attr">0</span> <span class="attr">auto</span>', <span class="attr">alignSelf:</span> '<span class="attr">stretch</span>' }}&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">Icon</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">icon</span>=<span class="string">"bi:plus-circle-fill"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">className</span>=<span class="string">"position-sticky w-auto 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">height:</span> '<span class="attr">44px</span>', <span class="attr">padding:</span> '<span class="attr">10px</span> <span class="attr">16px</span>', <span class="attr">top:</span> '<span class="attr">0</span>', <span class="attr">marginLeft:</span> '<span class="attr">-16px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  <span class="attr">background:</span> '<span class="attr">transparent</span>', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">181</span>, <span class="attr">186</span>, <span class="attr">193</span>)', <span class="attr">border:</span> '<span class="attr">0</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">              /&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></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">"p-0 fs-6 w-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></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">background:</span> '<span class="attr">transparent</span>', <span class="attr">resize:</span> '<span class="attr">none</span>', <span class="attr">border:</span> '<span class="attr">none</span>', <span class="attr">appearance:</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">400</span>', <span class="attr">lineHeight:</span> '<span class="attr">1.375rem</span>', <span class="attr">height:</span> '<span class="attr">44px</span>', <span class="attr">minHeight:</span> '<span class="attr">44px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">boxSizing:</span> '<span class="attr">border-box</span>', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            &gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">textarea</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">autoCapitalize</span>=<span class="string">"none"</span> <span class="attr">autoComplete</span>=<span class="string">"off"</span> <span class="attr">autoCorrect</span>=<span class="string">"off"</span> <span class="attr">autoFocus</span>=<span class="string">{</span> <span class="attr">true</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">placeholder</span>=<span class="string">"Text @dummy"</span> <span class="attr">spellCheck</span>=<span class="string">"true"</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">value</span>=<span class="string">{</span> <span class="attr">message</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">onChange</span>=<span class="string">{</span> <span class="attr">handleChange</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                <span class="attr">onKeyDown</span>=<span class="string">{</span> <span class="attr">handleKeyDown</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">border:</span> '<span class="attr">none</span>', <span class="attr">outline:</span> '<span class="attr">none</span>', <span class="attr">resize:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  <span class="attr">paddingBottom:</span> '<span class="attr">11px</span>', <span class="attr">paddingTop:</span> '<span class="attr">11px</span>', <span class="attr">paddingRight:</span> '<span class="attr">10px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  <span class="attr">left:</span> '<span class="attr">0</span>', <span class="attr">right:</span> '<span class="attr">10px</span>', <span class="attr">background:</span> '<span class="attr">transparent</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  <span class="attr">caretColor:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)'</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                }}</span></span></span><br><span class="line"><span class="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></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">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</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_">PrivateMessageTextBox</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>这个组件是一个输入框,用户输入消息后按下回车键就会发送消息。发送的消息会被 <code>SocketGateway</code> 监听到,然后打印到控制台。</p>
<p>其他的地方可以忽略掉不看,只需要重点看 <code>handleSendMessage</code> 方法。这个方法会向服务器发送一个 <code>privateMessageSent</code> 事件,事件的数据是一个对象,包含了消息的 ID、发送者 ID、接收者 ID 和消息内容:</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">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> 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_">UsersService</span>.<span class="title function_">getUserByUsername</span>(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">  socket.<span class="title function_">emit</span>(<span class="string">"privateMessageSent"</span>, {</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><br><span class="line">  <span class="title function_">setMessage</span>(<span class="string">""</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>这里提一嘴 <code>userId</code> 的来处。</p>
<p>在服务端的 <code>AuthController</code><code>login</code> 方法中,我们向客户端发送了一个 Token,但我们也可以发送其他东西,比如用户的 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> user = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">findByUsername</span>(loginUserDto.<span class="property">username</span>)</span><br><span class="line"></span><br><span class="line">res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">OK</span>).<span class="title function_">json</span>({</span><br><span class="line">  <span class="attr">status</span>: <span class="string">'00000'</span>,</span><br><span class="line">  <span class="attr">message</span>: <span class="string">'User logged in successfully'</span>,</span><br><span class="line">  <span class="attr">token</span>: resultToken,</span><br><span class="line">  <span class="attr">userId</span>: user.<span class="property">_id</span>,</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">loginUser</span> = (<span class="params"><span class="attr">userData</span>: <span class="title class_">User</span>, <span class="attr">navigate</span>: (path: <span class="built_in">string</span>) =&gt; <span class="built_in">void</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> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">login</span>(userData)</span><br><span class="line">      <span class="keyword">const</span> data = response.<span class="property">data</span></span><br><span class="line">      <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">'00000'</span>) {</span><br><span class="line">        <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">'jwtToken'</span>, data.<span class="property">token</span>)</span><br><span class="line">        <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">'userId'</span>, data.<span class="property">userId</span>)  <span class="comment">// &lt;--- 这里</span></span><br><span class="line">        <span class="title function_">dispatch</span>(</span><br><span class="line">          <span class="title function_">setCurrentUser</span>({</span><br><span class="line">            ...userData,</span><br><span class="line">            <span class="attr">access_token</span>: data.<span class="property">token</span>,</span><br><span class="line">            <span class="attr">id</span>: data.<span class="property">userId</span>,  <span class="comment">// &lt;--- 也把ID存储到Redux里,说不定未来会用到呢</span></span><br><span class="line">          }),</span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(data)</span><br><span class="line">        <span class="title function_">navigate</span>(<span class="string">'/'</span>)</span><br><span class="line">      } <span class="keyword">else</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>)</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>
</blockquote>
<p>消息从客户端发送到服务端后,服务端会打印出消息的内容。</p>
<p>一个简单的消息发送就这样完成了。</p>
<p>接下来还需要完成消息的接收、存储和展示,这个就留到下一篇文章再说了。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="b5ac.html">上一篇</a><a class="next" href="a039.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/40b4.html" data-full-url="https://cytrogen.icu/posts/40b4.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>