<!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 的登录 & 注册页面还是相当容易的:</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"><<span class="title class_">Container</span> className=<span class="string">'d-flex vh-100'</span>></span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">Row</span> <span class="attr">className</span>=<span class="string">'m-auto align-self-center'</span>></span> ... <span class="tag"></<span class="name">Row</span>></span></span></span><br><span class="line"></<span class="title class_">Container</span>></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"><<span class="title class_">Container</span>></span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">Row</span> <span class="attr">xs</span>=<span class="string">'3'</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'ps-0 pe-1'</span>></span><span class="tag"></<span class="name">Col</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'px-1'</span>></span><span class="tag"></<span class="name">Col</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Col</span> <span class="attr">className</span>=<span class="string">'ps-1 pe-0'</span>></span><span class="tag"></<span class="name">Col</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">Row</span>></span></span></span><br><span class="line"></<span class="title class_">Container</span>></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 < <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"><<span class="title class_">Input</span>></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> =></span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">option</span> <span class="attr">key</span>=<span class="string">{year}</span>></span>{year}<span class="tag"></<span class="name">option</span>></span></span></span><br><span class="line"> )</span><br><span class="line"> )}</span><br><span class="line"></<span class="title class_">Input</span>></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"><<span class="name">Input</span>></span></span></span><br><span class="line"><span class="language-xml"> {months.map(</span></span><br><span class="line"><span class="language-xml"> month => (</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">option</span> <span class="attr">key</span>=<span class="string">{month}</span>></span>{month}<span class="tag"></<span class="name">option</span>></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"></<span class="name">Input</span>></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"><<span class="title class_">Input</span>></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>) =></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> =></span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<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>)' }}></span>{day}<span class="tag"></<span class="name">option</span>></span></span></span><br><span class="line"> )</span><br><span class="line"> )}</span><br><span class="line"></<span class="title class_">Input</span>></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>) =></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>) =></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>) => {</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 => {</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"><<span class="name">Input</span> <span class="attr">onChange</span>=<span class="string">{</span> <span class="attr">handleChange</span> } /></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 => {</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"><<span class="name">Form</span> <span class="attr">onSubmit</span>=<span class="string">{</span> <span class="attr">handleSubmit</span> } /></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>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">dispatch</span> =></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> =></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 => {</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">() =></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> =></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>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">dispatch</span> =></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> =></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 => {</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>) =></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>