<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ranxiu</title>
        <link>https://ranxiu.vercel.app
</link>
        <description>Ranxiu's writing on software and building products</description>
        <lastBuildDate>Wed, 02 Sep 2026 08:51:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ranxiu</title>
            <url>https://ranxiu.vercel.app
/favicon.ico</url>
            <link>https://ranxiu.vercel.app
</link>
        </image>
        <copyright>All rights reserved Ranxiu 2026</copyright>
        <item>
            <title><![CDATA[Matt Pocock 的 AI 辅助编程完整工作流]]></title>
            <link>https://ranxiu.vercel.app
/blogs/matt-pocock-ai-coding-workflow</link>
            <guid>https://ranxiu.vercel.app
/blogs/matt-pocock-ai-coding-workflow</guid>
            <pubDate>Tue, 21 Jul 2026 16:00:00 GMT</pubDate>
            <description><![CDATA[从人类主导的白班规划，到 AI 执行的夜班实现，梳理一套强调对齐、反馈循环与工程品味的 AI Coding 工作流。]]></description>
            <content:encoded><![CDATA[
Matt Pocock 将 AI 辅助编程（AI Coding）的完整工作流划分为两个核心阶段：由人类主导的“白班”（Day Shift），负责规划与对齐；以及由 AI 独立运行的“夜班”（Night Shift），负责自动实现与迭代。完成自动化实现后，工作流还会回到人类手中，由开发者进行手动 QA、注入工程品味，并决定是否合并。

这套工作流的核心理念是：**不要把 AI 当作单纯的“需求转代码”编译器，而要通过明确的目标、严格的工程标准和持续的反馈循环，让人类与 AI 始终保持深度对齐。**

## 第一阶段：人类主导的规划与对齐

这一阶段的目标，是由人类明确“目的地”，并与 AI 一起制定抵达目的地的方案。开发者必须紧密参与，不能把尚未想清楚的需求直接交给 AI 实现。

### 1. 理解 LLM 的局限性

LLM 并不会随着对话变长而一直保持同样的判断力。Matt 用“聪明区”和“愚蠢区”来描述模型在不同上下文长度下的表现：

- 在对话刚开始、上下文较少时，模型通常处于“聪明区”，注意力集中，推理与决策质量较高。
- 随着 Token 不断增加，尤其当上下文接近或超过十万 Token 时，注意力会被大量历史信息拉伸，模型更容易进入“愚蠢区”，做出前后不一致或明显不合理的决定。

因此，不应无限延长同一段对话。进入新的工作阶段时，与其不断压缩旧上下文，让模型继承越来越多的“历史沉积物”，不如直接清空上下文，再把当前阶段真正需要的信息重新注入。

这种做法类似电影《记忆碎片》中的记忆重置：每个阶段都从一份清晰、可信的外部记录出发，而不是依赖模型对漫长对话的模糊记忆。

### 2. 使用“拷问我”技能澄清需求

当开发者收到一个模糊的业务构想时，不要立刻让 AI 写代码，甚至不要急着让它制定实现计划。首先应运行“Grill Me”流程，让 AI 化身为严格的产品经理和架构师，对需求进行连续追问。

这些问题应该覆盖完整业务链路，包括：

- 用户究竟要解决什么问题？
- 哪些用户会使用这个功能？
- 数据从哪里来，又会流向哪里？
- 历史数据是否需要追溯处理？
- 边界条件、失败场景和权限规则是什么？
- 本次迭代明确不处理哪些问题？

例如，需求中提到积分系统时，AI 应继续追问积分是否需要根据历史行为补发、规则变化后是否重新计算、异常数据如何处理等问题。

Grilling 的价值不只是补全需求，而是逼迫人类把隐含假设说出来。只有当开发者和 AI 对核心设计概念形成一致理解，后续的规划和实现才有可靠基础。

### 3. 编写 PRD，明确目的地

完成需求拷问后，让 AI 根据已经对齐的上下文生成产品需求文档（PRD）。这份文档是整个工作流的“目的地说明”，至少应包含：

- 产品目标与背景；
- 用户故事和验收标准；
- 关键业务规则与边界条件；
- 已确认的技术实现决策；
- 测试方向与质量要求；
- 本次迭代不包含的内容（Out of Scope）。

如果前面的 Grilling 足够深入，开发者通常不需要逐字雕琢 PRD。LLM 很擅长总结已经充分对齐的上下文，真正重要的是确认文档没有遗漏关键决策，也没有擅自扩大需求范围。

### 4. 将 PRD 拆解为看板任务

接下来，将 PRD 拆成一组可以放入看板的独立任务，并明确任务之间的阻断与依赖关系。最终得到的不是简单的线性待办列表，而是一张任务有向无环图（DAG）。

拆解时最重要的原则是采用**垂直切片（Vertical Slices）**，也就是“曳光弹（Tracer Bullets）”策略。

AI 很容易按照技术层次进行水平拆分：先完成所有数据库工作，再实现全部 API，最后才开始写前端。这种方式会把真正的集成反馈推迟到最后。一旦早期设计有误，所有层次都可能需要返工。

垂直切片要求每个任务都尽可能贯穿完整链路。第一个任务就应该连接数据库、服务层和用户可见的结果，即使它只实现了一个非常小的功能。这样，团队和 AI 能尽早验证架构、数据流与集成方式是否正确。

一个好的任务应当具备以下特征：

- 范围足够小，能由单个 Agent 独立完成；
- 交付一个可验证的端到端结果；
- 明确依赖哪些前置任务；
- 包含清晰的验收条件；
- 能在独立分支和隔离环境中实现与测试。

## 第二阶段：AI 自动实现的夜班

当 PRD 和任务 DAG 准备完成后，人类可以暂时离开键盘，把实现工作交给自动化 Agent 循环。这一阶段也被称为 AFK（Away From Keyboard）工作流。

### 5. 运行自动化 Ralph 循环

自动化脚本或 Sand Castle 一类工具会把任务 backlog、必要的代码上下文和工程约束注入隔离的 Docker 沙箱。

Agent 根据优先级和依赖关系认领一个当前可执行的 AFK 任务，为它创建独立分支，然后开始自主实现。隔离环境可以避免多个 Agent 互相污染工作区，也让失败任务更容易重试和回收。

整个 Ralph 循环大致如下：

1. 从 backlog 中选择一个没有被阻断的任务；
2. 创建独立分支和隔离沙箱；
3. 读取任务说明、代码上下文与团队规范；
4. 编写测试和业务代码；
5. 根据测试、类型检查和编译结果持续修复；
6. 全部检查通过后提交代码；
7. 将任务移交给独立审查流程。

### 6. 建立 TDD 反馈循环

高质量的自动化反馈，是 AI 编程质量的上限。必须为 Agent 提供可靠的单元测试、集成测试、类型检查、静态分析和编译检查，让它能根据明确结果自行迭代。

实现过程应遵循红—绿—重构（Red-Green-Refactor）：

1. **Red：**先编写一个能够表达预期行为、并且当前必然失败的测试；
2. **Green：**编写最小业务实现，让测试通过；
3. **Refactor：**在测试保护下改善结构，同时保持所有检查为绿色。

强制先看到测试失败非常重要。它可以证明测试确实覆盖了尚未实现的行为，降低 AI 在已有代码上补写“永远通过”的伪测试的风险。

如果出现类型错误、测试失败或编译问题，Agent 会在沙箱中读取反馈并继续修复，直到所有检查通过，再自动提交 Commit。这使实现过程形成了一个可重复、可观测的闭环，而不是一次性的代码生成。

### 7. 使用全新上下文进行自动化审查

代码完成后，不要让负责实现的 Agent 顺手审查自己的工作。此时应清空实现阶段的上下文，启用一个新的、处于“聪明区”的 LLM 进行独立审查。条件允许时，还可以使用不同模型交叉审查，例如由一个更强的模型审查另一个模型生成的代码。

审查 Agent 应获得：

- 当前任务及验收标准；
- 对应的代码变更或 Commit；
- 团队编码规范；
- 架构约束与测试要求。

全新的上下文能减少实现过程中的路径依赖和自我辩护倾向。审查 Agent 应重点检查遗漏的边界条件、错误处理、数据一致性、安全风险、测试有效性，以及代码是否破坏已有架构。

## 第三阶段：重回人类视野

自动化实现和审查结束后，工作并没有完成。代码需要回到人类开发者手中，接受手动 QA、工程品味判断和最终合并决策。

### 8. 人类手动 QA，并注入工程品味

即使所有测试都已通过，人类仍然必须亲自启动应用并进行手动 QA。自动化测试只能验证被明确表达出来的预期，无法替代开发者对整体体验、交互细节、信息层级和业务合理性的判断。

如果把规划、实现和 QA 全部交给 AI，工程很容易产出功能上“能跑”、但缺乏一致性和判断力的工业化垃圾。人类 QA 的真正作用，是向产品和代码中注入工程品味（Taste）。

开发者需要关注：

- 功能是否真正解决了原始问题；
- 操作流程是否自然，异常状态是否容易理解；
- 前端交互与视觉反馈是否符合产品整体风格；
- 实现是否引入了不必要的复杂度；
- 哪些地方虽然满足验收标准，但仍不够好。

如果 QA 发现问题，不必在当前上下文中临时指挥 AI 修补。更稳妥的做法是把问题写成新的任务，补充验收条件和阻断关系，再送回自动化实现循环。由此形成“规划—实现—审查—QA—再规划”的持续迭代。

### 9. 并行开发与最终合并

当任务被拆成带依赖关系的 DAG 后，多个没有相互阻断的垂直切片可以并行执行。团队可以同时启动多个 Docker 沙箱，让不同 Agent 各自在独立分支中开发。

并行化的前提不是简单地增加 Agent 数量，而是任务边界足够清晰、共享状态足够少、每个切片都能独立验证。否则，并行工作只会把实现成本转化为更高的冲突处理成本。

各分支完成后，可以由专门的合并 Agent 负责：

1. 按依赖顺序合并分支；
2. 处理代码冲突；
3. 对齐不同切片之间的接口和测试；
4. 运行完整测试、类型检查与构建；
5. 整理为最终 Pull Request，交由团队评审。

合并 Agent 负责机械性的整合与反馈循环，人类团队仍然负责最终的架构、产品和质量判断。

## 如何让代码库对 AI 更友好

除了工作流本身，代码库的模块设计也会直接影响 AI 的产出质量。Matt 借用了《软件设计哲学》中“深模块”和“浅模块”的概念来解释这个问题。

### 避免大量浅模块

浅模块暴露了复杂接口，却只封装很少的能力。代码库中如果存在大量碎片化的小文件和薄封装，AI 就必须跨越更多文件追踪依赖关系，消耗更多上下文，也更容易在局部修改时遗漏隐含影响。

浅模块还会制造大量不稳定的测试边界。为了测试一个简单行为，Agent 可能需要 Mock 多层依赖，最终得到脆弱且难以理解的测试。

### 推崇深模块

深模块通过简单、稳定的接口隐藏内部复杂性。调用者只需要理解少量概念，模块内部则可以包含完整的业务规则、数据处理和错误恢复逻辑。

这种结构尤其适合人类与 AI 协作：

- 人类负责设计模块边界、接口语义和关键不变量；
- AI 根据测试与约束实现“灰色盒子”内部的具体逻辑；
- 调用方只依赖稳定接口，不需要理解每个实现细节；
- 测试可以围绕明确边界验证行为，而不是大量 Mock 内部结构。

这并不意味着人类可以放弃架构设计。恰恰相反，人类需要更主动地掌控模块接口和系统边界，把适合自动化的内部实现交给 AI。这样既能提高迭代速度，也能避免开发者在快速变化的代码库中失去全局理解。

## 总结

这套工作流并不是追求“让 AI 写更多代码”，而是重新分配人类与 AI 的职责：

- 人类负责提出问题、澄清需求、设计边界、定义标准并判断质量；
- AI 负责在清晰约束下实现、测试、修复、审查和整合；
- 自动化反馈循环连接两者，使每一步都有可验证的结果；
- 通过主动重置上下文，让不同阶段的 Agent 尽量保持在最佳判断状态。

真正可靠的 AI Coding，不是一次提示词之后等待完整产品出现，而是一套由需求对齐、垂直切片、TDD、独立审查、人工 QA 和任务迭代组成的工程系统。AI 提供速度，人类提供方向、边界与品味。只有两者同时存在，自动化开发才可能持续产出值得合并的代码。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[揭秘微信“快捷登录”：为什么你不用扫码就能直接登录？]]></title>
            <link>https://ranxiu.vercel.app
/blogs/wechat-quick-login-explained</link>
            <guid>https://ranxiu.vercel.app
/blogs/wechat-quick-login-explained</guid>
            <pubDate>Sun, 01 Mar 2026 11:54:30 GMT</pubDate>
            <description><![CDATA[从 localhost 端口探测、iframe 隔离与本地客户端授权链路，拆解 PC 微信快捷登录的实现原理。]]></description>
            <content:encoded><![CDATA[
> **摘要**：你是否注意过，当电脑上挂着微信时，网页登录不再需要掏出手机扫码，而是直接弹出一个“微信快捷登录”的按钮？这背后并非魔法，而是一场浏览器与本地客户端之间精妙的“密谋”。本文将深入源码，拆解这一体验背后的技术原理。

---

## 引言：从“扫码”到“一键”的体验跃迁

　　曾几何时，PC 端网页登录微信生态的唯一方式就是：**打开网页 -> 掏出手机 -> 微信扫码 -> 手机确认**。这一套流程虽然安全，但在高频操作下显得繁琐。

　　如今，只要你的 PC 端微信处于登录状态，很多网站（如草料二维码、各类管理后台）会直接显示你的头像，并提供一个绿色的 **“微信快捷登录”** 按钮。点击即可瞬间完成登录。

![微信快捷登录入口](/images/blog/wechat-quick-login/image-20260301195638-0bb5y5y.png)

![微信快捷登录确认界面](/images/blog/wechat-quick-login/image-20260301195608-am84edf.png)

　　这种丝滑体验的背后，究竟隐藏着怎样的技术架构？是浏览器的黑科技，还是微信客户端的“特权”？让我们通过分析，一层层剥开它的内核。

---

## 第一层迷雾：消失的二维码与神秘的 Localhost

　　当我们打开一个支持微信登录的页面时，浏览器到底做了什么？

　　如果你打开浏览器的开发者工具（F12），查看网络请求（Network），你会发现一个非常有趣的请求：

```http
POST https://localhost.weixin.qq.com:14013/api/check-login
```

![浏览器向本地微信端口发起请求](/images/blog/wechat-quick-login/image-20260301195818-oqyqirw.png)

　　**关键点来了：**

1. **域名是** **​`localhost`​**​：这说明请求不是发往互联网上的微信服务器，而是发往**你自己电脑上的某个程序**。
2. **端口** **​`14013`​**​：这是微信 PC 客户端在启动时监听的一个特定端口。当你登录微信客户端可以在端口号找到这个信息，退出后没了。
   ![微信客户端监听本地端口](/images/blog/wechat-quick-login/image-20260301195924-srsz97i.png)
3. **协议是** **​`HTTPS`​**：即使是本地通信，也采用了加密传输，防止被恶意软件窃听。

　　**原理推断**：

网页正在尝试“敲门”。它在问：“嘿，本地有没有运行微信客户端？如果有，我想跟你商量个事。”

　　如果这个请求成功了，说明你的电脑里不仅装了微信，而且它正运行着且已登录。于是，网页就知道：“哦，不需要显示二维码了，可以直接走快捷通道。”

　　如果请求失败（比如没装微信，或者微信没开），网页就会 fallback（降级）处理，乖乖地去远程服务器请求一个二维码图片显示出来。

---

## 第二层架构：为什么是 iframe？

　　仔细观察页面结构，你会发现微信的登录框并不是直接写在第三方网站的 HTML 里的，而是被包裹在一个 `<iframe>` 标签中。

```html
<iframe
  src="https://open.weixin.qq.com/connect/qrconnect?..."
  allow="local-network-access"
>
</iframe>
```

![微信登录 iframe 配置](/images/blog/wechat-quick-login/image-20260301200224-rpo2vs2.png)

　　这里还有一个 `local-network-access` ，允许 iframe 访问本地网络的权限

　　很多人会问： **“引入一个 iframe 还要加载 jQuery 等库，不会拖慢网页速度吗？”**

　　确实，从资源加载的角度看，它增加了一个 HTTP 请求和一定的 JS 解析成本。但微信之所以坚持这么做，是基于两个核心考量：

### 1. 绝对的安全隔离（沙箱）

　　登录涉及用户的敏感信息（账号、授权权限）。如果微信的登录代码直接运行在第三方网站（例如 `example.com`）的 DOM 环境中，理论上存在被恶意脚本窃取数据的风险。

- **Iframe 的作用**：它创建了一个独立的**沙箱环境**。微信的代码运行在 `open.weixin.qq.com`​ 域名下，受浏览器的**同源策略**保护。第三方网站的 JS 无法读取 iframe 内的任何数据，也无法篡改登录逻辑。

### 2. 避免样式与脚本冲突

　　微信有一套自己的 UI 规范和 JS 库（如 jQuery）。第三方网站也有自己的样式。

- 如果不隔离，可能会出现“第三方网站的 CSS 把微信按钮变丑了”或者“双方 jQuery 版本冲突导致报错”的灾难现场。
- **Iframe** 彻底隔绝了样式污染，保证了登录框在任何网站上长得都一样，行为都一致。

　　**关于性能的真相**：

虽然首次加载会多一点点开销，但由于微信的静态资源（JS/CSS）都配置了**强缓存策略**。对于用户第二次访问，浏览器直接从本地磁盘读取，几乎零延迟。用微小的首屏代价换取极高的安全性和稳定性，这笔账算得非常值。

---

## 第三层核心：源码里的“快捷”逻辑

　　为了验证我们的猜想，我们深入分析了微信登录页面的前端源码（经过脱敏处理）。代码逻辑清晰地展示了“探测 - 决策 - 执行”的全过程。

### 1. 端口探测机制

　　代码中定义了一组端口列表，并尝试依次连接：

```javascript
// 源码片段示意
var PORTS = [14013, 14014, 14015, ...];

function checkLocalWeChat() {
    // 尝试向本地端口发送 POST 请求
    fetch(`https://localhost.weixin.qq.com:${port}/api/check-login`, {
        method: 'POST',
        body: JSON.stringify({
            apiname: "qrconnectchecklogin",
            jsdata: { appid: "...", redirect_uri: "..." }
        })
    })
    .then(response => {
        // 成功！说明本地微信存活
        showFastLoginButton();
    })
    .catch(() => {
        // 失败！说明没装微信或没运行
        showQRCode();
    });
}
```

![快捷登录的本地端口探测代码](/images/blog/wechat-quick-login/image-20260301200714-nvg8lot.png)

　　**这是微信在“盲测”端口。**

　　微信 PC 客户端在启动时，会尝试在本地监听一组特定的端口（例如 `14013`​, `14014`​, `13013` 等）。但是：

1. ​**端口可能冲突**​：如果 `14013`​ 被其他程序占用了，微信可能会自动切换到 `14014`。
2. ​**版本差异**：不同版本的微信客户端，默认监听的端口策略可能不同。
3. ​**动态分配**：某些情况下，端口可能是随机分配的。

　　网页端代码并不知道当前微信具体跑在哪个端口上。因此，它采用了一种 **“暴力遍历”** 的策略：这也侧面说明了为什么有时候快捷登录按钮出来得慢——因为浏览器在逐个端口“敲门”，直到找到为止。

### 2. 界面动态切换

　　一旦探测成功，JavaScript 会立即操作 DOM，隐藏二维码区域，显示快捷登录区域，并填充你的昵称和头像（这些信息也是通过本地接口获取的）。

```javascript
// 源码片段示意
if (isFastLoginEnabled) {
  $('.js_quick_login').show() // 显示快捷登录
  $('.js_normal_login').hide() // 隐藏普通扫码
  $('.js_quick_login_nickname').text(userData.nickname) // 填充昵称
}
```

### 3. 最终的“授权”动作

　　当你点击那个绿色的“微信快捷登录”按钮时，并没有发生跳转，而是再次向本地端口发送了一个指令：

```javascript
// 点击按钮触发
url: "https://localhost.weixin.qq.com:" + port + "/api/authorize",
data: {
    apiname: "qrconnectfastauthorize", // 快速授权接口
    authorize_uuid: "..."
}
```

　　收到这个指令后，**本地微信客户端**会在内部模拟一次“扫码 + 确认”的操作，直接向微信远程服务器发送授权确认。服务器验证通过后，通知网页端登录成功。

![微信客户端授权请求](/images/blog/wechat-quick-login/image-20260301201330-myjxy4q.png)

---

## 总结：以“设备信用”换“操作便捷”

　　微信的这套快捷登录机制，本质上是**将“手机扫码”这一物理动作，转化为了“本地进程间通信”的数字信号**。

　　它的核心逻辑链条是：

1. **信任链建立**：你登录 PC 微信时，已经通过了身份验证。微信服务器信任这台设备。
2. **本地桥接**：网页通过 `localhost` 接口与受信任的 PC 微信客户端建立连接。
3. **代理授权**：PC 微信客户端代替你的手机，向服务器发送“确认登录”的指令。
4. **安全隔离**：全程通过 `iframe` 沙箱保护，确保第三方网站无法触碰核心逻辑。

　　**这对我们有什么启示？**

- **用户体验至上**：减少一步操作，就能极大提升转化率。
- **安全与便捷的平衡**：通过操作系统级的进程通信和本地服务，可以在不降低安全性的前提下（甚至更高，因为依赖了设备指纹），实现极致的便捷。
- **架构解耦**：`iframe` 虽然看似“重”，但在跨域、安全、隔离场景下，依然是不可替代的黄金方案。

　　下次当你看到那个绿色的“快捷登录”按钮时，你就知道，那是你的浏览器和本地微信正在进行一次高效的“密谈”。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[复现 musicforprogramming.net 的音乐可视化效果]]></title>
            <link>https://ranxiu.vercel.app
/blogs/music-for-programming-visualizer</link>
            <guid>https://ranxiu.vercel.app
/blogs/music-for-programming-visualizer</guid>
            <pubDate>Fri, 28 Feb 2025 11:51:18 GMT</pubDate>
            <description><![CDATA[用 HTML、CSS 和 JavaScript 复现 musicforprogramming.net 的 ASCII 音乐可视化效果。]]></description>
            <content:encoded><![CDATA[
最近，我尝试复现了 [musicforprogramming.net](https://musicforprogramming.net/latest/) 网站上的音乐可视化效果。这个网站为程序员提供了一个独特的音乐播放体验，伴随着音乐的播放，屏幕上会显示动态的 ASCII 艺术效果。本文将详细介绍如何通过 HTML、CSS 和 JavaScript 来实现这一效果。

## HTML 结构

在 `index.html` 文件中，我们定义了一个简单的网页结构。页面包含一个音频播放器（目前被注释掉了）和四个 `<span>` 元素，用于显示不同颜色的 ASCII 艺术效果。此外，还有一个播放按钮，用于控制音乐的播放和暂停。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>musicforprogramming</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="pad grey pr-0 pb-0 pt-0">
        <span id="fuschia"
            class="fuschia">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><br>
        <span id="orange"
            class="orange">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><br>
        <span id="yellow"
            class="yellow">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><br>
        <span id="green" class="green">________________________________</span><br>
        <br>
        <br>
        <button id="play" class="green hover" disabled:true="false">[play]</button>
        <br>
    </div>
    <script src="index.js"></script>
</body>
</html>
```

