<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>

<channel>
  <title>孤芳不自赏的博客</title>
  <atom:link href="https://www.gfbzsblog.site/rss/feed" rel="self" type="application/rss+xml" />
  <link>https://www.gfbzsblog.site</link>
  <description>分享技术心得，记录生活感悟</description>
  <lastBuildDate>Fri, 16 Oct 2026 17:32:29 +0800</lastBuildDate>
  <language>zh-CN</language>
  <sy:updatePeriod>hourly</sy:updatePeriod>
  <sy:updateFrequency>1</sy:updateFrequency>
  <generator>https://www.gfbzsblog.site</generator>

  <item>
    <title>CRUD 不会消失，它只是退到了地基里</title>
    <link>https://www.gfbzsblog.site/main/Articles/172</link>
    <comments>https://www.gfbzsblog.site/main/Articles/172#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Mon, 31 Aug 2026 17:54:24 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/172</guid>

    <description><![CDATA[]]></description>
    <content:encoded><![CDATA[最近我又在想一个去年就想过的问题：**AI 继续发展下去，我们现在做的软件，到底会变成什么？**

这个问题其实在去年做瑶光工坊的时候，我就已经隐隐约约想过。那时候 Agent 还没有现在这么火，但我已经有一种感觉：AI 模型本身的进步，未必永远都会像过去几年这么夸张。

从 GPT-3.5 到 GPT-4，再到后来越来越强的推理模型，我们已经习惯了隔一段时间，模型就突然聪明一大截。上下文越来越长，推理越来越强，能做的事情也越来越多。久而久之，我们甚至会下意识地觉得，AI 的未来就是等待下一个更强的模型。

但这种增长速度真的能一直持续下去吗？

我不知道。可能会，也可能不会。

不过有一件事情，我现在越来越确定：**哪怕有一天模型能力的增长开始放缓，Agent 依然还有非常大的发展空间。**

因为说到底，Agent 还是软件。

只不过以前的软件主要是写给人操作的，而现在，我们开始写一种新的软件——**让 AI 去操作的软件。**

## 模型聪明，不等于软件聪明

以前我们很容易把 AI 的能力和模型能力直接画上等号。模型越强，AI 就越强。

但真正做了一些 AI 项目以后，我越来越觉得不是这么回事。

同一个模型，放进两个不同的系统里，最后表现出来的“智商”可能完全不是一个级别。一个系统可能只是把用户的问题丢给模型，然后把回答原样显示出来；另一个系统却可以让模型读取文件、搜索数据库、调用 API、操作浏览器，记住以前发生过的事情，把一个复杂任务拆成几十个步骤，在失败以后重新判断原因、调整方案，最后还会检查一遍自己的结果。

它们底下用的可能是完全相同的模型，但作为用户，你会觉得自己面对的是两个完全不同级别的 AI。

所以我现在越来越愿意把 AI 最终表现出来的能力，粗暴地理解成一个乘法：

**AI 最终能力 = 模型智能 × 系统智能**

过去几年，我们绝大部分注意力都放在前面那个数字上。GPT 又升级了，Claude 又升级了，上下文又变长了，推理又变强了。每次模型能力提升，整个行业都会跟着兴奋一轮。

这当然没有错。模型是所有东西的基础。

但当模型逐渐跨过“足够聪明”的门槛以后，后面那个数字的重要性一定会越来越高：**我们究竟怎样把一个已经足够聪明的模型，装进一个真正好用的软件系统里？**

我觉得这可能才是 Agent 接下来真正要解决的问题。

现在看 Agent 技术，会觉得好像到处都是新名词：Memory、Context Engineering、MCP、Multi-Agent、Computer Use、Agent Runtime……一个比一个新。

但如果把这些概念扒开来看，里面很多东西其实熟悉得不能再熟悉。

数据库、缓存、消息队列、状态机、任务调度、权限、RPC、gRPC、重试、幂等、可观测性、事件驱动、分布式系统……还是那些词，甚至还是我们以前背过的那些“八股”。

Agent 并没有凭空创造出一个脱离软件工程的新世界。它真正带来的变化，是往原来的软件系统里塞进了一个过去从未存在过的东西：

**一个具有推理能力，但行为又不是完全确定的执行节点。**

这件事情看起来只是多了一个模型，实际上却把很多原本简单的问题重新变复杂了。

以前一个任务失败，我们可以非常明确地写好规则：失败就重试，重试三次还不行就报错。程序并不需要理解发生了什么，它只需要按照我们事先规定好的流程执行。

但 Agent 不一样。

任务失败以后，它可能需要先读懂错误信息，判断失败究竟发生在哪里，再决定是直接重试、修改参数、换一个工具，还是彻底换一种解决方案。重新执行以后，它甚至还要自己判断结果到底对不对。

以前我们是在告诉软件：

**你应该怎么做。**

现在我们开始告诉软件：

**我要什么。**

至于中间究竟怎么做到，开始逐渐交给 AI 自己去想。

看起来只差了一句话，背后的软件设计却已经完全不同了。

## 软件没有消失，只是操作软件的人变了

所以我一直不太认同“AI 会把软件淘汰掉”这种说法。

恰恰相反，我觉得**软件不会消失，它会逐渐融进 AI 里面。**

比如今天的项目管理软件，无非就是任务列表、负责人、截止日期、评论、甘特图、报表这些东西。用户每天打开系统，创建任务、修改状态、调整负责人、看进度、催人，然后根据实际情况重新安排计划。

未来这些东西大概率一个都不会少。

数据库还是那套数据库，任务还是那张任务表，负责人、状态、截止日期这些字段依然存在，该有的 CRUD 也一个都不会少。

真正发生变化的，是谁在操作它。

以后用户可能只需要说一句：

“我们准备下个月上线这个版本，你看一下 Git、需求文档和最近的会议记录，帮我把项目安排一下。”

接下来，AI 自己读取需求和会议记录，分析代码改动，拆解任务和依赖关系，判断优先级，把任务创建到项目管理系统里，再持续跟踪提交和进度。当它发现某个模块存在延期风险时，还会重新计算计划，把不重要的任务向后移动。

最后有一天，它可能只是过来告诉你：

“支付模块预计会导致原计划延期三天，我重新调整了两个非关键任务，目前主版本发布时间不需要改变。”

这个时候，项目管理软件消失了吗？

当然没有。

它甚至比以前重要得多，因为 Agent 的每一次计划、修改、追踪和调整，最后仍然需要落到一个真实的软件系统里。

真正消失的，是人必须亲自完成每一次操作这件事。

以前是**人操作软件**。

以后可能逐渐变成**人提出目标，AI 操作软件**。

## UI 也许也会因此改变

沿着这个方向继续想下去，还有一件事情很有意思：UI 本身可能也会发生很大的变化。

我们今天设计 UI，本质上是在做什么？

很多时候，其实是在想办法让一个完全不懂程序的人，也能够准确地调用软件提供的能力。

“创建订单”按钮，本质上是在调用 `createOrder()`；“退款”按钮背后是 `refund()`；“导出 Excel”背后是 `export()`。

过去几十年的 GUI，其实一直在解决一个问题：**怎么让普通人不需要懂代码，也能够准确地告诉计算机自己想调用哪个功能。**

所以我们设计按钮、菜单、表单、筛选器、下拉框，再用一整套交互规范尽可能降低人的操作成本。

但如果有一天，计算机真的能够直接理解人的意图呢？

事情就会变得很不一样。

以前一个运营人员想处理异常退款订单，可能需要先打开订单后台，设置十几个筛选条件，导出 Excel，人工筛选一遍，再回到后台一个一个点击退款。

以后他可能只需要告诉 Agent：

“把上个月退款率异常的订单找出来，分析一下原因。金额超过 5000 的不要动，剩下符合退款政策的处理掉，最后给我一份报告。”

这句话背后其实依然是一连串传统的软件操作：查询订单、读取退款数据、获取公司政策、分析异常、筛选符合条件的订单、调用退款接口，最后生成报告。

原来的功能一个都没有消失。

**它们只是从给人使用的 Function，逐渐变成了给 Agent 使用的 Tool。**

我觉得这可能会成为未来软件非常重要的一次变化。

UI 不一定会消失，人依然需要看数据、确认结果、处理异常，也依然需要在高风险操作前进行审批。但 UI 不再需要承担所有操作入口。大量过去必须由人一步一步点击完成的流程，会逐渐沉到 Agent 身后，变成它可以自主组合和调用的能力。

软件的界面可能因此越来越简单，但软件内部反而会越来越复杂。

## CRUD 不会死，只会变成地基

所以我其实不太认同另一种说法：

**“以后 CRUD 没用了。”**

CRUD 怎么可能没用？

Agent 也需要数据库，也需要创建任务、查询状态、修改数据。企业依然需要订单、用户、权限、审批、库存、财务。这些东西不会因为 AI 出现，就突然从商业世界里消失。

只要这些东西存在，CRUD 就一定存在。

但是另一件事情也同样明显：

**只会 CRUD 的价值，会越来越低。**

以前一个开发者会 React，会写几个接口，会 MySQL，会登录鉴权，能够独立做出一个后台管理系统，其实已经具备了相当完整的软件开发能力。

但现在，这些恰恰是 AI 最容易生成的部分。

如果一个开发者长期做的事情只是产品说加一个字段就加一个字段，说加个按钮就加个按钮，说做个列表就写个 `/list`，说增加筛选条件就再补一个 `WHERE`，那未来确实会越来越危险。

不是因为 CRUD 消失了，而是因为**CRUD 正在从一种能力，变成一种基础。**

就像今天会 Git 已经不会让别人觉得你是一个多么厉害的工程师，但你不会 Git 又几乎不可能成为一个合格的现代软件工程师。

CRUD 可能也会越来越像这样。

它依然重要，只是不再足以定义一个工程师的价值。

## AI 越发展，我反而越不觉得传统软件工程过时

这也是一个挺有意思的反直觉。

AI 越发展，我反而越不觉得传统软件工程没用了。

甚至恰恰相反。

今天一个简单的 Agent Demo 很容易做出来。给模型几个 Tool，写一个循环，让它自己决定调用哪个工具，看起来就已经非常智能。

但如果真的有几十个 Agent 同时替一家企业工作，问题马上就不一样了。

它们需要共享状态，又不能随便看到彼此不该看到的数据；一个 Agent 执行到一半挂掉了，任务不能凭空消失；服务器重启以后，之前执行了一半的工作需要恢复；两个 Agent 同时修改同一条数据时，需要知道谁应该成功；涉及付款、退款、删除数据这些操作时，又必须防止因为重试而执行两遍。

与此同时，我们还要知道每一个 Agent 为什么做出某个决定，它到底调用了什么工具，消耗了多少 Token，花了多少钱，出了问题以后能不能回滚，涉及高风险操作时有没有经过人的确认。

仔细看这些问题，会发现几乎没有哪个是单纯靠 Prompt 能解决的。

它们依然是数据库、操作系统、计算机网络、分布式系统、事务、消息队列、权限系统和可观测性的问题。

只不过以前这些基础设施服务的是确定性的程序，而以后，它们还要服务一群会自己思考、自己规划，但行为具有不确定性的 Agent。

所以我现在越来越觉得，**未来真正厉害的 AI 工程师，不一定是最会写 Prompt 的那群人。**

很可能恰恰是那些软件工程基础本来就很扎实，同时又真正理解模型能力边界的人。

模型厂商负责让模型越来越聪明。

而软件工程师真正要解决的问题，是：

**怎么把这种聪明，可靠地装进现实世界。**

“可靠”这两个字，我觉得甚至比“聪明”更加重要。

因为一个 Demo 偶尔做对一次并不难。真正困难的是让它每天运行一万次，让它面对各种异常情况，让它接触真实用户、真实订单和真实资金以后，依然能够知道什么时候应该自己做决定，什么时候应该停下来，什么时候必须把决定权交还给人。

这已经不是 Prompt Engineering 了。

这是 Software Engineering。

## 软件不会消失，它只是开始拥有智能

所以现在再让我看 AI，我已经不太愿意把它理解成一个独立的行业了。

我更愿意把它理解成**软件的一次进化。**

过去的软件，是人告诉计算机每一步应该怎么做；未来的软件，则越来越像是人告诉计算机自己最终想要什么。

中间那一大段原本需要人亲自操作、判断、组合和执行的过程，开始逐渐交给 AI。

可能再过几年，我们甚至不会再刻意强调“这是一个 AI 软件”。

就像今天已经很少有人会郑重其事地介绍：

“这是一个互联网软件。”

因为联网早就已经变成软件理所当然拥有的能力。

以后，智能可能也一样。

AI 不再只是网页右下角那个聊天框，也不再是某个按钮旁边为了显得高级而加上的一个 ✨。它会真正进入软件的搜索、数据、权限、工作流、决策、自动化和执行系统里，最后变成整个软件的一部分。

到那个时候，所谓的 **Software Engineering + AI Engineering**，可能最终又会重新变回一个词：

**Software Engineering。**

而 CRUD 当然也还在那里。

数据库还在那里，API 还在那里，权限系统还在那里，那些软件工程师写了几十年的东西，也都还在那里。

它们甚至可能比今天更加重要。

只是有一天，当我们回过头看的时候，可能会突然发现：

**CRUD 从来没有被 AI 淘汰。**

**它只是从舞台中央，退到了地基里。**

而在这块已经被软件工程打磨了几十年的地基上，我们终于开始建造一种真正会理解、会判断，也会行动的软件。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Bun × Ai 时代的工程基座：一次正在发生但仍未定型的技术转向</title>
    <link>https://www.gfbzsblog.site/main/Articles/170</link>
    <comments>https://www.gfbzsblog.site/main/Articles/170#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sat, 10 Jan 2026 15:54:07 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/170</guid>

    <description><![CDATA[Bun 最初以性能优势受到关注，但本质是一场面向工程生命周期的设计变革，通过统一运行时、构建、测试与包管理等能力来降低复杂度；随着 AI 深度参与代码编写与执行，Bun 的一体化工具链、快速启动和 Web API 统一语义等特性，使其成为 AI 工程体系中高效适配自动化开发的关键基础设施，这也推动了 Anthropic 对 Bun 团队的战略收购，预示着运行时正从工具选择演变为 AI 驱动开发模式下的战略资源。]]></description>
    <content:encoded><![CDATA[时间过得很快。回头再看近一两年的前端与 JavaScript 生态，会发现许多变化并不是突然发生的，而是在不知不觉中逐渐累积，直到某一个节点，人们才意识到：原来方向已经发生了偏移。

Bun，正处在这样一个节点上。

它并不是凭空出现的新概念，也不是一夜之间取代谁的“颠覆者”。相反，它更像是在长期工程实践中，被一步步推到台前的产物。而当它与 AI 公司的工程体系产生强关联时，这种变化开始显得不再普通。

这篇文章并不试图给 Bun 下一个过于激进的结论，而是希望在事实与趋势之间，梳理一条相对克制、但清晰的技术脉络。

## 一、起点：Bun 并不是为了“取代”而生

最初谈起 Bun，几乎绕不开“性能”这个关键词。

启动速度、依赖安装速度、构建速度、测试速度——这些都是 Bun 最直观、也最容易被感知到的优势。在大量工程实践对比中，它确实在多个维度上展现出了明显的效率提升。

但如果只把 Bun 理解为“一个更快的 Node.js 替代品”，其实是一种过于表层的看法。

从设计层面看，Bun 的目标并不完全是兼容既有模式，而是试图在 **运行时、构建、测试、包管理** 这些长期被拆散的工程能力之间，重新建立统一的抽象。

换句话说，它关注的不是“某一个环节更快”，而是 **整个工程生命周期是否足够顺畅**。

这也是为什么 Bun 会选择：

- 内置 TypeScript 与 JSX 支持
- 自带 bundler 与 test runner
- 强调 Web API 风格而非 Node 专属 API

这些选择，在早期看起来多少有些“激进”，但它们背后指向的是同一个目标：**降低工程复杂度，提高系统整体可预测性**。
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/dcad5b65-0e1b-4795-9ad8-86e4c29bb963.png" alt="图片" width="500" height="300" />
</center>

> 我自己接触 Bun 的时间也不算长。是在目前公司的项目中开始使用的——公司需要做一个 AI 应用，后端框架是 Node.js，因此在主管的安排下我们开始用 Bun。起初我并没有过多考虑选择它的原因，只是按流程去做。但在实际开发过程中，我确实能切身体会到它在启动速度、构建和测试上的性能优势。随着项目推进，我也才慢慢对 Bun 的设计理念和更深层的工程价值有了更全面的理解。

### 工程方式的变化

在更深层的工程语境中，Bun 的出现并不只是工具层面的改良，而是与工程形态本身的变化高度同步。

过去相当长一段时间里，前端与 JavaScript 工程的核心假设是：**人类开发者是工程的唯一组织者**。工具的职责，是围绕人类的使用习惯不断拆分、组合与优化。

而今天，这一假设正在被动摇。

当代码开始由 AI 生成、修改、运行，工程系统所面对的对象，已经不再只是“人”。它需要同时服务于 **人类开发者与自动化系统**。在这种前提下，工程工具的可组合性、启动成本、语义一致性，都会被重新放大。

也正是在这样的背景下，Bun 所强调的“统一运行时”价值，才开始真正显现。

## 二、变化出现：当 AI 开始真正参与“写代码”和“跑代码”

真正让 Bun 进入更广泛讨论视野的，并不只是性能数据本身，而是 AI 技术形态的变化。

在早期，AI 更多是作为一种“能力服务”存在：

- 提供 API
- 生成文本或代码片段
- 由人类开发者完成最终集成

但随着 AI 编程工具的发展，情况正在发生变化。

AI 不再只是给出建议，而是开始：

- 主动创建项目结构
- 直接生成可运行的代码
- 参与调试、测试与重构

在这种模式下，**代码的执行环境本身，开始成为 AI 能力的一部分**。

运行时不再只是“被动承载者”，而是 AI 工程体系中的关键基础设施。

## 三、AI 工程场景中的 Bun 与 Anthropic 收购

随着 AI 不仅参与“写代码”，还开始直接“跑代码”，Bun 的价值逐渐凸显：

- 高性能的启动与构建速度，适合高频、短生命周期的任务
- Web API 风格统一，降低跨端出错概率
- 一体化工具链，压缩工程抽象层，便于自动化系统使用

正是在这种背景下，AI 公司开始将目光投向 Bun。Bun 不再只是一个高性能的 Node.js 替代选项，而是成为 AI 工程体系中可以直接利用的关键基础设施。

因此，Bun 与 AI 公司 Anthropic 的关系，也就不仅仅是“合作”那么简单。事实上，**Bun 团队已被 Anthropic 正式收购**。这次收购不仅涉及核心团队成员的纳入，也包括 Bun 框架及其相关技术在 Anthropic 的 AI 工程体系中的战略整合。

公开信息显示，收购后情况包括：

- **团队纳入**：Bun 核心团队成员正式加入 Anthropic，直接参与 Claude Code 等 AI 工程项目的开发和基础设施优化
- **技术整合**：Bun 框架及其运行时、构建、测试和包管理功能被用于提升 AI 工程效率，同时继续作为独立开源项目存在
- **保持开源与独立演进**：Bun 项目继续遵循 MIT 许可，核心开发由原团队主导，确保社区生态和技术创新不受影响
- **战略意义**：通过收购，Anthropic 可以直接利用 Bun 的高性能运行时和统一工具链，提升 AI 自动化开发、调试和部署能力

这次收购的价值，不仅在于团队和技术的整合，更体现在 **对未来 AI 驱动开发模式的战略布局**：

- **底层基础设施的强化**：统一的运行时与工具链降低跨工具出错风险，提高工程可预期性
- **技术自主性与社区延续**：框架继续开源，使技术发展不被公司战略完全绑定，同时吸引更多开发者贡献与使用
- **支撑指数级 AI 应用增长**：为 AI 编程工具的快速迭代和扩展提供稳定、高效的基础设施

因此，在严肃的技术讨论中，把这次事件理解为：

> **一次 AI 公司对 JavaScript 运行时层的战略收购与深度整合**

比简单称之为“合作”或“团队整合”更加准确，也更能体现其对工程体系和未来 AI 开发模式的深远影响。

## 四、为什么 Bun 会进入 AI 公司的视野

如果抛开市场叙事，只从工程角度看，Bun 与 AI 应用之间的契合并不难理解。

### 1. 启动成本与短生命周期任务

AI 生成与执行代码的场景，往往具有以下特征：

- 高频启动
- 生命周期短
- 资源创建与销毁频繁

在这种情况下，运行时的冷启动成本会被无限放大。Bun 在启动速度和任务调度上的优势，使它在此类场景中具备天然适配性。

### 2. Web API 语义的统一性

Bun 大量采用 Web 标准 API，而不是依赖 Node 特有接口。这意味着：

- 浏览器与服务端语义更加一致
- AI 在生成跨端代码时面对的心智负担更低

对 AI 来说，语义越统一，出错概率就越低。

### 3. 工程工具的一体化

当运行、构建、测试、依赖管理被拆分为多个工具时，人类尚且容易出错，更不用说自动化系统。

Bun 将这些能力收敛到同一运行时之中，本质上是在 **压缩抽象层级**。

而这，恰恰是 AI 系统最需要的特性之一。

### 4. Bun 仍然付出的代价

当然，这种契合并非没有代价。

从现实工程角度看，Bun 仍然面临一些无法回避的约束：

- 部分 Node 原生生态与底层依赖仍需适配
- 社区体量与长期稳定性尚在验证中
- 在复杂、超大规模系统中，其成熟度仍需时间检验

这意味着，Bun 目前更适合被视为 **“新型工程场景中的最优解之一”**，而非通用答案。

但需要注意的是，AI 工程体系本身，也并不追求完全通用。它更关注的是：**在可控边界内，是否存在更适合自动化执行的技术栈**。

## 五、2026：可以讨论，但不必急于定论

关于“Bun 是否会成为 AI 应用的原生基座”，现在下结论显然为时尚早。

Node.js 仍然拥有极其庞大的生态与成熟度，其他运行时也在持续演进。工程世界的惯性，从来不是轻易被打破的。

但可以确定的是：

- AI 公司已经开始认真对待运行时这一层
- JavaScript 执行环境正在从“工具选择”变成“战略资源”

在这样的背景下，Bun 所处的位置，已经不再只是一个“更快的选项”。

它更像是一次提前出现的尝试：

> 如果未来的代码，有相当一部分不是由人写，而是由 AI 写并运行，那么运行时应该长成什么样？

### 工程师视角的落点

从工程师的视角看，或许并不需要急于站队。

Bun 值得关注的，并不是它是否会“取代”谁，而是它所代表的那种工程取向：
 **为自动化系统与人类协作而设计的运行时。**

在可预见的未来，JavaScript 生态很可能会长期处于多运行时并存的状态。而 Bun 的价值，也许正是在这些交汇点上，被逐步放大。

## 六、回望：真正的变化往往发生在地基层

很多技术变革，在发生时并不喧哗。

它们不会以“全面替代”的姿态出现，而是先进入最敏感、也最苛刻的工程场景中，被反复打磨。

等人们意识到时，地基往往已经换过一次了。

Bun 是否会成为 AI 时代的原生运行时，目前没有人能给出确定答案。

但可以肯定的是，它已经不再只是一个实验性的项目，而是一次正在进行中的工程选择。

而所有真正重要的技术转向，往往正是从这种尚未定型的阶段开始的。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>津门遇瑶光</title>
    <link>https://www.gfbzsblog.site/main/Articles/169</link>
    <comments>https://www.gfbzsblog.site/main/Articles/169#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sun, 28 Dec 2025 23:30:27 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/169</guid>

    <description><![CDATA[一次以开阔眼界为始的天津研学，最终成为我情感世界的“融冰”之旅。在名校与企业的行走之间，我遇见了一个如蝶般生动、如玉般温润的女孩。短短半日的同行：看海、喂鸥、漫步、长谈，她以她的鲜活与细腻，悄然松动了我因过往而筑起的心墙。回归日常，我发现自己重新感受到了情绪的波纹。原来有些相遇，纵然如星光照亮一瞬，却足以让一颗心从此相信光，并带着那份暖意，继续走向更远的明天。]]></description>
    <content:encoded><![CDATA[时间过得很快，今天已经回到广州了，刚出地铁站，还是那份熟悉的景色。又开始回归到了正常生活，一如既往。直到回家躺下，回忆这一周，不禁思绪万千。
## 启程：奔赴一场未知的相遇
这次活动是学校组织的一次研学活动，目标地点是天津。学校为了让我们接触到更多的名校和民企，以拓宽我们的视野。这次活动意义非凡，我收获了很多，并且结识了一位女孩。同时，我非常感谢自己当时做了一个正确的决定。我本身在广州实习，由于距离原因，一开始不是很想去，后来再三思索还是和主管请了假。于是我便坐了28小时的火车，来到了这座我曾经来过数次的城市——天津。
## 首日印象：学府之间的瞻仰与自省
第一天的行程是天津大学。我们来到这里，听着解说员讲解天津大学——也就是北洋大学堂——的百年建校史，使我不禁赞叹与仰慕。之后我们参加了一个两校间的交流会，双方老师和同学相互分享着他们在学校内所做的事情和所获得的荣誉。这里我就不得不说，不愧是985高校，他们无论是学生还是老师的表达能力与逻辑思维都很不凡。我有些受困于自己的耐心不足和英语差劲，不然，如果让自己重新回到高中，真想再努力一把。然后，我们尝到了天津大学的美食——四十二斋。吃完之后，我在学校里逛了逛，我们就去到了下一个地点，河北工业大学。

这所大学是河北省属唯一的一所211，但现在坐落于天津，也是蛮有意思的。我们照例参观了河北工业大学的校史馆和科技园区。在听解说的过程中，我看到了自己最熟悉的前后端技术展示。看着其他同学对这些东西流露出的向往和羡慕，我内心想起一句老话：外行看热闹，内行看门道。对于我来说，这可能是再简单不过的东西了。但意义最深刻的环节是交流会——学校真的很用心，为我们每个人都摆了一张写着名字的纸牌。这次交流会印象也很深刻，听到河工大同学在校内所做的各种事情，我再一次意识到“人外有人，山外有山”。之后我们在学校食堂简单吃了饭，就回到了酒店。晚上和朋友们出去打了会台球，第一天就这样结束了。
## 翌日行程：科技企业与理工探索
第二天早晨，由于一些调整，出行计划由天津科技大学变为了参观科大讯飞。这是一家在国内专门做语音服务的大厂。我们走进企业，解说员带领我们参观了他们各种前沿的技术项目，比如AI语音、AI讲课软件等。行程比较快，讲解不到一小时就结束了。我和学院的朋友们在这里合了张影。

下午，我们来到了天津理工大学。还是一如既往地参观了学校的校史馆，这里便不赘述了。参观之后，我们被分到各个学院去听介绍。我被分到了材料科学与工程学院，这是一个化学类的学院。化学是我高中最喜欢的学科，但由于就业形势，我后来选择了计算机。但是，热爱依旧。我很认真地倾听了老师的全部讲解，以前在书本上看到的东西终于变成了实物，我被深深吸引，以至于身边的朋友都在调侃我。我倒无所谓地笑了笑。晚上，我和朋友们去津湾广场逛了逛，也吃了些饭。这时不得不提我另一位导助朋友——我跟他说我不爱吃蔬菜，于是他点了十盘肉，我真是笑了。
## 第三日：细微触动与初遇
第三天，我们按计划去了天津科技大学。流程和之前参观的学校差不多，我便不一一赘述了。但他们有一点令我感触很深：学校非常注重基层教育，格外关注辅导员与学生的关系，并且有切实的行动。离校前，我们拍了张集体照。就在那时，我遇到了那个女孩。她问我：“你是山西人，为什么会讲东北话？”我平淡地回答：“因为我室友是东北的，他教了我四年。”于是，我们加了微信。在这之后，感觉这次旅途的意义仿佛变得更重了。

下午，我们参观了天津的钢铁厂。我第一次感受到近千度高温辐射出的炙热，真的非常震撼，也听到了很多前辈的指导。我戴着一顶可能型号偏小的安全帽，样子有些突兀，朋友们也借此调侃我，这里就不放自己的“丑照”啦。今天晚上也挺“逆天”的，班长喊我去网吧，玩了一整晚的《三角洲行动》，我也没推脱掉。
## 最终日：海风、笑容与心灵的叩问
最后一天，是这次旅行对我而言意义最重的一天。因为学校想让我们自由活动，所以把原定上下午的行程都集中安排在了上午。我们在中午12点前就解散了，整个下午都是自由的。我本来要和两位朋友一起去天津港，但他们想直接去，而我想先回一趟酒店。恰巧，新认识的那位女生，她也想回酒店。她对我说：“我自己一个人去吧。”但我觉得那样太过孤单，于是便告诉朋友：“你们先去，我之后到。”之后，我便约她：“我们一起吧。”

我们回到酒店，吃完午饭后便打车前往天津港。因为路途遥远，车上我们一直在聊天。她给我讲她在学校里的各种事：包括争取国家奖学金时的忐忑与坎坷，在图书馆发生的趣事，在学院学生会认识的朋友等等。我作为一个倾听者，认真地听着她讲述的点点滴滴。别看路程久，在这一个小时的闲聊中，我们不知不觉就到了港口。我们在一位老太太那儿花了十块钱，买了几个面包和一根火腿肠，打算喂海鸥——之前我一直叫它们“鸽子”，她为我纠正了好几次。我的朋友已经先到了，他们参观完后因时间原因要提前去国家海洋博物馆，而我们刚到，便示意他们先走。于是，我们开始拍照。她耐心地告诉我她的大疆相机如何使用。我学会后，为她拍了许多照片。海风的声音、海鸥的叫声与她的笑容，仿佛一起定格在了我的心里。她是一个很有素养的女孩，在我为她拍完后，她也为我拍了很多，还用拍立得为我拍了一张（据说相纸超级贵）。在一次次拍照中，我们的关系慢慢熟络了起来。

由于国家海洋博物馆下午4:30禁止入内，在喂完海鸥后，考虑到时间，我们决定用猜拳来抉择——规则是我赢就去，她赢就回，每轮三局两胜。出乎意料地，两轮我都输了……看来是天意。于是我们原路返回，沿着海边慢悠悠地走着。这是我第一次看海。从小我就对海有着很深的向往，但不知为何，来过几次沿海城市，却从未真正走到海边。所以我望着海，又望向她，感到一种莫名的温馨——陪我第一次看海的人，竟然是才认识一天的女生。

之后我们上了地铁。她和我说，她的耳机好像丢了，可能落在了我们返回的路上。看着她难过的样子，我心中也流露出一丝苦涩。大约一小时后，我们下了车，步行去一家叫“胖子菜馆”的饭店吃饭。途中遇到一只小猫，她蹲下身，拿出下午买来的火腿肠喂它。那种自然而然的爱心，又一次触动了我。因为下午的车费是我付的，晚上她执意要请我吃饭。她点菜，我烫餐具，配合得有条不紊，仿佛早已相识多年。不得不说，这家饭店菜量真足，味道也很香。我们点了两个菜一个汤，到最后竟然剩下一大半。我们边吃边聊边开玩笑，吃了一个多小时。她带着一丝小埋怨的语气对我说：“我请你吃饭，你就吃这么点！”但我当时真的吃不下了。她说，等明年她回洛阳时，要带我去吃我没吃过的特色美食，还问我……要不要试试她的粉色学士服。

吃完饭，我们又去了和平区的民园广场和瓷房子。我依旧喜欢为她拍照。在这里，我似乎拍到一张很有感觉的照片，但也不确定。我们也合影了，因为她太漂亮，我总觉得自己的样子不入镜。她说：“我可不喜欢不自信的男孩。”这句话使我心头一震。这时我才意识到，自己何时变得不自信了？以前在朋友口中，我是所谓的“社牛”；在网友群里，他们称呼我是“群宠”。我的自信不止于此——它让我在当面试官时能不怒自威，也让我站在讲台前从容自若。我所过之处，似乎从未留下不自信的痕迹，怎么到了现在，却成了这样？也正是在这一刻，我的内心好像被轻轻拨动了。

之后往回走的路上，她问我：“你的MBTI是什么？”我跟她说我是“紫人”。她一步步猜，最后猜到了ENTJ——是的，这确实是我的人格。她也让我猜她的，我猜是ENFP。她说猜对了三个字母，实际她是INFP。我心里默默想着：这么多彩的一只“小蝴蝶”，是我第一次遇见。她的出现，仿佛为我原本单调的人生画布上，添了很大一抹亮色。

我们还经过了名创优品。她带我认识了“乌萨奇”（黄色的）、“吉伊”（粉色的）和“小八”（蓝色的），并且一次次考我它们的名字，答错了就会“狠狠”盯着我。她说：“下次见面，答错了你就完了。”我们此时的状态，就像我在青春时期幻想过的情侣那样，无话不说，无话不谈。我们还去了一家巧克力店，和路人一起合买了巧克力……

之后，我们就在往回走的路上了。她走不动了，蹲了下来。我伸手拉她，她抓住了我的手。我感受到她手冷冰冰的触感，但不知为什么，我的心却在缓缓融化。

路上，她说：“遇到你这么好的一个男生，会拍照，情绪还这么稳定，为什么大四了才遇到你。”

路口的微风正好穿过，吹起了她额前的几丝头发，又迅速落下。

是啊，为什么？为什么我大四了才遇到她？为什么我在临近离开的时候才遇到她……

之后我们回到了酒店。我帮她修理电脑直到深夜。第二天便是离开的时候，我把勉强修好，仍不稳定的电脑交还给她。她说想再和我聊一会儿，但只穿着薄薄的睡衣下楼，有些冷。而且因为时间紧张，我还要去买些东西，于是我们只是草草地击了个掌，道了声“再见”，便分别了。
## 归途回望：情感的涟漪与自省
两天时间一闪而过。回到家中，回到这个熟悉的地方，我躺在床上，感到一阵茫然，一切恍然若梦，一场甜美恍惚的梦。

在我大学的时候，由于家庭因素，我被动地终止了一段感情。也正是那次变故，让我的性格发生了很大变化——我关闭了情感闸门，不再想接收情绪价值，只想要切实可行的利益。倒不是自私自利，只是觉得情感太过脆弱。我每天都沉浸在代码与工作中，慢慢失去了对细微情绪的感知能力，生活的乐趣也全靠自己的成就驱动。我自己能清晰地感觉到，我的生活越来越淡然。但她的忽然闯入，为我本来灰暗的日常，添加了五颜六色。哪怕只是半天，半个下午，我发现自己内心的封锁好像有点松动了。我不再那么淡漠情感了，我不知道这到底是好是坏，但是呢，感觉好像还不错？

讲了这么多，现在自己也恢复了平静。写着写着，我竟潸然泪下，这很出乎我的意料。总归，这是一次很棒的旅行，我也很幸运，能结识这样一位美好的女孩。

明天就要步入正轨了。今天，是一次很久以来彻底的情感释放。可能有两年半了，自我压抑了太久。希望今晚可以让我重振信心。

我会继续努力，带着对她的思念与那份海风的声音。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>第一次漂泊的一点点勇气</title>
    <link>https://www.gfbzsblog.site/main/Articles/168</link>
    <comments>https://www.gfbzsblog.site/main/Articles/168#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Mon, 01 Dec 2025 21:09:05 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/168</guid>

    <description><![CDATA[从忙碌的离校、实习到坐上一天火车南下广州，我的生活在这三个月里经历了巨大的变化。初到广州的陌生、租房时的狼狈、新公司的适应、第一次真正意义上的“一个人生活”，让我从校园走向社会的过程变得真实又鲜活。虽然离家很远，但小公寓的温度、同事的友好、AI 工具带来的幸福感，以及这座城市的节奏，都让我慢慢找到了属于自己的新生活。]]></description>
    <content:encoded><![CDATA[距离上次写文章已经将近三个月了。中间各种事情堆在一起，学校的课、每天跟着学校实习累成狗、办理各种离校手续，时间被压得一点不剩。好不容易空出几天，又重新做回了“~~网瘾少年~~”，结果博客自然也就落下了。现在回头想想，那段离校前后的日子，其实挺让人感慨的。

离校前的节奏完全是匆匆忙忙的，赶着把所有课程上完，赶着处理学校里所有该收尾的事，赶着收拾宿舍、打包四年的生活。直到真正站在校门口准备离开时，那种突然冒出来的不舍感，让我一下子意识到大学真的要结束了。不舍我的辅导员，不舍朋友和同学，不舍那些曾经天天见面的老师。大学四年就这么一晃而过，下次再见，不知道得是多久以后了。那种惬意的大学生活，真的只有离开的时候才会格外怀念。

离校之后，我坐了一整天火车来到了广州——一个我之前从来没想过自己会来的城市。对我来说，这里离家太远了，我在山西，而广州在地图上几乎是对角线的尽头。我想家，而家人则担心我一个人在外面生活得不好。刚到广州那天，把行李放到公司之后我就去找房子了，很快租下了一间小公寓。第一晚没枕头、没褥子、没被子，睡得凉飕飕的，但过几天东西都置办齐了，小屋也被我整理得干干净净。
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/3d9efc46-4d83-41d3-b6c9-37fe30fc1231.jpg" alt="图片" width="300" />
</center>现在这个家虽然不大，却有种刚刚好的温度。楼下什么都有——餐馆、药店、快递驿站；再往前走一点就是商场和电影院；路上还能看到各种 coser。去公司也特别近，走路十几分钟，骑车七八分钟就到。老实说，这就是我梦寐以求的生活，以后换地方要是离公司不够近，我可能都会嫌弃。

刚来广州时还担心会不会水土不服，但住了几天就发现挺适合我。冬天的气候跟我家立秋差不多，白天甚至还要穿短袖。广州的饭也很对我胃口，尤其是我这种爱吃肉的人。隆江猪脚饭、煲仔饭、烧鸭、烧鹅、烧腊……每一样都能让我吃得很满足。来广州的某天下午，我还骑车去了广州塔。路是真的堵，又赶上下班高峰，我又不熟路，绕来绕去折腾了半天。不过好在最后还是打了卡，那天拍到的照片我还挺满意的。<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/e02de2b5-d5b6-461f-9bce-fd1b10f160fa.jpg" alt="图片" width="300" />
</center>

到了公司没几天，我就跟产品、UI、后端还有主管都熟络了。主管人很好，不是那种高压型领导，更像朋友一样。今天跟他聊天，他提到招人特别难，根本招不到。我当时还愣了一下，大家不是都说前端难找工作吗？那为什么企业又招不到人？聊着聊着，也算是得出一个共识：可能是很多求职者学得不够扎实，基础和技术栈都没准备到位。所以也想顺便分享一句感受：找工作的时候，真的要把基础打牢，把技术栈补全，准备越充分，选择就越多。

顺便说一句，公司配备的 AI 工具 Cursor 真是太好用啦，各种模型能随便切换还免费用，再也不用自己掏钱了😄，这大概是我入职后最快感受到“幸福感”的一件事。
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/b03a6970-fb71-4808-ba8f-e30762fa7b55.png" alt="图片" width="300" />
</center>

写到这里，其实心里多少有点感慨。从慢慢离开学校，到正式走向社会，这个过程像是人生的一个明显转折。不过每天上班路上，看着很多和我差不多年纪的人走在同一条街上，突然又觉得其实大家都一样，都是在为了自己的目标努力着。那一刻反而不觉得孤单了。

加油吧，各位。也加油吧，我自己。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>前端存储技术的对比</title>
    <link>https://www.gfbzsblog.site/main/Articles/158</link>
    <comments>https://www.gfbzsblog.site/main/Articles/158#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Wed, 17 Sep 2025 16:18:07 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/158</guid>

    <description><![CDATA[本文深入探讨了 sessionStorage、localStorage 和 cookie 三种前端存储技术的区别。Cookie 适合与服务器交互的场景，如用户认证；localStorage 适合持久化存储用户偏好和配置；sessionStorage 适合临时数据存储，如表单草稿。文章还分析了它们与浏览器缓存策略的本质区别，并提供了实际应用场景和最佳实践建议，帮助开发者在项目中做出正确的技术选择。]]></description>
    <content:encoded><![CDATA[&emsp;&emsp;在前端开发中，我们经常需要在浏览器中存储数据。无论是保存用户的登录状态、记住用户的偏好设置，还是实现购物车功能，都离不开浏览器存储技术。目前主流的浏览器存储方案有三种：sessionStorage、localStorage 和 cookie。虽然它们都能在浏览器中存储数据，但各自有着不同的特性和适用场景。

## 一、存储机制的本质差异

### Cookie：HTTP 协议的产物

&emsp;&emsp;Cookie 最初是为了解决 HTTP 协议无状态特性而设计的。HTTP 协议本身是无状态的，这意味着服务器无法区分不同的请求是否来自同一个用户。Cookie 通过在客户端存储标识信息，并在每次请求时自动发送给服务器，从而解决了这个问题。

&emsp;&emsp;Cookie 的工作机制是这样的：当服务器需要记住用户状态时，会在响应头中设置 `Set-Cookie`，浏览器接收到这个响应后，会将 Cookie 信息存储在本地。之后，浏览器在向同一域名发送请求时，会自动在请求头中携带这些 Cookie 信息。

### localStorage：HTML5 的本地存储解决方案

&emsp;&emsp;localStorage 是 HTML5 规范中引入的 Web Storage API 的一部分。它的设计目标是提供一个简单的键值对存储机制，让开发者可以在客户端持久化存储数据。与 Cookie 不同，localStorage 的数据不会自动发送给服务器，它纯粹是客户端的存储方案。

&emsp;&emsp;localStorage 的数据会一直保存在浏览器中，直到用户主动清除或者通过 JavaScript 代码删除。即使关闭浏览器、重启电脑，数据依然存在。

### sessionStorage：会话级别的存储

&emsp;&emsp;sessionStorage 同样属于 HTML5 的 Web Storage API，但它与 localStorage 有着根本性的区别。sessionStorage 的数据只在当前浏览器标签页或窗口中有效，一旦标签页关闭，数据就会被清除。

&emsp;&emsp;这种设计使得 sessionStorage 非常适合存储一些临时的、会话级别的数据，比如表单的草稿、临时的用户操作状态等。

## 二、详细特性对比

### 存储容量限制

**Cookie**：每个 Cookie 的大小限制为 4KB，每个域名下最多可以存储 20 个 Cookie（不同浏览器可能有差异）。这个限制相对较小，主要原因是 Cookie 会在每次 HTTP 请求中传输，过大的 Cookie 会显著影响网络性能。

**localStorage**：通常可以存储 5-10MB 的数据（具体大小因浏览器而异）。这个容量对于大多数客户端存储需求来说是足够的。

**sessionStorage**：容量限制与 localStorage 相同，通常也是 5-10MB。

### 数据生命周期

**Cookie**：
- 可以设置过期时间（expires 或 max-age）
- 如果不设置过期时间，则为会话 Cookie，关闭浏览器后失效
- 设置了过期时间的 Cookie 会一直存在，直到过期或被手动删除

**localStorage**：
- 数据永久存储，除非用户手动清除或通过代码删除
- 不会因为关闭浏览器而丢失
- 不会因为重启电脑而丢失

**sessionStorage**：
- 数据只在当前标签页有效
- 关闭标签页后数据立即清除
- 刷新页面不会清除数据

### 数据访问方式

**Cookie**：
```javascript
// 设置 Cookie
document.cookie = "username=john; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/";

// 读取 Cookie
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

// 删除 Cookie
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
```

**localStorage**：
```javascript
// 设置数据
localStorage.setItem('username', 'john');

// 读取数据
const username = localStorage.getItem('username');

// 删除数据
localStorage.removeItem('username');

// 清空所有数据
localStorage.clear();
```

**sessionStorage**：
```javascript
// 设置数据
sessionStorage.setItem('username', 'john');

// 读取数据
const username = sessionStorage.getItem('username');

// 删除数据
sessionStorage.removeItem('username');

// 清空所有数据
sessionStorage.clear();
```

### 作用域和安全性

&emsp;&emsp;Cookie 在作用域控制方面提供了最为精细的配置选项。通过 domain 属性，开发者可以控制 Cookie 在哪些域名下有效。比如，如果你设置 `domain=.example.com`，那么这个 Cookie 就可以在 `www.example.com`、`api.example.com` 等所有子域名下被访问。这种设计使得大型网站可以在不同子域名之间共享用户状态，比如让用户在购物网站和用户中心之间保持登录状态。

&emsp;&emsp;path 属性则进一步细化了 Cookie 的作用范围，它决定了 Cookie 在网站的哪些路径下有效。例如，设置 `path=/admin` 意味着这个 Cookie 只在 `/admin` 路径及其子路径下有效，这为不同功能模块提供了数据隔离的可能性。

&emsp;&emsp;在安全性方面，Cookie 提供了多层防护机制。secure 属性确保 Cookie 只在 HTTPS 连接下传输，这在当今网络安全要求越来越高的环境下显得尤为重要。httpOnly 属性则是一个重要的安全特性，它防止恶意 JavaScript 代码通过 `document.cookie` 访问敏感的 Cookie 数据，有效防止了 XSS（跨站脚本攻击）对用户认证信息的窃取。

&emsp;&emsp;sameSite 属性是近年来新增的重要安全特性，它有三种设置：Strict、Lax 和 None。Strict 模式最为严格，完全禁止跨站请求携带 Cookie，有效防止 CSRF（跨站请求伪造）攻击。Lax 模式则在保证安全性的同时提供了一定的灵活性，允许某些安全的跨站请求携带 Cookie。None 模式则允许所有跨站请求携带 Cookie，但必须配合 secure 属性使用。

### localStorage 的作用域和安全性特点

&emsp;&emsp;localStorage 的作用域控制相对简单，它严格遵循浏览器的同源策略。这意味着只有来自相同协议、域名和端口的页面才能访问同一个 localStorage 存储空间。这种设计在保证数据安全的同时，也为不同网站之间的数据隔离提供了天然的屏障。

&emsp;&emsp;然而，localStorage 在安全性方面存在一些局限性。它没有像 Cookie 那样的丰富安全属性，所有的数据都以明文形式存储，任何能够访问该页面的 JavaScript 代码都可以读取和修改 localStorage 中的数据。这种特性使得 localStorage 不适合存储敏感信息，如用户密码、支付信息等。

&emsp;&emsp;localStorage 的另一个特点是数据共享范围。一旦数据被存储到 localStorage 中，同源的所有页面和标签页都可以访问这些数据。这种设计使得 localStorage 非常适合存储用户偏好设置、应用配置等需要在多个页面间共享的数据，但同时也意味着如果某个页面被恶意代码攻击，可能会影响到整个域名的数据安全。

### sessionStorage 的作用域和安全性特征

&emsp;&emsp;sessionStorage 在作用域控制方面与 localStorage 类似，同样遵循同源策略，只有同源的页面才能访问。但它的独特之处在于数据隔离的粒度更细。sessionStorage 的数据只能在创建它的那个特定标签页或窗口中访问，即使是同一个域名的其他标签页也无法访问这些数据。

&emsp;&emsp;这种标签页级别的隔离设计使得 sessionStorage 在安全性方面具有独特的优势。即使某个标签页被恶意代码攻击，也不会影响到其他标签页的数据安全。这种特性使得 sessionStorage 非常适合存储临时性的敏感数据，比如表单的临时输入、临时的用户操作状态等。

&emsp;&emsp;不过，sessionStorage 同样缺乏额外的安全属性设置，数据以明文形式存储，任何能够访问该标签页的 JavaScript 代码都可以读取和修改数据。因此，虽然标签页级别的隔离提供了一定的安全保障，但在存储敏感信息时仍然需要谨慎考虑。

&emsp;&emsp;总的来说，这三种存储技术在作用域和安全性方面各有特点：Cookie 提供了最精细的作用域控制和最丰富的安全属性，但同时也带来了性能开销；localStorage 提供了简单的作用域控制和数据共享能力，但安全性相对较弱；sessionStorage 则在保证同源安全的基础上，提供了标签页级别的数据隔离，适合存储临时数据。在实际应用中，开发者需要根据数据的安全要求和共享需求来选择合适的存储方案。

## 三、一些实际应用场景

### Cookie 的典型应用场景

**用户认证和会话管理**：

&emsp;&emsp;Cookie 在用户认证和会话管理方面发挥着不可替代的作用。当用户成功登录后，服务器会生成一个唯一的认证令牌，这个令牌通过 Cookie 的方式存储在用户的浏览器中。由于 Cookie 会在每次 HTTP 请求中自动发送给服务器，服务器就能够通过这个令牌来识别用户的身份，无需用户重复登录。

&emsp;&emsp;这种机制的优势在于它的透明性和自动化。用户不需要手动处理认证信息，浏览器会自动管理这些数据。同时，通过设置合适的过期时间，可以实现"记住我"功能，让用户在指定时间内保持登录状态。更重要的是，通过设置 httpOnly 属性，可以防止恶意 JavaScript 代码窃取认证令牌，大大提高了安全性。

```javascript
// 登录成功后设置认证 Cookie
function setAuthCookie(token) {
    const expires = new Date();
    expires.setTime(expires.getTime() + (7 * 24 * 60 * 60 * 1000)); // 7天
    document.cookie = `auth_token=${token}; expires=${expires.toUTCString()}; path=/; secure; httpOnly`;
}
```

**用户偏好设置**：

&emsp;&emsp;Cookie 也非常适合存储用户的个性化偏好设置，比如语言选择、时区设置、显示偏好等。这些设置通常需要在用户访问网站的不同页面时保持一致，而 Cookie 的自动传输特性正好满足了这个需求。

&emsp;&emsp;当用户选择了某种语言后，这个偏好会被存储在 Cookie 中，用户访问网站的任何页面时，服务器都能读取到这个设置，从而为用户提供相应语言的内容。这种机制使得用户不需要在每个页面都重新设置偏好，大大提升了用户体验。

```javascript
// 记住用户的语言偏好
function setLanguagePreference(lang) {
    document.cookie = `language=${lang}; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/`;
}
```

**购物车状态**：

&emsp;&emsp;在电商网站中，Cookie 经常被用来保存用户的购物车状态。当用户添加商品到购物车时，商品信息会被存储在 Cookie 中。即使用户关闭浏览器后重新打开，购物车中的商品依然存在，这避免了用户因为意外关闭浏览器而丢失购物车内容的困扰。

&emsp;&emsp;这种应用场景特别适合那些不需要用户登录就能购物的网站，或者用户还没有登录但已经开始浏览商品的情况。通过 Cookie 保存购物车状态，可以确保用户的购物体验不会因为技术问题而中断。

```javascript
// 保存购物车商品ID
function saveCartItems(itemIds) {
    const cartData = JSON.stringify(itemIds);
    document.cookie = `cart_items=${cartData}; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/`;
}
```

### localStorage 的典型应用场景

**用户设置和配置**：

&emsp;&emsp;localStorage 是存储用户个性化设置的理想选择。与 Cookie 不同，localStorage 的数据不会在每次请求中传输，这意味着它不会增加网络开销，同时也没有大小限制的困扰。用户可以设置各种界面偏好，比如主题颜色、字体大小、布局方式等，这些设置会被永久保存在 localStorage 中。

&emsp;&emsp;这种持久化存储的特性使得用户在任何时候访问网站，都能看到自己熟悉的界面设置。即使是在不同的设备上，只要用户登录了同一个账户，这些设置也能被同步和恢复。这种一致性对于提升用户体验非常重要。

```javascript
// 保存用户的主题偏好
function saveThemePreference(theme) {
    localStorage.setItem('user_theme', theme);
}

// 保存用户的界面设置
function saveUISettings(settings) {
    localStorage.setItem('ui_settings', JSON.stringify(settings));
}
```

**离线数据缓存**：

&emsp;&emsp;localStorage 在数据缓存方面表现出色。现代 Web 应用经常需要从服务器获取大量数据，如果每次都通过网络请求获取，不仅会增加服务器负担，还会影响用户体验。通过 localStorage 缓存 API 响应数据，可以显著减少网络请求，提升应用性能。

&emsp;&emsp;这种缓存机制特别适合那些数据更新频率不高，但对访问速度要求较高的场景。比如新闻网站的文章内容、电商网站的商品信息、社交媒体的用户资料等。通过合理的缓存策略，可以在保证数据新鲜度的同时，大幅提升用户体验。

```javascript
// 缓存API响应数据
function cacheAPIData(key, data) {
    const cacheData = {
        data: data,
        timestamp: Date.now(),
        expires: Date.now() + (60 * 60 * 1000) // 1小时过期
    };
    localStorage.setItem(`cache_${key}`, JSON.stringify(cacheData));
}

// 读取缓存数据
function getCachedData(key) {
    const cached = localStorage.getItem(`cache_${key}`);
    if (!cached) return null;
    
    const cacheData = JSON.parse(cached);
    if (Date.now() > cacheData.expires) {
        localStorage.removeItem(`cache_${key}`);
        return null;
    }
    
    return cacheData.data;
}
```

**表单数据持久化**：

&emsp;&emsp;localStorage 在表单处理方面也有独特的优势。当用户在填写复杂的表单时，如果因为网络问题或其他原因导致页面刷新，用户之前输入的内容就会丢失，这会给用户带来很大的困扰。通过 localStorage 自动保存表单数据，可以避免这种情况的发生。

&emsp;&emsp;这种机制不仅适用于简单的表单，对于多步骤的复杂表单更加有用。用户可以随时中断填写过程，稍后继续完成，而不用担心数据丢失。这种贴心的功能设计，往往能够显著提升用户对网站的满意度和信任度。

```javascript
// 自动保存表单草稿
function saveFormDraft(formId, formData) {
    localStorage.setItem(`draft_${formId}`, JSON.stringify(formData));
}

// 恢复表单草稿
function restoreFormDraft(formId) {
    const draft = localStorage.getItem(`draft_${formId}`);
    return draft ? JSON.parse(draft) : null;
}
```

### sessionStorage 的典型应用场景

**临时状态管理**：

&emsp;&emsp;sessionStorage 在管理临时状态方面表现出色。当用户在浏览网页时，经常会遇到需要保持当前状态的情况，比如滚动位置、展开的菜单状态、选中的标签页等。这些状态信息通常只在当前会话中有效，不需要跨会话保持，sessionStorage 正好满足这种需求。

&emsp;&emsp;比如，当用户在一个长页面中滚动到某个位置，然后点击链接跳转到其他页面，再通过浏览器的后退按钮返回时，如果能够自动恢复到之前的滚动位置，用户体验会大大提升。sessionStorage 可以轻松实现这种功能，而且数据会在标签页关闭时自动清理，不会占用不必要的存储空间。

```javascript
// 保存当前页面的滚动位置
function saveScrollPosition() {
    sessionStorage.setItem('scroll_position', window.scrollY.toString());
}

// 恢复滚动位置
function restoreScrollPosition() {
    const position = sessionStorage.getItem('scroll_position');
    if (position) {
        window.scrollTo(0, parseInt(position));
    }
}
```

**多步骤表单状态**：

&emsp;&emsp;在多步骤表单中，sessionStorage 发挥着重要作用。用户填写复杂表单时，往往需要分多个步骤完成，每个步骤都有相应的数据需要保存。sessionStorage 可以保存当前步骤信息以及每个步骤的填写数据，确保用户可以在不同步骤之间自由切换，而不会丢失已填写的内容。

&emsp;&emsp;这种应用场景在注册流程、调查问卷、订单提交等场景中非常常见。用户可以在填写过程中随时返回修改之前的内容，也可以暂时离开页面，稍后继续完成。sessionStorage 的标签页级别隔离特性，确保了不同标签页中的表单数据不会相互干扰。

```javascript
// 保存多步骤表单的当前步骤
function saveFormStep(step, formData) {
    sessionStorage.setItem('current_step', step.toString());
    sessionStorage.setItem(`step_${step}_data`, JSON.stringify(formData));
}

// 获取当前步骤
function getCurrentStep() {
    return parseInt(sessionStorage.getItem('current_step') || '1');
}
```

**临时用户操作记录**：

&emsp;&emsp;sessionStorage 还经常被用来记录用户的临时操作行为。这些记录通常用于分析用户的使用习惯、提供个性化的用户体验，或者用于调试和问题排查。由于这些数据只在当前会话中有效，不需要长期保存，sessionStorage 是最合适的选择。

&emsp;&emsp;比如，可以记录用户在当前会话中访问了哪些页面、进行了哪些操作、遇到了哪些错误等。这些信息可以帮助开发者了解用户的使用模式，优化产品设计。同时，由于数据会在会话结束时自动清理，也避免了长期存储可能带来的隐私问题。

&emsp;&emsp;总的来说，这三种存储技术在实际应用中各有其独特的价值。Cookie 凭借其自动传输特性，在需要与服务器交互的场景中不可替代；localStorage 的大容量和持久化特性，使其成为客户端数据存储的主力军；sessionStorage 的临时性和标签页隔离特性，使其在管理临时状态方面表现出色。在实际开发中，往往需要根据具体的业务需求，灵活运用这些不同的存储技术。

```javascript
// 记录用户的临时操作
function logUserAction(action, data) {
    const actions = JSON.parse(sessionStorage.getItem('user_actions') || '[]');
    actions.push({
        action: action,
        data: data,
        timestamp: Date.now()
    });
    sessionStorage.setItem('user_actions', JSON.stringify(actions));
}
```

## 四、与浏览器缓存策略的区别

&emsp;&emsp;读到这里，有些小伙伴可能会产生疑问：这些存储技术听起来和浏览器的缓存策略有点像，比如强缓存、协商缓存之类的。确实，它们之间有一些相似之处，但本质上有着根本性的区别。

### 缓存策略的本质：性能优化的技术手段

&emsp;&emsp;浏览器缓存策略（包括强缓存和协商缓存）本质上是一种性能优化技术。它的主要目的是减少网络请求，提升页面加载速度。当浏览器请求一个资源时，会先检查本地缓存，如果缓存有效就直接使用，避免重复的网络请求。

&emsp;&emsp;强缓存通过设置 `Cache-Control` 和 `Expires` 等响应头来控制资源的缓存时间。在这个时间内，浏览器会直接使用本地缓存的资源，不会向服务器发送请求。协商缓存则通过 `ETag` 和 `Last-Modified` 等机制，让服务器判断资源是否有更新，如果没有更新就返回 304 状态码，告诉浏览器继续使用缓存。

&emsp;&emsp;这种缓存机制主要针对的是静态资源，比如 HTML 文件、CSS 样式表、JavaScript 脚本、图片、字体等。这些资源通常不会频繁变化，通过缓存可以显著减少网络传输，提升用户体验。

### Storage 的本质：用户偏好和状态管理

&emsp;&emsp;而 sessionStorage、localStorage 和 cookie 这些存储技术，它们的本质是用户偏好和状态管理。它们存储的不是静态资源，而是用户的个性化数据、应用状态、用户行为记录等。

&emsp;&emsp;比如，用户选择了深色主题，这个偏好会被存储在 localStorage 中，下次访问时应用会自动应用这个主题。用户填写了一半的表单，这些数据会被保存在 sessionStorage 中，即使页面刷新也不会丢失。用户登录后的认证信息会被存储在 cookie 中，确保用户在整个网站中保持登录状态。

### 数据生命周期和管理方式的不同

&emsp;&emsp;缓存策略的数据生命周期通常由服务器控制，通过 HTTP 响应头来设置缓存时间。当缓存过期时，浏览器会自动向服务器请求新的资源。这种管理方式是自动化的，用户通常感知不到缓存的存在。

&emsp;&emsp;而 Storage 的数据生命周期则由开发者控制，通过 JavaScript 代码来设置、读取和删除数据。开发者需要根据业务需求来决定何时存储数据、何时清理数据。这种管理方式更加灵活，但也需要开发者承担更多的责任。

### 存储内容的性质差异

&emsp;&emsp;缓存策略存储的内容通常是"公共的"，即所有用户访问同一个资源时，缓存的内容都是一样的。比如，所有用户访问同一个 CSS 文件时，缓存的内容都是相同的。

&emsp;&emsp;而 Storage 存储的内容通常是"个性化的"，即每个用户存储的内容可能都不同。比如，不同用户可能有不同的主题偏好、不同的表单数据、不同的浏览历史等。

### 性能影响的不同

&emsp;&emsp;缓存策略主要影响的是网络性能，通过减少网络请求来提升页面加载速度。它的性能优化效果是全局的，对所有用户都有益。

&emsp;&emsp;而 Storage 主要影响的是用户体验，通过保存用户状态和偏好来提升交互体验。它的优化效果是个性化的，主要针对单个用户的使用体验。

## 总结

&emsp;&emsp;通过上文的分析，我们对 sessionStorage、localStorage 和 cookie 这三种前端存储技术有了基本的认识。它们虽然都能在浏览器中存储数据，但各自有着独特的特性和适用场景。

&emsp;&emsp;Cookie 最适合需要与服务器交互的场景，如用户认证、会话管理等。虽然容量限制较大，但其自动传输特性和丰富的安全属性使其在特定场景下不可替代。

&emsp;&emsp;localStorage 是客户端持久化存储的最佳选择，适合存储用户偏好、应用配置等需要长期保存的数据。其大容量和简单的 API 使其成为现代 Web 应用的首选。
&emsp;&emsp;sessionStorage 则专注于临时数据存储，适合存储表单草稿、临时状态等不需要持久化的数据。其标签页级别的隔离特性使其在特定场景下非常有用。

&emsp;&emsp;值得注意的是，这三种存储技术与浏览器缓存策略有着本质性的区别。缓存策略是一种性能优化技术，主要解决网络传输效率问题；而 Storage 技术是一种状态管理技术，主要解决用户体验和个性化问题。

&emsp;&emsp;在实际开发中，我们应该根据数据的特点、安全需求和性能要求来选择合适的存储方案。很多时候，一个应用会同时使用多种存储技术，每种技术负责不同的数据存储需求。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>让代码自己说话：单元测试的艺术</title>
    <link>https://www.gfbzsblog.site/main/Articles/157</link>
    <comments>https://www.gfbzsblog.site/main/Articles/157#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Mon, 15 Sep 2025 19:48:52 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/157</guid>

    <description><![CDATA[本文从前端开发者的角度，分享了单元测试的学习心得和实践经验。从最初的困惑和抗拒，到踩坑后的理解，再到实际应用中的收获，详细介绍了单元测试的核心原则、实战技巧和最佳实践。通过具体的代码示例和实际案例，帮助前端开发者理解单元测试的价值，掌握测试技能，提升代码质量和开发效率。]]></description>
    <content:encoded><![CDATA[&emsp;&emsp;刚开始接触前端开发时，我对单元测试充满了困惑。为什么要写这些看起来"多余"的代码？为什么要花时间测试那些"显而易见"的功能？亦或是单元测试不是测试工程师应该干的，跟我们前端开发有什么关系？直到我在项目中踩了几个坑，好不容易写好了但是在运行的时候出bug了，让我不得不重新修改，重新提交，这种麻烦，才让我真正理解了单元测试的价值。

&emsp;&emsp;还记得有一次，我修改了一个看似简单的工具函数，结果把整个页面搞崩了，花了大半天才找到问题所在。如果当时有测试，这个问题可能几分钟就能发现。还有一次，我在重构代码时，不小心改坏了一个组件的逻辑，直到一位朋友反馈才知道出了问题。这些经历让我意识到，单元测试不是可有可无的，而是现代前端开发中不可或缺的一环。

&emsp;&emsp;近期刚对单元测试做了一些了解，也写了部分UI组件和工具函数的测试用例，收获颇丰。从最初的抗拒到现在的认可，这个过程让我对代码质量有了更深的理解。

&emsp;&emsp;今天，我想和大家分享我对单元测试的理解和感悟，希望能帮助刚入门的小伙伴们少走一些弯路。

## 一、什么是单元测试？为什么前端需要它？

### 什么是单元测试？

&emsp;&emsp;简单来说，单元测试就是**测试你写的每一个小功能是否按预期工作**。比如你写了一个计算两个数相加的函数，单元测试就是验证这个函数在各种情况下都能给出正确的结果。

```javascript
// 你写的函数
function add(a, b) {
  return a + b;
}
// 对应的单元测试
test('add函数应该能正确计算两个数的和', () => {
  expect(add(2, 3)).toBe(5);
  expect(add(-1, 1)).toBe(0);
  expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
```

### 为什么前端需要单元测试？

**1. 前端代码越来越复杂**
现在的前端不再是简单的页面展示，而是复杂的单页应用。一个按钮点击可能触发多个状态变化，一个表单提交可能涉及多个API调用。没有测试，真的很难保证所有功能都正常工作。

**2. 用户界面变化频繁**
前端需求变化快，今天要改个样式，明天要加个功能。有了测试，你就能放心地修改代码，不用担心改坏了其他地方。

**3. 团队协作需要**
在团队开发中，你的代码可能会被其他人修改。有了测试，就能确保代码在修改后仍然按预期工作。

&emsp;&emsp;由此可见，单元测试的重要性是不可忽略的。它不仅能提高代码质量，还能让开发过程更加高效和可靠。无论是个人项目还是团队协作，单元测试都是现代前端开发中不可或缺的一环。

## 二、单元测试的核心原则

### 1. 测试单一功能
&emsp;&emsp;每个测试应该只验证一个功能点。不要在一个测试中验证多个不相关的功能。这样做的好处是，当测试失败时，你能立即知道是哪个具体功能出了问题，而不是在一大堆测试中慢慢排查。

&emsp;&emsp;想象一下，如果你把用户登录的所有功能都放在一个测试里，当测试失败时，你根本不知道是用户名验证出了问题，还是密码验证有问题，或者是登录逻辑本身有bug。分开测试后，问题定位就变得非常精准。

```javascript
// 不好的做法：一个测试验证多个功能
test('用户登录功能', () => {
  // 验证用户名验证
  // 验证密码验证  
  // 验证登录成功
  // 验证错误处理
});

// 好的做法：分别测试每个功能
test('应该验证用户名格式', () => {
  expect(validateUsername('')).toBe(false);
  expect(validateUsername('abc')).toBe(true);
});

test('应该验证密码强度', () => {
  expect(validatePassword('123')).toBe(false);
  expect(validatePassword('abc123')).toBe(true);
});
```

### 2. 测试边界情况
&emsp;&emsp;不仅要测试正常情况，还要测试边界情况和异常情况。这是很多新手容易忽略的地方。我们往往只测试"正常流程"，但用户在使用过程中会遇到各种意外情况。

&emsp;&emsp;比如一个除法函数，不仅要测试10除以2等于5这种正常情况，还要测试0除以5等于0这种边界情况，更要测试10除以0这种异常情况。只有把这些都考虑到了，你的代码才真正健壮。

```javascript
test('计算器除法功能', () => {
  // 正常情况
  expect(divide(10, 2)).toBe(5);
  
  // 边界情况
  expect(divide(0, 5)).toBe(0);
  
  // 异常情况
  expect(() => divide(10, 0)).toThrow('除数不能为零');
});
```

### 3. 测试应该是独立的
&emsp;&emsp;每个测试应该能够独立运行，不依赖其他测试的结果。这个原则非常重要，因为测试的执行顺序是不确定的，如果测试之间有依赖关系，就会出现"有时候能通过，有时候不能通过"的奇怪现象。

&emsp;&emsp;大家想一下，如果第一个测试创建了一个用户，第二个测试依赖这个用户来删除，那么当测试顺序改变时，第二个测试就会失败。正确的做法是每个测试都自己准备需要的数据，测试结束后自己清理。

```javascript
// 不好的做法：测试之间有依赖
let user = null;

test('创建用户', () => {
  user = createUser('test@example.com');
  expect(user).toBeTruthy();
});

test('删除用户', () => {
  deleteUser(user.id); // 依赖上一个测试
  expect(getUser(user.id)).toBeNull();
});

// 好的做法：每个测试独立
test('创建用户', () => {
  const user = createUser('test@example.com');
  expect(user).toBeTruthy();
  // 测试结束后清理
  deleteUser(user.id);
});

test('删除用户', () => {
  const user = createUser('test@example.com');
  deleteUser(user.id);
  expect(getUser(user.id)).toBeNull();
});
```

## 三、前端单元测试的一些例子

### 测试React组件

&emsp;&emsp;在React项目中，我们通常使用Jest和React Testing Library来测试组件。React Testing Library的设计理念是"测试组件的行为，而不是实现细节"，这意味着我们关注的是用户能看到和交互的内容，而不是组件内部的状态管理。

&emsp;&emsp;比如测试一个按钮组件，我们关心的是按钮是否显示了正确的文本，点击后是否触发了正确的事件，而不是按钮内部用了什么状态管理方式。

```javascript
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('按钮应该显示正确的文本', () => {
  render(<Button>点击我</Button>);
  expect(screen.getByText('点击我')).toBeInTheDocument();
});

test('按钮点击应该触发onClick事件', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>点击我</Button>);
  
  fireEvent.click(screen.getByText('点击我'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

test('按钮应该支持不同的变体', () => {
  render(<Button variant="primary">主要按钮</Button>);
  expect(screen.getByText('主要按钮')).toHaveClass('btn-primary');
});
```

### 测试工具函数

&emsp;&emsp;工具函数通常是最容易测试的，因为它们没有副作用，输入什么就输出什么。但即使是简单的工具函数，也要考虑各种边界情况。

&emsp;&emsp;比如这个日期格式化函数，不仅要测试正常的日期输入，还要测试空值、无效值等边界情况。这些边界情况往往是bug的温床。

```javascript
// 工具函数
export function formatDate(date) {
  if (!date) return '';
  return new Date(date).toLocaleDateString('zh-CN');
}

// 测试
test('formatDate应该正确格式化日期', () => {
  const date = '2023-12-25';
  expect(formatDate(date)).toBe('2023/12/25');
});

test('formatDate应该处理空值', () => {
  expect(formatDate(null)).toBe('');
  expect(formatDate(undefined)).toBe('');
});
```

### 测试API调用

&emsp;&emsp;API调用是前端测试中的一个难点，因为涉及到网络请求。我们需要模拟这些网络请求，确保测试的稳定性和速度。

&emsp;&emsp;这里的关键是使用`jest.fn()`来模拟`fetch`函数，然后控制它的返回值。这样我们就能测试各种情况：成功的情况、失败的情况、网络错误的情况等等。

```javascript
// API函数
export async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('用户不存在');
  }
  return response.json();
}

// 测试
test('fetchUser应该返回用户数据', async () => {
  // 模拟fetch
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: '张三' })
  });

  const user = await fetchUser(1);
  expect(user).toEqual({ id: 1, name: '张三' });
});

test('fetchUser应该处理错误情况', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    ok: false
  });

  await expect(fetchUser(999)).rejects.toThrow('用户不存在');
});
```

## 四、常见测试场景

### 1. 测试表单组件

&emsp;&emsp;表单是前端开发中最常见的组件之一，也是最容易出错的地方。用户输入验证、提交处理、错误提示等等，每一个环节都需要仔细测试。

&emsp;&emsp;这里我们测试两个关键场景：一是表单验证，确保必填字段的验证逻辑正确；二是用户输入处理，确保表单能正确响应用户的输入操作。

```javascript
test('表单提交应该验证必填字段', async () => {
  render(<LoginForm />);
  
  const submitButton = screen.getByText('登录');
  fireEvent.click(submitButton);
  
  expect(await screen.findByText('用户名不能为空')).toBeInTheDocument();
  expect(await screen.findByText('密码不能为空')).toBeInTheDocument();
});

test('表单应该正确处理用户输入', () => {
  render(<LoginForm />);
  
  const usernameInput = screen.getByLabelText('用户名');
  const passwordInput = screen.getByLabelText('密码');
  
  fireEvent.change(usernameInput, { target: { value: 'testuser' } });
  fireEvent.change(passwordInput, { target: { value: 'password123' } });
  
  expect(usernameInput.value).toBe('testuser');
  expect(passwordInput.value).toBe('password123');
});
```

### 2. 测试条件渲染

&emsp;&emsp;条件渲染是React中非常常见的模式，根据不同的状态显示不同的内容。测试这种场景时，我们需要验证组件在不同状态下是否正确渲染了对应的内容。

&emsp;&emsp;这里使用`rerender`来重新渲染组件，模拟状态变化，然后验证渲染结果是否符合预期。

```javascript
test('应该根据用户状态显示不同内容', () => {
  const { rerender } = render(<UserProfile user={null} />);
  expect(screen.getByText('请先登录')).toBeInTheDocument();
  
  rerender(<UserProfile user={{ name: '张三' }} />);
  expect(screen.getByText('欢迎，张三')).toBeInTheDocument();
});
```

### 3. 测试异步操作

&emsp;&emsp;异步操作是前端开发中的另一个难点，特别是涉及到数据加载、状态更新等场景。测试异步操作时，我们需要等待异步操作完成，然后验证结果。

&emsp;&emsp;这里使用`waitFor`来等待异步操作完成，确保测试的稳定性。

```javascript
test('应该显示加载状态', async () => {
  render(<UserList />);
  
  expect(screen.getByText('加载中...')).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('用户列表')).toBeInTheDocument();
  });
});
```

## 五、测试最好的实践

### 1. 测试命名要清晰

&emsp;&emsp;测试的名称应该清楚地描述测试的内容和预期结果。一个好的测试名称应该让其他人（包括未来的你）一眼就能看懂这个测试在做什么，就像你在git上提交代码一样，如果写的不清不楚，到时候根本就不知道自己在干什么。

&emsp;&emsp;想象一下，几个月后你看到测试失败的报告，如果测试名称是"测试1"，你根本不知道这个测试在验证什么功能。但如果名称是"当用户点击删除按钮时，应该显示确认对话框"，你立即就知道问题出在哪里。

&emsp;&emsp;这就像写代码注释一样，好的测试名称本身就是最好的文档。当你的项目越来越大，测试用例越来越多时，清晰的命名就显得尤为重要。一个团队中，如果每个人都能通过测试名称快速理解测试的目的，那么代码维护的效率会大大提高。

```javascript
// 不好的命名
test('测试1', () => {});
test('应该工作', () => {});

// 好的命名
test('当用户点击删除按钮时，应该显示确认对话框', () => {});
test('当输入无效邮箱时，应该显示错误信息', () => {});
```

### 2. 使用描述性的断言

&emsp;&emsp;断言应该尽可能具体和描述性。不要只写`expect(result).toBe(true)`，而要写`expect(result).toHaveLength(3)`或`expect(result).toContain('expected value')`。

&emsp;&emsp;这样当测试失败时，错误信息会更加有用，能帮助你快速定位问题。

```javascript
// 不好的断言
expect(result).toBe(true);

// 好的断言
expect(result).toBe(true);
expect(result).toHaveLength(3);
expect(result).toContain('expected value');
```

### 3. 测试覆盖率要合理

&emsp;&emsp;不要盲目追求100%的覆盖率，重点测试核心业务逻辑和容易出错的地方。有些代码（比如简单的getter/setter）可能不值得写测试，而有些复杂的业务逻辑则需要重点测试。

### 4. 保持测试简单

&emsp;&emsp;测试代码应该比被测试的代码更简单，更容易理解。如果测试代码本身就很复杂，那么当测试失败时，你不仅要调试被测试的代码，还要调试测试代码，这就本末倒置了。

## 六、Jest单元测试的一些常见问题和解决方案

### 1. 测试环境配置问题

&emsp;&emsp;测试环境的配置往往是新手遇到的第一个难题。Jest需要知道如何运行你的代码，如何处理模块导入，如何模拟浏览器环境等等。

&emsp;&emsp;这个配置告诉Jest使用`jsdom`环境（模拟浏览器），设置测试文件的路径映射，以及测试前的准备工作。

```javascript
// 在jest.config.js中配置
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};
```

### 2. 模拟外部依赖

&emsp;&emsp;前端代码经常依赖浏览器的API，比如`localStorage`、`fetch`等。在测试环境中，这些API可能不存在或者行为不一致，所以我们需要模拟它们。

&emsp;&emsp;这里我们创建了一个`localStorage`的模拟对象，用`jest.fn()`来模拟各种方法，然后将其赋值给`global.localStorage`，这样测试代码就能正常使用了。

```javascript
// 模拟localStorage
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn()
};
global.localStorage = localStorageMock;
```

### 3. 测试异步代码

&emsp;&emsp;异步代码的测试是另一个常见难点。JavaScript的异步特性让测试变得复杂，我们需要等待异步操作完成，然后验证结果。

&emsp;&emsp;这里展示了两种处理异步测试的方法：使用async/await直接等待异步函数完成，或者使用waitFor等待DOM更新。

```javascript
// 使用async/await
test('异步函数测试', async () => {
  const result = await asyncFunction();
  expect(result).toBe('expected');
});

// 使用waitFor
test('等待元素出现', async () => {
  render(<Component />);
  await waitFor(() => {
    expect(screen.getByText('加载完成')).toBeInTheDocument();
  });
});
```

## 七、测试驱动开发(TDD)

&emsp;&emsp;测试驱动开发是一种开发方法，先写测试，再写代码。这种方法听起来反直觉，但实际上能带来很多好处：更好的代码设计、更高的测试覆盖率、更少的bug等等。

### TDD的基本流程

1. **Red**：写一个失败的测试
2. **Green**：写最少的代码让测试通过
3. **Refactor**：重构代码，保持测试通过

这个循环不断重复，每次循环都会让代码质量得到提升。

### 示例：实现一个简单的计算器

&emsp;&emsp;让我们通过一个简单的例子来理解TDD的过程。我们要实现一个计算器，能够计算两个数的和。

&emsp;&emsp;首先，我们写一个测试，描述我们期望的行为。这个测试会失败，因为我们还没有实现Calculator类。

```javascript
// 第一步：写测试（Red）
test('计算器应该能计算两个数的和', () => {
  const calculator = new Calculator();
  expect(calculator.add(2, 3)).toBe(5);
});

// 第二步：写代码（Green）
class Calculator {
  add(a, b) {
    return a + b;
  }
}

// 第三步：重构（Refactor）
// 优化代码结构，保持测试通过
```

&emsp;&emsp;然后，我们写最少的代码让测试通过。这里我们只需要实现一个简单的add方法。

&emsp;&emsp;最后，我们可以重构代码，优化结构，但必须保持测试通过。这样我们就有了一个既简单又可靠的实现。

## 总结：测试让代码更可靠

&emsp;&emsp;单元测试不是负担，而是帮助我们写出更好代码的工具。通过测试，我们能够：

- **提前发现问题**：在开发阶段就发现bug，而不需要你眼中那位“点点点”一直在给你挑毛病。
- **安全重构**：放心地优化代码结构，不用担心改坏了其他地方，测试会告诉你哪里出了问题。

- **提高代码质量**：测试过程促使我们思考边界情况，那些你平时容易忽略的异常场景。

- **增强信心**：知道代码按预期工作，部署时心里有底，不用提心吊胆。

&emsp;&emsp;记住，好的测试不是追求100%的覆盖率，而是测试那些真正重要的功能。从简单的函数开始，逐步建立测试习惯，你会发现测试让开发变得更加有趣和可靠。
]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>新功能上线——胡桃妙记&amp;豆包析义</title>
    <link>https://www.gfbzsblog.site/main/Articles/156</link>
    <comments>https://www.gfbzsblog.site/main/Articles/156#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sat, 30 Aug 2025 09:29:55 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/156</guid>

    <description><![CDATA[新增功能，双AI摘要模式，为您的阅读体验赋能。]]></description>
    <content:encoded><![CDATA[### 新功能上线啦
作为一个将要上职场的学生，我始终相信，技术的温度在于更好地服务他人。经过几天的探索与实现，我很高兴为博客带来了全新的**双模式 AI 摘要系统**，希望能为每一位读者提供更贴心、更个性化的阅读预览。

  **当前，您可以选择两种独特的摘要风格：**

- **豆包析义**：清晰亲切的技术概览，精准提炼文章核心要点，助您快速抓住重点，节省阅读时间。
- **胡桃妙记**：一份轻松有趣的“技术甜点”！借助《原神》中胡桃的活泼语气与生动比喻，让技术概念变得好玩好记，为阅读增添一抹亮色。

其实在此之前，我对胡桃这个角色也并未深入了解。但她那种活泼洒脱、亲切可爱的性格，却令人过目难忘。我希望通过她的语言风格，为读者在阅读技术长文前带来一丝轻松，用俏皮与热情缓解技术的枯燥，同时也为您的学习过程增添一份快乐。

我为摘要提供了部分淡色配色方案，并且您的所有偏好（摘要风格与配色）都会通过 **LocalStorage 保存在本地**。如果您偏好严谨专业的表述，**豆包**愿为您提供清晰的技术解读；如果您喜欢轻松活泼的风格，**胡桃**也乐意随时相伴。下次访问，您依然会看到自己最熟悉的界面。

我深知，再智能的AI也无法替代创作者的思考。因此，**“博主观点的”** 摘要也在规划之中。它将由我亲自执笔，为您点明文章的核心动机与设计脉络，为您提供一份最终的阅读“地图”。（其实已经做好了，但是我写的摘要太流水账了😅）

希望这个新功能能让您的技术探索之旅更愉悦、更高效。如果您喜欢这一设计，欢迎在评论区告诉我~~~

您的认可，将是我持续开发新功能的最大动力。

P.S. 也欢迎来抖音私信我哦！下面是我的抖音，最常用的通信工具😘
```meta
title: 熙夜
url: https://www.douyin.com/user/MS4wLjABAAAAeK227ZM7DqkdYg7Qr-KL7CSqB9B3hp8hPhtZPbjRKus
image: https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-i-0813c000-ce_oofo189CAYBoajuiGPieQEAyEBAGIwE0zFAevA~c5_300x300.jpeg?from=2956013662
desc: 水拥抱水沉默，水听见风起舞
```]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Node.js 包管理工具（npm、yarn、pnpm、cnpm）详解</title>
    <link>https://www.gfbzsblog.site/main/Articles/122</link>
    <comments>https://www.gfbzsblog.site/main/Articles/122#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sun, 24 Aug 2025 16:02:02 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/122</guid>

    <description><![CDATA[Node.js 常见的包管理工具有 npm、yarn、pnpm、cnpm。npm 是官方默认，生态最广；yarn 解决了依赖不一致和速度问题，适合团队协作；pnpm 通过硬链接提升速度并节省磁盘空间，尤其适合大型项目和 monorepo；cnpm 主要解决国内网络环境下的下载加速。实际选择中，小型项目推荐 npm，大型项目推荐 pnpm，团队项目可用 yarn，而 cnpm 建议仅作为加速手段使用。]]></description>
    <content:encoded><![CDATA[## 概述
在 Node.js 生态中，**包管理工具（Package Manager）** 是开发者日常使用最频繁的工具之一。它的主要职责包括：

* **安装依赖**：从远程仓库下载第三方包，并保存到本地 `node_modules`。
* **版本管理**：通过 `lock` 文件锁定依赖版本，避免团队成员环境不一致。
* **依赖解析**：根据 `package.json` 的描述，解析依赖树，安装直接依赖与间接依赖。
* **脚本执行**：运行 `package.json` 中的脚本命令，例如 `npm run dev`。

随着社区的发展，涌现了多个包管理工具：

* **npm**：Node.js 自带，最早也是最广泛使用。
* **yarn**：Facebook 推出，强调性能与一致性。
* **pnpm**：新一代方案，磁盘利用率和速度表现最佳。
* **cnpm**：国内团队推出的 npm 镜像工具，加速下载。

## 1. npm

### 背景

npm（Node Package Manager）是 **Node.js 官方默认的包管理器**，自 2010 年起逐渐成为全球最大的软件包生态。

### 特点

* **官方维护**，与 Node.js 版本强绑定。
* **package-lock.json**：确保依赖版本一致。
* **npx 工具**：便于执行临时命令或二进制文件。
* **生态最庞大**：npm registry 已经是世界上最大的软件仓库。

### 常见用法

```bash
npm init -y             # 初始化
npm install express     # 安装依赖
npm uninstall lodash    # 卸载依赖
npm update              # 更新依赖
npm run dev             # 运行脚本
```

### 适用场景

* 默认选择，**小型项目或通用场景**足够。
* 当不考虑磁盘占用和速度时，npm 是最稳妥的方案。

## 2. Yarn

### 背景

Yarn 由 Facebook 在 2016 年推出，当时 **npm v4** 还没有 lock 文件，团队协作中经常出现依赖版本不一致的问题。Yarn 引入了 **yarn.lock** 和并行下载，大幅度提升了开发体验。

### 特点

* **一致性强**：`yarn.lock` 保证每个人安装相同版本依赖。
* **安装速度快**：并行下载 + 本地缓存机制。
* **命令简洁**：如 `yarn add`，语义更清晰。
* **Yarn 2/3**：支持 Plug’n’Play (PnP)，不再依赖 `node_modules`。

### 常见用法

```bash
yarn init -y
yarn add express
yarn remove lodash
yarn upgrade
yarn dev
```

### 适用场景

* **团队协作项目**，确保依赖一致。
* 对速度有要求，但还未迁移到 pnpm。

## 3. pnpm

### 背景

**pnpm (Performant npm)** 是近几年迅速崛起的包管理器，以 **高效磁盘利用率** 和 **极快的安装速度** 著称。它的核心创新是 **内容寻址存储（Content-Addressable Storage）**。

### 特点

* **磁盘节省**：不同项目相同依赖只保存一份，通过符号链接共享。
* **极快安装**：硬链接 + 并行安装。
* **严格依赖隔离**：避免“幽灵依赖”（phantom dependency）问题。
* **支持 monorepo**：天然支持大型仓库管理（如 NX、Turborepo）。

### 常见用法

```bash
pnpm init
pnpm install express
pnpm remove lodash
pnpm update
pnpm dev
```

### 适用场景

* **大型项目**，依赖体积庞大，磁盘压力大。
* **monorepo 管理**，多个子项目共享依赖。

## 4. cnpm

### 背景

国内开发者使用 npm 时，经常因为网络原因下载缓慢或失败。为此，淘宝团队推出了 **cnpm**，默认使用 **淘宝 npm 镜像**。

### 特点

* 解决国内网络环境问题，加快安装速度。
* 命令与 npm 基本一致，学习成本低。

### 安装

```bash
npm install -g cnpm --registry=https://registry.npmmirror.com
```

### 使用

```bash
cnpm install express
```

### 适用场景

* 国内开发环境。
* 仅作为 **加速工具**，本质上依然依赖 npm。

## 5. 包管理工具对比

| 工具   | 速度     | 磁盘占用     | 一致性 | 特点                  | 适用场景 |
| ---- | ------ | -------- | --- | ------------------- | ---- |
| npm  | 中等     | 较大       | ✔️  | 官方维护，生态最广           | 默认选择 |
| yarn | 较快     | 中等       | ✔️  | 锁文件一致性、团队协作友好       | 团队项目 |
| pnpm | **最快** | **最小**   | ✔️  | 硬链接存储、monorepo 管理优秀 | 大型项目 |
| cnpm | 取决于镜像  | 与 npm 一致 | ✔️  | 国内加速工具              | 国内环境 |

## 6. 包管理工具工作原理（Mermaid 图）

下面这张图展示了 **npm/yarn/pnpm 在安装依赖时的基本工作流程**：

```mermaid
flowchart TD
  A[读取 package.json] --> B[解析依赖树]
  B --> C{是否已有 lock 文件}
  C -- 有 --> D[锁定版本 安装依赖]
  C -- 无 --> E[解析最新版本 写入 lock 文件]
  D --> F[下载包]
  E --> F[下载包]
  F --> G[npm: 每个项目独立 node_modules]
  F --> H[yarn: 并行下载 + 缓存]
  F --> I[pnpm: 硬链接到全局存储]
  G --> J[生成 node_modules]
  H --> J
  I --> J
```

## 7. 总结与最佳实践

* **npm**：默认选择，稳定可靠，适合所有场景。
* **yarn**：协作一致性好，适合已有团队规范。
* **pnpm**：性能与磁盘利用率最优，推荐新项目和 monorepo。
* **cnpm**：仅用于国内加速，建议改用 npm 镜像（如 `npmmirror.com`）。

👉 **推荐策略**

* 小型项目 → `npm`
* 团队协作 → `yarn`
* 大型项目 / monorepo → `pnpm`
* 国内环境 → 配置 npm 镜像，而非依赖 `cnpm`
如果想要了解更多Node.js，可以点击下方链接⬇️

```meta
title: Node.js
url: https://github.com/nodejs/examples
image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png
desc: Node.js 官方示例
```]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Node.js 模块化详解</title>
    <link>https://www.gfbzsblog.site/main/Articles/121</link>
    <comments>https://www.gfbzsblog.site/main/Articles/121#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sun, 24 Aug 2025 15:56:04 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/121</guid>

    <description><![CDATA[Node.js 模块化的发展经历了从 CommonJS 到 ES Module（ESM） 的演进。CommonJS 使用 require 和 module.exports，同步加载、适合服务端；ESM 使用 import 和 export，支持静态分析与顶层 await，是浏览器和 Node.js 的统一标准。模块化的核心价值在于 作用域隔离、代码复用、可维护性，新项目推荐使用 ESM 与前端保持一致。]]></description>
    <content:encoded><![CDATA[## 概述
在现代 JavaScript 开发中，**模块化** 是核心思想之一。它的目标是将代码拆分为 **独立的、可复用的单元**，从而提高可维护性与可扩展性。

Node.js 从诞生开始，就内置了 **模块化系统**，并且伴随着 ECMAScript 标准的发展，经历了从 **CommonJS → ES Module（ESM）** 的演进。

常见的模块化方案：

- **CommonJS**：Node.js 默认支持的规范（`require` / `module.exports`）。
- **ES Module（ESM）**：JavaScript 官方标准（`import` / `export`）。
- **AMD / CMD / UMD**：主要用于浏览器的早期模块化方案。

## 1. 为什么需要模块化？

早期 JavaScript 没有模块化，所有代码都放在一个文件或全局作用域中，导致：

- **命名冲突**：全局变量容易相互覆盖。
- **代码难以维护**：随着项目变大，逻辑混乱。
- **复用性差**：无法轻易在不同项目间共享代码。

模块化的优势：

- **隔离作用域**：不同模块互不干扰。
- **按需加载**：提高性能。
- **代码复用**：形成生态（如 npm 包）。
- **可维护性**：分治思想，便于协作开发。

## 2. Node.js 模块化体系

Node.js 默认支持 **CommonJS**，后来逐步支持 **ESM**，目前两者并存。

### 2.1 CommonJS

Node.js 早期采用的模块系统。

**语法**：

```js
// 导出
const fs = require('fs');

module.exports = {
  readConfig: () => fs.readFileSync('./config.json', 'utf8')
};
// 引入
const { readConfig } = require('./config.js');
console.log(readConfig());
```

特点：

- **同步加载**：适合服务端（磁盘读取速度快）。
- **缓存机制**：同一个模块只会加载一次，多次 `require` 返回同一个对象。
- **动态加载**：`require` 可以在逻辑语句中调用。

### 2.2 ES Module（ESM）

JavaScript 官方标准，Node.js 从 v12 开始支持。

**语法**：

```js
// 导出
export function sum(a, b) {
  return a + b;
}

export default function greet(name) {
  return `Hello, ${name}`;
}
// 引入
import greet, { sum } from './utils.js';

console.log(greet('Alice'));
console.log(sum(1, 2));
```

特点：

- **静态分析**：编译阶段就能确定依赖关系。
- **严格模式**：默认启用 `strict mode`。
- **顶层 await**：支持在模块顶层使用 `await`。

Node.js 中使用 ESM 需要：

1. 文件扩展名为 `.mjs`，或在 `package.json` 设置 `"type": "module"`。
2. 使用 `import` / `export`。

### 2.3 CommonJS 与 ESM 的区别

| 特性     | CommonJS (`require`)         | ESM (`import/export`)       |
| -------- | ---------------------------- | --------------------------- |
| 加载方式 | **同步**                     | **异步**                    |
| 导出     | `module.exports` / `exports` | `export` / `export default` |
| 引入     | `require()` 动态调用         | `import` 静态分析           |
| 缓存机制 | 有（单例）                   | 有（单例）                  |
| 运行环境 | Node.js 原生支持             | Node.js / 浏览器标准        |

## 3. 其他模块化方案（了解）

在 Node.js 出现之前，前端社区也有一些模块化方案：

- **AMD (Asynchronous Module Definition)**：RequireJS 提出，适合浏览器异步加载。
- **CMD (Common Module Definition)**：SeaJS 提出，按需加载，更灵活。
- **UMD (Universal Module Definition)**：兼容 AMD 和 CommonJS 的方案。

如今这些方案已逐渐被 ESM 取代。

## 4. Node.js 模块加载机制

Node.js 内部通过 **模块解析算法** 来加载模块：

1. **核心模块**：如 `fs`、`path`，优先级最高。
2. **文件模块**：相对路径或绝对路径。
3. **第三方模块**：通过 `node_modules` 目录查找（逐层向上）。

查找规则：

```bash
require('lodash')
# 查找顺序：
# ./node_modules/lodash
# ../node_modules/lodash
# ../../node_modules/lodash
```

**模块缓存**：

- 首次加载模块时会执行并缓存结果。
- 再次加载时直接从缓存中取，避免重复执行。

## 5. 总结与最佳实践

- 在 **Node.js 服务端开发**中，仍然以 **CommonJS** 为主，但 ESM 越来越普及。
- **新项目推荐使用 ESM**，与前端保持统一。
- **不要混用 CommonJS 和 ESM**，可能导致兼容性问题。
- **合理拆分模块**：保持单一职责，避免“大而全”的文件。
- **利用 npm 包生态**：模块化的最终目标是代码复用与共享。

如果想要了解更多Node.js，可以点击下方链接⬇️
```meta
title: Node.js
url: https://github.com/nodejs/examples
image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png
desc: Node.js 官方示例
```
]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Next.js 与 Nuxt.js：现代前端框架的王者对决</title>
    <link>https://www.gfbzsblog.site/main/Articles/119</link>
    <comments>https://www.gfbzsblog.site/main/Articles/119#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sat, 23 Aug 2025 10:24:09 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/119</guid>

    <description><![CDATA[Next.js（基于React）和Nuxt.js（基于Vue）均为现代全栈框架，支持SSR/SSG。Next更灵活强大，Nuxt更简单高效，选型取决于技术栈偏好。]]></description>
    <content:encoded><![CDATA[在追求高性能、高 SEO 友好度的现代 Web 开发中，基于 React 的 Next.js 和基于 Vue 的 Nuxt.js 无疑是两大顶流框架。它们都解决了单页面应用（SPA）在SEO和首屏加载上的核心痛点，提供了强大的服务端渲染（SSR）和静态站点生成（SSG）能力。但选择哪一个，往往取决于您的技术栈、项目需求和团队背景。

#### 一、框架概述与技术定位


Next.js 是基于 React 的全栈框架，由 Vercel 公司维护。它提供服务器端渲染（SSR）、静态站点生成（SSG）、API 路由等能力，是 React 生态中最成熟的企业级框架。最新版本支持的 App Router 引入了 React Server Components 等现代特性，进一步提升了开发体验和应用性能。

Nuxt.js 是基于 Vue.js 的渐进式框架，其设计理念深受 Next.js 启发。它通过约定优于配置的原则，为 Vue 开发者提供开箱即用的 SSR、SSG 和全栈开发能力。Nuxt 3 版本基于 Vue 3 重构，在性能和开发者体验上有显著提升。

#### 二、核心特性对比

1. **路由系统**
   Next.js 同时支持 Pages Router（基于文件系统）和 App Router（基于 React Server Components）。App Router 支持更复杂的布局嵌套、加载状态和错误处理，但学习曲线相对较高。
   Nuxt.js 采用基于文件系统的自动路由生成，只需在 `pages` 目录中创建 `.vue` 文件即可生成对应路由，配置简单直观。

2. **数据获取**
   Next.js 提供精细的数据获取方法：

- `getServerSideProps`：服务器端渲染时获取数据
- `getStaticProps`：构建时静态生成
- `getStaticPaths`：指定动态路由的静态生成
  Nuxt.js 提供统一的 `useAsyncData` 和 `useFetch` 组合式函数，可根据运行环境自动选择客户端或服务端数据获取。

3. **状态管理**
   Next.js 不内置状态管理，开发者可自由选择 Redux、Zustand 或 Context API 等方案。
   Nuxt.js 内置轻量级状态管理（`useState`），并推荐使用 Pinia 作为官方状态管理库。

4. **配置方式**
   Next.js 采用配置优于约定，通过 `next.config.js` 提供高度可定制化。
   Nuxt.js 采用约定优于配置，通过 `nuxt.config.ts` 提供合理的默认配置，减少决策成本。

| 特性         | Next.js (React)         | Nuxt.js (Vue)         |
| ------------ | ----------------------- | --------------------- |
| **技术栈**   | React                   | Vue                   |
| **路由**     | 文件系统/App Router     | 约定式文件路由        |
| **数据获取** | getServerSideProps等    | useAsyncData/useFetch |
| **状态管理** | 需自行选择（如Zustand） | 内置 + 推荐Pinia      |
| **配置方式** | 配置优于约定            | 约定优于配置          |
| **部署优化** | 增量静态再生(ISR)       | Nitro服务器引擎       |

#### 三、性能表现

两者在性能方面都表现出色：

- Next.js 的增量静态再生（ISR）是其标志性特性，允许在构建后更新静态内容
- Nuxt.js 基于 Nitro 服务器引擎，提供跨平台部署能力和卓越的服务器性能
- 两者都提供自动代码分割、图片优化等性能优化功能

#### 四、生态系统

Next.js 拥有更庞大的生态系统：

- 丰富的插件市场和社区资源
- 与 Vercel 平台深度集成
- 更完善的企业级解决方案

Nuxt.js 的模块系统颇具特色：

- 官方和社区提供大量功能模块
- 集成体验更加流畅
- 对内容型网站的支持更友好（如 @nuxt/content 模块）

#### 五、选型建议

选择 Next.js 当：

- 团队熟悉 React 生态系统
- 项目需要高度定制化和控制力
- 需要利用最新的 React 特性
- 项目复杂度高，需要企业级解决方案

选择 Nuxt.js 当：

- 团队熟悉 Vue.js 技术栈
- 追求开发效率和开箱即用体验
- 项目以内容展示为主（如文档、博客）
- 需要快速原型开发

#### 六、总结

Next.js 和 Nuxt.js 都是优秀的元框架，它们之间的差异主要反映了 React 和 Vue 两个生态系统不同的设计哲学。Next.js 更注重灵活性和控制力，适合复杂的大型应用；Nuxt.js 更注重开发体验和开箱即用，适合快速迭代的项目。

技术选型应该基于团队的技术背景、项目需求和长期维护考虑。无论选择哪个框架，都能获得现代化的开发体验和优秀的应用性能。建议在实际选型前，可以用两个框架分别实现一个简单的功能原型，亲身体验其开发模式和特性。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Vue 的双向绑定原理</title>
    <link>https://www.gfbzsblog.site/main/Articles/118</link>
    <comments>https://www.gfbzsblog.site/main/Articles/118#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Fri, 22 Aug 2025 21:21:02 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/118</guid>

    <description><![CDATA[Vue 的双向绑定依赖于“数据劫持 + 发布订阅 + 事件监听”机制。通过 Object.defineProperty（Vue2）或 Proxy（Vue3）拦截数据的读写，实现对属性变化的监听；在模板渲染时收集依赖，数据更新后通知相关视图重新渲染；而 v-model 本质上是 :value 与 @input 的语法糖，使得用户输入能反向更新数据。最终实现了数据与视图的双向同步。]]></description>
    <content:encoded><![CDATA[在 Vue 中，所谓的“双向绑定”指的是 **数据变化会自动更新视图，而视图输入的变化也会反过来修改数据**。典型例子：

```vue
<input v-model="message" />
<p>{{ message }}</p>
```

当用户输入时，`message` 自动更新；当代码里改 `message`，输入框的值也会变。这种自动同步效果背后，就是 **数据劫持 + 发布订阅 + 事件监听**。

## 一、数据劫持：监听数据的变化

Vue 2 使用 `Object.defineProperty`，Vue 3 使用 `Proxy`。以 Vue 2 为例，核心逻辑是：

```javascript
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log("读取属性:", key);
      return val;
    },
    set(newVal) {
      console.log("修改属性:", key, "=", newVal);
      val = newVal;
      // 通知视图更新
      updateView();
    }
  });
}

function updateView() {
  console.log("视图更新");
}

let data = {};
defineReactive(data, "message", "hello");

console.log(data.message); // 读取属性: message
data.message = "hi";       // 修改属性: message = hi
                           // 视图更新
```

这里 `Object.defineProperty` 就是 Vue 2 的核心，让每个属性变成“可观测的”。

## 二、依赖收集：视图订阅数据变化

Vue 渲染时，模板里的 `{{ message }}` 会触发 getter。Vue 在 getter 中做了一件事：**把当前的 Watcher（订阅者）记录到依赖里**。

```javascript
let currentWatcher = null;

function defineReactive(obj, key, val) {
  let dep = []; // 存储依赖的订阅者

  Object.defineProperty(obj, key, {
    get() {
      if (currentWatcher) dep.push(currentWatcher); 
      return val;
    },
    set(newVal) {
      val = newVal;
      dep.forEach(fn => fn()); // 通知所有订阅者更新
    }
  });
}

let data = {};
defineReactive(data, "message", "hello");

// 模拟 Watcher
currentWatcher = () => console.log("视图重新渲染:", data.message);
console.log(data.message); // 依赖收集
currentWatcher = null;

data.message = "hi"; // 视图重新渲染: hi
```

这就是 **发布订阅模式**：数据一改 → 通知订阅者 → 重新渲染。

## 三、视图到数据：事件监听

`v-model` 并不是什么魔法，它等价于：

```vue
<input :value="message" @input="message = $event.target.value">
```

也就是说：

1. `:value="message"` 保证输入框显示数据。
2. `@input="..."` 保证输入时数据被更新。

于是形成闭环：

- **数据改 → set → 通知更新 → 视图刷新**
- **视图改 → input 事件 → 数据更新 → 通知更新**

## 四、Vue 2 和 Vue 3 的区别

- **Vue 2**：基于 `Object.defineProperty`，不能检测到新增/删除属性，需要 `Vue.set`。
- **Vue 3**：基于 `Proxy`，可拦截对象的各种操作（读、写、删、增），更加彻底。

```javascript
let data = new Proxy({ message: "hello" }, {
  get(target, key) {
    console.log("读取:", key);
    return target[key];
  },
  set(target, key, value) {
    console.log("设置:", key, "=", value);
    target[key] = value;
    return true;
  }
});

console.log(data.message); // 读取: message
data.message = "hi";       // 设置: message = hi
```

## 小结

Vue 的双向绑定就是三步：

1. **数据劫持**：用 `defineProperty` 或 `Proxy` 让数据可被追踪。
2. **依赖收集**：渲染时记录依赖，数据变了就通知视图更新。
3. **事件监听**：通过 `v-model` 的 `input` 事件，把用户输入同步回数据。

这样就形成了 **数据 ↔ 视图** 的双向绑定机制。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Git 提交信息规范学习笔记</title>
    <link>https://www.gfbzsblog.site/main/Articles/117</link>
    <comments>https://www.gfbzsblog.site/main/Articles/117#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Fri, 22 Aug 2025 20:09:26 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/117</guid>

    <description><![CDATA[这份笔记整理了 Git 提交信息的规范写法，包含常见类型说明和示例，帮助避免“update”这种模糊提交，让项目记录更清晰，方便自己和团队回顾与维护代码。]]></description>
    <content:encoded><![CDATA[在写项目的时候，提交信息往往被忽视。很多时候我只是随便写 `update`、`test`、`fix bug`，当时觉得能交差就好，但过几天回头看，就完全不知道自己到底改了什么。后来才发现 Git 提交信息其实也有一套规范，写清楚能带来很多好处：

- 方便自己回顾项目历史，快速定位问题
- 团队协作时能让别人一眼看懂改动内容
- 结合自动化工具（如 changelog 生成、版本管理）时更有用

所以我整理了一份适合学生、个人项目使用的 **简易版 Git 提交规范**，以后就按照这个来写。

## 提交信息格式

提交信息的基本格式如下：

```Summary
<type>(<scope>): <subject>
```

- **type**：提交类型，说明这次改动属于哪一类
- **scope**：作用范围，比如是某个页面、模块，写不写都行
- **subject**：一句话简洁描述改动内容，不超过 50 个字

> 例子：
>
> feat(Login): 新增记住密码功能
>
> fix(Article): 修复文章列表分页错误
>

## 常见类型

这里整理了一个常见的 **type 对照表**，不仅有前面提到的几种，还补充了一些团队里常用的类型，方便对照记忆。

| 类型         | 说明                                             | 示例                                |
| ------------ | ------------------------------------------------ | ----------------------------------- |
| **feat**     | 新功能（feature）                                | `feat(Comments): 添加评论回复功能`  |
| **fix**      | 修复 bug                                         | `fix(Navbar): 修复导航栏跳转错误`   |
| **docs**     | 文档修改                                         | `docs(README): 更新项目启动说明`    |
| **style**    | 代码样式修改，不影响逻辑（空格、缩进、格式化等） | `style(App): 调整代码缩进`          |
| **refactor** | 重构，优化代码但不影响功能                       | `refactor(utils): 简化工具函数`     |
| **test**     | 测试相关修改                                     | `test(api): 添加登录接口的单元测试` |
| **perf**     | 性能优化（performance）                          | `perf(List): 优化列表渲染性能`      |
| **build**    | 构建相关修改（webpack、vite、npm 等）            | `build: 升级依赖到最新版本`         |
| **ci**       | 持续集成相关配置                                 | `ci: 修改 GitHub Actions 测试脚本`  |
| **chore**    | 杂项，不影响代码逻辑（构建流程、依赖管理）       | `chore: 更新 eslint 配置`           |
| **revert**   | 回滚之前的提交                                   | `revert: 回滚 Login 模块的修改`     |

## 示例提交

### 新增功能

```Summary
feat(Home): 新增骨架屏组件
feat(Articles): 添加文章点赞功能
feat(Comments): 实现评论系统
```

### 修复问题

```Summary
fix(Articles): 修复文章 ID 类型错误
fix(Login): 解决登录页面样式问题
fix(Navbar): 修复导航栏显示 bug
```

### 样式调整

```Summary
style(global): 调整全局字体大小
style(Article): 优化文章页面布局
style(Button): 改进按钮样式
```

### 代码重构

```Summary
refactor(api): 重构 API 调用逻辑
refactor(components): 优化组件结构
refactor(utils): 简化工具函数
```

### 文档更新

```Summary
docs(README): 更新项目说明
docs(components): 添加组件文档
docs(api): 完善 API 文档
```

### 性能优化

```Summary
perf(List): 使用虚拟滚动提升渲染性能
perf(Image): 优化图片加载策略
```

### 构建 & 配置

```Summary
build: 升级 vite 到 v5.0
ci: 调整测试环境 Node.js 版本
chore: 更新依赖，修改 eslint 配置
```

### 回滚

```Summary
revert: 回滚 utils 模块的重构
```

## 编写规则总结

1. 提交信息用 **现在时态**（写“add”，不要写“added”）
2. **第一行简短**，控制在 50 字以内，重点说明改了什么
3. 不要写模糊词，比如 “update”、“change”，要具体
4. 有明确模块就写 scope，没有可以省略
5. 每次提交尽量保持单一目的（一个提交只做一件事）

## 我的实际记录示例

在自己的项目里，我会这样写提交信息：

```Summary
feat: 新增博客文章页面
feat: 添加文章目录导航
feat: 实现点赞功能
fix: 修复移动端显示问题
style: 调整文章字体样式
refactor: 优化组件结构
perf: 使用懒加载优化首页性能
docs: 更新 README 添加部署说明
chore: 升级依赖到最新版本
```

## 小结

Git 提交规范看似小事，但能大大提升代码可维护性。
 哪怕是自己写的小项目，坚持用规范写提交信息，过几个月回头再看也能马上回忆起当时做了什么，而不是面对一堆“update”一脸懵。

对我来说，这更像是一个好习惯的养成。以后写提交时，就按照上面的表格来，不仅让自己更清晰，也能让别人看懂。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Node.js 网络模块（http）详解</title>
    <link>https://www.gfbzsblog.site/main/Articles/115</link>
    <comments>https://www.gfbzsblog.site/main/Articles/115#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Tue, 19 Aug 2025 22:16:25 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/115</guid>

    <description><![CDATA[在 Node.js 中，http 模块是最核心的内置模块之一，它允许开发者快速搭建 HTTP 服务器或发起 HTTP 请求。
 无论是本地调试 API，还是在生产环境中支撑一个 Web 服务，几乎所有 Node.js 项目都会直接或间接依赖 http 模块。]]></description>
    <content:encoded><![CDATA[## 一、概述

在 Node.js 中，`http` 模块是最核心的内置模块之一，它允许开发者快速搭建 HTTP 服务器或发起 HTTP 请求。
 无论是本地调试 API，还是在生产环境中支撑一个 Web 服务，几乎所有 Node.js 项目都会直接或间接依赖 `http` 模块。

与浏览器的 `fetch` 或其他 HTTP 客户端不同，Node.js 的 `http` 模块提供了**更底层的接口**：

- 可以创建一个 **HTTP 服务器**，监听端口，接收并处理客户端请求；
- 可以作为 **HTTP 客户端**，向远程服务器发起请求；
- 提供对 **请求（IncomingMessage）** 和 **响应（ServerResponse）** 的底层控制权，例如分块传输、流式处理等。

由于其是底层 API，`http` 模块通常作为 Express、Koa、NestJS 等框架的基础，但理解它的工作原理仍然非常重要。

## 二、HTTP 服务器

### 2.1 创建服务器

使用 `http.createServer` 可以快速创建一个 HTTP 服务器：

```javascript
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, world!');
});
```

- **参数**：
   `createServer` 接收一个回调函数 `(req, res)`，当有新的请求进入时，这个回调就会被执行。
  - `req` 是 `http.IncomingMessage` 实例，表示请求对象。
  - `res` 是 `http.ServerResponse` 实例，表示响应对象。
- **返回值**：
   返回一个 `http.Server` 实例，它是一个事件驱动的对象，后续可以通过 `listen` 方法启动监听。

这种模式非常适合快速开发 Demo 或简单服务。但在复杂应用中，通常会把请求处理逻辑拆分出来，避免 `createServer` 回调函数里代码过于臃肿。

------

### 2.2 端口绑定与监听

创建服务器之后，还需要调用 `server.listen` 来让它真正运行起来。这个方法告诉操作系统：“请在某个端口等待请求，并交给我处理”。

常见的用法有三种：

1. **指定端口监听（最常见）**

   ```javascript
   server.listen(3000, () => {
     console.log('Server is running at http://localhost:3000');
   });
   ```

   - 这里 `3000` 是端口号。
   - 回调函数会在服务成功启动时触发。
   - 省略 `hostname` 参数时，服务器会监听所有可用的网络接口（包括 `localhost` 和外网 IP）。

   > 实际场景：在开发环境中，我们通常监听 `localhost:3000`；在生产环境，很多时候服务器只监听一个内部端口（如 `127.0.0.1:8080`），然后由 Nginx 或其他反向代理转发外部请求。

2. **监听本地回环地址**

   ```javascript
   server.listen(8080, '127.0.0.1');
   ```

   - 这样服务器只会接受来自本机的请求，不会对外暴露。
   - 适用于本地开发、测试，或者只需要被同一台机器上的其他服务调用的场景。

3. **通过 UNIX 域套接字监听**（Linux/Unix 环境）

   ```javascript
   server.listen('/tmp/server.sock');
   ```

   - 这里 `path` 是一个文件路径，表示使用 UNIX 域套接字进行通信。
   - 适用于高性能 IPC（进程间通信）或与 Nginx 的本地 socket 转发结合使用。
   - 注意：需要保证路径有权限写入，否则会抛出错误。

## 三、请求对象（IncomingMessage）

当客户端发起请求时，`http.createServer` 回调中的第一个参数 `req` 就是一个 `http.IncomingMessage` 实例，它封装了请求的所有信息，包括请求方法、URL、头信息以及请求体。

### 3.1 请求方法与 URL

`req.method` 和 `req.url` 是最基础的两个属性：

```javascript
const http = require('http');

http.createServer((req, res) => {
  console.log('Request Method:', req.method);
  console.log('Request URL:', req.url);
  res.end('Check console for request details');
}).listen(3000);
```

- **req.method**：表示 HTTP 方法，如 `GET`、`POST`、`PUT`、`DELETE` 等。

- **req.url**：表示请求的路径，包括查询参数（query string）。

  > 注意：它并不是完整的 URL，通常会结合 `URL` 类或第三方库（如 `querystring`、`url`）解析参数。

**应用场景**：

- 根据 `req.method` 做不同路由处理，例如 `GET /users` 返回用户列表，`POST /users` 创建新用户。
- 使用 `req.url` 解析路径和查询参数，实现路由匹配。

------

### 3.2 请求头（Headers）

`req.headers` 是一个对象，包含客户端发送的所有 HTTP 头信息：

```javascript
console.log(req.headers['user-agent']); // 浏览器信息
console.log(req.headers['content-type']); // 请求内容类型
```

- **注意事项**：HTTP 头的键名总是小写，无论客户端怎么发送都统一小写。
- **常用头**：
  - `content-type`：指示请求体类型，如 `application/json`、`application/x-www-form-urlencoded`。
  - `authorization`：常用于携带 JWT 或 Token。
  - `cookie`：客户端 Cookie 信息。

**应用场景**：

- 判断请求类型决定如何解析请求体；
- 提取鉴权信息进行身份验证。

------

### 3.3 请求体（Body）处理

对于 `POST`、`PUT` 等方法，请求体可能包含数据。Node.js HTTP 模块将请求体作为 **流（Stream）** 处理，而不是一次性读取。

```javascript
let body = '';
req.on('data', chunk => {
  body += chunk; // 逐块累加数据
});
req.on('end', () => {
  console.log('Received body:', body);
  res.end('Request processed');
});
```

- **流式处理**的优势：
  - 支持大文件上传，不会一次性占用大量内存；
  - 可以在接收数据时就进行处理（如实时写入数据库或文件）。
- **注意事项**：
  - `data` 事件可能触发多次，每次接收到的是 Buffer，需要根据 `content-type` 转换；
  - `end` 事件表示整个请求体已接收完毕。

**实践建议**：

- 对于 JSON 数据，可在 `end` 中调用 `JSON.parse`；
- 对于文件上传，通常结合第三方库（如 `multer`、`busboy`）处理 multipart/form-data。

------

### 3.4 高级属性

- `req.httpVersion`：请求使用的 HTTP 版本（如 `1.1`、`2.0`）。
- `req.socket`：底层 TCP 套接字，可以获取客户端 IP、端口等。
- `req.rawHeaders`：原始头信息数组，保持客户端发送顺序。

**应用场景**：

- 日志记录客户端 IP 或请求来源；
- 在代理或负载均衡场景下，分析原始请求头。

## 四、响应对象（ServerResponse）

`http.createServer` 回调函数的第二个参数 `res` 是一个 `http.ServerResponse` 实例，用于构建并发送 HTTP 响应。理解 `res` 的工作方式，对于正确返回数据、设置状态码和响应头至关重要。

### 4.1 设置状态码

HTTP 状态码是服务器告诉客户端请求结果的标准方式。Node.js 提供了多种方式设置状态码：

```javascript
res.statusCode = 200; // 直接赋值
res.statusMessage = 'OK'; // 可选，自定义状态消息
res.end('Response body');
```

- **statusCode**：必须设置，默认为 200（成功）。
- **statusMessage**：可选，如果不设置，Node.js 会根据 `statusCode` 自动匹配标准文本。

**常用状态码示例**：

- 200 OK：请求成功
- 301 Moved Permanently：永久重定向
- 400 Bad Request：客户端请求错误
- 404 Not Found：请求资源不存在
- 500 Internal Server Error：服务器内部错误

**场景**：

- 根据业务逻辑返回不同状态码，例如验证失败返回 401，资源未找到返回 404。
- 与前端框架结合时，状态码可以用于条件渲染或错误提示。

------

### 4.2 设置响应头

响应头用于描述响应的元信息，如内容类型、缓存策略、CORS 等。可以使用 `setHeader` 方法：

```javascript
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-cache');
res.end(JSON.stringify({ message: 'Hello World' }));
```

- **注意事项**：
  - 必须在调用 `res.write()` 或 `res.end()` 前设置；
  - 同一个头字段调用多次会覆盖之前的值，可使用 `res.getHeader()` 检查。

**常用响应头**：

- `Content-Type`：指定响应体类型，如 `text/plain`、`application/json`、`text/html`。
- `Content-Length`：可选，告知客户端响应体字节长度；不设置 Node.js 会自动计算。
- `Set-Cookie`：用于发送 Cookie 给客户端。
- `Access-Control-Allow-Origin`：处理跨域请求。

------

### 4.3 发送响应体

最常见的发送方式是 `res.end()`，它既可以设置响应体，也会结束响应：

```javascript
res.end('Hello World'); // 发送文本
res.end(JSON.stringify({ ok: true })); // 发送 JSON
```

- 如果响应体较大，或需要分块发送，可以使用 `res.write()` + `res.end()`：

```javascript
res.write('Part 1\n');
res.write('Part 2\n');
res.end('End');
```

- **应用场景**：
  - 流式处理大文件或长轮询；
  - 分块传输 HTML 模板或日志数据；
  - 与 `fs.createReadStream()` 配合，实现文件下载。

------

### 4.4 响应流（Stream）与管道

由于 `res` 继承自 `Writable Stream`，可以直接将文件流或其他可读流管道输出：

```javascript
import fs from 'fs';
const stream = fs.createReadStream('./large-file.txt');
stream.pipe(res);
```

- **优势**：
  - 不需要一次性将整个文件读入内存，节省内存开销；
  - 对大文件传输和实时数据推送非常高效。

**注意事项**：

- 管道会自动处理 `res.end()`，无需手动调用；
- 出现错误时要监听 `error` 事件，否则可能导致服务器崩溃。

------

### 4.5 重定向

通过设置状态码和 `Location` 响应头可以实现 HTTP 重定向：

```javascript
res.statusCode = 301;
res.setHeader('Location', 'https://example.com');
res.end();
```

- **301**：永久重定向
- **302**：临时重定向
- **应用场景**：
  - URL 变更后保持 SEO；
  - 登录验证后跳转到目标页面。

## 五、HTTP 客户端请求

Node.js 的 `http` 模块不仅可以创建服务器，还可以向远程服务器发起 HTTP 请求。这对于实现 API 调用、微服务通信或数据抓取非常重要。核心方法是 `http.request` 和 `http.get`。

### 5.1 http.request

`http.request` 是最底层、功能最强大的客户端方法，用于发起任意 HTTP 请求（GET、POST、PUT 等）。

```javascript
import http from 'http';

const options = {
  hostname: 'example.com',
  port: 80,
  path: '/api/data',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  }
};

const req = http.request(options, res => {
  let data = '';
  res.on('data', chunk => { data += chunk; });
  res.on('end', () => { console.log('Response:', data); });
});

req.on('error', err => {
  console.error('Request error:', err);
});

req.write(JSON.stringify({ name: 'Node.js' }));
req.end();
```

- **options 解释**：
  - `hostname`：远程服务器域名或 IP 地址
  - `port`：端口，HTTP 默认 80，HTTPS 默认 443
  - `path`：请求路径，可包含查询参数
  - `method`：请求方法，如 `GET`、`POST`
  - `headers`：自定义请求头
- **req.write() / req.end()**：
  - 对于带请求体的请求（如 POST），需要调用 `req.write()` 写入数据，再用 `req.end()` 结束请求；
  - 对于 GET 请求，通常直接调用 `req.end()` 即可。

**应用场景**：

- 调用第三方 API；
- 服务间数据交互；
- 上传数据到远程服务器。

**注意事项**：

- 必须监听 `error` 事件，否则出现网络错误会导致进程崩溃；
- 对于 HTTPS 请求，应使用 `https` 模块（Node.js 内置）。

------

### 5.2 http.get

`http.get` 是 `http.request` 的简化版本，专门用于发起 GET 请求，内部已经自动调用了 `req.end()`。

```javascript
http.get('http://example.com/api/data', res => {
  let data = '';
  res.on('data', chunk => { data += chunk; });
  res.on('end', () => { console.log('GET Response:', data); });
}).on('error', err => {
  console.error('GET request error:', err);
});
```

- **优点**：
  - 简化了 GET 请求流程，无需手动调用 `req.end()`
  - 更适合快速抓取数据或读取简单 API
- **注意事项**：
  - 如果需要发送请求体或自定义方法（POST、PUT 等），仍然需要使用 `http.request`；
  - 需要处理数据流，否则响应体过大可能导致内存问题。

------

### 5.3 响应事件

客户端请求返回的数据是一个 **流（IncomingMessage）**，支持标准事件：

- `data`：接收响应数据块
- `end`：响应接收完毕
- `error`：网络或协议错误
- `aborted`：请求被中止

**示例：使用事件组合处理响应**

```javascript
req.on('response', res => {
  console.log('Status:', res.statusCode);
  res.on('data', chunk => process.stdout.write(chunk));
  res.on('end', () => console.log('\nRequest finished'));
});
```

**应用场景**：

- 流式读取大文件或图片数据；
- 实时处理数据流，如日志收集或 SSE（Server-Sent Events）；
- 捕获错误、超时等异常情况，提高客户端健壮性。

------

### 5.4 超时与错误处理

HTTP 客户端请求可能遇到网络延迟或远程服务器异常，因此需要设置超时：

```javascript
const req = http.request(options, res => { /* ... */ });
req.setTimeout(5000, () => { 
  console.error('Request timed out'); 
  req.abort(); 
});
req.on('error', err => console.error('Request error:', err));
req.end();
```

- **setTimeout**：指定毫秒数超时
- **req.abort()**：超时或需要中断请求时调用
- **错误监听**：防止未捕获异常导致 Node.js 进程退出

**实践建议**：

- 为所有客户端请求设置合理超时；
- 使用 `try/catch` 或错误事件捕获，确保服务稳定；
- 对高并发请求，可结合连接池或 `keep-alive` 提升效率。

## 六、HTTPS 与安全通信

Node.js 提供了 `https` 模块，它与 `http` 模块接口类似，但支持 **TLS/SSL** 加密通信，用于保护数据在网络传输中的安全性。HTTPS 是现代 Web 的标准通信方式，尤其在涉及用户信息、支付或敏感数据时必不可少。

### 6.1 创建 HTTPS 服务器

HTTPS 服务器需要 **证书（cert）和私钥（key）**。创建方式与 HTTP 类似，但需要引入 `https` 模块并提供安全配置：

```javascript
import https from 'https';
import fs from 'fs';

const options = {
  key: fs.readFileSync('./private-key.pem'),
  cert: fs.readFileSync('./certificate.pem')
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, Secure World!');
});

server.listen(443, () => {
  console.log('HTTPS server running at https://localhost');
});
```

- **options.key**：服务器私钥，用于解密客户端发送的加密信息
- **options.cert**：公钥证书，用于向客户端证明服务器身份
- **场景**：
  - 任何涉及敏感信息的服务，如用户登录、支付接口；
  - 强制全站 HTTPS，提高安全性和 SEO 优势

**注意事项**：

- 在生产环境中通常使用由受信任 CA 签发的证书，而不是自签名证书；
- 可以结合 Nginx 或负载均衡器做证书管理，Node.js 服务器仅监听内部端口。

------

### 6.2 HTTPS 客户端请求

HTTPS 客户端请求与 HTTP 类似，但使用 `https.request` 或 `https.get`：

```javascript
import https from 'https';

https.get('https://api.example.com/data', res => {
  let data = '';
  res.on('data', chunk => { data += chunk; });
  res.on('end', () => { console.log('Response:', data); });
}).on('error', err => {
  console.error('HTTPS request error:', err);
});
```

- **优点**：
  - 数据在传输过程中加密，防止被窃听或篡改
  - 支持证书验证，保障服务器身份真实性
- **场景**：
  - 调用安全 API，如支付网关、第三方登录服务；
  - 爬取 HTTPS 网站的数据

------

## 七、HTTP/2 支持

HTTP/2 是 HTTP 协议的升级版，引入了多路复用、头压缩和服务器推送，显著提高性能。Node.js 提供 `http2` 模块用于创建 HTTP/2 服务器和客户端。

### 7.1 HTTP/2 服务器

```javascript
import http2 from 'http2';
import fs from 'fs';

const server = http2.createSecureServer({
  key: fs.readFileSync('./private-key.pem'),
  cert: fs.readFileSync('./certificate.pem')
});

server.on('stream', (stream, headers) => {
  stream.respond({ ':status': 200 });
  stream.end('Hello HTTP/2');
});

server.listen(8443, () => {
  console.log('HTTP/2 server running at https://localhost:8443');
});
```

- **核心特点**：
  - `stream` 事件替代传统 `request` 回调，每个请求对应一个流对象
  - 可以实现**多路复用**，一个 TCP 连接上同时传输多个请求/响应
  - 支持**服务器推送**，主动向客户端发送资源，减少延迟

**场景**：

- 高并发 Web 服务，提高性能和资源加载速度；
- 结合 HTTPS 使用，提升安全性和性能。

------

### 7.2 HTTP/2 客户端请求

```javascript
import http2 from 'http2';

const client = http2.connect('https://localhost:8443');
const req = client.request({ ':path': '/' });

let data = '';
req.on('data', chunk => { data += chunk; });
req.on('end', () => {
  console.log('HTTP/2 Response:', data);
  client.close();
});
req.end();
```

- **特点**：
  - 使用 `http2.connect` 建立客户端连接，复用 TCP 连接
  - 请求通过 `client.request()` 发起，响应数据通过流式事件接收
- **注意事项**：
  - 必须使用 TLS 加密连接，否则只能使用 HTTP/2 的明文版本（h2c），不常见
  - 适合高性能微服务或前后端通信

## 八、HTTP 实用技巧与最佳实践

在实际 Node.js 开发中，仅仅掌握 HTTP API 的基本用法还不够。高性能、稳定、安全的服务器需要注意连接管理、请求体处理、响应优化和错误容忍。

### 8.1 连接复用（Keep-Alive）

HTTP 默认每次请求都会创建新的 TCP 连接，这会带来性能开销。使用 **Keep-Alive** 可以在同一连接上复用多个请求，提高吞吐量：

```javascript
const http = require('http');

const options = {
  hostname: 'example.com',
  port: 80,
  path: '/api/data',
  method: 'GET',
  headers: {
    'Connection': 'keep-alive'
  }
};

const req = http.request(options, res => {
  console.log('Status:', res.statusCode);
});
req.end();
```

- **优点**：
  - 减少 TCP 握手次数，降低延迟
  - 更适合频繁访问同一服务的微服务或前端 API 调用
- **注意事项**：
  - Keep-Alive 连接可能被防火墙或代理中断，需要处理异常
  - 对于长时间未使用的连接，可设置服务器或客户端超时策略

------

### 8.2 超时与重试策略

网络环境复杂，请求可能超时或失败，需要合理设置超时与重试机制：

```javascript
req.setTimeout(5000, () => { 
  console.error('Request timed out'); 
  req.abort(); 
});
req.on('error', err => console.error('Request failed:', err));
```

- **应用场景**：
  - 生产环境 API 调用，确保异常不会导致进程崩溃
  - 对高可用服务，结合重试策略减少请求失败率

**最佳实践**：

- 对客户端请求设置合理超时（通常 3~10 秒）；
- 对重要业务请求，可增加指数退避（exponential backoff）重试机制；
- 错误日志记录详细信息，便于定位问题。

------

### 8.3 大文件流处理

处理大文件时，避免一次性读取或写入内存，可使用流（Stream）方式：

```javascript
import fs from 'fs';
import http from 'http';

http.createServer((req, res) => {
  const readStream = fs.createReadStream('./large-file.zip');
  readStream.pipe(res); // 流式传输，自动处理 backpressure
}).listen(3000);
```

- **优势**：
  - 节约内存，支持任意大小文件传输
  - 实时传输，提高响应速度
- **注意事项**：
  - 流中可能出现错误，需要监听 `error` 事件
  - 对于压缩或加密文件，可能需要在流中处理变换（Transform Stream）

------

### 8.4 响应压缩

为了减小传输数据量，提高页面或 API 响应速度，可使用 gzip 或 Brotli 压缩：

```javascript
import http from 'http';
import zlib from 'zlib';

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Encoding': 'gzip' });
  const gzip = zlib.createGzip();
  gzip.pipe(res);
  gzip.end('Hello compressed world!');
}).listen(3000);
```

- **应用场景**：
  - 返回大量文本、JSON 或 HTML 内容
  - 提升 Web 性能，减少带宽占用
- **注意事项**：
  - 二进制文件无需压缩，可能反而增大
  - 可以结合中间件（如 Express 的 `compression`）自动处理

------

### 8.5 CORS 与安全策略

对于跨域请求，需要设置响应头来允许访问：

```javascript
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
```

- **应用场景**：
  - 前后端分离项目
  - 微服务架构中不同域名间的 API 调用
- **安全建议**：
  - 尽量限定允许的域名，不要使用 `*` 作为生产策略
  - 配合身份验证、Token 或 Cookie 保证安全

------

### 8.6 日志与监控

HTTP 服务通常需要记录访问日志和监控状态：

```javascript
http.createServer((req, res) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  res.end('Logged');
}).listen(3000);
```

- **重要性**：
  - 方便调试和定位问题
  - 用于统计流量、监控性能
- **推荐做法**：
  - 结合第三方库（如 `morgan`、`pino`）实现结构化日志
  - 生产环境可接入监控系统，如 Prometheus、Grafana

## 九、HTTP 模块方法总结

| 操作                 | 方法                                                         | 参数 & 说明                                                  | 使用场景                                         |
| -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------ |
| **创建 HTTP 服务器** | `http.createServer([options], requestListener)`              | `requestListener` 回调函数接收 `(req, res)`，`options` 可配置 `IncomingMessage` 和 `ServerResponse` 行为 | 快速搭建 HTTP 服务，处理请求和响应               |
| **监听端口**         | `server.listen(port[, hostname][, backlog][, callback])`   `server.listen(path[, callback])` | `port`：监听端口   `hostname`：绑定地址   `backlog`：连接队列长度   UNIX 域套接字可使用 path | 开启服务器监听，支持端口或 Unix Socket           |
| **关闭服务器**       | `server.close([callback])`                                   | 停止接收新请求，回调在关闭完成后执行                         | 用于优雅退出服务或单元测试                       |
| **HTTP 请求**        | `http.request(options[, callback])`                          | `options`：hostname, port, path, method, headers   返回 `ClientRequest` | 发起 GET/POST/PUT 等请求，支持请求体和自定义方法 |
| **简化 GET 请求**    | `http.get(options[, callback])`                              | 内部自动调用 `req.end()`，用于 GET 请求                      | 快速抓取数据或访问 API                           |
| **请求写入**         | `req.write(chunk[, encoding])`                               | 向请求体写入数据，常用于 POST                                | 发送请求体数据，如 JSON 或表单                   |
| **结束请求**         | `req.end([data[, encoding]])`                                | 结束请求，发送最后的数据                                     | 必须调用结束请求，确保服务器接收完整             |
| **请求事件**         | `req.on('response', callback)`   `req.on('error', callback)` | `response`：获取响应对象   `error`：捕获请求异常             | 流式处理响应和错误处理                           |
| **响应状态码**       | `res.statusCode` / `res.statusMessage`                       | 设置 HTTP 状态码和状态消息                                   | 控制请求结果，如 200、404、500                   |
| **设置响应头**       | `res.setHeader(name, value)`                                 | 在 `res.write()` 或 `res.end()` 前设置                       | 设置 Content-Type、Cache-Control、CORS 等        |
| **发送响应体**       | `res.write(chunk)` + `res.end([chunk])`                      | 分块发送响应，可组合流式输出                                 | 大文件传输或分块数据处理                         |
| **管道输出**         | `readableStream.pipe(res)`                                   | 将可读流直接发送给客户端                                     | 文件下载、实时数据流输出                         |
| **重定向**           | `res.statusCode = 301/302` + `res.setHeader('Location', url')` | 设置重定向状态码和 Location                                  | URL 重写、登录跳转、资源搬迁                     |
| **HTTPS 服务器**     | `https.createServer(options, requestListener)`               | `options`：key, cert, ca 等安全配置                          | 安全通信，保护敏感数据                           |
| **HTTP/2 服务器**    | `http2.createSecureServer(options)`                          | `options`：key, cert 等                                      | 支持多路复用、服务器推送，提高性能               |
| **HTTP/2 客户端**    | `http2.connect(authority)` + `client.request(headers)`       | 建立连接并发起请求，返回可读流                               | 高性能微服务间通信                               |
| **超时控制**         | `req.setTimeout(ms, callback)`                               | 超时触发回调并可调用 `req.abort()`                           | 避免长时间阻塞，提高服务健壮性                   |
| **监听请求事件**     | `res.on('data', callback)`   `res.on('end', callback)`   `res.on('error', callback)` | 流式处理响应数据                                             | 处理大文件、实时数据、错误捕获                   |
| **响应压缩**         | `zlib.createGzip()` / `zlib.createBrotliCompress()`          | 与流管道结合发送压缩内容                                     | 提升网络传输效率，减少带宽占用                   |

------

### 总结说明

- **服务端**：`http.createServer` + `server.listen` 是基础搭建方式；`res` 提供状态码、响应头、响应体、流式输出和重定向。
- **客户端**：`http.request` / `http.get` 支持自定义请求方法、请求体、事件处理和错误捕获；HTTPS 与 HTTP/2 提供加密和高性能特性。
- **高级实践**：连接复用、超时控制、大文件流、响应压缩、CORS、安全配置、日志监控是生产环境必备技巧。

## 十、HTTP 模块常见场景实战示例合集

以下示例展示了 Node.js HTTP 模块在实际开发中的典型用法，从服务器搭建、API 调用到大文件传输与压缩响应。

------

### 10.1 创建基础 HTTP 服务器

```javascript
import http from 'http';

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, Node.js HTTP!');
});

server.listen(3000, '127.0.0.1', () => {
  console.log('Server listening on http://127.0.0.1:3000');
});
```

**说明**：

- 适用于快速搭建本地测试服务
- 通过 `writeHead` 设置响应头，`end` 发送响应体

------

### 10.2 HTTP 客户端请求调用 API

```javascript
import http from 'http';

const options = {
  hostname: 'jsonplaceholder.typicode.com',
  port: 80,
  path: '/todos/1',
  method: 'GET'
};

const req = http.request(options, res => {
  let data = '';
  res.on('data', chunk => { data += chunk; });
  res.on('end', () => { console.log('API Response:', data); });
});

req.on('error', err => console.error('Request error:', err));
req.end();
```

**说明**：

- 发起 GET 请求获取 JSON 数据
- 流式处理响应，避免内存占用过大

------

### 10.3 HTTPS 服务器示例

```javascript
import https from 'https';
import fs from 'fs';

const options = {
  key: fs.readFileSync('./private-key.pem'),
  cert: fs.readFileSync('./certificate.pem')
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, Secure HTTPS!');
});

server.listen(443, () => {
  console.log('HTTPS server running at https://localhost');
});
```

**说明**：

- 用于保护敏感数据传输
- 需要正确配置证书和私钥
- 常用于生产环境与支付、登录等安全场景

------

### 10.4 HTTP/2 服务器与多路复用

```javascript
import http2 from 'http2';
import fs from 'fs';

const server = http2.createSecureServer({
  key: fs.readFileSync('./private-key.pem'),
  cert: fs.readFileSync('./certificate.pem')
});

server.on('stream', (stream, headers) => {
  stream.respond({ ':status': 200 });
  stream.end('Hello HTTP/2, multiple streams!');
});

server.listen(8443, () => {
  console.log('HTTP/2 server running at https://localhost:8443');
});
```

**说明**：

- 每个请求对应一个流 (`stream`)
- 支持多路复用，提升高并发性能
- 支持服务器推送，可主动发送资源

------

### 10.5 大文件流传输示例

```javascript
import http from 'http';
import fs from 'fs';

http.createServer((req, res) => {
  const fileStream = fs.createReadStream('./large-file.zip');
  res.writeHead(200, { 'Content-Type': 'application/zip' });
  fileStream.pipe(res);
  fileStream.on('error', err => {
    console.error('File read error:', err);
    res.statusCode = 500;
    res.end('Internal Server Error');
  });
}).listen(3000, () => {
  console.log('File streaming server running on port 3000');
});
```

**说明**：

- 避免一次性加载整个文件到内存
- 支持大文件下载或流式传输

------

### 10.6 响应压缩示例

```javascript
import http from 'http';
import zlib from 'zlib';

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Encoding': 'gzip' });
  const gzip = zlib.createGzip();
  gzip.pipe(res);
  gzip.end('This is compressed response using gzip.');
}).listen(3000, () => {
  console.log('Compressed server running on port 3000');
});
```

**说明**：

- 对文本或 JSON 响应进行压缩，提高传输效率
- 可结合流处理，实现大文件压缩传输

------

### 10.7 API 代理 / 跨域示例（CORS）

```javascript
import http from 'http';

http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'CORS enabled API' }));
}).listen(3000, () => {
  console.log('CORS API server running on port 3000');
});
```

**说明**：

- 支持前后端分离项目跨域访问
- 生产环境应限制允许域名，确保安全

------

### 10.8 日志与监控示例

```javascript
import http from 'http';

http.createServer((req, res) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Logged request!');
}).listen(3000, () => {
  console.log('Server with logging running on port 3000');
});
```

**说明**：

- 记录访问时间、方法和路径
- 结合日志库可实现结构化日志与监控
- 是生产环境常用的运维手段

## 结语

Node.js 的 `http` 模块是构建服务端应用的核心基础，它提供了完整的服务器与客户端 API，使开发者能够灵活地创建 HTTP/HTTPS 以及 HTTP/2 服务。通过掌握 `createServer`、`request`、流式处理、事件监听等机制，可以处理大规模并发请求、传输大文件、实现压缩响应和跨域访问。结合生产环境实践，如 Keep-Alive 连接复用、超时与重试策略、日志监控和安全配置，开发者能够构建高性能、可靠且安全的网络服务。掌握该模块不仅有助于理解 Node.js 异步 I/O 与事件驱动模型，也为进一步使用 Express、Koa 等框架打下坚实基础，使整个服务开发流程从请求接收、数据处理到响应发送都能高效、可控。

如果想要了解更多Node.js，可以点击下方链接⬇️
```meta
title: Node.js
url: https://github.com/nodejs/examples
image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png
desc: Node.js 官方示例
```]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title> Node.js 文件系统模块（fs）详解</title>
    <link>https://www.gfbzsblog.site/main/Articles/113</link>
    <comments>https://www.gfbzsblog.site/main/Articles/113#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Tue, 19 Aug 2025 19:50:13 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/113</guid>

    <description><![CDATA[在 Node.js 应用开发中，fs 模块是使用频率极高的核心模块之一，它提供了访问操作系统文件系统的 API。无论是 Web 服务端加载配置文件、日志写入，还是命令行工具扫描目录，几乎都会用到 fs。]]></description>
    <content:encoded><![CDATA[## 概述

在 Node.js 应用开发中，`fs` 模块是使用频率极高的核心模块之一，它提供了访问操作系统文件系统的 API。无论是 Web 服务端加载配置文件、日志写入，还是命令行工具扫描目录，几乎都会用到 `fs`。

与大多数异步 API 一样，Node.js 文件操作有三种调用形式：

- **异步回调（Callback）**：早期 Node.js 的默认风格，适合简单任务，但复杂逻辑会导致“回调地狱”。
- **同步（Sync）**：以 `Sync` 结尾的方法会阻塞事件循环，一般只在应用初始化阶段使用。
- **Promise**：`fs/promises` 提供了现代异步接口，推荐在生产环境中使用 `async/await`，提高可读性。

在使用 `fs` 时，路径拼接也非常重要，跨平台时推荐使用 `path` 模块，而不是手写斜杠（`/` 或 `\`）。

## 1. 文件操作

文件操作是 `fs` 模块最常见的使用场景，包括读取、写入、追加、复制和删除等。

### 1.1 读取文件（Read）

读取文件是开发中最基本的需求之一：

- 读取配置文件（如 `config.json`）；
- 加载模板文件；
- 读取日志内容；
- 处理图片、视频等二进制文件。

Node.js 提供三种方式：

- `fs.readFile(path[, options], callback)`
- `fs.readFileSync(path[, options])`
- `fs.promises.readFile(path[, options])`

常见参数：

- `encoding`: 文本文件常用 `'utf8'`，二进制文件保持 `null`，返回 Buffer。
- `flag`: 文件打开方式，默认 `'r'`（只读）。

**注意事项**：

- 对于大文件，`readFile` 会一次性读入内存，可能造成内存压力。推荐使用 **流（stream）** 来处理大文件。
- 异步方式优先于同步方式，除非是在初始化时必须阻塞。

**示例**

```js
import { readFile } from 'fs/promises';

async function readFileExample() {
  try {
    const data = await readFile('./file.txt', { encoding: 'utf8' });
    console.log(data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}
readFileExample();
```

### 1.2 写入文件（Write）

写入文件常用于生成日志、缓存、导出报表等。

- `fs.writeFile(file, data[, options], callback)`
- `fs.writeFileSync(file, data[, options])`
- `fs.promises.writeFile(file, data[, options])`

常见选项：

- `encoding`: 默认 `'utf8'`。
- `mode`: 文件权限，默认 `0o666`（可读写）。
- `flag`: 默认 `'w'`，覆盖写入。

**注意事项**：

- 如果文件不存在，会自动创建。
- 覆盖写入可能导致数据丢失，日志场景建议使用 `appendFile`。
- 可使用 `flag: 'wx'` 保证只有文件不存在时才写入。

### 1.3 追加内容（Append）

日志写入、记录用户行为时，通常选择追加写：

- `fs.appendFile(path, data[, options], callback)`
- `fs.appendFileSync(path, data[, options])`
- `fs.promises.appendFile(path[, options])`

**注意事项**：

- 内部默认 `flag: 'a'`，即在文件末尾追加。
- 高并发日志写入时，建议使用日志库（如 `winston`）封装。

### 1.4 打开文件（Open）与文件描述符操作

`fs.open` 提供对文件的低层级控制，可以获取文件描述符 `fd`，再通过 `fs.read`、`fs.write`、`fs.close` 等操作。

适合的场景包括：

- 只读取或修改文件的某一部分；
- 实现大文件分块处理；
- 构建数据库式的二进制存储格式。

**注意事项**：

- 打开文件后，必须记得调用 `fs.close` 关闭，否则可能导致文件句柄泄漏。
- 现代 Node.js 开发中，除非需要精细控制，否则一般直接用 `readFile` 和 `writeFile` 即可。

### 1.5 复制文件（Copy）

文件复制是常见的文件管理操作。

- `fs.copyFile(src, dest[, mode], callback)`
- `fs.promises.copyFile(src, dest[, mode])`

**注意事项**：

- 默认会覆盖目标文件。
- 如果希望在目标文件存在时报错，可以传 `fs.constants.COPYFILE_EXCL`。

### 1.6 删除文件（Unlink）

删除文件使用 `unlink`：

- `fs.unlink(path, callback)`
- `fs.promises.unlink(path)`

注意不要误用在目录上（删除目录请使用 `rmdir` 或 `rm`）。

## 2. 目录操作

Node.js 也提供了目录相关的操作 API，包括创建、读取和删除。

### 2.1 创建目录（Mkdir）

- `fs.mkdir(path[, options], callback)`
- `fs.promises.mkdir(path[, options])`

常见场景：

- 创建临时缓存目录；
- 搭建项目时批量生成目录结构。

常见选项：

- `recursive`: 递归创建目录。
- `mode`: 目录权限，默认 `0o777`。

### 2.2 读取目录（Readdir）

- `fs.readdir(path[, options], callback)`
- `fs.promises.readdir(path[, options])`

选项：

- `withFileTypes: true` 返回 `Dirent[]`，可以判断每一项是文件还是目录。

常见用途：

- 遍历目录下的所有文件；
- 实现批量文件处理工具。

### 2.3 删除目录（Rmdir / Rm）

- `fs.rmdir` 仅支持删除空目录；
- `fs.rm`（Node.js v14.14+）支持递归删除目录及内容。

选项：

- `recursive`: 删除非空目录。
- `force`: 忽略不存在路径的报错。

## 3. 文件与目录信息

### 3.1 文件状态（Stat）

- `fs.stat(path[, options], callback)`
- `fs.promises.stat(path[, options])`

返回 `Stats` 对象，可以获取：

- `stats.isFile()`
- `stats.isDirectory()`
- `stats.size`

应用场景：

- 区分目录与文件；
- 检查文件大小（如日志轮转）。

### 3.2 检查权限（Access）

- `fs.access(path[, mode], callback)`
- `fs.promises.access(path[, mode])`

常见检查：

- `F_OK`: 是否存在
- `R_OK`: 是否可读
- `W_OK`: 是否可写

**最佳实践**：
 代替已废弃的 `fs.exists`。

## 4. 高级操作

### 4.1 监听文件/目录（Watch）

- `fs.watch(filename[, options][, listener])`

应用场景：

- 热加载配置文件；
- 实现文件同步工具。

注意事项：

- 行为在不同操作系统上可能不一致；
- 建议生产环境使用 `chokidar`。

### 4.2 重命名/移动

- `fs.rename(oldPath, newPath, callback)`
- `fs.promises.rename(oldPath, newPath)`

可以用来重命名文件，或者移动到新目录。

### 4.3 链接

- `fs.link`: 硬链接
- `fs.symlink`: 符号链接

## 常用 API 总览表

| 操作     | 异步回调       | 同步             | Promise      | 说明          |
| -------- | -------------- | ---------------- | ------------ | ------------- |
| 读文件   | `readFile`     | `readFileSync`   | `readFile`   | 读取整个文件  |
| 写文件   | `writeFile`    | `writeFileSync`  | `writeFile`  | 覆盖写        |
| 追加写   | `appendFile`   | `appendFileSync` | `appendFile` | 末尾追加      |
| 删文件   | `unlink`       | `unlinkSync`     | `unlink`     | 删除文件      |
| 复制文件 | `copyFile`     | `copyFileSync`   | `copyFile`   | 复制          |
| 创建目录 | `mkdir`        | `mkdirSync`      | `mkdir`      | 可递归        |
| 读目录   | `readdir`      | `readdirSync`    | `readdir`    | 列出内容      |
| 删目录   | `rmdir` / `rm` | 同左             | 同左         | 递归删除      |
| 获取状态 | `stat`         | `statSync`       | `stat`       | 文件/目录信息 |
| 检查权限 | `access`       | `accessSync`     | `access`     | 权限检查      |
| 重命名   | `rename`       | `renameSync`     | `rename`     | 重命名/移动   |
| 监听     | `watch`        | N/A              | N/A          | 文件变化监控  |

## 最佳实践与总结

- **优先使用 Promise API**，配合 `async/await`。
- **避免使用 Sync API**，除非在初始化阶段。
- **路径操作必须用 `path` 模块**，保证跨平台安全性。
- **大文件处理推荐用流（Stream）**，避免内存溢出。
- **监听文件变化**时，用 `chokidar` 等库替代 `fs.watch`。
- **注意资源释放**：打开的文件记得关闭，防止文件句柄泄漏。

如果想要了解更多Node.js，可以点击下方链接⬇️
```meta
title: Node.js
url: https://github.com/nodejs/examples
image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png
desc: Node.js 官方示例
```
]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>Ajax、Axios 与 Fetch 的比较</title>
    <link>https://www.gfbzsblog.site/main/Articles/112</link>
    <comments>https://www.gfbzsblog.site/main/Articles/112#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Mon, 18 Aug 2025 21:51:37 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/112</guid>

    <description><![CDATA[在前端开发中，客户端与服务器之间的数据交互是必不可少的。无论是传统的页面刷新，还是现代的单页应用（SPA），都依赖异步请求来完成数据的获取与提交。常见的实现方式主要包括三种：基于 `XMLHttpRequest` 的 Ajax、第三方库 Axios 以及原生的 Fetch API。它们都能完成类似的任务，但在使用体验、功能特性和适用场景上却有明显差异。]]></description>
    <content:encoded><![CDATA[在前端开发中，客户端与服务器之间的数据交互是必不可少的。无论是传统的页面刷新，还是现代的单页应用（SPA），都依赖异步请求来完成数据的获取与提交。常见的实现方式主要包括三种：基于 `XMLHttpRequest` 的 Ajax、第三方库 Axios 以及原生的 Fetch API。它们都能完成类似的任务，但在使用体验、功能特性和适用场景上却有明显差异。

## 一、Ajax

Ajax 的全称是 *Asynchronous JavaScript and XML*，它并不是某种具体的技术，而是通过 `XMLHttpRequest` 对象来实现异步数据请求的一种方式。Ajax 的最大贡献在于推动了网页的异步交互，使得无需整页刷新即可获取数据，从而改善了用户体验。

- **优点**
  - 开创了异步请求模式，历史意义重大
  - 兼容性极好，几乎所有浏览器都支持
  - 能够高度自定义请求流程
- **缺点**
  - API 设计相对复杂，需要监听 `readyState` 和 `status`
  - 代码冗长，容易出现“回调地狱”
  - 没有内置的 Promise 支持，需要手动封装

**示例：**

```js
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();
```

## 二、Axios

Axios 是一个基于 Promise 的第三方库，本质上是对 `XMLHttpRequest` 的封装。它的目标是让网络请求更易用、更工程化。除了基本的 GET、POST 请求外，它还提供了拦截器、取消请求、自动转换响应数据等特性，因而在 Vue、React 等框架项目中被广泛使用。

- **优点**
  - 基于 Promise，语法简洁，避免回调地狱
  - 请求和响应拦截器，便于统一处理 token、错误提示
  - 自动 JSON 数据转换，减少手工操作
  - 支持取消请求、设置超时，适合复杂业务场景
  - 对老旧浏览器兼容性好
- **缺点**
  - 需要额外引入依赖，增加包体积
  - 封装较多，底层不透明

**示例：**

```js
axios.get("/api/data")
  .then(response => console.log(response.data))
  .catch(error => console.error(error));
```

## 三、Fetch

Fetch 是浏览器原生提供的现代化网络请求 API，它是对 `XMLHttpRequest` 的替代方案。其最大特点是基于 Promise 的语法，更加简洁易读。同时，Fetch 支持流式处理，能更好地应对大文件下载和实时数据场景。

- **优点**
  - 浏览器原生支持，无需引入额外库
  - 基于 Promise，代码清晰，配合 async/await 更加直观
  - 支持流式处理，适合大数据传输
  - API 设计更现代化，可读性强
- **缺点**
  - 不会自动 reject HTTP 错误，需要手动判断 `response.ok`
  - 默认不支持请求取消和超时控制（需结合 `AbortController`）
  - 对于旧版浏览器需要 polyfill

**示例：**

```js
fetch("/api/data")
  .then(response => {
    if (!response.ok) throw new Error("Network error");
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error(error));
```

## 四、对比总结

### 1. 功能特性对比表

| 特性         | Ajax (`XMLHttpRequest`) | Axios                     | Fetch (原生 API)                 |
| ------------ | ----------------------- | ------------------------- | -------------------------------- |
| API 设计     | 回调风格，代码冗长      | 基于 Promise，语法简洁    | 基于 Promise，语法现代化         |
| 错误处理     | 需手动判断 `status`     | 自动抛出错误，支持拦截器  | 需手动判断 `response.ok`         |
| 数据转换     | 手动 `JSON.parse`       | 自动处理 JSON             | 手动调用 `response.json()`       |
| 取消请求     | 较为复杂                | 内置支持                  | 通过 `AbortController`           |
| 超时设置     | 需手动实现              | 内置支持                  | 需结合 `Promise.race` 或中断机制 |
| 浏览器兼容性 | 最佳，老旧浏览器也支持  | 良好，依赖 XMLHttpRequest | 较新浏览器支持，需要 polyfill    |
| 工程化能力   | 较弱                    | 很强，适合中大型项目      | 中等，适合现代项目               |

## 五、总结

Ajax 是异步请求的起点，主要作为理解底层原理的工具。Axios 在工程实践中脱颖而出，凭借简洁的 API 和丰富的扩展能力，成为目前最常用的解决方案。而 Fetch 则是浏览器原生的现代化替代方案，代表了未来的发展方向。选择哪一种方式，取决于项目环境和需求：若强调兼容性和底层控制，可以用 Ajax；若项目复杂且需要统一的请求管理体系，Axios 更合适；若追求现代化、减少依赖，并且目标环境是新浏览器，那么 Fetch 是最佳选择。
]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>React 组件调用的两种方式区别</title>
    <link>https://www.gfbzsblog.site/main/Articles/110</link>
    <comments>https://www.gfbzsblog.site/main/Articles/110#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Mon, 18 Aug 2025 19:40:10 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/110</guid>

    <description><![CDATA[面试被问到的一道题，在结束之后发现自己的概念不清晰，因此做一个笔记来加深一下印象。]]></description>
    <content:encoded><![CDATA[
#### 1. **标准调用方式 (`<Component />`)**
```jsx
<ChildComponent prop={value} />
```
- **组件实例管理**：React 内部通过组件类型和 `key` 识别组件实例。
- **生命周期行为**：父组件重渲染时，若子组件的 `props` 或 `key` 未变化，**复用现有实例**，触发更新而非重新挂载。
- **状态保留**：子组件内部状态（`useState`, `useRef` 等）持续保留。
- **性能优化**：符合 React 协调算法，避免不必要的 DOM 操作。
- **Props 传递**：直接通过属性传递数据。

#### 2. **箭头函数调用方式 (`{() => <Component />}`)**
```jsx
{() => <ChildComponent prop={value} />}
```
- **组件实例管理**：每次渲染创建**新的函数实例**，React 视其为全新组件。
- **生命周期行为**：父组件每次重渲染都会触发子组件的 **完整卸载（unmount）和重新挂载（mount）**。
- **状态丢失**：子组件内部状态完全重置，所有 `useEffect` 依赖重新执行。
- **性能问题**：
  - 重复创建组件实例增加内存开销
  - 频繁触发挂载/卸载生命周期
  - 破坏 React 的复用优化机制
- **Props 传递**：需通过闭包捕获变量，可能引发过时闭包问题。

---

### 核心差异总结
| **特性**       | 标准方式            | 箭头函数方式                      |
| -------------- | ------------------- | --------------------------------- |
| **实例标识**   | 稳定 (类型/key)     | 每次创建新函数 (标识不稳定)       |
| **更新行为**   | 可复用实例 (Update) | 销毁旧实例 + 创建新实例 (Remount) |
| **状态保留**   | ✅                   | ❌ (完全重置)                      |
| **性能影响**   | 优化友好            | 额外实例化开销                    |
| **React 推荐** | ✅ 标准做法          | ❌ 反模式 (特殊情况除外)           |

---

### 技术本质
```mermaid
graph TD
    A[父组件渲染] --> B{调用方式}
    B -->|标准方式| C[React 检查组件标识]
    C -->|标识匹配| D[复用实例/更新]
    C -->|标识不匹配| E[卸载旧实例 + 挂载新实例]
    
    B -->|箭头函数| F[每次创建新函数]
    F --> G[React 视为新组件]
    G --> E
```]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>React 类组件 vs 函数组件：一份够用又够深的对照技术文档</title>
    <link>https://www.gfbzsblog.site/main/Articles/98</link>
    <comments>https://www.gfbzsblog.site/main/Articles/98#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sun, 17 Aug 2025 20:58:32 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/98</guid>

    <description><![CDATA[该文档系统比较了 React 中类组件与函数组件的差异，从语法、生命周期、状态管理、副作用、性能优化、逻辑复用到 TypeScript 与错误边界等方面逐一对照，结合示例和实践建议，说明了函数组件结合 Hooks 已成为主流选择，但类组件在错误边界和存量代码中仍有价值；最后给出迁移指南与最佳实践，帮助开发者理解两种范式的取舍与结合方式。]]></description>
    <content:encoded><![CDATA[React 中的类组件（Class Components）和函数组件（Function Components）是构建 UI 的两种主要方式。它们之间的区别随着 React 的发展（特别是 Hooks 的引入）而发生了重大变化。

## 一、一句话结论

**新代码优先用函数组件（Hooks）**：表达力强、逻辑复用更自然、与并发特性和数据获取模式更贴合。  

**类组件仍有价值**：**错误边界**目前只能由类实现；存量代码大量存在，读懂维护/迁移很重要。  

## 二、快速对比表

| 维度       | 类组件 Class Component                    | 函数组件 Function Component                 |
| ---------- | ----------------------------------------- | ------------------------------------------- |
| 定义方式   | `class extends React.Component`           | `function`/箭头函数                         |
| 状态       | `this.setState`（**浅合并**）             | `useState`（**替换**/自行合并）             |
| 生命周期   | `componentDidMount/Update/WillUnmount` 等 | `useEffect/useLayoutEffect` 统一描述副作用  |
| 性能控制   | `shouldComponentUpdate`、`PureComponent`  | `React.memo`、`useMemo`、`useCallback`      |
| 事件绑定   | 需处理 `this` 绑定                        | 无 `this`，闭包与依赖管理更关键             |
| Refs       | `createRef`、实例字段                     | `useRef`、`forwardRef`                      |
| Context    | `contextType` / `Context.Consumer`        | `useContext`                                |
| 错误边界   | 支持（`componentDidCatch`）               | 不能直接实现错误边界                        |
| 并发特性   | 可用 `startTransition`（非 Hook）         | `useTransition`、`useDeferredValue` 等 Hook |
| 逻辑复用   | HOC、Render Props                         | **自定义 Hook**（组合更轻量）               |
| 类型（TS） | 类泛型参数（`Props, State`）              | `FC<Props>` 或普通函数+类型                 |
| 默认值     | `defaultProps`                            | 更推荐函数参数默认值/解构默认值             |

##  三、语法对照：最小计数器 

**类组件：**

```jsx
class Counter extends React.Component<{}, { count: number }> {
  state = { count: 0 };

  componentDidMount() {
    document.title = `count: ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `count: ${this.state.count}`;
  }

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        {this.state.count}
      </button>
    );
  }
}
```

**函数组件：**

```jsx
function Counter() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    document.title = `count: ${count}`;
  }, [count]);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```

细看上面的对照，结果显而易见：

- **类组件**需要定义 `state`、在 `componentDidMount` 和 `componentDidUpdate` 中分别处理副作用，并且通过 `this.setState` 来更新状态，写法相对冗长且容易出错；
- **函数组件**则通过 `useState` 钩子直接声明状态，用 `useEffect` 钩子集中处理副作用逻辑，代码简洁明了，结构更加直观。

这种写法上的精简不仅仅是“代码量减少”，更重要的是函数组件在逻辑上更符合开发者的思维模型：状态与副作用都以函数形式组合，更容易拆分与复用。

## 四、生命周期 ⇄ Hooks 对照

| 类生命周期                                     | 对应 Hook 模式                            | 说明                     |
| ---------------------------------------------- | ----------------------------------------- | ------------------------ |
| `constructor`                                  | 组件外部初始化 / `useState` 初始值        | 尽量少做逻辑，避免副作用 |
| `componentDidMount`                            | `useEffect(fn, [])`                       | 首次渲染后执行           |
| `componentDidUpdate`                           | `useEffect(fn, [deps])`                   | 依赖变化后执行           |
| `componentWillUnmount`                         | `useEffect(() => { return cleanup }, [])` | 返回清理函数             |
| `getDerivedStateFromProps`                     | **手动派生**：`useMemo`/在渲染中计算      | 尽量避免“派生状态”       |
| `shouldComponentUpdate`                        | `React.memo` + `useMemo/useCallback`      | 控制重渲染               |
| `componentDidCatch`/`getDerivedStateFromError` | **无**                                    | 错误边界仅类支持         |

 从这个对照表可以看出：  

1. **大部分生命周期都能在 Hooks 中找到等价或更灵活的实现方式**，例如 `componentDidMount`、`componentDidUpdate` 对应 `useEffect`，`shouldComponentUpdate` 对应 `React.memo` 搭配 `useMemo/useCallback`。
2. **函数组件的核心理念是“按逻辑组织代码”，而不是“按生命周期阶段”**。Hooks 不再要求开发者记住各种生命周期名词，而是通过依赖项数组精准声明“什么时候运行”。
3. **并非所有生命周期都有直接对应**。比如 `componentDidCatch` 和 `getDerivedStateFromError` 目前依然只有类组件支持，用于错误边界；这意味着在需要错误边界的场景里仍需要类组件。
4. **最佳实践趋势**：减少“派生状态”的使用（即 `getDerivedStateFromProps` 的场景），优先通过 `useMemo` 或直接在渲染中计算衍生数据，保持状态单一、可控。

总结来说，Hooks 的设计让副作用和状态逻辑更加聚合、灵活和可复用，这是它逐渐取代类组件的根本原因；但在少数场景（如错误边界）类组件仍然不可替代。

## 五、Hooks副作用与常见陷阱（重点）

**副作用Hooks可分为两个**：

- `useEffect`：主要用于数据/订阅/事件。
- `useLayoutEffect`：主要用于视觉/布局，但是需要注意的是，`useLayoutEffect`是在所有的DOM变更之后，浏览器绘制之前同步执行的，这意味着它会阻塞浏览器的绘制。在 React 的[Fiber](https://www.gfbzsblog.site/main/Articles/38)架构下，尽管渲染过程已被优化为**可中断的异步模式**，但 `useLayoutEffect` 的使用仍需谨慎，原因在于它与 React 的**提交阶段（Commit Phase）**强关联，该阶段具有**同步且不可中断**的特性。
```meta
title: 孤芳不自赏
url: https://www.gfbzsblog.site/main/Articles/38
image: https://www.gfbzsblog.site/images/avatar_20250520_215057.png
desc: React Fiber架构全方位分析
```
**依赖数组**：

- 使用规则：使用到的外部变量必须出现在依赖中（也可以通过 `useRef` 规避重建）。
- 常见Bug：漏填依赖导致读到**过期闭包**中的值；乱填依赖导致不必要重跑，这些都是闭包陷阱，需要额外关注一下。
- 工具：配合 `eslint-plugin-react-hooks` 强制校验。

**清理**：

- 订阅、定时器、事件监听务必在 `return () => {...}` 中清理，依赖变化或卸载时运行。

## 六、性能：渲染控制与记忆化

**类组件**：

- `shouldComponentUpdate(nextProps, nextState)`：返回 `false` 可跳过渲染。
- `PureComponent`：浅比较 `props/state`。

**函数组件**：

- `React.memo(Component, areEqual?)`：浅比较 `props`。
- `useMemo(fn, deps)`：缓存**计算结果**；避免昂贵计算在不变时重复执行。
- `useCallback(fn, deps)`：缓存**函数引用**；配合 `memo`/依赖稳定化。
- 过度记忆化会增加复杂度；以**真实瓶颈**为准（profiling）。
## 七、事件与 `this` vs 闭包

类组件需要处理 `this`：构造器绑定或类字段箭头函数。

函数组件没有 `this`，但要管理**闭包**与依赖：

- 事件处理里用到的状态/参数如果会变，要么放依赖里，要么用函数式更新：`setX(x => x + 1)`。

## 八、Refs 与实例变量

**类**：`this.ref = React.createRef()`；实例字段天然存在。

**函数**：`const ref = useRef<T>(initial)`；`ref.current` 在渲染间持久但**变更不触发渲染**。

- 常用作：DOM 引用、外部 API 句柄、**可变的实例变量**（节流标记、上一次值等）。

```tsx
function Player() {
  const audioRef = React.useRef<HTMLAudioElement>(null);
  const playingRef = React.useRef(false);

  const toggle = () => {
    const el = audioRef.current!;
    if (playingRef.current) el.pause(); else el.play();
    playingRef.current = !playingRef.current;
  };

  return <>
    <audio ref={audioRef} src="/song.mp3" />
    <button onClick={toggle}>Play/Pause</button>
  </>;
}
```

## 九、 Context

**类**：`static contextType = ThemeContext` 或 `<ThemeContext.Consumer>`。

**函数**：`const theme = useContext(ThemeContext)` 更直接；避免“消费者地狱”。

## 十、错误边界（类独占能力）

**UI 崩溃隔离**：捕获子树渲染错误并展示降级 UI。

目前只能用类组件实现：

```tsx
class ErrorBoundary extends React.Component<{ fallback: React.ReactNode }, { hasError: boolean }> {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(error: unknown, info: React.ErrorInfo) { /* 上报日志 */ }
  render() { return this.state.hasError ? this.props.fallback : this.props.children; }
}
```

> 函数组件**不能**直接成为错误边界；通常把上层包成类的错误边界组件。

## 十一、并发与交互响应

两者都能使用**非 Hook** 的 `startTransition(() => setState(...))`。

函数组件独享 Hook：
- `useTransition()`：把某些更新标记为“可延后”，保持输入流畅。
- `useDeferredValue(value)`：延迟昂贵衍生计算。

场景：搜索输入→列表过滤；打字不卡，结果稍后刷新。

## 十二、逻辑复用：HOC/Render Props vs 自定义 Hook

类时代常用 HOC、Render Props，容易出现**包装地狱**与命名冲突。

Hooks 时代用**自定义 Hook** 聚合状态与副作用，组合更平坦：

```tsx
function useOnlineStatus() {
  const [online, setOnline] = React.useState(navigator.onLine);
  React.useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);
  return online;
}
```

## 十三、TypeScript 要点

**类**：`class C<P, S> extends React.Component<P, S>`；方法签名更显式。

函数：`function Comp(props: Props) {}` 或 `const Comp: React.FC<Props> = (props) => {}`
- `React.FC` 可省，**建议普通函数签名**（避免隐式 `children`、与泛型更灵活）。

**默认值**：函数参数默认值/解构默认值优先，少用 `defaultProps`（在函数组件上支持不佳且不推荐）。

## 十四、SSR / SSG / RSC（简述）

SSR/SSG 对两者都可用；不过 Hooks 的数据获取与 Suspense 配合更自然。

服务器组件（RSC）只适用于**函数式组件**范式（不含 stateful client hooks），这是现代数据获取的趋势点。

## 十五、迁移指南：类 → 函数的映射

状态：`this.state` → `useState`/`useReducer`。

副作用：`componentDidMount/Update/WillUnmount` → **一个** `useEffect`，在其中做订阅与清理。

`shouldComponentUpdate`/`PureComponent` → `React.memo` + 局部 `useMemo/useCallback`。

实例字段 → `useRef`。

错误边界：**保留类组件**或单独抽出成类错误边界。

注意**依赖数组**与**闭包**问题，是迁移中最常见的 bug 来源。

## 十六、常见坑位清单

**漏依赖**导致读到旧值：配合 ESLint 规则修正。

在 `useEffect` 里直接 `setInterval` 却不清理：卸载后仍在跑。

把大对象放入 `useState` 且频繁 `setState({...obj, x})`：考虑 `useReducer`。

过度使用 `useMemo/useCallback`：微优化吞掉了可读性。

在渲染期间做副作用（发请求/操作 DOM）：应放入 `useEffect`/`useLayoutEffect`。

React.memo` 搭配**不稳定 props**（匿名函数/字面量对象）→ 失效：用 `useCallback/useMemo` 稳定引用。

## 十七、什么时候仍考虑类组件？

需要**错误边界**且不想引入额外包装。

大量存量类代码，团队短期内以维护为主。

某些基于类的第三方库封装（逐渐变少）。

## 十八、实战建议

新功能：**默认函数组件 + Hooks**。

清晰的副作用分层：数据副作用（请求/订阅）与布局副作用（测量/同步）分开写。

必配：`eslint-plugin-react-hooks`、`@types/react` 最新版本。

性能：先量化（Profiler/Performance 面板），再上 `memo`/`useMemo`。

复杂状态：首选 `useReducer` + 自定义 Hook 拆模块。

错误边界：在路由/页面级放一层类组件 `ErrorBoundary`。

组件 API：优先**组合**而非继承/HOC；导出“受控 + 非受控”两种用法。

TS：函数组件用普通函数签名，避免 `React.FC` 带来的隐式行为。

## 十九、结语

函数组件是 React 的**主流与未来**，尤其在并发特性、数据获取与组件组合上优势明显。

类组件仍是生态的一部分，理解它的生命周期与错误边界机制，对维护老项目和设计稳健的边界层都很重要。

最理想的姿势：**以函数为主，错误边界用类兜底**；按实际性能与复杂度，理性使用记忆化与抽象。]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>本地极速体验指南</title>
    <link>https://www.gfbzsblog.site/main/Articles/77</link>
    <comments>https://www.gfbzsblog.site/main/Articles/77#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Thu, 17 Jul 2025 22:57:01 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/77</guid>

    <description><![CDATA[这个博客系统，以前端为主场，体验和工程化拉满。不是那种“能跑起来就行”的小玩具，而是我一行行打磨出来、追求极致体验和可维护性的全栈项目。]]></description>
    <content:encoded><![CDATA[> **性能优化，用户体验。LCP 0.6s，FCP 0.4s，内容快速加载，交互流畅。**

---

这是一个博客系统项目，主要关注前端体验和工程化实践。项目注重代码质量和用户体验，在功能实现和性能优化方面做了一些尝试。

## 在线体验

👉 [在线体验博客](https://www.gfbzsblog.site)

## 🛠️ 技术栈

- 前端：Next.js 15 + React 18 + TypeScript + Redux Toolkit + SCSS + Framer Motion
- 后端：Spring Boot 3 + MyBatis Plus + MySQL
- 部署：宝塔Linux + Nginx
- 其他：腾讯云COS、腾讯云SES、DeepSeek API 接口、腾讯云CDN内容分发与全球加速

## 目录结构

- /api：接口集中封装，类型安全，便于维护，接口变动一处修改即可全站生效

- /components：多个可复用组件，支持动画、响应式、主题切换，包括AI聊天、评论、目录、弹窗、分页、表情、打字机、代码高亮、友链、头像预览等

- /pages：Next.js 路由自动映射，包含主站、后台、404、首页、文章、归档、友链、相册、灵感、统计、AI 聊天、搜索、标签、留言板等页面

- /redux：全局状态管理，覆盖权限、主题、系统设置、认证、管理员认证等，支持异步操作、持久化

- /hooks：自定义 Hook，业务逻辑解耦，包括useAIChat、useAuth、useLoading、useDebounce、useThrottle、useTheme、useError等

- /utils：工具函数库，http 封装、加密、格式化、AI 工具、文章工具、类型安全、校验等功能

- /routes：路由集中配置，导航菜单自动生成，支持嵌套路由、权限控制

- /styles：全局样式、字体、主题变量、CSS Modules 样式隔离，支持暗黑/明亮模式切换

- /config：全局配置项，AI 助手、主题、接口、系统参数统一管理

- /client、/admin、/context、/http、/types：细分业务和基础能力，代码结构清晰，便于维护和扩展

## 工程化和体验

- HTTP 封装：自研 HTTP工具，支持全局 BASE_URL、自动带 token、错误统一处理，具备请求拦截、响应拦截、错误拦截、请求取消拦截、请求重试拦截等拦截器，API 层全部基于 http 封装

- 性能优化：采用SSG/SSR/ISR/CSR混合策略，首页 LCP 0.6s、FCP 0.4s，虚拟滚动优化长列表，图片懒加载+CDN，渐进加载，代码分割/懒加载分包策略，Tree Shaking减少包体积

- 类型安全：全量 TypeScript，接口、组件、工具、全局状态全部类型覆盖

- 动画和交互：使用Framer Motion、Typewriter等库，注重用户体验

- 功能：包含文章、标签、友链、相册、公告、AI 聊天、评论、后台管理、权限、邮箱通知、云存储、统计分析等

- 细节体验：智能文章目录、评论区 Markdown/表情/楼中楼、AI 聊天流式响应、操作提示弹窗、主题切换、后台权限管理、性能监控等

---

## 1. 博客内容截图 & Lighthouse 评分

**内容截图**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/b63a15c3-7d4a-4507-94ec-a7833eda11a6.png" alt="图片" width="1000" />

**Lighthouse 评分**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/6faeb1b3-3dd0-49cb-a092-eaf4a4f30afa.png" alt="图片" width="1000" />

> **首页 LCP 0.6s，FCP 0.4s，Lighthouse 评分 99+**  
> 通过SSR/SSG/ISR、Next.js图片压缩、图片懒加载、条件渲染+虚拟滚动、Tree Shaking、CDN内容分发、路由懒加载、组件懒加载等优化实现

---

## 2. 博客界面体验

- 首页设计参考了[@grtsinry43大佬](https://github.com/grtsinry43)的实现思路

- 文章列表页

  <img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/5e711698-b310-46df-a5b9-ba2180ad8960.png" alt="图片" width="1000" />

- 文章详情页

  文章详情页是博客系统的核心页面，支持Markdown 全格式渲染、代码高亮、表格、视频、序列图等内容的展示。左侧有智能目录，实时高亮当前阅读进度，右侧有文章侧边栏，支持字体调节、阅读时间统计、MarkDown导出和分享等功能。包含点赞动画、评论区、最近文章推荐、返回列表等交互功能。

  <img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/c389510e-7bb9-48d6-876a-b62f724afb57.png" alt="图片" width="1000" />

- 标签云页

  标签云页面的动画和交互设计，参考了@grtsinry43大佬的实现思路。每个标签都有独立的配色和动态背景，入场时带有弹性和旋转的动画效果，鼠标悬停时有缩放与旋转反馈。底部还叠加了动态的 TagCloud 背景，增加视觉层次感。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/db853d83-32dd-4154-b126-f89d35958f3f.png" alt="图片" width="1000" />

- 灵光一瞬

灵光一瞬页面用于记录生活中的点滴和小想法。每条内容以简洁的动漫风卡片形式展示，错落有致，方便浏览。整体风格轻松温和，可以随时回顾和分享这些生活片段。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/b6331b2d-77b4-4854-a0ff-0518300f0288.png" alt="图片" width="1000" />

- AI智能助手

小熙AI页面是博客里的智能助手空间。整体风格亲切，顶部有氛围感文案和欢迎语。未登录时会提示登录，登录后可以直接和AI对话。对话区采用了自研的 AIChat 组件，基于DeepSeek大模型API，支持流式消息、Markdown 渲染、代码高亮。页面布局简洁，交互细节到位。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/6bf93c49-c305-4e3a-8647-4c3f8d77f686.png" alt="图片" width="1000"  />

- 留言板

留言板页面提供留言、互动、交流的功能。支持匿名或带头像留言（支持QQ邮箱自动解析头像，也可以自定义上传），每条留言都能被置顶、回复，支持性别标识和时间显示。包含表单校验、头像上传、操作提示、加载动画等基础功能。留言内容支持动态打字机动画，管理员回复会高亮展示。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/0678ce32-0977-40c3-adc4-38a566a5d8b4.png" alt="图片" width="1000" />

- 友链页面

友链页面除了展示链接外，还提供了实时预览功能：点击任意友链，右侧会弹出对方网站的实时窗口（iframe 预览），不用跳转就能预览对方站点。如果对方网站不支持嵌入，会有友好的提示。预览区自适应缩放，窗口高度和布局会自动同步。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/fb9a1c27-7118-453e-9cf7-e4520e75ba7b.png" alt="图片" width="1000" />

- 暗黑/明亮模式切换

主题切换支持在明亮模式和暗黑模式之间切换，整个站点的配色、背景、字体会实时变化。所有页面、组件、动画都适配了主题变化，细节统一。

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/ac3a310c-11a6-40cc-9337-7042a6f7dc1f.png" alt="图片" width="1000" />

- 后台管理

后台管理系统用于管理博客的所有内容和功能。采用模块化、组件化的设计，每个功能区独立成模块，便于维护和扩展。

后台分为文章管理、评论管理、标签管理、用户管理、友链管理、留言板管理、相册管理、灵感管理、面试题收集管理、系统设置等多个板块。每个板块都有专属的表单、列表、筛选、批量操作、权限校验等功能，支持增删改查、批量导入导出、内容审核、置顶、回复、关联等操作。

后台采用响应式布局，适配不同屏幕，包含权限管理和路由守卫。每个管理模块都拆分成独立的 React 组件，样式隔离，逻辑清晰。

**仪表盘**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/c7cdc5ed-f095-422f-8e1a-c80844dadb2e.png" alt="图片" width="1000" />

**文章管理表单**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/666eef03-cde8-461e-b4f9-5ffac49d09cc.png" alt="图片" width="1000" />

**留言板管理**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/026cad66-9562-483b-82d6-eebf24eacbc2.png" alt="图片" width="1000" />

**全局系统设置**

<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/e24431c4-75f7-4a44-8c0c-c7194d21cb8d.png" alt="图片" width="1000" />

---

## 3. 本地安装

### 环境要求

- Node.js 18+
- npm 9+
- Java 17+
- Maven 3+
- MySQL 5.7+/8.0+
- 推荐 Edge 浏览器体验

### 克隆项目

```bash
git clone <your-repo-url>
cd <your-repo-directory>
```

---

### 后端配置（Spring Boot）

#### 1. 数据库准备

创建数据库（如有需要）：

```sql
CREATE DATABASE my_blog ;
```
数据库文件路径：src/main/resources/db/migration/mysql_db.sql

#### 2. .env 配置（blogBackend/.env）

```env
# 数据库
MYSQL_USERNAME=your_mysql_user
MYSQL_PASSWORD=your_mysql_password

# 邮箱
TENCENT_CLOUD_MAIL_USERNAME=your_mail@domain.com
TENCENT_CLOUD_MAIL_PASSWORD=your_mail_smtp_password

# 腾讯云COS
COS_SECRET_ID=your_cos_secret_id
COS_SECRET_KEY=your_cos_secret_key
COS_BUCKET_NAME=your_cos_bucket
COS_REGION=your_cos_region
COS_URL=https://your-cos-url

# Google OAuth（如用到）
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
```

#### 3. 启动后端

```bash
cd blogBackend
mvn spring-boot:run
```

默认端口：**8000**  
配置文件：`src/main/resources/application.yml`（支持环境变量注入）

---

### 前端配置（Next.js）

#### 1. .env 配置（blogFrontend/.env.local）

```env
NEXT_PUBLIC_BASE_URL=http://localhost:8000/api
NEXT_PUBLIC_PASSWORD_SALT=your-salt-here
NEXT_PUBLIC_API_ENDPOINT=https://api.deepseek.com/v1/chat/completions
NEXT_PUBLIC_API_KEY=your-deepseek-api-key
```

#### 2. 启动前端

```bash
cd blogFrontend
npm install
npm run dev
```

默认端口：**3000**  
访问地址：[http://localhost:3000](http://localhost:3000)

---

## 4. 性能优化

- **SSR + SSG + ISR**：采用多种渲染策略，首屏内容直出，提升加载速度
- **图片懒加载 + CDN**：使用CDN加速和Next.js图片压缩，优化图片加载
- **按需加载**：使用虚拟滚动和IntersectionObserver API，按需加载内容
- **代码分割**：路由级和组件级的代码分割，减少主包体积
- **Tree Shaking**：移除未使用的代码，减少打包体积
- **缓存策略**：静态资源强缓存，接口智能缓存
- **Lighthouse 评分 99+**：性能、可访问性、最佳实践等方面表现良好

> **首页 LCP 0.6s，FCP 0.4s**

---

## 5. 致谢

特别感谢 [@grtsinry43大佬](https://github.com/grtsinry43) 的灵感与技术分享，他的博客项目给了我很多启发。
```meta
title: grtsinry43
url: https://github.com/grtsinry43
image: https://dogeoss.grtsinry43.com/img/author-removebg.png
desc: Hi there, I'm grtsinry43
```
---

## 6. 生产部署

如需了解一键部署、线上环境变量配置等生产环境相关内容，  
👉 [查看生产部署指南](https://www.gfbzsblog.site/main/Articles/49)
```meta
title: 孤芳不自赏
url: https://www.gfbzsblog.site/main/Articles/49
image: https://www.gfbzsblog.site/images/avatar_20250520_215057.png
desc: 手把手教你将前后端分离项目部署上线
```
---

## 贡献指南

欢迎 PR、Issue、建议和反馈！  
如需本地开发、二次开发、定制功能，欢迎联系或直接提 Issue。

## License / 许可证

This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.

本项目采用 MIT 开源许可证，详见 [LICENSE](./LICENSE) 文件。

## 📬联系方式

- 邮箱：624787243@qq.com


]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>电影院选座组件（SeatSelection）渲染流程技术文档（含数据结构示例）</title>
    <link>https://www.gfbzsblog.site/main/Articles/71</link>
    <comments>https://www.gfbzsblog.site/main/Articles/71#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Tue, 15 Jul 2025 16:13:03 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/71</guid>

    <description><![CDATA[影院二维座位图的渲染具体是如何实现的]]></description>
    <content:encoded><![CDATA[## 1. 组件概述

`SeatSelection` 是电影院系统中负责“选座”功能的核心组件。它的主要职责有四点：

- 获取并管理当前场次的座位数据。
- 支持不同影厅类型、座位类型、过道、空位等复杂排布。
- 实现选座、取消选座、成对选座（情侣厅）、价格统计等交互。
- 动态渲染出还原真实影院的二维座位图。
## 2. 数据获取与状态管理

### 2.1 路由参数获取

```javascript
const { scheduleId } = useParams();
// scheduleId: string | undefined
// 例: "123"
```

- 通过路由参数获取当前场次ID，驱动后续数据请求。
### 2.2 异步获取座位数据

```javascript
useEffect(() => {
    const fetchSeatData = async () => {
        const data = await seatAPI.getSeatsByScheduleId(Number(scheduleId));
        setScheduleDetail(data);
        // 处理行标签
        const labels = Array.from(new Set(data.seats.map(seat => seat.rowLabel))).sort();
        setRowLabels(labels);
        setLoading(false);
        // 获取可用场次（略）
    };
    if (scheduleId) fetchSeatData();
}, [scheduleId]);
//
```

- `useEffect` 依赖 `scheduleId` 变化触发数据请求。当用户切换场次时，`scheduleId` 更新，自动拉取新座位数据。
- 根据 `scheduleId` 请求后端，`**Number()**`是一个**构造函数**，前面获取到的路由参数`scheduleId`为字符串，用这个包装之后转换位数字格式，然后获取当前场次的所有座位数据（含座位状态、类型、排号、列号等）。

**解释**`const labels = Array.from(new Set(data.seats.map(seat => seat.rowLabel))).sort();`

我们首先把这一行代码拆成几个部分：

- `data.seats.map(seat => seat.rowLabel)`：此时可以看下面`data:ScheduleDetail`的数据结构如下：因此`data.seats`就是一个数组，这个数组中每一个座位都是一个对象，有八个属性，需要对这个数组进行遍历，拿到键为`rowLabel`的值，返回一个新数组。此时数组中的值是重复的，需要进行去重处理。
- `new Set()`：传入一个数组，比如[A，A，B，B，C，C，C，D，...]对这个数组中的值进行去重，然后返回一个对象，例如：**{'A','B','C'}**，此时是对象，无法使用数组方法，如`sort/map/filter`。
- `Array.from()`：创建一个新的浅拷贝数组实例，简单来说就是将上一步的对象转换为数组。
- `.sort()`：对上面三步操作返回的数组进行排序，按照它们的 UTF-16 码元值进行升序排序。

为什么用 `Array.from(new Set())` 而不用 `Lodash` 的 `_.uniq()`？

保持零依赖原则，减少包体积。

``` typescript
export interface ScheduleDetail {
    schedule: {
        scheduleId: number;
        movieId: number;
        hallId: number;
        startTime: string;
        endTime: string;
        basePrice: number;
        vipPrice: number;
        loverPrice: number;
    };
    movie: {
        movieId: number;
        title: string;
        englishTitle: string;
        director: string;
        genres: string;
        actors: string;
        duration: number;
        releaseDate: string;
        releaseLocation: string;
        posterUrl: string;
        trailerUrl: string;
        description: string;
        avgRating: number;
        boxOffice: number;
        wantToWatch: number;
    };
    hall: {
    hallId: number;
    name: string;
    type: HallType;
    rowCount: number;
    colCount: number;
    rowLabels: string;
    cinemaId: number;
};
    seats: [
      {
        ssId: number;
        rowLabel: string;
        colNum: number;
        seatType: SeatType;
        status: 'AVAILABLE' | 'OCCUPIED' | 'LOCKED';
        price: number;
        userId: number | null;
        lockTime: string | null;
      },
    ];
}
```
- 提取所有排的 `rowLabel`，去重排序，得到 `rowLabels`，用于后续渲染每一排。
```
const [scheduleDetail, setScheduleDetail] = useState<ScheduleDetail | null>(null);
const [rowLabels, setRowLabels] = useState<string[]>([]);
```
- **scheduleDetail状态**（接口返回结构）示例：

```javascript
{
  seats: [
    { ssId: 1, rowLabel: 'A', colNum: 1, seatType: 'STANDARD', status: 'AVAILABLE', price: 40 },
    { ssId: 2, rowLabel: 'A', colNum: 2, seatType: 'STANDARD', status: 'OCCUPIED', price: 40 },
    { ssId: 3, rowLabel: 'B', colNum: 1, seatType: 'STANDARD', status: 'AVAILABLE', price: 40 },
    // ...
  ],
  // 其他 movie, hall, schedule 信息略，这里只展示座位数据集结构格式
}
```

- **rowLabels** 示例：

```javascript
// 由所有座位的 rowLabel 去重排序得到
['A', 'B', 'C', ...]
```

- 之所以要求rowLabels是计算会得出座位的行号，便于后续对数据进行行号进行渲染。

### 2.3 其他状态

- `loading`、`error`：加载和错误状态
- `showLoginModal`、`showScheduleList` 等：交互相关状态

------

## 3. 计算辅助数据

### 3.1 计算最大列数（maxColCount）

```javascript
const maxColCount = useMemo(() => {
    if (!scheduleDetail) return 0;
    const allRows = scheduleDetail.seats.reduce((acc, seat) => {
        if (!acc[seat.rowLabel]) acc[seat.rowLabel] = [];
        acc[seat.rowLabel].push(seat.colNum);
        return acc;
    }, {} as Record<string, number[]>);
    let max = 0;
    Object.values(allRows).forEach(cols => {
        const rowMax = Math.max(...cols);
        if (rowMax > max) max = rowMax;
    });
    return max;
}, [scheduleDetail]);
```

- 统计所有排中“列数最多的那一排”的列数，保证所有排渲染时对齐。
- `useMemo()`是用于缓存计算的结果的，由于影厅的座位较多，并且数量是固定的，因此计算的答案最大列数也不会发生改变(无特殊情况下)，那么用`useMemo()`就可以很大的减少性能开销，当`scheduleDetail`发生变化时，意味着电影的场次进行切换，影厅也会随之切换，那么座位数也不会一成不变，此时就需要重新计算结果了。

**解释：**`if (!acc[seat.rowLabel]) acc[seat.rowLabel] = [];`

- 如果`allRows`目前还没有`seat.rowLabel`所对应的那一排，那么就在`acc`对象中新建一个`seat.rowLabel`，它的值作为`acc`对象中的键，用来获取对应的数组，这个数组就是键值对中的值，与前方的值不是同一个概念的。示例：`A:seat.rowLabel: []`

**解释：**`acc[seat.rowLabel].push(seat.colNum);`

- `.push()`JavaScript中模拟栈的用法，这个意思是将seat.colNum(一个数字)推入`acc`对象中`seat.rowLabel`数组中，先对`seat.rowLabel`进行比对，当相同时就会将列号进行推入。

**解释：**`scheduleDetail.seats.reduce((acc, seat) => {},{} as Record<string, number[]>);`

- `reduce()`该方法对数组中的每个元素按序执行一个提供的 **reducer** 函数，每一次运行 **reducer** 会将先前元素的计算结果作为参数传入，最后将其结果汇总为单个返回值。这里`scheduleDetail.seats.reduce((acc, seat) => {})`就是对`scheduleDetail.seats`进行遍历，数据结构看上文。累积值为`acc`，经过对seat进行计算得出键值对，然后将键值对累计到acc中。
- `{} as Record<string, number[]>`这个是一个TS断言，如果没有这个断言，在初始化时{}为空对象，TS将会认为这个空对象始终为空，就无法向对象中加入元素。
- `Record<string, number[]`是TS的内置类型，意思是一个对象，key是字符串，value是数字数组。

- `reduce`执行步骤：

| *迭代次数* | `acc`**（累积值）**             | `seat`**（当前座位）**         | **执行操作**                                                 |
| ------------ | ----------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
| **1**        | `{}`                            | `{ rowLabel: "A", colNum: 1 }` | `acc["A"]` 不存在 → `acc["A"] = []` → `acc["A"].push(1)` → `acc = { A: [1] }` |
| **2**        | `{ A: [1] }`                    | `{ rowLabel: "B", colNum: 2 }` | `acc["B"]` 不存在 → `acc["B"] = []` → `acc["B"].push(2)` → `acc = { A: [1], B: [2] }` |
| **3**        | `{ A: [1], B: [2] }`            | `{ rowLabel: "A", colNum: 3 }` | `acc["A"]` 已存在 → `acc["A"].push(3)` → `acc = { A: [1, 3], B: [2] }` |
| **4**        | `{ A: [1, 3], B: [2] }`         | `{ rowLabel: "C", colNum: 4 }` | `acc["C"]` 不存在 → `acc["C"] = []` → `acc["C"].push(4)` → `acc = { A: [1, 3], B: [2], C: [4] }` |
| **5**        | `{ A: [1, 3], B: [2], C: [4] }` | `{ rowLabel: "B", colNum: 5 }` | `acc["B"]` 已存在 → `acc["B"].push(5)` → `acc = { A: [1, 3], B: [2, 5], C: [4] }` |

- 经过处理后，allRows的数据结构如下：

```javascript
{
  A: [1, 2, 4],
  B: [1, 2, 3, 4],
  C: [1, 2]
}
```

- 然后来对allRows中的最大列数进行计算，简单来说就是计算该对象中，哪个键值对的值的有最大值。下面的代码块有用到闭包，在每一次遍历后都有可能对函数外作用域的max值进行修改。

```javascript
let max = 0;
Object.values(allRows).forEach(cols => {
        const rowMax = Math.max(...cols);
        if (rowMax > max) max = rowMax;//如果计算值大于最大值，则进行替换
    });
```

**解释：**`Object.values(allRows).forEach(cols => {})`

- `Object.values()`：这个方法返回一个给定对象的自有可枚举字符串键属性值组成的数组。里面的值为`allRows`，如果以上面的数据结构为例，那么就返回了：[[1, 2, 4],[1, 2, 3, 4],[1, 2]]，简单说就是把对象中的键值对中，键去掉，剩下的值存放到数组中。
- `forEach()`：对数组中的每个元素执行一次。这里的元素就是[1, 2, 4],[1, 2, 3, 4],[1, 2]，每一个数组就是一个元素。
- `cols => {}`：cols是一个形参，代表着遍历的每一个数组。

**解释：**const rowMax = Math.max(...cols);

- `Math.max()`：函数用来返回输入参数中的最大值。
- `...cols`：`...`为展开运算符，因为此时cols是数组，那么如果Math.max(cols)等价于Math.max([1, 2, 4])，但是这个函数参数不可是数组，因此用...cols，那么Math.max(...cols)等价于Math.max(1, 2, 4)，此时就可以计算出最大值为4。

- 最终计算完成，计算出列数的最大值，之后渲染座位是根据这个最大值进行参考的，用来处理空位等情况。
- **maxColCount** 示例：

```javascript
maxColCount = 4	
```

------

## 4. 渲染一排座位（核心函数）

### 4.1 函数签名

```javascript
const renderSeatsInRow = (rowLabel: string) => { ... }
```

- 负责渲染指定排（如“A”排）的所有座位格子。

### 4.2 主要流程

1. **获取当前排的所有座位并排序**

```javascript
const rowSeats = scheduleDetail.seats
    .filter(seat => seat.rowLabel === rowLabel)
    .sort((a, b) => a.colNum - b.colNum);
```

**解释：**`.filter(seat => seat.rowLabel === rowLabel)`

`.filter`：过滤器，计算过程为：对所有的座位进行遍历，过滤得出座位中的行号与传如参数中行号相同的那一行的所有座位。

`.sort()`：按照列好从小到大排序，`a-b`是从小到大，`b-a`是从大到小。

- 经过处理后，**rowSeats** 示例：

```javascript
[
  { ssId: 1, rowLabel: 'A', colNum: 1, ... },
  { ssId: 2, rowLabel: 'A', colNum: 2, ... },
  { ssId: 4, rowLabel: 'A', colNum: 4, ... }
]
```

1. **构建 seatMap，便于按列号查找座位**

```javascript
const seatMap = new Map(rowSeats.map(seat => [seat.colNum, seat]));
```

**解释：**`new Map(rowSeats.map(seat => [seat.colNum, seat]));`

- `rowSeats.map(seat => [seat.colNum, seat])`：这里会对rowSeats进行遍历，并且返回一个新的键值对数组，这个数组的格式为：[[seat.colNum,{ } :seat ]]
- `Map`：Map是一种键值对集合，和普通对象类似，但键可以是任何类型。Map会对处理之后的rowSeats进行二次处理，转换为特殊的键值集合。

```javascript
//rowSeats处理前
[
  { ssId: 1, rowLabel: 'A', colNum: 1, ... },
  { ssId: 2, rowLabel: 'A', colNum: 2, ... },
  { ssId: 4, rowLabel: 'A', colNum: 4, ... }
]
//map遍历处理
[
  [1, { ssId: 1, rowLabel: 'A', colNum: 1 }],
  [2, { ssId: 2, rowLabel: 'A', colNum: 2 }],
  [4, { ssId: 4, rowLabel: 'A', colNum: 4 }]
]
//Map再处理
{
  1 => { ssId: 1, rowLabel: 'A', colNum: 1, ... },
  2 => { ssId: 2, rowLabel: 'A', colNum: 2, ... },
  4 => { ssId: 4, rowLabel: 'A', colNum: 4, ... }
}
```

关于为什么要把数组转为 `Map` 结构，我来做一些解释：

- **快速查找（O(1)时间复杂度）**

- **数组查找**：需要遍历（O(n)时间）
- **Map查找**：`Map` 内部使用了哈希表（Hash Table）的数据结构。直接通过键访问（可以瞬间完成，极大提高了查找效率）

- **维护明确的映射关系**

- 用 `**colNum**` 作为键，天然表达了 *"通过列号找座位"* 的业务逻辑

这种转换的本质是 **用空间换时间**（内存占用稍多，但查询极快），在需要高效查找的场景下非常值得。

1. **计算本排的过道位置**

```javascript
const hallType = scheduleDetail.hall.type;
const hallTemplate = hallConfig[hallType];
const colCount = maxColCount; // 用最大列数

let aislePositions: number[] = [];
if (colCount < 8) {
    aislePositions = [];
} else if (typeof hallTemplate.aislePositions === 'function') {
    aislePositions = hallTemplate.aislePositions(colCount);
} else {
    aislePositions = hallTemplate.aislePositions || [];
}
```

**解释：**这个难度不大，就是进行判断，当列数小于8时，就不渲染过道，如果大于等于8列，那么就进行过道的绘制，这里先判断`hallTemplate.aislePositions`是否为函数(aislePositions是配置文件HallTypeConfig接口中的一个属性，它可能为函数，这个函数接收总列数，通过计算返回一个数组，数组的值表示在第几列之后插入过道；这个属性也可能为数组，与函数计算后的数组的数据结构相同)

```javascript
export function getAislePositions(colCount: number, minSide :number, forceEven :boolen): number[] {
    // 两边数量为总列数1/5，最小为2
    let side = Math.floor(colCount / 5);
    if (side < minSide) side = minSide;
    // 情侣厅强制偶数
    if (forceEven && side % 2 !== 0) side++;
    // 如果两边数量太大导致中间区太小，自动减少两边
    while ((colCount - side * 2) < 2 && side > minSide) {
        side -= forceEven ? 2 : 1;
    }
    // 返回分割点
    return [side, colCount - side];
}
```

- 这个回调函数用来动态计算过道所在的位置。

**解释：**这个回调函数接受三个参数：

- `colCount: number`：总列数，通过总列数动态计算过道位置
- `minSide :number`：列数的最小数量，强制为2，1列不合理，无需设置过道。
- `forceEven = false`：是否强制让两边的座位数为偶数，一般不强制,情侣厅会强制偶数，因为座位是成对出现的。

**解释：**`Math.floor(colCount / 5);`

- `Math.floor`:向下取整，通过计算总列数除以五，算出两边座位所需的列数，通常为1/5，这样1 3 1的座位排布更符合实际情况。

**解释：**`if (side < minSide) side = minSide;`

- 当列数小于10列时，左右两边无论如何计算都是1列，并且1列不符合实际影厅需要，因此强制变为2列。

**解释：**`if (forceEven && side % 2 !== 0) side++;`

- 针对于情侣厅，当`forceEven`为`true`时强制偶数并且对`side`进行判断，如果为奇数则自动+1变为偶数。

**解释：**`while ((colCount - side * 2) < 2 && side > minSide)`

- 如果总列数减去两边的列数小于2列同时两边的列数大于最小列数的话，进行处理，处理过程为判断两边座位是否强制为偶数，强制的话减去2，不强制的话减去1，直到循环结束（这句话的涵义就是中间的列数过于少了，需要增加）。

最后返回分割点，`[side, colCount - side]`这个数组最终返回的过道位置是对称的。

1. **渲染每一列（格子）**

```jsx
return (
            <div key={rowLabel} className={styles['seat-row']}>
                {/* 字母单独一列，始终左对齐 */}
                <div className={styles['row-label']}>{rowLabel}</div>
                <div className={styles['row-seats']}>
                    {Array.from({ length: colCount }).map((_, index) => {
                        const colNum = index + 1;
                        const seat = seatMap.get(colNum);
                        return (
                            <React.Fragment key={colNum}>
                                {seat ? (
                                    <div
                                        className={`seat ${seat.status === 'OCCUPIED' ? 'occupied' : ''} ${seat.status === 'LOCKED' ? 'locked' : ''} ${selectedSeats.some(s => s.ssId === seat.ssId) ? 'selected' : ''}`}
                                        onClick={() => handleSeatClick(seat)}
                                    >
                                        {getSeatIcon(seat)}
                                    </div>
                                ) : (
                                    <div className={styles['empty-seat']} />
                                )}
                                {aislePositions.includes(colNum) && <div className={styles['aisle']} />}
                            </React.Fragment>
                        );
                    })}
                </div>
            </div>
        );
```

**解释：**`<div key={rowLabel} className={styles['seat-row']}>`

- key={rowLabel}用rewLable座位唯一key，是React diff 算法高效更新 UI 的基础。

**解释：**`<div className={styles['row-label']}>{rowLabel}</div>`

- 用来渲染排号，在所有座位的最左侧渲染，始终左对齐

**解释：**`{Array.from({ length: colCount })`

- 生成了一个长度为colCount的数组，colCount是之前计算过的最大列数。

**解释：**`.map((_, index) => {}`是对上面生成的数组的每一项进行遍历，map的回调函数有两个常用的参数，一个是当前元素的值，另一个是索引，因为只生成了一个长度为colCount的数组，并没有值，因此我们不需要它，命名为_，我们目前只需要当前数组的索引即可。

**解释：**`const seat = seatMap.get(colNum);`

- 去根据seatMap查找是否存在座位，传入的参数是colnums，这个是数组的索引值+1且是递增的，因为要绘制最大列数，那么遍历就要从0到最大列数全部遍历一便，但是有些列可能没有座位，根据哈希映射去查找，如果有返回true；反之返回false。

**解释：**`<React.Fragment> </React.Fragment>`

- 它等价于`<>  </>`，用来占位。在 React 里，一个组件的 return 只能有一个根节点。如果你想返回多个并列的元素，通常要用一个容器包起来，比如 <div>。但有时候你只是为了语法需要包一下，不想让页面多出一层标签，这时就可以用 React.Fragment，也可以用`<>  </>`。

**解释：**`{seat ?<div>{getSeatIcon(seat)}</div>：<div className={styles['empty-seat']} />`

- 一个三元表达式，当seat为true时，渲染座位图标；反之渲染一个透明格子。

**解释：**`{aislePositions.includes(colNum) && <div className={styles['aisle']} />}`

- 这个用来判断是否增加过道，根据当前的列数判断，如果aislePositions计算返回的数组中包含当前这一列，就在这一列的后面增加过道。

------

## 5. 渲染整个二维座位图

### 5.1 外层循环

```jsx
<div className={styles['seat-map']}>
    {rowLabels.map(rowLabel => renderSeatsInRow(rowLabel))}
</div>
```

- 遍历所有排（rowLabels），对每一排调用 `renderSeatsInRow`，最终渲染出完整的二维座位图。
## 6. 交互与状态更新

- **选座/取消选座**：点击座位格子，触发 `handleSeatClick`，更新 `selectedSeats`。
- **情侣厅成对选座**：特殊逻辑，自动选中/取消配对座位。
- **座位状态渲染**：`getSeatIcon` 根据座位状态和类型，动态渲染不同的图标和样式。
- **价格统计**：`calculateTotalPrice` 实时统计已选座位总价。

------

## 7. 其他辅助渲染

- **电影信息、影厅信息、观影小贴士**：顶部展示
- **图例（legend）**：展示不同状态的座位含义
- **已选座位列表、价格、确认按钮**：底部展示
- **场次切换、登录弹窗等**：交互细节

------

## 8. 总结

- **核心思想**：通过“外层循环渲染每一排 + 内层循环渲染每一列”，结合 seatMap、maxColCount、空位补齐、过道分隔，实现了高度还原真实影院的二维座位图。
- **技术亮点**：数据驱动、配置抽象、状态管理、复杂交互、性能优化。
- **用户体验**：所有排对齐、状态清晰、交互流畅，支持多种影厅和复杂排布。

------

## 9. 渲染流程图（伪代码）

```plain
for rowLabel in rowLabels:
    render row label
    for colNum in 1..maxColCount:
        if seatMap.get(colNum):
            render seat (with status, icon, click)
        else:
            render empty seat
        if colNum in aislePositions:
            render aisle
```

]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>
  <item>
    <title>手把手教你将前后端分离项目部署上线</title>
    <link>https://www.gfbzsblog.site/main/Articles/49</link>
    <comments>https://www.gfbzsblog.site/main/Articles/49#comments</comments>
    <dc:creator><![CDATA[孤芳不自赏]]></dc:creator>
    <pubDate>Sat, 24 May 2025 22:10:58 +0800</pubDate>
    <category><![CDATA[技术博客]]></category>
    <guid isPermaLink="false">https://www.gfbzsblog.site/main/Articles/49</guid>

    <description><![CDATA[从0-1 教你将全栈项目部署上线]]></description>
    <content:encoded><![CDATA[## Next.js + Spring Boot + MySQL
在我第一天将项目做好之后有打包部署的想法到部署完成起码有一周的时间了！在尝试了各种镜像之后，我最后还是选择了 Node.js 镜像。为啥呢？因为我的项目需要 node 版本在 **Node.js 18** 以上，而我用其他的镜像最高版本只能安装 **Node.js 18**，我嫌麻烦，就选择了这个。用下来目前是完全没问题的。

大家可以按照我的步骤一步一步来，别慌：

### 第一步：购买一台轻量级的服务器
---
选择可太多了！阿里云、京东云、刷抖音还是刷到什么火山引擎的... 但是我还是偏向应用腾讯云啊！别的不说，老🐎对学生优惠力度还是蛮大的，就它了！

[点我](https://cloud.tencent.com/act/campus?from=18929#step1) 可以直接跳转到购买页面。其他啥都不用管，随便装就行了，之后我教你怎么用。

买完返回腾讯云官网，点左侧那个汉堡菜单🍔，会弹出侧边栏。点 **我的资源 -> 轻量应用服务器** 就能进服务器面板了。

你看到的就是这个页面：
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/markdown%E4%B8%93%E7%94%A8/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250524213829_148.png" alt="服务器面板截图" width="700">
</center>
<br>

然后直接点这个服务器，在右边第三个栏子可以看到 **镜像**。我们这里点 **重装系统** -> **应用模板**，选择 **Node.js**，然后自己设置个密码就好了。为了防止之后的麻烦事，你可以先检查一下防火墙是不是全开了。没开的话先把它们全打开，不然到时候会访问不到！我是这么设置的，你们参考下：
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/markdown%E4%B8%93%E7%94%A8/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250524215349_149.png" alt="防火墙设置截图" width="700">
</center>
<br>

### 第二步：登录宝塔面板并且在服务器安装
---
创建账号这种事情大家都会吧？我就不啰嗦了。进去之后，点侧边栏的**安装宝塔**，下面有个 **在线一键快速安装宝塔**。看我上面那个图片（被白线遮住的），就是你需要输入的 **IP 地址**。复制过来，输入账号和你设置的密码，等着安装就好了......

> [!WARNING]
> **一定要记住！** 宝塔面板装好之后会给你面板地址和账号密码，**一定记得保存！** 不然忘掉了还得重新装一遍，特别麻烦！

装好之后，接下来的任务就简单多了。那个只能输指令的控制台拜拜了👋，图形化工具让你得心应手！

### 第三步：安装 MySQL
---
进到宝塔面板，侧边栏找到 **软件商店**，在官方应用里搜 **MySQL** 装上。装好之后，还是点侧边栏的 **数据库** 进 MySQL 操控面板。点 **添加数据库**。我这里**强烈推荐**把数据库名、用户名和密码设置成跟你电脑上的一样！不然 Spring Boot 打包一次改一次真的会吐🤮！访问权限就选 **本地服务器**。按这个步骤先建好库。

接下来要把你电脑上的数据库导进去。我这边用的是 JetBrain 家的 **DataGrip**，蛮不错的。当然你用 **IntelliJ IDEA** 也完全没问题。点右边数据库那一栏，选中你要导出的数据库右键：
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/markdown%E4%B8%93%E7%94%A8/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250524215845_150.png" alt="数据库右键菜单截图" width="500">
</center>
<br>

点 **使用 'mysqldump' 导出...**，出来个表单。那个可执行文件路径，去你 MySQL 安装目录下找叫 **mysqldump.exe** 的程序就行。下面那个就是导出 SQL 文件的位置。执行完，你的数据库就导成 SQL 文件了。

然后回到宝塔面板，点你刚新建的数据库那一栏，点 **导入 -> 从本地上传**，把 SQL 文件传上去导入到面板数据库里：
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/markdown%E4%B8%93%E7%94%A8/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250524220344_151.png" alt="数据库导入截图" width="500">
</center>
<br>

导完退出弹窗，把鼠标移到 **同步数据库** 那个按钮上，会弹出下拉菜单，点 **从面板同步到服务器**。搞定！咱们的数据库就部署完成了。

### 第四步：前端部署
---
前端其实没啥好说的。因为咱服务器已经是 Node.js 环境了（还是 18+ 的！），省事儿！
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/8b947584-b32e-4c32-8b41-1c477d351e84.png" alt="Node 版本截图" width="300">
</center>
<br>

接下来就是上传文件。展示下我的文件目录：
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/170deffa-958d-46d1-8d94-487d582ced36.png" alt="前端目录截图" width="500">
</center>
<br>

你只需要把这些文件装一个文件夹里，压缩上传就行。上传到你自己建的目录下，怕你们找不到，比如建在 `/root/blogFrontend` 就完全 OK。压缩包放这里面，解压，再把文件**都剪切出来**。点开终端，输入命令 `npm i`，系统会把依赖都下好，版本冲突啥的也不用太担心（一般没事儿）。下好依赖，打包！输入命令 `npm run build`。打包完，继续输入 `npm run start`。如果能启动没报错，OK，前端稳了，可以搞后端了。

### 第五步：后端部署
---
后端其实更简单！就是把 Spring Boot 项目打包成 JAR 上传上去就完事儿。
<br>
<center>
<img src="https://images-1359353257.cos.ap-beijing.myqcloud.com/images/a65acdd4-c5bb-48a5-9511-5da801224644.png" alt="Maven 打包截图" width="600">
</center>
<br>

双击这个（clean 可以点也可以不点），就会在项目根目录的 `blogBackend/target` 文件夹下生成一个 `jar` 包（名字看你项目配置）。把这个 JAR 包上传到服务器（比如 `/root/blogBackend.jar`）。然后在服务器上装个对应版本的 Java（比如 `yum install java-17-openjdk-devel` 或 `apt install openjdk-17-jdk`）。

启动这个 JAR 包的命令是：

```bash
java -jar /root/blogBackend.jar
//或者带生产环境配置（如果你有的话）：
//java -jar /root/blogBackend.jar --spring.profiles.active=prod
```

### 第六步：前后端服务的自动启动与守护
---
#### 前端（Next.js）用 pm2 管理
第一步，装 pm2（全局装）：
```bash
npm install -g pm2
```
然后进到你的前端目录：
```bash
cd /root/blogFrontend
```
直接用 pm2 启动服务：
```bash
pm2 start npm --name blog-frontend -- run start
```
设置 pm2 开机自启动（这样服务器重启也不用怕）：
```bash
pm2 startup  # 执行它提示的命令
pm2 save     # 保存当前运行列表
```
**pm2 常用指令备忘：**
*   `pm2 list`：看看哪些进程在跑
*   `pm2 logs blog-frontend`：盯日志（看有没有幺蛾子）
*   `pm2 restart blog-frontend`：重启前端服务（改配置啥的之后用）
*   `pm2 stop blog-frontend`：停掉服务
*   `pm2 delete blog-frontend`：删掉这个服务记录

#### 后端（Spring Boot）用 systemd（systemctl）守护
JAR 包已经上传了？那就配个文件！新建一个服务配置文件 `/etc/systemd/system/blog-backend.service`，把下面内容复制进去（用 `nano` 或 `vim` 编辑）：
```ini
[Unit]
Description=Blog Spring Boot Backend
After=network.target

[Service]
User=root
ExecStart=/usr/bin/java -jar /root/blogBackend.jar --spring.profiles.active=prod
SuccessExitStatus=143
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
```
**注意：** 把 `ExecStart` 后面的路径 `/root/blogBackend.jar` 换成你实际传上去的 JAR 包位置和名字！`--spring.profiles.active=prod` 这个参数如果你没配生产环境配置文件也可以去掉。

保存退出后，依次执行：
```bash
systemctl daemon-reload  # 重新加载配置
systemctl start blog-backend    # 启动服务
systemctl enable blog-backend   # 开机自启
```

**systemctl 常用指令备忘：**
*   `systemctl status blog-backend`：瞅瞅服务状态（绿色就是好的）
*   `systemctl restart blog-backend`：重启后端（改配置后用它）
*   `systemctl stop blog-backend`：停止服务
*   `journalctl -u blog-backend -f`：实时看后端日志（按 `Ctrl+C` 退出）

#### Nginx 反向代理配置
最后一步！给你的网站加个门面（域名访问）。宝塔面板这点做得确实人性化，基本不用手写配置。

1.  在宝塔的 **软件商店** 里安装 **Nginx**（如果还没装的话）。
2.  点侧边栏 **网站** -> **添加站点**。
3.  填上你的域名（没域名就用服务器 IP 也行），其他默认。
4.  站点添加成功后，点站点名进设置，找到SSL选项卡。
5.  点 **Let'sEncrypt**，勾选域名，点申请（免费的！）。等个几秒钟，证书自动部署好，**强制 HTTPS** 的开关也会自动打开。
6.  最后关键一步！点 **反向代理** 选项卡：
7.  代理名称随意写（比如 `blog-frontend`），目标URL填 `http://localhost:3000`（这是你前端 Next.js 默认跑的地址）。**提交！**
8.  再点 **添加反向代理**，名称写 `blog-backend`，目标URL填 `http://localhost:8080`（这是你 Spring Boot 默认跑的端口，按你项目配置改）。**提交！**

搞定！现在你的网站就能通过域名访问了，前后端请求会自动被 Nginx 转发到对应服务🎉。

### 第七步：小结
---
跟着步骤走下来，你的网站应该已经顺利上线啦🎉🎉！其实熟练了之后，整套操作十分钟就能搞定，难的是第一次摸索时踩的那些坑（懂的都懂😭）。希望这篇啰嗦的指南能帮你少走弯路，节省点头发！下次见~]]></content:encoded>
    <wfw:commentRss>https://www.gfbzsblog.site/rss/feed</wfw:commentRss>
  </item>

</channel>
</rss>