diff options
| author | muqiuhan <[email protected]> | 2025-09-09 06:17:00 +0000 |
|---|---|---|
| committer | muqiuhan <[email protected]> | 2025-09-09 06:17:00 +0000 |
| commit | d5de65fdb1802cdf498d65d93397f290813c377c (patch) | |
| tree | 92fe61a76d20d1203203665d4b48e7f151f088bd /2025/07 | |
| parent | 48efa2dfde7c263f84ee5bb0872747034908d607 (diff) | |
| download | blog-d5de65fdb1802cdf498d65d93397f290813c377c.tar.gz | |
deploy: 9665097f0fa0dae9f92123fac54d26c0818758a5
Diffstat (limited to '2025/07')
| -rw-r--r-- | 2025/07/08/dadgad/index.html | 5 | ||||
| -rw-r--r-- | 2025/07/11/fuck-hcho/index.html | 75 | ||||
| -rw-r--r-- | 2025/07/11/rep-date/index.html | 14 | ||||
| -rw-r--r-- | 2025/07/29/nestjs-nested-object-dto/index.html | 42 | ||||
| -rw-r--r-- | 2025/07/31/svelte-lazyquery/index.html | 35 |
5 files changed, 105 insertions, 66 deletions
diff --git a/2025/07/08/dadgad/index.html b/2025/07/08/dadgad/index.html index a1c061b4..044a3a33 100644 --- a/2025/07/08/dadgad/index.html +++ b/2025/07/08/dadgad/index.html @@ -194,9 +194,10 @@ <div class="post-content"> <p>模态定弦的空弦音不直接构成传统意义上的大调或小调和弦,而是形成一种听感上更为空灵、悬浮的“挂留和弦”(Suspended Chord)效果。</p> <p><strong>DADGAD (发音为 “dad-gad”):</strong> 这是凯尔特音乐(Celtic Music)中最具代表性的定弦。它的空弦音给人一种既非大调也非小调的模糊感,充满了神秘和悠远的色彩,非常适合营造氛围音乐和复杂的指弹旋律。</p> -<p>挂留和弦(Suspended Chord),通常简写为 <strong>“sus”<strong>,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:</strong>用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音</strong>,从而创造出一种独特的声音效果。</p> +<p>挂留和弦(Suspended Chord),通常简写为 <strong>“sus”</strong>,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:<strong>用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音</strong>,从而创造出一种独特的声音效果。</p> <p>想象一个标准的大三和弦或小三和弦,它由<strong>根音、三度音和五度音</strong>构成。其中,<strong>三度音</strong>是决定这个和弦听起来是明亮、开心的“大调”,还是忧郁、伤感的“小调”的关键。</p> -<p>而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:<br>一是 <strong>纯四度音 (Perfect 4th)</strong> ,二是 <strong>大二度音 (Major 2nd)</strong> 。</p> +<p>而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:<br> +一是 <strong>纯四度音 (Perfect 4th)</strong> ,二是 <strong>大二度音 (Major 2nd)</strong> 。</p> <p><strong>sus4 和弦 (挂四和弦)</strong> 由 <strong>根音 (1) + 纯四度音 (4) + 纯五度音 (5)</strong> 构成,即用<strong>四度音</strong>替代了原本的三度音。听感上,这是最常见的挂留和弦。它创造出一种<strong>强烈而悬浮的张力感</strong>,声音既不属于大调也不属于小调,听起来非常“开阔”。例如,一个 <strong>Csus4</strong> 和弦,就是由 C、F、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。</p> <p><strong>sus2 和弦 (挂二和弦)</strong> 由 <strong>根音 (1) + 大二度音 (2) + 纯五度音 (5)</strong> 构成,特点是用二度音替代了原本的三度音。听感上,sus2和弦的声音比sus4更柔和、更明亮一些。它同样具有开放、模糊的色彩,但张力感稍弱,听起来更像是一种平静、梦幻的色彩和弦。例如,一个 <strong>Csus2</strong> 和弦,就是由 C、D、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。</p> <p>而 DADGAD 定弦方式的空弦音,其核心就是一个挂留和弦,具体来说是一个 Dsus4 和弦。当你弹响 DADGAD 的所有空弦时,你实际上听到的就是 <strong>D、G、A</strong> 这三个音在不同八度上的叠加和共鸣。这三个音不多不少,正好构成了 <strong>Dsus4</strong> 和弦。</p> diff --git a/2025/07/11/fuck-hcho/index.html b/2025/07/11/fuck-hcho/index.html index a3d153a1..6692c0f5 100644 --- a/2025/07/11/fuck-hcho/index.html +++ b/2025/07/11/fuck-hcho/index.html @@ -192,11 +192,12 @@ </div> </div> <div class="post-content"> - <p>前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,<del>我去吸了两口</del> ,先来看看一些无聊的总结:</p> + <p>前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,<s>我去吸了两口</s> ,先来看看一些无聊的总结:</p> <p>吸入甲醛后,人体之所以会产生眼部灼痛、喉咙刺痒、呼吸困难等一系列强烈的不适感,是因为甲醛作为一种高活性、强刺激性的化学物质,对我们的身体构成了从宏观到微观的多层次、多途径的直接攻击。其影响方式主要可以归结为两大方面:<strong>直接的刺激与腐蚀作用</strong>和<strong>深层的细胞毒性与遗传毒性</strong>。</p> <p>当甲醛气体通过呼吸进入人体,首当其冲的是我们的眼睛和呼吸道黏膜。这些部位的细胞直接暴露在甲醛的攻击下,引发一系列即时且强烈的不适症状:</p> <ul> -<li><strong>对黏膜的强烈刺激:</strong> 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。<ul> +<li><strong>对黏膜的强烈刺激:</strong> 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。 +<ul> <li><strong>眼部症状:</strong> 眼睛会感到灼烧、刺痛、发痒和不自主流泪。</li> <li><strong>呼吸道症状:</strong> 喉咙会感到干燥、刺痛、咳嗽,高浓度下更会引发胸闷、气喘,甚至喉头水肿和肺水肿,导致呼吸极度困难。</li> </ul> @@ -207,66 +208,82 @@ <ul> <li><strong>蛋白质变性:</strong> 甲醛能够与构成生命基础的蛋白质中的氨基(-NH2)发生反应,形成“亚甲基桥”,破坏蛋白质原有的三维结构,使其失去正常的生理功能。细胞的功能依赖于各种蛋白质(酶、结构蛋白等)的正常工作,蛋白质变性意味着细胞代谢紊乱、功能障碍,甚至死亡。这在本质上是一种“腐蚀”作用,也是福尔马林(甲醛水溶液)能用作防腐剂的根本原因。</li> <li><strong>诱导细胞凋亡与氧化应激:</strong> 研究表明,甲醛会损伤细胞内的能量工厂——线粒体,诱导细胞产生大量的活性氧(ROS)。过量的活性氧会破坏细胞膜、蛋白质和DNA,引发“氧化应激”反应,并最终启动细胞的程序性死亡(凋亡)机制。</li> -<li><strong>遗传毒性与致癌性:</strong> 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。<ul> +<li><strong>遗传毒性与致癌性:</strong> 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。 +<ul> <li><strong>致癌风险:</strong> 当这种基因损伤发生在关键的癌基因或抑癌基因上,并且人体的修复机制无法有效清除时,就可能导致细胞的异常增殖,最终引发癌症。世界卫生组织(WHO)下属的国际癌症研究机构(IARC)已将甲醛列为<strong>一类致癌物</strong>,有充分证据表明其与鼻咽癌、白血病等恶性肿瘤的发生密切相关。</li> </ul> </li> <li><strong>神经与免疫毒性:</strong> 甲醛还能对中枢神经系统产生毒性作用,导致头晕、头痛、记忆力减退、失眠等症状。同时,它也可能干扰免疫系统的正常功能,使人更容易过敏(如引发过敏性皮炎、哮喘)或抵抗力下降。</li> </ul> <hr> -<h2 id="甲醛与粘膜表面的蛋白质和水分发生反应?"><a href="#甲醛与粘膜表面的蛋白质和水分发生反应?" class="headerlink" title="甲醛与粘膜表面的蛋白质和水分发生反应?"></a>甲醛与粘膜表面的蛋白质和水分发生反应?</h2><p>当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 <strong>蛋白质</strong>。</p> +<h2 id="甲醛与粘膜表面的蛋白质和水分发生反应?"><a class="header-anchor" href="#甲醛与粘膜表面的蛋白质和水分发生反应?">¶</a>甲醛与粘膜表面的蛋白质和水分发生反应?</h2> +<p>当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 <strong>蛋白质</strong>。</p> <p>这个反应并非简单的接触,而是一种被称为“交联反应”(Cross-linking)的化学过程。具体来说:甲醛分子会精准地找到蛋白质分子链上的氨基(-NH₂),这是许多氨基酸的共同特征。接着与蛋白质的氨基发生反应,形成一个不稳定的中间体。这个中间体可以进一步与邻近的另一个蛋白质的氨基反应,从而在两个原本独立的蛋白质(或同一个蛋白质的不同部分)之间,强行架起一座“亚甲基桥”(-CH₂-)。</p> <p>蛋白质就像一团经过精确折叠、有着特定三维结构的精密机器,它的功能完全依赖于这个构型。而甲醛的“交联”反应,就像是往这台精密机器的各个活动关节之间,甚至和旁边的机器之间倒 502,给它们粘死了。</p> -<h3 id="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"><a href="#为什么甲醛分子能精准地找到蛋白质分子链上的氨基?" class="headerlink" title="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"></a>为什么甲醛分子能精准地找到蛋白质分子链上的氨基?</h3><p>“精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。</p> -<p>甲醛分子(HCHO)的核心是一个<strong>羰基(C=O)</strong>。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。</p> +<h3 id="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"><a class="header-anchor" href="#为什么甲醛分子能精准地找到蛋白质分子链上的氨基?">¶</a>为什么甲醛分子能精准地找到蛋白质分子链上的氨基?</h3> +<p>“精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。</p> +<p>甲醛分子(HCHO)的核心是一个<strong>羰基(C=O)</strong>。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。</p> <p>在化学上,这样一个“缺少电子”、对电子充满渴望的原子或基团,被称为“亲电体”(Electrophile)。</p> <p>氨基(-NH₂)在蛋白质中广泛存在,尤其是在赖氨酸、精氨酸等氨基酸的侧链上。氨基的核心是氮原子(N)。根据它的电子排布,氮原子的最外层有一对没有参与形成化学键的电子,这叫做 “孤对电子”(Lone Pair Electrons)。 这对孤对电子,使得氮原子成了一个电子云密度很高的区域,带上了<strong>部分负电荷(δ-)</strong>。</p> <p>这样一个拥有“富余”电子、并愿意用它去攻击其他正电中心的原子或基团,在化学上被称为 “亲核体”(Nucleophile)。</p> <p>当甲醛分子靠近蛋白质时,甲醛分子上带部分正电荷的碳原子(亲电体),与蛋白质氨基上带部分负电荷的氮原子(亲核体),会因基本的静电引力而相互靠近。一旦距离足够近,氨基上慷慨的“孤对电子”会毫不犹豫地“攻击”甲醛上那个渴望电子的碳原子,并与它形成一个新的化学键。</p> <p>这个过程,就是化学中的“亲核加成反应”。</p> -<h3 id="这个反应为何会损伤黏膜组织?"><a href="#这个反应为何会损伤黏膜组织?" class="headerlink" title="这个反应为何会损伤黏膜组织?"></a>这个反应为何会损伤黏膜组织?</h3><p>蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。</p> +<h3 id="这个反应为何会损伤黏膜组织?"><a class="header-anchor" href="#这个反应为何会损伤黏膜组织?">¶</a>这个反应为何会损伤黏膜组织?</h3> +<p>蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。</p> <p>首先黏膜细胞需要各种酶(本身也是蛋白质)来维持新陈代谢。被交联后,这些酶的结构被破坏,失去了活性,细胞的正常生理活动瞬间停滞。而细胞的骨架、细胞膜上的离子通道等结构蛋白,同样会被交联破坏。这导致细胞失去原有的形态,细胞膜的通透性被改变,甚至直接破裂。当大量的细胞功能瘫痪、结构崩解后,它们会迅速走向死亡。成片的细胞死亡,就意味着我们宏观可见的<strong>组织损伤</strong>。这本质上是一种<strong>微观的化学性灼伤</strong>。</p> <p>这也是为什么福尔马林(甲醛水溶液)能用来制作标本——它通过“交联”反应,将生物组织的所有蛋白质“固化”,使其永不腐败,但这对于活体组织而言,就是彻头彻尾的毁灭。</p> -<h3 id="为何神经末梢会受到强烈刺激?"><a href="#为何神经末梢会受到强烈刺激?" class="headerlink" title="为何神经末梢会受到强烈刺激?"></a>为何神经末梢会受到强烈刺激?</h3><p>我们的黏膜组织下,密布着丰富的神经末梢。</p> +<h3 id="为何神经末梢会受到强烈刺激?"><a class="header-anchor" href="#为何神经末梢会受到强烈刺激?">¶</a>为何神经末梢会受到强烈刺激?</h3> +<p>我们的黏膜组织下,密布着丰富的神经末梢。</p> <p>完好的黏膜上皮细胞是神经末梢的保护层。当这层保护被甲醛破坏,下方的神经末梢就直接暴露了出来。此时,高活性的甲醛分子可以直接攻击这些裸露的神经末梢,就像用化学品直接触碰神经一样,瞬间产生剧烈的刺激信号。</p> <p>被损伤和死亡的细胞,在“弥留之际”会释放出大量的化学信号物质,如前列腺素、缓激肽、组胺等。这些物质被称为“炎症介质”。而这些炎症介质本身,就是神经末梢上痛觉感受器的强效激活剂。它们会放大并持续地刺激神经,导致灼痛、瘙痒和刺痛感。</p> -<h3 id="什么是活性氧?"><a href="#什么是活性氧?" class="headerlink" title="什么是活性氧?"></a>什么是活性氧?</h3><p><strong>活性氧(Reactive Oxygen Species, ROS)</strong> 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。</p> -<p> 在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。</p> -<p> 一旦活性氧的产生<strong>远远超过</strong>了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“<strong>氧化应激</strong>”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。</p> -<h3 id="甲醛如何诱导线粒体产生大量活性氧?"><a href="#甲醛如何诱导线粒体产生大量活性氧?" class="headerlink" title="甲醛如何诱导线粒体产生大量活性氧?"></a>甲醛如何诱导线粒体产生大量活性氧?</h3><p>线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。</p> +<h3 id="什么是活性氧?"><a class="header-anchor" href="#什么是活性氧?">¶</a>什么是活性氧?</h3> +<p><strong>活性氧(Reactive Oxygen Species, ROS)</strong> 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。</p> +<p>在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。</p> +<p>一旦活性氧的产生<strong>远远超过</strong>了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“<strong>氧化应激</strong>”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。</p> +<h3 id="甲醛如何诱导线粒体产生大量活性氧?"><a class="header-anchor" href="#甲醛如何诱导线粒体产生大量活性氧?">¶</a>甲醛如何诱导线粒体产生大量活性氧?</h3> +<p>线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。</p> <p>而甲醛及其代谢产物(如甲酸)会直接损伤电子传递链上的复合体蛋白(特别是复合体I和III)。这就像是破坏了生产线上的关键机器。</p> <p>当生产线上的机器受损或堵塞时,本该被有序传递的电子就会从传递链上“泄漏”出来。</p> <p>这些泄漏出来的高能电子,会直接传递给旁边无辜的氧气分子(O₂)。正常的氧气分子得到一个额外的电子后,就成为了极不稳定的<strong>超氧阴离子(O₂⁻•)</strong> (活性氧)。</p> -<h3 id="为什么活性氧能破坏细胞膜、蛋白质和DNA?"><a href="#为什么活性氧能破坏细胞膜、蛋白质和DNA?" class="headerlink" title="为什么活性氧能破坏细胞膜、蛋白质和DNA?"></a>为什么活性氧能破坏细胞膜、蛋白质和DNA?</h3><p>失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。</p> +<h3 id="为什么活性氧能破坏细胞膜、蛋白质和DNA?"><a class="header-anchor" href="#为什么活性氧能破坏细胞膜、蛋白质和DNA?">¶</a>为什么活性氧能破坏细胞膜、蛋白质和DNA?</h3> +<p>失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。</p> <p>细胞膜主要由<strong>磷脂双分子层</strong>构成,其中含有大量的<strong>不饱和脂肪酸</strong>。活性氧(特别是羟自由基•OH)会攻击不饱和脂肪酸中的双键,抢走一个电子,引发一个叫做“脂质过氧化”的链式反应。脂质过氧化会破坏细胞膜的流动性和完整性,使其变得僵硬、脆裂,最终导致细胞膜破裂,细胞内容物泄漏。</p> -<p> 而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。</p> -<p> 活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致<strong>碱基修饰、DNA单链或双链断裂</strong>。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。</p> -<h3 id="什么是细胞的程序性死亡?"><a href="#什么是细胞的程序性死亡?" class="headerlink" title="什么是细胞的程序性死亡?"></a>什么是细胞的程序性死亡?</h3><p><strong>细胞程序性死亡(Programmed Cell Death)</strong>,最主要的形式是<strong>细胞凋亡(Apoptosis)</strong>。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。</p> +<p>而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。</p> +<p>活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致<strong>碱基修饰、DNA单链或双链断裂</strong>。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。</p> +<h3 id="什么是细胞的程序性死亡?"><a class="header-anchor" href="#什么是细胞的程序性死亡?">¶</a>什么是细胞的程序性死亡?</h3> +<p><strong>细胞程序性死亡(Programmed Cell Death)</strong>,最主要的形式是<strong>细胞凋亡(Apoptosis)</strong>。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。</p> <p>在这个过程中每,细胞会主动收缩、染色质固缩、细胞核碎裂,最终形成多个被完整细胞膜包裹的“凋亡小体”,等待被免疫细胞吞噬清理。整个过程非常“干净”,不会引发周围的炎症。</p> -<p>在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——<strong>细胞色素C(Cytochrome C)</strong>,就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 <strong>Caspase</strong> 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。</p> +<p>在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——<strong>细胞色素C(Cytochrome C)</strong>,就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 <strong>Caspase</strong> 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。</p> <p><strong>“激活”在这里指的是一个剧烈的“构象变化”(Conformational Change)</strong>,即蛋白质从一种三维折叠形态,转变为另一种形态。</p> -<h4 id="什么是Bax-Bak蛋白?"><a href="#什么是Bax-Bak蛋白?" class="headerlink" title="什么是Bax/Bak蛋白?"></a>什么是Bax/Bak蛋白?</h4><p><strong>Bax和Bak</strong>是细胞内一个庞大家族——<strong>Bcl-2蛋白家族</strong>中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 <strong>抗凋亡派(Anti-apoptotic):</strong> </p> +<h4 id="什么是Bax-Bak蛋白?"><a class="header-anchor" href="#什么是Bax-Bak蛋白?">¶</a>什么是Bax/Bak蛋白?</h4> +<p><strong>Bax和Bak</strong>是细胞内一个庞大家族——<strong>Bcl-2蛋白家族</strong>中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 <strong>抗凋亡派(Anti-apoptotic):</strong></p> <p>在正常健康的细胞中,它们处于一种<strong>无害的、折叠起来的休眠状态</strong>。<strong>Bax</strong>大部分时间游荡在细胞质中。<strong>Bak</strong> 则停靠在线粒体的外膜上,但同样处于被抑制的休眠状态。</p> <p>当细胞遭受了大量、无法修复的DNA或蛋白质损伤时,这个信号会被细胞内的 <strong>p53蛋白</strong> 感知到。被激活的p53蛋白,会生产大量的一类被称为 “仅含BH3结构域蛋白”(BH3-only proteins)的小蛋白,例如 PUMA 和 Noxa。</p> -<h4 id="Bax-Bak-蛋白如何打孔?"><a href="#Bax-Bak-蛋白如何打孔?" class="headerlink" title="Bax/Bak 蛋白如何打孔?"></a>Bax/Bak 蛋白如何打孔?</h4><p>当Bax被激活后,它会暴露内部的<strong>疏水结构域</strong>。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,<strong>自发地从细胞质中转移并插入到线粒体外膜上</strong>。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。</p> -<p>这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并<strong>不是</strong>贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的<strong>外层</strong>。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的<strong>物理张力和弯曲应力</strong>。</p> -<p> 最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。</p> -<h4 id="为什么“严重损伤”会激活p53蛋白?"><a href="#为什么“严重损伤”会激活p53蛋白?" class="headerlink" title="为什么“严重损伤”会激活p53蛋白?"></a>为什么“严重损伤”会激活p53蛋白?</h4><p>在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 <strong>MDM2</strong> 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——<strong>蛋白酶体</strong>中迅速降解。因此,正常细胞里的p53蛋白水平极低。</p> +<h4 id="Bax-Bak-蛋白如何打孔?"><a class="header-anchor" href="#Bax-Bak-蛋白如何打孔?">¶</a>Bax/Bak 蛋白如何打孔?</h4> +<p>当Bax被激活后,它会暴露内部的<strong>疏水结构域</strong>。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,<strong>自发地从细胞质中转移并插入到线粒体外膜上</strong>。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。</p> +<p>这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并<strong>不是</strong>贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的<strong>外层</strong>。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的<strong>物理张力和弯曲应力</strong>。</p> +<p>最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。</p> +<h4 id="为什么“严重损伤”会激活p53蛋白?"><a class="header-anchor" href="#为什么“严重损伤”会激活p53蛋白?">¶</a>为什么“严重损伤”会激活p53蛋白?</h4> +<p>在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 <strong>MDM2</strong> 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——<strong>蛋白酶体</strong>中迅速降解。因此,正常细胞里的p53蛋白水平极低。</p> <p>当 “严重损伤”<strong>,尤其是</strong>DNA双链断裂(这被视为细胞最危急的损伤之一)发生时,DNA断裂处会立刻吸引并激活一批蛋白,其中最关键的是一类激酶(Kinase),比如<strong>ATM</strong>和<strong>ATR</strong>。激酶是一种能给其他蛋白质“安装”上磷酸基团(-PO₃²⁻)的工具酶。</p> -<p>被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。</p> -<p>对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——<strong>“DNA结合域”</strong>。同时,它会促使四个p53蛋白分子结合在一起,形成一个<strong>四聚体(Tetramer)</strong>。 </p> -<h5 id="为什么-MDM2-会盯上-p53?"><a href="#为什么-MDM2-会盯上-p53?" class="headerlink" title="为什么 MDM2 会盯上 p53?"></a>为什么 MDM2 会盯上 p53?</h5><p>在MDM2蛋白的N端(头部),有一个精心构造的、深邃的<strong>疏水性口袋(hydrophobic pocket)</strong>,就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——<strong>苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)</strong>。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。</p> -<h5 id="p53被激活后如何“下令”生产PUMA等蛋白?"><a href="#p53被激活后如何“下令”生产PUMA等蛋白?" class="headerlink" title="p53被激活后如何“下令”生产PUMA等蛋白?"></a>p53被激活后如何“下令”生产PUMA等蛋白?</h5><p> PUMA这个蛋白,它的“设计图纸”编码在一段叫做 _PUMA_(也称 _BBC3_)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。</p> +<p>被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。</p> +<p>对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——<strong>“DNA结合域”</strong>。同时,它会促使四个p53蛋白分子结合在一起,形成一个<strong>四聚体(Tetramer)</strong>。</p> +<h5 id="为什么-MDM2-会盯上-p53?"><a class="header-anchor" href="#为什么-MDM2-会盯上-p53?">¶</a>为什么 MDM2 会盯上 p53?</h5> +<p>在MDM2蛋白的N端(头部),有一个精心构造的、深邃的<strong>疏水性口袋(hydrophobic pocket)</strong>,就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——<strong>苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)</strong>。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。</p> +<h5 id="p53被激活后如何“下令”生产PUMA等蛋白?"><a class="header-anchor" href="#p53被激活后如何“下令”生产PUMA等蛋白?">¶</a>p53被激活后如何“下令”生产PUMA等蛋白?</h5> +<p>PUMA这个蛋白,它的“设计图纸”编码在一段叫做 <em>PUMA</em>(也称 <em>BBC3</em>)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。</p> <p>被激活的p53四聚体,凭借其暴露出来的DNA结合域,会精准地找到并牢牢地结合在PUMA基因的这个“停泊位点”上。一旦结合,p53 自身就会像一块磁铁,吸引来细胞核内负责“抄写”基因的庞大机器——<strong>RNA聚合酶II</strong>以及其他大量的辅助蛋白。这个庞大的蛋白质复合体,就是负责执行命令的“抄写员”。</p> <p>“抄写员”以DNA为模板,“抄写”出一段信使RNA(mRNA)。这段mRNA会离开细胞核进入细胞质,并被核糖体(蛋白质的“生产工厂”)读取,最终翻译、生产出我们需要的<strong>PUMA蛋白</strong>。</p> <p>这就是基因转录。</p> -<h4 id="为什么-DNA-的断裂会吸引激酶?"><a href="#为什么-DNA-的断裂会吸引激酶?" class="headerlink" title="为什么 DNA 的断裂会吸引激酶?"></a>为什么 DNA 的断裂会吸引激酶?</h4><p>在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。</p> +<h4 id="为什么-DNA-的断裂会吸引激酶?"><a class="header-anchor" href="#为什么-DNA-的断裂会吸引激酶?">¶</a>为什么 DNA 的断裂会吸引激酶?</h4> +<p>在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。</p> <p>当DNA发生双链断裂时,会暴露出两个<strong>DNA断端</strong>。这在正常的、连续的DNA双螺旋结构中是绝对不会出现的。这个裸露的、不连续的断端,就是一个<strong>强烈的物理异常信号</strong>。</p> <p>细胞内有一组蛋白质,叫做<strong>MRN复合体</strong>(由Mre11、Rad50、Nbs1三个蛋白组成)。</p> <p><strong>Mre11和Rad50</strong> 能特异性地识别并抓住DNA的断端,像桥梁一样将两个断端物理性地拉近,防止它们飘远。MRN复合体一旦抓住了DNA断端,它自身的构象就会发生改变。这种改变使得它能够吸引在细胞核内游走的、处于休眠二聚体状态的ATM。MRN复合体(特别是其中的Nbs1蛋白)上会暴露出一个能与ATM二聚体结合的“接口”。这种分子间的特异性亲和力,使得ATM被“吸引”并<strong>物理性地富集</strong>到DNA断裂的地方。</p> <p>当大量的ATM二聚体被招募到MRN复合体标记的DNA断端附近时,DNA断端的物理存在,加上MRN复合体的“撬动”作用,会诱导ATM二聚体的构象发生变化,促使这俩解离,变成两个独立的单体。</p> -<p> 解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。</p> -<h1 id="先到这儿吧,我要吃炒粉了"><a href="#先到这儿吧,我要吃炒粉了" class="headerlink" title="先到这儿吧,我要吃炒粉了"></a>先到这儿吧,我要吃炒粉了</h1> +<p>解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。</p> +<h1>先到这儿吧,我要吃炒粉了</h1> + </div> <script> diff --git a/2025/07/11/rep-date/index.html b/2025/07/11/rep-date/index.html index f2b10afd..86e4b75f 100644 --- a/2025/07/11/rep-date/index.html +++ b/2025/07/11/rep-date/index.html @@ -201,27 +201,31 @@ <p><strong>除权 (Ex-rights):</strong> 如果公司不是派现金,而是送股票(比如“10送3股”),那么总股本会增加,每股代表的实际价值就被稀释了。<strong>“除权”就是调整股价,以反映这种股本增加带来的稀释效应。</strong></p> <p>现金分红主要是除息。</p> <p>在2025年7月14日这一天,该股票的开盘参考价会人为地向下调整。</p> -<p>如果在除息日前一天(假设是T日)持有这只股票,那么资产 = 股票市值 + 即将到手的现金分红。 到了除息日(T+1日,即7月14日),市场会重新定价。新的股价 = 原股价 - 每股分红。</p> +<p>如果在除息日前一天(假设是T日)持有这只股票,那么资产 = 股票市值 + 即将到手的现金分红。 到了除息日(T+1日,即7月14日),市场会重新定价。新的股价 = 原股价 - 每股分红。</p> <p><strong>所以,除权除息日,本质上是股价因分红派息而进行调整的日子。</strong> 股价会下跌,但这并不意味着亏钱,因为股价减少的部分,恰好等于即将收到的现金红利。总资产在理论上保持不变。</p> <p>不过 “除权除息日” 并不是钱到账的日子。这一天发生的是<strong>股价的调整</strong>,而不是现金的发放。</p> <p>完整的分红流程如下:</p> <ol> -<li>股权登记日 (Record Date) <ul> +<li>股权登记日 (Record Date) +<ul> <li>按照A股市场的惯例(T+1制度),<strong>除权除息日通常是股权登记日的下一个交易日。</strong></li> <li>因此,如果除权除息日是2025年7月14日(周一),那么<strong>股权登记日很可能就是前一个交易日,即2025年7月11日(周五)。</strong> (注:这需要以公司正式公告为准,但逻辑如此)。</li> <li><strong>所以必须在股权登记日当天收盘时持有该股票,才能拿到分红。</strong></li> </ul> </li> -<li>除权除息日 (Ex-Date) — 股价调整的日子<ul> +<li>除权除息日 (Ex-Date) — 股价调整的日子 +<ul> <li><strong>股价调整:</strong> 股票价格会下跌,反映红利已被剔除。</li> -<li><strong>权利分割:</strong><ul> +<li><strong>权利分割:</strong> +<ul> <li>如果在这一天<strong>买入</strong>这只股票,无法享受这次分红。因为买的是已经“打折”后的股票。</li> <li>如果在这一天<strong>卖出</strong>在登记日持有的股票,依然可以拿到分红!因为在登记日是股东。</li> </ul> </li> </ul> </li> -<li>派息日 (Payment Date) <ul> +<li>派息日 (Payment Date) +<ul> <li>现金红利会在这一天自动打入你的证券资金账户。</li> <li>派息日通常在除权除息日之后几天,有时甚至是同一天,但这需要看上市公司的具体安排和公告。</li> </ul> diff --git a/2025/07/29/nestjs-nested-object-dto/index.html b/2025/07/29/nestjs-nested-object-dto/index.html index 3e7695e5..b67e6ef3 100644 --- a/2025/07/29/nestjs-nested-object-dto/index.html +++ b/2025/07/29/nestjs-nested-object-dto/index.html @@ -193,40 +193,50 @@ </div> <div class="post-content"> <p>在开发 NestJS 应用程序时,一个常见的场景是根据复杂的条件筛选资源。当使用嵌套查询参数来表达这些条件时,例如通过 URL <code>http://localhost:3000/products/filter?filters[price][min]=100</code>,可能会意外地遇到 <code>property filters[price][min] should not exist</code> 错误。本文将深入剖析此问题背后的原因,并提供针对 Express 和 Fastify 两种底层框架的解决方案。</p> -<h4 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h4><p>假设我们正在构建一个电子商务平台的后端,需要实现一个商品筛选接口 <code>/products/filter</code>。该接口应允许客户端根据不同的属性(如价格 <code>price</code>、库存 <code>stock</code>)进行筛选,并支持对数值型属性(如价格)指定一个范围。</p> +<h4 id="问题背景"><a class="header-anchor" href="#问题背景">¶</a>问题背景</h4> +<p>假设我们正在构建一个电子商务平台的后端,需要实现一个商品筛选接口 <code>/products/filter</code>。该接口应允许客户端根据不同的属性(如价格 <code>price</code>、库存 <code>stock</code>)进行筛选,并支持对数值型属性(如价格)指定一个范围。</p> <p>为了实现这一功能,我们定义了以下 NestJS 数据传输对象 (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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ApiPropertyOptional</span> } <span class="keyword">from</span> <span class="string">"@nestjs/swagger"</span>;</span><br><span class="line"><span class="keyword">import</span> {</span><br><span class="line"> <span class="title class_">IsNumber</span>,</span><br><span class="line"> <span class="title class_">IsOptional</span>,</span><br><span class="line"> <span class="title class_">Min</span>,</span><br><span class="line"> <span class="title class_">ValidateNested</span>,</span><br><span class="line">} <span class="keyword">from</span> <span class="string">"class-validator"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Type</span> } <span class="keyword">from</span> <span class="string">"class-transformer"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RangeFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"最小值 (大于等于)"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">Number</span>)</span><br><span class="line"> <span class="meta">@IsNumber</span>()</span><br><span class="line"> <span class="meta">@Min</span>(<span class="number">0</span>)</span><br><span class="line"> <span class="attr">min</span>?: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"最大值 (小于等于)"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">Number</span>)</span><br><span class="line"> <span class="meta">@IsNumber</span>()</span><br><span class="line"> <span class="meta">@Min</span>(<span class="number">0</span>)</span><br><span class="line"> <span class="attr">max</span>?: <span class="built_in">number</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ProductAttributeFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({</span><br><span class="line"> <span class="attr">description</span>: <span class="string">"价格范围"</span>,</span><br><span class="line"> <span class="attr">type</span>: <span class="title class_">RangeFilterDto</span>,</span><br><span class="line"> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@ValidateNested</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">RangeFilterDto</span>)</span><br><span class="line"> <span class="attr">price</span>?: <span class="title class_">RangeFilterDto</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"商品属性筛选条件"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@ValidateNested</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">ProductAttributeFilterDto</span>)</span><br><span class="line"> <span class="attr">filters</span>?: <span class="title class_">ProductAttributeFilterDto</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>对应的控制器代码如下:</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></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_">Query</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_">ApiOperation</span>, <span class="title class_">ApiResponse</span>, <span class="title class_">ApiTags</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_">ProductFilterDto</span> } <span class="keyword">from</span> <span class="string">'./dtos/product-filter.dto'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@ApiTags</span>(<span class="string">'products'</span>)</span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'products'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductsController</span> {</span><br><span class="line"> <span class="meta">@Get</span>(<span class="string">"filter"</span>)</span><br><span class="line"> <span class="meta">@ApiOperation</span>({ <span class="attr">summary</span>: <span class="string">"根据条件筛选商品"</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">"筛选商品成功"</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">"请求参数错误"</span> })</span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">getFilteredProducts</span>(</span><br><span class="line"> <span class="meta">@Query</span>() <span class="attr">query</span>: <span class="title class_">ProductFilterDto</span>,</span><br><span class="line"> ): <span class="title class_">Promise</span><<span class="built_in">any</span>> {</span><br><span class="line"> <span class="comment">// 在实际应用中,这里会调用服务处理筛选逻辑</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(query);</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">message</span>: <span class="string">"Products filtered successfully."</span>,</span><br><span class="line"> <span class="attr">data</span>: query,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>当客户端通过以下 URL 发送请求时:</p> <p><code>http://localhost:3000/products/filter?filters[price][min]=100</code></p> <p>后端返回了 HTTP 400 错误,并附带以下日志:</p> <figure class="highlight plaintext"><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><span class="line">[Nest] 12345 - 07/29/2025, 11:00:00 AM ERROR [HttpExceptionFilter] [GET /products/filter?filters%5Bprice%5D%5Bmin%5D=100] HTTP 400 Error: property filters[price][min] should not exist</span><br><span class="line">BadRequestException: Bad Request Exception</span><br><span class="line"> at ValidationPipe.exceptionFactory (/path/to/project/node_modules/@nestjs/common/pipes/validation.pipe.js:107:20)</span><br><span class="line"> at ValidationPipe.transform (/path/to/project/node_modules/@nestjs/common/pipes/validation.pipe.js:74:30)</span><br><span class="line"> ... (其他堆栈信息)</span><br></pre></td></tr></table></figure> - -<h4 id="错误分析"><a href="#错误分析" class="headerlink" title="错误分析"></a>错误分析</h4><p>此错误的核心在于 NestJS 的 <code>@Query()</code> 装饰器与 <code>ValidationPipe</code> 在处理 URL 查询字符串时的默认行为,未能正确地将客户端提供的嵌套方括号语法 (<code>filters[price][min]</code>) 解析为 DTO 所期望的 JavaScript 嵌套对象结构。</p> +<h4 id="错误分析"><a class="header-anchor" href="#错误分析">¶</a>错误分析</h4> +<p>此错误的核心在于 NestJS 的 <code>@Query()</code> 装饰器与 <code>ValidationPipe</code> 在处理 URL 查询字符串时的默认行为,未能正确地将客户端提供的嵌套方括号语法 (<code>filters[price][min]</code>) 解析为 DTO 所期望的 JavaScript 嵌套对象结构。</p> <ol> -<li><p><strong>DTO 的预期结构:</strong><br><code>ProductFilterDto</code> 定义了 <code>filters</code> 属性,其类型为 <code>ProductAttributeFilterDto</code>。<code>ProductAttributeFilterDto</code> 又包含了 <code>price</code> 属性,类型为 <code>RangeFilterDto</code>,最终含有 <code>min</code> 和 <code>max</code> 属性。这意味着 DTO 期望接收的数据结构应为:<code>{ filters: { price: { min: 100 } } }</code>。</p> +<li> +<p><strong>DTO 的预期结构:</strong><br> +<code>ProductFilterDto</code> 定义了 <code>filters</code> 属性,其类型为 <code>ProductAttributeFilterDto</code>。<code>ProductAttributeFilterDto</code> 又包含了 <code>price</code> 属性,类型为 <code>RangeFilterDto</code>,最终含有 <code>min</code> 和 <code>max</code> 属性。这意味着 DTO 期望接收的数据结构应为:<code>{ filters: { price: { min: 100 } } }</code>。</p> </li> -<li><p><strong>URL 查询参数的扁平化解析:</strong><br>HTTP 协议的查询字符串本质上是扁平的键值对集合。虽然 <code>key[nestedKey]=value</code> 这种方括号语法被许多 Web 框架(如 PHP、Ruby on Rails)和库(如 <code>qs</code>)广泛用于表示嵌套数据,但这并非 HTTP 协议的标准。<br>NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的请求。在默认配置下,Express 不会自动地、递归地将 <code>filters[price][min]</code> 这样的字符串键名解析成一个具有正确嵌套层级的 JavaScript 对象。相反,它会将其视为一个完整的、扁平的字符串键名 <code>filters[price][min]</code>,并将其值 <code>100</code> 与之对应。</p> +<li> +<p><strong>URL 查询参数的扁平化解析:</strong><br> +HTTP 协议的查询字符串本质上是扁平的键值对集合。虽然 <code>key[nestedKey]=value</code> 这种方括号语法被许多 Web 框架(如 PHP、Ruby on Rails)和库(如 <code>qs</code>)广泛用于表示嵌套数据,但这并非 HTTP 协议的标准。<br> +NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的请求。在默认配置下,Express 不会自动地、递归地将 <code>filters[price][min]</code> 这样的字符串键名解析成一个具有正确嵌套层级的 JavaScript 对象。相反,它会将其视为一个完整的、扁平的字符串键名 <code>filters[price][min]</code>,并将其值 <code>100</code> 与之对应。</p> </li> -<li><p><strong><code>ValidationPipe</code> 的验证失败:</strong><br>当 <code>ValidationPipe</code> 接收到被扁平化解析后的查询参数时,它会在 <code>ProductFilterDto</code> 中寻找一个名为 <code>filters[price][min]</code> 的顶层属性。由于 DTO 中并未直接定义这样一个扁平的属性,并且 <code>ValidationPipe</code> 通常会启用 <code>forbidNonWhitelisted: true</code> 或 <code>whitelist: true</code> 选项来拒绝 DTO 中未明确声明的属性,因此验证过程失败,并抛出 <code>property filters[price][min] should not exist</code> 的错误。这表明 <code>ValidationPipe</code> 将 <code>filters[price][min]</code> 视为一个非法的、未在白名单中的属性,而不是预期的嵌套对象 <code>filters</code> 下的深层子属性。</p> +<li> +<p><strong><code>ValidationPipe</code> 的验证失败:</strong><br> +当 <code>ValidationPipe</code> 接收到被扁平化解析后的查询参数时,它会在 <code>ProductFilterDto</code> 中寻找一个名为 <code>filters[price][min]</code> 的顶层属性。由于 DTO 中并未直接定义这样一个扁平的属性,并且 <code>ValidationPipe</code> 通常会启用 <code>forbidNonWhitelisted: true</code> 或 <code>whitelist: true</code> 选项来拒绝 DTO 中未明确声明的属性,因此验证过程失败,并抛出 <code>property filters[price][min] should not exist</code> 的错误。这表明 <code>ValidationPipe</code> 将 <code>filters[price][min]</code> 视为一个非法的、未在白名单中的属性,而不是预期的嵌套对象 <code>filters</code> 下的深层子属性。</p> </li> </ol> <p>简而言之,问题不在于 DTO 定义或 <code>@Query()</code> 装饰器本身,而在于底层 HTTP 框架对查询字符串的默认解析行为与 NestJS <code>ValidationPipe</code> 对复杂嵌套 DTO 结构的需求不匹配。</p> -<h4 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h4><p>要解决此问题,关键在于配置 NestJS 应用的底层 HTTP 适配器,使其能够正确地解析包含方括号语法的嵌套查询参数,将其转换为嵌套的 JavaScript 对象。</p> -<h5 id="针对-Express-适配器"><a href="#针对-Express-适配器" class="headerlink" title="针对 Express 适配器"></a>针对 Express 适配器</h5><p>如果您的 NestJS 应用使用 Express 作为底层 HTTP 框架(这是新项目的默认配置),您需要在应用的入口文件(通常是 <code>main.ts</code>)中,通过 <code>app.set('query parser', 'extended');</code> 显式启用 Express 的扩展查询字符串解析功能。此配置会指示 Express 使用 <code>qs</code> 库(Express 内置依赖)来处理查询字符串,该库能够正确处理嵌套结构。</p> +<h4 id="解决方案"><a class="header-anchor" href="#解决方案">¶</a>解决方案</h4> +<p>要解决此问题,关键在于配置 NestJS 应用的底层 HTTP 适配器,使其能够正确地解析包含方括号语法的嵌套查询参数,将其转换为嵌套的 JavaScript 对象。</p> +<h5 id="针对-Express-适配器"><a class="header-anchor" href="#针对-Express-适配器">¶</a>针对 Express 适配器</h5> +<p>如果您的 NestJS 应用使用 Express 作为底层 HTTP 框架(这是新项目的默认配置),您需要在应用的入口文件(通常是 <code>main.ts</code>)中,通过 <code>app.set('query parser', 'extended');</code> 显式启用 Express 的扩展查询字符串解析功能。此配置会指示 Express 使用 <code>qs</code> 库(Express 内置依赖)来处理查询字符串,该库能够正确处理嵌套结构。</p> <p>示例 (<code>main.ts</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ValidationPipe</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_">NestExpressApplication</span> } <span class="keyword">from</span> <span class="string">'@nestjs/platform-express'</span>; <span class="comment">// 导入此类型</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="property">create</span><<span class="title class_">NestExpressApplication</span>>(<span class="title class_">AppModule</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 启用 Express 的扩展查询参数解析</span></span><br><span class="line"> <span class="comment">// 'simple' (默认) | 'extended'</span></span><br><span class="line"> app.<span class="title function_">set</span>(<span class="string">'query parser'</span>, <span class="string">'extended'</span>);</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalPipes</span>(<span class="keyword">new</span> <span class="title class_">ValidationPipe</span>({</span><br><span class="line"> <span class="attr">whitelist</span>: <span class="literal">true</span>, <span class="comment">// 自动剥离 DTO 中未定义的属性</span></span><br><span class="line"> <span class="attr">forbidNonWhitelisted</span>: <span class="literal">true</span>, <span class="comment">// 如果传入 DTO 中未定义的属性,则抛出错误</span></span><br><span class="line"> <span class="attr">transform</span>: <span class="literal">true</span>, <span class="comment">// 自动将负载转换为 DTO 类的实例</span></span><br><span class="line"> <span class="attr">transformOptions</span>: {</span><br><span class="line"> <span class="attr">enableImplicitConversion</span>: <span class="literal">true</span>, <span class="comment">// 允许隐式类型转换,配合 @Type()</span></span><br><span class="line"> },</span><br><span class="line"> }));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ... 其他配置</span></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure> - -<h5 id="针对-Fastify-适配器"><a href="#针对-Fastify-适配器" class="headerlink" title="针对 Fastify 适配器"></a>针对 Fastify 适配器</h5><p>如果您的 NestJS 应用使用 Fastify 作为底层 HTTP 框架,您需要在创建 Fastify 适配器实例时,通过 <code>querystringParser</code> 选项提供一个自定义的查询字符串解析函数。通常,我们会利用 <code>qs</code> 这样的成熟库来完成递归解析。</p> -<p>首先,确保已安装 <code>qs</code> 库及其类型定义:<br><code>npm install qs</code><br><code>npm install -D @types/qs</code></p> +<h5 id="针对-Fastify-适配器"><a class="header-anchor" href="#针对-Fastify-适配器">¶</a>针对 Fastify 适配器</h5> +<p>如果您的 NestJS 应用使用 Fastify 作为底层 HTTP 框架,您需要在创建 Fastify 适配器实例时,通过 <code>querystringParser</code> 选项提供一个自定义的查询字符串解析函数。通常,我们会利用 <code>qs</code> 这样的成熟库来完成递归解析。</p> +<p>首先,确保已安装 <code>qs</code> 库及其类型定义:<br> +<code>npm install qs</code><br> +<code>npm install -D @types/qs</code></p> <p>示例 (<code>main.ts</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><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ValidationPipe</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> {</span><br><span class="line"> <span class="title class_">FastifyAdapter</span>,</span><br><span class="line"> <span class="title class_">NestFastifyApplication</span>,</span><br><span class="line">} <span class="keyword">from</span> <span class="string">'@nestjs/platform-fastify'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> qs <span class="keyword">from</span> <span class="string">'qs'</span>; <span class="comment">// 引入 qs 库</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="property">create</span><<span class="title class_">NestFastifyApplication</span>>(</span><br><span class="line"> <span class="title class_">AppModule</span>,</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">FastifyAdapter</span>({</span><br><span class="line"> <span class="comment">// 配置 Fastify 的查询字符串解析器</span></span><br><span class="line"> <span class="attr">querystringParser</span>: <span class="function">(<span class="params">str</span>) =></span> qs.<span class="title function_">parse</span>(str),</span><br><span class="line"> }),</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalPipes</span>(<span class="keyword">new</span> <span class="title class_">ValidationPipe</span>({</span><br><span class="line"> <span class="attr">whitelist</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">forbidNonWhitelisted</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">transform</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">transformOptions</span>: {</span><br><span class="line"> <span class="attr">enableImplicitConversion</span>: <span class="literal">true</span>,</span><br><span class="line"> },</span><br><span class="line"> }));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ... 其他配置</span></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure> - -<p>通过上述配置,无论是使用 Express 还是 Fastify,NestJS 应用都将能够正确地将 <code>?filters[price][min]=100</code> 解析为 <code>{ filters: { price: { min: '100' } } }</code>。随后,在 <code>ValidationPipe</code> 的 <code>transform</code> 阶段,<code>@Type(() => Number)</code> 装饰器会确保 <code>min</code> 的值从字符串 <code>'100'</code> 转换为数字 <code>100</code>,从而顺利通过验证并注入到控制器方法中。</p> -<h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><ul> +<p>通过上述配置,无论是使用 Express 还是 Fastify,NestJS 应用都将能够正确地将 <code>?filters[price][min]=100</code> 解析为 <code>{ filters: { price: { min: '100' } } }</code>。随后,在 <code>ValidationPipe</code> 的 <code>transform</code> 阶段,<code>@Type(() => Number)</code> 装饰器会确保 <code>min</code> 的值从字符串 <code>'100'</code> 转换为数字 <code>100</code>,从而顺利通过验证并注入到控制器方法中。</p> +<h3 id="引用"><a class="header-anchor" href="#引用">¶</a>引用</h3> +<ul> <li><a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/29960764/what-does-extended-mean-in-express-4-0">What does ‘extended’ mean in express 4.0? - Stack Overflow</a></li> <li><a target="_blank" rel="noopener" href="https://expressjs.com/en/5x/api.html#app.set">Express 5.0 - API Reference (<code>app.set</code>)</a></li> </ul> diff --git a/2025/07/31/svelte-lazyquery/index.html b/2025/07/31/svelte-lazyquery/index.html index a90029c0..436b509b 100644 --- a/2025/07/31/svelte-lazyquery/index.html +++ b/2025/07/31/svelte-lazyquery/index.html @@ -195,41 +195,47 @@ <p>在前端,一个常见的性能瓶颈是初始加载时请求了用户当前 viewpoint 之外的非必要数据。惰性加载(Lazy Loading)是一种关键策略,它将数据获取推迟到组件进入 viewpoint 时才执行。本文将阐述如何结合使用 Svelte 5、<code>@tanstack/svelte-query</code> 和 <code>runed.dev</code> 的 <code>useIntersectionObserver</code>,构建一个通用的、可复用的惰性加载组件。</p> <p><strong>核心依赖与环境</strong></p> <ul> -<li><p><strong>Svelte 5:</strong> 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。</p> +<li> +<p><strong>Svelte 5:</strong> 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。</p> </li> -<li><p><strong>@tanstack/svelte-query:</strong> TanStack Query 的 Svelte 适配版,用户获取数据。</p> +<li> +<p><strong>@tanstack/svelte-query:</strong> TanStack Query 的 Svelte 适配版,用户获取数据。</p> </li> -<li><p><strong>runed.dev:</strong> 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 <code>useIntersectionObserver</code>。</p> +<li> +<p><strong>runed.dev:</strong> 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 <code>useIntersectionObserver</code>。</p> </li> </ul> -<h4 id="设计哲学:分离关注点"><a href="#设计哲学:分离关注点" class="headerlink" title="设计哲学:分离关注点"></a><strong>设计哲学:分离关注点</strong></h4><p>该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。</p> +<h4 id="设计哲学:分离关注点"><a class="header-anchor" href="#设计哲学:分离关注点">¶</a><strong>设计哲学:分离关注点</strong></h4> +<p>该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。</p> <ol> -<li><p><strong>何时加载 (When to Load):</strong> 组件的可见性决定了数据加载的时机。我们利用 <code>Intersection Observer API</code> 来精确、高效地监听一个元素是否进入 viewpoint。<code>runed.dev</code> 库为此提供了名为 <code>useIntersectionObserver</code> 的便捷封装。</p> +<li> +<p><strong>何时加载 (When to Load):</strong> 组件的可见性决定了数据加载的时机。我们利用 <code>Intersection Observer API</code> 来精确、高效地监听一个元素是否进入 viewpoint。<code>runed.dev</code> 库为此提供了名为 <code>useIntersectionObserver</code> 的便捷封装。</p> </li> -<li><p><strong>如何加载 (How to Load):</strong> 数据获取、缓存、同步和状态管理的复杂性由 <code>@tanstack/svelte-query</code> (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。</p> +<li> +<p><strong>如何加载 (How to Load):</strong> 数据获取、缓存、同步和状态管理的复杂性由 <code>@tanstack/svelte-query</code> (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。</p> </li> </ol> <p>通过将这两者结合,可以创建一个名为 <code>LazyQuery</code> 的抽象组件。该组件内部处理可见性检测,并根据检测结果动态控制 TanStack Query 的执行,而将具体的查询逻辑(<code>queryFn</code>)和键(<code>queryKey</code>)完全交由使用者定义。</p> -<h4 id="LazyQuery-组件的实现"><a href="#LazyQuery-组件的实现" class="headerlink" title="LazyQuery 组件的实现"></a><code>LazyQuery</code> 组件的实现</h4><blockquote> +<h4 id="LazyQuery-组件的实现"><a class="header-anchor" href="#LazyQuery-组件的实现">¶</a><code>LazyQuery</code> 组件的实现</h4> +<blockquote> <p>目标:封装惰性加载逻辑,并向外暴露一个标准的 TanStack Query 接口。</p> </blockquote> <p>一、对组件的接口类型做如下定义:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">CreateQueryOptions</span>, <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Props</span><</span><br><span class="line"> <span class="title class_">TQueryFnData</span> = <span class="built_in">unknown</span>,</span><br><span class="line"> <span class="title class_">TError</span> = <span class="title class_">Error</span>,</span><br><span class="line"> <span class="title class_">TData</span> = <span class="title class_">TQueryFnData</span>,</span><br><span class="line"> <span class="title class_">TQueryKey</span> <span class="keyword">extends</span> <span class="title class_">QueryKey</span> = <span class="title class_">QueryKey</span></span><br><span class="line">> = {</span><br><span class="line"> <span class="comment">/// 一个标准的 TanStack Query 配置对象</span></span><br><span class="line"> <span class="attr">queryOptions</span>: <span class="title class_">CreateQueryOptions</span><<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// Svelte 5 的 `Snippet` 类型,它允许父组件向子组件传递一段可执行的 UI 模板。</span></span><br><span class="line"> <span class="comment">/// 这个 `Snippet` 会接收到 `createQuery` 返回的完整查询对象 `query`,</span></span><br><span class="line"> <span class="comment">/// 从而可以访问 `data`, `isLoading`, `error` 等所有状态。</span></span><br><span class="line"> <span class="attr">children</span>: <span class="title class_">Snippet</span><{</span><br><span class="line"> <span class="attr">query</span>: <span class="title class_">ReturnType</span><<span class="keyword">typeof</span> createQuery<<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>>;</span><br><span class="line"> }>;</span><br><span class="line">};</span><br></pre></td></tr></table></figure> - <p>二、组件逻辑:</p> <figure class="highlight html"><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></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { createQuery, type <span class="title class_">CreateQueryOptions</span>, type <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { useIntersectionObserver } <span class="keyword">from</span> <span class="string">'runed'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> type { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// [上述的类型定义]</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> { queryOptions, children }: <span class="title class_">Props</span> = $props();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> <span class="attr">el</span>: <span class="title class_">Element</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> { isIntersecting } = <span class="title function_">useIntersectionObserver</span>(</span></span><br><span class="line"><span class="language-javascript"> <span class="function">() =></span> el,</span></span><br><span class="line"><span class="language-javascript"> {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">rootMargin</span>: <span class="string">'200px'</span>,</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> );</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> query = <span class="title function_">createQuery</span>({</span></span><br><span class="line"><span class="language-javascript"> ...queryOptions,</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">get</span> <span class="title function_">enabled</span>() {</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> ($state.<span class="title function_">is</span>(isIntersecting) && (queryOptions.<span class="property">enabled</span> ?? <span class="literal">true</span>));</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> });</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">bind:this</span>=<span class="string">{el}</span>></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {/snippet}</span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure> - <ul> <li>我们创建一个 <code>div</code> 元素作为哨兵(sentinel),并用 <code>bind:this={el}</code> 将其 DOM 引用绑定到变量 <code>el</code>。</li> -<li><code>useIntersectionObserver</code> 接收一个返回目标元素的函数 <code>() => el</code>。<ul> +<li><code>useIntersectionObserver</code> 接收一个返回目标元素的函数 <code>() => el</code>。 +<ul> <li>它返回一个响应式的状态对象,其中 <code>isIntersecting</code> 是一个布尔值的符文(rune),当 <code>div</code> 元素进入 viewpoint 时为 <code>true</code>,否则为 <code>false</code>。</li> </ul> </li> -<li><code>rootMargin: '200px'</code> 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。</li> +<li><code>rootMargin: '200px'</code> 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。</li> </ul> -<h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><p>使用 <code>LazyQuery</code> 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景:</p> +<h4 id="使用"><a class="header-anchor" href="#使用">¶</a>使用</h4> +<p>使用 <code>LazyQuery</code> 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景:</p> <figure class="highlight html"><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><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></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { <span class="title class_">QueryClientProvider</span>, <span class="title class_">QueryClient</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">LazyQuery</span> <span class="keyword">from</span> <span class="string">'./LazyQuery.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">CopdGoldGradingChart</span> <span class="keyword">from</span> <span class="string">'./CopdGoldGradingChart.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Define the query configuration object, just like with a standard `createQuery`.</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryOptions = {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryKey</span>: [<span class="string">'goldGradingData'</span>],</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryFn</span>: <span class="title function_">async</span> () => {</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Simulate a network request</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">resolve</span> =></span> <span class="built_in">setTimeout</span>(resolve, <span class="number">1500</span>));</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// In a real application, this would be an API call</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// const response = await fetch('/api/gold-grading');</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// return await response.json();</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> { <span class="attr">data</span>: { <span class="attr">totalPatients</span>: <span class="number">1234</span>, <span class="attr">chartPoints</span>: [<span class="comment">/* ... */</span>] } };</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> };</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">{queryClient}</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">style</span>=<span class="string">"height: 200vh;"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Scroll down to see the chart...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">LazyQuery</span> {<span class="attr">queryOptions</span>}></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {#if query.isLoading}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Loading chart data...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.error}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Error: {query.error.message}<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.data}</span><br><span class="line"> <span class="tag"><<span class="name">CopdGoldGradingChart</span></span></span><br><span class="line"><span class="tag"> <span class="attr">isLoading</span>=<span class="string">{query.isFetching}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">error</span>=<span class="string">{query.error}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">refetch</span>=<span class="string">{()</span> =></span> query.refetch()}</span><br><span class="line"> data={query.data.data}</span><br><span class="line"> totalPatients={query.data.data.totalPatients}</span><br><span class="line"> /></span><br><span class="line"> {/if}</span><br><span class="line"> {/snippet}</span><br><span class="line"> <span class="tag"></<span class="name">LazyQuery</span>></span></span><br><span class="line"><span class="tag"></<span class="name">QueryClientProvider</span>></span></span><br></pre></td></tr></table></figure> - <ul> <li>页面初始加载时,<code>LazyQuery</code> 组件被渲染,但由于其 <code>div</code> 在 viewpoint 之外,<code>isIntersecting</code> 为 <code>false</code>。</li> <li><code>createQuery</code> 被调用,但因为 <code>enabled</code> 条件为 <code>false</code>,查询处于禁用状态,不会发起任何网络请求。</li> @@ -239,7 +245,8 @@ <li>这个变化被 <code>createQuery</code> 的 <code>enabled</code> 访问器捕获,查询被自动激活,<code>queryFn</code> 开始执行。</li> <li>TanStack Query 负责管理后续的状态变化(<code>isFetching</code>, <code>data</code>, <code>error</code>),并驱动 <code>children</code> 片段内的 UI 自动更新。</li> </ul> -<h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><ul> +<h4 id="参考"><a class="header-anchor" href="#参考">¶</a>参考</h4> +<ul> <li><a target="_blank" rel="noopener" href="https://runed.dev/docs/utilities/use-intersection-observer">https://runed.dev/docs/utilities/use-intersection-observer</a></li> <li><a target="_blank" rel="noopener" href="https://tanstack.com/table/latest/docs/introduction">https://tanstack.com/table/latest/docs/introduction</a></li> </ul> |