## JavaScript 逻辑

`index.js` 文件包含了实现音乐播放和可视化效果的核心逻辑。我们使用 `AudioContext` 和 `AnalyserNode` 来处理音频数据，并通过 `requestAnimationFrame` 实现动态的可视化效果。

```javascript
// 获取页面上的元素
const F = document.getElementById("fuschia"); // 获取紫色区域的元素
const O = document.getElementById("orange"); // 获取橙色区域的元素
const Y = document.getElementById("yellow"); // 获取黄色区域的元素
const G = document.getElementById("green");   // 获取绿色区域的元素
const play = document.getElementById("play"); // 获取播放按钮元素

// 创建音频上下文和音频对象
const audioCtx = new AudioContext(); // 创建音频上下文
const audio = new Audio('./daoxiang.mp3'); // 创建音频对象，加载音频文件

// 为播放按钮添加点击事件监听器
play.addEventListener('click', () => {
    if (!audio.paused) { // 如果音频正在播放
        audio.pause(); // 暂停音频
        play.innerText = "[play]"; // 将按钮文本改为 "[play]"
        return;
    }
    audio.play(); // 播放音频
    play.innerText = "[pause]"; // 将按钮文本改为 "[pause]"
});

// 创建音频分析器
const analyser = audioCtx.createAnalyser(); // 创建音频分析器
analyser.minDecibels = -114; // 设置分析器的最小分贝值
analyser.maxDecibels = -30;  // 设置分析器的最大分贝值
analyser.fftSize = 2048;     // 设置 FFT（快速傅里叶变换）的大小
analyser.smoothingTimeConstant = .666; // 设置平滑时间常数，控制音频数据的平滑程度

// 设置音频属性
audio.type = "audio/mp3"; // 设置音频类型
audio.crossOrigin = "anonymous"; // 设置跨域属性，允许跨域请求
audio.volume = .5; // 设置音频音量
audio.autoplay = true; // 设置音频自动播放

// 当音频可以播放时，执行以下操作
audio.addEventListener('canplaythrough', () => {
    // 创建音频源节点
    const source = audioCtx.createMediaElementSource(audio);
    // 创建增益节点（用于控制音量）
    const gainNode = audioCtx.createGain();
    // 将音频源连接到分析器
    source.connect(analyser);
    // 将音频源连接到增益节点
    source.connect(gainNode);
    // 将增益节点连接到音频上下文的输出（通常是扬声器）
    gainNode.connect(audioCtx.destination);

    // 创建一个数组来存储频率数据
    const dataArray = new Uint8Array(analyser.frequencyBinCount);
    // 调用可视化函数，开始渲染音频可视化效果
    visualize(dataArray);
})

// 可视化函数，用于渲染音频的 ASCII 艺术效果
function visualize(dataArray) {
    // 获取当前频率数据
    analyser.getByteFrequencyData(dataArray);
    // 初始化四个颜色的 ASCII 字符串
    let fuschia = "", orange = "", yellow = "", green = "",
        index = 0, adjustValue = -40, multiplier = 1.08, offset = 1;

    // 遍历频率数据，生成 ASCII 字符
    for (i = 0; i < 32; i++) {
        // 计算当前字符的索引，把当前频率数据映射到字符集范围中
        index = Math.max(dataArray[~~offset + i] + adjustValue >> 3, 0);
        offset *= multiplier; // 调整偏移量
        adjustValue += 1.5;    // 调整值
        multiplier += .01;    // 调整乘数
        // 根据索引从字符集中选择字符
        fuschia += "                       _.-•:*^º'"[index];
        orange += "               _.-•:*^º'        "[index];
        yellow += "       _.-•:*^º'                "[index];
        green += "_.-•:*^º'                       "[index];
    }

    // 将生成的 ASCII 字符显示在页面上
    F.innerText = fuschia;
    O.innerText = orange;
    Y.innerText = yellow;
    G.innerText = green;

    // 使用 requestAnimationFrame 递归调用 visualize 函数，实现动态效果
    requestAnimationFrame(() => visualize(dataArray));
}
```

