-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path2862114693.html
More file actions
847 lines (525 loc) · 53.8 KB
/
Copy path2862114693.html
File metadata and controls
847 lines (525 loc) · 53.8 KB
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
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
<!DOCTYPE html>
<html lang="en" data-default-color-scheme="auto">
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" sizes="76x76" href="/img/fluid.png">
<link rel="icon" href="/img/fluid.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="theme-color" content="#2f4154">
<meta name="author" content="FangZhou">
<meta name="keywords" content="Vue,React,Nodejs,blogs,frontend,web,developer,programmer">
<meta name="description" content="使用TypeORM配合Nest.js开发,维护实体与DTO数据的正确处理">
<meta property="og:type" content="article">
<meta property="og:title" content="NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤">
<meta property="og:url" content="http://arkpln.github.io/2862114693.html">
<meta property="og:site_name" content="Ark的技术博客">
<meta property="og:description" content="使用TypeORM配合Nest.js开发,维护实体与DTO数据的正确处理">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="http://arkpln.github.io/images/nest-logo.png">
<meta property="article:published_time" content="2026-02-08T09:40:24.000Z">
<meta property="article:modified_time" content="2026-03-19T11:40:26.746Z">
<meta property="article:author" content="FangZhou">
<meta property="article:tag" content="nodejs">
<meta property="article:tag" content="backend">
<meta property="article:tag" content="nestjs">
<meta property="article:tag" content="fullstack">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="http://arkpln.github.io/images/nest-logo.png">
<title>NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤 - Ark的技术博客</title>
<link rel="stylesheet" href="https://lib.baomitu.com/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/github-markdown-css/4.0.0/github-markdown.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/hint.css/2.7.0/hint.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.css">
<!-- 主题依赖的图标库,不要自行修改 -->
<!-- Do not modify the link that theme dependent icons -->
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_1749284_5i9bdhy70f8.css">
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_1736178_k526ubmyhba.css">
<link rel="stylesheet" href="/css/main.css">
<link id="highlight-css" rel="stylesheet" href="/css/highlight.css">
<link id="highlight-css-dark" rel="stylesheet" href="/css/highlight-dark.css">
<script id="fluid-configs">
var Fluid = window.Fluid || {};
Fluid.ctx = Object.assign({}, Fluid.ctx)
var CONFIG = {"hostname":"arkpln.github.io","root":"/","version":"1.9.8","typing":{"enable":true,"typeSpeed":70,"cursorChar":"_","loop":false,"scope":[]},"anchorjs":{"enable":true,"element":"h1,h2,h3,h4,h5,h6","placement":"left","visible":"hover","icon":""},"progressbar":{"enable":true,"height_px":3,"color":"#29d","options":{"showSpinner":false,"trickleSpeed":100}},"code_language":{"enable":true,"default":"TEXT"},"copy_btn":true,"image_caption":{"enable":true},"image_zoom":{"enable":true,"img_url_replace":["",""]},"toc":{"enable":true,"placement":"right","headingSelector":"h1,h2,h3,h4,h5,h6","collapseDepth":0},"lazyload":{"enable":true,"loading_img":"/img/loading.gif","onlypost":false,"offset_factor":2},"web_analytics":{"enable":true,"follow_dnt":true,"baidu":null,"google":{"measurement_id":null},"tencent":{"sid":null,"cid":null},"leancloud":{"app_id":"mEwkD7geCX7RWNO8srhDE2oP-gzGzoHsz","app_key":"4EF76hyiVA1QILHZThVbfoat","server_url":"https://mewkd7ge.lc-cn-n1-shared.com","path":"window.location.pathname","ignore_local":false},"umami":{"src":null,"website_id":null,"domains":null,"start_time":"2024-01-01T00:00:00.000Z","token":null,"api_server":null}},"search_path":"/local-search.xml","include_content_in_search":true};
if (CONFIG.web_analytics.follow_dnt) {
var dntVal = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
Fluid.ctx.dnt = dntVal && (dntVal.startsWith('1') || dntVal.startsWith('yes') || dntVal.startsWith('on'));
}
</script>
<script src="/js/utils.js"></script>
<script src="/js/color-schema.js"></script>
<meta name="google-site-verification" content="5KfTZvtlydmBIzY97rVo-caP5jljAfmD3Qs3sBzY2Go"><style>ins.adsbygoogle[data-ad-status="unfilled"] { display: none !important; }</style>
<meta name="generator" content="Hexo 7.3.0"></head>
<body>
<header>
<div class="header-inner" style="height: 70vh;">
<nav id="navbar" class="navbar fixed-top navbar-expand-lg navbar-dark scrolling-navbar">
<div class="container">
<a class="navbar-brand" href="/">
<strong>Ark的技术博客</strong>
</a>
<button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<div class="animated-icon"><span></span><span></span><span></span></div>
</button>
<!-- Collapsible content -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto text-center">
<li class="nav-item">
<a class="nav-link" href="/" target="_self">
<i class="iconfont icon-home-fill"></i>
<span>快乐老家</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/archives/" target="_self">
<i class="iconfont icon-archive-fill"></i>
<span>笔岚档案</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/categories/" target="_self">
<i class="iconfont icon-category-fill"></i>
<span>文以类聚</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tags/" target="_self">
<i class="iconfont icon-tags-fill"></i>
<span>按标签阅览</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about/" target="_self">
<i class="iconfont icon-user-fill"></i>
<span>联系我</span>
</a>
</li>
<li class="nav-item" id="search-btn">
<a class="nav-link" target="_self" href="javascript:;" data-toggle="modal" data-target="#modalSearch" aria-label="Search">
<i class="iconfont icon-search"></i>
</a>
</li>
<li class="nav-item" id="color-toggle-btn">
<a class="nav-link" target="_self" href="javascript:;" aria-label="Color Toggle">
<i class="iconfont icon-dark" id="color-toggle-icon"></i>
</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="banner" class="banner" parallax="true" style="background: url('/images/nest-logo.png') no-repeat center center; background-size: cover;">
<div class="full-bg-img">
<div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
<div class="banner-text text-center fade-in-up">
<div class="h2">
<span id="subtitle" data-typed-text="NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤"></span>
</div>
<div class="mt-3">
<span class="post-meta">
<i class="iconfont icon-date-fill" aria-hidden="true"></i>
<time datetime="2026-02-08 17:40" pubdate>
February 8, 2026 pm
</time>
</span>
</div>
<div class="mt-1">
<span class="post-meta mr-2">
<i class="iconfont icon-chart"></i>
7.7k words
</span>
<span class="post-meta mr-2">
<i class="iconfont icon-clock-fill"></i>
65 mins
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<div class="container-fluid nopadding-x">
<div class="row nomargin-x">
<div class="side-col d-none d-lg-block col-lg-2">
<aside class="sidebar d-none d-xl-block" style="margin-right:-1rem;z-index:-1"><ins class="adsbygoogle" style="display:flex;justify-content:center;min-width:160px;max-width:300px;width:100%;height:600px;position:sticky;top:2rem" data-ad-client="ca-pub-3240358922862141" data-ad-slot="6522434837"></ins><script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></aside>
</div>
<div class="col-lg-8 nopadding-x-md">
<div class="container nopadding-x-md" id="board-ctn">
<div id="board">
<article class="post-content mx-auto">
<h1 id="seo-header">NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤</h1>
<div class="markdown-body">
<h1 id="NestJS全栈实战笔记:序列化与更优雅的响应结果处理"><a href="#NestJS全栈实战笔记:序列化与更优雅的响应结果处理" class="headerlink" title="NestJS全栈实战笔记:序列化与更优雅的响应结果处理"></a>NestJS全栈实战笔记:序列化与更优雅的响应结果处理</h1><p>在利用AI完成Nestjs全栈开发中,一个常见但容易被<strong>0后端经验开发者</strong>忽视的问题是:<strong>如何优雅地控制 HTTP 响应体结构,避免手动拼装导致重复、出错以及难以维护?</strong></p>
<p>传统做法通常是在<code>service</code>层或 <code>controller</code> 层手动构造返回对象,例如创建一个新的对象、解构赋值某些字段、手动过滤敏感字段等,而且几乎成为了所有AI的<strong>默认做法</strong>,但这种做法真的就是万全之策吗?在项目初期看似简单、符合直觉,甚至AI的上下文也能很好的记忆其属性和字段,但随着业务增长,它带来的问题痛点也愈发明显:</p>
<ul>
<li>需要写大量重复代码;</li>
<li>每次实体属性变更都要手动同步响应处理逻辑;</li>
<li>容易忘记某些字段导致响应结构不一致;</li>
<li>无法利用 ORM 元数据、类型系统带来的优势。</li>
</ul>
<p><code>NestJS</code> 与 <code>class-transformer</code> 结合提供了一套更优雅的响应控制机制,使得序列化(serialization)成为可声明、可复用的过程,从而大幅提升代码质量及开发体验。</p>
<h3 id="序列化与反序列化的概念"><a href="#序列化与反序列化的概念" class="headerlink" title="序列化与反序列化的概念"></a>序列化与反序列化的概念</h3><p><strong>想要清晰的解决问题,就必须掌握好扎实的技术基础,否则我们甚至都无法清晰的把自己的想法表述给AI,甚至都不知道我们绞尽脑汁的方案,早有智者给我铺好了路。</strong></p>
<h4 id="1-为什么需要序列化?"><a href="#1-为什么需要序列化?" class="headerlink" title="1. 为什么需要序列化?"></a>1. 为什么需要序列化?</h4><p>在 Node.js 服务端,一切数据都以 <strong>内存对象</strong> 的形式存在,这些对象可能来自 ORM、业务逻辑封装、缓存结构等,它们可以有方法、原型链、元数据等复杂特性。</p>
<p>然而,<strong>HTTP 协议是基于文本协议的</strong>,只能在 TCP 连接上发送一串又一串的字符,而不能直接理解内存中的 JavaScript 对象结构。因此必须先把这些对象“变成文本”再发送,这个动作就叫 <strong>序列化(serialization)</strong> 。在 Web 开发里最常用的序列化格式是 JSON,这是因为它轻量、跨语言、与 JavaScript 语法天然契合并被所有浏览器与服务器端环境支持。</p>
<h4 id="2-Node-js-中的序列化过程"><a href="#2-Node-js-中的序列化过程" class="headerlink" title="2. Node.js 中的序列化过程"></a>2. Node.js 中的序列化过程</h4><p>在 Node.js 中,JSON 序列化的核心 API 是:</p>
<figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs angelscript">JSON.<span class="hljs-built_in">string</span>ify(obj)<br></code></pre></td></tr></table></figure>
<p>它以文本格式生成一个符合 JSON 标准的字符串,供网络传输或写入文件等用途。这个函数的基本规则有几点:</p>
<ul>
<li>只输出标准 JSON 支持的类型,如数字、字符串、布尔值、对象、数组、null。</li>
<li>忽略 <code>undefined</code>、函数、Symbol,或者将它们转换为 <code>null</code>。</li>
<li>对象的键和值都必须是 JSON 允许的格式,否则会丢失或报错。</li>
<li>对于复杂数据类型(如 Date),默认行为是转换为字符串。</li>
</ul>
<p>举例:</p>
<figure class="highlight pgsql"><table><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><code class="hljs pgsql">const <span class="hljs-keyword">user</span> = { id: <span class="hljs-number">1</span>, <span class="hljs-type">name</span>: "Alice", <span class="hljs-keyword">password</span>: "secret" };<br>const jsonString = <span class="hljs-type">JSON</span>.stringify(<span class="hljs-keyword">user</span>);<br>// 结果 {"id":<span class="hljs-number">1</span>,"name":"Alice","password":"secret"}<br></code></pre></td></tr></table></figure>
<p>这个字符串才是可以通过 HTTP 响应发送给浏览器的真正“有效载荷”。</p>
<p>在使用现代框架(比如 Express、NestJS)时,这一步通常被框架封装好了:<code>res.json()</code> 或者 NestJS 调用的序列化内部流程都在底层执行了 <code>JSON.stringify()</code>。</p>
<h4 id="3-HTTP-层如何传输序列化后的-JSON"><a href="#3-HTTP-层如何传输序列化后的-JSON" class="headerlink" title="3. HTTP 层如何传输序列化后的 JSON"></a>3. HTTP 层如何传输序列化后的 JSON</h4><p>当服务器准备好 JSON 字符串之后,它会配合 HTTP 头部告诉客户端它发送的数据类型:</p>
<figure class="highlight http"><table><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><code class="hljs http"><span class="hljs-meta">HTTP/1.1</span> <span class="hljs-number">200</span> OK<br><span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=utf-8<br><br><span class="language-json"><span class="hljs-punctuation">{</span><span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span><span class="hljs-number">1</span><span class="hljs-punctuation">,</span><span class="hljs-attr">"name"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"Alice"</span><span class="hljs-punctuation">,</span><span class="hljs-attr">"password"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"secret"</span><span class="hljs-punctuation">}</span></span><br></code></pre></td></tr></table></figure>
<p>在这里:</p>
<ul>
<li><code>Content-Type: application/json</code> 告诉浏览器响应体是 JSON;</li>
<li>HTTP 响应体本质上是一个<strong>文本流</strong>,可以被客户端逐块接收。</li>
</ul>
<p>HTTP 协议不会检查内部结构是否 JSON,它只是负责把这串字符送达。</p>
<h4 id="4-浏览器端的反序列化过程"><a href="#4-浏览器端的反序列化过程" class="headerlink" title="4. 浏览器端的反序列化过程"></a>4. 浏览器端的反序列化过程</h4><p>浏览器接收到这个文本响应后,如果我们使用 <code>fetch</code> API,一般会这样处理:</p>
<figure class="highlight js"><table><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><code class="hljs js"><span class="hljs-title function_">fetch</span>(<span class="hljs-string">'/api/user'</span>)<br> .<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.<span class="hljs-title function_">json</span>())<br> .<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">data</span> =></span> {<br> <span class="hljs-comment">// data 就是反序列化后的对象</span><br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(data.<span class="hljs-property">name</span>);<br> });<br></code></pre></td></tr></table></figure>
<p>这里的 <code>.json()</code> 方法就是浏览器为我们封装的 <strong>反序列化(deserialization)</strong> 动作。它会:</p>
<ol>
<li>从 HTTP 响应中读取完整的文本;</li>
<li>使用内部实现的 JSON 解析逻辑(类似 <code>JSON.parse()</code>);</li>
<li>将 JSON 字符串还原成 JavaScript 原生对象(没有方法、原型、Class 等行为,仅是纯数据)。</li>
</ol>
<p>最终,前端得到的是一个普通的对象:</p>
<figure class="highlight json"><table><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><code class="hljs json"><span class="hljs-punctuation">{</span><br> id<span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span><br> name<span class="hljs-punctuation">:</span> <span class="hljs-string">"Alice"</span><span class="hljs-punctuation">,</span><br> password<span class="hljs-punctuation">:</span> <span class="hljs-string">"secret"</span><br><span class="hljs-punctuation">}</span><br></code></pre></td></tr></table></figure>
<p>这时它可以在浏览器逻辑中自由访问,但它<strong>不再是服务端 ORM 的实体实例</strong>,没有方法、没有元数据,仅是一个普通的结构体数据。</p>
<h4 id="5-为什么反序列化后不是原生对象?"><a href="#5-为什么反序列化后不是原生对象?" class="headerlink" title="5. 为什么反序列化后不是原生对象?"></a>5. 为什么反序列化后不是原生对象?</h4><p>这是因为 JSON 是一种 <strong>纯文本、纯数据格式</strong>,只包含值,不包含行为或函数等:</p>
<ul>
<li>它不包含原型链信息;</li>
<li>不包含类方法;</li>
<li>不包含私有数据结构。<br>所以一旦反序列化后,所有原本对象的行为(包括 ORM 实例方法等)都消失了。浏览器端得到的是一个简单数据容器。</li>
</ul>
<h4 id="6-一个完整的端到端过程回顾"><a href="#6-一个完整的端到端过程回顾" class="headerlink" title="6. 一个完整的端到端过程回顾"></a>6. 一个完整的端到端过程回顾</h4><ol>
<li><strong>内存对象(服务端)</strong><br>例如一个 ORM 实例,包含方法、属性、元数据等。</li>
<li><strong>序列化为 JSON 字符串</strong><br>Node.js 使用 <code>JSON.stringify()</code>,输出严格的结构化文本。</li>
<li><strong>HTTP 响应发送 JSON 字符串</strong><br>浏览器通过网络接收这串字符。</li>
<li><strong>浏览器将 JSON 解析回 JavaScript 对象</strong><br>调用 <code>.json()</code> 或 <code>JSON.parse()</code>,得到普通对象。</li>
<li><strong>前端使用这个对象作为数据模型或渲染依据</strong></li>
</ol>
<h4 id="7-序列化-反序列化的工程影响"><a href="#7-序列化-反序列化的工程影响" class="headerlink" title="7. 序列化/反序列化的工程影响"></a>7. 序列化/反序列化的工程影响</h4><p>这个过程虽看似简单,但有几个实际需要注意的点:</p>
<ul>
<li>你永远无法把服务端的实例行为“原路还原”到前端;</li>
<li>JSON 不支持函数或原型,不支持循环引用;</li>
<li>提前规划好响应结构,避免发送敏感字段或大量无用数据。<br>因此,使用像 NestJS 提供的序列化控制(比如通过 DTO 或 <code>class-transformer</code>)来定制最终的 JSON 输出,在工程实践中是非常值得的优化,不仅改善了开发效率,还提高了 API 的一致性和安全性。</li>
</ul>
<h3 id="业务开发中的真实痛点"><a href="#业务开发中的真实痛点" class="headerlink" title="业务开发中的真实痛点"></a>业务开发中的真实痛点</h3><p>理解了上述基础后,我们来看看在实际NestJS业务开发中,处理API响应时经常面临的三个真实痛点:</p>
<p>第一,手动构建响应对象不够优雅。在控制器层返回数据时,开发者经常需要手动创建新对象并进行解构赋值,将ORM实例的属性逐一填入。这种做法需要编写大量样板代码,无法利用ORM工具和类本身的特性,且在属性众多时极易发生漏写或错写。</p>
<p>第二,ORM实体与响应结果存在割裂。随着业务迭代,数据库表结构会发生变化,ORM层中的实体类也会随之更新。如果我们在控制器中采用手动赋值的方式,即使实体类增加了新属性,只要控制器代码没有同步修改,新属性就无法自动体现在API响应中。这增加了维护成本,容易导致前后端数据不同步。</p>
<p>第三,DTO与实体的重复声明问题。为了规范接口,我们通常会编写DTO(数据传输对象)来约束请求和响应格式。在很多场景下,我们希望响应数据基于数据库实体,但需要遮蔽部分敏感字段。如果为了实现遮蔽而重新手写一个DTO,会导致DTO类和实体类中存在大量重复的字段声明。这不仅违背了DRY(Don’t Repeat Yourself)原则,手动解构赋值也无法从根源上保障数据结构的严谨性。</p>
<h3 id="解决方案:类型映射与序列化的结合"><a href="#解决方案:类型映射与序列化的结合" class="headerlink" title="解决方案:类型映射与序列化的结合"></a>解决方案:类型映射与序列化的结合</h3><p>为了解决上述问题,最佳实践是引入<code>class-transformer</code>库的装饰器,配合NestJS内置的类型映射(<code>Mapped Types</code>)功能以及<code>plainToInstance</code>序列化方法。</p>
<p>NestJS本身并不直接操作 <code>JSON.stringify</code>,而是通过 <code>class-transformer</code> 库,通过装饰器在类上声明序列化行为,并在响应时应用这些行为,利用<code>class-transformer</code>,我们可以实现对JavaScript类的更深入的操作。</p>
<p>基础概念来自官方文档:<br><a href="%22https://docs.nestjs.cn/techniques/serialization%22">序列化</a><br><a href="%22%22https://docs.nestjs.cn/graphql/mapped-types#%E6%98%A0%E5%B0%84%E7%B1%BB%E5%9E%8B">类型映射</a></p>
<ul>
<li><strong>@Exclude</strong>:排除字段,不出现在序列化输出;</li>
<li><strong>@Expose</strong>:显式声明字段可序列化;</li>
<li><strong>plainToInstance / instanceToPlain</strong>:在 plain object 与 Class 实例之间转换对象;</li>
<li><strong>ClassSerializerInterceptor</strong>:拦截器,在控制器返回对象时执行序列化转换。</li>
</ul>
<p>这种做法带来的好处在于,它把字段控制的逻辑放到了类定义层,而不是分散在每个 API 的手动处理逻辑里。</p>
<p>通过这套组合拳,我们可以将ORM实体作为唯一的真实数据源(Single Source of Truth),让DTO直接继承实体的定义,再通过序列化机制自动过滤敏感信息,从而实现高度复用和自动化的响应处理。</p>
<h4 id="解决方案:结合-class-transformer-的序列化策略"><a href="#解决方案:结合-class-transformer-的序列化策略" class="headerlink" title="解决方案:结合 class-transformer 的序列化策略"></a>解决方案:结合 class-transformer 的序列化策略</h4><p>为了解决这些问题,可以借助以下几种 NestJS 级工具:</p>
<h4 id="•-利用-Expose-与-Exclude-控制序列化字段"><a href="#•-利用-Expose-与-Exclude-控制序列化字段" class="headerlink" title="• 利用 @Expose 与 @Exclude 控制序列化字段"></a>• 利用 <code>@Expose</code> 与 <code>@Exclude</code> 控制序列化字段</h4><p>通过在实体类中使用 <code>class-transformer</code> 的装饰器,可以精确控制哪些字段最终出现在序列化输出。例如:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-meta">@Exclude</span>()<br>password!: <span class="hljs-built_in">string</span>;<br></code></pre></td></tr></table></figure>
<p>使得 <code>password</code> 字段不会包含在 HTTP 相应响应里。</p>
<p>在你的实体定义中,采用了 <code>@Expose()</code> 和 <code>@Exclude()</code> 有选择地声明字段:</p>
<figure class="highlight typescript"><table><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><code class="hljs typescript"><span class="hljs-meta">@Expose</span>()<br><span class="hljs-meta">@ApiProperty</span>(...)<br>id!: <span class="hljs-built_in">number</span>;<br><br><span class="hljs-meta">@Exclude</span>()<br><span class="hljs-meta">@property</span>()<br>password!: <span class="hljs-built_in">string</span>;<br><br><span class="hljs-meta">@Exclude</span>()<br><span class="hljs-meta">@property</span>()<br><span class="hljs-attr">hashedRefreshToken</span>?: <span class="hljs-built_in">string</span>;<br></code></pre></td></tr></table></figure>
<p>这种方式避免了每次显式构造返回对象的必要,序列化过程可以自动过滤敏感字段。</p>
<h4 id="•-使用-mapped-types-简化-DTO-定义"><a href="#•-使用-mapped-types-简化-DTO-定义" class="headerlink" title="• 使用 mapped-types 简化 DTO 定义"></a>• 使用 mapped-types 简化 DTO 定义</h4><p>NestJS 官方提供的映射类型工具(如 <code>OmitType</code>、<code>PartialType</code> 等)可以在已有类上派生新的 DTO,而不需要重复声明字段。</p>
<p>在用户模块中,我们就定义了多个基于实体的 DTO:</p>
<figure class="highlight typescript"><table><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><code class="hljs typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserResponseDto</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">OmitType</span>(<span class="hljs-title class_">User</span>, [<br> <span class="hljs-string">'password'</span>,<br> <span class="hljs-string">'hashedRefreshToken'</span>,<br>] <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>) {}<br></code></pre></td></tr></table></figure>
<p>这样定义后,响应 DTO 自动继承了实体的字段,排除了敏感项。</p>
<h4 id="•-使用-plainToInstance-进行纯净转换"><a href="#•-使用-plainToInstance-进行纯净转换" class="headerlink" title="• 使用 plainToInstance 进行纯净转换"></a>• 使用 <code>plainToInstance</code> 进行纯净转换</h4><p>在 controller 层,我们调用进来 ORM 查询的实体对象通常是一个模型实例。为了确保最终输出符合 DTO 声明的序列化规则,需要显式执行转换:</p>
<figure class="highlight typescript"><table><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><code class="hljs typescript"><span class="hljs-keyword">return</span> <span class="hljs-title function_">plainToInstance</span>(<span class="hljs-title class_">MeResponseDto</span>, user, {<br> <span class="hljs-attr">excludeExtraneousValues</span>: <span class="hljs-literal">true</span>,<br>});<br></code></pre></td></tr></table></figure>
<p>这里:</p>
<ul>
<li><code>MeResponseDto</code> 是基于实体定义但排除了敏感字段的响应模型;</li>
<li><code>excludeExtraneousValues: true</code> 确保只输出有 <code>@Expose()</code> 或在 DTO 中声明的字段。</li>
</ul>
<h3 id="业务场景实战解析"><a href="#业务场景实战解析" class="headerlink" title="业务场景实战解析"></a>业务场景实战解析</h3><p>接下来,我们将结合真实场景的代码,逐步解析这套方案的落地过程。在我遇到的真实开发场景中,项目要求我开发<code>GET user/me</code>的RESTful API,在这个API下,我们需要实现获取当前用户信息的API,并且要求在返回结果中严格剔除用户的密码和哈希处理后的刷新令牌。</p>
<h4 id="1-实体类的基础定义与装饰器配置"><a href="#1-实体类的基础定义与装饰器配置" class="headerlink" title="1. 实体类的基础定义与装饰器配置"></a>1. 实体类的基础定义与装饰器配置</h4><p>首先,我们查看数据库实体类的定义。在这个类中,我们不仅定义了数据库映射关系,同时引入了<code>class-transformer</code>的<code>Expose</code>和<code>Exclude</code>装饰器。</p>
<figure class="highlight typescript"><table><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></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// user.entity.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Entity</span>, <span class="hljs-title class_">PrimaryKey</span>, <span class="hljs-title class_">Property</span>, <span class="hljs-title class_">Unique</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@mikro-orm/core'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">ApiProperty</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/swagger'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Exclude</span>, <span class="hljs-title class_">Expose</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>;<br><br><span class="hljs-meta">@Entity</span>({ <span class="hljs-attr">tableName</span>: <span class="hljs-string">'users'</span> })<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span> {<br> <span class="hljs-meta">@Expose</span>()<br> <span class="hljs-meta">@ApiProperty</span>({ <span class="hljs-attr">description</span>: <span class="hljs-string">'用户唯一标识ID'</span> })<br> <span class="hljs-meta">@PrimaryKey</span>()<br> id!: <span class="hljs-built_in">number</span>;<br><br> <span class="hljs-meta">@Expose</span>()<br> <span class="hljs-meta">@ApiProperty</span>({ <span class="hljs-attr">description</span>: <span class="hljs-string">'学校学号'</span> })<br> <span class="hljs-meta">@Unique</span>()<br> <span class="hljs-meta">@Property</span>({ <span class="hljs-attr">nullable</span>: <span class="hljs-literal">false</span> })<br> schoolId!: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// 关键点:使用Exclude标记敏感字段</span><br> <span class="hljs-meta">@Exclude</span>()<br> <span class="hljs-meta">@Property</span>()<br> password!: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// 关键点:同样使用Exclude标记刷新令牌</span><br> <span class="hljs-meta">@Exclude</span>()<br> <span class="hljs-meta">@Property</span>({ <span class="hljs-attr">nullable</span>: <span class="hljs-literal">true</span> })<br> <span class="hljs-attr">hashedRefreshToken</span>?: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// ... 其他属性省略</span><br>}<br></code></pre></td></tr></table></figure>
<p>在这里,<code>@Expose</code>表示该字段在序列化时应该被暴露(保留),而<code>@Exclude</code>则明确指示该字段在序列化过程中必须被剔除。这就在数据源头确立了序列化规则。</p>
<h4 id="2-使用类型映射构建DTO"><a href="#2-使用类型映射构建DTO" class="headerlink" title="2. 使用类型映射构建DTO"></a>2. 使用类型映射构建DTO</h4><p>确立了实体类后,我们不需要重新编写一个包含几十个字段的响应DTO。借助NestJS提供的<code>OmitType</code>(也可以是<code>PickType</code>等),我们可以直接从User实体中派生出新的DTO类。</p>
<figure class="highlight typescript"><table><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><code class="hljs typescript"><span class="hljs-comment">// user-response.dto.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">OmitType</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/swagger'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">User</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'../entities/user.entity'</span>;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">MeResponseDto</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">OmitType</span>(<span class="hljs-title class_">User</span>, [<br> <span class="hljs-string">'password'</span>,<br> <span class="hljs-string">'hashedRefreshToken'</span>,<br> <span class="hljs-string">'avatarUrl'</span>,<br> <span class="hljs-string">'createdAt'</span>,<br> <span class="hljs-string">'updatedAt'</span>,<br>] <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>) {}<br></code></pre></td></tr></table></figure>
<p>OmitType在类型层面去除了User类中的指定字段,生成了MeResponseDto。这种做法完全遵循DRY原则,当User实体新增了普通业务字段(如age)并标记为Expose时,MeResponseDto会自动继承该字段,无需任何额外修改,直接解决了上文提到的维护割裂问题。</p>
<h4 id="3-控制器层的数据处理与转换"><a href="#3-控制器层的数据处理与转换" class="headerlink" title="3. 控制器层的数据处理与转换"></a>3. 控制器层的数据处理与转换</h4><p>最后一步是在控制器中应用这些规则。我们获取到ORM返回的用户实例后,不再手动拼接对象,而是使用plainToInstance进行转换。</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// user.controller.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Controller</span>, <span class="hljs-title class_">Get</span>, <span class="hljs-title class_">Req</span>, <span class="hljs-title class_">UnauthorizedException</span>, <span class="hljs-title class_">UseGuards</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;<br><span class="hljs-keyword">import</span> { plainToInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">MeResponseDto</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/user-response.dto'</span>;<br><span class="hljs-comment">// ... 其他导入省略</span><br><br><span class="hljs-meta">@Controller</span>(<span class="hljs-string">'user'</span>)<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserController</span> {<br> <span class="hljs-title function_">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-attr">userService</span>: <span class="hljs-title class_">UserService</span></span>) {}<br><br> <span class="hljs-meta">@UseGuards</span>(<span class="hljs-title class_">JwtAuthGuard</span>)<br> <span class="hljs-meta">@Get</span>(<span class="hljs-string">'me'</span>)<br> <span class="hljs-keyword">async</span> <span class="hljs-title function_">getMe</span>(<span class="hljs-meta">@Req</span>() <span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">MeResponseDto</span>> {<br> <span class="hljs-keyword">const</span> userPayload = req[<span class="hljs-string">'user'</span>];<br><br> <span class="hljs-comment">// 从数据库获取ORM实体实例</span><br> <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">userService</span>.<span class="hljs-title function_">findOneBySchoolId</span>(userPayload.<span class="hljs-property">schoolId</span>);<br><br> <span class="hljs-keyword">if</span> (!user) {<br> <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">UnauthorizedException</span>(<span class="hljs-string">'用户不存在'</span>);<br> }<br><br> <span class="hljs-comment">// 关键点:使用plainToInstance进行序列化转换</span><br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">plainToInstance</span>(<span class="hljs-title class_">MeResponseDto</span>, user, {<br> <span class="hljs-attr">excludeExtraneousValues</span>: <span class="hljs-literal">true</span>,<br> });<br> }<br>}<br></code></pre></td></tr></table></figure>
<p>在这里,<code>plainToInstance</code>接收目标类<code>MeResponseDto</code>和原始数据<code>user</code>。配置项<code>excludeExtraneousValues</code>设为<code>true</code>是核心所在,它告诉转换器:只保留目标类中带有<code>@Expose</code>装饰器的属性,其余属性一律丢弃。</p>
<p>由于我们在<code>User</code>实体中对<code>password</code>等字段使用了<code>@Exclude</code>(未带有<code>@Expose</code>),在转换过程中这些敏感信息会被彻底剥离。最终返回给前端的JSON数据完全符合我们期望的安全结构。</p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>通过在实体类中使用<code>@Expose</code>和<code>@Exclude</code>,结合NestJS的<code>Mapped Types</code>复用类型定义,并在控制器端通过<code>plainToInstance</code>进行数据清洗,我们建立了一套清晰且健壮的响应处理机制。这种做法消除了冗余代码,降低了因遗忘导致的敏感数据泄露风险。</p>
<p>即便是在AI时代,提高项目的工程化质量不仅使NestJS后端的开发体验更加工程化和优雅,也可以变相促进AI更好的利用仓库代码已有的最佳实践,从而提升整体项目的健壮性和可维护性。</p>
</div>
<hr>
<div>
<div class="post-metas my-3">
<div class="post-meta mr-3 d-flex align-items-center">
<i class="iconfont icon-category"></i>
<span class="category-chains">
<span class="category-chain">
<a href="/categories/backend/" class="category-chain-item">backend</a>
</span>
</span>
</div>
<div class="post-meta">
<i class="iconfont icon-tags"></i>
<a href="/tags/nodejs/" class="print-no-link">#nodejs</a>
<a href="/tags/backend/" class="print-no-link">#backend</a>
<a href="/tags/nestjs/" class="print-no-link">#nestjs</a>
<a href="/tags/fullstack/" class="print-no-link">#fullstack</a>
</div>
</div>
<div class="license-box my-3">
<div class="license-title">
<div>NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤</div>
<div>http://arkpln.github.io/2862114693.html</div>
</div>
<div class="license-meta">
<div class="license-meta-item">
<div>Author</div>
<div>FangZhou</div>
</div>
<div class="license-meta-item license-meta-date">
<div>Posted on</div>
<div>February 8, 2026</div>
</div>
<div class="license-meta-item">
<div>Licensed under</div>
<div>
<a class="print-no-link" target="_blank" href="https://creativecommons.org/licenses/by/4.0/" rel="external nofollow noopener noreferrer">
<span class="hint--top hint--rounded" aria-label="BY - Attribution">
<i class="iconfont icon-cc-by"></i>
</span>
</a>
</div>
</div>
</div>
<div class="license-icon iconfont"></div>
</div>
<div style="width:100%;display:flex;justify-content:center;margin-bottom:1.5rem"><ins class="adsbygoogle" style="display:flex;justify-content:center;max-width:845px;width:100%;height:90px" data-ad-client="ca-pub-3240358922862141" data-ad-slot="6522434837"></ins><script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></div>
<div class="post-prevnext my-3">
<article class="post-prev col-6">
<a href="/4238683975.html" title="NestJS全栈实战笔记:序列化与更优雅的响应结果处理">
<i class="iconfont icon-arrowleft"></i>
<span class="hidden-mobile">NestJS全栈实战笔记:序列化与更优雅的响应结果处理</span>
<span class="visible-mobile">Previous</span>
</a>
</article>
<article class="post-next col-6">
<a href="/2379479118.html" title="Expo React Native + EAS 本地/云端实战真实构建避坑指南">
<span class="hidden-mobile">Expo React Native + EAS 本地/云端实战真实构建避坑指南</span>
<span class="visible-mobile">Next</span>
<i class="iconfont icon-arrowright"></i>
</a>
</article>
</div>
</div>
<article id="comments" lazyload>
<div id="valine"></div>
<script type="text/javascript">
Fluid.utils.loadComments('#valine', function() {
Fluid.utils.createScript('https://lib.baomitu.com/valine/1.5.1/Valine.min.js', function() {
var options = Object.assign(
{"appId":"mEwkD7geCX7RWNO8srhDE2oP-gzGzoHsz","appKey":"4EF76hyiVA1QILHZThVbfoat","path":"window.location.pathname","placeholder":null,"avatar":"retro","meta":["nick","mail","link"],"requiredFields":[],"pageSize":10,"lang":"zh-CN","highlight":false,"recordIP":false,"serverURLs":"","emojiCDN":null,"emojiMaps":null,"enableQQ":false,"el":"#vcomments"},
{
el: "#valine",
path: window.location.pathname
}
)
new Valine(options);
Fluid.utils.waitElementVisible('#valine .vcontent', () => {
var imgSelector = '#valine .vcontent img:not(.vemoji)';
Fluid.plugins.imageCaption(imgSelector);
Fluid.plugins.fancyBox(imgSelector);
})
});
});
</script>
<noscript>Please enable JavaScript to view the comments</noscript>
</article>
</article>
</div>
</div>
</div>
<div class="side-col d-none d-lg-block col-lg-2">
<aside class="sidebar" style="margin-left: -1rem">
<div id="toc">
<p class="toc-header">
<i class="iconfont icon-list"></i>
<span>Table of Contents</span>
</p>
<div class="toc-body" id="toc-body"></div>
</div>
</aside>
</div>
</div>
</div>
<a id="scroll-top-button" aria-label="TOP" href="#" role="button">
<i class="iconfont icon-arrowup" aria-hidden="true"></i>
</a>
<div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<div class="modal-content">
<div class="modal-header text-center">
<h4 class="modal-title w-100 font-weight-bold">Search</h4>
<button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body mx-3">
<div class="md-form mb-5">
<input type="text" id="local-search-input" class="form-control validate">
<label data-error="x" data-success="v" for="local-search-input">Keyword</label>
</div>
<div class="list-group" id="local-search-result"></div>
</div>
</div>
</div>
</div>
</main>
<footer>
<div class="footer-inner">
<div class="footer-content">
<a href="https://hexo.io" target="_blank" rel="external nofollow noopener noreferrer"><span>Hexo</span></a> <i class="iconfont icon-love"></i> <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="external nofollow noopener noreferrer"><span>Fluid</span></a>
</div>
<div class="statistics">
<span id="leancloud-site-pv-container" style="display: none">
总访问量
<span id="leancloud-site-pv"></span>
次
</span>
<span id="leancloud-site-uv-container" style="display: none">
总访客数
<span id="leancloud-site-uv"></span>
人
</span>
</div>
</div>
</footer>
<!-- Scripts -->
<script src="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.js"></script>
<link rel="stylesheet" href="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.css">
<script>
NProgress.configure({"showSpinner":false,"trickleSpeed":100})
NProgress.start()
window.addEventListener('load', function() {
NProgress.done();
})
</script>
<script src="https://lib.baomitu.com/jquery/3.6.4/jquery.min.js"></script>
<script src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js"></script>
<script src="/js/events.js"></script>
<script src="/js/plugins.js"></script>
<script src="https://lib.baomitu.com/typed.js/2.0.12/typed.min.js"></script>
<script>
(function (window, document) {
var typing = Fluid.plugins.typing;
var subtitle = document.getElementById('subtitle');
if (!subtitle || !typing) {
return;
}
var text = subtitle.getAttribute('data-typed-text');
typing(text);
})(window, document);
</script>
<script src="/js/img-lazyload.js"></script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/tocbot/4.20.1/tocbot.min.js', function() {
var toc = jQuery('#toc');
if (toc.length === 0 || !window.tocbot) { return; }
var boardCtn = jQuery('#board-ctn');
var boardTop = boardCtn.offset().top;
window.tocbot.init(Object.assign({
tocSelector : '#toc-body',
contentSelector : '.markdown-body',
linkClass : 'tocbot-link',
activeLinkClass : 'tocbot-active-link',
listClass : 'tocbot-list',
isCollapsedClass: 'tocbot-is-collapsed',
collapsibleClass: 'tocbot-is-collapsible',
scrollSmooth : true,
includeTitleTags: true,
headingsOffset : -boardTop,
}, CONFIG.toc));
if (toc.find('.toc-list-item').length > 0) {
toc.css('visibility', 'visible');
}
Fluid.events.registerRefreshCallback(function() {
if ('tocbot' in window) {
tocbot.refresh();
var toc = jQuery('#toc');
if (toc.length === 0 || !tocbot) {
return;
}
if (toc.find('.toc-list-item').length > 0) {
toc.css('visibility', 'visible');
}
}
});
});
</script>
<script src="https://lib.baomitu.com/clipboard.js/2.0.11/clipboard.min.js"></script>
<script>Fluid.plugins.codeWidget();</script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/anchor-js/5.0.0/anchor.min.js', function() {
window.anchors.options = {
placement: CONFIG.anchorjs.placement,
visible : CONFIG.anchorjs.visible
};
if (CONFIG.anchorjs.icon) {
window.anchors.options.icon = CONFIG.anchorjs.icon;
}
var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(',');
var res = [];
for (var item of el) {
res.push('.markdown-body > ' + item.trim());
}
if (CONFIG.anchorjs.placement === 'left') {
window.anchors.options.class = 'anchorjs-link-left';
}
window.anchors.add(res.join(', '));
Fluid.events.registerRefreshCallback(function() {
if ('anchors' in window) {
anchors.removeAll();
var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(',');
var res = [];
for (var item of el) {
res.push('.markdown-body > ' + item.trim());
}
if (CONFIG.anchorjs.placement === 'left') {
anchors.options.class = 'anchorjs-link-left';
}
anchors.add(res.join(', '));
}
});
});
</script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.js', function() {
Fluid.plugins.fancyBox();
});
</script>
<script>Fluid.plugins.imageCaption();</script>
<script defer src="/js/leancloud.js"></script>
<script src="/js/local-search.js"></script>
<!-- 主题的启动项,将它保持在最底部 -->
<!-- the boot of the theme, keep it at the bottom -->
<script src="/js/boot.js"></script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3240358922862141" crossorigin="anonymous"></script>
<noscript>
<div class="noscript-warning">Blog works best with JavaScript enabled</div>
</noscript>
</body>
</html>