~cytrogen/blog-public

ref: 88eebf3dfdd8ab819fa1a84e1976a8a75d5af2b6 blog-public/posts/bb3e.html -rw-r--r-- 64.2 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
<!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 + Express + Socket.io 之间的实时通信【2】:注册登录 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + Express + Socket.io 实时通信应用教程的第二篇,重点实现用户的注册与登录功能。在前端,教程介绍了如何使用 Redux Toolkit (@reduxjs/toolkit) 管理认证状态,并通过 Socket.io 向后端发送用户数据。在后端,文章演示了如何使用 Mongoose (MongoDB) 建立用户模型,监听客户端事件,并利用 bcrypt 对密码进行哈希加密与验证。本教程完整地展示了一个包含前后端交互、数据库操作和安全实践的全栈用户认证流程。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/bb3e.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/bb3e.html">永久链接</a><div class="p-summary visually-hidden"><p><s>接连着昨日的年轻莽撞,今天</s>继续研究如何去制作一个类 Slack、Discord 的网页聊天室 App。</p>
<p>其实这篇文在 1 月 23 日开始起草的,然后写代码写着写着就忘了写文。</p>
<p>再加上近期加入了一个新的项目,自己的项目不得不搁置一下。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/%E5%85%A8%E6%A0%88%E5%AE%9E%E8%B7%B5/">全栈实践</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a><a class="p-category" href="../tags/Node-js/">Node.js</a><a class="p-category" href="../tags/Express-js/">Express.js</a><a class="p-category" href="../tags/React-js/">React.js</a></div><h1 class="post-title p-name">React + Express + Socket.io 之间的实时通信【2】:注册登录</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-02-19T05:06:13.000Z">2/19/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.997Z"></time></div><div class="post-content e-content"><html><head></head><body><p><s>接连着昨日的年轻莽撞,今天</s> 继续研究如何去制作一个类 Slack、Discord 的网页聊天室 App。</p>
<p>其实这篇文在 1 月 23 日开始起草的,然后写代码写着写着就忘了写文。</p>
<p>再加上近期加入了一个新的项目,自己的项目不得不搁置一下。</p>
<span id="more"></span>
<h1 id="前端"><a class="markdownIt-Anchor" href="#前端"></a> 前端</h1>
<p>页面设计的工作我交给了 reactstrap 包,其实用 React-bootstrap 包、或者干脆直接引入 Bootstrap 的 CSS 文件都是可以的。</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install reactstrap</span><br></pre></td></tr></tbody></table></figure>
<p>仿制 Discord 的登录 &amp; 注册页面还是相当容易的:</p>
<p><img src="/posts/bb3e/login.png" alt="登录页面"></p>
<p><img src="/posts/bb3e/Register.png" alt="注册页面"></p>
<p>这里就不说写页面的具体细节,只挑几个我花了时间去搞的地方说。</p>
<h2 id="1-卡片居中"><a class="markdownIt-Anchor" href="#1-卡片居中"></a> 1. 卡片居中</h2>
<p>居中,是前端界最老生常谈的话题之一。浏览器上搜索「居中」一词,会发现十年前大家在聊怎么居中,几年后在聊怎么居中,现在还有 GitHub 网页上出现没有好好居中的标签。</p>
<p>Discord 的登录 / 注册页面的设计方案很简单,正中间一个卡片,上面嘎嘎放表单即可。</p>
<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></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Container</span> className=<span class="string">'d-flex vh-100'</span>&gt;</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Row</span> <span class="attr">className</span>=<span class="string">'m-auto align-self-center'</span>&gt;</span> ... <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Container</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<p>这里推荐一下微软近期出的强力工具:PowerToys,用快捷键 <code>Windows</code> + <code>Shift</code> + <code>C</code> 就可以在屏幕上吸色了,吸的 RGB 值正好用来给我们的标签添加颜色样式。</p>
<h2 id="2-生日日期选择"><a class="markdownIt-Anchor" href="#2-生日日期选择"></a> 2. 生日日期选择</h2>
<p><img src="/posts/bb3e/Birthday_Select.png" alt=""></p>
<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><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">&lt;<span class="title class_">Container</span>&gt;</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Row</span> <span class="attr">xs</span>=<span class="string">'3'</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'ps-0 pe-1'</span>&gt;</span><span class="tag">&lt;/<span class="name">Col</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'px-1'</span>&gt;</span><span class="tag">&lt;/<span class="name">Col</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'ps-1 pe-0'</span>&gt;</span><span class="tag">&lt;/<span class="name">Col</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Row</span>&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Container</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<p>不过重点不在这里,而在 JSX 中用 <code>.map()</code> 方法生成下拉选项。</p>
<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><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">const</span> currentYear = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getFullYear</span>();</span><br><span class="line"><span class="keyword">const</span> years = [];</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">100</span>; i++) {</span><br><span class="line">    years.<span class="title function_">push</span>(currentYear - i);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&lt;<span class="title class_">Input</span>&gt;</span><br><span class="line">    {years.<span class="title function_">map</span>(</span><br><span class="line">        <span class="function"><span class="params">year</span> =&gt;</span> (</span><br><span class="line">            <span class="language-xml"><span class="tag">&lt;<span class="name">option</span> <span class="attr">key</span>=<span class="string">{year}</span>&gt;</span>{year}<span class="tag">&lt;/<span class="name">option</span>&gt;</span></span></span><br><span class="line">        )</span><br><span class="line">    )}</span><br><span class="line">&lt;/<span class="title class_">Input</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>其实可以再简化一些,不过能看就行!</p>
</blockquote>
<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><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">const</span> months = [</span><br><span class="line">    <span class="string">'January'</span>, <span class="string">'February'</span>, <span class="string">'March'</span>, <span class="string">'April'</span>, <span class="string">'May'</span>, <span class="string">'June'</span>,</span><br><span class="line">    <span class="string">'July'</span>, <span class="string">'August'</span>, <span class="string">'September'</span>, <span class="string">'November'</span>, <span class="string">'December'</span></span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Input</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    {months.map(</span></span><br><span class="line"><span class="language-xml">        month =&gt; (</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">option</span> <span class="attr">key</span>=<span class="string">{month}</span>&gt;</span>{month}<span class="tag">&lt;/<span class="name">option</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        )</span></span><br><span class="line"><span class="language-xml">    )}</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">Input</span>&gt;</span></span></span><br></pre></td></tr></tbody></table></figure>
<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><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">&lt;<span class="title class_">Input</span>&gt;</span><br><span class="line">    {<span class="title class_">Array</span>.<span class="title function_">from</span>({ <span class="attr">length</span>: <span class="number">31</span> }, <span class="function">(<span class="params">_, i</span>) =&gt;</span> i + <span class="number">1</span>).<span class="title function_">map</span>(</span><br><span class="line">        <span class="function"><span class="params">day</span> =&gt;</span> (</span><br><span class="line">            <span class="language-xml"><span class="tag">&lt;<span class="name">option</span> <span class="attr">key</span>=<span class="string">{day}</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">117</span>,<span class="attr">122</span>,<span class="attr">129</span>)' }}&gt;</span>{day}<span class="tag">&lt;/<span class="name">option</span>&gt;</span></span></span><br><span class="line">        )</span><br><span class="line">    )}</span><br><span class="line">&lt;/<span class="title class_">Input</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>看着没做每个月内有多少天的逻辑对吧?其实 Discord 也是这样设计的。这种逻辑交给后端就好啦~</p>
</blockquote>
<p>不过我研究了会儿都没实现出来 Discord 的效果:用户还未选择时,年月份三个下拉框都默认显示「年」/「月」/「日」。</p>
<h2 id="3-数据处理"><a class="markdownIt-Anchor" href="#3-数据处理"></a> 3. 数据处理</h2>
<p>今日的重头戏。用户在注册时这些数据总要传到服务端去的吧?今天就是来解决这个的。</p>
<p>先装包:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install react-redux @reduxjs/toolkit</span><br></pre></td></tr></tbody></table></figure>
<p>Redux 的知识点可以去看我之前的一篇关于 React 的文章。Redux 的概念可以用三个东西来概括:Action、Store 和 Reducer。每当用户与某个组件交互时就会触发 Action(比方说点击按钮),接着 Action 会携带着数据去往 Store 进行存储,途中遇到 Reducer、状态被按照我们要求的进行了更改,最终回到 Store 这个大仓库手里。</p>
<p>为什么我们要使用 Redux 呢?如果我们需要在客户端向服务端发送数据,Redux 可以更好地帮我们管理这些数据,并且 Store 还是全局的,在任一组件中我们都可以访问 Store 中的数据。</p>
<p>我们可以用 @reduxjs / toolkit 包配置 Redux Store。新建一个文件 <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></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><br><span class="line"><span class="keyword">const</span> store = <span class="title function_">configureStore</span>({ <span class="attr">reducers</span>: {} });</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>不过在完成这段代码之前,我们需要初始化状态。我们的状态需要什么样的数据存储其中?作为一个可以登陆注册的网页 App,我们需要存储用户的登录状态、用户的信息、错误信息等等。这些存储的动作都需要一个 Reducer 来完成。</p>
<p>Redux 官网中在文档里使用了 <code>createSlice</code> 方法来创建 State Slice。</p>
<p>新建一个文件 <code>authSlice.js</code></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><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>store.js</code> 了:</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><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> { configureStore } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"></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>Store 和 Reducer 都有了,自然少不了 Action。</p>
<p>Socket.io 的连接逻辑我会在下一篇文章中讲解,这里我们只需要知道,当用户点击注册按钮时,我们需要将用户的信息发送到服务端。这个过程就是一个 Action。</p>
<p>目前我们只需要验证用户信息是否合法,所以只写了一个 Action:<code>authActions.js</code></p>
<p>验证用户信息需要使用到 Socket.io 来和服务端进行通信:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> socketIO <span class="keyword">from</span> <span class="string">"socket.io-client"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> socket = socketIO.<span class="title function_">connect</span>(<span class="string">"http://localhost:4000"</span>);</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>此处假设服务端的端口是 4000。</p>
</blockquote>
<h2 id="4-注册和登录"><a class="markdownIt-Anchor" href="#4-注册和登录"></a> 4. 注册和登录</h2>
<p>这两个页面的逻辑是一样的,都是用户输入信息后点击按钮,触发 Action,将用户信息发送到服务端。</p>
<p>在 Socket.io 的连接逻辑中,用户点击按钮后、信息会在客户端中被发送到服务端,服务端会对用户信息进行验证,如果验证通过,服务端会返回一个 Token 给客户端,客户端将 Token 存储到 Store 中;如果验证不通过,服务端则会返回一个错误信息给客户端。</p>
<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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useState } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"></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> { useNavigate } <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> { registerUser } <span class="keyword">from</span> <span class="string">"./utils/actions/authActions"</span>;</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>useState</code> 是 React 的一个 Hook,用于在函数组件中使用状态。</p>
<p><code>useNavigate</code> 是 React Router 的一个 Hook,用于在函数组件中进行页面跳转。</p>
</blockquote>
<p><code>registerUser</code> 是等会儿我们会定义的 Action,先不写。</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><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">const</span> <span class="title function_">Register</span> = (<span class="params"></span>) =&gt; {</span><br><span class="line">    <span class="keyword">const</span> dispatch = <span class="title function_">useDispatch</span>();</span><br><span class="line">    <span class="keyword">const</span> navigate = <span class="title function_">useNavigate</span>();</span><br><span class="line">    <span class="keyword">const</span> [userData, setUserData] = <span class="title function_">useState</span>({</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><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>useState</code> 的用法是这样的:接受一个参数作为状态的初始值,比方说我们这里是一个字典,包含着注册页面中所有输入框的值。它会返回一个数组,第一个元素代表着状态的当前值,第二个元素代表着一个函数,用于更新状态。</p>
<p>每当用户输入信息时,我们都应该更新状态。React 提供了一个 <code>onChange</code> 事件供我们使用:</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><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">const</span> <span class="title function_">handleChange</span> = e =&gt; {</span><br><span class="line">    <span class="title function_">setUserData</span>({</span><br><span class="line">        ...userData,</span><br><span class="line">        [e.<span class="property">target</span>.<span class="property">name</span>]: e.<span class="property">target</span>.<span class="property">value</span></span><br><span class="line">    });</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Input</span> <span class="attr">onChange</span>=<span class="string">{</span> <span class="attr">handleChange</span> } /&gt;</span></span></span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>每次用户输入信息时,<code>handleChange</code> 函数都会被触发,而 <code>handleChange</code> 函数会调用 <code>setUserData</code> 函数,更新状态。</p>
</blockquote>
<p>表单被用户提交后,我们也需要触发一个 Action 来发送用户信息到服务端:</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><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> <span class="title function_">handleSubmit</span> = e =&gt; {</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">    <span class="title function_">dispatch</span>(<span class="title function_">registerUser</span>(userData, navigate));</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Form</span> <span class="attr">onSubmit</span>=<span class="string">{</span> <span class="attr">handleSubmit</span> } /&gt;</span></span></span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>useDispatch</code> 是 React Redux 的一个 Hook,用于在函数组件中传递 Action。</p>
</blockquote>
<p>那么 <code>registerUser</code> 方法是怎么写的呢?</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><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>
<blockquote>
<p><code>socket.emit</code> 用于发送数据到服务端;<code>socket.on</code> 用于接收服务端返回的数据。</p>
</blockquote>
<p><code>registerUser</code> 方法会先发送用户填写的信息到服务端,接着监听服务端返回的数据。如果服务端返回的数据中 <code>status</code><code>00000</code>,则说明注册成功,我们就跳转到登录页面;如果不是,我们就在控制台打印出服务端返回的错误信息。</p>
<p>而在服务端中,我们需要接受名为 <code>register</code> 的事件、验证用户填写的信息,然后发送 <code>newRegisteredUser</code> 事件:</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><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">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> existingUser = <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 class="attr">username</span>: userData.<span class="property">username</span></span><br><span class="line">    });</span><br><span class="line">    <span class="keyword">if</span> (existingUser) {</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">        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>
<blockquote>
<p><code>User</code> 是一个 Mongoose 模型,用于操作 MongoDB 数据库。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> mongoose = <span class="built_in">require</span>(<span class="string">"mongoose"</span>);</span><br><span class="line"></span><br><span class="line">mongoose.<span class="title function_">connect</span>(<span class="string">"mongodb://localhost:27017/hotaru"</span>)</span><br><span class="line">    .<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">"Connected to MongoDB"</span>))</span><br><span class="line">    .<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>(<span class="string">"Could not connect to MongoDB"</span>, err));</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = mongoose;</span><br></pre></td></tr></tbody></table></figure>
<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> bcrypt = <span class="built_in">require</span>( <span class="string">"bcrypt"</span>);</span><br><span class="line"><span class="keyword">const</span> mongoose = <span class="built_in">require</span>(<span class="string">"./mongodb"</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">UserSchema</span> = <span class="keyword">new</span> mongoose.<span class="title class_">Schema</span>({</span><br><span class="line">    <span class="attr">emailAddress</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">String</span>,</span><br><span class="line">        <span class="attr">unique</span>: <span class="literal">true</span></span><br><span class="line">    },</span><br><span class="line">    <span class="attr">username</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">String</span>,</span><br><span class="line">        <span class="attr">unique</span>: <span class="literal">true</span></span><br><span class="line">    },</span><br><span class="line">    <span class="attr">password</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">String</span>,</span><br><span class="line">        <span class="title function_">set</span>(<span class="params">val</span>) { <span class="keyword">return</span> bcrypt.<span class="title function_">hashSync</span>(val, <span class="number">10</span>) },</span><br><span class="line">        <span class="attr">select</span>: <span class="literal">false</span></span><br><span class="line">    },</span><br><span class="line">    <span class="title class_">DOBYear</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">Number</span></span><br><span class="line">    },</span><br><span class="line">    <span class="title class_">DOBMonth</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">Number</span></span><br><span class="line">    },</span><br><span class="line">    <span class="title class_">DOBDay</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">Number</span></span><br><span class="line">    },</span><br><span class="line">    <span class="attr">createTime</span>: {</span><br><span class="line">        <span class="attr">type</span>: <span class="title class_">Date</span>,</span><br><span class="line">        <span class="attr">default</span>: <span class="title class_">Date</span>.<span class="property">now</span></span><br><span class="line">    }</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">User</span> = mongoose.<span class="title function_">model</span>(<span class="string">"User"</span>, <span class="title class_">UserSchema</span>);</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = { <span class="title class_">User</span> };</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>bcrypt</code> 是一个用于加密密码的包,不只是加密密码,我们验证用户登录时也会用到它。</p>
<p>最根本的原因是我们不会在数据库中存储用户的明文密码,要验证用户登陆的话,只能用加密后的用户输入的密码和数据库中的密码进行比对。</p>
</blockquote>
<p>我这里也根据网上的文章自己定义了一套错误码,未来可能会展开说说。</p>
</blockquote>
<p>这样,我们就完成了注册页面的逻辑。</p>
<p>登录页面的逻辑和注册页面的逻辑是一样的,只是在 <code>registerUser</code> 方法中,我们需要发送 <code>login</code> 事件,而在服务端中,我们需要接受名为 <code>login</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">loginUser</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">"login"</span>, userData);</span><br><span class="line"></span><br><span class="line">        socket.<span class="title function_">on</span>(<span class="string">"loggedInUser"</span>, <span class="function"><span class="params">data</span> =&gt;</span> {</span><br><span class="line">            <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">"00000"</span>) {</span><br><span class="line">                <span class="keyword">const</span> { token } = data;</span><br><span class="line">                <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">"jwtToken"</span>, token);</span><br><span class="line">                userData[<span class="string">"token"</span>] = token;</span><br><span class="line">                <span class="title function_">dispatch</span>(<span class="title function_">setCurrentUser</span>(userData));</span><br><span class="line">                <span class="title function_">navigate</span>(<span class="string">"/channels/@me"</span>);</span><br><span class="line">            } <span class="keyword">else</span> {</span><br><span class="line">                <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>);</span><br><span class="line">            }</span><br><span class="line">        });</span><br><span class="line">    }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>localStorage</code> 是浏览器提供的一个 API,用于在浏览器中存储数据。这里存储了 JWT Token,以后会提到这是什么。</p>
</blockquote>
<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></pre></td><td class="code"><pre><span class="line">socket.<span class="title function_">on</span>(<span class="string">"login"</span>, <span class="keyword">async</span> userData =&gt; {</span><br><span class="line">    <span class="keyword">const</span> existingUser = <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 class="keyword">if</span> (!existingUser) {</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[U0201] User does not exist: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line">        socket.<span class="title function_">emit</span>(<span class="string">"loggedInUser"</span>, {</span><br><span class="line">            <span class="attr">status</span>: <span class="string">"U0201"</span>,</span><br><span class="line">            <span class="attr">message</span>: <span class="string">"User does not exist."</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">    bcrypt.<span class="title function_">compare</span>(userData.<span class="property">password</span>, existingUser.<span class="property">password</span>, <span class="function">(<span class="params">err, confirmPassword</span>) =&gt;</span> {</span><br><span class="line">        <span class="keyword">if</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 class="keyword">return</span>;</span><br><span class="line">        }</span><br><span class="line">        <span class="keyword">if</span> (!confirmPassword) {</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[U0202] Password is incorrect: <span class="subst">${userData.password}</span>`</span>);</span><br><span class="line">            socket.<span class="title function_">emit</span>(<span class="string">"loggedInUser"</span>, {</span><br><span class="line">                <span class="attr">status</span>: <span class="string">"U0202"</span>,</span><br><span class="line">                <span class="attr">message</span>: <span class="string">"Password is incorrect."</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="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[00000] User logged in: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line">        socket.<span class="title function_">emit</span>(<span class="string">"loggedInUser"</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><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>一套组合拳下来,一旦用户的信息被验证成功,就会跳转到频道页面。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="14d5.html">上一篇</a><a class="next" href="948f.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/bb3e.html" data-full-url="https://cytrogen.icu/posts/bb3e.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>