## 可视化效果

在 `visualize` 函数中，我们通过分析音频的频率数据，动态生成 ASCII 字符并将其显示在页面上。每个 `<span>` 元素对应不同的颜色和字符集，从而在页面上呈现出动态的视觉效果。

[观看音乐可视化效果演示](https://www.bilibili.com/video/BV1qiQEYQEaJ/)

## 总结

通过这个项目，我们成功复现了 musicforprogramming.net 的音乐可视化效果。这个项目不仅展示了如何使用 Web Audio API 处理音频数据，还展示了如何通过 JavaScript 和 HTML 实现动态的视觉效果。希望这篇文章对你有所帮助，欢迎在评论区分享你的想法和改进建议。

你可以访问 [GitHub 仓库](https://github.com/afetmin/musicforprogramming) 查看完整的代码和效果展示。

Happy coding! 🎵
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[记一次包体积超限问题]]></title>
            <link>https://ranxiu.vercel.app
/blogs/debugging-bundle-size-alert</link>
            <guid>https://ranxiu.vercel.app
/blogs/debugging-bundle-size-alert</guid>
            <pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[一次从包体积告警追查到 Resource Timing 缓冲区配置的排障记录，以及由此暴露的监控口径问题。]]></description>
            <content:encoded><![CDATA[
## 背景

某日发布节点后，监控平台发现包体积超限，增长很多，于是拉了各个业务方进行排查

## 排查

我这边用 webpack-bundle-analyzer 发现增长的体积和懒加载的代码体积刚好对得上，怀疑是不是懒加载代码加载的时间提前了，代码有循环引用之类的，导致应该懒加载的代码，提前加载进来了。但是逐一查看改动代码，没有发现循环引用的情况。

又看了下监控的数据，发现统计的数据是不稳定的，在这之前也有部分数据超限，感觉统计代码的脚本不是很稳定，于是又去看统计代码体积的脚本。
看了代码，发现统计脚本是在插件加载完成后使用 [performance.getEntriesByType](https://developer.mozilla.org/en-US/docs/Web/API/Performance/getEntriesByType) 来获取所有加载的 script 资源的体积数据。

又想到前两天上线的[全局图片监控](/blogs/global-image-monitoring)功能中，有一行代码改动了 performance 的缓存空间大小：

```ts
performance.setResourceTimingBufferSize(2000)
```

于是怀疑是不是这个改动影响了统计代码体积的脚本，于是把这段代码删掉，重新发布，发现包体积恢复“正常”。
把这个size改为一个小值，跑一下脚本，发现统计的数据更少了，说明这个改动确实影响了统计代码体积的脚本。

看了下这个 setResourceTimingBufferSize，这个接口用于设置浏览器资源计时缓冲区的大小。
这个缓冲区用于存储页面加载过程中各种资源的性能数据，例如加载时间、传输时间等。这些数据可以通过 Performance 接口进行访问和分析。

当缓冲区达到设定的最大大小时，新的资源计时数据会替换掉最早的数据。这意味着，如果缓冲区已满，最早进入缓冲区的数据会被新的数据覆盖。

这个值不手动设置默认是 250， 又手动看了下打开页面后的资源大概多少，发现数值在 400 左右，所以初始的值 250 统计的值可能是偏小的，部分数据被丢掉了，数据不准确。

把这个问题反馈上去，调整统计体积脚本的代码，设置了一个较大值去统计所有初始加载的资源，包括部分 prefetch 的懒加载代码，以调整后的资源大小作为基线去评估后面的包体积增长情况。

## 总结

这次排查总的来说还是比较幸运的，还是需要从代码和监控数据入手，大胆假设，小心求证。

1. webpack-bundle-analyzer 可以用来查看打包后的体积分布情况
2. performance.getEntriesByType 可以用来获取所有加载的资源的体积数据
3. performance.setResourceTimingBufferSize 可以用来设置 performance 的缓存空间大小，这个改动可能会影响 performance.getEntriesByType 的结果
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何实现全局图片监控]]></title>
            <link>https://ranxiu.vercel.app
/blogs/global-image-monitoring</link>
            <guid>https://ranxiu.vercel.app
/blogs/global-image-monitoring</guid>
            <pubDate>Wed, 20 Nov 2024 07:02:26 GMT</pubDate>
            <description><![CDATA[结合 PerformanceObserver、MutationObserver 与原生 API 拦截，设计一套覆盖动态图片的全局质量监控方案。]]></description>
            <content:encoded><![CDATA[
### 为什么要做这个？

1. 图片过大占用 CDN 资源
2. 拖慢加载速度，体验不好

### 要怎么做？

PerformanceObserver 可以获取已缓存图片的 entry 信息，多个相同请求 entry 只会报告一次，能够拿到 decodedBodySize。但在跨域且未使用 `Timing-Allow-Origin` ​HTTP 相应标头情况下，这个值为 0 。

主要拿到图片的原始宽高和图片显示的实际宽高，超出一定比例，图片大小超过一定阈值（比如 1M），基本可以判断图片不太符合规范，上报该数据。上报的数据可以是图片的 DOM 路径，用于定位排查。

### 实现思路

针对不同情况，有不同的监控手段

一些工具函数

```ts
const getNodeKey = (src: string, path: string[]) => `${src}::${path.join('/')}`
const getNodeName = (node: Node) => node.nodeName?.toLowerCase() ?? 'unknown'

const isElement = (node: any): node is Element =>
  !!(node.tagName && node.classList)
const isHTMLImageElement = (node: Node): node is HTMLImageElement =>
  getNodeName(node) === 'img'

const getNodePath = (node: Node, path: string[] = []): string[] => {
  if (!isElement(node)) {
    return path
  }

  const nodeName = getNodeName(node)
  const { id } = node
  const { className } = node

  const key = `${nodeName}${id ? `#${id}` : ''}${className ? `.${className}` : ''}`
  path.push(key)
  return node.parentElement ? getNodePath(node.parentElement, path) : path
}
```

利用 PerformanceObserver 获取图片大小

```ts
const perfWatchSet = new Set(['img', 'css', 'body']);
const perfObserver = new PerformanceObserver(
    list => {
        const entries = list.getEntries();
        for (let index = 0, len = entries.length; index < len; index++) {
           const entry = entries[index] as PerformanceResourceTiming;
        const { initiatorType, encodedBodySize, decodedBodySize, transferSize, name } = entry;
        const src = filterImgSrc(name);
        if (perfWatchSet.has(initiatorType) && src && decodedBodySize > 0) {
        perfEntries.set(src, entry);
        if (transferSize === 0 && encodedBodySize > 0) {
            // 处理逻辑
        }
        },
});

// 浏览器默认是250，不设置大点前面的会被丢弃，监听不到
performance.setResourceTimingBufferSize(2000);
perfObserver.observe({ type: 'resource', buffered: true });
```

#### 场景一：在 HTML DOM 上的 `<img>` 标签

分为初始处理和增量处理

```ts
// 工具函数
const getImgSrc = (node: HTMLImageElement) => node.src
const getBgSrc = (node: Element): string => {
  const { backgroundImage } = window.getComputedStyle(node)
  return (
    ((backgroundImage && regex4BgImage.exec(backgroundImage)) || [])[1] || ''
  )
}

const handleNode = (node: Node) => {
  // 处理 img.src
  handleImageElements(node)
  // 处理backgorundimg style
  handleBgImageElements(node)
}

const visitedNodeSet = new WeakSet<Node>()
const handleNodes = (nodeList: ArrayLike<Node>) => {
  for (let index = 0, len = nodeList.length; index < len; index++) {
    const node = nodeList[index]

    if (visitedNodeSet.has(node)) {
      continue
    }

    visitedNodeSet.add(node)

    if (isElement(node)) {
      handleNodes(node.children)
      handleNode(node)
    }
  }
}

// 1、初始处理
handleNodes([document.documentElement])

// 2、增量处理
const observer = new MutationObserver((mutations: MutationRecord[]) => {
  for (let index = 0, len = mutations.length; index < len; index++) {
    const mutation = mutations[index]
    handleNodes(mutation.addedNodes)
  }
})

observer.observe(document.documentElement, {
  attributes: false,
  childList: true,
  subtree: true,
})
```

还有 img.src 属性变化的情况，也需要用 MutationObserver 监听处理一下

```ts
// 省略处理流程
const imgSrcObserver = new MutationObserver(() => {})

const handleImageElements = (node: Node) => {
  if (isHTMLImageElement(node)) {
    const src = filterImgSrc(getImgSrc(node))
    if (src) {
      imgByImageElement.push({ node, src })
    }
    imgSrcObserver.observe(node, { attributeFilter: ['src'] })
    //
  }
}
```

#### 场景二：添加到 HTML DOM 上的有 backgroundImage 的标签

```ts
const handleBgImageElements = (node: Node) => {
  if (isElement(node)) {
    const src = filterImgSrc(getBgSrc(node))
    if (src) {
      imgByBgImageElement.push({ src, node })
    }
    //
  }
}

// 其他代码参考上面场景一
```

#### 场景三: 使用 API 动态创建

拦截并重写 原生方法

```ts
const oCreateElement = document.createElement.bind(document)
document.createElement = function (
  tagName: string,
  options?: ElementCreationOptions,
) {
  const newElement = oCreateElement(tagName, options)

  if (isHTMLImageElement(newElement)) {
    handleLoaded(newElement, 'createElement')
  }

  return newElement
}

const oCreateElementNS = document.createElementNS.bind(document)
document.createElementNS = function (
  namespaceURI: string,
  qualifiedName: string,
  options?: ElementCreationOptions,
) {
  const newElement = oCreateElementNS(namespaceURI, qualifiedName, options)

  if (isHTMLImageElement(newElement)) {
    handleLoaded(newElement, 'createElementNS')
  }

  return newElement
} as typeof document.createElementNS

const oImage = window.Image
window.Image = function (width?: number, height?: number) {
  const newImage: HTMLImageElement = new oImage(width, height)
  handleLoaded(newImage, 'Image')

  return newImage
} as unknown as typeof window.Image

// Preserve static properties
Object.assign(window.Image, oImage)
```

handleLoaded

```ts
function handleLoaded(node: HTMLImageElement, sourceFrom: string) {
  const loadListener = (event: Event) => {
    onLoaded(event, sourceFrom)
  }

  const errorListener = () => {
    //
  }

  node.addEventListener('load', loadListener, { once: true })
  node.addEventListener('error', errorListener, { once: true })
}
```

以上是基本框架，还有一些上报逻辑，缓存清理逻辑需要补充，完成后便可以得到一个全局的图片监控。

### 性能

因为全局的监听图片的使用，拿图片的 width、height 还有 backgroundImage 等信息时会强制触发重排重绘，会影响到加载和操作性能，所以不能大批量的全部监控，可以每天挑选部分高性能用户开启，降低影响面。

‍

‍
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[浅谈 SOLID 原则在前端的使用]]></title>
            <link>https://ranxiu.vercel.app
/blogs/solid-principles-in-frontend</link>
            <guid>https://ranxiu.vercel.app
/blogs/solid-principles-in-frontend</guid>
            <pubDate>Thu, 07 Nov 2024 07:44:04 GMT</pubDate>
            <description><![CDATA[从 JavaScript、TypeScript 和 React 场景理解 SOLID 原则如何改善前端代码的可维护性。]]></description>
            <content:encoded><![CDATA[
## 简介

`SOLID` 原则是由 `Robert C. Martin` 在 2000 年提出的一套软件开发准则，最初用于面向对象编程（OOP），旨在解决软件开发中的复杂性和维护问题。随着时间推移，它不仅在传统 OOP 语言中广泛应用，也被引入到 JavaScript 和 TypeScript 等现代编程语言和框架中，如 `React` 和 `Angular`。

SOLID 原则包括以下五个方面：

1. 单一职责原则（`Single Responsibility Principle - SRP`）
2. 开闭原则（`Open/Closed Principle - OCP`）
3. 里氏替换原则（`Liskov Substitution Principle - LSP`）
4. 接口隔离原则（`Interface Segregation Principle - ISP`）
5. 依赖倒置原则（`Dependency Inversion Principle - DIP`）

在 `JavaScript` 和 `TypeScript` 中，尽管它们是动态语言且不以类为核心，但这些原则可融入组件化和模块化架构，开发者能借此确保代码简洁、可扩展、易维护和测试

## 一、 单一职责原则 (SRP)

### 原则

一个类或模块应只有一个发生变化的原因，仅负责一项特定功能。在前端开发中，尤其是在 `React` 等组件化框架中，我们经常会看到组件承担了太多职责——不仅负责 `UI` 渲染，还处理业务逻辑和数据请求。这种情况很容易导致代码难以维护和测试，违反了 `SRP` 原则。

### 反例(js-react)

```jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData();
  }, [userId]);

  async function fetchUserData() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
  }

  return <div>{user?.name}</div>;
}
```

此例中，`UserProfile` 组件既负责 `UI` 渲染又负责数据获取，违反 `SRP` 原则，当修改数据获取或界面渲染逻辑时，可能影响组件其他部分，增加维护复杂性。

### 重构后代码

为了遵循 `SRP` 原则，我们可以将数据获取逻辑提取到一个自定义的 `Hook` 中，让组件 `UserProfile` 只关注 `UI` 渲染。

```jsx
// 自定义 Hook 用于获取用户数据
function useUserData(userId) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    async function fetchUserData() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    }
    fetchUserData();
  }, [userId]);

  return user;
}
// UI 组件
function UserProfile({ userId }) {
  const user = useUserData(userId); // 将数据获取逻辑移到了 Hook 中
  return <div>{user?.name}</div>;
}
```

通过自定义 `Hook`（`useUserData`）将数据获取逻辑与 `UI` 逻辑分离，符合 `SRP` 原则，提升了代码的可维护性和复用性。

### 反例(ts-angular)

```jsx
@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }

  updateUserProfile(userId: string, data: any) {
    // 更新用户信息并处理通知
    return this.http.put(`/api/users/${userId}`, data).subscribe(() => {
      console.log('User updated');
      alert('Profile updated successfully');
    });
  }
}
```

`UserService` 类承担多个职责，包括获取和更新用户信息以及处理通知，违背 `SRP` 原则，导致维护困难。

### 重构后代码

```jsx
@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }

  updateUserProfile(userId: string, data: any) {
    return this.http.put(`/api/users/${userId}`, data);
  }
}

// 独立的通知服务
@Injectable()
export class NotificationService {
  notify(message: string) {
    alert(message);
  }
}
```

通过将通知逻辑分离到一个独立的 `NotificationService` 中，我们遵循了 `单一职责原则（SRP）`，将通知逻辑分离到 `NotificationService` 中，遵循 `SRP` 原则，每个类职责明确，带来诸多好处：

1. 职责明确，增强可维护性。修改通知方式只需更改 `NotificationService`，不影响用户服务其他功能。
2. 提高复用性。`NotificationService` 可在其他服务或组件中复用。
3. 测试更加方便。可单独为 `UserService` 和 `NotificationService` 编写测试。
4. 代码扩展更加灵活。如需更改通知方式，只需修改或扩展 `NotificationService`。

```jsx
// **职责明确，增强可维护性：**修改通知为弹出窗口通知
@Injectable()
export class NotificationService {
  notify(message: string) {
    showModal(message);  // 假设我们有一个 showModal 函数用于展示弹窗
  }
}
```

```jsx
// 提高复用性。NotificationService 可在其他服务或组件中复用
@Injectable()
export class OrderService {
  constructor(private notificationService: NotificationService) {}
  placeOrder(orderData: any) {
    // 订单处理逻辑
    this.notificationService.notify('Order placed successfully');
  }
}
```

```jsx
// 测试更加方便。可单独为 UserService 和 NotificationService 编写测试。
it('should fetch user data', () => {
  const userService = new UserService(httpClientMock);
  userService.getUser('1').subscribe(data => {
    expect(data).toEqual(mockUserData);
  });
});
// NotificationService 测试
it('should notify the user', () => {
  const notificationService = new NotificationService();
  spyOn(window, 'alert');
  notificationService.notify('Test message');
  expect(window.alert).toHaveBeenCalledWith('Test message');
});
```

```jsx
//代码扩展更加灵活。如需更改通知方式，只需修改或扩展 NotificationService
@Injectable()
export class EmailNotificationService extends NotificationService {
  notify(message: string) {
    sendEmail(message);  // 假设我们有一个 sendEmail 函数发送邮件
  }
}
```

## 二、开闭原则（OCP）

### 原则

软件实体应能在不修改模块源代码的情况下扩展其行为，即对扩展开放，对修改封闭。

### 反例(js-react)

假设我们有一个表单验证函数，它目前工作正常，但未来可能需要添加更多的验证逻辑。

```jsx
function validateForm(values) {
  let errors = {};
  if (!values.name) {
    errors.name = "Name is required";
  }
  if (!values.email) {
    errors.email = "Email is required";
  } else if (!/\S+@\S+\.\S+/.test(values.email)) {
    errors.email = "Email is invalid";
  }
  return errors;
}
```

`validateForm` 函数包含所有验证逻辑，添加新验证规则需修改现有代码，违背 `OCP` 原则，增加维护难度和出错风险。

### 重构后代码

```jsx
// 基础验证器接口
class Validator {
  validate(value) {
    throw new Error("validate method must be implemented");
  }
}
// 具体的验证器
class RequiredValidator extends Validator {
  validate(value) {
    return value ? null : "This field is required";
  }
}
class EmailValidator extends Validator {
  validate(value) {
    return /\S+@\S+\.\S+/.test(value) ? null : "Email is invalid";
  }
}
// 验证表单函数
function validateForm(values, validators) {
  let errors = {};

  for (let field in validators) {
    const error = validators[field].validate(values[field]);
    if (error) {
      errors[field] = error;
    }
  }

  return errors;
}
// 使用示例
const validators = {
  name: new RequiredValidator(),
  email: new EmailValidator(),
};
const errors = validateForm({ name: "", email: "invalid email" }, validators);
console.log(errors);
```

通过将验证逻辑封装到独立的类（如 `RequiredValidator` 和 `EmailValidator`）中，我们使得验证器符合 **开放/封闭原则（OCP）**。现在，如果需要添加新的验证规则（例如电话号码验证），只需创建一个新的验证器类，而无需修改现有的验证逻辑；换句话说，应该允许在不修改现有核心代码的情况下添加新功能。

### 反例(ts-angular)

在 `Angular` 中，服务和组件的设计应允许添加新功能，而无需修改核心逻辑。

```jsx
export class NotificationService {
  send(type: 'email' | 'sms', message: string) {
    if (type === 'email') {
      // 发送电子邮件
    } else if (type === 'sms') {
      // 发送短信
    }
  }
}
```

在这个例子中，`NotificationService` 类违反了 **开放/封闭原则（OCP）**，因为每次需要支持新类型的通知（例如推送通知）时，必须修改 `send` 方法。这不仅会增加维护成本，还容易引发错误，尤其是当代码变得越来越复杂时。

### 重构后代码

```jsx
interface Notification {
  send(message: string): void;
}

@Injectable()
export class EmailNotification implements Notification {
  send(message: string) {
    // 发送电子邮件的逻辑
  }
}

@Injectable()
export class SMSNotification implements Notification {
  send(message: string) {
    // 发送短信的逻辑
  }
}

@Injectable()
export class NotificationService {
  constructor(private notifications: Notification[]) {}

  notify(message: string) {
    this.notifications.forEach(n => n.send(message));
  }
}
```

通过将通知发送逻辑封装到各自独立的类（`EmailNotification` 和 `SMSNotification`）中，我们实现了符合 **开放/封闭原则（OCP）** 的设计。这个设计的核心思想是，所有新功能（例如新的通知类型）都可以通过创建新的类来扩展，而不需要修改现有的 `NotificationService` 类。好处：对扩展开放，对修改封闭、提高复用性、测试更加简单、增强代码的灵活性与维护性。

---

## 三、 里氏替换原则 (LSP)

### 原则

子类型必须可以替换其基类型。派生类或组件应该能够替换基类，而不会影响程序的正确性。

### 反例(js-react)

当使用高阶组件 (`HOC`) 或有条件地渲染不同组件时，`LSP` 有助于确保所有组件的行为都可预测。

```jsx
function Button({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}
function LinkButton({ href }) {
  return <a href={href}>Click me</a>;
}
<Button onClick={() => {}} />;
<LinkButton href="/home" />;
```

这里 `Button` 和 `LinkButton` 不一致，一个用 `onClick`，一个用 `href`，替换起来比较困难。

### 重构后代码

```jsx
function Clickable({ children, onClick }) {
  return <div onClick={onClick}>{children}</div>;
}

function Button({ onClick }) {
  return <Clickable onClick={onClick}>
    <button>Click me</button>
  </Clickable>;
}

function LinkButton({ href }) {
  return <Clickable onClick={() => window.location.href = href}>
    <a href={href}>Click me</a>
  </Clickable>;
}
```

现在，`Button` 和 `LinkButton` 的行为类似，均遵循 `LSP`。

### 反例(ts-angular)

```jsx
class Rectangle {
  constructor(protected width: number, protected height: number) {}

  area() {
    return this.width * this.height;
  }
}
class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }

  setWidth(width: number) {
    this.width = width;
    this.height = width; // Breaks LSP
  }
}
```

修改 `Square` 中的 `setWidth` 违反了 `LSP`，因为 `Square` 的行为与 `Rectangle` 不同。

### 重构后代码

```jsx
class Shape {
  area(): number {
    throw new Error('Method not implemented');
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }

  area() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(private size: number) {
    super();
  }

  area() {
    return this.size * this.size;
  }
}
```

现在，`Square` 和 `Rectangle` 可以相互替代而不违反 LSP。

---

## 四、接口隔离原则 (ISP)

### 原则

客户端不应被迫依赖他们不使用的接口

### 反例(js-react)

`React` 组件有时会收到不必要的 `props`，导致代码紧密耦合且庞大。

```jsx
function MultiPurposeComponent({ user, posts, comments }) {
  return (
    <div>
      <UserProfile user={user} />
      <UserPosts posts={posts} />
      <UserComments comments={comments} />
    </div>
  );
}
```

这里，组件依赖于多个 `props`，即使它可能并不总是使用它们。

### 重构后代码

```jsx
function UserProfileComponent({ user }) {
  return <UserProfile user={user} />;
}

function UserPostsComponent({ posts }) {
  return <UserPosts posts={posts} />;
}

function UserCommentsComponent({ comments }) {
  return <UserComments comments={comments} />;
}
```

通过将组件拆分成更小的组件，每个组件仅依赖于它实际使用的数据。

### 反例(ts-angular)

```jsx
interface Worker {
  work(): void;
  eat(): void;
}

class HumanWorker implements Worker {
  work() {
    console.log('Working');
  }
  eat() {
    console.log('Eating');
  }
}

class RobotWorker implements Worker {
  work() {
    console.log('Working');
  }
  eat() {
    throw new Error('Robots do not eat'); // Violates ISP
  }
}
```

这里，`RobotWorker` 被迫实现了不相关的 `eat` 方法。

### 重构后代码

```jsx
interface Worker {
  work(): void;
}
interface Eater {
  eat(): void;
}
class HumanWorker implements Worker, Eater {
  work() {
    console.log('Working');
  }
  eat() {
    console.log('Eating');
  }
}
class RobotWorker implements Worker {
  work() {
    console.log('Working');
  }
}
```

通过分离 `Worker` 和 `Eater` 接口，我们确保客户端只依赖于它们所需要的。

---

## 五、依赖倒置原则 (DIP)

### 原则

高级模块不应依赖于低级模块。两者都应依赖于抽象（例如接口）。

### 反例(js-react)

```jsx
function fetchUser(userId) {
  return fetch(`/api/users/${userId}`).then(res => res.json());
}

function UserComponent({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user?.name}</div>;
}
```

这里，`UserComponent` 与 `fetchUser` 函数紧密耦合。

### 重构后代码

```jsx
function UserComponent({ userId, fetchUserData }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData(userId).then(setUser);
  }, [userId, fetchUserData]);

  return <div>{user?.name}</div>;
}

// Usage
<UserComponent userId={1} fetchUserData={fetchUser} />;
```

通过将 `fetchUserData` 注入组件，我们可以轻松地交换实现以进行测试或用于不同的用例。

### 反例(ts-angular)

```jsx
@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }
}

@Injectable()
export class UserComponent {
  constructor(private userService: UserService) {}

  loadUser(userId: string) {
    this.userService.getUser(userId).subscribe(user => console.log(user));
  }
}
```

`UserComponent` 与 `UserService` 紧密耦合，因此很难替换掉 `UserService`。

### 重构后代码

```jsx
interface UserService {
  getUser(userId: string): Observable<User>;
}

@Injectable()
export class ApiUserService implements UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get<User>(`/api/users/${userId}`);
  }
}
@Injectable()
export class UserComponent {
  constructor(private userService: UserService) {}

  loadUser(userId: string) {
    this.userService.getUser(userId).subscribe(user => console.log(user));
  }
}
```

通过依赖接口（`UserService`），`UserComponent` 现在与 `ApiUserService` 的具体实现分离。

---

## 结论

无论是前端的 `React`、`Angular` 等框架，还是后端的 `Node.js`，`SOLID` 原则都能作为指南，让软件架构更加稳固。`SOLID` 原则能非常有效地确保代码干净、可维护且可扩展，在 `JavaScript` 和 `TypeScript` 框架（如 `React` 和 `Angular`）中同样如此。应用这些原则，开发人员能编写灵活且可重复使用的代码，随着需求的发展，这些代码也能轻松扩展和重构。遵循 `SOLID` 原则，能让代码库变得强大，为未来的增长做好准备。


]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如果一个 npm 包不满足需求，如何修改其部分功能]]></title>
            <link>https://ranxiu.vercel.app
/blogs/how-to-customize-an-npm-package</link>
            <guid>https://ranxiu.vercel.app
/blogs/how-to-customize-an-npm-package</guid>
            <pubDate>Wed, 18 Sep 2024 06:21:04 GMT</pubDate>
            <description><![CDATA[梳理 Fork、提交 PR、补丁、模块替换与 npm overrides 等定制第三方包的方法。]]></description>
            <content:encoded><![CDATA[
### 一、使用 Fork

Fork 源代码，通过在 GitHub 上或其他托管平台上 Fork 第三方包的源代码库。对其源代码进行修改，修改完成后将修改后的包发布到 npm 上。如果你不希望它是公开的，那么你可以搭建一个 npm 的私有包。直接将项目中的包切换我们自己发布的包。

### 二、提交 PR

如果你认为你的修改对其他用户也有帮助，可以向原始包的维护者提交 Pull Request（PR）。如果 PR 被接受并合并，那么你就可以直接使用未来版本的官方包。

### 三、本地修改和补丁

这种方式可以避免直接修改 node\_modules 目录下的代码，也确保了项目的其他成员或在其他环境中部署时能够应用同样的修改。

1. 在本地对包进行修改：直接在项目的 node\_modules 目录下找到并修改对应的第三方包文件。
2. 创建补丁文件：一旦完成了必要的修改，你可以使用 git diff 或其他差异比较工具来生成一个补丁文件。

   ```bash
   git diff > patches/third-party-package.patch
   ```

3. 应用补丁：为了自动化地在每次安装依赖时应用这个补丁，你可以使用如 patch-package 这样的工具。patch-package 允许在 node\_modules 中的包上应用补丁，并且这些补丁可以和你的项目代码一起被版本控制。

安装 patch-pacakge ，然后，将应用补丁的步骤添加到 package.json 中的 scripts 字段：

```bash
"scripts": {
  "postinstall": "patch-package"
}
```

这样，每次运行 npm install 时，postinstall 脚本都会执行，自动应用保存在 patches/目录下的所有补丁。

#### 生成补丁

假设我们要要修改 axios 包，那么我们可以直接在项目的 node\_modules/axios 目录下对 axios 进行必要的修改。这些修改可以是任何东西，从简单的配置更改到函数逻辑的更新。

使用 patch-package 生成一个补丁文件。这个命令会比较你对 node\_modules 中 axios 的修改，并将这些修改保存为一个补丁文件。

```bash
npx patch-package axios
```

执行这个命令后，patch-package 会在项目的根目录下创建一个 patches 目录（如果还没有的话），并在里面生成一个名为 axios+ 版本号.patch 的文件，其中版本号是你项目中使用的 axios 的版本。

### 四、包装三方包

创建一个新的文件（如 third-party-wrapper.js），在这个文件中导入第三方包，并实现需要修改或扩展的功能。

在项目中的其他部分，你可以直接引入并使用这个封装模块，而不是直接使用第三方包。这样，你就可以利用修改后的功能，同时避免了对第三方包的直接修改。

这种方法的好处是，它提供了一个清晰的隔离层，使得第三方包的任何更新不会直接影响到你对功能的定制。同时，这也使得维护和升级第三方包变得更加容易，因为你只需要在封装层中做出相应的调整。

### 总结

以上 4 种方式，通常提交 PR 和使用 Fork 是最推荐的，因为它们可以避免维护自定义修改所带来的长期负担。但是由于业务的紧急性，我们也可以选择后两种方式。


]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[性能优化的一般性原则]]></title>
            <link>https://ranxiu.vercel.app
/blogs/general-principles-of-performance-optimization</link>
            <guid>https://ranxiu.vercel.app
/blogs/general-principles-of-performance-optimization</guid>
            <pubDate>Wed, 03 May 2023 01:17:08 GMT</pubDate>
            <description><![CDATA[从数据、时机、可维护性与投入产出比出发，讨论性能优化中值得长期遵循的原则。]]></description>
            <content:encoded><![CDATA[

## 一般性原则

### 依据数据而不是凭空猜测

这是前端性能优化的第一原则，当我们怀疑前端性能有问题的时候，应该通过测试、浏览器开发者工具、性能分析工具来分析出哪里有问题，有的放矢，而不是凭感觉、撞运气。一个前端页面有了性能问题，瓶颈有可能是 JavaScript 执行时间过长，有可能是网络请求过多或请求资源过大，有可能是 DOM 操作频繁导致重排重绘，大方向的定位可以使用浏览器开发者工具的 Performance 面板来定位，针对具体的代码块，可以使用 console.time 和 console.timeEnd 来分析执行时间。

### 忌过早优化

> The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

我并不十分清楚 Donald Knuth 说出这句名言的上下文环境，但我自己是十分认同这个观念的。在我的工作环境（以及典型的互联网应用开发）与编程模式下，追求的是快速的迭代与试错，过早的优化往往是无用功。而且，过早的优化很容易拍脑袋，优化的点往往不是真正的性能瓶颈。

### 忌过度优化

> As performance is part of the specification of a program – a program that is unusably slow is not fit for purpose

性能优化的目标是追求合适的性价比。

在不同的阶段，我们对系统的性能会有一定的要求，比如吞吐量要达到多少多少。如果达不到这个指标，就需要去优化。如果能满足预期，那么就无需花费时间精力去优化，比如只有几十个人使用的内部系统，就不用按照十万在线的目标去优化。

而且，一些优化方法是“有损”的，可能会对代码的可读性、可维护性有副作用。这个时候，就更不能过度优化。

### 深入理解业务

代码是服务于业务的，也许是服务于最终用户，也许是服务于其他程序员。不了解业务，很难理解系统的流程，很难找出系统设计的不足之处。

### 性能优化是持久战

当核心业务方向明确之后，就应该开始关注性能问题，当项目上线之后，更应该持续的进行性能检测与优化。

现在的互联网产品，不再是一锤子买卖，在上线之后还需要持续的开发，用户的涌入也会带来性能问题。因此需要自动化的检测性能问题，保持稳定的测试环境，持续的发现并解决性能问题，而不是被动地等到用户的投诉。

### 选择合适的衡量指标、测试用例、测试环境

正因为性能优化是一个长期的行为，所以需要固定衡量指标、测试用例、测试环境，这样才能客观反映性能的实际情况，也能展现出优化的效果。

衡量性能有很多指标，比如系统响应时间、系统吞吐量、系统并发量。不同的系统核心指标是不一样的，首先要明确本系统的核心性能诉求，固定测试用例；其次也要兼顾其他指标，不能顾此失彼。

测试环境也很重要，有一次突然发现我们的 QPS 高了许多，但是程序压根儿没优化，查了半天，才发现是换了一个更牛逼的物理机做测试服务器。

## 性能优化的层次

可以分为需求阶段，设计阶段，实现阶段；越上层的阶段优化效果越明显，同时也更需要对业务、需求的深入理解。

### 需求阶段

程序员的需求可能来自 PM、UI 的业务需求（或者说是功能性需求），也可能来自 Team Leader 的需求。当我们拿到一个需求的时候，首先需要的是思考、讨论需求的合理性，而不是立刻去设计、去编码。

需求是为了解决某个问题，**问题是本质，需求是解决问题的手段**。那么需求是否能否真正的解决问题，程序员也得自己去思考，产品经理（特别是知道一点技术的产品经理）的某个需求可能只是某个问题的解决方案，他认为这个方法可以解决他的问题，于是把解决方案当成了需求，而不是真正的问题。

需求讨论的前提对业务的深入了解，如果不了解业务，根本没法讨论。即使需求已经实现了，当我们发现有性能问题的时候，首先也可以从需求出发。

需求分析对性能优化有什么帮助呢，第一，为了达到同样的目的，解决同样问题，也许可以有性能更优（消耗更小）的办法。这种优化是无损的，即不改变需求本质的同时，又能达到性能优化的效果；第二种情况，有损的优化，即在不明显影响用户的体验，稍微修改需求、放宽条件，就能大大解决性能问题。PM 退步一小步，程序前进一大步。

需求讨论也有助于设计时更具扩展性，应对未来的需求变化。

### 设计阶段

高手都是花 80% 时间思考，20% 时间实现；新手写起代码来很快，但后面是无穷无尽的修 bug

设计的概念很宽泛，包括架构设计、技术选型、接口设计等等。架构设计约束了系统的扩展、技术选型决定了代码实现。编程语言、框架都是工具，不同的系统、业务需要选择适当的工具集。如果设计的时候做的不够好，那么后面就很难优化，甚至需要推到重来。

### 实现阶段

实现是把功能翻译成代码的过程，这个层面的优化，主要是针对一个调用流程，一个函数，一段代码的优化。各种 profile 工具也主要是在这个阶段生效。除了静态的代码的优化，还有编译时优化，运行时优化。后二者要求就很高了，程序员可控性较弱。

代码层面，造成性能瓶颈的原因通常是高频调用的函数、或者单次消耗非常高的函数、或者二者的结合。

## 一般性方法

### 缓存

> 没有什么性能问题是缓存解决不了的，如果有，那就再加一级缓存

缓存的本质是加速访问，访问的数据要么是其他数据的副本 -- 让数据离用户更近；要么是之前的计算结果 -- 避免重复计算.

缓存需要用空间换时间，在缓存空间有限的情况下，需要优秀的置换换算来保证缓存有较高的命中率。

### 数据的缓存

这是我们最常见的缓存形式，将数据缓存在离使用者更近的地方。比如操作系统中的 CPU cache、disk cache。对于一个 web 应用，前端会有浏览器缓存，有 CDN，有反向代理提供的静态内容缓存；后端则有本地缓存、分布式缓存。

数据的缓存，很多时候是设计层面的考虑。

对于数据缓存，需要考虑的是缓存一致性问题。对于分布式系统中有强一致性要求的场景，可行的解决办法有 lease，版本号。

### 计算结果的缓存

对于消耗较大的计算，可以将计算结果缓存起来，下次直接使用。

我们知道，对递归代码的一个有效优化手段就是缓存中间结果，lookup table，避免了重复计算。python 中的 method cache 就是这种思想.

对于可能重复创建、销毁，且创建销毁代价很大的对象，比如进程、线程，也可以缓存，对应的缓存形式如单例、资源池（连接池、线程池）。

对于计算结果的缓存，也需要考虑缓存失效的情况，对于 pure function，固定的输入有固定的输出，缓存是不会失效的。

### 并发

一个人干不完的活，那就找两个人干。并发既增加了系统的吞吐，又减少了用户的平均等待时间。

### 惰性

将计算推迟到必需的时刻，这样很可能避免了多余的计算，甚至根本不用计算。

CopyOnWrite、Dirty flag

### 批量，合并

前端开发中经常会有资源的压缩和合并。

当涉及到网络请求的时候，网络传输的时间可能远大于请求的处理时间，因此合并网络请求就很有必要。

### 更高效的实现

同一个算法，肯定会有不同的实现，那么就会有不同的性能；有的实现可能是时间换空间，有的实现可能是空间换时间，那么就需要根据自己的实际情况权衡。

程序员都喜欢早轮子，用于练手无可厚非，但在项目中，使用成熟的、经过验证的轮子往往比自己造的轮子性能更好。当然不管使用别人的轮子，还是自己的工具，当出现性能的问题的时候，要么优化它，要么替换掉他。

### 缩小解空间

缩小解空间的意思是说，在一个更小的数据范围内进行计算，而不是遍历全部数据。最常见的就是索引，通过索引，能够很快定位数据，对数据库的优化绝大多数时候都是对索引的优化。

如果有本地缓存，那么使用索引也会大大加快访问速度。不过，索引比较适合读多写少的情况，毕竟索引的构建也是需有消耗的。

## 性能优化与代码质量

衡量代码质量的标准是可读性、可维护性、可扩展性，但性能优化有可能会违背这些特性，比如为了屏蔽实现细节与使用方式，我们会可能会加入接口层（虚拟层），这样可读性、可维护性、可扩展性会好很多，但是额外增加了一层函数调用，如果这个地方调用频繁，那么也是一笔开销；又如前面提到的 C 扩展，也是会降低可维护性、

这种有损代码质量的优化，应该放到最后，不得已而为之，同时写清楚注释与文档。

为了追求可扩展性，我们经常会引入一些设计模式，如状态模式、策略模式、模板方法、装饰器模式等，但这些模式不一定是性能友好的。所以，为了性能，我们可能写出一些反模式的、定制化的、不那么优雅的代码，这些代码其实是脆弱的，需求的一点点变动，对代码逻辑可能有至关重要的影响，所以还是回到前面所说，不要过早优化，不要过度优化。



]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[前端错误监控]]></title>
            <link>https://ranxiu.vercel.app
/blogs/frontend-error-monitoring</link>
            <guid>https://ranxiu.vercel.app
/blogs/frontend-error-monitoring</guid>
            <pubDate>Thu, 09 Sep 2021 02:14:05 GMT</pubDate>
            <description><![CDATA[系统梳理运行时、资源、Promise 与框架错误的捕获边界，以及前端异常上报方式。]]></description>
            <content:encoded><![CDATA[
## 常见错误类型

| 错误           | 解释                       | 示例                                 |
| -------------- | -------------------------- | ------------------------------------ |
| SyntaxError    | 解析时发生语法错误         | const x                              |
| TypeError      | 值不是所期待的类型         | const person = 1; person.name        |
| ReferenceError | 引用未声明的变量           | x                                    |
| RangeError     | 一个值不在其所允许的范围中 | new Array(-1)                        |
| ResourceError  | 资源加载错误               | new Image().src = '/remote/null.jpg' |
| HttpError      | http 请求错误              | fetch('/remote/null')                |

## 如何捕获错误

### try/catch

能够捕获常规运行时错误，语法错误和异步错误无法捕获

```js
// 常规运行时错误，可以捕获 ✅
try {
  console.log(notdefined);
} catch(e) {
  console.log('捕获到异常：', e);
}

// 语法错误，不能捕获 ❌
try {
  const notdefined,
} catch(e) {
  console.log('捕获到异常：', e);
}

// 异步错误，不能捕获 ❌
try {
  setTimeout(() => {
    console.log(notdefined);
  }, 0)
} catch(e) {
  console.log('捕获到异常：',e);
}

```

### window.onerror

> 混合事件 GlobalEventHandlers 的 onerror 属性是用于处理 error 的事件
> Error 事件的事件处理程序，在各种目标对象的不同类型错误被触发：

> - 当 JavaScript 运行时错误（包括语法错误）发生时，window 会触发一个 ErrorEvent 接口的 error 事件，并执行 window.onerror()。
> - 当一项资源（如\<img\>或\<script\>）加载失败，加载资源的元素会触发一个 Event 接口的 error 事件，并执行该元素上的 onerror() 处理函数。这些 error 事件不会向上冒泡到 window，不过（至少在 Firefox 中）能被单一的 window.addEventListener 捕获。

```js
window.onerror = function(message, source, lineno, colno, error) { ... }
```

函数参数：

- message：错误信息（字符串）。可用于 HTML onerror=""处理程序中的 event。
- source：发生错误的脚本 URL（字符串）
- lineno：发生错误的行号（数字）
- colno：发生错误的列号（数字）
- error：Error 对象（对象）

若该函数返回 true，则阻止执行默认事件处理函数。

```js
// 常规运行时错误，可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常：',{message, source, lineno, colno, error});
}
console.log(notdefined);

// 语法错误，不能捕获 ❌
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常：',{message, source, lineno, colno, error});
}
const notdefined,

// 异步错误，可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常：',{message, source, lineno, colno, error});
}
setTimeout(() => {
  console.log(notdefined);
}, 0)

// 资源错误，不能捕获 ❌
<script>
  window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常：',{message, source, lineno, colno, error});
  return true;
}
</script>
<img src="https://unknown/image/null.png">

```

### window.addEventListener

```html
// 图片、script、css加载错误，都能被捕获 ✅
<script>
  window.addEventListener(
    'error',
    (error) => {
      console.log('捕获到异常：', error)
    },
    true,
  )
</script>
<img src="https://unknown/image/null.png" />
<script src="https://unknown/foundnull.js"></script>
<link href="https://unknown/foundnull.css" rel="stylesheet" />

// new Image错误，不能捕获 ❌
<script>
  window.addEventListener(
    'error',
    (error) => {
      console.log('捕获到异常：', error)
    },
    true,
  )
</script>
<script>
  new Image().src = 'https://unknown/image/null.png'
</script>

// fetch错误，不能捕获 ❌
<script>
  window.addEventListener(
    'error',
    (error) => {
      console.log('捕获到异常：', error)
    },
    true,
  )
</script>
<script>
  fetch('https://unknown/test')
</script>
```

### 异步错误

如果使用 try/catch 能捕获 await 的错误
普通 Promise 错误 使用 catch

### 全局捕获错误 - unhandledrejection

```js
// 全局统一处理Promise
window.addEventListener('unhandledrejection', function (e) {
  console.log('捕获到异常：', e)
})
fetch('https://unknown/test')
```

### Vue 的错误

vue 的错误会被 vue 自动捕获，并且抛给 Vue.config.errorHandler。

```js
/**
 * 全局捕获Vue错误，直接扔出给onerror处理
 */
Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })
}
```

### React 错误

react 通过 componentDidCatch，声明一个错误边界的组件

## 数据上报接口

使用 1\*1 像素的 gif 图片进行上报，有以下几点好处

- 不会阻塞页面渲染
- 图片天然跨域
- 不会携带 Cookie
- 不需等待服务器返回数据
- gif 图片所需流量最小

但数据太大，最好还是用 post
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[requestAnimationFrame 思考]]></title>
            <link>https://ranxiu.vercel.app
/blogs/thinking-about-request-animation-frame</link>
            <guid>https://ranxiu.vercel.app
/blogs/thinking-about-request-animation-frame</guid>
            <pubDate>Thu, 08 Apr 2021 02:43:16 GMT</pubDate>
            <description><![CDATA[从浏览器绘制节奏与丢帧现象理解 requestAnimationFrame 相对 setTimeout 的优势。]]></description>
            <content:encoded><![CDATA[
## 关于setTimeout
首先要明白，setTimeout 的执行只是在内存中对元素属性进行改变，这个变化必须要等到屏幕下次绘制时才会被更新到屏幕上。如果两者的步调不一致，就可能会导致中间某一帧的操作被跨越过去，而直接更新下一帧的元素。假设屏幕每隔16.7ms刷新一次，而setTimeout 每隔10ms设置图像向左移动1px， 就会出现如下绘制过程（表格）：

 - 第    0  ms：屏幕未绘制，  等待中，setTimeout 也未执行，等待中；
 - 第   10 ms：屏幕未绘制，等待中，setTimeout 开始执行并设置元素属性 left=1px；
 - 第 16.7 ms：屏幕开始绘制，屏幕上的元素向左移动了 1px， setTimeout 未执行，继续等待中；
 - 第   20 ms：屏幕未绘制，等待中，setTimeout 开始执行并设置 left=2px;
 - 第   30 ms：屏幕未绘制，等待中，setTimeout 开始执行并设置 left=3px;
 - 第33.4 ms：屏幕开始绘制，屏幕上的元素向左移动了 3px， setTimeout 未执行，继续等待中；
...

从上面的绘制过程中可以看出，屏幕没有更新 left=2px 的那一帧画面，元素直接从left=1px 的位置跳到了 left=3px 的的位置，这就是丢帧现象，这种现象就会引起动画卡顿。

## 关于requestAnimationFrame
与 setTimeout 相比，rAF 最大的优势是 由系统来决定回调函数的执行时机。具体一点讲就是，`系统每次绘制之前会主动调用 rAF 中的回调函数`，如果系统绘制率是 60Hz，那么回调函数就每16.7ms 被执行一次，如果绘制频率是75Hz，那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是，rAF 的执行步伐跟着系统的绘制频率走。`它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次`，这样就不会引起丢帧现象，也不会导致动画出现卡顿的问题。

***但是rAF并不能保证每次绘制都会执行,如果有个计算任务执行了  20ms，那么再一次回调会在 16.7 * 3 ms时执行，跳过了一次绘制。16.7+20 = 36.7，36.7+16.7 = 53.4， 16.7 * 3 = 50.1, 50.1 &lt; 53.4***

## rAF的优势
 - CPU节能：使用 setTimeout 实现的动画，当页面被隐藏或最小化时，setTimeout 仍然在后台执行动画任务，由于此时页面处于不可见或不可用状态，刷新动画是没有意义的，而且还浪费 CPU 资源。而 rAF 则完全不同，当页面处理未激活的状态下，该页面的屏幕绘制任务也会被系统暂停，因此跟着系统步伐走的 rAF 也会停止渲染，当页面被激活时，动画就从上次停留的地方继续执行，有效节省了 CPU 开销。像是埋点也可以使用这个特性，当页面不可见时，不需要上报埋点，等页面可见时再上报。

 - 函数节流：在高频率事件(resize,scroll 等)中，为了防止在一个刷新间隔内发生多次函数执行，使用 rAF 可保证每个绘制间隔内，函数只被执行一次，这样既能保证流畅性，也能更好的节省函数执行的开销。一个绘制间隔内函数执行多次时没有意义的，因为显示器每16.7ms 绘制一次，多次绘制并不会在屏幕上体现出来。
]]></content:encoded>
        </item>
    </channel>
</rss>