<!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 购物平台练习【2】后端项目框架搭建 · Cytrogen 的个人博客</title><meta name="description" content="本文是 React + NestJS 全栈购物平台实践的第二篇,专注于从零搭建一个生产级的 NestJS 后端项目框架。教程详细讲解了如何通过 @nestjs/config 和 Joi 实现类型安全的环境变量配置;如何使用 TypeORM 动态连接数据库;以及如何集成 Swagger 快速生成 API 文档。此外,文章还介绍了如何配置 helmet 安全防护、compression 响应压缩、@nestjs/throttler 速率限制等核心中间件,并重点演示了如何使用 Winston 构建一个支持分级、文件轮转和彩色输出的专业日志系统。本教程为启动一个健壮、可维护的 NestJS 应用提供了全方位的基础设施搭建指南。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/8e94.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/8e94.html">永久链接</a><div class="p-summary visually-hidden"><p>搭建基础的 NestJS 项目框架,包括以下内容:</p>
<ul>
<li>初始化 NestJS 项目</li>
<li>配置环境变量</li>
<li>配置数据库连接</li>
<li>配置 Swagger 文档</li>
<li>设置基础中间件</li>
<li>配置日志系统</li>
</ul></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 购物平台练习【2】后端项目框架搭建</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-11-04T05:00:00.000Z">11/4/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.997Z"></time></div><div class="post-content e-content"><html><head></head><body><p>搭建基础的 NestJS 项目框架,包括以下内容:</p>
<ul>
<li>初始化 NestJS 项目</li>
<li>配置环境变量</li>
<li>配置数据库连接</li>
<li>配置 Swagger 文档</li>
<li>设置基础中间件</li>
<li>配置日志系统</li>
</ul>
<span id="more"></span>
<h1 id="1-初始化-nestjs-项目"><a class="markdownIt-Anchor" href="#1-初始化-nestjs-项目"></a> 1. 初始化 NestJS 项目</h1>
<h2 id="11-安装-nestjs-cli-工具"><a class="markdownIt-Anchor" href="#11-安装-nestjs-cli-工具"></a> 1.1. 安装 NestJS CLI 工具</h2>
<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">yarn global add @nestjs/cli</span><br></pre></td></tr></tbody></table></figure>
<p>确保全局安装的包路径已被添加到环境变量 <code>PATH</code> 中,否则无法在终端中使用 <code>nest</code> 命令。</p>
<p>可以使用以下命令查看 Yarn 的全局包路径:</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">yarn global bin</span><br></pre></td></tr></tbody></table></figure>
<p>将输出的路径添加到 <code>PATH</code> 中。</p>
<h2 id="12-创建新的-nestjs-项目"><a class="markdownIt-Anchor" href="#12-创建新的-nestjs-项目"></a> 1.2. 创建新的 NestJS 项目</h2>
<p>执行以下命令,创建一个名为 <code>shopping-nest-server</code> 的新项目:</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 new shopping-nest-server</span><br></pre></td></tr></tbody></table></figure>
<p>运行后,NestJS CLI 会提示选择包管理工具,选择 <code>Yarn</code>,等待其自动安装所需的依赖。</p>
<p>项目生成后,NestJS CLI 默认会在根目录下生成一个 <code>test</code> 目录和一些单元测试文件(<code>.spec.ts</code>)。我暂时不需要测试,所以删去了这些内容。</p>
<p>NestJS CLI 创建的 NestJS 项目会自带 ESLint 和 Prettier,我们只需要将 <a href="/6d86">上一章</a> 配置好的 <code>.prettierrc</code> 复制过来、确保前端后端都遵循相同的代码规范即可:</p>
<figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"printWidth"</span><span class="punctuation">:</span> <span class="number">120</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"tabWidth"</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"useTabs"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"semi"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"singleQuote"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"trailingComma"</span><span class="punctuation">:</span> <span class="string">"none"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bracketSpacing"</span><span class="punctuation">:</span><span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bracketSameLine"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"arrowParens"</span><span class="punctuation">:</span> <span class="string">"avoid"</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
<h1 id="2-配置环境变量和配置文件"><a class="markdownIt-Anchor" href="#2-配置环境变量和配置文件"></a> 2. 配置环境变量和配置文件</h1>
<p>在实际配置前,我们先来考虑一下,什么是需要配置的:</p>
<ol>
<li>
<p>应用基本配置</p>
<ul>
<li><code>APP_PORT</code>:定义应用监听的端口号</li>
<li><code>APP_ENV</code>:表示当前的应用环境。应用将根据环境做出一些环境特定的设置,比方说日志的详细级别</li>
</ul>
</li>
<li>
<p>核心数据库配置</p>
<p>数据库是应用的核心部分,用于存储应用的持久化数据。</p>
<ul>
<li><code>DB_TYPE</code>:数据库的类型。我选择使用 <code>mysql</code> 或者 <code>postgres</code></li>
<li><code>DB_HOST</code>、<code>DB_PORT</code>、<code>DB_USER</code>、<code>DB_PASSWORD</code>、<code>DB_NAME</code>:这些参数确保应用能连接到正确的数据库实例。不同环境中的数据库配置往往不尽相同,开发环境中可能连接到本地数据库、生产环境中可能连接到远程数据库</li>
</ul>
</li>
<li>
<p>缓存配置</p>
<p>缓存系统是提升应用性能的重要手段。</p>
<p>Redis 是一个高效的内存缓存数据库,常用于缓存频繁访问的数据,从而减轻数据库负载、提升响应速度。</p>
<ul>
<li><code>REDIS_HOST</code>、<code>REDIS_PORT</code>:让应用访问正确的 Redis 实例</li>
<li><code>REDIS_PASSWORD</code> 部分 Redis 服务需要密码验证,通过密码可以保障缓存数据的安全</li>
<li><code>REDIS_DB</code>:指定 Redis 数据库编号,有助于在同一 Redis 实例中隔离不同用途的数据</li>
</ul>
</li>
<li>
<p>Elasticsearch 配置</p>
<p>Elasticsearch 是一个分布式搜索和分析引擎,适用于海量数据的全文检索和分析。</p>
<ul>
<li><code>ELASTICSEARCH_HOST</code>、<code>ELASTICSEARCH_PORT</code>:定义了 Elasticsearch 服务的位置和端口</li>
<li><code>ELASTICSEARCH_USERNAME</code>、<code>ELASTICSEARCH_PASSWORD</code>:通过用户名和密码的方式实现对 Elasticsearch 集群的访问控制</li>
<li><code>ELASTICSEARCH_INDEX</code>:定义应用使用的索引。索引类似于数据库中的表,是 Elasticsearch 存储和查询数据的基本单位</li>
</ul>
</li>
<li>
<p>文件存储配置</p>
<p>对于需要上传文件(如用户头像、商品图片等)的应用来说,文件存储服务是不可或缺的。</p>
<p>很多应用会选择云存储服务(如 Amazon S3、Aliyun OSS)或者本地可用的 MinIO 来满足存储需求。</p>
<ul>
<li><code>STORAGE_ENDPOINT</code>:指定文件存储服务的 API 端点,便于与远程存储服务连接</li>
<li><code>STORAGE_ACCESS_KEY</code> 和 <code>STORAGE_SECRET_KEY</code>:用于认证的密钥对,保障文件存储服务的安全访问</li>
<li><code>STORAGE_BUCKET</code>、<code>STORAGE_REGION</code>:定义存储的目标存储桶和区域位置,以便更合理地管理文件资源,减少延迟</li>
<li><code>STORAGE_USE_SSL</code>:配置是否启用 HTTPS,以增强数据传输的安全性</li>
</ul>
</li>
<li>
<p>JWT 配置</p>
<p>JWT 用于实现用户身份验证和会话管理,是一种轻量的认证方式。</p>
<p>在过去的 <a href="/posts/40b4">React + NestJS + SocketIO 教程文章</a> 中,我们已经讲过了 JWT,感兴趣的可以看看。</p>
<ul>
<li><code>JWT_SECRET</code>:用于签发和验证 JWT 的密钥,确保身份认证的安全性。设计一个足够强度的密钥并保持其私密性至关重要</li>
<li><code>JWT_TOKEN_AUDIENCE</code>:指定 JWT 的受众,即令牌面向的服务或应用。设置受众可以帮助确保令牌仅被指定应用使用,提高认证的安全性</li>
<li><code>JWT_TOKEN_ISSUER</code>:用于声明 JWT 的发布者,一般设置为认证服务器的标识,确保 JWT 的来源是可信的</li>
<li><code>JWT_ACCESS_TOKEN_TTL</code>:JWT 访问令牌的有效时间,单位为秒。合理设置过期时间既能提升安全性(防止会话过长导致会话劫持风险),又可避免用户频繁重新登录带来的不便</li>
</ul>
</li>
</ol>
<h2 id="21-安装必要依赖"><a class="markdownIt-Anchor" href="#21-安装必要依赖"></a> 2.1. 安装必要依赖</h2>
<p>首先安装 <code>@nestjs/config</code>、<code>dotenv</code> 以及数据库驱动和 TypeORM,以支持加载环境变量、进行数据库连接和配置。</p>
<figure class="highlight bash"><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">yarn add @nestjs/config</span><br><span class="line">yarn add dotenv -D</span><br><span class="line">yarn add @nestjs/typeorm typeorm mysql2 <span class="comment"># 替换 mysql2 为 pg 以使用 PostgreSQL</span></span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>@nestjs/config</code> 是 NestJS 提供的官方配置模块,专为加载、管理和验证环境变量而设计</li>
<li><code>dotenv</code> 是配置模块的底层依赖,通过 <code>.env</code> 文件加载环境变量</li>
<li>TypeORM 是一个 TypeScript 支持的 ORM(对象关系映射),能够与关系型数据库集成</li>
</ul>
<h2 id="22-创建环境变量文件"><a class="markdownIt-Anchor" href="#22-创建环境变量文件"></a> 2.2. 创建环境变量文件</h2>
<p>在项目根目录下创建 <code>.env</code> 文件:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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"># 应用</span><br><span class="line">APP_PORT=4000</span><br><span class="line">APP_ENV=development</span><br><span class="line"></span><br><span class="line"># 核心数据库</span><br><span class="line">DB_TYPE=mysql</span><br><span class="line">DB_HOST=localhost</span><br><span class="line">DB_PORT=3306</span><br><span class="line">DB_USER=用户名</span><br><span class="line">DB_PASSWORD=密码</span><br><span class="line">DB_NAME=数据库名称</span><br><span class="line"></span><br><span class="line"># Redis 缓存</span><br><span class="line">REDIS_HOST=localhost</span><br><span class="line">REDIS_PORT=6379</span><br><span class="line">REDIS_PASSWORD=密码</span><br><span class="line">REDIS_DB=0</span><br><span class="line"></span><br><span class="line"># Elasticsearch</span><br><span class="line">ELASTICSEARCH_HOST=localhost</span><br><span class="line">ELASTICSEARCH_PORT=9200</span><br><span class="line">ELASTICSEARCH_USERNAME=用户名</span><br><span class="line">ELASTICSEARCH_PASSWORD=密码</span><br><span class="line">ELASTICSEARCH_INDEX=products</span><br><span class="line"></span><br><span class="line"># 文件存储</span><br><span class="line">STORAGE_ENDPOINT=</span><br><span class="line">STORAGE_ACCESS_KEY=access_key_id</span><br><span class="line">STORAGE_SECRET_KEY=secret_access_key</span><br><span class="line">STORAGE_BUCKET=桶名</span><br><span class="line">STORAGE_REGION=</span><br><span class="line">STORAGE_USE_SSL=true</span><br><span class="line"></span><br><span class="line"># JWT</span><br><span class="line">JWT_SECRET=secret</span><br><span class="line">JWT_TOKEN_AUDIENCE=localhost:4000</span><br><span class="line">JWT_TOKEN_ISSUER=localhost:4000</span><br><span class="line">JWT_ACCESS_TOKEN_TTL=3600</span><br></pre></td></tr></tbody></table></figure>
<p>这里的环境变量覆盖了不同模块所需的配置项,确保各模块配置的灵活性。</p>
<h2 id="23-引入配置模块和验证"><a class="markdownIt-Anchor" href="#23-引入配置模块和验证"></a> 2.3. 引入配置模块和验证</h2>
<p>在 <code>AppModule</code> 中配置 <code>@nestjs/config</code>,使用 <code>Joi</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><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></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_">ConfigModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="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_">DatabaseModule</span> } <span class="keyword">from</span> <span class="string">'./database/database.module'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Joi</span> <span class="keyword">from</span> <span class="string">'joi'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">ConfigModule</span>.<span class="title function_">forRoot</span>({</span><br><span class="line"> <span class="attr">isGlobal</span>: <span class="literal">true</span>, <span class="comment">// 让 ConfigModule 全局可用</span></span><br><span class="line"> <span class="attr">validationSchema</span>: <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">APP_PORT</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">4000</span>),</span><br><span class="line"> <span class="attr">APP_ENV</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">valid</span>(<span class="string">'development'</span>, <span class="string">'production'</span>, <span class="string">'test'</span>).<span class="title function_">default</span>(<span class="string">'development'</span>),</span><br><span class="line"> <span class="comment">// 核心数据库</span></span><br><span class="line"> <span class="attr">DB_TYPE</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">valid</span>(<span class="string">'mysql'</span>, <span class="string">'postgres'</span>).<span class="title function_">default</span>(<span class="string">'mysql'</span>),</span><br><span class="line"> <span class="attr">DB_HOST</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">DB_PORT</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">3306</span>),</span><br><span class="line"> <span class="attr">DB_USER</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">DB_PASSWORD</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">DB_NAME</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="comment">// Redis 缓存</span></span><br><span class="line"> <span class="attr">REDIS_HOST</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">REDIS_PORT</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">6379</span>),</span><br><span class="line"> <span class="attr">REDIS_PASSWORD</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">allow</span>(<span class="string">''</span>),</span><br><span class="line"> <span class="attr">REDIS_DB</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">0</span>),</span><br><span class="line"> <span class="comment">// Elasticsearch</span></span><br><span class="line"> <span class="attr">ELASTICSEARCH_HOST</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">ELASTICSEARCH_PORT</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">9200</span>),</span><br><span class="line"> <span class="attr">ELASTICSEARCH_USER</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">ELASTICSEARCH_PASSWORD</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">ELASTICSEARCH_INDEX</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="comment">// 文件存储</span></span><br><span class="line"> <span class="attr">STORAGE_ENDPOINT</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">allow</span>(<span class="string">''</span>),</span><br><span class="line"> <span class="attr">STORAGE_ACCESS_KEY</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">STORAGE_SECRET_KEY</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">STORAGE_BUCKET</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">STORAGE_REGION</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">allow</span>(<span class="string">''</span>),</span><br><span class="line"> <span class="attr">STORAGE_USE_SSL</span>: <span class="title class_">Joi</span>.<span class="title function_">boolean</span>().<span class="title function_">default</span>(<span class="literal">true</span>),</span><br><span class="line"> <span class="comment">// JWT</span></span><br><span class="line"> <span class="attr">JWT_SECRET</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_TOKEN_AUDIENCE</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_TOKEN_ISSUER</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">JWT_ACCESS_TOKEN_TTL</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">3600</span>)</span><br><span class="line"> })</span><br><span class="line"> }),</span><br><span class="line"> <span class="title class_">DatabaseModule</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 class="title class_">AppService</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_">AppModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>isGlobal</code>:将配置模块设为全局模块,避免在其他模块中重复引入</li>
<li><code>validationSchema</code>:通过 <code>Joi</code> 验证环境变量的值,确保值类型与业务需求匹配;例如 <code>DB_HOST</code> 需要是字符串,<code>APP_PORT</code> 应为数值,且数据库和 JWT 密钥都必须存在</li>
</ul>
<blockquote>
<p><code>Joi</code> 是一个 JavaScript 数据验证库,通常用来确保应用中的数据符合特定的规则或格式。</p>
<h4 id="231-基本用法"><a class="markdownIt-Anchor" href="#231-基本用法"></a> 2.3.1. 基本用法</h4>
<p><code>Joi</code> 提供了一个简单的链式 API 来定义验证规则。验证的流程一般是:定义 schema(验证规则) -> 验证数据 -> 获取结果或错误。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">Joi</span> = <span class="built_in">require</span>(<span class="string">'joi'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义 schema</span></span><br><span class="line"><span class="keyword">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">name</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">min</span>(<span class="number">3</span>).<span class="title function_">max</span>(<span class="number">30</span>).<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">age</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">integer</span>().<span class="title function_">min</span>(<span class="number">0</span>).<span class="title function_">max</span>(<span class="number">120</span>),</span><br><span class="line"> <span class="attr">email</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">email</span>()</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="comment">// 验证数据</span></span><br><span class="line"><span class="keyword">const</span> result = schema.<span class="title function_">validate</span>({ <span class="attr">name</span>: <span class="string">'Alice'</span>, <span class="attr">age</span>: <span class="number">25</span>, <span class="attr">email</span>: <span class="string">'alice@example.com'</span> });</span><br><span class="line"></span><br><span class="line"><span class="comment">// 检查结果</span></span><br><span class="line"><span class="keyword">if</span> (result.<span class="property">error</span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(result.<span class="property">error</span>.<span class="property">details</span>);</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(result.<span class="property">value</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h4 id="232-基本类型验证"><a class="markdownIt-Anchor" href="#232-基本类型验证"></a> 2.3.2. 基本类型验证</h4>
<p><code>Joi</code> 支持的基本类型包括:<code>string</code>、<code>number</code>、<code>boolean</code>、<code>array</code>、<code>object</code> 等。每种类型可以组合其他规则,如最小/最大值、必填/选填、格式限制等。</p>
<ol>
<li>字符串验证</li>
</ol>
<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></pre></td><td class="code"><pre><span class="line"><span class="title class_">Joi</span>.<span class="title function_">string</span>() <span class="comment">// 定义为字符串类型</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">min</span>(<span class="number">3</span>) <span class="comment">// 最小长度</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">max</span>(<span class="number">30</span>) <span class="comment">// 最大长度</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">email</span>() <span class="comment">// 必须是电子邮箱格式</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">regex</span>(<span class="regexp">/^[a-zA-Z0-9]{3,30}$/</span>) <span class="comment">// 使用正则表达式验证格式</span></span><br></pre></td></tr></tbody></table></figure>
<ol start="2">
<li>数字验证</li>
</ol>
<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></pre></td><td class="code"><pre><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>() <span class="comment">// 定义为数字类型</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">integer</span>() <span class="comment">// 必须是整数</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">min</span>(<span class="number">0</span>) <span class="comment">// 最小值</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">max</span>(<span class="number">100</span>) <span class="comment">// 最大值</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">positive</span>() <span class="comment">// 必须是正数</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">negative</span>() <span class="comment">// 必须是负数</span></span><br></pre></td></tr></tbody></table></figure>
<ol start="3">
<li>布尔值验证</li>
</ol>
<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="title class_">Joi</span>.<span class="title function_">boolean</span>() <span class="comment">// 定义为布尔类型</span></span><br></pre></td></tr></tbody></table></figure>
<ol start="4">
<li>数组验证</li>
</ol>
<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="title class_">Joi</span>.<span class="title function_">array</span>() <span class="comment">// 定义为数组类型</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">array</span>().<span class="title function_">items</span>(<span class="title class_">Joi</span>.<span class="title function_">number</span>()) <span class="comment">// 数组中每项都必须是数字</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">array</span>().<span class="title function_">min</span>(<span class="number">1</span>).<span class="title function_">max</span>(<span class="number">5</span>) <span class="comment">// 数组长度限制</span></span><br><span class="line"><span class="title class_">Joi</span>.<span class="title function_">array</span>().<span class="title function_">unique</span>() <span class="comment">// 数组中的每个元素必须唯一</span></span><br></pre></td></tr></tbody></table></figure>
<ol start="5">
<li>对象验证</li>
</ol>
<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="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">username</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">password</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">min</span>(<span class="number">8</span>).<span class="title function_">required</span>(),</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
<h4 id="233-条件验证"><a class="markdownIt-Anchor" href="#233-条件验证"></a> 2.3.3. 条件验证</h4>
<p>条件验证允许定义复杂的规则,如基于字段值的条件或逻辑分支。</p>
<p><code>when</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">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">password</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">min</span>(<span class="number">8</span>).<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">confirmPassword</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">valid</span>(<span class="title class_">Joi</span>.<span class="title function_">ref</span>(<span class="string">'password'</span>)).<span class="title function_">when</span>(<span class="string">'password'</span>, {</span><br><span class="line"> <span class="attr">is</span>: <span class="title class_">Joi</span>.<span class="title function_">exist</span>(), <span class="comment">// 如果 password 存在……</span></span><br><span class="line"> <span class="attr">then</span>: <span class="title class_">Joi</span>.<span class="title function_">required</span>(), <span class="comment">// ……confirmPassword 也是必填</span></span><br><span class="line"> <span class="attr">otherwise</span>: <span class="title class_">Joi</span>.<span class="title function_">forbidden</span>() <span class="comment">// 否则不允许出现 confirmPassword</span></span><br><span class="line"> })</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<h4 id="234-嵌套对象和数组验证"><a class="markdownIt-Anchor" href="#234-嵌套对象和数组验证"></a> 2.3.4. 嵌套对象和数组验证</h4>
<p><code>Joi</code> 允许定义嵌套结构,如对象嵌套和数组嵌套。</p>
<ol>
<li>嵌套对象</li>
</ol>
<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">user</span>: <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">name</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">age</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">min</span>(<span class="number">0</span>)</span><br><span class="line"> }).<span class="title function_">required</span>()</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<ol start="2">
<li>嵌套数组</li>
</ol>
<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">array</span>().<span class="title function_">items</span>(</span><br><span class="line"> <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">id</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">name</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>()</span><br><span class="line"> })</span><br><span class="line">);</span><br></pre></td></tr></tbody></table></figure>
<h4 id="235-自定义验证器"><a class="markdownIt-Anchor" href="#235-自定义验证器"></a> 2.3.5. 自定义验证器</h4>
<p><code>Joi</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">custom</span>(<span class="function">(<span class="params">value, helpers</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="regexp">/^[a-zA-Z]+$/</span>.<span class="title function_">test</span>(value)) {</span><br><span class="line"> <span class="keyword">return</span> helpers.<span class="title function_">error</span>(<span class="string">'any.invalid'</span>); <span class="comment">// 返回一个自定义错误</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> value; <span class="comment">// 验证通过</span></span><br><span class="line">}, <span class="string">'Custom alphabet validation'</span>);</span><br></pre></td></tr></tbody></table></figure>
<h4 id="236-配合-nestjs-使用"><a class="markdownIt-Anchor" href="#236-配合-nestjs-使用"></a> 2.3.6. 配合 NestJS 使用</h4>
<p>在 NestJS 中,可以结合 <code>@nestjs/config</code> 模块来使用 <code>Joi</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></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_">ConfigModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Joi</span> <span class="keyword">from</span> <span class="string">'joi'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">ConfigModule</span>.<span class="title function_">forRoot</span>({</span><br><span class="line"> <span class="attr">validationSchema</span>: <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="comment">// 假设有核心数据库的配置</span></span><br><span class="line"> <span class="attr">DB_HOST</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">required</span>(),</span><br><span class="line"> <span class="attr">DB_PORT</span>: <span class="title class_">Joi</span>.<span class="title function_">number</span>().<span class="title function_">default</span>(<span class="number">5432</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">class</span> <span class="title class_">AppModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<h4 id="237-错误处理与自定义错误消息"><a class="markdownIt-Anchor" href="#237-错误处理与自定义错误消息"></a> 2.3.7. 错误处理与自定义错误消息</h4>
<p><code>Joi</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">const</span> schema = <span class="title class_">Joi</span>.<span class="title function_">object</span>({</span><br><span class="line"> <span class="attr">name</span>: <span class="title class_">Joi</span>.<span class="title function_">string</span>().<span class="title function_">min</span>(<span class="number">3</span>).<span class="title function_">required</span>().<span class="title function_">messages</span>({</span><br><span class="line"> <span class="string">'string.base'</span>: <span class="string">`"name" should be a type of 'text'`</span>,</span><br><span class="line"> <span class="string">'string.empty'</span>: <span class="string">`"name" cannot be an empty field`</span>,</span><br><span class="line"> <span class="string">'string.min'</span>: <span class="string">`"name" should have a minimum length of {#limit}`</span>,</span><br><span class="line"> <span class="string">'any.required'</span>: <span class="string">`"name" is a required field`</span></span><br><span class="line"> })</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<h2 id="24-使用配置"><a class="markdownIt-Anchor" href="#24-使用配置"></a> 2.4. 使用配置</h2>
<p>作为一个使用 <code>ConfigService</code> 的例子,我们将从 <code>.env</code> 中读取 <code>APP_PORT</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_">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_">ConfigService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</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"></span><br><span class="line"> <span class="keyword">const</span> configService = app.<span class="title function_">get</span>(<span class="title class_">ConfigService</span>);</span><br><span class="line"> <span class="keyword">const</span> port = configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'APP_PORT'</span>) || <span class="number">4000</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(port);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></tbody></table></figure>
<p>运行 <code>nest start</code> 来查看是否有任何问题。</p>
<h1 id="3-设置数据库连接"><a class="markdownIt-Anchor" href="#3-设置数据库连接"></a> 3. 设置数据库连接</h1>
<div class="danger">
<p>首先,确保自己的本机环境中有安装 MySQL 或者 PostgreSQL。</p>
<p>安装教程请自行在网上搜索,本篇文档将只会使用 <code>8.0.40 MySQL Community</code>。</p>
<blockquote>
<p>挖坑:未来可能会添加支持其他数据库的功能。</p>
</blockquote>
</div>
<h2 id="31-配置-mysql"><a class="markdownIt-Anchor" href="#31-配置-mysql"></a> 3.1. 配置 MySQL</h2>
<ol>
<li>
<p>打开 MySQL CLI(或者使用 <code>mysql -u root -p</code> 来进行连接)</p>
</li>
<li>
<p>创建数据库:</p>
<figure class="highlight sql"><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">CREATE</span> DATABASE 数据库名称;</span><br></pre></td></tr></tbody></table></figure>
<p><em>数据库名称</em> 要匹配 <code>.env</code> 中的:</p>
<figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DB_NAME<span class="operator">=</span>数据库名称</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>创建用户:</p>
<figure class="highlight sql"><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">CREATE</span> <span class="keyword">USER</span> <span class="string">'用户名'</span>@<span class="string">'localhost'</span> IDENTIFIED <span class="keyword">BY</span> <span class="string">'密码'</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>这里的内容要匹配 <code>.env</code> 中的这一段内容:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">DB_USER=用户名</span><br><span class="line">DB_PASSWORD=密码</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>设置权限:</p>
<figure class="highlight sql"><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="keyword">GRANT</span> <span class="keyword">ALL</span> PRIVILEGES <span class="keyword">ON</span> 数据库名称.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">'用户名'</span>@<span class="string">'localhost'</span>;</span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>设置完后可以测试一下:</p>
<figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql <span class="operator">-</span>u 用户名 <span class="operator">-</span>p <span class="operator">-</span>h localhost <span class="operator">-</span>P <span class="number">3306</span> 数据库名称</span><br></pre></td></tr></tbody></table></figure>
</li>
</ol>
<h2 id="32-创建-databasemodule"><a class="markdownIt-Anchor" href="#32-创建-databasemodule"></a> 3.2. 创建 <code>DatabaseModule</code></h2>
<p>在 NestJS 项目中,集中管理数据库连接的配置非常重要,尤其是在需要支持多种环境(如开发、测试、生产)时。</p>
<p>创建 <code>DatabaseModule</code> 能让我们将数据库的配置代码分离出来,以便在不同的环境中灵活调整配置,比如使用 <code>ConfigService</code> 来获取环境变量。</p>
<p>通过 <code>TypeOrmModule.forRootAsync</code> 方法,我们可以使用异步的方式配置 TypeORM。这样可以确保数据库配置在应用初始化时依赖于环境变量,如 <code>DB_HOST</code>、<code>DB_USER</code>、<code>DB_PASSWORD</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></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_">TypeOrmModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/typeorm'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ConfigModule</span>, <span class="title class_">ConfigService</span> } <span class="keyword">from</span> <span class="string">'@nestjs/config'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="title class_">ConfigModule</span>,</span><br><span class="line"> <span class="title class_">TypeOrmModule</span>.<span class="title function_">forRootAsync</span>({</span><br><span class="line"> <span class="attr">imports</span>: [<span class="title class_">ConfigModule</span>],</span><br><span class="line"> <span class="attr">inject</span>: [<span class="title class_">ConfigService</span>],</span><br><span class="line"> <span class="attr">useFactory</span>: <span class="function">(<span class="params"><span class="attr">configService</span>: <span class="title class_">ConfigService</span></span>) =></span> ({</span><br><span class="line"> <span class="attr">type</span>: configService.<span class="property">get</span><<span class="string">'mysql'</span> | <span class="string">'postgres'</span>>(<span class="string">'DB_TYPE'</span>),</span><br><span class="line"> <span class="attr">host</span>: configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'DB_HOST'</span>),</span><br><span class="line"> <span class="attr">port</span>: configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'DB_PORT'</span>),</span><br><span class="line"> <span class="attr">username</span>: configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'DB_USER'</span>),</span><br><span class="line"> <span class="attr">password</span>: configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'DB_PASSWORD'</span>),</span><br><span class="line"> <span class="attr">database</span>: configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'DB_NAME'</span>),</span><br><span class="line"> <span class="attr">autoLoadEntities</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">synchronize</span>: configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'APP_ENV'</span>) === <span class="string">'development'</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">class</span> <span class="title class_">DatabaseModule</span> {}</span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>ConfigService</code>:用于从环境变量获取配置,确保 <code>DB_TYPE</code> 等参数的灵活性</li>
<li><code>forRootAsync</code>:动态配置 <code>TypeOrmModule</code>,适用于需要依赖环境变量初始化的模块</li>
<li><code>autoLoadEntities: true</code>:TypeORM 会自动加载应用中定义的所有实体。这让我们可以在项目中自由地添加新的实体,而不需要每次手动导入</li>
<li><code>synchronize</code>:将其设置为 <code>true</code> 会在开发环境中自动同步数据库表结构,以便在本地开发时快速响应数据结构的修改。但在生产环境中,建议关闭 <code>synchronize</code>,以防止意外数据丢失或表结构破坏</li>
</ul>
<h1 id="4-配置-swagger-文档"><a class="markdownIt-Anchor" href="#4-配置-swagger-文档"></a> 4. 配置 Swagger 文档</h1>
<p>在现代 Web 开发中,API 文档对于开发人员和用户来说都是至关重要的,特别是在团队协作中,清晰的 API 文档可以大大提高开发效率。</p>
<p>NestJS 提供了内置的 Swagger 支持,允许我们快速生成符合 OpenAPI 标准的文档,为用户提供更好的接口可视化。</p>
<blockquote>
<p>OpenAPI 是一种用于描述 RESTful API 的规范,它提供了一种标准化的格式,用于定义 API 的端点、请求、响应、认证等内容。</p>
<p>它的前身是 Swagger 规范,因此你可能听过 Swagger 和 OpenAPI 这两个词被混用。</p>
<p>OpenAPI 的主要目标是使 API 设计、文档、测试和集成过程更为高效和一致。</p>
</blockquote>
<h2 id="41-安装依赖"><a class="markdownIt-Anchor" href="#41-安装依赖"></a> 4.1. 安装依赖</h2>
<p>首先,我们需要安装 <code>@nestjs/swagger</code> 和 <code>swagger-ui-express</code> 两个模块。</p>
<ul>
<li><code>@nestjs/swagger</code> 提供了 NestJS 对 Swagger 的支持</li>
<li><code>swagger-ui-express</code> 是 Swagger UI 的依赖包,用于在浏览器中显示 API 文档</li>
</ul>
<p>在项目根目录下运行以下命令来安装它们:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add @nestjs/swagger swagger-ui-express</span><br></pre></td></tr></tbody></table></figure>
<h2 id="42-配置-swagger"><a class="markdownIt-Anchor" href="#42-配置-swagger"></a> 4.2. 配置 Swagger</h2>
<p>打开 <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></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">DocumentBuilder</span>, <span class="title class_">SwaggerModule</span> } <span class="keyword">from</span> <span class="string">'@nestjs/swagger'</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="comment">// ...</span></span><br><span class="line"> <span class="keyword">const</span> swaggerConfig = <span class="keyword">new</span> <span class="title class_">DocumentBuilder</span>()</span><br><span class="line"> .<span class="title function_">setTitle</span>(<span class="string">'API 文档'</span>)</span><br><span class="line"> .<span class="title function_">setDescription</span>(<span class="string">'Shopping-Nest 的 API 文档'</span>)</span><br><span class="line"> .<span class="title function_">setVersion</span>(<span class="string">'1.0'</span>)</span><br><span class="line"> .<span class="title function_">addBearerAuth</span>()</span><br><span class="line"> .<span class="title function_">build</span>();</span><br><span class="line"> <span class="keyword">const</span> <span class="variable language_">document</span> = <span class="title class_">SwaggerModule</span>.<span class="title function_">createDocument</span>(app, swaggerConfig);</span><br><span class="line"> <span class="title class_">SwaggerModule</span>.<span class="title function_">setup</span>(<span class="string">'api-docs'</span>, app, <span class="variable language_">document</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>DocumentBuilder</code> 来创建 Swagger 文档的基本信息。常见的配置项有:</p>
<ul>
<li><code>.setTitle()</code>:设置 API 文档的标题</li>
<li><code>.setDescription()</code>:提供 API 的描述信息</li>
<li><code>.setVersion()</code>:指定 API 的版本号</li>
<li><code>.addBearerAuth()</code>:如果 API 需要 JWT 认证(通常用于保护 API),可以添加 Bearer 认证支持</li>
</ul>
<p><code>SwaggerModule.setup()</code> 方法将 Swagger UI 绑定到指定的路由路径(这里是 <code>/api-docs</code>),之后,我们可以通过访问 <code>http://localhost:APP_PORT/api-docs</code> 来查看生成的文档。</p>
<h2 id="43-如何使用-swagger"><a class="markdownIt-Anchor" href="#43-如何使用-swagger"></a> 4.3. 如何使用 Swagger</h2>
<p>在我们完成 Swagger 的基础配置后,接下来的步骤将详细介绍如何利用 Swagger 注释来生成清晰的 API 文档。这一部分将涵盖如何为控制器、DTO(数据传输对象)和请求参数添加 Swagger 装饰器,以便 Swagger 能够生成准确且全面的 API 文档。</p>
<h4 id="431-为控制器添加注释"><a class="markdownIt-Anchor" href="#431-为控制器添加注释"></a> 4.3.1. 为控制器添加注释</h4>
<p>在 NestJS 中,控制器负责处理客户端请求并返回响应。我们可以使用 Swagger 提供的装饰器为控制器中的每个方法添加注释,以描述其功能、请求参数和返回结果。</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_">Controller</span>, <span class="title class_">Get</span>, <span class="title class_">Post</span>, <span class="title class_">Body</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_">ApiTags</span>, <span class="title class_">ApiOperation</span>, <span class="title class_">ApiResponse</span> } <span class="keyword">from</span> <span class="string">'@nestjs/swagger'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">CreateUserDto</span> } <span class="keyword">from</span> <span class="string">'./dto/create-user.dto'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@ApiTags</span>(<span class="string">'Users'</span>) <span class="comment">// 给控制器添加标签</span></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Post</span>()</span><br><span class="line"> <span class="meta">@ApiOperation</span>({ <span class="attr">summary</span>: <span class="string">'Create a new user'</span> }) <span class="comment">// 描述此 API 的作用</span></span><br><span class="line"> <span class="meta">@ApiResponse</span>({ <span class="attr">status</span>: <span class="number">201</span>, <span class="attr">description</span>: <span class="string">'The user has been successfully created.'</span> }) <span class="comment">// 201 状态响应</span></span><br><span class="line"> <span class="meta">@ApiResponse</span>({ <span class="attr">status</span>: <span class="number">400</span>, <span class="attr">description</span>: <span class="string">'Invalid input data.'</span> }) <span class="comment">// 400 状态响应</span></span><br><span class="line"> <span class="title function_">create</span>(<span class="params"><span class="meta">@Body</span>() <span class="attr">createUserDto</span>: <span class="title class_">CreateUserDto</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'This action adds a new user'</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Get</span>()</span><br><span class="line"> <span class="meta">@ApiOperation</span>({ <span class="attr">summary</span>: <span class="string">'Retrieve a list of users'</span> }) <span class="comment">// 描述此 API 的作用</span></span><br><span class="line"> <span class="meta">@ApiResponse</span>({ <span class="attr">status</span>: <span class="number">200</span>, <span class="attr">description</span>: <span class="string">'A list of users.'</span> }) <span class="comment">// 200 状态响应</span></span><br><span class="line"> <span class="title function_">getAllUsers</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> [{ <span class="attr">id</span>: <span class="number">1</span>, <span class="attr">name</span>: <span class="string">'John Doe'</span> }];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h4 id="432-为-dto-添加注释"><a class="markdownIt-Anchor" href="#432-为-dto-添加注释"></a> 4.3.2. 为 DTO 添加注释</h4>
<p>DTO(数据传输对象)用于定义请求和响应的结构。使用 Swagger 的 <code>@ApiProperty</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ApiProperty</span> } <span class="keyword">from</span> <span class="string">'@nestjs/swagger'</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_">CreateUserDto</span> {</span><br><span class="line"> <span class="meta">@ApiProperty</span>({ <span class="attr">description</span>: <span class="string">'The name of the user'</span> }) <span class="comment">// 描述 name 字段</span></span><br><span class="line"> <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ApiProperty</span>({ <span class="attr">description</span>: <span class="string">'The age of the user'</span>, <span class="attr">minimum</span>: <span class="number">1</span> }) <span class="comment">// 描述 age 字段并设置最小值</span></span><br><span class="line"> <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ApiProperty</span>({ <span class="attr">description</span>: <span class="string">'The email of the user'</span>, <span class="attr">required</span>: <span class="literal">true</span> }) <span class="comment">// 描述 email 字段</span></span><br><span class="line"> <span class="attr">email</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在上面的例子中,<code>CreateUserDto</code> 包含了三个属性:<code>name</code>、<code>age</code> 和 <code>email</code>。每个属性都使用了 <code>@ApiProperty</code> 装饰器来提供详细描述,并且可以设置字段的其他约束(如是否必填、类型等)。</p>
<h4 id="433-为请求参数添加注释"><a class="markdownIt-Anchor" href="#433-为请求参数添加注释"></a> 4.3.3. 为请求参数添加注释</h4>
<p>如果你的 API 需要接受路径参数、查询参数或请求体中的数据,Swagger 也提供了相关的装饰器来帮助描述这些参数。</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_">Controller</span>, <span class="title class_">Get</span>, <span class="title class_">Param</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ApiParam</span> } <span class="keyword">from</span> <span class="string">'@nestjs/swagger'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'users'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br><span class="line"> <span class="meta">@Get</span>(<span class="string">':id'</span>)</span><br><span class="line"> <span class="meta">@ApiOperation</span>({ <span class="attr">summary</span>: <span class="string">'Retrieve a user by ID'</span> }) <span class="comment">// 描述此 API 的作用</span></span><br><span class="line"> <span class="meta">@ApiParam</span>({ <span class="attr">name</span>: <span class="string">'id'</span>, <span class="attr">required</span>: <span class="literal">true</span>, <span class="attr">description</span>: <span class="string">'The ID of the user to retrieve'</span>, <span class="attr">type</span>: <span class="title class_">Number</span> }) <span class="comment">// 描述路径参数</span></span><br><span class="line"> <span class="title function_">getUserById</span>(<span class="params"><span class="meta">@Param</span>(<span class="string">'id'</span>) <span class="attr">id</span>: <span class="built_in">number</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> { id, <span class="attr">name</span>: <span class="string">'John Doe'</span> }; <span class="comment">// 示例返回</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在这个示例中,<code>@ApiParam</code> 用于描述路径参数 id。它帮助用户理解这个参数是必须的,且应该是一个数字。</p>
<h1 id="5-设置基础中间件"><a class="markdownIt-Anchor" href="#5-设置基础中间件"></a> 5. 设置基础中间件</h1>
<p>在现代 Web 开发中,处理安全性、请求速率限制、响应压缩以及自定义日志记录是打造可靠、高效应用的基础。</p>
<p>NestJS 提供了简单灵活的中间件配置支持,通过整合 <code>helmet</code>、<code>@nestjs/throttler</code>、<code>compression</code> 等库,开发者可以轻松地实现这些功能。</p>
<h2 id="51-添加-cors-支持"><a class="markdownIt-Anchor" href="#51-添加-cors-支持"></a> 5.1. 添加 CORS 支持</h2>
<p>首先,我们需要确保应用支持跨域请求(CORS),特别是在前后端分离的情况下。以下是启用和配置 CORS 的方法:</p>
<figure class="highlight ts"><figcaption><span>main.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">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">const</span> corsOrigin = configService.<span class="property">get</span><<span class="built_in">string</span>>(<span class="string">'CORS_ORIGIN'</span>);</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">enableCors</span>({</span><br><span class="line"> <span class="attr">origin</span>: corsOrigin,</span><br><span class="line"> <span class="attr">methods</span>: <span class="string">'GET,POST'</span>,</span><br><span class="line"> <span class="attr">credentials</span>: <span class="literal">true</span>,</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 <code>.env</code> 中添加:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CORS_ORIGIN=http://localhost:3000</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>使用 <code>http://localhost:3000</code> 是因为 React 本地环境默认运行在 <code>localhost:3000</code>。</p>
</blockquote>
<h2 id="52-增强安全性"><a class="markdownIt-Anchor" href="#52-增强安全性"></a> 5.2. 增强安全性</h2>
<p><code>helmet</code> 是一组帮助设置安全 HTTP 头的中间件,能够防范常见的 Web 攻击(例如,XSS 攻击和点击劫持)。</p>
<p>安装 <code>helmet</code> 库:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add helmet</span><br></pre></td></tr></tbody></table></figure>
<p>开启 <code>helmet</code> 保护:</p>
<figure class="highlight ts"><figcaption><span>main.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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> helmet <span class="keyword">from</span> <span class="string">'helmet'</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="comment">// ...</span></span><br><span class="line"> </span><br><span class="line"> app.<span class="title function_">use</span>(<span class="title function_">helmet</span>());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>通过这段简单的代码,<code>helmet</code> 会自动添加一组常用的安全头(以下信息来自于 <a target="_blank" rel="noopener" href="https://www.npmjs.com/package/helmet">npm</a>):</p>
<ul>
<li><strong>Content-Security-Policy</strong>:一个强大的允许清单,控制页面上可以发生的操作,有助于缓解多种攻击</li>
<li><strong>Cross-Origin-Opener-Policy</strong>:帮助页面实现进程隔离</li>
<li><strong>Cross-Origin-Resource-Policy</strong>:阻止其他网站跨域加载您的资源</li>
<li><strong>Origin-Agent-Cluster</strong>:将进程隔离改为基于源的方式</li>
<li><strong>Referrer-Policy</strong>:控制 <code>Referer</code> 请求头</li>
<li><strong>Strict-Transport-Security</strong>:告知浏览器优先使用 HTTPS</li>
<li><strong>X-Content-Type-Options</strong>:避免 MIME 类型嗅探</li>
<li><strong>X-DNS-Prefetch-Control</strong>:控制 DNS 预取</li>
<li><strong>X-Download-Options</strong>:强制将下载的文件保存到本地(仅适用于 Internet Explorer)</li>
<li><strong>X-Frame-Options</strong>:传统的标头,用于防范点击劫持攻击</li>
<li><strong>X-Permitted-Cross-Domain-Policies</strong>:控制 Adobe 产品(如 Acrobat)的跨域行为</li>
<li><strong>X-Powered-By</strong>:关于 Web 服务器的信息,已移除,以防止简单攻击利用该信息</li>
<li><strong>X-XSS-Protection</strong>:传统的标头,旨在防止 XSS 攻击,但通常效果不佳,因此 Helmet 将其禁用</li>
</ul>
<h2 id="53-压缩响应"><a class="markdownIt-Anchor" href="#53-压缩响应"></a> 5.3. 压缩响应</h2>
<p>压缩响应能够有效减少传输的数据量,提升页面加载速度。</p>
<p>安装 <code>compression</code> 库:</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">yarn add compression</span><br></pre></td></tr></tbody></table></figure>
<p>配置压缩的级别和触发条件:</p>
<figure class="highlight ts"><figcaption><span>main.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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> compression <span class="keyword">from</span> <span class="string">'compression'</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="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> compressionLevel = configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'COMPRESSION_LEVEL'</span>) || <span class="number">6</span>;</span><br><span class="line"> <span class="keyword">const</span> compressionThreshold = configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'COMPRESSION_THRESHOLD'</span>) || <span class="number">1024</span>;</span><br><span class="line"> app.<span class="title function_">use</span>(</span><br><span class="line"> <span class="title function_">compression</span>({</span><br><span class="line"> <span class="attr">level</span>: compressionLevel,</span><br><span class="line"> <span class="attr">threshold</span>: compressionThreshold,</span><br><span class="line"> })</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在 <code>.env</code> 中添加:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># 响应压缩</span><br><span class="line">COMPRESSION_LEVEL=6</span><br><span class="line">COMPRESSION_THRESHOLD=1024</span><br></pre></td></tr></tbody></table></figure>
<p>在上面的代码中,<code>level</code> 设置了压缩级别(范围从 0-9,数字越大压缩越强,但 CPU 负荷越高),而 <code>threshold</code> 设置了触发压缩的响应体积阈值(单位为字节)。</p>
<h2 id="54-限制请求速率"><a class="markdownIt-Anchor" href="#54-限制请求速率"></a> 5.4. 限制请求速率</h2>
<p>防止滥用 API 资源是每个 Web 应用的核心需求之一。我们可以使用 <code>@nestjs/throttler</code> 模块对请求速率进行限制,确保服务不会被大量请求淹没。</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">yarn add @nestjs/throttler</span><br></pre></td></tr></tbody></table></figure>
<p>我们在 <code>AppModule</code> 中通过 <code>ThrottlerModule.forRootAsync</code> 配置速率限制。利用 <code>ConfigService</code> 从 <code>.env</code> 文件中获取 <code>ttl</code>(时间窗口)和 <code>limit</code>(最大请求数)参数:</p>
<figure class="highlight ts"><figcaption><span>app.module.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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ThrottlerModule</span>, <span class="title class_">ThrottlerModuleOptions</span> } <span class="keyword">from</span> <span class="string">'@nestjs/throttler'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="title class_">ThrottlerModule</span>.<span class="title function_">forRootAsync</span>({</span><br><span class="line"> <span class="attr">inject</span>: [<span class="title class_">ConfigService</span>],</span><br><span class="line"> <span class="attr">useFactory</span>: (<span class="attr">configService</span>: <span class="title class_">ConfigService</span>): <span class="function"><span class="params">ThrottlerModuleOptions</span> =></span> [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">ttl</span>: configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'THROTTLE_TTL'</span>) || <span class="number">60</span>,</span><br><span class="line"> <span class="attr">limit</span>: configService.<span class="property">get</span><<span class="built_in">number</span>>(<span class="string">'THROTTLE_LIMIT'</span>) || <span class="number">10</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><br></pre></td></tr></tbody></table></figure>
<p>在 <code>.env</code> 中添加:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># 速率限制</span><br><span class="line">THROTTLE_TTL=60</span><br><span class="line">THROTTLE_LIMIT=10</span><br></pre></td></tr></tbody></table></figure>
<h4 id="541-使用例子"><a class="markdownIt-Anchor" href="#541-使用例子"></a> 5.4.1. 使用例子</h4>
<p>配置后,系统会自动为所有 API 路由设置速率限制。也可以在特定控制器或路由中通过 <code>@Throttle</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">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_">Throttle</span> } <span class="keyword">from</span> <span class="string">'@nestjs/throttler'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'test'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">TestController</span> {</span><br><span class="line"> <span class="meta">@Throttle</span>(<span class="number">5</span>, <span class="number">10</span>) <span class="comment">// 每 10 秒最多 5 个请求</span></span><br><span class="line"> <span class="meta">@Get</span>()</span><br><span class="line"> <span class="title function_">testRoute</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Testing rate limiting"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h2 id="55-自定义日志记录"><a class="markdownIt-Anchor" href="#55-自定义日志记录"></a> 5.5. 自定义日志记录</h2>
<p>为了记录请求信息,我们可以实现一个简单的 <code>LoggerMiddleware</code>,并在 <code>AppModule</code> 中配置它:</p>
<figure class="highlight ts"><figcaption><span>logger.middleware.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></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 class="subst">${req.method}</span> <span class="subst">${req.url}</span>`</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>在 <code>AppModule</code> 中使用 <code>configure</code> 方法应用此中间件:</p>
<figure class="highlight ts"><figcaption><span>app.module.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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">MiddlewareConsumer</span>, <span class="title class_">Module</span>, <span class="title class_">NestModule</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_">LoggerMiddleware</span> } <span class="keyword">from</span> <span class="string">'./middlewares/logger.middleware'</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="comment">/* 其他模块 */</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_">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 class="title function_">apply</span>(<span class="title class_">LoggerMiddleware</span>).<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>这样,每次请求都会在控制台输出请求方法和 URL 路径,帮助我们跟踪请求流向和响应情况。</p>
<p>启动 NestJS 项目,在浏览器中访问 <code>localhost:APP_PORT</code>(或者默认的 <code>localhost:4000</code>),就能在终端中看到 <code>Request... GET /</code> 的字眼。</p>
<h2 id="56-设置全局错误处理"><a class="markdownIt-Anchor" href="#56-设置全局错误处理"></a> 5.6. 设置全局错误处理</h2>
<p>为了统一错误响应格式,可以创建自定义异常过滤器来捕获异常,并返回标准化的错误信息。</p>
<p>我们在项目中定义 <code>HttpExceptionFilter</code> 类,并将其注册为全局过滤器:</p>
<figure class="highlight ts"><figcaption><span>http-exception.filter.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><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="keyword">import</span> { <span class="title class_">ExceptionFilter</span>, <span class="title class_">Catch</span>, <span class="title class_">ArgumentsHost</span>, <span class="title class_">HttpException</span>, <span class="title class_">HttpStatus</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Catch</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">HttpExceptionFilter</span> <span class="keyword">implements</span> <span class="title class_">ExceptionFilter</span> {</span><br><span class="line"> <span class="keyword">catch</span>(<span class="attr">exception</span>: <span class="built_in">unknown</span>, <span class="attr">host</span>: <span class="title class_">ArgumentsHost</span>) {</span><br><span class="line"> <span class="keyword">const</span> ctx = host.<span class="title function_">switchToHttp</span>();</span><br><span class="line"> <span class="keyword">const</span> response = ctx.<span class="title function_">getResponse</span>();</span><br><span class="line"> <span class="keyword">const</span> request = ctx.<span class="title function_">getRequest</span>();</span><br><span class="line"> <span class="keyword">const</span> status = exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getStatus</span>() : <span class="title class_">HttpStatus</span>.<span class="property">INTERNAL_SERVER_ERROR</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> errorResponse = {</span><br><span class="line"> <span class="attr">statusCode</span>: status,</span><br><span class="line"> <span class="attr">timestamp</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toISOString</span>(),</span><br><span class="line"> <span class="attr">path</span>: request.<span class="property">url</span>,</span><br><span class="line"> <span class="attr">message</span>: exception <span class="keyword">instanceof</span> <span class="title class_">HttpException</span> ? exception.<span class="title function_">getResponse</span>() : <span class="string">'Internal server error'</span></span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> response.<span class="title function_">status</span>(status).<span class="title function_">json</span>(errorResponse);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>接着在 <code>main.ts</code> 中注册过滤器:</p>
<figure class="highlight ts"><figcaption><span>main.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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">HttpExceptionFilter</span> } <span class="keyword">from</span> <span class="string">'./filters/http-exception.filter'</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="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalFilters</span>(<span class="keyword">new</span> <span class="title class_">HttpExceptionFilter</span>());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>通过此过滤器,所有未处理的异常都会以标准格式返回。</p>
<h1 id="6-配置日志系统"><a class="markdownIt-Anchor" href="#6-配置日志系统"></a> 6. 配置日志系统</h1>
<p>在开发和运维中,日志记录是至关重要的一部分。通过详细的日志记录,我们可以更好地了解应用的运行状态、排查错误,甚至帮助团队进行性能优化。</p>
<p>在开发或生产环境中,可能会遇到以下问题:</p>
<ul>
<li>控制台日志输出过于混乱:控制台日志输出没有明显的视觉区分,开发者难以快速找到关键信息</li>
<li>文件日志管理不当:日志文件没有分目录管理,日志存储时间不固定,且文件体积容易过大</li>
<li>日志轮转:没有对日志文件进行按日期轮换,容易导致单个日志文件过大,不利于维护</li>
</ul>
<h2 id="61-安装依赖"><a class="markdownIt-Anchor" href="#61-安装依赖"></a> 6.1. 安装依赖</h2>
<p>我们首先需要安装 <code>chalk</code>、<code>nest-winston</code>、<code>winston</code> 和 <code>winston-daily-rotate-file</code> 这些依赖。</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">yarn add chalk@^4 nest-winston winston winston-daily-rotate-file</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>注意:由于我的 NestJS 项目使用的是 CommonJS 模块系统,和使用 ESM 的 <code>chalk@5</code> 不兼容,所以我采用的最简单直接的方法就是降级到 <code>chalk@4</code>。</p>
</div>
<h2 id="62-配置-winston-日志文件"><a class="markdownIt-Anchor" href="#62-配置-winston-日志文件"></a> 6.2. 配置 <code>winston</code> 日志文件</h2>
<p>在 NestJS 项目中创建一个 <code>winston.logger.ts</code> 文件,用于配置 <code>winston</code> 的日志记录选项,包括日志等级、日志格式、文件轮转等。</p>
<h4 id="621-配置日志目录"><a class="markdownIt-Anchor" href="#621-配置日志目录"></a> 6.2.1. 配置日志目录</h4>
<p>我们将设置不同的日志目录来分别存储错误日志、警告日志和常规应用日志。</p>
<p>使用 <code>fs</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> fs <span class="keyword">from</span> <span class="string">'fs'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> path <span class="keyword">from</span> <span class="string">'path'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> logDirectories = [<span class="string">'logs/errors'</span>, <span class="string">'logs/warnings'</span>, <span class="string">'logs/app'</span>];</span><br><span class="line"></span><br><span class="line">logDirectories.<span class="title function_">forEach</span>(<span class="function"><span class="params">dir</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(__dirname, <span class="string">'..'</span>, <span class="string">'..'</span>, dir);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(fullPath)) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Creating directory at: <span class="subst">${fullPath}</span>`</span>);</span><br><span class="line"> fs.<span class="title function_">mkdirSync</span>(fullPath, { <span class="attr">recursive</span>: <span class="literal">true</span> });</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`Error creating directory <span class="subst">${fullPath}</span>:`</span>, error);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>这段代码创建了 <code>logs/errors</code>、<code>logs/warnings</code> 和 <code>logs/app</code> 三个目录,用于分别保存错误、警告和常规日志。</p>
<h4 id="622-定义日志颜色"><a class="markdownIt-Anchor" href="#622-定义日志颜色"></a> 6.2.2. 定义日志颜色</h4>
<p>接下来,通过 <code>chalk</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> chalk <span class="keyword">from</span> <span class="string">'chalk'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> getChalkColor = (<span class="attr">level</span>: <span class="built_in">string</span>): chalk.<span class="property">Chalk</span> => {</span><br><span class="line"> <span class="keyword">switch</span> (level) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'error'</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">red</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'warn'</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">yellow</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'info'</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">green</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'debug'</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">blue</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'verbose'</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">cyan</span>;</span><br><span class="line"> <span class="attr">default</span>:</span><br><span class="line"> <span class="keyword">return</span> chalk.<span class="property">white</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p>这样做的好处是,可以根据日志等级设置不同颜色,从而在控制台中快速识别重要的日志信息。</p>
<h4 id="623-配置-winston-日志选项"><a class="markdownIt-Anchor" href="#623-配置-winston-日志选项"></a> 6.2.3. 配置 <code>winston</code> 日志选项</h4>
<p>接下来,我们配置 <code>winston</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><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { createLogger, format, transports } <span class="keyword">from</span> <span class="string">'winston'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">DailyRotateFile</span> <span class="keyword">from</span> <span class="string">'winston-daily-rotate-file'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> winstonLogger = <span class="title function_">createLogger</span>({</span><br><span class="line"> <span class="attr">format</span>: format.<span class="title function_">combine</span>(format.<span class="title function_">timestamp</span>(), format.<span class="title function_">errors</span>({ <span class="attr">stack</span>: <span class="literal">true</span> }), format.<span class="title function_">splat</span>(), format.<span class="title function_">json</span>()),</span><br><span class="line"> <span class="attr">defaultMeta</span>: { <span class="attr">service</span>: <span class="string">'log-service'</span> },</span><br><span class="line"> <span class="attr">transports</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DailyRotateFile</span>({</span><br><span class="line"> <span class="attr">filename</span>: path.<span class="title function_">join</span>(__dirname, <span class="string">'..'</span>, <span class="string">'..'</span>, <span class="string">'logs/errors/error-%DATE%.log'</span>),</span><br><span class="line"> <span class="attr">datePattern</span>: <span class="string">'YYYY-MM-DD-HH-mm-ss'</span>,</span><br><span class="line"> <span class="attr">zippedArchive</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">maxSize</span>: <span class="string">'20m'</span>,</span><br><span class="line"> <span class="attr">maxFiles</span>: <span class="string">'14d'</span>,</span><br><span class="line"> <span class="attr">level</span>: <span class="string">'error'</span></span><br><span class="line"> }),</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DailyRotateFile</span>({</span><br><span class="line"> <span class="attr">filename</span>: path.<span class="title function_">join</span>(__dirname, <span class="string">'..'</span>, <span class="string">'..'</span>, <span class="string">'logs/warnings/warning-%DATE%.log'</span>),</span><br><span class="line"> <span class="attr">datePattern</span>: <span class="string">'YYYY-MM-DD-HH-mm-ss'</span>,</span><br><span class="line"> <span class="attr">zippedArchive</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">maxSize</span>: <span class="string">'20m'</span>,</span><br><span class="line"> <span class="attr">maxFiles</span>: <span class="string">'14d'</span>,</span><br><span class="line"> <span class="attr">level</span>: <span class="string">'warn'</span></span><br><span class="line"> }),</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DailyRotateFile</span>({</span><br><span class="line"> <span class="attr">filename</span>: path.<span class="title function_">join</span>(__dirname, <span class="string">'..'</span>, <span class="string">'..'</span>, <span class="string">'logs/app/app-%DATE%.log'</span>),</span><br><span class="line"> <span class="attr">datePattern</span>: <span class="string">'YYYY-MM-DD-HH-mm-ss'</span>,</span><br><span class="line"> <span class="attr">zippedArchive</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">maxSize</span>: <span class="string">'20m'</span>,</span><br><span class="line"> <span class="attr">maxFiles</span>: <span class="string">'14d'</span></span><br><span class="line"> }),</span><br><span class="line"> <span class="keyword">new</span> transports.<span class="title class_">Console</span>({</span><br><span class="line"> <span class="attr">format</span>: format.<span class="title function_">combine</span>(</span><br><span class="line"> format.<span class="title function_">colorize</span>(),</span><br><span class="line"> format.<span class="title function_">simple</span>(),</span><br><span class="line"> format.<span class="title function_">printf</span>(<span class="function"><span class="params">info</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> level = info.<span class="property">level</span>.<span class="title function_">toLowerCase</span>();</span><br><span class="line"> <span class="keyword">const</span> chalkColor = <span class="title function_">getChalkColor</span>(level);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${chalkColor(<span class="string">`<span class="subst">${info.timestamp}</span> - <span class="subst">${info.level}</span>:`</span>)}</span> <span class="subst">${info.message}</span>`</span>;</span><br><span class="line"> })</span><br><span class="line"> ),</span><br><span class="line"> <span class="attr">level</span>: <span class="string">'debug'</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">default</span> winstonLogger;</span><br></pre></td></tr></tbody></table></figure>
<p>这段配置实现了以下几个功能:</p>
<ul>
<li><code>DailyRotateFile</code>:设置了日志文件的轮转,每个日志类型(错误、警告、应用)都将按日期命名并存储</li>
<li>控制台输出:控制台输出带有颜色区分,并包含时间戳和日志级别,便于快速读取</li>
<li>日志格式:定义了 <code>json</code> 格式日志输出,并包含 <code>timestamp</code> 和 <code>stack</code> 信息</li>
</ul>
<h2 id="63-引入日志配置"><a class="markdownIt-Anchor" href="#63-引入日志配置"></a> 6.3. 引入日志配置</h2>
<p>在 <code>AppModule</code> 中通过 <code>WinstonModule</code> 引入 <code>winstonLogger</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">import</span> { <span class="title class_">WinstonModule</span> } <span class="keyword">from</span> <span class="string">'nest-winston'</span>;</span><br><span class="line"><span class="keyword">import</span> winstonLogger <span class="keyword">from</span> <span class="string">'./loggers/winston.logger'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Module</span>({</span><br><span class="line"> <span class="attr">imports</span>: [</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="title class_">WinstonModule</span>.<span class="title function_">forRoot</span>({</span><br><span class="line"> <span class="attr">transports</span>: winstonLogger.<span class="property">transports</span>,</span><br><span class="line"> <span class="attr">format</span>: winstonLogger.<span class="property">format</span>,</span><br><span class="line"> <span class="attr">defaultMeta</span>: winstonLogger.<span class="property">defaultMeta</span>,</span><br><span class="line"> <span class="attr">exitOnError</span>: <span class="literal">false</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>WinstonModule.forRoot()</code> 配置,我们将之前定义的 <code>winstonLogger</code> 作为全局日志管理器,使 NestJS 自动将应用日志转发到 <code>winston</code>。</p>
<h2 id="64-应用日志系统"><a class="markdownIt-Anchor" href="#64-应用日志系统"></a> 6.4. 应用日志系统</h2>
<p>为了启用配置的日志系统,我们需要在 <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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="variable constant_">WINSTON_MODULE_NEST_PROVIDER</span> } <span class="keyword">from</span> <span class="string">'nest-winston'</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="comment">// ...</span></span><br><span class="line"> </span><br><span class="line"> app.<span class="title function_">useLogger</span>(app.<span class="title function_">get</span>(<span class="variable constant_">WINSTON_MODULE_NEST_PROVIDER</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这样就将 <code>winston</code> 配置的日志记录器注入到应用中,使日志管理器可以通过 NestJS 的日志 API 来记录日志。</p>
<h2 id="65-修改-loggermiddleware"><a class="markdownIt-Anchor" href="#65-修改-loggermiddleware"></a> 6.5. 修改 <code>LoggerMiddleware</code></h2>
<p>最后,我们来修改一下 <code>LoggerMiddleware</code>,将其记录下每个请求的详细信息,包括:</p>
<ul>
<li>请求方法</li>
<li>URL</li>
<li>IP</li>
<li>HTTP 版本</li>
<li>状态码</li>
<li>响应时间等</li>
</ul>
<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><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_">Injectable</span>, <span class="title class_">Logger</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 class="keyword">import</span> * <span class="keyword">as</span> dayjs <span class="keyword">from</span> <span class="string">'dayjs'</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="keyword">private</span> logger = <span class="keyword">new</span> <span class="title class_">Logger</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="keyword">const</span> start = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="keyword">const</span> { method, originalUrl, ip, httpVersion, headers } = req;</span><br><span class="line"> <span class="keyword">const</span> { statusCode } = res;</span><br><span class="line"></span><br><span class="line"> res.<span class="title function_">on</span>(<span class="string">'finish'</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> end = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="keyword">const</span> duration = end - start;</span><br><span class="line"> <span class="keyword">const</span> logFormat = <span class="string">`<span class="subst">${dayjs().valueOf()}</span> <span class="subst">${method}</span> <span class="subst">${originalUrl}</span> HTTP/<span class="subst">${httpVersion}</span> <span class="subst">${ip}</span> <span class="subst">${statusCode}</span> <span class="subst">${duration}</span>ms <span class="subst">${headers[<span class="string">'user-agent'</span>]}</span>`</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (statusCode >= <span class="number">500</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">logger</span>.<span class="title function_">error</span>(logFormat, originalUrl);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (statusCode >= <span class="number">400</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">logger</span>.<span class="title function_">warn</span>(logFormat, originalUrl);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">logger</span>.<span class="title function_">log</span>(logFormat, originalUrl);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></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>
<ul>
<li>事件监听:使用 <code>finish</code> 事件来确保所有响应数据都已发送</li>
<li>日志格式:每个请求记录的格式包括请求时间、请求方法、URL、IP、状态码、响应耗时等信息</li>
<li>自动区分日志级别:根据响应的状态码自动设置日志等级
<ul>
<li>错误状态码(500+)记录为 <code>error</code></li>
<li>客户端错误(400+)记录为 <code>warn</code></li>
<li>其他成功请求则都记录为 <code>log</code></li>
</ul>
</li>
</ul>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="5a4b.html">上一篇</a><a class="next" href="6d86.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/8e94.html" data-full-url="https://cytrogen.icu/posts/8e94.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>