<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>React + NestJS + Socket.io 项目实践【2】:JWT 验证 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS + Socket.io 全栈项目实践的第二篇,核心内容是实现基于 JWT 的用户身份验证。后端部分,教程详细讲解了如何配置 NestJS 的JWT模块、在登录成功后生成Token,并创建全局路由守卫 AccessTokenGuard 配合 @Public 装饰器来保护接口。前端部分,则演示了 React 应用在登录后如何存储Token,并利用自定义 useAuth Hook 和 ProtectedRoute 组件,根据认证状态实现动态的页面访问控制。本教程完整地展示了一个安全的全栈JWT认证流程,为后续功能开发奠定了权限基础。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/40b4.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/40b4.html">永久链接</a><div class="p-summary visually-hidden"><p>上篇文章我们迁移了整个项目到 NestJS,这篇文章我们将实现 JWT 验证。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/%E5%85%A8%E6%A0%88%E5%AE%9E%E8%B7%B5/">全栈实践</a><a class="p-category" href="../tags/Node-js/">Node.js</a><a class="p-category" href="../tags/React-js/">React.js</a><a class="p-category" href="../tags/TypeScript/">TypeScript</a><a class="p-category" href="../tags/NestJS/">NestJS</a></div><h1 class="post-title p-name">React + NestJS + Socket.io 项目实践【2】:JWT 验证</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-04-28T04:25:50.000Z">4/28/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.993Z"></time></div><div class="post-content e-content"><html><head></head><body><p>上篇文章我们迁移了整个项目到 NestJS,这篇文章我们将实现 JWT 验证。</p>
<span id="more"></span>
<h2 id="一些改动"><a class="markdownIt-Anchor" href="#一些改动"></a> 一些改动</h2>
<p>为了项目的可维护性,我对项目的目录结构进行了一些调整。</p>
<p>首先是把所有注册登录相关的代码都从 <code>users</code> 文件夹放到 <code>auth</code> 文件夹中,只给 <code>users</code> 文件夹留下用户信息的相关代码。</p>
<p><code>UsersModule</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongooseModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span>, <span class="title class_">UserSchema</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersController</span> } <span class="keyword">from</span> <span class="string">'./users.controller'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'./users.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">MongooseModule</span>.<span class="title function_">forFeature</span>([</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">name</span>: <span class="title class_">User</span>.<span class="property">name</span>,</span><br><span class="line"> <span class="attr">schema</span>: <span class="title class_">UserSchema</span>,</span><br><span class="line"> },</span><br><span class="line"> ]),</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">controllers</span>: [<span class="title class_">UsersController</span>],</span><br><span class="line"> <span class="attr">providers</span>: [<span class="title class_">UsersService</span>],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<p><code>UsersService</code> 添加了更多用来查询用户的方法:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">InjectModel</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Model</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersService</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="meta">@InjectModel</span>(User.name)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="attr">usersModel</span>: <span class="title class_">Model</span><<span class="title class_">User</span>>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findByEmail</span>(<span class="attr">emailAddress</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span><<span class="title class_">User</span> | <span class="literal">null</span>> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({ emailAddress }).<span class="title function_">exec</span>()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findByUsername</span>(<span class="attr">username</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span><<span class="title class_">User</span> | <span class="literal">null</span>> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({ username }).<span class="title function_">exec</span>()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findAll</span>(): <span class="title class_">Promise</span><<span class="title class_">User</span>[]> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">find</span>().<span class="title function_">exec</span>()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>UsersController</code> 新添了一个 <code>GET</code> 路由,用于根据用户名查询用户信息,后续会用到:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Controller</span>, <span class="title class_">Get</span>, <span class="title class_">Param</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'./users.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Public</span> } <span class="keyword">from</span> <span class="string">'../common/decorator/public.decorator'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersController</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">usersService</span>: <span class="title class_">UsersService</span></span>) {}</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Public</span>()</span><br><span class="line"> <span class="meta">@Get</span>(<span class="string">':username'</span>)</span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findOne</span>(<span class="params"><span class="meta">@Param</span>(<span class="string">'username'</span>) <span class="attr">username</span>: <span class="built_in">string</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">findByUsername</span>(username)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>假设要查询的用户的用户名是 <code>dummy</code>,那么请求的 URL 就是 <code>/api/users/dummy</code>。</p>
</blockquote>
<p>客户端的文件改动则是将大部分文件从 <code>components</code> 目录中拿了出来,例如 Redux 相关的文件被统一放到了 <code>redux</code> 目录、和 <code>components</code> 同级。</p>
<p>同时我还删除了上一篇文章中写的 <code>Guard</code> 组件,因为在这篇文章中我们会要写 <a href="#%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%88%A4%E6%96%AD%E7%94%A8%E6%88%B7%E6%98%AF%E5%90%A6%E7%99%BB%E5%BD%95">功能更为复杂的升级版路由守卫</a>。</p>
<h2 id="jwt生成"><a class="markdownIt-Anchor" href="#jwt生成"></a> JWT 生成</h2>
<p>JWT 是一种用于在网络上传输信息的简洁方法。对比 Session 和 Cookie,JWT 的优势在于不需要在服务端存储用户信息,而是通过加密的方式将用户信息存储在 Token 中,然后在客户端存储这个 Token。</p>
<p>有关 JWT 的更多信息,可以自行查阅资料。</p>
<p>JWT 验证的实现很大程度上参照了稀土掘金上的一篇文章:<a target="_blank" rel="noopener" href="https://juejin.cn/post/7236593818594312229">NestJS 登录功能:基于 JWT 的身份验证</a>。</p>
<p><code>.env</code> 文件中配置:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">JWT_SECRET=secret</span><br><span class="line">JWT_TOKEN_AUDIENCE=localhost:4000</span><br><span class="line">JWT_TOKEN_ISSUER=localhost:4000</span><br><span class="line">JWT_ACCESS_TOKEN_TTL=3600</span><br></pre></td></tr></tbody></table></figure>
<p><code>AppModule</code> 中配置 <code>ConfigModule</code>,这里需要用到 <code>Joi</code> 包(用于验证环境变量):</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ConfigModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Joi</span> <span class="keyword">from</span> <span class="string">'joi'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">ConfigModule</span>.<span class="title function_">forRoot</span>({</span><br><span class="line"> <span class="attr">validationSchema</span>: <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">JWT_SECRET</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_TOKEN_AUDIENCE</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_TOKEN_ISSUER</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_ACCESS_TOKEN_TTL</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">3600</span>),</span><br><span class="line"> }),</span><br><span class="line"> }),</span><br><span class="line"> ],</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>使用 <code>Joi</code> 包的目的是为了验证环境变量是否存在,以及是否符合预期的类型。</p>
<p>假设环境变量 <code>JWT_SECRET</code> 不存在,那么 <code>Joi</code> 会抛出一个错误,阻止应用程序启动。</p>
</blockquote>
<p>新建一个 <code>jwt.config.ts</code> 文件,用于配置 <code>JwtModule</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { registerAs } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">registerAs</span>(<span class="string">'jwt'</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">secret</span>: process.<span class="property">env</span>.<span class="property">JWT_SECRET</span>,</span><br><span class="line"> <span class="attr">audience</span>: process.<span class="property">env</span>.<span class="property">JWT_TOKEN_AUDIENCE</span>,</span><br><span class="line"> <span class="attr">issuer</span>: process.<span class="property">env</span>.<span class="property">JWT_TOKEN_ISSUER</span>,</span><br><span class="line"> <span class="attr">accessTokenTtl</span>: <span class="built_in">parseInt</span>(process.<span class="property">env</span>.<span class="property">JWT_ACCESS_TOKEN_TTL</span> ?? <span class="string">'3600'</span>, <span class="number">10</span>),</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>接着在 <code>AuthModule</code> 引入这个配置:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../common/config/jwt.config'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">ConfigModule</span>.<span class="title function_">forFeature</span>(jwtConfig),</span><br><span class="line"> <span class="title class_">JwtModule</span>.<span class="title function_">registerAsync</span>(jwtConfig.<span class="title function_">asProvider</span>()),</span><br><span class="line"> ],</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>回到我们用户登陆的逻辑。当服务端验证用户信息后,我们需要生成一个 Token,然后返回给客户端。</p>
<p>新建一个 <code>active-user-data.interface.ts</code> 文件,用于定义 Token 中的负载:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">ActiveUserData</span> {</span><br><span class="line"> <span class="attr">sub</span>: <span class="built_in">number</span></span><br><span class="line"> <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 JWT 中,<code>sub</code>(主题,通常是 ID)和 <code>name</code> 是常见的有效载荷字段,定义这些字段有助于应用进行身份验证和授权、帮助服务器识别发送请求的用户。</p>
<p><code>AuthService</code> 中添加两个方法,用于生成 Token 和验证 Token:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ConfigType</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">'../users/user.schema'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../common/config/jwt.config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ActiveUserData</span> } <span class="keyword">from</span> <span class="string">'./interfaces/active-user-data.interface'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AuthService</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params"> <span class="meta">@Inject</span>(jwtConfig.KEY)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtConfiguration</span>: <span class="title class_">ConfigType</span><<span class="keyword">typeof</span> jwtConfig>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">generateTokens</span>(<span class="params"><span class="attr">user</span>: <span class="title class_">User</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">signToken</span><<span class="title class_">Partial</span><<span class="title class_">ActiveUserData</span>>>(user.<span class="property">_id</span>, {<span class="attr">name</span>: user.<span class="property">username</span>})</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">async</span> signToken<T>(<span class="attr">userId</span>: <span class="built_in">number</span>, <span class="attr">payload</span>?: T) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">jwtService</span>.<span class="title function_">signAsync</span>(</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">sub</span>: userId,</span><br><span class="line"> ...payload,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">secret</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">secret</span>,</span><br><span class="line"> <span class="attr">audience</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">audience</span>,</span><br><span class="line"> <span class="attr">issuer</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">issuer</span>,</span><br><span class="line"> <span class="attr">expiresIn</span>: <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>.<span class="property">accessTokenTtl</span>,</span><br><span class="line"> },</span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>signToken</code> 方法接收一个用户 ID 和一个负载对象,然后使用 <code>JwtService</code> 的 <code>signAsync</code> 方法生成一个 Token。这里使用的 <code>jwtConfig</code> 是我们之前配置的 JWT 配置。</p>
<p><code>generateTokens</code> 方法接收一个用户对象,然后调用 <code>signToken</code> 方法生成一个 Token。</p>
</blockquote>
<p>在 <code>login</code> 方法中调用 <code>generateTokens</code> 方法:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"><span class="attr">loginUserDto</span>: <span class="title class_">LoginUserDto</span></span>) {</span><br><span class="line"> <span class="keyword">const</span> user = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersModel</span>.<span class="title function_">findOne</span>({</span><br><span class="line"> <span class="attr">username</span>: loginUserDto.<span class="property">username</span>,</span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">if</span> (!user) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>(<span class="string">'The username is invalid'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> passwordValid = <span class="keyword">await</span> bcrypt.<span class="title function_">compare</span>(loginUserDto.<span class="property">password</span>, user.<span class="property">password</span>)</span><br><span class="line"> <span class="keyword">if</span> (!passwordValid) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>(<span class="string">'The password is invalid'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 返回Token</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">generateTokens</span>(user)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>AuthController</code> 也需要做一些改动:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Post</span>(<span class="string">'login'</span>)</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"><span class="meta">@Body</span>() <span class="attr">loginUserDto</span>: <span class="title class_">LoginUserDto</span>, <span class="meta">@Res</span>() <span class="attr">res</span>: <span class="title class_">Response</span></span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> resultToken = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">authService</span>.<span class="title function_">login</span>(loginUserDto)</span><br><span class="line"></span><br><span class="line"> res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">OK</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'00000'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'User logged in successfully'</span>,</span><br><span class="line"> <span class="attr">token</span>: resultToken, <span class="comment">// <-- 向客户端返回Token</span></span><br><span class="line"> })</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</span><br><span class="line"> <span class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">UnauthorizedException</span>) {</span><br><span class="line"> res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">UNAUTHORIZED</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'U0202'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'The username or password is invalid'</span>,</span><br><span class="line"> })</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'U0200'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'Failed to log in user due to an unknown error'</span>,</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>客户端接收到 Token 后,可以将 Token 存储在 <code>localStorage</code> 中,以便后续请求时使用:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">loginUser</span> = (<span class="params"><span class="attr">userData</span>: <span class="title class_">User</span>, <span class="attr">navigate</span>: (path: <span class="built_in">string</span>) => <span class="built_in">void</span></span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">async</span> (<span class="attr">dispatch</span>: <span class="title class_">Dispatch</span>) => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">login</span>(userData);</span><br><span class="line"> <span class="keyword">const</span> data = response.<span class="property">data</span>;</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">"00000"</span>) {</span><br><span class="line"> <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">"jwtToken"</span>, data.<span class="property">token</span>);</span><br><span class="line"> <span class="title function_">dispatch</span>(<span class="title function_">setCurrentUser</span>({</span><br><span class="line"> ...userData,</span><br><span class="line"> <span class="attr">access_token</span>: data.<span class="property">token</span>,</span><br><span class="line"> }));</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(data);</span><br><span class="line"> <span class="title function_">navigate</span>(<span class="string">"/"</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(error);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h2 id="jwt验证"><a class="markdownIt-Anchor" href="#jwt验证"></a> JWT 验证</h2>
<p>生成 Token 后,我们需要在每次请求时验证 Token。</p>
<p>服务端新建一个 <code>access-token.guard.ts</code> 文件:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">CanActivate</span>, <span class="title class_">ExecutionContext</span>, <span class="title class_">Inject</span>, <span class="title class_">Injectable</span>, <span class="title class_">UnauthorizedException</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ConfigType</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Reflector</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">REQUEST_USER_KEY</span> } <span class="keyword">from</span> <span class="string">'../../common'</span></span><br><span class="line"><span class="keyword">import</span> jwtConfig <span class="keyword">from</span> <span class="string">'../../common/config/jwt.config'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">IS_PUBLIC_KEY</span> } <span class="keyword">from</span> <span class="string">'../../common/decorator/public.decorator'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AccessTokenGuard</span> <span class="keyword">implements</span> <span class="title class_">CanActivate</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">reflector</span>: <span class="title class_">Reflector</span>,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params"> <span class="meta">@Inject</span>(jwtConfig.KEY)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">jwtConfiguration</span>: <span class="title class_">ConfigType</span><<span class="keyword">typeof</span> jwtConfig>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">canActivate</span>(<span class="attr">context</span>: <span class="title class_">ExecutionContext</span>): <span class="title class_">Promise</span><<span class="built_in">boolean</span>> {</span><br><span class="line"> <span class="keyword">const</span> isPublic = <span class="variable language_">this</span>.<span class="property">reflector</span>.<span class="title function_">get</span>(<span class="variable constant_">IS_PUBLIC_KEY</span>, context.<span class="title function_">getHandler</span>())</span><br><span class="line"> <span class="keyword">if</span> (isPublic) <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> request = context.<span class="title function_">switchToHttp</span>().<span class="title function_">getRequest</span>()</span><br><span class="line"> <span class="keyword">const</span> token = <span class="variable language_">this</span>.<span class="title function_">extractTokenFromHeader</span>(request)</span><br><span class="line"> <span class="keyword">if</span> (!token) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> request[<span class="variable constant_">REQUEST_USER_KEY</span>] = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">jwtService</span>.<span class="title function_">verifyAsync</span>(token, <span class="variable language_">this</span>.<span class="property">jwtConfiguration</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnauthorizedException</span>()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">extractTokenFromHeader</span>(<span class="attr">request</span>: <span class="title class_">Request</span>): <span class="built_in">string</span> | <span class="literal">undefined</span> {</span><br><span class="line"> <span class="keyword">const</span> authorization = request.<span class="property">headers</span>[<span class="string">'authorization'</span>]</span><br><span class="line"> <span class="keyword">const</span> [, token] = authorization?.<span class="title function_">split</span>(<span class="string">' '</span>) ?? []</span><br><span class="line"> <span class="keyword">return</span> token</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>AccessTokenGuard</code> 类实现了 <code>CanActivate</code> 接口,该接口用于验证请求是否可以通过。</p>
<p><code>extractTokenFromHeader</code> 方法用于从请求头中提取 Token。</p>
</blockquote>
<p>上述代码中导入的两个新文件分别是 <code>common/index.ts</code> 和 <code>common/decorator/public.decorator.ts</code>:</p>
<figure class="highlight typescript"><figcaption><span>common/index.ts</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="variable constant_">REQUEST_USER_KEY</span> = <span class="string">'user'</span></span><br></pre></td></tr></tbody></table></figure>
<p>这个常量在 <code>request</code> 对象中用作键、用于存储已验证的 JWT 的解码信息。具体来说,当一个请求到达并通过 <code>canActivate</code> 方法时,会从请求的 <code>Authorization</code> 头中提取 JWT。如果 JWT 存在且有效(也就是能够被 <code>JwtService</code> 和 <code>verifyAsync</code> 方法验证),那么 JWT 的解码信息就会被存储在 <code>request[REQUEST_USER_KEY]</code> 中。</p>
<p>这样做的目的是为了在后续的请求处理中,可以直接通过 <code>request[REQUEST_USER_KEY]</code> 来获取 JWT 的解码信息,无需再次验证。</p>
<figure class="highlight typescript"><figcaption><span>common/decorator/public.decorator.ts</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">SetMetadata</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="variable constant_">IS_PUBLIC_KEY</span> = <span class="string">'isPublic'</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">Public</span> = (<span class="params"></span>) => <span class="title class_">SetMetadata</span>(<span class="variable constant_">IS_PUBLIC_KEY</span>, <span class="literal">true</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>首先我们要清楚一个概念。在我们启用了 JWT 验证的情况下,会导致所有的请求都需要携带 JWT Token。但是有些请求我们并不想要求用户携带 Token,比如说注册和登录请求(因为用户还没有 Token)。这时我们就需要一个装饰器来标记这些请求是公开的。</p>
<p><code>Public</code> 装饰器标记了一个请求是公开的,这样在 <code>AccessTokenGuard</code> 中就可以根据这个标记来判断是否需要验证 Token。</p>
<p>有了 <code>Public</code> 装饰器后我们就可以在 <code>AuthController</code> 中标记 <code>register</code> 和 <code>login</code> 路由是公开的:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Public</span> } <span class="keyword">from</span> <span class="string">'../common/decorator/public.decorator'</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Public</span>()</span><br><span class="line"><span class="meta">@Post</span>(<span class="string">'register'</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@Public</span>()</span><br><span class="line"><span class="meta">@Post</span>(<span class="string">'login'</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>NestJS 的项目每当新建一个东西时,都需要在 <code>app.module.ts</code> 中引入。这里我们导入 <code>AccessTokenGuard</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="variable constant_">APP_GUARD</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AccessTokenGuard</span> } <span class="keyword">from</span> <span class="string">'./guards/access-token.guard'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="attr">providers</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">provide</span>: <span class="variable constant_">APP_GUARD</span>,</span><br><span class="line"> <span class="attr">useClass</span>: <span class="title class_">AccessTokenGuard</span>,</span><br><span class="line"> },</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> ],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>APP_GUARD</code> 用于告诉 NestJS 我们要使用 <code>AccessTokenGuard</code> 这个守卫。</p>
</blockquote>
<p>有了这个守卫后,每次请求都会被验证 Token。如果请求中没有 Token 或者 Token 无效,那么请求就会被拒绝。</p>
<h2 id="客户端判断用户是否登录"><a class="markdownIt-Anchor" href="#客户端判断用户是否登录"></a> 客户端判断用户是否登录</h2>
<p>未登录的用户是不能访问某些页面的,同理,已登录的用户也不能访问登录和注册页面。</p>
<p>我们先自定义一个 Hook 来检查用户是否已经进行了身份验证。新建一个 <code>useAuth</code> 文件:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { useEffect, useState } <span class="keyword">from</span> <span class="string">'react'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useAuth</span> = (<span class="params"></span>) => {</span><br><span class="line"> <span class="keyword">const</span> [isAuthenticated, setIsAuthenticated] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"> <span class="keyword">const</span> [isAuthChecked, setIsAuthChecked] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">"jwtToken"</span>);</span><br><span class="line"> <span class="keyword">if</span> (token) {</span><br><span class="line"> <span class="title function_">setIsAuthenticated</span>(<span class="literal">true</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="title function_">setIsAuthenticated</span>(<span class="literal">false</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">setIsAuthChecked</span>(<span class="literal">true</span>);</span><br><span class="line"> }, []);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> { isAuthenticated, isAuthChecked };</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这个 Hook 会检查 <code>localStorage</code> 中是否存在 <code>jwtToken</code>,如果存在则认为用户已经登录、设置 <code>isAuthenticated</code> 为 <code>true</code>,否则设置为 <code>false</code>。</p>
<p>为了不重复进行这个检查,我们还设置了一个 <code>isAuthChecked</code> 状态,用于标记用户是否已经进行了身份验证。</p>
<p>在我们当前的项目中,身份验证无非有两种必需导向用户到不同页面的情况:</p>
<ol>
<li>用户未登录,但是访问了首页,这时我们需要将用户导向登录页面。</li>
<li>用户已经登录,但是访问了登录或注册页面,这时我们需要将用户导向首页。</li>
</ol>
<p>我们先解决第一个问题。新建一个 <code>ProtectedRoute</code> 组件(意味着只有登录用户才能访问的路由):</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">ProtectedRoute</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">RouteProps</span>> = <span class="function">(<span class="params">{ children }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> { isAuthenticated, isAuthChecked } = <span class="title function_">useAuth</span>()</span><br><span class="line"> <span class="keyword">const</span> navigate = <span class="title function_">useNavigate</span>()</span><br><span class="line"> </span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!isAuthenticated && isAuthChecked) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">pathname</span> === <span class="string">'/channels/@me'</span>) {</span><br><span class="line"> <span class="title function_">navigate</span>(<span class="string">'/login'</span>, { <span class="attr">replace</span>: <span class="literal">true</span> })</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }, [isAuthenticated, isAuthChecked, navigate])</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!isAuthChecked) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="language-xml"><span class="tag"><></span>{isAuthenticated ? children : <span class="tag"><<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/login"</span> <span class="attr">replace</span> /></span>}<span class="tag"></></span></span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 <code>App.tsx</code> 中使用这个组件:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">BrowserRouter</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Routes</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">Navigate</span> <span class="attr">to</span>=<span class="string">"/channels/@me"</span> <span class="attr">replace</span> /></span> } /></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/channels/@me/*"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">ProtectedRoute</span>></span><span class="tag"><<span class="name">Home</span> /></span><span class="tag"></<span class="name">ProtectedRoute</span>></span> } /> {/* <--- 这里 */}</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">Routes</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">BrowserRouter</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></pre></td></tr></tbody></table></figure>
<p>当用户尝试访问 <code>/channels/@me</code> 页面或其子页面时,如果用户未登录,那么就会被导向登录页面。</p>
<p>第二个问题同理,只要写一个逻辑相反的 <code>UnauthenticatedRoute</code> 组件即可:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UnauthenticatedRoute</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">RouteProps</span>> = <span class="function">(<span class="params">{ children }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> { isAuthenticated, isAuthChecked } = <span class="title function_">useAuth</span>()</span><br><span class="line"> <span class="keyword">const</span> navigate = <span class="title function_">useNavigate</span>()</span><br><span class="line"></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (isAuthenticated && isAuthChecked) {</span><br><span class="line"> <span class="title function_">navigate</span>(<span class="string">'/channels/@me'</span>, { <span class="attr">replace</span>: <span class="literal">true</span> })</span><br><span class="line"> }</span><br><span class="line"> }, [isAuthenticated, isAuthChecked, navigate])</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!isAuthChecked) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="language-xml"><span class="tag"><></span>{!isAuthenticated ? children : <span class="tag"><<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/channels/@me"</span> <span class="attr">replace</span> /></span>}<span class="tag"></></span></span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>使用方法也是一样的:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">BrowserRouter</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Routes</span>></span></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">Route</span> <span class="attr">path</span>=<span class="string">"/login"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">UnauthenticatedRoute</span>></span><span class="tag"><<span class="name">Login</span> /></span><span class="tag"></<span class="name">UnauthenticatedRoute</span>></span> } /></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/register"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">UnauthenticatedRoute</span>></span><span class="tag"><<span class="name">Register</span> /></span><span class="tag"></<span class="name">UnauthenticatedRoute</span>></span> } /></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">Routes</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">BrowserRouter</span>></span></span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h2 id="eslint和prettier配置"><a class="markdownIt-Anchor" href="#eslint和prettier配置"></a> ESLint 和 Prettier 配置</h2>
<p>我开发这个项目一直用的是 WebStorm。最近换设备后,一进入客户端的目录后都会弹出错误,说是 ESLint 配置冲突,就决定重新为两个目录配置 ESLint 和 Prettier。</p>
<p>服务端因为新建项目时就自带了 <code>.eslintrc.js</code> 和 <code>.prettierrc</code> 文件,所以只需要在客户端新建这两个文件即可。</p>
<p>客户端下安装必要的包:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev eslint-plugin-prettier eslint-config-prettier</span><br></pre></td></tr></tbody></table></figure>
<p>新建 <code>.eslintrc.js</code> 文件:</p>
<figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = {</span><br><span class="line"> <span class="attr">parser</span>: <span class="string">'@typescript-eslint/parser'</span>,</span><br><span class="line"> <span class="attr">parserOptions</span>: {</span><br><span class="line"> <span class="attr">project</span>: <span class="string">'tsconfig.json'</span>, <span class="comment">// <--- 这个文件是WebStorm新建项目时自动生成的</span></span><br><span class="line"> <span class="attr">tsconfigRootDir</span>: __dirname,</span><br><span class="line"> <span class="attr">sourceType</span>: <span class="string">'module'</span>,</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">plugins</span>: [<span class="string">'@typescript-eslint/eslint-plugin'</span>, <span class="string">'react'</span>],</span><br><span class="line"> <span class="attr">extends</span>: [</span><br><span class="line"> <span class="string">'plugin:@typescript-eslint/recommended'</span>,</span><br><span class="line"> <span class="string">'plugin:react/recommended'</span>,</span><br><span class="line"> <span class="string">'plugin:prettier/recommended'</span>,</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">root</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">env</span>: {</span><br><span class="line"> <span class="attr">browser</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">jest</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">es6</span>: <span class="literal">true</span>,</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">rules</span>: {</span><br><span class="line"> <span class="string">'@typescript-eslint/interface-name-prefix'</span>: <span class="string">'off'</span>,</span><br><span class="line"> <span class="string">'@typescript-eslint/explicit-function-return-type'</span>: <span class="string">'off'</span>,</span><br><span class="line"> <span class="string">'@typescript-eslint/explicit-module-boundary-types'</span>: <span class="string">'off'</span>,</span><br><span class="line"> <span class="string">'@typescript-eslint/no-explicit-any'</span>: <span class="string">'off'</span>,</span><br><span class="line"> <span class="string">'react/prop-types'</span>: <span class="string">'off'</span>,</span><br><span class="line"> },</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p><code>.prettierrc</code> 文件我直接搬的服务端的配置,如下:</p>
<figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"singleQuote"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"trailingComma"</span><span class="punctuation">:</span> <span class="string">"all"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"endOfLine"</span><span class="punctuation">:</span> <span class="string">"lf"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"arrowParens"</span><span class="punctuation">:</span> <span class="string">"always"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bracketSameLine"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bracketSpacing"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"embeddedLanguageFormatting"</span><span class="punctuation">:</span> <span class="string">"auto"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"htmlWhitespaceSensitivity"</span><span class="punctuation">:</span> <span class="string">"css"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"insertPragma"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"jsxSingleQuote"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"printWidth"</span><span class="punctuation">:</span> <span class="number">120</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"proseWrap"</span><span class="punctuation">:</span> <span class="string">"never"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"quoteProps"</span><span class="punctuation">:</span> <span class="string">"as-needed"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"requirePragma"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"semi"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"tabWidth"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"useTabs"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"vueIndentScriptAndStyle"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"singleAttributePerLine"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
<p>重启 WebStorm 后,错误提示消失,一切正常。</p>
<h2 id="其他"><a class="markdownIt-Anchor" href="#其他"></a> 其他</h2>
<p>我长期没有更新的原因非常简单,我于三月底的时候遇到了一个不知道是什么 BUG 的 BUG。无论怎么修改代码,客户端都无法连接到服务端,而服务端也没有报告详细的错误信息。那时候直接摆烂了,也没有再继续开发。</p>
<p>最近重新看了一下代码,发现问题竟然是服务端的 <code>logger.middleware.ts</code> 的一个参数被我手贱抹去!</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span>, <span class="title class_">NestMiddleware</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span>, <span class="title class_">Response</span>, <span class="title class_">NextFunction</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">LoggerMiddleware</span> <span class="keyword">implements</span> <span class="title class_">NestMiddleware</span> {</span><br><span class="line"> <span class="title function_">use</span>(<span class="params"><span class="attr">req</span>: <span class="title class_">Request</span>, <span class="attr">res</span>: <span class="title class_">Response</span>, <span class="attr">next</span>: <span class="title class_">NextFunction</span></span>) { <span class="comment">// <--- res参数被我删掉了</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Request...'</span>, req.<span class="property">method</span>, req.<span class="property">originalUrl</span>)</span><br><span class="line"> <span class="title function_">next</span>()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>那么为什么这个异常没有被服务端捕获呢?因为我的全局异常过滤器也没有写好!</p>
<p>这是原先的全局异常过滤器:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ExceptionFilter</span>, <span class="title class_">Catch</span>, <span class="title class_">ArgumentsHost</span>, <span class="title class_">HttpException</span>, <span class="title class_">HttpStatus</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Catch</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AnyExceptionFilter</span> <span class="keyword">implements</span> <span class="title class_">ExceptionFilter</span> {</span><br><span class="line"> <span class="keyword">catch</span>(<span class="attr">exception</span>: <span class="built_in">any</span>, <span class="attr">host</span>: <span class="title class_">ArgumentsHost</span>) {</span><br><span class="line"> <span class="keyword">const</span> ctx = host.<span class="title function_">switchToHttp</span>()</span><br><span class="line"> <span class="keyword">const</span> response = ctx.<span class="title function_">getResponse</span>()</span><br><span class="line"> <span class="keyword">const</span> request = ctx.<span class="title function_">getRequest</span>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> status = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getStatus</span>() : <span class="title class_">HttpStatus</span>.<span class="property">INTERNAL_SERVER_ERROR</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> message = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getResponse</span>() : <span class="string">'Internal server error'</span></span><br><span class="line"></span><br><span class="line"> response.<span class="title function_">status</span>(status).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">statusCode</span>: status,</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toISOString</span>(),</span><br><span class="line"> <span class="attr">path</span>: request.<span class="property">url</span>,</span><br><span class="line"> <span class="attr">message</span>: message,</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>能发现问题没有?<code>status</code> 和 <code>message</code> 都被我写死了!如果异常不是 <code>HttpException</code>,那么这两个值就都只会是 <code>500</code> 和 <code>Internal server error</code>。</p>
<p>这是我修改后的全局异常过滤器:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ExceptionFilter</span>, <span class="title class_">Catch</span>, <span class="title class_">ArgumentsHost</span>, <span class="title class_">HttpException</span>, <span class="title class_">HttpStatus</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span>, <span class="title class_">Response</span> } <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Catch</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AnyExceptionFilter</span> <span class="keyword">implements</span> <span class="title class_">ExceptionFilter</span> {</span><br><span class="line"> <span class="keyword">catch</span>(<span class="attr">exception</span>: <span class="built_in">any</span>, <span class="attr">host</span>: <span class="title class_">ArgumentsHost</span>) {</span><br><span class="line"> <span class="keyword">const</span> ctx = host.<span class="title function_">switchToHttp</span>()</span><br><span class="line"> <span class="keyword">const</span> response = ctx.<span class="property">getResponse</span><<span class="title class_">Response</span>>()</span><br><span class="line"> <span class="keyword">const</span> request = ctx.<span class="property">getRequest</span><<span class="title class_">Request</span>>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> status = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getStatus</span>() : <span class="title class_">HttpStatus</span>.<span class="property">INTERNAL_SERVER_ERROR</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> message = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getResponse</span>() : exception</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(exception)</span><br><span class="line"></span><br><span class="line"> response.<span class="title function_">status</span>(status).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">statusCode</span>: status,</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toISOString</span>(),</span><br><span class="line"> <span class="attr">path</span>: request.<span class="property">url</span>,</span><br><span class="line"> <span class="attr">message</span>: message,</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>现在,无论异常是什么,都会被正确地捕获并返回给客户端。</p>
<p>希望大家以我为戒,不要犯我这样的错误。</p>
<h4 id="新添加但没有完成的东西"><a class="markdownIt-Anchor" href="#新添加但没有完成的东西"></a> 新添加但没有完成的东西</h4>
<p>服务端新增了 Socket 模块,毕竟完成了注册登录、JWT 生成验证等功能后,下一个就是老生常态的消息接收发送。既然我们使用的是 Socket.IO,那就要新建一个 Socket 模块。</p>
<p><code>SocketModule</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketService</span> } <span class="keyword">from</span> <span class="string">'./socket.service'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketGateway</span> } <span class="keyword">from</span> <span class="string">'./socket.gateway'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">providers</span>: [<span class="title class_">SocketGateway</span>, <span class="title class_">SocketService</span>],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SocketModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<p><code>SocketService</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Server</span> } <span class="keyword">from</span> <span class="string">'socket.io'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SocketService</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">server</span>: <span class="title class_">Server</span></span><br><span class="line"></span><br><span class="line"> <span class="title function_">initialize</span>(<span class="params"><span class="attr">server</span>: <span class="title class_">Server</span></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">server</span> = server</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">sendMessage</span>(<span class="params"><span class="attr">event</span>: <span class="built_in">string</span>, <span class="attr">message</span>: <span class="built_in">any</span></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">server</span>.<span class="title function_">emit</span>(event, message)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>initialize</code> 方法用于初始化 Socket 服务器。</p>
<p><code>sendMessage</code> 方法用于向所有连接的客户端发送消息。</p>
</blockquote>
<p>这里讲一下 Controller 和 Gateway 的区别。</p>
<p>在传统的 HTTP 请求 / 响应模型中,Controller 负责处理来自客户端的请求并返回响应。然而,我们在使用 WebSocket 时,不再有请求和响应的概念,而是有事件和消息的概念。这时,Gateway(网关)就是用来处理这些事件和消息的。</p>
<p><code>SocketGateway</code>:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">WebSocketGateway</span>, <span class="title class_">WebSocketServer</span>, <span class="title class_">SubscribeMessage</span>, <span class="title class_">MessageBody</span>, <span class="title class_">OnGatewayInit</span> } <span class="keyword">from</span> <span class="string">'@nestjs/websockets'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Server</span>, <span class="title class_">Socket</span> } <span class="keyword">from</span> <span class="string">'socket.io'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketService</span> } <span class="keyword">from</span> <span class="string">'./socket.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@WebSocketGateway</span>(<span class="number">3001</span>, {</span><br><span class="line"> <span class="attr">allowEIO3</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">cors</span>: {</span><br><span class="line"> <span class="attr">origin</span>: process.<span class="property">env</span>.<span class="property">CLIENT_ORIGIN</span> || <span class="string">'http://localhost:3000'</span>,</span><br><span class="line"> <span class="attr">credentials</span>: <span class="literal">true</span>,</span><br><span class="line"> },</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SocketGateway</span> <span class="keyword">implements</span> <span class="title class_">OnGatewayInit</span> {</span><br><span class="line"> <span class="meta">@WebSocketServer</span>()</span><br><span class="line"> <span class="attr">server</span>: <span class="title class_">Server</span></span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">socketService</span>: <span class="title class_">SocketService</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="title function_">afterInit</span>(<span class="params"><span class="attr">server</span>: <span class="title class_">Server</span></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">socketService</span>.<span class="title function_">initialize</span>(server)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@SubscribeMessage</span>(<span class="string">'privateMessageSent'</span>)</span><br><span class="line"> <span class="title function_">handlePrivateMessage</span>(<span class="params"><span class="meta">@MessageBody</span>() <span class="attr">data</span>: <span class="built_in">any</span>, <span class="attr">client</span>: <span class="title class_">Socket</span></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Received private message:'</span>, data, <span class="string">'from'</span>, client)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 <code>SocketGateway</code> 中,我们使用 <code>this.socketService.initialize(server)</code> 初始化了 Socket 服务器,然后使用 <code>@SubscribeMessage</code> 装饰器来监听客户端发送的消息。</p>
<p>我们可以在客户端发送消息时调用 <code>socket.emit('privateMessageSent', message)</code>。来看一下我的输入框 <code>PrivateMessageTextBox</code> 组件:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, { useState, <span class="title class_">KeyboardEvent</span>, <span class="title class_">ChangeEvent</span>, <span class="title class_">FormEvent</span> } <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Icon</span> } <span class="keyword">from</span> <span class="string">"@iconify/react"</span>;</span><br><span class="line"><span class="keyword">import</span> socket <span class="keyword">from</span> <span class="string">"../../redux/actions/messageActions"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'../../redux/actions/serverConnection'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">PrivateMessageTextBoxProps</span> {</span><br><span class="line"> <span class="attr">receiverUsername</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PrivateMessageTextBox</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">PrivateMessageTextBoxProps</span>> = <span class="function">(<span class="params">{ receiverUsername }</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> [message, setMessage] = useState<<span class="built_in">string</span>>(<span class="string">""</span>);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">handleKeyDown</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">KeyboardEvent</span></span>) => {</span><br><span class="line"> <span class="keyword">if</span> (e.<span class="property">key</span> === <span class="string">"Enter"</span> && !e.<span class="property">shiftKey</span>) {</span><br><span class="line"> e.<span class="title function_">preventDefault</span>();</span><br><span class="line"> <span class="title function_">handleSendMessage</span>();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">ChangeEvent</span><<span class="title class_">HTMLTextAreaElement</span>></span>) => {</span><br><span class="line"> <span class="title function_">setMessage</span>(e.<span class="property">target</span>.<span class="property">value</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">handleSendMessage</span> = <span class="keyword">async</span> (<span class="params"><span class="attr">e</span>?: <span class="title class_">FormEvent</span></span>) => {</span><br><span class="line"> e && e.<span class="title function_">preventDefault</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> senderId = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">"userId"</span>);</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">getUserByUsername</span>(receiverUsername);</span><br><span class="line"> <span class="keyword">const</span> receiver = response.<span class="property">data</span>;</span><br><span class="line"></span><br><span class="line"> socket.<span class="title function_">emit</span>(<span class="string">"privateMessageSent"</span>, {</span><br><span class="line"> <span class="attr">id</span>: <span class="string">`<span class="subst">${socket.id}</span><span class="subst">${<span class="built_in">Math</span>.random()}</span>`</span>,</span><br><span class="line"> <span class="attr">senderId</span>: senderId,</span><br><span class="line"> <span class="attr">receiverId</span>: receiver.<span class="property">_id</span>,</span><br><span class="line"> <span class="attr">text</span>: message</span><br><span class="line"> });</span><br><span class="line"> <span class="title function_">setMessage</span>(<span class="string">""</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">form</span> <span class="attr">className</span>=<span class="string">"px-2 m-3"</span> <span class="attr">onSubmit</span>=<span class="string">{</span> <span class="attr">handleSendMessage</span> }></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"w-100 p-0 m-0"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">marginBottom:</span> '<span class="attr">24px</span>', <span class="attr">backgroundColor:</span> '<span class="attr">rgba</span>(<span class="attr">56</span>, <span class="attr">58</span>, <span class="attr">64</span>)', <span class="attr">textIndent:</span> '<span class="attr">0</span>', <span class="attr">borderRadius:</span> '<span class="attr">8px</span>' }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> ></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"overflow-x-hidden overflow-y-scroll"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">borderRadius:</span> '<span class="attr">8px</span>', <span class="attr">backfaceVisibility:</span> '<span class="attr">hidden</span>', <span class="attr">scrollbarWidth:</span> '<span class="attr">none</span>' }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> ></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">div</span> <span class="attr">className</span>=<span class="string">"d-flex position-relative"</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">span</span> <span class="attr">className</span>=<span class="string">"position-sticky"</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">0</span> <span class="attr">0</span> <span class="attr">auto</span>', <span class="attr">alignSelf:</span> '<span class="attr">stretch</span>' }}></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Icon</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">icon</span>=<span class="string">"bi:plus-circle-fill"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"position-sticky w-auto m-0"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">height:</span> '<span class="attr">44px</span>', <span class="attr">padding:</span> '<span class="attr">10px</span> <span class="attr">16px</span>', <span class="attr">top:</span> '<span class="attr">0</span>', <span class="attr">marginLeft:</span> '<span class="attr">-16px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">background:</span> '<span class="attr">transparent</span>', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">181</span>, <span class="attr">186</span>, <span class="attr">193</span>)', <span class="attr">border:</span> '<span class="attr">0</span>'</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">span</span>></span></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">span</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"p-0 fs-6 w-100 position-relative"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">background:</span> '<span class="attr">transparent</span>', <span class="attr">resize:</span> '<span class="attr">none</span>', <span class="attr">border:</span> '<span class="attr">none</span>', <span class="attr">appearance:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">fontWeight:</span> '<span class="attr">400</span>', <span class="attr">lineHeight:</span> '<span class="attr">1.375rem</span>', <span class="attr">height:</span> '<span class="attr">44px</span>', <span class="attr">minHeight:</span> '<span class="attr">44px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">boxSizing:</span> '<span class="attr">border-box</span>', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> ></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">textarea</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">autoCapitalize</span>=<span class="string">"none"</span> <span class="attr">autoComplete</span>=<span class="string">"off"</span> <span class="attr">autoCorrect</span>=<span class="string">"off"</span> <span class="attr">autoFocus</span>=<span class="string">{</span> <span class="attr">true</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">placeholder</span>=<span class="string">"Text @dummy"</span> <span class="attr">spellCheck</span>=<span class="string">"true"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">className</span>=<span class="string">"position-absolute overflow-hidden"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">value</span>=<span class="string">{</span> <span class="attr">message</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">onChange</span>=<span class="string">{</span> <span class="attr">handleChange</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">onKeyDown</span>=<span class="string">{</span> <span class="attr">handleKeyDown</span> }</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{{</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">border:</span> '<span class="attr">none</span>', <span class="attr">outline:</span> '<span class="attr">none</span>', <span class="attr">resize:</span> '<span class="attr">none</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">paddingBottom:</span> '<span class="attr">11px</span>', <span class="attr">paddingTop:</span> '<span class="attr">11px</span>', <span class="attr">paddingRight:</span> '<span class="attr">10px</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">left:</span> '<span class="attr">0</span>', <span class="attr">right:</span> '<span class="attr">10px</span>', <span class="attr">background:</span> '<span class="attr">transparent</span>',</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">caretColor:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)', <span class="attr">color:</span> '<span class="attr">rgba</span>(<span class="attr">219</span>, <span class="attr">222</span>, <span class="attr">225</span>)'</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> }}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">span</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">div</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">form</span>></span></span></span><br><span class="line"> )</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">PrivateMessageTextBox</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>这个组件是一个输入框,用户输入消息后按下回车键就会发送消息。发送的消息会被 <code>SocketGateway</code> 监听到,然后打印到控制台。</p>
<p>其他的地方可以忽略掉不看,只需要重点看 <code>handleSendMessage</code> 方法。这个方法会向服务器发送一个 <code>privateMessageSent</code> 事件,事件的数据是一个对象,包含了消息的 ID、发送者 ID、接收者 ID 和消息内容:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handleSendMessage</span> = <span class="keyword">async</span> (<span class="params"><span class="attr">e</span>?: <span class="title class_">FormEvent</span></span>) => {</span><br><span class="line"> e && e.<span class="title function_">preventDefault</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> senderId = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">"userId"</span>);</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">getUserByUsername</span>(receiverUsername);</span><br><span class="line"> <span class="keyword">const</span> receiver = response.<span class="property">data</span>;</span><br><span class="line"></span><br><span class="line"> socket.<span class="title function_">emit</span>(<span class="string">"privateMessageSent"</span>, {</span><br><span class="line"> <span class="attr">id</span>: <span class="string">`<span class="subst">${socket.id}</span><span class="subst">${<span class="built_in">Math</span>.random()}</span>`</span>,</span><br><span class="line"> <span class="attr">senderId</span>: senderId,</span><br><span class="line"> <span class="attr">receiverId</span>: receiver.<span class="property">_id</span>,</span><br><span class="line"> <span class="attr">text</span>: message</span><br><span class="line"> });</span><br><span class="line"> <span class="title function_">setMessage</span>(<span class="string">""</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>这里提一嘴 <code>userId</code> 的来处。</p>
<p>在服务端的 <code>AuthController</code> 的 <code>login</code> 方法中,我们向客户端发送了一个 Token,但我们也可以发送其他东西,比如用户的 ID。</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> user = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">findByUsername</span>(loginUserDto.<span class="property">username</span>)</span><br><span class="line"></span><br><span class="line">res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">OK</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'00000'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'User logged in successfully'</span>,</span><br><span class="line"> <span class="attr">token</span>: resultToken,</span><br><span class="line"> <span class="attr">userId</span>: user.<span class="property">_id</span>,</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>在客户端也要写对应的接收逻辑:</p>
<figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">loginUser</span> = (<span class="params"><span class="attr">userData</span>: <span class="title class_">User</span>, <span class="attr">navigate</span>: (path: <span class="built_in">string</span>) => <span class="built_in">void</span></span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">async</span> (<span class="attr">dispatch</span>: <span class="title class_">Dispatch</span>) => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title class_">UsersService</span>.<span class="title function_">login</span>(userData)</span><br><span class="line"> <span class="keyword">const</span> data = response.<span class="property">data</span></span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">'00000'</span>) {</span><br><span class="line"> <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">'jwtToken'</span>, data.<span class="property">token</span>)</span><br><span class="line"> <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">'userId'</span>, data.<span class="property">userId</span>) <span class="comment">// <--- 这里</span></span><br><span class="line"> <span class="title function_">dispatch</span>(</span><br><span class="line"> <span class="title function_">setCurrentUser</span>({</span><br><span class="line"> ...userData,</span><br><span class="line"> <span class="attr">access_token</span>: data.<span class="property">token</span>,</span><br><span class="line"> <span class="attr">id</span>: data.<span class="property">userId</span>, <span class="comment">// <--- 也把ID存储到Redux里,说不定未来会用到呢</span></span><br><span class="line"> }),</span><br><span class="line"> )</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(data)</span><br><span class="line"> <span class="title function_">navigate</span>(<span class="string">'/'</span>)</span><br><span class="line"> } <span class="keyword">else</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<p>消息从客户端发送到服务端后,服务端会打印出消息的内容。</p>
<p>一个简单的消息发送就这样完成了。</p>
<p>接下来还需要完成消息的接收、存储和展示,这个就留到下一篇文章再说了。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="b5ac.html">上一篇</a><a class="next" href="a039.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/40b4.html" data-full-url="https://cytrogen.icu/posts/40b4.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>