~cytrogen/blog-public

ref: 88eebf3dfdd8ab819fa1a84e1976a8a75d5af2b6 blog-public/posts/3b97.html -rw-r--r-- 130.4 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
<!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 项目实践【1】:从 Express 迁移 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS + Socket.io 全栈项目实践的第一篇,详细记录了如何将原有的 Express.js 后端迁移至 TypeScript 驱动的 NestJS 框架。前端部分,文章重点展示了将 React 项目升级为 TypeScript 的关键改动,包括为 Redux store、slice 和 action 添加强类型,并使用 Axios 处理用户认证请求。后端部分,则深入介绍了 NestJS 的核心概念(控制器、服务、模块),并一步步重构了用户注册等业务逻辑,内容涵盖了使用 DTO 进行数据验证和 Mongoose 定义数据模型。本文为希望从 Express 过渡到 NestJS 的开发者提供了一份清晰的迁移指南。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/3b97.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/3b97.html">永久链接</a><div class="p-summary visually-hidden"><p>二月份的时候我写了一篇<a href="/posts/bb3e">React + Express + Socket.io 之间的实时通信【2】:注册登录</a>,那时候我还在用 Express 作为后端框架。</p>
<p>因为中途想到使用 TypeScript,所以我决定迁移到 NestJS。</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 项目实践【1】:从 Express 迁移</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-03-30T04:06:13.000Z">3/30/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:55.001Z"></time></div><div class="post-content e-content"><html><head></head><body><p>二月份的时候我写了一篇 <a href="/posts/bb3e">React + Express + Socket.io 之间的实时通信【2】:注册登录</a>,那时候我还在用 Express 作为后端框架。</p>
<p>因为中途想到使用 TypeScript,所以我决定迁移到 NestJS。</p>
<span id="more"></span>
<h1 id="前端"><a class="markdownIt-Anchor" href="#前端"></a> 前端</h1>
<p>先说一下我对前端的改动。</p>
<p>因为是想要整个项目都用 TypeScript,所以我把 <code>src</code> 目录下的所有 <code>.js</code> 文件都改成了 <code>.tsx</code></p>
<p>很多文件都不需要改动,例如 <code>App.js</code></p>
<figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">BrowserRouter</span>, <span class="title class_">Routes</span>, <span class="title class_">Route</span>, <span class="title class_">Navigate</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"./App.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"bootstrap/dist/css/bootstrap.min.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">"./components/Home"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Login</span> <span class="keyword">from</span> <span class="string">"./components/Login"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Register</span> <span class="keyword">from</span> <span class="string">"./components/Register"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageHomepage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_homepage/Private_Message_Homepage"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageChatpage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_chatpage/Private_Message_Chatpage"</span>;</span><br><span class="line"><span class="keyword">import</span> socket <span class="keyword">from</span> <span class="string">"./components/utils/actions/authActions"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line">    <span class="keyword">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">Home</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">""</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">PrivateMessageHomepage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /&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">"dummy"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">PrivateMessageChatpage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /&gt;</span> }/&gt;</span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;/<span class="name">Route</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">Login</span> <span class="attr">socket</span>=<span class="string">{</span> <span class="attr">socket</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">Register</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">BrowserRouter</span>, <span class="title class_">Routes</span>, <span class="title class_">Route</span>, <span class="title class_">Navigate</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"bootstrap/dist/css/bootstrap.min.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"./App.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">"./components/Home"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Login</span> <span class="keyword">from</span> <span class="string">"./components/Login"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Register</span> <span class="keyword">from</span> <span class="string">"./components/Register"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Guard</span> <span class="keyword">from</span> <span class="string">"./components/utils/guard"</span>;</span><br><span class="line"><span class="comment">// import PrivateMessageHomepage from "./components/private_message_homepage/Private_Message_Homepage";</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageChatPage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_chat_page/Private_Message_Chat_Page"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line">  <span class="keyword">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">Guard</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">""</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">Home</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">"dummy"</span> <span class="attr">element</span>=<span class="string">{</span> &lt;<span class="attr">PrivateMessageChatPage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /&gt;</span> }/&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Route</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">Login</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">Register</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>对比一下会发现其实没有变化,<code>PrivateMessageHomepage</code> 改成了 <code>Guard</code> 只是因为业务逻辑的改动,跟 TypeScript 无关。</p>
<h2 id="redux"><a class="markdownIt-Anchor" href="#redux"></a> Redux</h2>
<p>涉及到 Redux 的文件多多少少都有一些改动。</p>
<p><code>store.js</code></p>
<figure class="highlight js"><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> { configureStore } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"><span class="keyword">import</span> authSlice <span class="keyword">from</span> <span class="string">"./reducers/authSlice"</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_">configureStore</span>({</span><br><span class="line">  <span class="attr">reducer</span>: {</span><br><span class="line">    <span class="attr">auth</span>: authSlice</span><br><span class="line">  }</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p><code>store.ts</code></p>
<figure class="highlight ts"><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> { configureStore } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"><span class="keyword">import</span> { useDispatch } <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="keyword">import</span> authSlice <span class="keyword">from</span> <span class="string">"./reducers/authSlice"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = <span class="title function_">configureStore</span>({</span><br><span class="line">  <span class="attr">reducer</span>: {</span><br><span class="line">    <span class="attr">auth</span>: authSlice,</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">type</span> <span class="title class_">RootState</span> = <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> store.<span class="property">getState</span>&gt;;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">AppDispatch</span> = <span class="keyword">typeof</span> store.<span class="property">dispatch</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useAppDispatch</span> = (<span class="params"></span>) =&gt; useDispatch&lt;<span class="title class_">AppDispatch</span>&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></tbody></table></figure>
<p>原先的 <code>store.js</code> 是直接导出了 <code>configureStore</code> 的返回值;<code>store.ts</code> 先是导出了 <code>RootState</code><code>AppDispatch</code> 这两个类型,然后道出了 <code>useAppDispatch</code> 这个自定义 Hook、以替代 <code>useDispatch</code></p>
<p><code>authSlice.js</code></p>
<figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { createSlice } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> authSlice = <span class="title function_">createSlice</span>({</span><br><span class="line">    <span class="attr">name</span>: <span class="string">"auth"</span>,</span><br><span class="line">    <span class="attr">initialState</span>: {</span><br><span class="line">        <span class="attr">isAuthenticated</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="attr">user</span>: {},</span><br><span class="line">        <span class="attr">error</span>: <span class="literal">null</span></span><br><span class="line">    },</span><br><span class="line">    <span class="attr">reducers</span>: {</span><br><span class="line">        <span class="attr">setCurrentUser</span>: <span class="function">(<span class="params">state, action</span>) =&gt;</span> {</span><br><span class="line">            state.<span class="property">isAuthenticated</span> = <span class="literal">true</span>;</span><br><span class="line">            state.<span class="property">user</span> = action.<span class="property">payload</span>;</span><br><span class="line">        },</span><br><span class="line">        <span class="attr">setError</span>: <span class="function">(<span class="params">state, action</span>) =&gt;</span> {</span><br><span class="line">            state.<span class="property">error</span> = action.<span class="property">payload</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="keyword">export</span> <span class="keyword">const</span> { setCurrentUser, setError } = authSlice.<span class="property">actions</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> authSlice.<span class="property">reducer</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>authSlice.ts</code></p>
<figure class="highlight ts"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { createSlice, <span class="title class_">PayloadAction</span> } <span class="keyword">from</span> <span class="string">'@reduxjs/toolkit'</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">'../interfaces'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">AuthState</span> {</span><br><span class="line">  <span class="attr">isAuthenticated</span>: <span class="built_in">boolean</span>;</span><br><span class="line">  <span class="attr">user</span>: <span class="title class_">User</span> | <span class="literal">null</span>;</span><br><span class="line">  <span class="attr">error</span>: <span class="built_in">string</span> | <span class="literal">null</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">initialState</span>: <span class="title class_">AuthState</span> = {</span><br><span class="line">  <span class="attr">isAuthenticated</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">user</span>: <span class="literal">null</span>,</span><br><span class="line">  <span class="attr">error</span>: <span class="literal">null</span>,</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> authSlice = <span class="title function_">createSlice</span>({</span><br><span class="line">  <span class="attr">name</span>: <span class="string">'auth'</span>,</span><br><span class="line">  initialState,</span><br><span class="line">  <span class="attr">reducers</span>: {</span><br><span class="line">    <span class="attr">setCurrentUser</span>: <span class="function">(<span class="params">state, <span class="attr">action</span>: <span class="title class_">PayloadAction</span>&lt;<span class="title class_">User</span>&gt;</span>) =&gt;</span> {</span><br><span class="line">      state.<span class="property">isAuthenticated</span> = <span class="literal">true</span>;</span><br><span class="line">      state.<span class="property">user</span> = action.<span class="property">payload</span>;</span><br><span class="line">    },</span><br><span class="line">    <span class="attr">setError</span>: <span class="function">(<span class="params">state, <span class="attr">action</span>: <span class="title class_">PayloadAction</span>&lt;<span class="built_in">string</span>&gt;</span>) =&gt;</span> {</span><br><span class="line">      state.<span class="property">error</span> = action.<span class="property">payload</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="keyword">export</span> <span class="keyword">const</span> { setCurrentUser, setError } = authSlice.<span class="property">actions</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> authSlice.<span class="property">reducer</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>authSlice.js</code><code>authSlice.ts</code> 的区别在于 <code>action</code> 的类型声明。TypeScript 是 JavaScript 的超集,目的是为了更好地进行静态类型检查,以避免各种各样的错误。要知道 JavaScript 是弱类型语言,这意味着你可以在不同的地方使用不同的类型而不报错。</p>
<p><code>interface</code> 关键字用于定义一个接口,接口是一种抽象的结构、定义了一个对象应该具有的属性和方法。<code>AuthState</code> 接口被定义后,有三个属性:<code>isAuthenticated</code><code>user</code><code>error</code>。然后设置 <code>initialState</code><code>AuthState</code> 类型。</p>
<p>也就是说 <code>initialState</code> 无论怎么改动,都必须符合 <code>AuthState</code> 的结构。假设我在 <code>initialState</code><code>isAuthenticated</code> 属性后面加了一个 <code>isRegistered</code> 属性,那么在 <code>reducers</code> 中的 <code>state</code> 就会报错,因为 <code>isRegistered</code> 属性并不在 <code>AuthState</code> 中。</p>
<p><code>setCurrentUser</code><code>setError</code><code>action</code> 参数都是 <code>PayloadAction</code> 类型,<code>PayloadAction</code> 是一个泛型接口,接受一个类型参数,这个类型参数就是 <code>action.payload</code> 的类型。这样一来,<code>action.payload</code> 的类型就被限制了,不会出现不符合预期的情况。</p>
<p>比方说 <code>setError</code><code>action</code> 参数就被限制为 <code>string</code> 类型。</p>
<p>我还新建了一个 <code>interfaces.ts</code> 文件来存放会被多个文件引用的接口:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">User</span> {</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">number</span>;</span><br><span class="line">  <span class="attr">emailAddress</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">username</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">password</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">access_token</span>?: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>属性后面的 <code>?</code> 表示这个属性是可选的。想象一下,<code>User</code> 接口会被注册、登录以及登出的组件引用:</p>
<ul>
<li>注册时,用户传来的数据中不会有 <code>id</code><code>access_token</code> 这两个属性</li>
<li>登录时,用户传来的数据中不会有 <code>emailAddress</code> 这个属性</li>
<li>登出时,用户传来的数据中不会有 <code>password</code> 这个属性</li>
</ul>
<p>所以这些属性都是可选的。</p>
<h2 id="axios"><a class="markdownIt-Anchor" href="#axios"></a> Axios</h2>
<p>我本来是全程使用 Socket.io 来进行通信,这也包括了登录、注册等操作。但是 Socket.io 并不适合用来做这些操作,所以我还是用了 Axios。</p>
<p>比方说注册用户,使用 Socket.io 的话就是这样:</p>
<figure class="highlight js"><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">const</span> <span class="title function_">registerUser</span> = (<span class="params">userData, navigate</span>) =&gt; {</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="params">dispatch</span> =&gt;</span> {</span><br><span class="line">    socket.<span class="title function_">emit</span>(<span class="string">"register"</span>, userData);</span><br><span class="line">    </span><br><span class="line">    socket.<span class="title function_">on</span>(<span class="string">"newRegisteredUser"</span>, <span class="function"><span class="params">data</span> =&gt;</span> {</span><br><span class="line">      data.<span class="property">status</span> === <span class="string">"00000"</span> ? <span class="title function_">navigate</span>(<span class="string">"/login"</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><br><span class="line">  }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p>先发送 <code>register</code> 事件,然后接收从服务端发来的 <code>newRegisteredUser</code> 事件,根据 <code>data.status</code> 的值来决定跳转到登录页面还是打印错误信息。</p>
<p>换成 Axios 的话,要先创建一个服务:</p>
<figure class="highlight ts"><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> axios <span class="keyword">from</span> <span class="string">'axios'</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">"../interfaces"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> api = axios.<span class="title function_">create</span>({</span><br><span class="line">  <span class="attr">baseURL</span>: <span class="string">'http://localhost:4000/api'</span>,</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UsersService</span> = {</span><br><span class="line">  <span class="attr">register</span>: <span class="function">(<span class="params"><span class="attr">data</span>: <span class="title class_">User</span></span>) =&gt;</span> {</span><br><span class="line">    <span class="keyword">return</span> api.<span class="title function_">post</span>(<span class="string">'/users/register'</span>, data);</span><br><span class="line">  },</span><br><span class="line"></span><br><span class="line">  <span class="attr">login</span>: <span class="function">(<span class="params"><span class="attr">data</span>: <span class="title class_">User</span></span>) =&gt;</span> {</span><br><span class="line">    <span class="keyword">return</span> api.<span class="title function_">post</span>(<span class="string">'/users/login'</span>, data);</span><br><span class="line">  },</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>使用 <code>baseURL</code> 的好处是如果后端地址改变、只需要改动一次就行了。</p>
<p><code>UsersService</code> 对象中有两个方法:<code>register</code><code>login</code>,分别用于注册和登录。当我们需要向后端发送请求时,只需要调用这些方法即可:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">registerUser</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_">register</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 class="title function_">navigate</span>(<span class="string">"/login"</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>
<p>我还添加了一个 <code>try...catch</code> 语句,用于捕获请求失败的情况。</p>
<h2 id="登录和注册"><a class="markdownIt-Anchor" href="#登录和注册"></a> 登录和注册</h2>
<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Register</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =&gt;</span> {</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>如果该函数组件还有 props,那么就需要定义 props 的类型:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">PrivateMessageHomepageProps</span> {</span><br><span class="line">    <span class="attr">style</span>: <span class="title class_">React</span>.<span class="property">CSSProperties</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_">PrivateMessageHomepage</span>: <span class="title class_">React</span>.<span class="property">FC</span>&lt;<span class="title class_">PrivateMessageHomepageProps</span>&gt; = <span class="function">(<span class="params">{ style }</span>) =&gt;</span> {</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>例如这里的 <code>PrivateMessageHomepage</code> 组件有一个 <code>style</code> 属性,所以需要定义其类型为 <code>React.CSSProperties</code>,毕竟是一个 CSS 样式对象嘛。</p>
</blockquote>
<p>之前讲到共用的 <code>User</code> 接口,但对于注册来说还需要 4 个必需的属性:<code>emailAddress</code><code>birthYear</code><code>birthMonth</code><code>birthDay</code>。其中 <code>emailAddress</code> 虽在 <code>User</code> 接口中,但是是可选的,不符合注册的要求。</p>
<p>所以我们可以使用 <code>&amp;</code> 运算符来合并两个接口:</p>
<figure class="highlight ts"><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_">User</span> } <span class="keyword">from</span> <span class="string">"./utils/interfaces"</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">RegisterUser</span> = <span class="title class_">User</span> &amp; {</span><br><span class="line">  <span class="attr">emailAddress</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">birthYear</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">birthMonth</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">birthDay</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>RegisterUser</code> 接口继承了 <code>User</code> 接口,并添加了 4 个必需的属性。后加的属性会覆盖前面的属性,所以 <code>User</code> 接口中的 <code>emailAddress</code> 属性被覆盖了。</p>
<p>之后再使用 <code>useState</code> 来定义 <code>userData</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [userData, setUserData] = useState&lt;<span class="title class_">RegisterUser</span>&gt;({</span><br><span class="line">    <span class="attr">username</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">emailAddress</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">password</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">birthYear</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">birthMonth</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">birthDay</span>: <span class="string">""</span></span><br><span class="line">  });</span><br></pre></td></tr></tbody></table></figure>
<p>登录时我们不需要 <code>User</code> 接口中其他的属性,只需要 <code>username</code><code>password</code> 属性。所以我们可以挑选出需要的属性:</p>
<figure class="highlight tsx"><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">type</span> <span class="title class_">LoginUser</span> = <span class="title class_">Pick</span>&lt;<span class="title class_">User</span>, <span class="string">"username"</span> | <span class="string">"password"</span>&gt;;</span><br></pre></td></tr></tbody></table></figure>
<p><code>Pick</code> 的用处是从一个对象中挑选出一些属性,返回一个新的对象。这意味着 <code>LoginUser</code> 接口只包含 <code>User</code> 接口中的 <code>username</code><code>password</code> 属性。</p>
<blockquote>
<p>像是需要展现出用户名的地方我们也可以这样写:</p>
<figure class="highlight tsx"><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">type</span> <span class="title class_">UserProfile</span> = <span class="title class_">Pick</span>&lt;<span class="title class_">User</span>, <span class="string">"username"</span>&gt;;</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<h2 id="页面跳转"><a class="markdownIt-Anchor" href="#页面跳转"></a> 页面跳转</h2>
<p>最前面提及到的 <code>Guard</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">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Navigate</span>, <span class="title class_">Outlet</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Guard</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =&gt;</span> {</span><br><span class="line">  <span class="keyword">const</span> auth = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">"auth"</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (auth) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Outlet</span>/&gt;</span></span>;</span><br><span class="line">  <span class="keyword">else</span> <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/login"</span> /&gt;</span></span>;</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_">Guard</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>如果 <code>localStorage</code> 中有 <code>auth</code> 这个键,那么就渲染 <code>Outlet</code> 组件,否则就跳转到登录页面。</p>
<p><code>Outlet</code> 组件是用来渲染子路由的。回到 <code>App.tsx</code>,能看到 <code>/channels/@me</code> 路由被 <code>Guard</code> 组件保护,子路由分别是默认的 <code>Home</code> 组件和 <code>dummy</code> 子路由。</p>
<p>也就是说用户在登陆后跳转到 <code>/channels/@me</code> 路由,会被 <code>Guard</code> 组件验证,然后渲染 <code>Home</code> 组件。</p>
<p><code>/channels/@me/dummy</code> 路由是用来测试私聊的,但是它也在 <code>Guard</code> 组件的保护之下。</p>
<h2 id="其他"><a class="markdownIt-Anchor" href="#其他"></a> 其他</h2>
<p>我的项目中有一些按钮在被鼠标悬停时会有一些样式变化,原先的逻辑是这样的:</p>
<figure class="highlight jsx"><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">const</span> [hoverStates, setHoverStates] = <span class="title function_">useState</span>({});</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">updateHoverState</span> = (<span class="params">item, isHovered</span>) =&gt; {</span><br><span class="line">    <span class="title function_">setHoverStates</span>(<span class="function"><span class="params">prev</span> =&gt;</span> ({ ...prev, [item]: isHovered }));</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>转换到 TypeScript 后:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [hoverStates, setHoverStates] = useState&lt;<span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">boolean</span>&gt;&gt;({});</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">updateHoverState</span> = (<span class="params"><span class="attr">item</span>: <span class="built_in">string</span>, <span class="attr">isHovered</span>: <span class="built_in">boolean</span></span>) =&gt; {</span><br><span class="line">    <span class="title function_">setHoverStates</span>({</span><br><span class="line">      ...hoverStates,</span><br><span class="line">      [item]: isHovered</span><br><span class="line">    });</span><br><span class="line">  }</span><br></pre></td></tr></tbody></table></figure>
<p><code>Record</code> 的第一个参数定义了键的类型,第二个参数定义了值的类型。<code>hoverStates</code> 被定义为一个字典,键和值再被定义为 <code>string</code><code>boolean</code> 类型。</p>
<p>输入框组件里分别有着:</p>
<ul>
<li>判断用户是否按下了回车键的 <code>handleKeyDown</code> 函数</li>
<li>处理用户输入信息的 <code>handleChange</code> 函数</li>
<li>处理用户提交表单的 <code>handleSendMessage</code> 函数</li>
</ul>
<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></pre></td><td class="code"><pre><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><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><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleSendMessage</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">FormEvent</span></span>) =&gt; {}</span><br></pre></td></tr></tbody></table></figure>
<h1 id="后端"><a class="markdownIt-Anchor" href="#后端"></a> 后端</h1>
<p>NestJS 是一个基于 Node.JS 的后端框架,它使用 TypeScript 编写,提供了一些装饰器来简化开发。</p>
<p>我目前有的依赖:</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"@nestjs/common"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/core"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/jwt"</span><span class="punctuation">:</span> <span class="string">"^10.2.0"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/mapped-types"</span><span class="punctuation">:</span> <span class="string">"*"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/mongoose"</span><span class="punctuation">:</span> <span class="string">"^10.0.4"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/platform-express"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/platform-socket.io"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/typeorm"</span><span class="punctuation">:</span> <span class="string">"^10.0.2"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"@nestjs/websockets"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"bcrypt"</span><span class="punctuation">:</span> <span class="string">"^5.1.1"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"class-validator"</span><span class="punctuation">:</span> <span class="string">"^0.14.1"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"cookie-parser"</span><span class="punctuation">:</span> <span class="string">"^1.4.6"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"dotenv"</span><span class="punctuation">:</span> <span class="string">"^16.4.5"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"mongoose"</span><span class="punctuation">:</span> <span class="string">"^8.2.1"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"morgan"</span><span class="punctuation">:</span> <span class="string">"^1.10.0"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"reflect-metadata"</span><span class="punctuation">:</span> <span class="string">"^0.2.1"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"rxjs"</span><span class="punctuation">:</span> <span class="string">"^7.8.1"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"typeorm"</span><span class="punctuation">:</span> <span class="string">"^0.3.20"</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
<p>NestJS 的入口文件是 <code>main.ts</code></p>
<figure class="highlight ts"><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">import</span> { <span class="title class_">NestFactory</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_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> dotenv <span class="keyword">from</span> <span class="string">'dotenv'</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">dotenv.<span class="title function_">config</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line">  <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="title function_">create</span>(<span class="title class_">AppModule</span>)</span><br><span class="line">  app.<span class="title function_">enableCors</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">  app.<span class="title function_">setGlobalPrefix</span>(<span class="string">'api'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> server = app.<span class="title function_">getHttpServer</span>()</span><br><span class="line">  <span class="keyword">new</span> <span class="title class_">Server</span>(server)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> app.<span class="title function_">listen</span>(process.<span class="property">env</span>.<span class="property">PORT</span> || <span class="number">4000</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="title function_">bootstrap</span>().<span class="title function_">catch</span>(<span class="function">(<span class="params">err</span>) =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">error</span>(err))</span><br></pre></td></tr></tbody></table></figure>
<p><code>dotenv</code> 是用来读取 <code>.env</code> 文件的,<code>.env</code> 文件用来存放环境变量。<code>CLIENT_ORIGIN</code> 是前端的地址,<code>PORT</code> 是后端的端口。</p>
<p>因为前后端分离的项目中,前端和后端是不同的域名,所以会有跨域问题。<code>app.enableCors</code> 方法用来解决跨域问题,<code>origin</code> 参数是前端的地址,<code>credentials</code> 参数是 <code>true</code> 表示允许携带 cookie。</p>
<p><code>app.setGlobalPrefix</code> 方法用来设置全局前缀,所有的路由都会加上这个前缀。比方说后面设置的 <code>/users/register</code> 路由会变成 <code>/api/users/register</code></p>
<p><code>app.getHttpServer</code> 方法返回一个 <code>http.Server</code> 实例,<code>new Server(server)</code> 用来创建一个 Socket.io 服务器。</p>
<p>最后调用 <code>app.listen</code> 方法来启动服务器。</p>
<h2 id="nestjs概念"><a class="markdownIt-Anchor" href="#nestjs概念"></a> NestJS 概念</h2>
<p>NestJS 目前支持两个 HTTP 平台:Express 和 Fastify。这是因为 NestJS 的开发团队认为 NestJS 立志于成为一个模块化的框架,不单单是一个 HTTP 框架。只要创建了适配器,NestJS 就可以在任何平台上运行。</p>
<p>NestJS 的核心概念有:</p>
<ul>
<li>控制器(Controller)</li>
<li>服务(Service)</li>
<li>模块(Module)</li>
</ul>
<h4 id="控制器"><a class="markdownIt-Anchor" href="#控制器"></a> 控制器</h4>
<p>控制器是处理传入请求的地方,它们会调用服务来完成请求。控制器的方法可以使用装饰器来定义路由。</p>
<figure class="highlight ts"><figcaption><span>app.controller.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><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">import</span> { <span class="title class_">Controller</span>, <span class="title class_">Get</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_">AppService</span> } <span class="keyword">from</span> <span class="string">'./app.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppController</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">appService</span>: <span class="title class_">AppService</span></span>) {}</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Get</span>()</span><br><span class="line">  <span class="title function_">getHello</span>(): <span class="built_in">string</span> {</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">appService</span>.<span class="title function_">getHello</span>()</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Controller</code> 装饰器用来定义一个控制器。假设我们的后端地址是 <code>http://localhost:4000</code>,那么 <code>@Controller()</code> 装饰器的参数就是 <code>http://localhost:4000</code></p>
<p><code>@Get()</code> 装饰器用来定义一个 GET 请求,这个请求的路径就是控制器的路径,也就是请求 <code>http://localhost:4000</code></p>
<p>假设我想要请求 <code>http://localhost:4000/api/users/register</code>,那么就要写成:</p>
<figure class="highlight ts"><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@Get</span>(<span class="string">'register'</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>为什么不写成 <code>@Controller('api/users')</code> 呢?因为全局前缀已经被我们设置为 <code>api</code> 了。</p>
<h4 id="服务"><a class="markdownIt-Anchor" href="#服务"></a> 服务</h4>
<p>刚才的控制器中有一个 <code>AppService</code> 服务,服务是处理业务逻辑的地方。服务可以被控制器调用,也可以被其他服务调用。</p>
<figure class="highlight ts"><figcaption><span>app.service.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><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_">Injectable</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">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppService</span> {</span><br><span class="line">  <span class="title function_">getHello</span>(): <span class="built_in">string</span> {</span><br><span class="line">    <span class="keyword">return</span> <span class="string">'Welcome to HotaruTS!!'</span></span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Injectable</code> 装饰器用来定义一个服务。服务中的方法可以被其他服务调用,也可以被控制器调用。</p>
<p>刚才调用的是 <code>getHello</code> 方法,返回的是一个字符串。如果使用 Postman 请求 <code>http://localhost:4000</code>,响应的内容就是 <code>Welcome to HotaruTS!!</code></p>
<h4 id="模块"><a class="markdownIt-Anchor" href="#模块"></a> 模块</h4>
<p>模块是一个用来组织应用程序的地方,每个应用程序至少有一个根模块。模块中可以包含控制器、服务、提供器等。</p>
<p>根模块可以看成是 Express 里的 <code>app</code> 对象,它是所有模块的入口。</p>
<p>先来看一下我 Express 项目中的 <code>app.js</code></p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> createError = <span class="built_in">require</span>(<span class="string">'http-errors'</span>);</span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">'express'</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> cookieParser = <span class="built_in">require</span>(<span class="string">'cookie-parser'</span>);</span><br><span class="line"><span class="keyword">const</span> logger = <span class="built_in">require</span>(<span class="string">'morgan'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> indexRouter = <span class="built_in">require</span>(<span class="string">'./routes/index'</span>);</span><br><span class="line"><span class="keyword">const</span> usersRouter = <span class="built_in">require</span>(<span class="string">'./routes/users'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">express</span>();</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">set</span>(<span class="string">'views'</span>, path.<span class="title function_">join</span>(__dirname, <span class="string">'views'</span>));</span><br><span class="line">app.<span class="title function_">set</span>(<span class="string">'view engine'</span>, <span class="string">'pug'</span>);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">logger</span>(<span class="string">'dev'</span>));</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">json</span>());</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">urlencoded</span>({ <span class="attr">extended</span>: <span class="literal">false</span> }));</span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">cookieParser</span>());</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">static</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">'public'</span>)));</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="string">'/'</span>, indexRouter);</span><br><span class="line">app.<span class="title function_">use</span>(<span class="string">'/users.js'</span>, usersRouter);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="keyword">function</span>(<span class="params">req, res, next</span>) {</span><br><span class="line">    <span class="title function_">next</span>(<span class="title function_">createError</span>(<span class="number">404</span>));</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="keyword">function</span>(<span class="params">err, req, res, next</span>) {</span><br><span class="line">    res.<span class="property">locals</span>.<span class="property">message</span> = err.<span class="property">message</span>;</span><br><span class="line">    res.<span class="property">locals</span>.<span class="property">error</span> = req.<span class="property">app</span>.<span class="title function_">get</span>(<span class="string">'env'</span>) === <span class="string">'development'</span> ? err : {};</span><br><span class="line"></span><br><span class="line">    res.<span class="title function_">status</span>(err.<span class="property">status</span> || <span class="number">500</span>);</span><br><span class="line">    res.<span class="title function_">render</span>(<span class="string">'error'</span>);</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = app;</span><br></pre></td></tr></tbody></table></figure>
<ol>
<li>导入依赖</li>
<li>创建路由器 <code>indexRouter</code><code>usersRouter</code></li>
<li>创建 <code>app</code> 对象,也就是 Express 的实例</li>
<li>设置视图引擎和视图路径</li>
<li>使用中间件,分别是:
<ol>
<li><code>logger</code>:记录请求日志,<code>dev</code> 参数表示开发环境</li>
<li><code>express.json</code>:解析 JSON 格式的请求体</li>
<li><code>express.urlencoded</code>:解析 URL 编码的请求体,<code>extended</code> 参数表示是否使用 <code>qs</code></li>
<li><code>cookieParser</code>:解析 cookie</li>
<li><code>express.static</code>:设置静态文件目录,也就是 <code>public</code> 目录</li>
</ol>
</li>
<li>配置路由,让 <code>indexRouter</code><code>usersRouter</code> 分别处理 <code>/</code><code>/users</code> 路径</li>
<li>处理 404 错误</li>
<li>处理其他错误</li>
<li>导出 <code>app</code> 对象</li>
</ol>
<p>得知了这些,我们就可以仿照 Express 的写法来写 NestJS 的模块。</p>
<p>首先导入依赖:</p>
<figure class="highlight ts"><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">import</span> { <span class="title class_">Module</span>, <span class="title class_">NestModule</span>, <span class="title class_">MiddlewareConsumer</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">APP_FILTER</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_">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="keyword">as</span> express <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> cookieParser <span class="keyword">from</span> <span class="string">'cookie-parser'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> morgan <span class="keyword">from</span> <span class="string">'morgan'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppController</span> } <span class="keyword">from</span> <span class="string">'./app.controller'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppService</span> } <span class="keyword">from</span> <span class="string">'./app.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AnyExceptionFilter</span> } <span class="keyword">from</span> <span class="string">'./any-exception.filter'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">LoggerMiddleware</span> } <span class="keyword">from</span> <span class="string">'./common/middleware/logger.middleware'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketModule</span> } <span class="keyword">from</span> <span class="string">'./socket/socket.module'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersModule</span> } <span class="keyword">from</span> <span class="string">'./users/users.module'</span></span><br></pre></td></tr></tbody></table></figure>
<p><code>@Module</code> 装饰器可以定义一个模块,参数分别是:</p>
<ul>
<li><code>imports</code>:导入其他模块</li>
<li><code>controllers</code>:控制器</li>
<li><code>providers</code>:提供器</li>
</ul>
<p>因为这个项目的数据库是 MongoDB,所以我导入了 <code>MongooseModule</code> 模块。<code>SocketModule</code><code>UsersModule</code> 是自定义的模块。</p>
<p><code>UsersModule</code> 后面会详细讲解,<code>SocketModule</code> 等未来写到消息传递时再讲。</p>
<p>控制器就不用多说了,模块本来就是用来组织控制器的。<code>AppModule</code> 中只有一个控制器 <code>AppController</code></p>
<p>提供器是一个用来提供服务的地方,服务可以被控制器调用。<code>AppService</code> 就是一个提供器。除此之外,还有一个全局异常过滤器 <code>AnyExceptionFilter</code></p>
<figure class="highlight ts"><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="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_">forRoot</span>(process.<span class="property">env</span>.<span class="property">DATABASE_URL</span> || <span class="string">'mongodb://localhost:27017/hotaru'</span>),</span><br><span class="line">    <span class="title class_">SocketModule</span>,</span><br><span class="line">    <span class="title class_">UsersModule</span>,</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">controllers</span>: [<span class="title class_">AppController</span>],</span><br><span class="line">  <span class="attr">providers</span>: [</span><br><span class="line">    <span class="title class_">AppService</span>,</span><br><span class="line">    {</span><br><span class="line">      <span class="attr">provide</span>: <span class="variable constant_">APP_FILTER</span>,</span><br><span class="line">      <span class="attr">useClass</span>: <span class="title class_">AnyExceptionFilter</span>,</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>AppModule</code> 添加了一个 <code>configure</code> 方法,这个方法是 <code>NestModule</code> 接口的一个方法,作用是添加中间件。</p>
<p><code>consumer.apply</code> 方法用来添加中间件,参数是一个或多个中间件。这里添加了 <code>morgan</code><code>express.json</code><code>express.urlencoded</code><code>cookieParser</code><code>express.static</code><code>LoggerMiddleware</code> 中间件。</p>
<p><code>forRoutes('*')</code> 表示所有路由都会使用这些中间件。</p>
<p>最终导出 <code>AppModule</code></p>
<figure class="highlight ts"><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">class</span> <span class="title class_">AppModule</span> <span class="keyword">implements</span> <span class="title class_">NestModule</span> {</span><br><span class="line">  <span class="title function_">configure</span>(<span class="params"><span class="attr">consumer</span>: <span class="title class_">MiddlewareConsumer</span></span>) {</span><br><span class="line">    consumer</span><br><span class="line">      .<span class="title function_">apply</span>(</span><br><span class="line">        <span class="title function_">morgan</span>(<span class="string">'dev'</span>),</span><br><span class="line">        express.<span class="title function_">json</span>(),</span><br><span class="line">        express.<span class="title function_">urlencoded</span>({ <span class="attr">extended</span>: <span class="literal">false</span> }),</span><br><span class="line">        <span class="title function_">cookieParser</span>(),</span><br><span class="line">        express.<span class="title function_">static</span>(<span class="string">'public'</span>),</span><br><span class="line">        <span class="title class_">LoggerMiddleware</span>,</span><br><span class="line">      )</span><br><span class="line">      .<span class="title function_">forRoutes</span>(<span class="string">'*'</span>)</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这样,我们就完成了一个 NestJS 的模块。</p>
<h2 id="日志中间件"><a class="markdownIt-Anchor" href="#日志中间件"></a> 日志中间件</h2>
<p><code>LoggerMiddleware</code> 中间件是一个自定义的中间件,用来记录请求日志:</p>
<figure class="highlight ts"><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><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>
<h2 id="用户模块"><a class="markdownIt-Anchor" href="#用户模块"></a> 用户模块</h2>
<p>用户模块是一个用来处理用户注册、登录、登出的模块。</p>
<p>在 NestJS 里创建一个模块可以使用 CLI:</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">nest g module <span class="built_in">users</span></span><br></pre></td></tr></tbody></table></figure>
<p>这个命令会在 <code>src</code> 目录下创建一个 <code>users</code> 目录,里面有一个 <code>users.module.ts</code> 文件。</p>
<p>首先我们得知道原先的 Express 项目里,用户注册的逻辑是怎么写的:</p>
<figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line">socket.<span class="title function_">on</span>(<span class="string">"register"</span>, <span class="keyword">async</span> userData =&gt; {</span><br><span class="line">    <span class="keyword">const</span> existingUserEmail = <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">findOne</span>({</span><br><span class="line">        <span class="attr">emailAddress</span>: userData.<span class="property">emailAddress</span></span><br><span class="line">    });</span><br><span class="line">    <span class="keyword">const</span> existingUsername = <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">findOne</span>({</span><br><span class="line">        <span class="attr">username</span>: userData.<span class="property">username</span></span><br><span class="line">    });</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (existingUserEmail || existingUsername) {</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[U0102] User already exists: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line">        <span class="title class_">User</span>.<span class="title function_">find</span>({}).<span class="title function_">then</span>(<span class="function">(<span class="params">docs</span>) =&gt;</span> {</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">log</span>(docs);</span><br><span class="line">        }).<span class="title function_">catch</span>(<span class="function">(<span class="params">err</span>) =&gt;</span> {</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">error</span>(err);</span><br><span class="line">        });</span><br><span class="line"></span><br><span class="line">        socket.<span class="title function_">emit</span>(<span class="string">"newRegisteredUser"</span>, {</span><br><span class="line">            <span class="attr">status</span>: <span class="string">"U0102"</span>,</span><br><span class="line">            <span class="attr">message</span>: <span class="string">"User already exists."</span></span><br><span class="line">        });</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    }</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">create</span>({</span><br><span class="line">        <span class="attr">emailAddress</span>: userData.<span class="property">emailAddress</span>,</span><br><span class="line">        <span class="attr">username</span>: userData.<span class="property">username</span>,</span><br><span class="line">        <span class="attr">password</span>: userData.<span class="property">password</span>,</span><br><span class="line">        <span class="title class_">DOBYear</span>: userData.<span class="property">birthYear</span>,</span><br><span class="line">        <span class="title class_">DOBMonth</span>: <span class="title class_">MonthToNumber</span>[userData.<span class="property">birthMonth</span>],</span><br><span class="line">        <span class="title class_">DOBDay</span>: userData.<span class="property">birthDay</span></span><br><span class="line">    })</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[00000] User registered: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line">    </span><br><span class="line">    socketIO.<span class="title function_">emit</span>(<span class="string">"newRegisteredUser"</span>, {</span><br><span class="line">        <span class="attr">status</span>: <span class="string">"00000"</span>,</span><br><span class="line">        <span class="attr">token</span>: <span class="title function_">generateJWT</span>(userData.<span class="property">username</span>)</span><br><span class="line">    });</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>因为使用的是 Socket.io,所以要监听 <code>register</code> 事件,然后验证用户填写的信息。如果用户已经存在,就返回错误信息;如果用户不存在,就创建一个新用户。</p>
<p>其中的状态码是自定义的,采用的是类似于阿里巴巴代码规约的状态码。</p>
<p>在 NestJS 中,鉴于客户端已经改为使用 Axios 这样的 HTTP 库,我们就不再使用 Socket.io 了。先创建一个路径为 <code>/users</code> 的控制器:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@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_">UsersControllers</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></pre></td></tr></tbody></table></figure>
<p><code>constructor</code> 方法中注入了一个私有且只读的 <code>usersService</code> 服务,这意味着这个服务只能在这个控制器中使用。</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Post</span>(<span class="string">'register'</span>)</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">register</span>(<span class="params"><span class="meta">@Body</span>() <span class="attr">registerUserDto</span>: <span class="title class_">RegisterUserDto</span>, <span class="meta">@Res</span>() <span class="attr">res</span>: <span class="title class_">Response</span></span>) {}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Post('register')</code> 装饰器用来定义一个 POST 请求,请求路径是 <code>/users/register</code></p>
<p><code>@Body()</code> 装饰器用来获取请求体,<code>registerUserDto</code> 是一个数据传输对象,包含了用户注册时需要的信息,也就是客户端传来的数据:</p>
<ul>
<li><code>emailAddress</code></li>
<li><code>username</code></li>
<li><code>password</code></li>
<li><code>birthYear</code></li>
<li><code>birthMonth</code></li>
<li><code>birthDay</code></li>
</ul>
<p><code>@Res()</code> 装饰器用来获取响应对象。</p>
<figure class="highlight ts"><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> existingUser = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">findByEmail</span>(registerUserDto.<span class="property">emailAddress</span>)</span><br><span class="line"><span class="keyword">if</span> (existingUser) {</span><br><span class="line">  <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">HttpException</span>(<span class="string">'Email address already in use'</span>, <span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!registerUserDto.<span class="property">emailAddress</span>) {</span><br><span class="line">  <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">HttpException</span>(<span class="string">'Email address is required'</span>, <span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">  <span class="keyword">const</span> user = <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">register</span>(registerUserDto)</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 registered successfully'</span>,</span><br><span class="line">    <span class="attr">user</span>: user,</span><br><span class="line">  })</span><br><span class="line">} <span class="keyword">catch</span> (error) {</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">'U0100'</span>,</span><br><span class="line">    <span class="attr">message</span>: <span class="string">'Failed to register user'</span>,</span><br><span class="line">  })</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>首先调用 <code>this.usersService.findByEmail</code> 方法来查找用户是否已经存在,如果存在就返回错误信息。接着判断用户填写的信息是否完整,如果不完整就返回错误信息。</p>
<p>最后调用 <code>this.usersService.register</code> 方法来注册用户,如果注册成功就返回成功信息,否则返回错误信息。</p>
<p>每次返回响应时都要设置状态码,比方说请求成功时写的 <code>HttpStatus.OK</code>,请求失败时写的 <code>HttpStatus.BAD_REQUEST</code>。尽管已经自定义了一套状态码,但是还是要遵循 HTTP 协议的状态码,谁叫我们是用 HTTP 协议的呢。</p>
<p>那么 <code>UsersService</code> 里到底是什么样的呢?</p>
<figure class="highlight ts"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</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_">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_">InjectModel</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { v4 <span class="keyword">as</span> uuidv4 } <span class="keyword">from</span> <span class="string">'uuid'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> bcrypt <span class="keyword">from</span> <span class="string">'bcrypt'</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_">RegisterUserDto</span>, <span class="title class_">LoginUserDto</span> } <span class="keyword">from</span> <span class="string">'./dto'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span>, <span class="title class_">UserDocument</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 class="keyword">private</span> <span class="attr">usersModel</span>: <span class="title class_">Model</span>&lt;<span class="title class_">UserDocument</span>&gt;,</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params">  </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_">register</span>(<span class="attr">registerUserDto</span>: <span class="title class_">RegisterUserDto</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span> | <span class="title class_">User</span>&gt; {</span><br><span class="line">    <span class="keyword">const</span> id = <span class="title function_">uuidv4</span>()</span><br><span class="line">    <span class="keyword">const</span> hashedPassword = <span class="keyword">await</span> bcrypt.<span class="title function_">hash</span>(registerUserDto.<span class="property">password</span>, <span class="number">10</span>)</span><br><span class="line">    <span class="keyword">const</span> newUser = <span class="keyword">new</span> <span class="variable language_">this</span>.<span class="title function_">usersModel</span>({</span><br><span class="line">      id,</span><br><span class="line">      ...registerUserDto,</span><br><span class="line">      <span class="attr">password</span>: hashedPassword,</span><br><span class="line">    })</span><br><span class="line">		  </span><br><span class="line">    <span class="keyword">let</span> <span class="attr">savedUser</span>: <span class="built_in">void</span> | <span class="title class_">User</span></span><br><span class="line">    <span class="keyword">try</span> {</span><br><span class="line">      savedUser = <span class="keyword">await</span> newUser.<span class="title function_">save</span>().<span class="title function_">then</span>(<span class="function">() =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'User registered successfully'</span>))</span><br><span class="line">    } <span class="keyword">catch</span> (err) {</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(err)</span><br><span class="line">    }</span><br><span class="line">    <span class="keyword">return</span> savedUser</span><br><span class="line">  }</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></pre></td></tr></tbody></table></figure>
<p><code>@InjectModel(User.name)</code> 注入了一个 Mongoose 模型,用来操作数据库。下面的 <code>jwtService</code> 是用来生成 JWT 的服务,以后再聊聊 JWT。</p>
<p><code>register</code> 方法中我们先用 <code>uuidv4</code> 方法生成一个唯一的 ID,然后用 <code>bcrypt</code> 库对密码进行加密。接着使用 <code>new</code> 关键字创建一个用户实例、传入所有创建用户时需要的信息。 最后调用 <code>save</code> 方法保存用户信息,如果保存成功就返回用户信息,否则返回 <code>void</code></p>
<p><code>findByEmail</code> 方法用来查找用户是否已经存在,如果存在就返回用户信息,否则返回 <code>null</code></p>
<blockquote>
<p>现在已经看到了很多次 <code>...Dto</code>,这是什么呢?</p>
<p>DTO 的全程为 Data Transfer Object,数据传输对象。它是一个用来传输数据的对象,通常用来传输数据给服务端或者从服务端传输数据给客户端。在 NestJS 中,DTO 是一个用来定义数据结构的类,用来规范数据的传输。</p>
<p>例如 <code>RegisterUserDto</code> 类用来定义用户注册时需要的信息:</p>
<figure class="highlight ts"><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">import</span> { <span class="title class_">IsEmail</span>, <span class="title class_">IsNotEmpty</span>, <span class="title class_">IsString</span> } <span class="keyword">from</span> <span class="string">'class-validator'</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_">RegisterUserDto</span> {</span><br><span class="line">  <span class="meta">@IsEmail</span>()</span><br><span class="line">  <span class="attr">emailAddress</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@IsString</span>()</span><br><span class="line">  <span class="meta">@MinLength</span>(<span class="number">6</span>)</span><br><span class="line">  <span class="attr">username</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@IsString</span>()</span><br><span class="line">  <span class="meta">@MinLength</span>(<span class="number">8</span>)</span><br><span class="line">  <span class="attr">password</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@IsString</span>()</span><br><span class="line">  <span class="attr">birthYear</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@IsString</span>()</span><br><span class="line">  <span class="attr">birthMonth</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@IsString</span>()</span><br><span class="line">  <span class="attr">birthDay</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这里使用了多个装饰器来定义每个属性的类型,有助于进行数据验证。</p>
</blockquote>
<p><code>UserSchema</code> 则是用来定义用户模型的,是 MongoDB 要求的数据结构:</p>
<figure class="highlight ts"><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="keyword">import</span> { <span class="title class_">Prop</span>, <span class="title class_">Schema</span>, <span class="title class_">SchemaFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Document</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">UserDocument</span> = <span class="title class_">User</span> &amp; <span class="title class_">Document</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Schema</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">User</span> {</span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span>, <span class="attr">unique</span>: <span class="literal">true</span> })</span><br><span class="line">  <span class="attr">username</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span>, <span class="attr">unique</span>: <span class="literal">true</span> })</span><br><span class="line">  <span class="attr">emailAddress</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span> })</span><br><span class="line">  <span class="attr">password</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line">  <span class="attr">birthYear</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line">  <span class="attr">birthMonth</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line">  <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line">  <span class="attr">birthDay</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">export</span> <span class="keyword">const</span> <span class="title class_">UserSchema</span> = <span class="title class_">SchemaFactory</span>.<span class="title function_">createForClass</span>(<span class="title class_">User</span>)</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Prop</code> 装饰器用来定义一个属性,<code>@Schema</code> 装饰器用来定义一个模式。<code>UserDocument</code> 是一个用户文档,继承了 <code>User</code><code>Document</code></p>
<p>这里我们定义的属性和 <code>RegisterUserDto</code> 中的属性是一样的,但 <code>UserSchema</code> 注重于定义会被存储在数据库中的数据结构,<code>RegisterUserDto</code> 注重于定义会在客户端和服务端之间传输的数据结构。</p>
<p>最终,我们在 <code>UsersModule</code> 中导入这些模块:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongooseModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">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_">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_">UsersControllers</span> } <span class="keyword">from</span> <span class="string">'./users.controllers'</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 class="title class_">MongooseModule</span>.<span class="title function_">forFeature</span>([{ <span class="attr">name</span>: <span class="title class_">User</span>.<span class="property">name</span>, <span class="attr">schema</span>: <span class="title class_">UserSchema</span> }])],</span><br><span class="line">  <span class="attr">controllers</span>: [<span class="title class_">UsersControllers</span>],</span><br><span class="line">  <span class="attr">providers</span>: [<span class="title class_">UsersService</span>, <span class="title class_">JwtService</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>
<div class="danger">
<p>不要忘了,新建的模块要在 <code>AppModule</code> 中导入:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Module</span>({ <span class="attr">imports</span>: [<span class="title class_">UsersModule</span>] })</span><br></pre></td></tr></tbody></table></figure>
</div>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="a039.html">上一篇</a><a class="next" href="2537.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/3b97.html" data-full-url="https://cytrogen.icu/posts/3b97.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>