~cytrogen/blog-public

blog-public/posts/8e94.html -rw-r--r-- 140.5 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
<!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(验证规则) -&gt; 验证数据 -&gt; 获取结果或错误。</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>) =&gt;</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>&lt;<span class="built_in">number</span>&gt;(<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>) =&gt;</span> ({</span><br><span class="line">        <span class="attr">type</span>: configService.<span class="property">get</span>&lt;<span class="string">'mysql'</span> | <span class="string">'postgres'</span>&gt;(<span class="string">'DB_TYPE'</span>),</span><br><span class="line">        <span class="attr">host</span>: configService.<span class="property">get</span>&lt;<span class="built_in">string</span>&gt;(<span class="string">'DB_HOST'</span>),</span><br><span class="line">        <span class="attr">port</span>: configService.<span class="property">get</span>&lt;<span class="built_in">number</span>&gt;(<span class="string">'DB_PORT'</span>),</span><br><span class="line">        <span class="attr">username</span>: configService.<span class="property">get</span>&lt;<span class="built_in">string</span>&gt;(<span class="string">'DB_USER'</span>),</span><br><span class="line">        <span class="attr">password</span>: configService.<span class="property">get</span>&lt;<span class="built_in">string</span>&gt;(<span class="string">'DB_PASSWORD'</span>),</span><br><span class="line">        <span class="attr">database</span>: configService.<span class="property">get</span>&lt;<span class="built_in">string</span>&gt;(<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>&lt;<span class="built_in">string</span>&gt;(<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>&lt;<span class="built_in">string</span>&gt;(<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>&lt;<span class="built_in">number</span>&gt;(<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>&lt;<span class="built_in">number</span>&gt;(<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> =&gt;</span> [</span><br><span class="line">        {</span><br><span class="line">          <span class="attr">ttl</span>: configService.<span class="property">get</span>&lt;<span class="built_in">number</span>&gt;(<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>&lt;<span class="built_in">number</span>&gt;(<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> =&gt;</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> =&gt; {</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> =&gt;</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">() =&gt;</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 &gt;= <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 &gt;= <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>