<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>React + NestJS + Socket.io 项目实践【1】:从 Express 迁移 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS + Socket.io 全栈项目实践的第一篇,详细记录了如何将原有的 Express.js 后端迁移至 TypeScript 驱动的 NestJS 框架。前端部分,文章重点展示了将 React 项目升级为 TypeScript 的关键改动,包括为 Redux store、slice 和 action 添加强类型,并使用 Axios 处理用户认证请求。后端部分,则深入介绍了 NestJS 的核心概念(控制器、服务、模块),并一步步重构了用户注册等业务逻辑,内容涵盖了使用 DTO 进行数据验证和 Mongoose 定义数据模型。本文为希望从 Express 过渡到 NestJS 的开发者提供了一份清晰的迁移指南。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/3b97.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/3b97.html">永久链接</a><div class="p-summary visually-hidden"><p>二月份的时候我写了一篇<a href="/posts/bb3e">React + Express + Socket.io 之间的实时通信【2】:注册登录</a>,那时候我还在用 Express 作为后端框架。</p>
<p>因为中途想到使用 TypeScript,所以我决定迁移到 NestJS。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/%E5%85%A8%E6%A0%88%E5%AE%9E%E8%B7%B5/">全栈实践</a><a class="p-category" href="../tags/Node-js/">Node.js</a><a class="p-category" href="../tags/React-js/">React.js</a><a class="p-category" href="../tags/TypeScript/">TypeScript</a><a class="p-category" href="../tags/NestJS/">NestJS</a></div><h1 class="post-title p-name">React + NestJS + Socket.io 项目实践【1】:从 Express 迁移</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-03-30T04:06:13.000Z">3/30/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:55.001Z"></time></div><div class="post-content e-content"><html><head></head><body><p>二月份的时候我写了一篇 <a href="/posts/bb3e">React + Express + Socket.io 之间的实时通信【2】:注册登录</a>,那时候我还在用 Express 作为后端框架。</p>
<p>因为中途想到使用 TypeScript,所以我决定迁移到 NestJS。</p>
<span id="more"></span>
<h1 id="前端"><a class="markdownIt-Anchor" href="#前端"></a> 前端</h1>
<p>先说一下我对前端的改动。</p>
<p>因为是想要整个项目都用 TypeScript,所以我把 <code>src</code> 目录下的所有 <code>.js</code> 文件都改成了 <code>.tsx</code>。</p>
<p>很多文件都不需要改动,例如 <code>App.js</code>:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">BrowserRouter</span>, <span class="title class_">Routes</span>, <span class="title class_">Route</span>, <span class="title class_">Navigate</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"./App.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"bootstrap/dist/css/bootstrap.min.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">"./components/Home"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Login</span> <span class="keyword">from</span> <span class="string">"./components/Login"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Register</span> <span class="keyword">from</span> <span class="string">"./components/Register"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageHomepage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_homepage/Private_Message_Homepage"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageChatpage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_chatpage/Private_Message_Chatpage"</span>;</span><br><span class="line"><span class="keyword">import</span> socket <span class="keyword">from</span> <span class="string">"./components/utils/actions/authActions"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<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">Home</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">PrivateMessageHomepage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /></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">"dummy"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">PrivateMessageChatpage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /></span> }/></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">Route</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">Login</span> <span class="attr">socket</span>=<span class="string">{</span> <span class="attr">socket</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">Register</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>App.tsx</code>:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">BrowserRouter</span>, <span class="title class_">Routes</span>, <span class="title class_">Route</span>, <span class="title class_">Navigate</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"bootstrap/dist/css/bootstrap.min.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"./App.css"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">"./components/Home"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Login</span> <span class="keyword">from</span> <span class="string">"./components/Login"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Register</span> <span class="keyword">from</span> <span class="string">"./components/Register"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Guard</span> <span class="keyword">from</span> <span class="string">"./components/utils/guard"</span>;</span><br><span class="line"><span class="comment">// import PrivateMessageHomepage from "./components/private_message_homepage/Private_Message_Homepage";</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PrivateMessageChatPage</span> <span class="keyword">from</span> <span class="string">"./components/private_message_chat_page/Private_Message_Chat_Page"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<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">Guard</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">Home</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">"dummy"</span> <span class="attr">element</span>=<span class="string">{</span> <<span class="attr">PrivateMessageChatPage</span> <span class="attr">style</span>=<span class="string">{{</span> <span class="attr">flex:</span> '<span class="attr">1</span> <span class="attr">1</span> <span class="attr">auto</span>' }} /></span> }/></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">Route</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">Login</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">Register</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>对比一下会发现其实没有变化,<code>PrivateMessageHomepage</code> 改成了 <code>Guard</code> 只是因为业务逻辑的改动,跟 TypeScript 无关。</p>
<h2 id="redux"><a class="markdownIt-Anchor" href="#redux"></a> Redux</h2>
<p>涉及到 Redux 的文件多多少少都有一些改动。</p>
<p><code>store.js</code>:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { configureStore } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"><span class="keyword">import</span> authSlice <span class="keyword">from</span> <span class="string">"./reducers/authSlice"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">configureStore</span>({</span><br><span class="line"> <span class="attr">reducer</span>: {</span><br><span class="line"> <span class="attr">auth</span>: authSlice</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p><code>store.ts</code>:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { configureStore } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"><span class="keyword">import</span> { useDispatch } <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="keyword">import</span> authSlice <span class="keyword">from</span> <span class="string">"./reducers/authSlice"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = <span class="title function_">configureStore</span>({</span><br><span class="line"> <span class="attr">reducer</span>: {</span><br><span class="line"> <span class="attr">auth</span>: authSlice,</span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">RootState</span> = <span class="title class_">ReturnType</span><<span class="keyword">typeof</span> store.<span class="property">getState</span>>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">AppDispatch</span> = <span class="keyword">typeof</span> store.<span class="property">dispatch</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useAppDispatch</span> = (<span class="params"></span>) => useDispatch<<span class="title class_">AppDispatch</span>>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></tbody></table></figure>
<p>原先的 <code>store.js</code> 是直接导出了 <code>configureStore</code> 的返回值;<code>store.ts</code> 先是导出了 <code>RootState</code> 和 <code>AppDispatch</code> 这两个类型,然后道出了 <code>useAppDispatch</code> 这个自定义 Hook、以替代 <code>useDispatch</code>。</p>
<p><code>authSlice.js</code>:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { createSlice } <span class="keyword">from</span> <span class="string">"@reduxjs/toolkit"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> authSlice = <span class="title function_">createSlice</span>({</span><br><span class="line"> <span class="attr">name</span>: <span class="string">"auth"</span>,</span><br><span class="line"> <span class="attr">initialState</span>: {</span><br><span class="line"> <span class="attr">isAuthenticated</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">user</span>: {},</span><br><span class="line"> <span class="attr">error</span>: <span class="literal">null</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">reducers</span>: {</span><br><span class="line"> <span class="attr">setCurrentUser</span>: <span class="function">(<span class="params">state, action</span>) =></span> {</span><br><span class="line"> state.<span class="property">isAuthenticated</span> = <span class="literal">true</span>;</span><br><span class="line"> state.<span class="property">user</span> = action.<span class="property">payload</span>;</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">setError</span>: <span class="function">(<span class="params">state, action</span>) =></span> {</span><br><span class="line"> state.<span class="property">error</span> = action.<span class="property">payload</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> { setCurrentUser, setError } = authSlice.<span class="property">actions</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> authSlice.<span class="property">reducer</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>authSlice.ts</code>:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { createSlice, <span class="title class_">PayloadAction</span> } <span class="keyword">from</span> <span class="string">'@reduxjs/toolkit'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">'../interfaces'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">AuthState</span> {</span><br><span class="line"> <span class="attr">isAuthenticated</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">user</span>: <span class="title class_">User</span> | <span class="literal">null</span>;</span><br><span class="line"> <span class="attr">error</span>: <span class="built_in">string</span> | <span class="literal">null</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">initialState</span>: <span class="title class_">AuthState</span> = {</span><br><span class="line"> <span class="attr">isAuthenticated</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">user</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">error</span>: <span class="literal">null</span>,</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> authSlice = <span class="title function_">createSlice</span>({</span><br><span class="line"> <span class="attr">name</span>: <span class="string">'auth'</span>,</span><br><span class="line"> initialState,</span><br><span class="line"> <span class="attr">reducers</span>: {</span><br><span class="line"> <span class="attr">setCurrentUser</span>: <span class="function">(<span class="params">state, <span class="attr">action</span>: <span class="title class_">PayloadAction</span><<span class="title class_">User</span>></span>) =></span> {</span><br><span class="line"> state.<span class="property">isAuthenticated</span> = <span class="literal">true</span>;</span><br><span class="line"> state.<span class="property">user</span> = action.<span class="property">payload</span>;</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">setError</span>: <span class="function">(<span class="params">state, <span class="attr">action</span>: <span class="title class_">PayloadAction</span><<span class="built_in">string</span>></span>) =></span> {</span><br><span class="line"> state.<span class="property">error</span> = action.<span class="property">payload</span>;</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> { setCurrentUser, setError } = authSlice.<span class="property">actions</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> authSlice.<span class="property">reducer</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>authSlice.js</code> 和 <code>authSlice.ts</code> 的区别在于 <code>action</code> 的类型声明。TypeScript 是 JavaScript 的超集,目的是为了更好地进行静态类型检查,以避免各种各样的错误。要知道 JavaScript 是弱类型语言,这意味着你可以在不同的地方使用不同的类型而不报错。</p>
<p><code>interface</code> 关键字用于定义一个接口,接口是一种抽象的结构、定义了一个对象应该具有的属性和方法。<code>AuthState</code> 接口被定义后,有三个属性:<code>isAuthenticated</code>、<code>user</code> 和 <code>error</code>。然后设置 <code>initialState</code> 为 <code>AuthState</code> 类型。</p>
<p>也就是说 <code>initialState</code> 无论怎么改动,都必须符合 <code>AuthState</code> 的结构。假设我在 <code>initialState</code> 的 <code>isAuthenticated</code> 属性后面加了一个 <code>isRegistered</code> 属性,那么在 <code>reducers</code> 中的 <code>state</code> 就会报错,因为 <code>isRegistered</code> 属性并不在 <code>AuthState</code> 中。</p>
<p><code>setCurrentUser</code> 和 <code>setError</code> 的 <code>action</code> 参数都是 <code>PayloadAction</code> 类型,<code>PayloadAction</code> 是一个泛型接口,接受一个类型参数,这个类型参数就是 <code>action.payload</code> 的类型。这样一来,<code>action.payload</code> 的类型就被限制了,不会出现不符合预期的情况。</p>
<p>比方说 <code>setError</code> 的 <code>action</code> 参数就被限制为 <code>string</code> 类型。</p>
<p>我还新建了一个 <code>interfaces.ts</code> 文件来存放会被多个文件引用的接口:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">User</span> {</span><br><span class="line"> <span class="attr">id</span>?: <span class="built_in">number</span>;</span><br><span class="line"> <span class="attr">emailAddress</span>?: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">username</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">password</span>?: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">access_token</span>?: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>属性后面的 <code>?</code> 表示这个属性是可选的。想象一下,<code>User</code> 接口会被注册、登录以及登出的组件引用:</p>
<ul>
<li>注册时,用户传来的数据中不会有 <code>id</code>、<code>access_token</code> 这两个属性</li>
<li>登录时,用户传来的数据中不会有 <code>emailAddress</code> 这个属性</li>
<li>登出时,用户传来的数据中不会有 <code>password</code> 这个属性</li>
</ul>
<p>所以这些属性都是可选的。</p>
<h2 id="axios"><a class="markdownIt-Anchor" href="#axios"></a> Axios</h2>
<p>我本来是全程使用 Socket.io 来进行通信,这也包括了登录、注册等操作。但是 Socket.io 并不适合用来做这些操作,所以我还是用了 Axios。</p>
<p>比方说注册用户,使用 Socket.io 的话就是这样:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">registerUser</span> = (<span class="params">userData, navigate</span>) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">dispatch</span> =></span> {</span><br><span class="line"> socket.<span class="title function_">emit</span>(<span class="string">"register"</span>, userData);</span><br><span class="line"> </span><br><span class="line"> socket.<span class="title function_">on</span>(<span class="string">"newRegisteredUser"</span>, <span class="function"><span class="params">data</span> =></span> {</span><br><span class="line"> data.<span class="property">status</span> === <span class="string">"00000"</span> ? <span class="title function_">navigate</span>(<span class="string">"/login"</span>) : <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">message</span>);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p>先发送 <code>register</code> 事件,然后接收从服务端发来的 <code>newRegisteredUser</code> 事件,根据 <code>data.status</code> 的值来决定跳转到登录页面还是打印错误信息。</p>
<p>换成 Axios 的话,要先创建一个服务:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">'axios'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">"../interfaces"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> api = axios.<span class="title function_">create</span>({</span><br><span class="line"> <span class="attr">baseURL</span>: <span class="string">'http://localhost:4000/api'</span>,</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UsersService</span> = {</span><br><span class="line"> <span class="attr">register</span>: <span class="function">(<span class="params"><span class="attr">data</span>: <span class="title class_">User</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> api.<span class="title function_">post</span>(<span class="string">'/users/register'</span>, data);</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="attr">login</span>: <span class="function">(<span class="params"><span class="attr">data</span>: <span class="title class_">User</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> api.<span class="title function_">post</span>(<span class="string">'/users/login'</span>, data);</span><br><span class="line"> },</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>使用 <code>baseURL</code> 的好处是如果后端地址改变、只需要改动一次就行了。</p>
<p><code>UsersService</code> 对象中有两个方法:<code>register</code> 和 <code>login</code>,分别用于注册和登录。当我们需要向后端发送请求时,只需要调用这些方法即可:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">registerUser</span> = (<span class="params"><span class="attr">userData</span>: <span class="title class_">User</span>, <span class="attr">navigate</span>: (path: <span class="built_in">string</span>) => <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_">register</span>(userData);</span><br><span class="line"> <span class="keyword">const</span> data = response.<span class="property">data</span>;</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">status</span> === <span class="string">"00000"</span>) <span class="title function_">navigate</span>(<span class="string">"/login"</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(error);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>我还添加了一个 <code>try...catch</code> 语句,用于捕获请求失败的情况。</p>
<h2 id="登录和注册"><a class="markdownIt-Anchor" href="#登录和注册"></a> 登录和注册</h2>
<p>在写函数组件时,需要定义函数组件的类型:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Register</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>如果该函数组件还有 props,那么就需要定义 props 的类型:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">PrivateMessageHomepageProps</span> {</span><br><span class="line"> <span class="attr">style</span>: <span class="title class_">React</span>.<span class="property">CSSProperties</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PrivateMessageHomepage</span>: <span class="title class_">React</span>.<span class="property">FC</span><<span class="title class_">PrivateMessageHomepageProps</span>> = <span class="function">(<span class="params">{ style }</span>) =></span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>例如这里的 <code>PrivateMessageHomepage</code> 组件有一个 <code>style</code> 属性,所以需要定义其类型为 <code>React.CSSProperties</code>,毕竟是一个 CSS 样式对象嘛。</p>
</blockquote>
<p>之前讲到共用的 <code>User</code> 接口,但对于注册来说还需要 4 个必需的属性:<code>emailAddress</code>、<code>birthYear</code>、<code>birthMonth</code> 和 <code>birthDay</code>。其中 <code>emailAddress</code> 虽在 <code>User</code> 接口中,但是是可选的,不符合注册的要求。</p>
<p>所以我们可以使用 <code>&</code> 运算符来合并两个接口:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span> } <span class="keyword">from</span> <span class="string">"./utils/interfaces"</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">RegisterUser</span> = <span class="title class_">User</span> & {</span><br><span class="line"> <span class="attr">emailAddress</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">birthYear</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">birthMonth</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">birthDay</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>RegisterUser</code> 接口继承了 <code>User</code> 接口,并添加了 4 个必需的属性。后加的属性会覆盖前面的属性,所以 <code>User</code> 接口中的 <code>emailAddress</code> 属性被覆盖了。</p>
<p>之后再使用 <code>useState</code> 来定义 <code>userData</code>:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [userData, setUserData] = useState<<span class="title class_">RegisterUser</span>>({</span><br><span class="line"> <span class="attr">username</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">emailAddress</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">password</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">birthYear</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">birthMonth</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">birthDay</span>: <span class="string">""</span></span><br><span class="line"> });</span><br></pre></td></tr></tbody></table></figure>
<p>登录时我们不需要 <code>User</code> 接口中其他的属性,只需要 <code>username</code> 和 <code>password</code> 属性。所以我们可以挑选出需要的属性:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">LoginUser</span> = <span class="title class_">Pick</span><<span class="title class_">User</span>, <span class="string">"username"</span> | <span class="string">"password"</span>>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>Pick</code> 的用处是从一个对象中挑选出一些属性,返回一个新的对象。这意味着 <code>LoginUser</code> 接口只包含 <code>User</code> 接口中的 <code>username</code> 和 <code>password</code> 属性。</p>
<blockquote>
<p>像是需要展现出用户名的地方我们也可以这样写:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">UserProfile</span> = <span class="title class_">Pick</span><<span class="title class_">User</span>, <span class="string">"username"</span>>;</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<h2 id="页面跳转"><a class="markdownIt-Anchor" href="#页面跳转"></a> 页面跳转</h2>
<p>最前面提及到的 <code>Guard</code> 组件是用来判断用户是否登录的,如果用户没有登录,那么就跳转到登录页面:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Navigate</span>, <span class="title class_">Outlet</span> } <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Guard</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> auth = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">"auth"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (auth) <span class="keyword">return</span> <span class="language-xml"><span class="tag"><<span class="name">Outlet</span>/></span></span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="language-xml"><span class="tag"><<span class="name">Navigate</span> <span class="attr">to</span>=<span class="string">"/login"</span> /></span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Guard</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>如果 <code>localStorage</code> 中有 <code>auth</code> 这个键,那么就渲染 <code>Outlet</code> 组件,否则就跳转到登录页面。</p>
<p><code>Outlet</code> 组件是用来渲染子路由的。回到 <code>App.tsx</code>,能看到 <code>/channels/@me</code> 路由被 <code>Guard</code> 组件保护,子路由分别是默认的 <code>Home</code> 组件和 <code>dummy</code> 子路由。</p>
<p>也就是说用户在登陆后跳转到 <code>/channels/@me</code> 路由,会被 <code>Guard</code> 组件验证,然后渲染 <code>Home</code> 组件。</p>
<p><code>/channels/@me/dummy</code> 路由是用来测试私聊的,但是它也在 <code>Guard</code> 组件的保护之下。</p>
<h2 id="其他"><a class="markdownIt-Anchor" href="#其他"></a> 其他</h2>
<p>我的项目中有一些按钮在被鼠标悬停时会有一些样式变化,原先的逻辑是这样的:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [hoverStates, setHoverStates] = <span class="title function_">useState</span>({});</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">updateHoverState</span> = (<span class="params">item, isHovered</span>) => {</span><br><span class="line"> <span class="title function_">setHoverStates</span>(<span class="function"><span class="params">prev</span> =></span> ({ ...prev, [item]: isHovered }));</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>转换到 TypeScript 后:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [hoverStates, setHoverStates] = useState<<span class="title class_">Record</span><<span class="built_in">string</span>, <span class="built_in">boolean</span>>>({});</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">updateHoverState</span> = (<span class="params"><span class="attr">item</span>: <span class="built_in">string</span>, <span class="attr">isHovered</span>: <span class="built_in">boolean</span></span>) => {</span><br><span class="line"> <span class="title function_">setHoverStates</span>({</span><br><span class="line"> ...hoverStates,</span><br><span class="line"> [item]: isHovered</span><br><span class="line"> });</span><br><span class="line"> }</span><br></pre></td></tr></tbody></table></figure>
<p><code>Record</code> 的第一个参数定义了键的类型,第二个参数定义了值的类型。<code>hoverStates</code> 被定义为一个字典,键和值再被定义为 <code>string</code> 和 <code>boolean</code> 类型。</p>
<p>输入框组件里分别有着:</p>
<ul>
<li>判断用户是否按下了回车键的 <code>handleKeyDown</code> 函数</li>
<li>处理用户输入信息的 <code>handleChange</code> 函数</li>
<li>处理用户提交表单的 <code>handleSendMessage</code> 函数</li>
</ul>
<p>这些函数都需要定义类型:</p>
<figure class="highlight tsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handleKeyDown</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">KeyboardEvent</span></span>) => {}</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><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleSendMessage</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">FormEvent</span></span>) => {}</span><br></pre></td></tr></tbody></table></figure>
<h1 id="后端"><a class="markdownIt-Anchor" href="#后端"></a> 后端</h1>
<p>NestJS 是一个基于 Node.JS 的后端框架,它使用 TypeScript 编写,提供了一些装饰器来简化开发。</p>
<p>我目前有的依赖:</p>
<figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"@nestjs/common"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/core"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/jwt"</span><span class="punctuation">:</span> <span class="string">"^10.2.0"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/mapped-types"</span><span class="punctuation">:</span> <span class="string">"*"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/mongoose"</span><span class="punctuation">:</span> <span class="string">"^10.0.4"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/platform-express"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/platform-socket.io"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/typeorm"</span><span class="punctuation">:</span> <span class="string">"^10.0.2"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"@nestjs/websockets"</span><span class="punctuation">:</span> <span class="string">"^10.3.3"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bcrypt"</span><span class="punctuation">:</span> <span class="string">"^5.1.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"class-validator"</span><span class="punctuation">:</span> <span class="string">"^0.14.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"cookie-parser"</span><span class="punctuation">:</span> <span class="string">"^1.4.6"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"dotenv"</span><span class="punctuation">:</span> <span class="string">"^16.4.5"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"mongoose"</span><span class="punctuation">:</span> <span class="string">"^8.2.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"morgan"</span><span class="punctuation">:</span> <span class="string">"^1.10.0"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"reflect-metadata"</span><span class="punctuation">:</span> <span class="string">"^0.2.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"rxjs"</span><span class="punctuation">:</span> <span class="string">"^7.8.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"typeorm"</span><span class="punctuation">:</span> <span class="string">"^0.3.20"</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
<p>NestJS 的入口文件是 <code>main.ts</code>:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> dotenv <span class="keyword">from</span> <span class="string">'dotenv'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Server</span> } <span class="keyword">from</span> <span class="string">'socket.io'</span></span><br><span class="line"></span><br><span class="line">dotenv.<span class="title function_">config</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="title function_">create</span>(<span class="title class_">AppModule</span>)</span><br><span class="line"> app.<span class="title function_">enableCors</span>({</span><br><span class="line"> <span class="attr">origin</span>: process.<span class="property">env</span>.<span class="property">CLIENT_ORIGIN</span> || <span class="string">'http://localhost:3000'</span>,</span><br><span class="line"> <span class="attr">credentials</span>: <span class="literal">true</span>,</span><br><span class="line"> })</span><br><span class="line"> </span><br><span class="line"> app.<span class="title function_">setGlobalPrefix</span>(<span class="string">'api'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> server = app.<span class="title function_">getHttpServer</span>()</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Server</span>(server)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(process.<span class="property">env</span>.<span class="property">PORT</span> || <span class="number">4000</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="title function_">bootstrap</span>().<span class="title function_">catch</span>(<span class="function">(<span class="params">err</span>) =></span> <span class="variable language_">console</span>.<span class="title function_">error</span>(err))</span><br></pre></td></tr></tbody></table></figure>
<p><code>dotenv</code> 是用来读取 <code>.env</code> 文件的,<code>.env</code> 文件用来存放环境变量。<code>CLIENT_ORIGIN</code> 是前端的地址,<code>PORT</code> 是后端的端口。</p>
<p>因为前后端分离的项目中,前端和后端是不同的域名,所以会有跨域问题。<code>app.enableCors</code> 方法用来解决跨域问题,<code>origin</code> 参数是前端的地址,<code>credentials</code> 参数是 <code>true</code> 表示允许携带 cookie。</p>
<p><code>app.setGlobalPrefix</code> 方法用来设置全局前缀,所有的路由都会加上这个前缀。比方说后面设置的 <code>/users/register</code> 路由会变成 <code>/api/users/register</code>。</p>
<p><code>app.getHttpServer</code> 方法返回一个 <code>http.Server</code> 实例,<code>new Server(server)</code> 用来创建一个 Socket.io 服务器。</p>
<p>最后调用 <code>app.listen</code> 方法来启动服务器。</p>
<h2 id="nestjs概念"><a class="markdownIt-Anchor" href="#nestjs概念"></a> NestJS 概念</h2>
<p>NestJS 目前支持两个 HTTP 平台:Express 和 Fastify。这是因为 NestJS 的开发团队认为 NestJS 立志于成为一个模块化的框架,不单单是一个 HTTP 框架。只要创建了适配器,NestJS 就可以在任何平台上运行。</p>
<p>NestJS 的核心概念有:</p>
<ul>
<li>控制器(Controller)</li>
<li>服务(Service)</li>
<li>模块(Module)</li>
</ul>
<h4 id="控制器"><a class="markdownIt-Anchor" href="#控制器"></a> 控制器</h4>
<p>控制器是处理传入请求的地方,它们会调用服务来完成请求。控制器的方法可以使用装饰器来定义路由。</p>
<figure class="highlight ts"><figcaption><span>app.controller.ts</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Controller</span>, <span class="title class_">Get</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppService</span> } <span class="keyword">from</span> <span class="string">'./app.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppController</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">appService</span>: <span class="title class_">AppService</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Get</span>()</span><br><span class="line"> <span class="title function_">getHello</span>(): <span class="built_in">string</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">appService</span>.<span class="title function_">getHello</span>()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Controller</code> 装饰器用来定义一个控制器。假设我们的后端地址是 <code>http://localhost:4000</code>,那么 <code>@Controller()</code> 装饰器的参数就是 <code>http://localhost:4000</code>。</p>
<p><code>@Get()</code> 装饰器用来定义一个 GET 请求,这个请求的路径就是控制器的路径,也就是请求 <code>http://localhost:4000</code>。</p>
<p>假设我想要请求 <code>http://localhost:4000/api/users/register</code>,那么就要写成:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@Get</span>(<span class="string">'register'</span>)</span><br></pre></td></tr></tbody></table></figure>
<p>为什么不写成 <code>@Controller('api/users')</code> 呢?因为全局前缀已经被我们设置为 <code>api</code> 了。</p>
<h4 id="服务"><a class="markdownIt-Anchor" href="#服务"></a> 服务</h4>
<p>刚才的控制器中有一个 <code>AppService</code> 服务,服务是处理业务逻辑的地方。服务可以被控制器调用,也可以被其他服务调用。</p>
<figure class="highlight ts"><figcaption><span>app.service.ts</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppService</span> {</span><br><span class="line"> <span class="title function_">getHello</span>(): <span class="built_in">string</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'Welcome to HotaruTS!!'</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Injectable</code> 装饰器用来定义一个服务。服务中的方法可以被其他服务调用,也可以被控制器调用。</p>
<p>刚才调用的是 <code>getHello</code> 方法,返回的是一个字符串。如果使用 Postman 请求 <code>http://localhost:4000</code>,响应的内容就是 <code>Welcome to HotaruTS!!</code>。</p>
<h4 id="模块"><a class="markdownIt-Anchor" href="#模块"></a> 模块</h4>
<p>模块是一个用来组织应用程序的地方,每个应用程序至少有一个根模块。模块中可以包含控制器、服务、提供器等。</p>
<p>根模块可以看成是 Express 里的 <code>app</code> 对象,它是所有模块的入口。</p>
<p>先来看一下我 Express 项目中的 <code>app.js</code>:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> createError = <span class="built_in">require</span>(<span class="string">'http-errors'</span>);</span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">'express'</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> cookieParser = <span class="built_in">require</span>(<span class="string">'cookie-parser'</span>);</span><br><span class="line"><span class="keyword">const</span> logger = <span class="built_in">require</span>(<span class="string">'morgan'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> indexRouter = <span class="built_in">require</span>(<span class="string">'./routes/index'</span>);</span><br><span class="line"><span class="keyword">const</span> usersRouter = <span class="built_in">require</span>(<span class="string">'./routes/users'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">express</span>();</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">set</span>(<span class="string">'views'</span>, path.<span class="title function_">join</span>(__dirname, <span class="string">'views'</span>));</span><br><span class="line">app.<span class="title function_">set</span>(<span class="string">'view engine'</span>, <span class="string">'pug'</span>);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">logger</span>(<span class="string">'dev'</span>));</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">json</span>());</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">urlencoded</span>({ <span class="attr">extended</span>: <span class="literal">false</span> }));</span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">cookieParser</span>());</span><br><span class="line">app.<span class="title function_">use</span>(express.<span class="title function_">static</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">'public'</span>)));</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="string">'/'</span>, indexRouter);</span><br><span class="line">app.<span class="title function_">use</span>(<span class="string">'/users.js'</span>, usersRouter);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="keyword">function</span>(<span class="params">req, res, next</span>) {</span><br><span class="line"> <span class="title function_">next</span>(<span class="title function_">createError</span>(<span class="number">404</span>));</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="keyword">function</span>(<span class="params">err, req, res, next</span>) {</span><br><span class="line"> res.<span class="property">locals</span>.<span class="property">message</span> = err.<span class="property">message</span>;</span><br><span class="line"> res.<span class="property">locals</span>.<span class="property">error</span> = req.<span class="property">app</span>.<span class="title function_">get</span>(<span class="string">'env'</span>) === <span class="string">'development'</span> ? err : {};</span><br><span class="line"></span><br><span class="line"> res.<span class="title function_">status</span>(err.<span class="property">status</span> || <span class="number">500</span>);</span><br><span class="line"> res.<span class="title function_">render</span>(<span class="string">'error'</span>);</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = app;</span><br></pre></td></tr></tbody></table></figure>
<ol>
<li>导入依赖</li>
<li>创建路由器 <code>indexRouter</code> 和 <code>usersRouter</code></li>
<li>创建 <code>app</code> 对象,也就是 Express 的实例</li>
<li>设置视图引擎和视图路径</li>
<li>使用中间件,分别是:
<ol>
<li><code>logger</code>:记录请求日志,<code>dev</code> 参数表示开发环境</li>
<li><code>express.json</code>:解析 JSON 格式的请求体</li>
<li><code>express.urlencoded</code>:解析 URL 编码的请求体,<code>extended</code> 参数表示是否使用 <code>qs</code> 库</li>
<li><code>cookieParser</code>:解析 cookie</li>
<li><code>express.static</code>:设置静态文件目录,也就是 <code>public</code> 目录</li>
</ol>
</li>
<li>配置路由,让 <code>indexRouter</code> 和 <code>usersRouter</code> 分别处理 <code>/</code> 和 <code>/users</code> 路径</li>
<li>处理 404 错误</li>
<li>处理其他错误</li>
<li>导出 <code>app</code> 对象</li>
</ol>
<p>得知了这些,我们就可以仿照 Express 的写法来写 NestJS 的模块。</p>
<p>首先导入依赖:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span>, <span class="title class_">NestModule</span>, <span class="title class_">MiddlewareConsumer</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="variable constant_">APP_FILTER</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongooseModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> express <span class="keyword">from</span> <span class="string">'express'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> cookieParser <span class="keyword">from</span> <span class="string">'cookie-parser'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> morgan <span class="keyword">from</span> <span class="string">'morgan'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppController</span> } <span class="keyword">from</span> <span class="string">'./app.controller'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppService</span> } <span class="keyword">from</span> <span class="string">'./app.service'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AnyExceptionFilter</span> } <span class="keyword">from</span> <span class="string">'./any-exception.filter'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">LoggerMiddleware</span> } <span class="keyword">from</span> <span class="string">'./common/middleware/logger.middleware'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">SocketModule</span> } <span class="keyword">from</span> <span class="string">'./socket/socket.module'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersModule</span> } <span class="keyword">from</span> <span class="string">'./users/users.module'</span></span><br></pre></td></tr></tbody></table></figure>
<p><code>@Module</code> 装饰器可以定义一个模块,参数分别是:</p>
<ul>
<li><code>imports</code>:导入其他模块</li>
<li><code>controllers</code>:控制器</li>
<li><code>providers</code>:提供器</li>
</ul>
<p>因为这个项目的数据库是 MongoDB,所以我导入了 <code>MongooseModule</code> 模块。<code>SocketModule</code> 和 <code>UsersModule</code> 是自定义的模块。</p>
<p><code>UsersModule</code> 后面会详细讲解,<code>SocketModule</code> 等未来写到消息传递时再讲。</p>
<p>控制器就不用多说了,模块本来就是用来组织控制器的。<code>AppModule</code> 中只有一个控制器 <code>AppController</code>。</p>
<p>提供器是一个用来提供服务的地方,服务可以被控制器调用。<code>AppService</code> 就是一个提供器。除此之外,还有一个全局异常过滤器 <code>AnyExceptionFilter</code>。</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">MongooseModule</span>.<span class="title function_">forRoot</span>(process.<span class="property">env</span>.<span class="property">DATABASE_URL</span> || <span class="string">'mongodb://localhost:27017/hotaru'</span>),</span><br><span class="line"> <span class="title class_">SocketModule</span>,</span><br><span class="line"> <span class="title class_">UsersModule</span>,</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">controllers</span>: [<span class="title class_">AppController</span>],</span><br><span class="line"> <span class="attr">providers</span>: [</span><br><span class="line"> <span class="title class_">AppService</span>,</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">provide</span>: <span class="variable constant_">APP_FILTER</span>,</span><br><span class="line"> <span class="attr">useClass</span>: <span class="title class_">AnyExceptionFilter</span>,</span><br><span class="line"> },</span><br><span class="line"> ],</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<p>为 <code>AppModule</code> 添加了一个 <code>configure</code> 方法,这个方法是 <code>NestModule</code> 接口的一个方法,作用是添加中间件。</p>
<p><code>consumer.apply</code> 方法用来添加中间件,参数是一个或多个中间件。这里添加了 <code>morgan</code>、<code>express.json</code>、<code>express.urlencoded</code>、<code>cookieParser</code>、<code>express.static</code> 和 <code>LoggerMiddleware</code> 中间件。</p>
<p><code>forRoutes('*')</code> 表示所有路由都会使用这些中间件。</p>
<p>最终导出 <code>AppModule</code>:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AppModule</span> <span class="keyword">implements</span> <span class="title class_">NestModule</span> {</span><br><span class="line"> <span class="title function_">configure</span>(<span class="params"><span class="attr">consumer</span>: <span class="title class_">MiddlewareConsumer</span></span>) {</span><br><span class="line"> consumer</span><br><span class="line"> .<span class="title function_">apply</span>(</span><br><span class="line"> <span class="title function_">morgan</span>(<span class="string">'dev'</span>),</span><br><span class="line"> express.<span class="title function_">json</span>(),</span><br><span class="line"> express.<span class="title function_">urlencoded</span>({ <span class="attr">extended</span>: <span class="literal">false</span> }),</span><br><span class="line"> <span class="title function_">cookieParser</span>(),</span><br><span class="line"> express.<span class="title function_">static</span>(<span class="string">'public'</span>),</span><br><span class="line"> <span class="title class_">LoggerMiddleware</span>,</span><br><span class="line"> )</span><br><span class="line"> .<span class="title function_">forRoutes</span>(<span class="string">'*'</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这样,我们就完成了一个 NestJS 的模块。</p>
<h2 id="日志中间件"><a class="markdownIt-Anchor" href="#日志中间件"></a> 日志中间件</h2>
<p><code>LoggerMiddleware</code> 中间件是一个自定义的中间件,用来记录请求日志:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span>, <span class="title class_">NestMiddleware</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Request</span>, <span class="title class_">Response</span>, <span class="title class_">NextFunction</span> } <span class="keyword">from</span> <span class="string">'express'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">LoggerMiddleware</span> <span class="keyword">implements</span> <span class="title class_">NestMiddleware</span> {</span><br><span class="line"> <span class="title function_">use</span>(<span class="params"><span class="attr">req</span>: <span class="title class_">Request</span>, <span class="attr">res</span>: <span class="title class_">Response</span>, <span class="attr">next</span>: <span class="title class_">NextFunction</span></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'Request...'</span>, req.<span class="property">method</span>, req.<span class="property">originalUrl</span>);</span><br><span class="line"> <span class="title function_">next</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>目前这个中间件只是简单地打印请求方法和请求路径。</p>
<h2 id="用户模块"><a class="markdownIt-Anchor" href="#用户模块"></a> 用户模块</h2>
<p>用户模块是一个用来处理用户注册、登录、登出的模块。</p>
<p>在 NestJS 里创建一个模块可以使用 CLI:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nest g module <span class="built_in">users</span></span><br></pre></td></tr></tbody></table></figure>
<p>这个命令会在 <code>src</code> 目录下创建一个 <code>users</code> 目录,里面有一个 <code>users.module.ts</code> 文件。</p>
<p>首先我们得知道原先的 Express 项目里,用户注册的逻辑是怎么写的:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line">socket.<span class="title function_">on</span>(<span class="string">"register"</span>, <span class="keyword">async</span> userData => {</span><br><span class="line"> <span class="keyword">const</span> existingUserEmail = <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">findOne</span>({</span><br><span class="line"> <span class="attr">emailAddress</span>: userData.<span class="property">emailAddress</span></span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">const</span> existingUsername = <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">findOne</span>({</span><br><span class="line"> <span class="attr">username</span>: userData.<span class="property">username</span></span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (existingUserEmail || existingUsername) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[U0102] User already exists: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line"> <span class="title class_">User</span>.<span class="title function_">find</span>({}).<span class="title function_">then</span>(<span class="function">(<span class="params">docs</span>) =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(docs);</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function">(<span class="params">err</span>) =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(err);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> socket.<span class="title function_">emit</span>(<span class="string">"newRegisteredUser"</span>, {</span><br><span class="line"> <span class="attr">status</span>: <span class="string">"U0102"</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">"User already exists."</span></span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">await</span> <span class="title class_">User</span>.<span class="title function_">create</span>({</span><br><span class="line"> <span class="attr">emailAddress</span>: userData.<span class="property">emailAddress</span>,</span><br><span class="line"> <span class="attr">username</span>: userData.<span class="property">username</span>,</span><br><span class="line"> <span class="attr">password</span>: userData.<span class="property">password</span>,</span><br><span class="line"> <span class="title class_">DOBYear</span>: userData.<span class="property">birthYear</span>,</span><br><span class="line"> <span class="title class_">DOBMonth</span>: <span class="title class_">MonthToNumber</span>[userData.<span class="property">birthMonth</span>],</span><br><span class="line"> <span class="title class_">DOBDay</span>: userData.<span class="property">birthDay</span></span><br><span class="line"> })</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[00000] User registered: <span class="subst">${userData.username}</span>`</span>);</span><br><span class="line"> </span><br><span class="line"> socketIO.<span class="title function_">emit</span>(<span class="string">"newRegisteredUser"</span>, {</span><br><span class="line"> <span class="attr">status</span>: <span class="string">"00000"</span>,</span><br><span class="line"> <span class="attr">token</span>: <span class="title function_">generateJWT</span>(userData.<span class="property">username</span>)</span><br><span class="line"> });</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>因为使用的是 Socket.io,所以要监听 <code>register</code> 事件,然后验证用户填写的信息。如果用户已经存在,就返回错误信息;如果用户不存在,就创建一个新用户。</p>
<p>其中的状态码是自定义的,采用的是类似于阿里巴巴代码规约的状态码。</p>
<p>在 NestJS 中,鉴于客户端已经改为使用 Axios 这样的 HTTP 库,我们就不再使用 Socket.io 了。先创建一个路径为 <code>/users</code> 的控制器:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersControllers</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">usersService</span>: <span class="title class_">UsersService</span></span>) { }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>constructor</code> 方法中注入了一个私有且只读的 <code>usersService</code> 服务,这意味着这个服务只能在这个控制器中使用。</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Post</span>(<span class="string">'register'</span>)</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">register</span>(<span class="params"><span class="meta">@Body</span>() <span class="attr">registerUserDto</span>: <span class="title class_">RegisterUserDto</span>, <span class="meta">@Res</span>() <span class="attr">res</span>: <span class="title class_">Response</span></span>) {}</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Post('register')</code> 装饰器用来定义一个 POST 请求,请求路径是 <code>/users/register</code>。</p>
<p><code>@Body()</code> 装饰器用来获取请求体,<code>registerUserDto</code> 是一个数据传输对象,包含了用户注册时需要的信息,也就是客户端传来的数据:</p>
<ul>
<li><code>emailAddress</code></li>
<li><code>username</code></li>
<li><code>password</code></li>
<li><code>birthYear</code></li>
<li><code>birthMonth</code></li>
<li><code>birthDay</code></li>
</ul>
<p><code>@Res()</code> 装饰器用来获取响应对象。</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> existingUser = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">findByEmail</span>(registerUserDto.<span class="property">emailAddress</span>)</span><br><span class="line"><span class="keyword">if</span> (existingUser) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">HttpException</span>(<span class="string">'Email address already in use'</span>, <span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!registerUserDto.<span class="property">emailAddress</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">HttpException</span>(<span class="string">'Email address is required'</span>, <span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> user = <span class="variable language_">this</span>.<span class="property">usersService</span>.<span class="title function_">register</span>(registerUserDto)</span><br><span class="line"></span><br><span class="line"> res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">OK</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'00000'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'User registered successfully'</span>,</span><br><span class="line"> <span class="attr">user</span>: user,</span><br><span class="line"> })</span><br><span class="line">} <span class="keyword">catch</span> (error) {</span><br><span class="line"> res.<span class="title function_">status</span>(<span class="title class_">HttpStatus</span>.<span class="property">BAD_REQUEST</span>).<span class="title function_">json</span>({</span><br><span class="line"> <span class="attr">status</span>: <span class="string">'U0100'</span>,</span><br><span class="line"> <span class="attr">message</span>: <span class="string">'Failed to register user'</span>,</span><br><span class="line"> })</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>首先调用 <code>this.usersService.findByEmail</code> 方法来查找用户是否已经存在,如果存在就返回错误信息。接着判断用户填写的信息是否完整,如果不完整就返回错误信息。</p>
<p>最后调用 <code>this.usersService.register</code> 方法来注册用户,如果注册成功就返回成功信息,否则返回错误信息。</p>
<p>每次返回响应时都要设置状态码,比方说请求成功时写的 <code>HttpStatus.OK</code>,请求失败时写的 <code>HttpStatus.BAD_REQUEST</code>。尽管已经自定义了一套状态码,但是还是要遵循 HTTP 协议的状态码,谁叫我们是用 HTTP 协议的呢。</p>
<p>那么 <code>UsersService</code> 里到底是什么样的呢?</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span>, <span class="title class_">UnauthorizedException</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">InjectModel</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { v4 <span class="keyword">as</span> uuidv4 } <span class="keyword">from</span> <span class="string">'uuid'</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> bcrypt <span class="keyword">from</span> <span class="string">'bcrypt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Model</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">RegisterUserDto</span>, <span class="title class_">LoginUserDto</span> } <span class="keyword">from</span> <span class="string">'./dto'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span>, <span class="title class_">UserDocument</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersService</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="meta">@InjectModel</span>(User.name) <span class="keyword">private</span> <span class="attr">usersModel</span>: <span class="title class_">Model</span><<span class="title class_">UserDocument</span>>,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="attr">jwtService</span>: <span class="title class_">JwtService</span>,</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">register</span>(<span class="attr">registerUserDto</span>: <span class="title class_">RegisterUserDto</span>): <span class="title class_">Promise</span><<span class="built_in">void</span> | <span class="title class_">User</span>> {</span><br><span class="line"> <span class="keyword">const</span> id = <span class="title function_">uuidv4</span>()</span><br><span class="line"> <span class="keyword">const</span> hashedPassword = <span class="keyword">await</span> bcrypt.<span class="title function_">hash</span>(registerUserDto.<span class="property">password</span>, <span class="number">10</span>)</span><br><span class="line"> <span class="keyword">const</span> newUser = <span class="keyword">new</span> <span class="variable language_">this</span>.<span class="title function_">usersModel</span>({</span><br><span class="line"> id,</span><br><span class="line"> ...registerUserDto,</span><br><span class="line"> <span class="attr">password</span>: hashedPassword,</span><br><span class="line"> })</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> <span class="attr">savedUser</span>: <span class="built_in">void</span> | <span class="title class_">User</span></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> savedUser = <span class="keyword">await</span> newUser.<span class="title function_">save</span>().<span class="title function_">then</span>(<span class="function">() =></span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'User registered successfully'</span>))</span><br><span class="line"> } <span class="keyword">catch</span> (err) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(err)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> savedUser</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findByEmail</span>(<span class="attr">emailAddress</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span><<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></pre></td></tr></tbody></table></figure>
<p><code>@InjectModel(User.name)</code> 注入了一个 Mongoose 模型,用来操作数据库。下面的 <code>jwtService</code> 是用来生成 JWT 的服务,以后再聊聊 JWT。</p>
<p><code>register</code> 方法中我们先用 <code>uuidv4</code> 方法生成一个唯一的 ID,然后用 <code>bcrypt</code> 库对密码进行加密。接着使用 <code>new</code> 关键字创建一个用户实例、传入所有创建用户时需要的信息。 最后调用 <code>save</code> 方法保存用户信息,如果保存成功就返回用户信息,否则返回 <code>void</code>。</p>
<p><code>findByEmail</code> 方法用来查找用户是否已经存在,如果存在就返回用户信息,否则返回 <code>null</code>。</p>
<blockquote>
<p>现在已经看到了很多次 <code>...Dto</code>,这是什么呢?</p>
<p>DTO 的全程为 Data Transfer Object,数据传输对象。它是一个用来传输数据的对象,通常用来传输数据给服务端或者从服务端传输数据给客户端。在 NestJS 中,DTO 是一个用来定义数据结构的类,用来规范数据的传输。</p>
<p>例如 <code>RegisterUserDto</code> 类用来定义用户注册时需要的信息:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">IsEmail</span>, <span class="title class_">IsNotEmpty</span>, <span class="title class_">IsString</span> } <span class="keyword">from</span> <span class="string">'class-validator'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">RegisterUserDto</span> {</span><br><span class="line"> <span class="meta">@IsEmail</span>()</span><br><span class="line"> <span class="attr">emailAddress</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsString</span>()</span><br><span class="line"> <span class="meta">@MinLength</span>(<span class="number">6</span>)</span><br><span class="line"> <span class="attr">username</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsString</span>()</span><br><span class="line"> <span class="meta">@MinLength</span>(<span class="number">8</span>)</span><br><span class="line"> <span class="attr">password</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsString</span>()</span><br><span class="line"> <span class="attr">birthYear</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsString</span>()</span><br><span class="line"> <span class="attr">birthMonth</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsString</span>()</span><br><span class="line"> <span class="attr">birthDay</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这里使用了多个装饰器来定义每个属性的类型,有助于进行数据验证。</p>
</blockquote>
<p><code>UserSchema</code> 则是用来定义用户模型的,是 MongoDB 要求的数据结构:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Prop</span>, <span class="title class_">Schema</span>, <span class="title class_">SchemaFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Document</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">UserDocument</span> = <span class="title class_">User</span> & <span class="title class_">Document</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Schema</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">User</span> {</span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span>, <span class="attr">unique</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">username</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span>, <span class="attr">unique</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">emailAddress</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">true</span> })</span><br><span class="line"> <span class="attr">password</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line"> <span class="attr">birthYear</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line"> <span class="attr">birthMonth</span>: <span class="built_in">string</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Prop</span>({ <span class="attr">required</span>: <span class="literal">false</span> })</span><br><span class="line"> <span class="attr">birthDay</span>: <span class="built_in">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UserSchema</span> = <span class="title class_">SchemaFactory</span>.<span class="title function_">createForClass</span>(<span class="title class_">User</span>)</span><br></pre></td></tr></tbody></table></figure>
<p><code>@Prop</code> 装饰器用来定义一个属性,<code>@Schema</code> 装饰器用来定义一个模式。<code>UserDocument</code> 是一个用户文档,继承了 <code>User</code> 和 <code>Document</code>。</p>
<p>这里我们定义的属性和 <code>RegisterUserDto</code> 中的属性是一样的,但 <code>UserSchema</code> 注重于定义会被存储在数据库中的数据结构,<code>RegisterUserDto</code> 注重于定义会在客户端和服务端之间传输的数据结构。</p>
<p>最终,我们在 <code>UsersModule</code> 中导入这些模块:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Module</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongooseModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">JwtService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/jwt'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">User</span>, <span class="title class_">UserSchema</span> } <span class="keyword">from</span> <span class="string">'./user.schema'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersControllers</span> } <span class="keyword">from</span> <span class="string">'./users.controllers'</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">UsersService</span> } <span class="keyword">from</span> <span class="string">'./users.service'</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [<span class="title class_">MongooseModule</span>.<span class="title function_">forFeature</span>([{ <span class="attr">name</span>: <span class="title class_">User</span>.<span class="property">name</span>, <span class="attr">schema</span>: <span class="title class_">UserSchema</span> }])],</span><br><span class="line"> <span class="attr">controllers</span>: [<span class="title class_">UsersControllers</span>],</span><br><span class="line"> <span class="attr">providers</span>: [<span class="title class_">UsersService</span>, <span class="title class_">JwtService</span>],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UsersModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>不要忘了,新建的模块要在 <code>AppModule</code> 中导入:</p>
<figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Module</span>({ <span class="attr">imports</span>: [<span class="title class_">UsersModule</span>] })</span><br></pre></td></tr></tbody></table></figure>
</div>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="a039.html">上一篇</a><a class="next" href="2537.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/3b97.html" data-full-url="https://cytrogen.icu/posts/3b97.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>