普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月14日掘金 前端

C# Dictionary 入门:用键值对告别低效遍历

作者 烛阴
2025年12月13日 19:25

一、Dictionary 是什么

Dictionary<TKey, TValue>

  • 一个“键值对”集合
  • 通过 Key 快速查 Value
  • 查找、添加、删除的平均复杂度接近 O(1)
  • 底层是哈希表(hash table)

典型场景:

  • 用户ID(Key) → 用户对象(Value)
  • 商品编码 → 商品信息
  • 配置项名称 → 配置值
  • 状态码 → 描述字符串

命名空间在:

using System.Collections.Generic;

二、如何创建一个 Dictionary

1. 无参构造

var dict = new Dictionary<string, int>();

含义:

  • Key 类型:string
  • Value 类型:int
  • 初始容量默认(会按需要自动扩容)

2. 指定初始容量(推荐在数据量较大时用)

var dict = new Dictionary<string, int>(capacity: 1000);

好处:

  • 减少扩容次数,性能更稳定
  • 适合“我大概知道会放多少条数据”的场景

3.直接初始化一些数据(集合初始化器)

var dict = new Dictionary<string, int>
{
    { "apple", 3 },
    { "banana", 5 },
};

还可以用索引器形式:

var dict = new Dictionary<string, int>
{
    ["apple"] = 3,
    ["banana"] = 5,
};

三、日常要用到的基本操作

var stock = new Dictionary<string, int>();

1. 添加:Add vs 直接用索引器

// 方式1:Add
stock.Add("apple", 10);
stock.Add("banana", 5);

// 方式2:索引器
stock["orange"] = 8;    // orange 不存在时 → 添加
stock["orange"] = 12;   // orange 已存在时 → 覆盖为 12

区别:

  • Add(key, value)
    • 如果 Key 已经存在,会抛 ArgumentException
    • 适合“逻辑上不该有重复 Key,有就是 Bug”的情况
  • stock[key] = value
    • Key 不存在 → 添加
    • Key 已存在 → 覆盖
    • 适合“重复 Key 表示更新”的场景

2. 读取:索引器 vs TryGetValue

// 已经有一些数据
stock["apple"] = 10;

// 方式1:索引器
int appleCount = stock["apple"];  // 如果 apple 不存在会抛 KeyNotFoundException

// 方式2:TryGetValue(推荐)
if (stock.TryGetValue("banana", out int bananaCount))
{
    Console.WriteLine($"banana: {bananaCount}");
}
else
{
    Console.WriteLine("banana 不存在");
}

使用建议:

  • 确定 Key 一定存在 → 可以直接用索引器
  • 不确定 Key 是否存在 → 优先用 TryGetValue,防止异常

3. 修改:直接给索引器赋值即可

// 已有 "apple" → 10
stock["apple"] = 15; // 覆盖为 15

如果你想“在原有值上累加”,可以搭配 TryGetValue

void AddStock(string name, int delta)
{
    stock.TryGetValue(name, out int current); // 不存在时 current=0
    stock[name] = current + delta;
}

// 用法:
AddStock("apple", 5);  // apple: 10 → 15
AddStock("pear", 3);   // pear: 0  → 3(新增)

4. 删除:Remove / Clear

// 删除某个键值对
bool removed = stock.Remove("apple");   // 删除成功返回 true,不存在返回 false

// 清空所有数据
stock.Clear();

四、几个非常重要的属性和方法

1. Count:当前元素个数

Console.WriteLine(stock.Count);

2. KeysValues:获取所有 Key / Value

var keys = stock.Keys;       // ICollection<string>
var values = stock.Values;   // ICollection<int>

stock["apple"] = 33;

foreach (var name in stock.Keys)
{
    Console.WriteLine(name);
}

foreach (var count in stock.Values)
{
    Console.WriteLine(count);
}

注意:

  • Keys / Values 是引用,不是复制品
  • 修改原字典,这两个集合感知得到变化

3. ContainsKey / ContainsValue

bool hasApple = stock.ContainsKey("apple");
bool hasCount10 = stock.ContainsValue(10);

区别与性能:

  • ContainsKey:平均 O(1),很快
  • ContainsValue:需要遍历所有 Value,O(n),大字典慎用

五、如何正确遍历 Dictionary

1. 遍历键值对

foreach (var kv in stock)
{
    Console.WriteLine($"水果:{kv.Key},库存:{kv.Value}");
}

2. 解构写法

foreach (var (name, count) in stock)
{
    Console.WriteLine($"{name} => {count}");
}

3. 只遍历 Key 或只遍历 Value

foreach (var name in stock.Keys)
{
    Console.WriteLine(name);
}

foreach (var count in stock.Values)
{
    Console.WriteLine(count);
}

4. 注意:遍历时不要直接修改字典

下面这种写法在运行时会抛 InvalidOperationException

foreach (var (name, count) in stock)
{
    if (count == 0)
    {
        stock.Remove(name); // 遍历中修改集合 → 异常
    }
}

正确写法之一:先记录要删的 Key,再统一删:

var toRemove = new List<string>();

foreach (var (name, count) in stock)
{
    if (count == 0)
        toRemove.Add(name);
}

foreach (var name in toRemove)
{
    stock.Remove(name);
}

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

昨天 — 2025年12月13日掘金 前端

从一个“不能输负号”的数字输入框说起:Web Component 数字输入组件重构实录

作者 莫石
2025年12月13日 17:56

背景

拿到需求时,因为工期还比较宽松,官网开发,我又只做其中一个组件,框架又没有定。

我决定使用原生开发,并封装为Web Component以适配任何框架(如果不能适配,说明框架有问题)。其中就有一个数字输入框带拉杆的,数字输入框和拉杆这两个东西,原生组件都有。

于是在我的要求下,ai很快给我封装了一个还可以的东西,不过后面ui又去掉拉杆了。

临近发布,要合代码了,同事才发现这个输入框有点儿问题!


起点:一段“差不多能用”的代码

这里就不赘述Web Component的开发了,因为确实很简单,看代码就行了。

这是我最初写的 NumInput 组件(为简洁省略部分 CSS):

export class NumInput extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // ... 模板里用了 <input type="number" steop="0.1">
  }
  _onNumberInput(event) {
    const newValue = this.clamp(event.target.value, this.min, this.max);
    this.setAttribute('value', newValue);
    this._dispatchEvent();
  }
}

功能上:

  • 支持 labelunitminmaxstep
  • 聚焦有高亮效果
  • 值变化会派发 value-changed 事件
  • 还有后缀单位

看起来没问题?直到女同事问我:“为什么我输 - 没反应?” 我压根儿没想过手输,因为这个组件最开始的时候,还是带拉杆的,纯鼠标操作一点儿问题没有。

当然,现在UI变了。


问题一:type="number" 不让你输 “-” 和 “.”

首先,这不是一个bug,嗯。 我仔细研究了一下原生的数字框的机制。

1 实时校验,你每输入一个字符都会校验。 2 只能输入数字相关的,0-9 . -

那么问题来了,为什么她无法输入“-” 和 “.”呢?

实际上,并不是无法输入,只是时机和位置不对。 如果输入框中已经有一个数字1了,这个时候,你就可以在这个数字前面输入一个 “-”,在它后面输入一个“.”,这两种情况(-1和1.)都是合法的。

其余情况都是不合法的,所以无法输入。

结论:type="number"校验过于严苛,鼠标操作足矣,不适合手动输入。


重构第一步:放弃 type="number",拥抱 type="text"

使用text输入框,意味着之前数字框有的功能,我现在也都要也有,这是这个手动输入的校验规则要自定义。

我改成了:

<input type="text" inputmode="decimal" />

inputmode="decimal" 能让移动端弹出带小数点的数字键盘,体验不降反升。

但光改类型不够,得自己控制输入内容。

宽松过滤,只拦非法字符

input 事件中,我只做一件事:

_onTextInput(e) {
  let val = e.target.value;
  val = val.replace(/[^0-9.\-+]/g, ''); // 只留数字、点、正负号
  // 再处理符号位置、小数点数量...
  e.target.value = val;
}

关键原则

输入过程中,只过滤,不校验
允许用户输 -.5-12.,这些“中间状态”必须保留。


重构第二步:什么时候才该“认真”校验?

要保留用户输入的字符,又要在结束后校验,一般可能会想到节流,我觉得太麻烦了,不是指实现节流麻烦,而是节流这个逻辑本身,会一直后延,让js很麻烦。

所以,怎么判断:输入结束了

我定义了两个“结束信号”:

  1. 失焦(blur
  2. 按下回车(Enter) 刚开始没想到这个,直到我输了数字没有反应,习惯性地回车了一下。

在这两个时机,调用同一个函数 _finalizeInput()

_finalizeInput() {
  const raw = this.numberEl.value.trim();
  // 如果是中间状态(如 '-'),不处理
  if (raw === '' || raw === '-' || raw === '.') return;

  let num = parseFloat(raw);
  if (isNaN(num)) {
    // 无效?回退到上次合法值
    this.numberEl.value = this.getAttribute('value') || '';
    return;
  }

  // clamp 到 [min, max]
  num = Math.min(Math.max(num, this.min), this.max);

  // 修正浮点精度(关键!)
  num = this._roundToStepPrecision(num, this.step);

  this.numberEl.value = String(num);
  this.setAttribute('value', num);
  this._dispatchEvent(); // 派发的是数字,不是字符串!
}

问题二:0.1 + 0.2 ≠ 0.3?

  • 0.1 + 0.2 → 显示 0.30000000000000004 JavaScript 的浮点精度问题是老朋友了。
    但用户不关心这些,他们只看到“我 step=0.1,怎么变出一串小数?

解法:按 step 的小数位数四舍五入

_roundToStepPrecision(value, step) {
  if (Number.isInteger(step)) return Math.round(value);
  const decimalPlaces = step.toString().split('.')[1]?.length || 0;
  const factor = 10 ** decimalPlaces;
  return Math.round(value * factor) / factor;
}
  • step=0.1 → 保留 1 位 → 0.300000000000000040.3
  • step=0.01 → 保留 2 位 → 0.13
  • step=1 → 整数 → 4

所有赋值路径(手动输入、上下键、外部设置)都走这个修正,彻底告别脏数字。


监听一下回车作为“确认”。

这里直接不仅走了失焦的逻辑,还主动失焦,避免二次“失焦”。

this.numberEl.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    this._finalizeInput();
    this.numberEl.blur(); // 自动失焦,统一交互
  }
});

其他细节打磨

  • 修复 typosteop="0.1"step="0.1"(别笑,真有人写错)
  • 移除无用代码:原始代码里声明了 rangeEl 但没用,删掉
  • 事件传数字value-changeddetail.valuenumber 类型,不是字符串
  • 外部设置值也修正setAttribute('value', '0.30000000000000004') 会自动转成 0.3

最终效果

✅ 自由输入 -.-12.
✅ 按 ↑/↓ 按 step 精确增减
✅ 按 Enter 或失焦自动校验+修正
✅ 支持 min/max 限制
✅ 移动端弹出数字键盘
✅ 事件传出干净的数字
✅ 完全 Web Component,零依赖


结语

这个组件最终代码比最初长了近一倍,但用户体验提升是质的飞跃

有时候,看似简单的功能,深挖下去全是坑。
但正是这些“小细节”,决定了产品是“能用”还是“好用”。

最后,女同事看了一眼说:“这个输入框终于不抽风了。”
我笑了笑,没告诉她,我让AI改了四版。

(完)


🧩 一文搞懂 HarmonyOS 中的 HAP、HAR 和 HSP:它们到底是什么?怎么用?

作者 90后晨仔
2025年12月13日 17:49

Snip20251213_6.png

🌟 开头一句话总结

  • HAP 是你最终安装到手机上的“App 包”;
  • HAR 是可被多个 App 共享的“动态库”(像 npm 包);
  • HSP 是只能被一个 App 内部使用的“静态库”(像私有工具函数集合)。

📦 1. HAP:HarmonyOS Ability Package(能力包)

✅ 它是什么?

HAP 是 鸿蒙应用的安装单元。你可以把它理解为 Android 的 APK 或 iOS 的 IPA。

每个鸿蒙 App 至少包含一个 HAP,通常分为两种:

类型 说明
Entry HAP 主模块,用户点击图标启动的就是它(必须有)
Feature HAP 可选功能模块,按需下载(比如“直播”、“支付”等独立功能)

📁 文件结构示例:

MyApp/
├── entry/          ← Entry HAP
│   ├── src/main/
│   └── module.json5
├── feature_live/   ← Feature HAP(可选)
└── build-profile.json5

💡 关键点:

  • 用户安装的是 .hap 文件(实际是 ZIP 格式)。
  • 一个 App 可以有多个 HAP,但只有一个 Entry。
  • HAP 里包含代码、资源、配置、Ability(页面/服务)等。

🧱 2. HAR:HarmonyOS Archive(共享归档包)

✅ 它是什么?

HAR 是 可复用的共享库,类似 Web 开发中的 npm 包,或 Android 的 AAR。

  • 多个 App 或多个 HAP 都可以引用同一个 HAR
  • 编译后生成 .har 文件。
  • 支持包含 TS/JS 代码、C++ 原生代码、资源文件(图片、字符串等)

🛠️ 什么时候用 HAR?

  • 你有一套 UI 组件库(比如 Design System)要给多个项目用;
  • 封装了网络请求、日志、加密等通用逻辑;
  • 团队协作,需要模块解耦。

📁 创建方式(DevEco Studio):

新建模块 → 选择 “Shared Library” → 生成的就是 HAR。

⚠️ 注意限制:

  • HAR 不能包含 Ability(页面/服务) —— 它只是“工具箱”,不是“应用”。
  • 资源 ID 在不同 HAR 间可能冲突(建议加前缀)。

🔒 3. HSP:HarmonyOS Static Package(静态包)

✅ 它是什么?

HSP 是 仅限当前 App 内部使用的静态库,编译时会直接“合并”进主 HAP。

  • 不会被其他 App 引用;
  • 最终不会生成独立文件,而是“内联”到 HAP 中;
  • 更安全(代码不暴露)、更轻量(无运行时开销)。

🛠️ 什么时候用 HSP?

  • 工具函数、常量、私有业务逻辑,不想对外暴露;
  • 追求极致性能,避免 HAR 的动态加载开销;
  • 模块只在本 App 内使用,无需共享。

📁 创建方式:

新建模块 → 选择 “Static Library” → 生成 HSP。


🔁 对比总结表

特性 HAP HAR HSP
用途 应用安装包 共享库 静态私有库
能否被安装 ✅ 是 ❌ 否 ❌ 否
能否包含页面(Ability) ✅ 是 ❌ 否 ❌ 否
能否被多个 App 共用 ❌ 否 ✅ 是 ❌ 否
编译产物 .hap .har 无独立文件(内联)
创建模板 Empty Ability Shared Library Static Library

🎯 实际开发建议

  1. 主 App 功能 → 用 HAP(Entry + Feature);
  2. 跨项目复用组件/逻辑 → 用 HAR
  3. 仅本项目内部工具 → 用 HSP(更安全高效);
  4. 不要把业务页面放进 HAR/HSP —— 它们只能放“辅助代码”。

🧪 举个例子

假设你在开发一个电商 App:

  • entry → 主 HAP(首页、商品列表)
  • feature_cart → 购物车 HAP(按需加载)
  • common_ui.har → 通用按钮、弹窗组件(多个 App 共用)
  • utils.hsp → 本地加密、时间格式化(仅本 App 用)

这样结构清晰,复用性强,也便于团队分工!


✅ 结语

HAP、HAR、HSP 是鸿蒙模块化开发的三大基石。
理解它们的区别,能帮你写出更规范、可维护、高性能的 HarmonyOS 应用。

📌 记住口诀:
HAP 装得下,HAR 分享它,HSP 私藏吧!

Arco Design 停摆!字节跳动 UI 库凉了?

作者 HexCIer
2025年12月13日 17:12

1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落

在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这不仅是技术实力的展示,更是企业工程化标准的话语权争夺。在这一背景下,字节跳动推出了 Arco Design,这是一套旨在挑战 Ant Design 霸主地位的“双栈”(React & Vue)企业级设计系统。

Arco Design 在发布之初,凭借其现代化的视觉语言、对 TypeScript 的原生支持以及极具创新性的“Design Lab”设计令牌(Design Token)管理系统,迅速吸引了大量开发者的关注。它被定位为不仅仅是一个组件库,而是一套涵盖设计、开发、工具链的完整解决方案。然而,就在其社区声量达到顶峰后的短短两两年内,这一曾被视为“下一代标准”的项目却陷入了令人费解的沉寂。

截至 2025 年末,GitHub 上的 Issues 堆积如山,关键的基础设施服务(如 IconBox 图标平台)频繁宕机,官方团队的维护活动几乎归零。对于数以万计采用了 Arco Design 的企业和独立开发者而言,这无疑是一场技术选型的灾难。

本文将深入剖析 Arco Design 从辉煌到停摆的全过程。我们将剥开代码的表层,深入字节跳动的组织架构变革、内部团队的博弈(赛马机制)、以及中国互联网大厂特有的“KPI 开源”文化,为您还原整件事情的全貌。

2. 溯源:Arco Design 的诞生背景与技术野心

要理解 Arco Design 为何走向衰败,首先必须理解它诞生时的宏大野心及其背后的组织推手。Arco 并不仅仅是一个简单的 UI 库,它是字节跳动在高速扩张期,为了解决内部极其复杂的国际化与商业化业务需求而孵化的产物。

1.png

2.1 “务实的浪漫主义”:差异化的产品定位

Arco Design 在推出时,鲜明地提出了“务实的浪漫主义”这一设计哲学。这一口号的提出,实际上是为了在市场上与阿里巴巴的 Ant Design 进行差异化竞争。

  • Ant Design 的困境:作为行业标准,Ant Design 以“确定性”著称,其风格克制、理性,甚至略显单调。虽然极其适合金融和后台管理系统,但在需要更强品牌表达力和 C 端体验感的场景下显得力不从心。
  • Arco 的切入点:字节跳动的产品基因(如抖音、TikTok)强调视觉冲击力和用户体验的流畅性。Arco 试图在中后台系统中注入这种基因,主张在解决业务问题(务实)的同时,允许设计师发挥更多的想象力(浪漫)。

这种定位在技术层面体现为对 主题定制(Theming) 的极致追求。Arco Design 并没有像传统库那样仅仅提供几个 Less 变量,而是构建了一个庞大的“Design Lab”平台,允许用户在网页端通过可视化界面细粒度地调整成千上万个 Design Token,并一键生成代码。这种“设计即代码”的早期尝试,是 Arco 最核心的竞争力之一。

2.2 组织架构:GIP UED 与架构前端的联姻

Arco Design 的官方介绍中明确指出,该系统是由 字节跳动 GIP UED 团队架构前端团队(Infrastructure FrontEnd Team) 联合推出的。这一血统注定了它的命运与“GIP”这个业务单元的兴衰紧密绑定。

2.2.1 GIP 的含义与地位

“GIP” 通常指代 Global Internet Products(全球互联网产品)或与之相关的国际化/商业化业务部门。在字节跳动 2019-2021 年的扩张期,这是一个充满活力的部门,负责探索除了核心 App(抖音/TikTok)之外的各种创新业务,包括海外新闻应用(BuzzVideo)、办公套件、以及各种尝试性的出海产品。

  • UED 的话语权:在这一时期,GIP 部门拥有庞大的设计师团队(UED)。为了统一各条分散业务线的设计语言,UED 团队急需一套属于自己的设计系统,而不是直接沿用外部的 Ant Design。
  • 技术基建的配合:架构前端团队的加入,为 Arco Design 提供了工程化落地的保障。这种“设计+技术”的双驱动模式,使得 Arco 在初期展现出了极高的完成度,不仅有 React 版本,还同步推出了 Vue 版本,甚至包括移动端组件库。

2.3 黄金时代的技术堆栈

在 2021 年左右,Arco Design 的技术选型是极具前瞻性的,这也是它能迅速获得 5.5k Star 的原因之一:

  • 全链路 TypeScript:所有组件均采用 TypeScript 编写,提供了优秀的类型推导体验,解决了当时 Ant Design v4 在某些复杂场景下类型定义不友好的痛点。
  • 双框架并进:@arco-design/web-react 和 @arco-design/web-vue 保持了高度统一的 API 设计和视觉风格。这对于那些技术栈不统一的大型公司极具吸引力,意味着设计规范可以跨框架复用。
  • 生态闭环:除了组件库,Arco 还发布了 arco-cli(脚手架)、Arco Pro(中后台模板)、IconBox(图标管理平台)以及 Material Market(物料市场)。这表明团队不仅是在做一个库,而是在构建一个类似 Salesforce Lightning 或 SAP Fiori 的企业级生态。

然而,正是这种庞大的生态铺设,为日后的维护埋下了巨大的隐患。当背后的组织架构发生震荡时,维持如此庞大的产品矩阵所需的资源将变得不可持续。

3. 停摆的证据:基于数据与现象的法医式分析

尽管字节跳动从未发布过一份正式的“Arco Design 停止维护声明”,但通过对代码仓库、社区反馈以及基础设施状态的深入分析,我们可以断定该项目已进入实质性的“脑死亡”状态。

3.1 代码仓库的“心跳停止”

对 GitHub 仓库 arco-design/arco-design (React) 和 arco-design/arco-design-vue (Vue) 的提交记录分析显示,活跃度在 2023 年底至 2024 年初出现了断崖式下跌。

3.png

3.1.1 提交频率分析

虽然 React 版本的最新 Release 版本号为 2.66.8(截至文章撰写时),但这更多是惯性维护。

  • 核心贡献者的离场:早期的高频贡献者(如 sHow8e、jadelike-wine 等)在 2024 年后的活跃度显著降低。许多提交变成了依赖项升级(Dependabot)或极其微小的文档修复,缺乏实质性的功能迭代。
  • Vue 版本的停滞:Vue 版本的状态更为糟糕。最近的提交多集中在构建工具迁移(如迁移到 pnpm)或很久以前的 Bug 修复。核心组件的 Feature Request 长期无人响应。

3.1.2 积重难返的 Issue 列表

Issue 面板是衡量开源项目生命力的体温计。目前,Arco Design 仓库中积累了超过 330 个 Open Issue。

  • 严重的 Bug 无人修复:例如 Issue #3091 “tree-select 组件在虚拟列表状态下搜索无法选中最后一个” 和 Issue #3089 “table 组件的 default-expand-all-rows 属性设置不生效”。这些都是影响生产环境使用的核心组件 Bug,却长期处于 Open 状态。
  • 社区的绝望呐喊:Issue #3090 直接以 “又一个没人维护的 UI 库” 为题,表达了社区用户的愤怒与失望。更有用户在 Discussion 中直言 “这个是不是 KPI 项目啊,现在维护更新好像都越来越少了”。这种负面情绪的蔓延,通常是一个项目走向终结的社会学信号。

3.2 基础设施的崩塌:IconBox 事件

如果说代码更新变慢还可以解释为“功能稳定”,那么基础设施的故障则是项目被放弃的直接证据。

  • IconBox 无法发布:Issue #3092 指出 “IconBox 无法发布包了”。IconBox 是 Arco 生态中用于管理和分发自定义图标的 SaaS 服务。这类服务需要后端服务器、数据库以及运维支持。
  • 含义解读:当一个大厂开源项目的配套 SaaS 服务出现故障且无人修复时,这不仅仅是开发人员没时间的问题,而是意味着服务器的预算可能已经被切断,或者负责运维该服务的团队(GIP 相关的基建团队)已经被解散。这是项目“断供”的最强物理证据。

3.3 文档站点的维护降级

Arco Design 的文档站点虽然目前仍可访问,但其内容更新已经明显滞后。例如,关于 React 18/19 的并发特性支持、最新的 SSR 实践指南等现代前端话题,在文档中鲜有提及。与竞争对手 Ant Design 紧跟 React 官方版本发布的节奏相比,Arco 的文档显得停留在 2022 年的时光胶囊中。

4. 深层归因:组织架构变革下的牺牲品

Arco Design 的陨落,本质上不是技术失败,而是组织架构变革的牺牲品。要理解这一点,我们需要将视线从 GitHub 移向字节跳动的办公大楼,审视这家巨头在过去三年中发生的剧烈动荡。

2.png

4.1 “去肥增瘦”战略与 GIP 的解体

2022 年至 2024 年,字节跳动 CEO 梁汝波多次强调“去肥增瘦”战略,旨在削减低效业务,聚焦核心增长点。这一战略直接冲击了 Arco Design 的母体——GIP 部门。

4.1.1 战略投资部的解散与业务收缩

2022 年初,字节跳动解散了战略投资部,并将原有的投资业务线员工分流。这一动作标志着公司从无边界扩张转向防御性收缩。紧接着,教育(大力教育)、游戏(朝夕光年)以及各类边缘化的国际化尝试业务(GIP 的核心腹地)遭遇了毁灭性的裁员。

4.1.2 GIP 团队的消失

在多轮裁员中,GIP 及其相关的商业化技术团队是重灾区。

  • 人员流失:Arco Design 的核心维护者作为 GIP UED 和架构前端的一员,极有可能在这些轮次的“组织优化”中离职,或者被转岗到核心业务(如抖音电商、AI 模型 Doubao)以保住职位。
  • 业务目标转移:留下来的人员也面临着 KPI 的重置。当业务线都在为生存而战,或者全力以赴投入 AI 军备竞赛时,维护一个无法直接带来营收的开源 UI 库,显然不再是绩效考核中的加分项,甚至是负担。

4.2 内部赛马机制:Arco Design vs. Semi Design

字节跳动素以“APP 工厂”和“内部赛马”文化著称。这种文化不仅存在于 C 端产品中,也渗透到了技术基建领域。Arco Design 的停摆,很大程度上是因为它在与内部竞争对手 Semi Design 的博弈中败下阵来。

4.2.1 Semi Design 的崛起

Semi Design 是由 抖音前端团队MED 产品设计团队 联合推出的设计系统。

  • 出身显赫:与 GIP 这个边缘化的“探索型”部门不同,Semi Design 背靠的是字节跳动的“现金牛”——抖音。抖音前端团队拥有极其充裕的资源和稳固的业务地位。
  • 业务渗透率:Semi Design 官方宣称支持了公司内部“近千个平台产品”,服务 10 万+ 用户。它深度嵌入在抖音的内容生产、审核、运营后台中。这些业务是字节跳动的生命线,因此 Semi Design 被视为“核心资产”。

4.2.2 为什么 Arco 输了?

在资源收缩期,公司高层显然不需要维护两套功能高度重叠的企业级 UI 库。选择保留哪一个,不仅看技术优劣,更看业务绑定深度。

  • 技术路线之争:Semi Design 在 D2C(Design-to-Code)领域走得更远,提供了强大的 Figma 插件,能直接将设计稿转为 React 代码。这种极其强调效率的工具链,更符合字节跳动“大力出奇迹”的工程文化。
  • 归属权:Arco 属于 GIP,GIP 被裁撤或缩编;Semi 属于抖音,抖音如日中天。这几乎是一场没有悬念的战役。当 GIP 团队分崩离析,Arco 自然就成了没人认领的“孤儿”。

4.3 中国大厂的“KPI 开源”陷阱

Arco Design 的命运也折射出中国互联网大厂普遍存在的“KPI 开源”现象。

  • 晋升阶梯:在阿里的 P7/P8 或字节的 2-2/3-1 晋升答辩中,主导一个“行业领先”的开源项目是极具说服力的业绩。因此,很多工程师或团队 Leader 会发起此类项目,投入巨大资源进行推广(刷 Star、做精美官网)。
  • 晋升后的遗弃:一旦发起人成功晋升、转岗或离职,该项目的“剩余价值”就被榨干了。接手的新人往往不愿意维护“前人的功劳簿”,更愿意另起炉灶做一个新的项目来证明自己。
  • Arco 的轨迹:Arco 的高调发布(2021年)恰逢互联网泡沫顶峰。随着 2022-2024 年行业进入寒冬,晋升通道收窄,维护开源项目的 ROI(投入产出比)变得极低,导致项目被遗弃。

5. 社区自救的幻象:为何没有强有力的 Fork?

面对官方的停摆,用户自然会问:既然代码是开源的(MIT 协议),为什么没有人 Fork 出来继续维护?调查显示,虽然存在一些零星的 Fork,但并未形成气候。

5.png

5.1 Fork 的现状调查

通过对 GitHub 和 Gitee 的检索,我们发现了一些 Fork 版本,但并未找到具备生产力的社区继任者。

  • vrx-arco:这是一个名为 vrx-arco/arco-design-pro 的仓库,声称是 "aro-design-vue 的部分功能扩展"。然而,这更像是一个补丁集,而不是一个完整的 Fork。它主要解决特定开发者的个人需求,缺乏长期维护的路线图。
  • imoty_studio/arco-design-designer:这是一个基于 Arco 的表单设计器,并非组件库本身的 Fork。
  • 被动 Fork:GitHub 显示 Arco Design 有 713 个 Fork。经抽样检查,绝大多数是开发者为了阅读源码或修复单一 Bug 而进行的“快照式 Fork”,并没有持续的代码提交。

5.2 为什么难以 Fork?

维护一个像 Arco Design 这样的大型组件库,其门槛远超普通开发者的想象。

  1. Monorepo 构建复杂度:Arco 采用了 Lerna + pnpm 的 Monorepo 架构,包含 React 库、Vue 库、CLI 工具、图标库等多个 Package。其构建脚本极其复杂,往往依赖于字节内部的某些环境配置或私有源。外部开发者即使拉下来代码,要跑通完整的 Build、Test、Doc 生成流程都非常困难。
  2. 生态维护成本:Arco 的核心优势在于 Design Lab 和 IconBox 等配套 SaaS 服务。Fork 代码容易,但 Fork 整个后端服务是不可能的。失去了 Design Lab 的 Arco,就像失去了灵魂的空壳,吸引力大减。
  3. 技术栈锁定:Arco 的一些底层实现可能为了适配字节内部的微前端框架或构建工具(如 Modern.js)做了特定优化,这增加了通用化的难度。

因此,社区更倾向于迁移,而不是接盘

6. 用户生存指南:现状评估与迁移策略

对于目前仍在使用 Arco Design 的团队,局势十分严峻。随着 React 19 的临近和 Vue 3 生态的演进,Arco 将面临越来越多的兼容性问题。

6.1 风险评估表

风险维度 风险等级 具体表现
安全性 🔴 高危 依赖的第三方包(如 lodash, async-validator 等)若爆出漏洞,Arco 不会发版修复,需用户手动通过 resolutions 强行覆盖。
框架兼容性 🔴 高危 React 19 可能会废弃某些 Arco 内部使用的旧生命周期或模式;Vue 3.5+ 的新特性无法享受。
浏览器兼容性 🟠 中等 新版 Chrome/Safari 的样式渲染变更可能导致 UI 错位,无人修复。
基础设施 ⚫ 已崩溃 IconBox 无法上传新图标,Design Lab 可能随时下线,导致主题无法更新。

6.png

6.2 迁移路径推荐

方案 A:迁移至 Semi Design(推荐指数:⭐⭐⭐⭐)

如果你是因为喜欢字节系的设计风格而选择 Arco,那么 Semi Design 是最自然的替代者。

  • 优势:同为字节出品,设计语言的命名规范和逻辑有相似之处。Semi 目前维护活跃,背靠抖音,拥有强大的 D2C 工具链。
  • 劣势:API 并非 100% 兼容,仍需重构大量代码。且 Semi 主要是 React 优先,Vue 生态支持相对较弱(主要靠社区适配)。

7.png

方案 B:迁移至 Ant Design v5/v6(推荐指数:⭐⭐⭐⭐⭐)

如果你追求极致的稳定和长期的维护保障,Ant Design 是不二之选。

  • 优势:行业标准,庞大的社区,Ant Group 背书。v5 版本引入了 CSS-in-JS,在定制能力上已经大幅追赶 Arco 的 Design Lab。
  • 劣势:设计风格偏保守,需要设计师重新调整 UI 规范。

方案 C:本地魔改(推荐指数:⭐)

如果项目庞大无法迁移,唯一的出路是将 @arco-design/web-react 源码下载到本地 packages 目录,作为私有组件库维护。

  • 策略:放弃官方更新,仅修复阻塞性 Bug。这需要团队内有资深的前端架构师能够理解 Arco 的源码。

4.png

7. 结语与启示

Arco Design 的故事是现代软件工程史上的一个典型悲剧。它证明了在企业级开源领域,康威定律(Conway's Law) 依然是铁律——软件的架构和命运取决于开发它的组织架构。

当 GIP 部门意气风发时,Arco 是那颗最耀眼的星,承载着“务实浪漫主义”的理想;当组织收缩、业务调整时,它便成了由于缺乏商业造血能力而被迅速遗弃的资产。对于技术决策者而言,Arco Design 的教训是惨痛的:在进行技术选型时,不能仅看 README 上的 Star 数或官网的精美程度,更要审视项目背后的组织生命力维护动机

8.png

目前来看,Arco Design 并没有复活的迹象,社区也没有出现强有力的接棒者。这套组件库正在数字化浪潮的沙滩上,慢慢风化成一座无人问津的丰碑。

做后台系统别再只会单体架构了,微前端才是更优解

作者 初辰ge
2025年12月13日 17:10

在后台系统开发领域,传统的单体架构已经无法满足现代企业的复杂需求。本文将深入探讨为什么微前端架构是后台系统的更优选择,并通过一个完整的开源项目案例,展示如何构建高性能、可扩展的微前端后台系统。

前言:单体架构的那些坑

说实话,做后台系统开发这么多年,单体架构的痛点真是深有体会:

  • 代码耦合严重:多个业务模块混杂在一起,修改一个功能可能影响到其他模块
  • 代码无法隔离:所有代码都在一个仓库中,无法进行物理隔离,权限管理困难
  • 构建速度缓慢:随着业务增长,项目体积越来越大,构建时间从几分钟到十几分钟不等
  • 技术栈锁定:整个项目被限制在单一技术栈,无法灵活选择最适合的技术方案
  • 团队协作困难:多人同时开发时,代码冲突频发,发布需要协调所有模块
  • 部署风险高:任何小改动都需要重新部署整个应用,风险巨大

这些问题的根源在于传统的单体架构模式。在单体架构中,所有的业务功能都被打包在一个巨大的代码库中,就像一个臃肿的巨人,行动迟缓且容易摔倒。

微前端:一种可行的解决方案

微前端(Micro Frontends)把前端应用拆分成多个小型、独立的部分。每个部分都能独立开发、测试、部署,最后组合成一个完整的应用。

这样做的好处很明显:

  • 独立部署:改一个模块不用重新发布整个系统
  • 团队自治:每个团队管好自己的模块就行
  • 渐进迁移:不用一次性重写,可以慢慢来
  • 故障隔离:一个模块崩了不会拖垮整个应用

一个实际案例:PbstarAdmin

为了让大家看看微前端在后台系统中怎么用,我分享一下最近做的一个项目 —— PbstarAdmin

这个项目用到了腾讯的 wujie 微前端框架,解决了一些实际开发中的痛点。

代码隔离这块是怎么做的?

PbstarAdmin 用了一个比较实用的办法:Git子模块 + Rsbuild构建 双重隔离。

Git子模块隔离

简单说就是把代码分成两类:

  • 内部子应用:放在主仓库的 apps/ 目录下,适合核心业务,改起来方便
  • 外部子应用:用Git子模块管理,完全独立的仓库,适合第三方模块或者需要权限控制的代码

Rsbuild构建隔离

每个子应用都有自己的构建配置:

  • 独立的构建配置和输出目录
  • 子应用之间没有依赖耦合
  • 可以独立部署和版本管理

这样做的好处是实实在在的:不同团队负责不同模块,互不干扰;出问题也容易定位。

项目特色

PbstarAdmin 这个项目主要解决了几个实际问题:

  • 微前端架构:用腾讯 wujie 框架,支持动态加载子应用
  • 模块化设计:pnpm monorepo 管理,支持内外部子应用
  • 组件复用:共享组件库,统一别名引用
  • 工程化工具:CLI 工具链简化开发流程
  • 高性能构建:基于 Rsbuild,支持多环境配置

技术选型

技术选型比较务实,都是现在主流的方案:

  • Vue 3: Composition API 开发体验不错
  • Pinia:状态管理比 Vuex 简洁
  • Element Plus:组件库成熟稳定
  • Rsbuild:基于 Rspack,构建速度很快
  • pnpm:monorepo 管理很方便
  • wujie:腾讯的微前端方案,相对成熟

架构设计

项目结构

整个项目结构比较清晰:

pbstar-admin/
├── main/                      # 主应用(基座)
├── apps/                      # 子应用目录
│   ├── app-common/            # 公共子应用模块
│   ├── system/                # 系统管理应用
│   ├── example/               # 示例应用
│   ├── equipment/             # 设备管理应用(外部子应用)
│   └── apps.json              # 子应用配置
├── components/                # 共享组件库
├── assets/                    # 共享资源
└── tools/                     # 工具模块(CLI)

微应用配置

apps/apps.json 中配置各个微应用的信息:

[
  {
    "key": "system",
    "devPort": 8801,
    "proUrl": "http://pbstar-admin-system.pbstar.cn/"
  },
  {
    "key": "example",
    "devPort": 8802,
    "proUrl": "http://pbstar-admin-example.pbstar.cn/"
  },
  {
    "key": "equipment",
    "devPort": 8803,
    "proUrl": "http://pbstar-admin-equipment.pbstar.cn/"
  }
]

主应用核心代码

主应用负责整体布局、导航菜单管理和微应用加载:

// main/src/stores/apps.js
export const useAppsStore = defineStore("apps", () => {
  const myApps = ref([]); // 存储用户的应用
  const appId = ref(0); // 存储当前激活的应用

  const setApps = (apps) => {
    myApps.value = apps.map((item) => {
      return {
        id: item.id,
        key: item.key,
        name: item.name,
        icon: item.icon,
        group: item.group,
        navs: [],
        navsTree: [],
      };
    });
  };

  const setAppId = async ({ id, key }) => {
    let aId = 0;
    if (id) {
      aId = id;
    } else if (key) {
      const app = myApps.value.find((item) => item.key === key);
      if (app) aId = app.id;
    }
    if (aId) {
      const navRes = await request.get({
        url: "/main/getMyNavListByAppId",
        data: { appId: aId },
      });
      if (navRes.code !== 200) {
        ElMessage.error("获取应用导航失败!请稍后重试");
        return false;
      }
      setAppNavs(aId, navRes.data);
    }
    appId.value = aId;
    return true;
  };

  return {
    appId,
    setApps,
    setAppId,
    getApp,
    getApps,
    hasAppNav,
  };
});

导航菜单管理

// main/src/components/layout/layout.js
export function useNavMenu() {
  const router = useRouter();
  const route = useRoute();
  const appsStore = useAppsStore();

  const activeIndex = ref("1");
  const list = ref([]);
  const listTree = ref([]);

  const updateNavData = () => {
    if (appsStore.appId) {
      const app = appsStore.getApp();
      if (!app) return;
      list.value = app.navs;
      listTree.value = app.navsTree;
    } else {
      list.value = [
        {
          id: 1,
          name: "首页",
          url: "/admin/pHome",
          icon: "el-icon-house",
        },
      ];
      listTree.value = list.value;
    }
  };

  const selectNav = (val) => {
    activeIndex.value = val;
    const url = list.value.find((item) => item.id.toString() === val)?.url;
    if (url) {
      router.push(url);
    }
  };

  return {
    listTree,
    activeIndex,
    selectNav,
    updateNavData,
    updateActiveIndex,
  };
}

构建配置

使用 Rsbuild 进行高性能构建配置:

// rsbuild.config.mjs
export default defineConfig({
  plugins: [pluginVue(), pluginSass(), distZipPlugin()],
  output: { legalComments: "none" },
  resolve: {
    alias: {
      "@Pcomponents": "./components",
      "@Passets": "./assets",
    },
  },
  server: {
    proxy: {
      "/api": {
        target: import.meta.env.PUBLIC_API_BASE_URL,
        pathRewrite: { "^/api": "" },
        changeOrigin: true,
      },
    },
  },
  environments: {
    main: mainConfig,
    ...Object.fromEntries(apps.map((app) => [app.key, createAppConfig(app)])),
  },
});

CLI 工具开发

提供完整的 CLI 工具链,简化开发流程:

// tools/cli/dev.mjs
const list = ["main", ...apps.map((item) => item.key)];

program
  .version("1.0.0")
  .description("启动应用模块")
  .action(async () => {
    try {
      const answers = await inquirer.prompt([
        {
          type: "list",
          name: "appKey",
          message: "请选择要启动的应用模块:",
          choices: list,
        },
      ]);
      const { appKey } = answers;
      // 构建启动命令
      let command = "";
      if (appKey === "main") {
        command = "rsbuild dev --environment main --port 8800 --open";
      } else {
        const app = apps.find((item) => item.key === appKey);
        command = `rsbuild dev --environment ${appKey} --port ${app.devPort}`;
      }
      execSync(command, { stdio: "inherit", cwd: "../" });
    } catch (err) {
      console.error(chalk.red("Error:"), err);
      process.exit(1);
    }
  });

代码隔离的终极解决方案

传统方案的局限性

之前用过一些微前端方案,发现隔离做得并不好:

  • 代码都在一起:所有子应用代码混在一个仓库,权限控制很麻烦
  • 依赖经常冲突:这个子应用要Vue3,那个要Vue2,构建时各种问题
  • 构建互相影响:一个子应用构建失败了,整个项目都跑不起来
  • 版本管理混乱:没法单独给某个业务模块打版本标签

PbstarAdmin的双重隔离机制

PbstarAdmin 用了 Git子模块 + Rsbuild构建 的双重隔离,算是把代码隔离做到了物理层面。

1. Git子模块隔离

# .gitmodules 配置,其实就是普通的git子模块
[submodule "apps/equipment"]
path = apps/equipment
url = https://github.com/pbstar/pbstar-admin-quipment.git

内部子应用(in类型)

  • 代码放在主仓库里,适合核心业务
  • 团队协作方便,代码复用容易
  • 构建起来也快

外部子应用(out类型)

  • 用Git子模块管理,完全独立的仓库
  • 适合业务团队或者需要保密的模块
  • 版本控制完全独立

2. Rsbuild构建隔离

// rsbuild.config.mjs - 给每个子应用单独的配置
const createAppConfig = (app) => {
  const basePath = `./apps/${app.key}`;
  return {
    source: {
      entry: { index: `${basePath}/src/main.js` },
    },
    output: {
      distPath: { root: `./build/dist/${app.key}` },
    },
    resolve: {
      alias: {
        "@": basePath + "/src",
      },
    },
    plugins: [
      checkUniqueKeyPlugin({
        checkPath: `${basePath}/src`,
        checkKeys: ["btnkey"],
      }),
    ],
  };
};

子应用创建流程

// tools/cli/create.mjs - 智能创建子应用
const answers = await inquirer.prompt([
  {
    type: "list",
    name: "appType",
    message: "子应用类型:",
    choices: ["in", "out"],
  },
  {
    type: "input",
    name: "appKey",
    message: "子应用Key:",
    validate: (input) => {
      if (!/^[a-z0-9-]+$/.test(input)) {
        return "子应用Key只能包含小写字母、数字和连字符";
      }
      return true;
    },
  },
]);

// 外部子应用就加个git子模块
if (appType === "out" && gitUrl) {
  execSync(`git submodule add ${gitUrl} apps/${appKey}`, {
    cwd: path.join(__dirname, "../../"),
    stdio: "inherit",
  });
}

代码隔离的实际效果

用了双重隔离后,确实比传统单体架构方便不少:

权限管理

  • 仓库级别权限:外部子应用可以单独设置Git权限
  • 代码审查隔离:每个子应用可以有自己的Code Review流程
  • 敏感代码保护:核心业务代码可以放在内部子应用中

依赖管理

// 每个子应用可以有自己的依赖,不会冲突
{
  "name": "system-subapp",
  "dependencies": {
    "vue": "^3.5.18",
    "element-plus": "^2.10.7",
    // 子应用特定的依赖
    "echarts": "^5.4.0"
  }
}

// 另一个子应用可以用不同版本
{
  "name": "equipment-subapp",
  "dependencies": {
    "vue": "^3.5.18",
    "element-plus": "^2.8.0",
    "echarts": "^4.9.0"  // 版本不一样,但不会冲突
  }
}

独立部署

# 用ptools构建指定子应用(推荐)
pnpm run build
# 选择要构建的子应用,比如equipment

Ptools:CLI工具链

PbstarAdmin 还有个特色是 Ptools - 一套CLI工具链,把复杂的构建流程都封装起来了。

Ptools的核心命令

# 启动开发环境 - 会让你选择子应用
pnpm run dev

# 构建指定子应用 - 交互式选择
pnpm run build

# 创建新的子应用 - 引导式创建
pnpm run create

# 添加依赖包 - 精确到具体工程
pnpm run add

# 移除依赖包 - 清理依赖
pnpm run remove

为什么用Ptools而不是直接敲命令

直接敲命令的问题

# 要记住复杂的命令和参数
rsbuild build --environment equipment --port 8803

# 容易敲错,还得手动指定端口和环境
rsbuild dev --environment system --port 8801

Ptools的交互方式

// tools/cli/build.mjs - 构建命令其实就是帮你选一下
const list = ["main", ...apps.map((item) => item.key)];

program
  .version("1.0.0")
  .description("构建应用模块")
  .action(async () => {
    const answers = await inquirer.prompt([
      {
        type: "list",
        name: "appKey",
        message: "请选择要构建的应用模块:",
        choices: list, // 自动读取所有可用模块
      },
    ]);
    const { appKey } = answers;
    // 自动构建正确的环境和配置
    const command = `rsbuild build --environment ${appKey}`;
    execSync(command, { stdio: "inherit", cwd: "../" });
  });

Ptools的好处

  1. 不用记配置:开发者不用了解底层的Rsbuild配置
  2. 不会选错:自动发现可用的子应用,避免手打错误
  3. 统一入口:所有操作都通过统一的CLI,比较好记
  4. 减少出错:内置参数验证和错误处理
  5. 流程统一:确保团队成员用相同的流程

依赖管理

// tools/cli/add.mjs - 添加依赖包
const answers = await inquirer.prompt([
  {
    type: "list",
    name: "appKey",
    message: "请选择要添加依赖包的工程:",
    choices: [
      "全局工程",
      "assets",
      "components",
      "tools",
      "main",
      ...apps.map((item) => item.key),
    ],
  },
  {
    type: "input",
    name: "packageName",
    message: "请输入要添加的依赖包名称:",
  },
  {
    type: "list",
    name: "packageType",
    message: "请选择要添加的依赖包类型:",
    choices: ["dependencies", "devDependencies"],
  },
]);

通过Ptools,PbstarAdmin把从创建到构建的流程都标准化了。

故障隔离

// 子应用A构建出错了,不会影响子应用B
const appConfigs = {
  system: createAppConfig({ key: "system" }), // ✅ 正常构建
  equipment: createAppConfig({ key: "equipment" }), // ❌ 构建失败
  example: createAppConfig({ key: "example" }), // ✅ 不受影响
};

实际使用效果

开发体验

  • 独立开发:每个团队可以独立开发自己的微应用,互不干扰
  • 构建速度:单个微应用构建比整个项目快很多
  • 热更新:修改一个微应用不会影响其他应用,热更新很快

部署运维

  • 独立部署:每个微应用可以独立部署,降低风险
  • 灰度发布:支持微应用级别的灰度发布
  • 故障隔离:单个微应用出错不会影响整个系统

团队协作

  • 团队自治:每个团队负责自己的微应用,职责清晰
  • 技术选型自由:不同团队可以选择最适合的技术栈
  • 并行开发:多个团队可以并行开发,提高效率

快速开始

想要体验这个微前端后台系统?只需要简单的几步:

# 克隆项目
git clone https://github.com/pbstar/pbstar-admin.git

# 进入项目目录
cd pbstar-admin

# 克隆外部子应用仓库(可选)
git submodule update --init

# 安装依赖
pnpm install

# 使用Ptools启动开发环境(推荐方式)
pnpm run dev
# 交互式选择要启动的子应用

# 使用Ptools构建项目
pnpm run build
# 选择要构建的子应用

# 创建新的子应用
pnpm run create

# 添加依赖包
pnpm run add

# 移除依赖包
pnpm run remove

Ptools使用示例

# 开发环境 - 交互式选择子应用
$ pnpm run dev
? 请选择要启动的应用模块: (Use arrow keys)
❯ main
  system
  example
  equipment

# 构建指定子应用
$ pnpm run build
? 请选择要构建的应用模块: (Use arrow keys)
❯ main
  system
  example
  equipment

# 添加依赖包到指定工程
$ pnpm run add
? 请选择要添加依赖包的工程: (Use arrow keys)
❯ 全局工程
  assets
  components
  tools
  main
  system
  example
  equipment
? 请输入要添加的依赖包名称: axios
? 请选择要添加的依赖包类型: (Use arrow keys)
❯ dependencies
  devDependencies

总结

单体架构就像一搜巨大的航空母舰,虽然功能强大,但转向困难,维护成本高。而微前端架构就像一支现代化的舰队,每艘舰艇都有自己的使命,既能独立作战,又能协同配合。

通过 PbstarAdmin 的实践,我发现微前端在后台系统中的优势确实很明显:

  • 开发效率:构建速度快了很多,热更新基本是秒级
  • 部署运维:可以独立部署,不用每次都全量发布
  • 团队协作:各团队负责自己的模块,冲突少了很多
  • 系统稳定性:一个模块出问题不会拖垮整个系统

当然,微前端也有它的复杂性,比如通信机制、状态同步等问题。但总的来说,对于大型后台系统,微前端是一个值得考虑的方向。

如果你也在做后台系统,建议可以试试看微前端的思路,或许能解决你当前遇到的一些痛点。

相关资料

项目地址和文档都整理在这里了,有兴趣的可以看看:


💡 这里是初辰,一个有理想的切图仔!

🎉 如果本文对你有帮助,别忘了点赞、收藏、评论哦!

给项目点个Star,支持开源精神,让更多人发现这个优秀的微前端解决方案!

斐波那契数列:从递归到缓存优化的极致拆解

作者 闲云ing
2025年12月13日 16:57

斐波那契数列:从递归到缓存优化的极致拆解

斐波那契数列是算法入门的经典案例,也是理解「递归」「缓存优化」「闭包」核心思想的绝佳载体。本文会从最基础的递归解法入手,逐步拆解重复计算的痛点,再通过哈希缓存、闭包缓存等方式优化,带你吃透斐波那契数列的解题思路。

一、斐波那契数列的定义

先明确斐波那契数列的核心规则:

  • 起始项:f(0) = 0f(1) = 1
  • 递推公式:f(n) = f(n-1) + f(n-2)(n ≥ 2);
  • 数列示例:0, 1, 1, 2, 3, 5, 8, 13, 21...

简单来说,从0和1开始,后续每一项都等于前两项之和。

二、基础递归解法:思路简单但效率拉胯

1. 递归核心思想

递归的本质是「大问题拆解为小问题」:计算 f(n) 时,先拆解为计算 f(n-1)f(n-2),直到拆解到 f(0)f(1) 这个「递归终止条件」,再逐层返回结果。

2. 代码实现

// 基础递归版斐波那契
function fib(n) {
  // 递归退出条件:触底到0或1,直接返回
  if (n <= 1) return n;
  // 递推公式:拆分为两个子问题
  return fib(n - 1) + fib(n - 2);
}

console.log(fib(10)); // 55(小数值正常)
console.log(fib(100)); // 卡死(重复计算导致超时)

3. 核心问题分析

(1)重复计算严重

fib(5) 为例,拆解过程如下:

fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)

可以看到:fib(3) 被计算了2次,fib(2) 被计算了3次,fib(1) 被计算了5次。随着n增大,重复计算呈指数级增长。

(2)时间复杂度爆炸
  • 时间复杂度:O(2ⁿ),指数级复杂度,n=40时计算时间就会明显增加,n=100直接卡死;
  • 空间复杂度:O(n),递归调用栈的深度等于n,极端情况下会触发「栈溢出」。
(3)调用栈溢出风险

递归依赖函数调用栈存储上下文,当n过大时(比如n=10000),会超出JS引擎的调用栈限制,抛出 Maximum call stack size exceeded 错误。

三、优化1:哈希缓存(空间换时间)

1. 优化思路

既然重复计算是核心问题,我们可以用「哈希表(对象)」缓存已经计算过的结果:

  • 计算前先查缓存,存在则直接返回;
  • 计算后将结果存入缓存,避免重复计算。

这是典型的「空间换时间」策略,用少量内存开销换取时间复杂度的大幅降低。

2. 代码实现

// 缓存对象:存储已计算的斐波那契值
const cache = {};

function fib(n) {
  // 1. 优先查缓存,存在则直接返回
  if (n in cache) {
    return cache[n];
  }
  // 2. 递归终止条件
  if (n <= 1) {
    cache[n] = n; // 存入缓存
    return n;
  }
  // 3. 计算并缓存结果
  const result = fib(n - 1) + fib(n - 2);
  cache[n] = result;
  return result;
}

console.log(fib(100)); // 顺利输出:354224848179261915075

3. 优化效果分析

  • 时间复杂度:O(n),每个n只计算一次,后续直接取缓存;
  • 空间复杂度:O(n),缓存对象存储n个值 + 递归调用栈深度n;
  • 核心改进:彻底解决重复计算问题,n=100也能快速计算。

4. 小问题

缓存对象 cache 暴露在全局作用域中,容易被意外修改,破坏了函数逻辑的独立性。

四、优化2:闭包封装缓存(更优雅的空间换时间)

1. 优化思路

用「立即执行函数(IIFE)」创建闭包,将缓存对象封装在函数内部,避免全局污染:

  • IIFE 立即执行,创建独立的作用域;
  • 内部定义缓存对象(自由变量),返回一个计算斐波那契的函数;
  • 返回的函数可以访问闭包中的缓存对象,且外部无法修改。

2. 代码实现

// IIFE 创建闭包,封装缓存
const fib = (function() {
  // 闭包中的缓存:仅内部可访问,避免全局污染
  const cache = {};
  
  // 返回实际的计算函数
  return function(n) {
    if (n in cache) {
      return cache[n];
    }
    if (n <= 1) {
      cache[n] = n;
      return n;
    }
    // 注意:此处调用的是外部的fib(即返回的这个函数)
    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  }
})();

console.log(fib(100)); // 依然快速输出结果
console.log(cache); // undefined(外部无法访问缓存,更安全)

3. 核心优势

  • 缓存私有化:闭包中的 cache 仅被返回的 fib 函数访问,避免全局污染和意外修改;
  • 代码更优雅:把缓存和计算的逻辑打包在一起,就像把相关工具放进同一个工具箱,用起来方便还不杂乱;
  • 性能不变:时间复杂度仍为O(n),空间复杂度仍为O(n)。

五、补充:递归 vs 迭代(拓展思路)

除了缓存优化递归,还可以用「迭代」彻底避免递归调用栈问题:

// 迭代版斐波那契(空间复杂度可优化至O(1))
function fib(n) {
  if (n <= 1) return n;
  let prev = 0, curr = 1;
  for (let i = 2; i <= n; i++) {
    const next = prev + curr;
    prev = curr;
    curr = next;
  }
  return curr;
}
  • 时间复杂度:O(n);
  • 空间复杂度:O(1),仅用三个变量存储状态,无递归栈和缓存开销。

六、核心知识点总结

1. 递归的适用场景

递归适合解决「可拆分为相似子问题、有明确终止条件、符合树形结构」的问题,但必须注意:

  • 避免重复计算(用缓存优化);
  • 防止栈溢出(n过大时优先用迭代)。

2. 缓存优化的核心思想

「空间换时间」是算法优化的常用策略,核心是存储已计算的结果,避免重复劳动,常见载体包括:

  • 哈希表(对象/Map);
  • 数组;
  • 闭包私有化缓存。

3. IIFE + 闭包的价值

  • IIFE:立即执行函数,创建独立作用域,避免全局污染;
  • 闭包:让内部函数访问外部作用域的变量(如cache),且变量不会被垃圾回收,持续有效。

4. 各版本对比

版本 时间复杂度 空间复杂度 优点 缺点
基础递归 O(2ⁿ) O(n) 思路简单 重复计算、易栈溢出
哈希缓存 O(n) O(n) 解决重复计算 缓存全局暴露
闭包缓存 O(n) O(n) 缓存私有化、代码优雅 仍有递归栈开销
迭代 O(n) O(1) 性能最优、无栈溢出 思路稍绕

七、总结

斐波那契数列的优化过程,是算法思维从「简单实现」到「高效优雅」的典型体现:

  1. 基础递归:满足「能跑」,但存在重复计算和栈溢出问题;
  2. 哈希缓存:解决重复计算,时间复杂度从O(2ⁿ)降到O(n);
  3. 闭包缓存:在缓存的基础上优化代码结构,实现缓存私有化;
  4. 迭代优化:彻底摆脱递归栈,空间复杂度降到O(1)。

斐波那契看似简单,却是理解算法优化的绝佳入口。从朴素递归的指数爆炸,到缓存记忆化的时间换空间,再到闭包封装的工程优雅,最后迭代实现极致效率——每一步都体现了“用合适工具解决合适问题”的编程智慧。

JSX 基本语法与 React 组件化思想

作者 冻梨政哥
2025年12月13日 16:31

JSX 基本语法与 React 组件化思想

在现代前端开发中,React 框架凭借其独特的 JSX 语法和组件化思想占据了重要地位。本文将结合实际代码示例,详细介绍 JSX 语法特性、组件化思想以及基本使用方法。

什么是 JSX?

JSX(JavaScript XML)是 React 中用于描述用户界面的语法扩展,它允许我们在 JavaScript 代码中直接编写类似 HTML 的标记,实现了 "在 JS 中写 HTML" 的开发体验。

// JSX语法示例
const element = <h2>JSX 是 React 中用于描述用户界面的语法扩展</h2>

这看似是 HTML,实则是 JavaScript 的语法糖。JSX 会被 Babel 等工具编译为普通的 JavaScript 函数调用:

// 编译后的JavaScript
const element2 = createElement('h2', null, 'JSX 是 React 中用于描述用户界面的语法扩展')

两者效果完全一致,但 JSX 的可读性和开发效率明显更高。

React 组件化思想

React 的核心思想之一是组件化,即将 UI 拆分为独立、可复用的部分,每个部分都可以单独维护。

组件的定义方式

在 React 中,组件可以通过函数来定义,返回 JSX 的函数就是一个组件:

// 函数组件定义示例
function JuejinHeader() {
  return (
    <div>
      <header>
        <h1>JueJin首页</h1>
      </header>
    </div>
  )
}

// 箭头函数形式的组件
const Ariticles = () => {
  return (
    <div>
      Articles
    </div>
  )
}

组件组合与嵌套

组件可以像搭积木一样组合使用,形成组件树:

function App() {
  return (
    <div>
      {/* 头部组件 */}
      <JuejinHeader />
      <main>
        {/* 文章列表组件 */}
        <Ariticles />
        <aside>
          {/* 侧边栏组件 */}
          <Checkin />
          <TopArticles />
        </aside>
      </main>
    </div>
  )
}

这种组合方式让我们可以将复杂页面拆分为多个简单组件,提高代码的可维护性和复用性。

JSX 基本语法规则

  1. 表达式插入:使用{}在 JSX 中插入 JavaScript 表达式
// 数据绑定示例
const [name, setName] = useState("vue");

// 在JSX中使用表达式
return (
  <h1>Hello <span className="title">{name}!</span></h1>
)
  1. 条件渲染:通过逻辑与运算符或三元表达式实现
// 条件渲染示例
{isLoggedIn ? <div>已登录</div> : <div>未登录</div>}

// 按钮文本根据状态变化
<button onClick={toggleLogin}>
  {isLoggedIn ? "退出登录" : "登录"}
</button>
  1. 列表渲染:使用数组的 map 方法渲染列表,需提供唯一 key
// 列表渲染示例
{todos.length > 0 ? (
  <ul>
    {todos.map((todo) => (
      <li key={todo.id}>
        {todo.title}
      </li>
    ))}
  </ul>
) : (
  <div>暂无待办事项</div>
)}
  1. 样式处理:class 属性需使用 className(因为 class 是 JavaScript 关键字)
// CSS类名使用className
<span className="title">{name}!</span>
/* 对应的CSS样式 */
.title{
  color: red;
}
  1. 根元素限制:JSX 最外层必须有一个根元素,或使用片段<>
// 使用片段避免多余的div嵌套
return (
  <>  
    <h1>标题</h1>
    <p>内容</p>
  </>
)
  1. 事件处理:使用驼峰命名法,如 onClick
// 事件处理示例
const toggleLogin = () => {
 setIsLoggedIn(!isLoggedIn);
}

<button onClick={toggleLogin}>切换登录状态</button>

响应式数据与 JSX

React 通过 useState 钩子实现响应式数据,当状态变化时,JSX 会自动重新渲染:

// 响应式数据示例
const [name, setName] = useState("vue");

// 3秒后更新状态,视图会自动更新
setTimeout(() => {
  setName("react");
}, 3000);

当 name 状态从 "vue" 变为 "react" 时,使用{name}的地方会自动更新,无需手动操作 DOM。

总结

JSX 和组件化是 React 的两大核心特性:

  • JSX 提供了一种直观、高效的方式描述 UI,将 HTML 和 JavaScript 无缝结合
  • 组件化思想将 UI 拆分为独立可复用的单元,使复杂应用的开发和维护变得简单
  • 通过状态管理实现响应式更新,让开发者专注于数据逻辑而非 DOM 操作

这种开发模式使得 React 在构建大型应用时具有明显优势,也是现代前端开发的重要思想。

异步并行任务执行工具

作者 NuLL
2025年12月13日 16:30

📖 概述

runParallelTasks 是一个生产级的并行异步任务执行工具,它提供了一种优雅的方式来并行执行多个异步任务,同时支持丰富的功能如重试机制、超时控制、进度追踪和任务取消。

🎯 设计哲学

为什么这样设计?

传统异步并行处理(如 Promise.all())存在以下局限性:

  1. 错误处理粗糙:一个任务失败会导致整个批次失败
  2. 缺乏进度反馈:无法知道任务执行进度
  3. 无取消机制:无法中途停止任务执行
  4. 缺乏重试能力:网络波动时无法自动恢复
  5. 资源管理困难:无法清理超时任务和监听器

本工具的设计目标是解决这些问题,提供:

  • ✅ 细粒度错误处理:每个任务独立处理成功/失败
  • ✅ 实时进度追踪:精确掌握执行进度
  • ✅ 完善的取消机制:支持随时取消所有任务
  • ✅ 智能重试策略:自动重试失败任务
  • ✅ 资源自动管理:避免内存泄漏

🆚 与传统方案对比

特性 Promise.all() Promise.allSettled() runParallelTasks
错误处理 一个失败全部失败 收集所有结果,无后续处理 每个任务独立错误处理 + 全局兜底
进度追踪 ❌ 不支持 ❌ 不支持 ✅ 实时进度回调
取消机制 ❌ 不支持 ❌ 不支持 ✅ 支持取消所有任务
重试机制 ❌ 不支持 ❌ 不支持 ✅ 支持配置化重试
超时控制 ❌ 不支持 ❌ 不支持 ✅ 支持任务级超时
资源清理 ❌ 无 ❌ 无 ✅ 自动清理定时器/监听器
错误调试 简单错误信息 简单状态信息 ✅ 完整错误历史记录

🏗️ 架构设计

核心执行流程

// 执行流程:重试 → 超时 → 取消
const executeTask = () => withRetry(asyncTask, retryCount, retryDelay, signal, taskIndex, taskName);
const taskPromise = Promise.resolve()
  .then(() => withTimeout(executeTask, timeout, taskIndex, taskName))
  // 后续处理...

设计说明

  • 执行顺序:超时包裹重试,确保总超时包含所有重试尝试
  • 取消检查:每次重试前检查取消状态,避免无效执行
  • 错误传播:重试用尽后向上抛出最终错误

重试机制 (withRetry)

/**
 * 带重试的任务执行器
 * 设计特点:
 * 1. 迭代实现:避免递归导致的堆栈溢出
 * 2. 取消检查:每次重试前检查取消信号
 * 3. 错误记录:记录所有重试错误的历史记录
 * 4. 延迟响应:重试延迟期间可立即响应取消
 */
const withRetry = async (asyncTask, retryCount = 0, retryDelay = 0, signal, taskIndex, taskName) => {
  const retryErrors = []; // 记录所有重试错误
  let currentRetry = 0;

  while (currentRetry <= retryCount) {
    // 检查取消(第一道防线)
    if (signal?.aborted) {
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止重试`);
      abortError.name = 'AbortError';
      abortError.retryErrors = retryErrors;
      abortError.retryCount = currentRetry;
      abortError.totalRetry = retryCount;
      throw abortError;
    }

    try {
      const result = await asyncTask(signal);
      return {
        data: result,
        retryCount: currentRetry,
        totalRetry: retryCount,
        retryErrors
      };
    } catch (error) {
      // 记录错误历史
      retryErrors.push({
        retry: currentRetry,
        error: error.message,
        timestamp: new Date().toISOString()
      });

      // 重试用尽
      if (currentRetry >= retryCount) {
        error.retryErrors = retryErrors;
        error.retryCount = currentRetry;
        error.totalRetry = retryCount;
        throw error;
      }

      // 延迟重试(支持取消)
      await delayWithCancel(retryDelay, signal, taskIndex, taskName);
      currentRetry++;
    }
  }
};

延迟函数 (delayWithCancel)

/**
 * 带取消响应的延迟函数
 * 设计特点:
 * 1. 取消响应:延迟期间监听取消信号,立即中断
 * 2. 资源清理:自动清理定时器和事件监听器
 * 3. 原子操作:确保清理操作只执行一次
 */
const delayWithCancel = (delay, signal, taskIndex, taskName) => {
  return new Promise((resolve, reject) => {
    if (delay <= 0) return resolve();
    
    // 立即检查取消状态
    if (signal?.aborted) {
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止延迟`);
      abortError.name = 'AbortError';
      return reject(abortError);
    }

    let timeoutId;
    let abortHandler;
    
    // 统一的清理函数
    const cleanup = () => {
      clearTimeout(timeoutId);
      if (abortHandler) {
        signal?.removeEventListener('abort', abortHandler);
      }
    };

    // 延迟成功结束
    const onFinish = () => {
      cleanup();
      resolve();
    };

    // 取消处理函数
    abortHandler = () => {
      cleanup();
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})取消,中断重试延迟`);
      abortError.name = 'AbortError';
      reject(abortError);
    };

    // 设置延迟
    timeoutId = setTimeout(onFinish, delay);
    
    // 监听取消信号
    signal?.addEventListener('abort', abortHandler);
  });
};

超时控制 (withTimeout)

/**
 * 带超时的任务执行器
 * 设计特点:
 * 1. 总超时:超时时间包含所有重试尝试
 * 2. 竞态执行:任务执行与超时竞态,先完成者生效
 * 3. 自动清理:任务完成后自动清理超时定时器
 */
const withTimeout = (taskFn, timeout, taskIndex, taskName) => {
  if (!timeout || timeout <= 0) return taskFn();

  return new Promise((resolve, reject) => {
    let timeoutId;
    
    // 超时Promise
    const timeoutPromise = new Promise((_, reject) => {
      timeoutId = setTimeout(() => {
        const timeoutError = new Error(`任务[${taskIndex}](${taskName || '未知'})超时(${timeout}ms,含所有重试)`);
        timeoutError.name = 'TaskTimeoutError';
        timeoutError.taskIndex = taskIndex;
        timeoutError.taskName = taskName;
        reject(timeoutError);
      }, timeout);
    });

    // 竞态执行
    Promise.race([taskFn(), timeoutPromise])
      .then(resolve)
      .catch(reject)
      .finally(() => {
        clearTimeout(timeoutId); // 关键:清理超时定时器
      });
  });
};

结果聚合 (allDone)

/**
 * 聚合所有任务结果
 * 设计特点:
 * 1. 统一格式:将所有任务结果格式化为统一结构
 * 2. 错误兜底:处理意料之外的错误
 * 3. 完整信息:包含任务索引、名称、重试信息等
 */
const allDone = Promise.allSettled(taskPromises).then((settledResults) => {
  return settledResults.map((item) => {
    if (item.status === 'fulfilled') return item.value;
    
    // 兜底处理:理论上不会执行到这里(内部已catch所有错误)
    return {
      success: false,
      error: item.reason,
      taskIndex: -1,
      taskName: '未知任务',
      isAborted: false,
      reason: 'UNHANDLED_ERROR',
      retryCount: 0,
      totalRetry: 0,
      retryErrors: []
    };
  });
});

📚 使用方法

基本安装

// 1. 复制 runParallelTasks 函数到你的项目
// 2. 导入函数
import { runParallelTasks } from './utils/asyncTask';

// 或者作为独立模块使用
// import runParallelTasks from 'parallel-task-runner';

任务队列配置

每个任务可以配置以下属性:

const task = {
  // 必需:异步任务函数,可接收 AbortSignal
  asyncTask: (signal) => fetch('/api/data', { signal }).then(r => r.json()),
  
  // 可选:任务成功回调(支持异步)
  onSuccess: (data, index) => {
    console.log(`任务${index}成功:`, data);
    updateUI(data);
  },
  
  // 可选:任务失败回调(支持异步)
  onError: (error, index) => {
    console.error(`任务${index}失败:`, error);
    showError(error);
  },
  
  // 可选:任务名称(用于日志和调试)
  taskName: '获取用户数据',
  
  // 可选:总超时时间(毫秒,包含所有重试)
  timeout: 10000,
  
  // 可选:重试次数(默认0,不重试)
  retryCount: 3,
  
  // 可选:重试延迟(毫秒,默认0)
  retryDelay: 1000
};

执行配置

const options = {
  // 必需:任务队列数组
  taskQueue: [...],
  
  // 可选:全局进度回调
  onProgress: (completed, total, taskIndex, taskName) => {
    console.log(`进度: ${completed}/${total}`);
    updateProgressBar(completed / total);
  },
  
  // 可选:全局错误兜底
  onGlobalError: (error, taskIndex, taskName) => {
    console.error(`任务${taskIndex}(${taskName})未处理错误:`, error);
    sendToErrorTracking(error);
  },
  
  // 可选:是否启用取消功能(默认true)
  enableAbort: true
};

执行和结果处理

// 执行任务
const runner = runParallelTasks(options);

// 1. 使用 allDone 等待所有任务完成
runner.allDone.then(results => {
  const successCount = results.filter(r => r.success).length;
  const failedCount = results.filter(r => !r.success).length;
  
  console.log(`完成: ${successCount}成功, ${failedCount}失败`);
  
  // 处理成功结果
  results.filter(r => r.success).forEach(result => {
    console.log(`任务${result.taskIndex}结果:`, result.result);
  });
  
  // 处理失败结果
  results.filter(r => !r.success).forEach(result => {
    console.error(`任务${result.taskIndex}失败原因:`, result.error.message);
    if (result.retryCount > 0) {
      console.error(`已重试${result.retryCount}次`, result.retryErrors);
    }
  });
});

// 2. 随时取消任务(如页面卸载时)
// runner.abort();

// 3. 访问单个任务的Promise(高级用法)
// runner.promises[0].then(result => console.log('第一个任务结果:', result));

📋 使用案例

案例1:页面数据加载

/**
 * 场景:页面初始化时需要并行加载多个API数据
 * 需求:需要进度显示,支持取消,关键数据需要重试
 */
const loadPageData = () => {
  const taskQueue = [
    {
      taskName: '用户信息',
      asyncTask: (signal) => api.getUserInfo({ signal }),
      timeout: 5000,
      retryCount: 1,
      retryDelay: 1000,
      onSuccess: (data) => store.commit('SET_USER', data),
      onError: (error) => {
        console.error('用户信息加载失败');
        showFallbackUserInfo();
      }
    },
    {
      taskName: '配置信息',
      asyncTask: (signal) => api.getConfig({ signal }),
      timeout: 3000,
      onSuccess: (data) => store.commit('SET_CONFIG', data)
    },
    {
      taskName: '推荐内容',
      asyncTask: (signal) => api.getRecommendations({ signal }),
      timeout: 8000,
      onSuccess: (data) => store.commit('SET_RECOMMENDATIONS', data)
    }
  ];

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      showLoadingProgress(completed / total * 100);
    },
    onGlobalError: (error, index, name) => {
      logToMonitoring('页面数据加载失败', { taskIndex: index, taskName: name, error });
    }
  });

  // 返回runner,以便在组件卸载时取消
  return runner;
};

// 使用
const pageDataLoader = loadPageData();

// 等待所有数据加载完成
pageDataLoader.allDone.then(results => {
  const allSuccess = results.every(r => r.success);
  if (allSuccess) {
    showPageContent();
  } else {
    showPartialContent(results);
  }
});

// 页面卸载时取消未完成的任务
onBeforeUnmount(() => {
  pageDataLoader.abort();
});

案例2:批量文件上传

/**
 * 场景:批量上传多个文件
 * 需求:显示总进度,单个文件可重试,支持取消上传
 */
const uploadFiles = (files) => {
  const taskQueue = files.map((file, index) => ({
    taskName: `文件: ${file.name}`,
    asyncTask: async (signal) => {
      // 使用FormData上传
      const formData = new FormData();
      formData.append('file', file);
      
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        signal // 支持取消
      });
      
      if (!response.ok) {
        throw new Error(`上传失败: ${response.status}`);
      }
      
      return await response.json();
    },
    timeout: 30000, // 30秒超时
    retryCount: 2,  // 重试2次
    retryDelay: 2000, // 2秒后重试
    onSuccess: (result, index) => {
      updateFileStatus(index, 'success');
      console.log(`文件${file.name}上传成功:`, result);
    },
    onError: (error, index) => {
      updateFileStatus(index, 'error');
      console.error(`文件${file.name}上传失败:`, error);
      
      // 根据重试情况显示不同提示
      if (error.retryCount > 0) {
        showToast(`${file.name}上传失败,已重试${error.retryCount}次`);
      }
    }
  }));

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      updateTotalProgress(completed / total * 100);
    },
    onGlobalError: (error, index) => {
      console.error(`文件${files[index]?.name}上传异常:`, error);
    }
  });

  return runner;
};

// 使用
const files = [...]; // 文件列表
const uploadRunner = uploadFiles(files);

// 监控上传结果
uploadRunner.allDone.then(results => {
  const successCount = results.filter(r => r.success).length;
  showToast(`上传完成: ${successCount}/${files.length}个文件成功`);
  
  // 处理失败的文件
  results.filter(r => !r.success).forEach(result => {
    logUploadFailure(result);
  });
});

// 用户取消上传
cancelButton.onclick = () => {
  uploadRunner.abort();
  showToast('上传已取消');
};

案例3:健康检查监控

/**
 * 场景:监控多个微服务的健康状态
 * 需求:并行检查,快速失败,记录检查历史
 */
const checkServiceHealth = (services) => {
  const taskQueue = services.map((service, index) => ({
    taskName: service.name,
    asyncTask: async (signal) => {
      const response = await fetch(`${service.url}/health`, {
        signal,
        timeout: 3000
      });
      
      const data = await response.json();
      
      if (data.status !== 'healthy') {
        throw new Error(`服务状态异常: ${data.status}`);
      }
      
      return data;
    },
    timeout: 5000, // 5秒超时
    retryCount: 1, // 快速重试1次
    retryDelay: 1000,
    onSuccess: (data, index) => {
      markServiceHealthy(services[index].id);
      console.log(`${services[index].name}健康检查通过`);
    },
    onError: (error, index) => {
      const service = services[index];
      markServiceUnhealthy(service.id);
      
      // 记录详细的健康检查失败信息
      logHealthCheckFailure({
        service: service.name,
        error: error.message,
        retryCount: error.retryCount || 0,
        retryErrors: error.retryErrors || []
      });
    }
  }));

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      updateDashboardHealthStatus(completed, total);
    },
    onGlobalError: (error, index, name) => {
      // 发送到监控系统
      sendToMonitoringSystem({
        type: 'HEALTH_CHECK_ERROR',
        service: name,
        error: error.message
      });
    },
    enableAbort: false // 健康检查不需要取消
  });

  return runner;
};

// 定时执行健康检查
setInterval(() => {
  const services = [
    { id: 'auth', name: '认证服务', url: 'https://auth.example.com' },
    { id: 'payment', name: '支付服务', url: 'https://payment.example.com' },
    { id: 'notification', name: '通知服务', url: 'https://notification.example.com' }
  ];
  
  const healthChecker = checkServiceHealth(services);
  
  healthChecker.allDone.then(results => {
    const healthyCount = results.filter(r => r.success).length;
    updateSystemHealthIndicator(healthyCount / results.length * 100);
    
    // 如果有服务不健康,发送警报
    const unhealthy = results.filter(r => !r.success);
    if (unhealthy.length > 0) {
      sendAlert(`有${unhealthy.length}个服务不健康`);
    }
  });
}, 60000); // 每分钟检查一次

案例4:API请求合并优化

/**
 * 场景:页面需要多个API数据,传统方案是串行请求
 * 优化:使用并行请求减少总加载时间
 */
const fetchDashboardData = () => {
  const taskQueue = [
    {
      taskName: '用户统计',
      asyncTask: () => api.getUserStats(),
      timeout: 3000,
      onSuccess: (data) => store.commit('SET_USER_STATS', data)
    },
    {
      taskName: '销售数据',
      asyncTask: () => api.getSalesData(),
      timeout: 5000,
      retryCount: 1,
      onSuccess: (data) => store.commit('SET_SALES_DATA', data)
    },
    {
      taskName: '库存状态',
      asyncTask: () => api.getInventoryStatus(),
      timeout: 4000,
      onSuccess: (data) => store.commit('SET_INVENTORY', data)
    },
    {
      taskName: '活动列表',
      asyncTask: () => api.getActivities(),
      timeout: 6000,
      onSuccess: (data) => store.commit('SET_ACTIVITIES', data)
    }
  ];

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      // 显示加载进度
      const progress = Math.min(completed / total * 100, 99); // 最大99%,留1%给最终处理
      updateLoadingProgress(progress);
    },
    onGlobalError: (error, index, name) => {
      console.error(`仪表板数据加载失败: ${name}`, error);
    }
  });

  return runner;
};

// 使用 - 相比串行请求,时间从 sum(time) 减少到 max(time)
const dashboardLoader = fetchDashboardData();

// 传统串行方式大约需要 3+5+4+6 = 18秒
// 并行方式最多只需要 max(3,5,4,6) = 6秒

dashboardLoader.allDone.then(results => {
  const allLoaded = results.every(r => r.success);
  
  if (allLoaded) {
    showDashboard();
  } else {
    // 部分数据加载失败,显示降级内容
    showDegradedDashboard(results);
  }
});

🔧 高级配置

自定义重试策略

// 基于错误类型的重试策略
const createRetryConfig = (error) => {
  // 网络错误:重试3次
  if (error.name === 'NetworkError' || error.name === 'TypeError') {
    return { retryCount: 3, retryDelay: 1000 };
  }
  
  // 服务器5xx错误:重试2次
  if (error.status >= 500 && error.status < 600) {
    return { retryCount: 2, retryDelay: 2000 };
  }
  
  // 其他错误:不重试
  return { retryCount: 0 };
};

// 在任务配置中使用
const task = {
  asyncTask: async (signal) => {
    try {
      return await fetch('/api/data', { signal }).then(r => r.json());
    } catch (error) {
      // 根据错误类型动态决定重试策略
      const retryConfig = createRetryConfig(error);
      error.retryConfig = retryConfig;
      throw error;
    }
  },
  // 动态重试配置
  retryCount: (task) => task.error?.retryConfig?.retryCount || 0,
  retryDelay: (task) => task.error?.retryConfig?.retryDelay || 0
};

性能监控集成

// 添加性能监控
const monitoredRunParallelTasks = (options) => {
  const startTime = performance.now();
  const taskCount = options.taskQueue.length;
  
  const runner = runParallelTasks({
    ...options,
    onProgress: (completed, total, taskIndex, taskName) => {
      // 调用原始进度回调
      options.onProgress?.(completed, total, taskIndex, taskName);
      
      // 性能监控
      if (completed === total) {
        const endTime = performance.now();
        const duration = endTime - startTime;
        
        sendToAnalytics({
          event: 'PARALLEL_TASKS_COMPLETED',
          taskCount,
          duration,
          successRate: completed / total
        });
      }
    }
  });
  
  return runner;
};

📊 性能建议

最佳实践

  1. 合理设置超时时间
    • 关键任务:5-10秒
    • 非关键任务:3-5秒
    • 后台任务:10-30秒
  1. 重试策略建议
    • 网络请求:重试2-3次,延迟1-2秒
    • 支付操作:重试1-2次,延迟2-3秒
    • 文件上传:重试1次,延迟3秒
  1. 并发控制
    • 虽然工具支持无限并发,但建议根据实际情况控制任务数量
    • 大量任务(>50)建议分批执行
  1. 内存管理
    • 页面卸载时务必调用 abort() 取消未完成任务
    • 监控长时间运行的任务,避免内存泄漏

🐛 常见问题

Q1: 任务取消后,allDone 还会返回结果吗?

A: 会的。取消的任务会返回一个特殊的结果对象,其中 isAborted: truereason: 'USER_CANCELLED'allDone 会等待所有任务(包括被取消的)完成。

Q2: 重试期间超时如何计算?

A: 超时时间是从任务开始到结束的总时间,包含所有重试尝试。例如:设置 timeout: 10000,重试3次,那么从第一次尝试开始计时,10秒后如果还没成功则超时。

Q3: 任务函数必须接收 signal 参数吗?

A: 不需要。工具总是传递 signal 参数,但如果你的任务函数不需要取消功能,可以忽略这个参数。

Q4: 如何实现并发控制?

A: 当前版本不内置并发控制,因为设计目标是真正的并行执行。如果需要并发控制,建议在外部实现任务分批。

Q5: 错误对象中的 retryErrors 包含什么?

A: 包含所有重试尝试的错误记录数组,每个记录包含:

  • retry: 第几次重试(从0开始)
  • error: 错误信息
  • timestamp: 错误发生时间

📈 扩展建议

如果未来需要扩展功能,可以考虑:

  1. 优先级调度:为任务添加优先级,高优先级先执行
  2. 依赖关系:支持任务间的依赖关系
  3. 并发限制:限制同时执行的任务数量
  4. 断点续传:对于长时间任务支持暂停/恢复
  5. 更复杂的重试策略:指数退避、抖动等算法

📝 总结

runParallelTasks 是一个功能全面、设计优雅的并行任务执行工具,它解决了传统异步并行处理的诸多痛点,特别适合以下场景:

  • ✅ 复杂页面初始化:需要加载多个API
  • ✅ 批量操作:文件上传、数据导入导出
  • ✅ 监控检查:服务健康检查、心跳检测
  • ✅ 实时数据处理:并行处理多个数据流
  • ✅ 用户交互响应:多个后台任务并行执行

通过合理使用这个工具,可以显著提升应用的用户体验和代码的可维护性。


📄 完整代码

最后,这是完整的 runParallelTasks 函数代码:

/**
 * @file utils/asyncTask.js
 * @description 并行执行异步任务队列(重试机制终极优化版)
 * 核心特性:
 * 1. 重试延迟期间可立即响应取消(无需等待延迟结束)
 * 2. 所有定时器(重试延迟/超时)自动清理,无内存泄漏
 * 3. 每次重试前检查取消状态,避免无效重试
 * 4. 记录所有重试错误(保留最后一次错误为主,附带错误列表)
 * 5. 总超时包裹整个重试过程(符合需求),重试次数/延迟可配置
 * 6. 取消/超时/重试逻辑解耦,代码结构清晰
 */

export function runParallelTasks({
  taskQueue,
  onProgress,
  onGlobalError,
  enableAbort = true
}) {
  // 初始化取消控制器
  const controller = enableAbort ? new AbortController() : null;
  const { signal } = controller || {};
  const total = taskQueue.length;
  let completed = 0;
  const taskPromises = [];

  // 空队列兜底
  if (total === 0) {
    console.warn('runParallelTasks: 任务队列为空');
    return {
      promises: taskPromises,
      abort: () => {},
      allDone: Promise.resolve([])
    };
  }

  /**
   * 带取消响应的延迟函数(核心改进:延迟期间可取消,清理定时器)
   * @param {number} delay 延迟毫秒数
   * @param {AbortSignal} signal 取消信号
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<void>} 延迟Promise,取消时立即reject
   */
  const delayWithCancel = (delay, signal, taskIndex, taskName) => {
    return new Promise((resolve, reject) => {
      if (delay <= 0) return resolve();
      
      // 检查是否已取消
      if (signal?.aborted) {
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止延迟`);
        abortError.name = 'AbortError';
        return reject(abortError);
      }

      let timeoutId;
      let abortHandler;
      
      // 清理函数
      const cleanup = () => {
        clearTimeout(timeoutId);
        if (abortHandler) {
          signal?.removeEventListener('abort', abortHandler);
        }
      };

      // 延迟成功结束
      const onFinish = () => {
        cleanup();
        resolve();
      };

      // 取消处理
      abortHandler = () => {
        cleanup();
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})取消,中断重试延迟`);
        abortError.name = 'AbortError';
        reject(abortError);
      };

      // 设置延迟定时器
      timeoutId = setTimeout(onFinish, delay);
      
      // 监听取消信号
      signal?.addEventListener('abort', abortHandler);
    });
  };

  /**
   * 带重试的任务执行器(核心改进:延迟响应取消、记录所有错误、清理定时器)
   * @param {Function} asyncTask 异步任务函数
   * @param {number} retryCount 重试次数
   * @param {number} retryDelay 重试间隔
   * @param {AbortSignal} signal 取消信号
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<any>} 任务执行结果
   */
  const withRetry = async (asyncTask, retryCount = 0, retryDelay = 0, signal, taskIndex, taskName) => {
    const retryErrors = []; // 记录所有重试错误
    let currentRetry = 0;

    while (currentRetry <= retryCount) {
      // 每次重试前检查是否已取消(第一道防线)
      if (signal?.aborted) {
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止重试`);
        abortError.name = 'AbortError';
        abortError.retryErrors = retryErrors;
        abortError.retryCount = currentRetry;
        abortError.totalRetry = retryCount;
        throw abortError;
      }

      try {
        // 执行单次任务
        const result = await asyncTask(signal);
        // 成功则返回结果,附带重试信息
        return {
          data: result,
          retryCount: currentRetry,
          totalRetry: retryCount,
          retryErrors
        };
      } catch (error) {
        // 记录当前错误
        retryErrors.push({
          retry: currentRetry,
          error: error.message || String(error),
          timestamp: new Date().toISOString()
        });

        // 重试用尽,抛出最终错误(附带所有重试错误)
        if (currentRetry >= retryCount) {
          if (!error.retryErrors) error.retryErrors = retryErrors;
          if (!error.retryCount) error.retryCount = currentRetry;
          if (!error.totalRetry) error.totalRetry = retryCount;
          throw error;
        }

        console.log(`任务[${taskIndex}](${taskName || '未知'})执行失败,将在${retryDelay}ms后重试(第${currentRetry + 1}/${retryCount}次)`, error.message);
        
        try {
          // 重试延迟(支持取消)
          await delayWithCancel(retryDelay, signal, taskIndex, taskName);
        } catch (delayError) {
          // 延迟期间被取消,传播取消错误
          delayError.retryErrors = retryErrors;
          delayError.retryCount = currentRetry;
          delayError.totalRetry = retryCount;
          throw delayError;
        }
        
        currentRetry++;
      }
    }
  };

  /**
   * 带超时的任务执行器(总超时包裹整个重试过程)
   * @param {Function} taskFn 任务函数(含重试逻辑)
   * @param {number} timeout 总超时时间
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<any>} 任务执行结果
   */
  const withTimeout = (taskFn, timeout, taskIndex, taskName) => {
    if (!timeout || timeout <= 0) return taskFn();

    return new Promise((resolve, reject) => {
      let timeoutId;
      // 总超时Promise
      const timeoutPromise = new Promise((_, reject) => {
        timeoutId = setTimeout(() => {
          const timeoutError = new Error(`任务[${taskIndex}](${taskName || '未知'})超时(${timeout}ms,含所有重试)`);
          timeoutError.name = 'TaskTimeoutError';
          timeoutError.taskIndex = taskIndex;
          timeoutError.taskName = taskName;
          reject(timeoutError);
        }, timeout);
      });

      // 竞态执行:任务(含重试) vs 总超时
      Promise.race([taskFn(), timeoutPromise])
        .then(resolve)
        .catch(reject)
        .finally(() => {
          clearTimeout(timeoutId); // 清理超时定时器
        });
    });
  };

  // 遍历执行每个任务
  taskQueue.forEach((task, taskIndex) => {
    const {
      asyncTask,
      onSuccess,
      onError,
      taskName,
      timeout,
      retryCount = 0,
      retryDelay = 0
    } = task;

    // 执行任务:重试(带取消/延迟清理) → 总超时 → 取消
    const executeTask = () => withRetry(asyncTask, retryCount, retryDelay, signal, taskIndex, taskName);

    const taskPromise = Promise.resolve()
      .then(() => withTimeout(executeTask, timeout, taskIndex, taskName))
      // 成功处理
      .then((result) => {
        // 解构重试结果(兼容无重试的情况)
        const { data, retryCount: actualRetry, totalRetry, retryErrors } = result || {};
        return Promise.resolve(onSuccess?.(data, taskIndex))
          .then(() => ({
            success: true,
            result: data,
            taskIndex,
            taskName,
            isAborted: false,
            retryCount: actualRetry || 0,
            totalRetry: totalRetry || 0,
            retryErrors: retryErrors || []
          }));
      })
      // 失败处理
      .catch((error) => {
        // 处理主动取消(含重试延迟中取消)
        if (error.name === 'AbortError') {
          console.log(`runParallelTasks: 任务[${taskIndex}](${taskName || '未知'})已取消`, error.message);
          return {
            success: false,
            error,
            taskIndex,
            taskName,
            isAborted: true,
            reason: 'USER_CANCELLED',
            retryCount: error.retryCount || 0,
            totalRetry: error.totalRetry || 0,
            retryErrors: error.retryErrors || []
          };
        }

        // 处理超时/最终执行失败(重试用尽)
        return Promise.resolve()
          .then(() => {
            // 优先执行专属错误回调
            if (onError) {
              return onError(error, taskIndex);
            }
            // 全局错误处理(try-catch兜底)
            try {
              onGlobalError?.(error, taskIndex, taskName);
            } catch (globalErr) {
              console.error(`runParallelTasks: 全局错误处理函数执行失败`, globalErr);
            }
            console.error(
              `runParallelTasks: 任务[${taskIndex}](${taskName || '未知'})最终执行失败`,
              `已重试${error.retryCount || 0}/${error.totalRetry || 0}次`,
              `错误列表:${JSON.stringify(error.retryErrors || [])}`,
              error
            );
          })
          .then(() => ({
            success: false,
            error,
            taskIndex,
            taskName,
            isAborted: false,
            reason: error.name === 'TaskTimeoutError' ? 'TIMEOUT' : 'EXECUTION_FAILED',
            retryCount: error.retryCount || 0,
            totalRetry: error.totalRetry || 0,
            retryErrors: error.retryErrors || []
          }));
      })
      // 进度更新(原子操作)
      .finally(() => {
        const currentCompleted = ++completed;
        onProgress?.(currentCompleted, total, taskIndex, taskName);
      });

    taskPromises.push(taskPromise);
  });

  // 聚合Promise:格式化所有任务结果
  const allDone = Promise.allSettled(taskPromises).then((settledResults) => {
    return settledResults.map((item) => {
      if (item.status === 'fulfilled') return item.value;
      // 兜底处理
      return {
        success: false,
        error: item.reason,
        taskIndex: -1,
        taskName: '未知任务',
        isAborted: false,
        reason: 'UNHANDLED_ERROR',
        retryCount: 0,
        totalRetry: 0,
        retryErrors: []
      };
    });
  });

  // 取消方法
  const abort = () => {
    if (controller) {
      controller.abort();
      console.log('runParallelTasks: 已触发取消所有任务');
    } else {
      console.warn('runParallelTasks: 未开启取消功能(enableAbort=false)');
    }
  };

  return {
    promises: taskPromises,
    abort,
    allDone
  };
}

异步互斥锁

作者 NuLL
2025年12月13日 16:27

异步任务互斥锁工具 (Async Lock Manager)

📖 概述

LockManager 是一个生产级的异步任务互斥锁管理工具,专为现代 Web 应用中的并发控制设计。它通过互斥锁机制防止异步任务重复执行,提供队列管理、智能重试、超时控制和资源自动清理等功能。

🎯 设计哲学

为什么需要异步任务互斥锁?

传统的防抖节流方案存在以下局限性:

  1. 无法防止长时间异步操作:防抖节流只能控制函数调用频率,但无法防止 API 接口长时间未返回时的重复调用
  2. 缺乏队列管理:多个并发请求无法有序排队执行
  3. 缺少取消机制:无法中断已发起的异步任务
  4. 资源管理困难:无法自动清理过期锁和等待任务
  5. 缺乏智能重试:简单的重试策略无法适应复杂错误场景

本工具的设计目标是解决这些问题,提供:

  • ✅ 原子性操作:确保锁的获取和释放是原子操作
  • ✅ 智能队列管理:支持 FIFO 队列,可配置队列大小和超时
  • ✅ 可中断执行:支持任务取消和超时中断
  • ✅ 指数退避重试:支持自定义重试条件和退避策略
  • ✅ 资源自动管理:自动清理过期锁和队列项
  • ✅ 完整监控统计:提供执行统计和状态监控

🆚 与传统方案对比

特性 防抖 (Debounce) 节流 (Throttle) 简单互斥锁 LockManager
防止重复调用 ✅ 时间窗口内 ✅ 固定频率 ✅ 直到完成 ✅ 直到完成 + 队列
异步任务支持 ❌ 有限 ❌ 有限 ✅ 基础 ✅ 完整(重试、超时、取消)
队列管理 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持 FIFO 队列
取消机制 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持主动取消
重试策略 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 指数退避 + 自定义条件
超时控制 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持任务和队列超时
资源清理 ❌ 无 ❌ 无 ❌ 无 ✅ 自动清理过期锁
状态监控 ❌ 无 ❌ 无 ❌ 无 ✅ 完整统计信息

🏗️ 架构设计

核心执行流程

// 执行流程:检查锁 → 加入队列(可选) → 获取锁 → 执行任务 → 释放锁 → 处理队列
async execute(options) {
  // 1. 检查锁状态
  // 2. 如果已锁定且启用队列,加入队列等待
  // 3. 获取锁(原子操作)
  // 4. 执行任务(支持重试)
  // 5. 清理锁资源
  // 6. 处理队列中的下一个任务
}

锁管理机制 (_acquireLock)

/**
 * 原子性地获取锁
 * 设计特点:
 * 1. 三重检查:确保锁获取的原子性
 * 2. 唯一标识:为每个锁尝试生成唯一ID
 * 3. 资源预分配:提前创建取消控制器
 * 4. 验证机制:设置后验证确保原子性
 */
_acquireLock(name) {
  const attemptId = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
  
  // 第一重检查
  const existing = this._lockMap.get(name);
  if (existing?.locked) {
    return null;
  }
  
  // 创建锁对象
  const lockItem = {
    locked: true,
    abortController: new AbortController(),
    timeoutTimer: null,
    createdAt: Date.now(),
    taskId: `${name}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
    attemptId: attemptId
  };
  
  // 第二重检查(原子性保障)
  const current = this._lockMap.get(name);
  if (current?.locked) {
    return null;
  }
  
  // 设置锁
  this._lockMap.set(name, lockItem);
  
  // 最终验证(确保原子性)
  const afterSet = this._lockMap.get(name);
  if (afterSet?.attemptId !== attemptId) {
    lockItem.abortController.abort();
    return null;
  }
  
  return lockItem;
}

队列管理机制 (_addToQueue_processNextInQueue)

/**
 * 将任务加入等待队列
 * 设计特点:
 * 1. 容量控制:可配置最大队列大小
 * 2. 超时管理:队列等待也有超时控制
 * 3. 有序执行:FIFO(先进先出)原则
 * 4. 资源清理:超时自动清理队列项
 */
_addToQueue(options) {
  const { name, maxQueueSize } = options;
  
  let queue = this._queueMap.get(name);
  if (!queue) {
    queue = [];
    this._queueMap.set(name, queue);
  }
  
  // 队列容量检查
  if (queue.length >= maxQueueSize) {
    const error = new Error(`任务队列【${name}】已满(最大${maxQueueSize})`);
    error.type = 'queue_full';
    error.code = 'QUEUE_FULL';
    return Promise.reject(error);
  }
  
  return new Promise((resolve, reject) => {
    const queueItem = {
      options,
      resolve,
      reject,
      enqueuedAt: Date.now()
    };
    
    queue.push(queueItem);
    
    // 队列等待超时
    if (options.timeout > 0) {
      queueItem.timeoutTimer = setTimeout(() => {
        const index = queue.indexOf(queueItem);
        if (index > -1) {
          queue.splice(index, 1);
          const error = new Error(`任务【${name}】在队列中等待超时`);
          error.type = 'queue_timeout';
          error.code = 'QUEUE_TIMEOUT';
          reject(error);
        }
      }, options.timeout);
    }
  });
}

/**
 * 处理队列中的下一个任务
 * 设计特点:
 * 1. 微任务调度:使用 Promise.resolve() 避免 setTimeout 延迟
 * 2. 递归处理:自动处理队列中的所有任务
 * 3. 资源清理:处理完成后清理空队列
 * 4. 错误传播:正确处理任务成功和失败
 */
async _processNextInQueue(name) {
  const queue = this._queueMap.get(name);
  if (!queue || queue.length === 0) {
    this._queueMap.delete(name);
    return;
  }
  
  // 使用微任务处理,避免 setTimeout 的延迟
  await Promise.resolve();
  
  const queueItem = queue.shift();
  
  // 清理队列项的超时定时器
  if (queueItem.timeoutTimer) {
    clearTimeout(queueItem.timeoutTimer);
  }
  
  try {
    const result = await this._executeTask(queueItem.options);
    queueItem.resolve(result);
  } catch (error) {
    queueItem.reject(error);
  } finally {
    // 递归处理下一个任务
    if (queue.length > 0) {
      Promise.resolve().then(() => this._processNextInQueue(name));
    } else {
      this._queueMap.delete(name);
    }
  }
}

智能重试机制 (_executeWithExponentialBackoff)

/**
 * 指数退避重试执行
 * 设计特点:
 * 1. 取消检查:每次重试前检查取消信号
 * 2. 退避算法:指数退避 + 随机抖动
 * 3. 自定义条件:支持根据错误类型决定是否重试
 * 4. 安全延迟:可中断的延时函数
 */
async _executeWithExponentialBackoff(fn, maxRetries, baseDelay, maxDelay, abortController, retryCondition) {
  let lastError;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      // 检查是否已取消
      if (abortController.signal.aborted) {
        const cancelError = new Error('任务已被取消');
        cancelError.type = 'cancel';
        cancelError.code = 'CANCELLED';
        throw cancelError;
      }
      
      // 非首次尝试时延迟
      if (attempt > 0) {
        const delay = this._calculateExponentialBackoffDelay(attempt, baseDelay, maxDelay);
        await this._sleep(delay, abortController.signal);
      }
      
      return await fn();
      
    } catch (error) {
      lastError = error;
      
      // 判断是否应该重试
      if (!this._shouldRetry(error, retryCondition)) {
        throw error;
      }
      
      // 重试用尽
      if (attempt === maxRetries) {
        error.retryAttempts = attempt;
        throw error;
      }
    }
  }
  
  throw lastError;
}

/**
 * 判断是否应该重试(支持自定义重试条件)
 */
_shouldRetry(error, retryCondition) {
  // 不重试的错误类型
  const noRetryTypes = ['cancel', 'timeout', 'queue_full', 'queue_timeout', 'lock_failed'];
  if (noRetryTypes.includes(error.type)) {
    return false;
  }
  
  // 如果提供了自定义重试条件函数,使用它
  if (typeof retryCondition === 'function') {
    return retryCondition(error);
  }
  
  // 默认重试条件:非特定错误都重试
  return true;
}

资源自动清理 (_cleanupExpiredLocks)

/**
 * 清理过期锁和队列
 * 设计特点:
 * 1. 定期执行:每60秒自动清理一次
 * 2. 双重清理:同时清理过期锁和队列项
 * 3. 优雅终止:清理时发送取消信号
 * 4. 统计记录:记录清理操作便于监控
 */
_cleanupExpiredLocks() {
  const now = Date.now();
  const maxAge = this._defaults.maxLockAge;
  
  // 清理过期锁
  for (const [name, lockItem] of this._lockMap.entries()) {
    if (lockItem.locked && (now - lockItem.createdAt) > maxAge) {
      console.warn(`清理过期锁【${name}】,已锁定${now - lockItem.createdAt}ms`);
      
      const error = new Error('锁过期自动清理');
      error.type = 'timeout';
      error.code = 'LOCK_EXPIRED';
      
      if (lockItem.abortController) {
        lockItem.abortController.abort(error);
      }
      
      this._lockMap.delete(name);
    }
  }
  
  // 清理过期队列项
  for (const [name, queue] of this._queueMap.entries()) {
    // ... 清理逻辑
  }
}

📚 使用方法

基本安装

// 方式1:使用默认单例(无控制台警告)
import { asyncLock, releaseLock } from './asyncLock';

// 方式2:创建自定义实例
import { createLockManager } from './asyncLock';
const myLockManager = createLockManager({
  timeout: 10000,
  maxQueueSize: 10,
  tipHandler: (msg) => console.warn(msg)
});

// 方式3:使用带控制台警告的单例
import { verboseLockManager } from './asyncLock';

基础配置选项

const options = {
  // 必需:锁名称(用于标识任务类型)
  name: 'submitForm',
  
  // 必需:异步任务函数
  asyncFn: async (signal) => {
    // signal 是 AbortSignal,用于取消任务
    if (signal.aborted) throw new Error('任务已取消');
    return await fetch('/api/submit', { signal }).then(r => r.json());
  },
  
  // 可选:任务超时时间(毫秒)
  timeout: 8000,
  
  // 可选:重试次数(默认0)
  retryCount: 2,
  
  // 可选:基础重试延迟(毫秒)
  baseRetryDelay: 1000,
  
  // 可选:最大重试延迟(毫秒)
  maxRetryDelay: 10000,
  
  // 可选:自定义重试条件函数
  retryCondition: (error) => {
    // 只对网络错误重试
    return error.message.includes('Network') || error.message.includes('timeout');
  },
  
  // 可选:重复执行时的提示信息
  repeatTip: '操作中,请稍后...',
  
  // 可选:重复执行时是否抛出错误(默认true)
  throwRepeatError: true,
  
  // 可选:是否启用队列(默认false)
  enableQueue: true,
  
  // 可选:队列最大长度(默认100)
  maxQueueSize: 5,
  
  // 可选:成功回调
  onSuccess: (result) => {
    console.log('任务成功:', result);
  },
  
  // 可选:失败回调
  onFail: (error) => {
    console.error('任务失败:', error.message);
  },
  
  // 可选:提示处理器(用于显示重复提示)
  tipHandler: (message) => {
    Toast.warning(message);
  }
};

执行任务

// 使用默认单例
try {
  const result = await asyncLock(options);
  console.log('执行结果:', result);
} catch (error) {
  if (error.code === 'LOCKED') {
    // 重复执行被拒绝
    console.warn('请勿重复操作');
  } else if (error.code === 'QUEUE_FULL') {
    // 队列已满
    console.error('系统繁忙,请稍后重试');
  } else {
    // 其他错误
    console.error('执行失败:', error);
  }
}

// 使用自定义实例
try {
  const result = await myLockManager.execute(options);
  console.log('执行结果:', result);
} catch (error) {
  // 错误处理
}

锁管理操作

import { 
  asyncLock, 
  releaseLock, 
  releaseAllLocks, 
  cancelLockTask,
  getLockStatus,
  getStats,
  resetStats 
} from './asyncLock';

// 1. 手动释放指定锁
releaseLock('submitForm');

// 2. 释放所有锁
releaseAllLocks();

// 3. 取消正在执行的任务
const cancelled = cancelLockTask('submitForm', '用户主动取消');
if (cancelled) {
  console.log('任务已取消');
}

// 4. 获取锁状态
const status = getLockStatus('submitForm');
console.log('锁状态:', {
  是否锁定: status.locked,
  锁定时长: `${status.age}ms`,
  队列长度: status.queueLength
});

// 5. 获取统计信息
const stats = getStats();
console.log('执行统计:', {
  总执行次数: stats.totalExecutions,
  成功次数: stats.successCount,
  超时次数: stats.timeoutCount,
  当前活跃锁: stats.activeLocks.length
});

// 6. 重置统计
resetStats();

📋 使用案例

案例1:表单提交防重复

/**
 * 场景:表单提交按钮防止用户重复点击
 * 需求:提交期间禁用按钮,防止重复提交,支持取消
 */
class FormSubmitService {
  constructor() {
    this.isSubmitting = false;
  }
  
  async submitForm(formData) {
    if (this.isSubmitting) {
      Toast.warning('正在提交,请稍候...');
      return;
    }
    
    this.isSubmitting = true;
    
    try {
      const result = await asyncLock({
        name: 'formSubmit',
        asyncFn: async (signal) => {
          // 模拟API调用
          const response = await fetch('/api/submit', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(formData),
            signal
          });
          
          if (!response.ok) {
            throw new Error(`提交失败: ${response.status}`);
          }
          
          return await response.json();
        },
        timeout: 10000,
        retryCount: 1,
        baseRetryDelay: 2000,
        repeatTip: '正在提交中,请勿重复点击',
        tipHandler: (msg) => Toast.warning(msg),
        onSuccess: (result) => {
          Toast.success('提交成功!');
          console.log('提交结果:', result);
        },
        onFail: (error) => {
          if (error.code !== 'LOCKED') {
            Toast.error(`提交失败: ${error.message}`);
          }
        }
      });
      
      return result;
    } finally {
      this.isSubmitting = false;
    }
  }
  
  // 用户离开页面时取消提交
  cancelSubmit() {
    cancelLockTask('formSubmit', '用户离开页面');
  }
}

// 使用
const formService = new FormSubmitService();

// 提交表单
submitButton.addEventListener('click', async () => {
  const formData = collectFormData();
  await formService.submitForm(formData);
});

// 页面离开时取消
window.addEventListener('beforeunload', () => {
  formService.cancelSubmit();
});

案例2:支付订单防重复

/**
 * 场景:支付订单防止重复支付
 * 需求:支付期间锁定订单,防止重复支付,支持队列
 */
class PaymentService {
  constructor(orderId) {
    this.orderId = orderId;
    this.lockName = `payment_${orderId}`;
  }
  
  async processPayment(paymentData) {
    try {
      return await asyncLock({
        name: this.lockName,
        asyncFn: async (signal) => {
          // 调用支付接口
          const paymentResult = await this.callPaymentApi(paymentData, signal);
          
          // 更新订单状态
          await this.updateOrderStatus(paymentResult, signal);
          
          return paymentResult;
        },
        timeout: 30000, // 支付操作需要更长时间
        retryCount: 2,
        baseRetryDelay: 3000,
        maxRetryDelay: 15000,
        // 只对网络错误和服务器5xx错误重试
        retryCondition: (error) => {
          const isNetworkError = error.message.includes('Network') || 
                                 error.message.includes('fetch');
          const isServerError = error.message.includes('50') || 
                                error.message.includes('服务不可用');
          return isNetworkError || isServerError;
        },
        enableQueue: true,
        maxQueueSize: 1, // 同一订单只允许一个排队
        repeatTip: '订单支付处理中,请稍候...',
        tipHandler: (msg) => {
          showPaymentStatus(msg);
        },
        onSuccess: (result) => {
          showPaymentSuccess(result);
          trackPaymentEvent('success', this.orderId);
        },
        onFail: (error) => {
          if (error.code === 'LOCKED') {
            // 重复支付被阻止
            trackPaymentEvent('prevented_duplicate', this.orderId);
          } else if (error.code === 'QUEUE_FULL') {
            showPaymentError('订单正在处理,请勿重复操作');
          } else {
            showPaymentError(`支付失败: ${error.message}`);
            trackPaymentEvent('failed', this.orderId, error);
          }
        }
      });
    } catch (error) {
      console.error('支付处理异常:', error);
      throw error;
    }
  }
  
  async callPaymentApi(paymentData, signal) {
    // 模拟支付API调用
    const response = await fetch('/api/payment/process', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        orderId: this.orderId,
        ...paymentData
      }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`支付API错误: ${response.status}`);
    }
    
    return await response.json();
  }
  
  async updateOrderStatus(paymentResult, signal) {
    // 更新订单状态
    const response = await fetch(`/api/orders/${this.orderId}/status`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        status: 'paid',
        paymentId: paymentResult.paymentId,
        paidAt: new Date().toISOString()
      }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`订单状态更新失败: ${response.status}`);
    }
  }
  
  // 取消支付
  cancelPayment() {
    const cancelled = cancelLockTask(this.lockName, '用户取消支付');
    if (cancelled) {
      showPaymentStatus('支付已取消');
      trackPaymentEvent('cancelled', this.orderId);
    }
    return cancelled;
  }
}

// 使用
const paymentService = new PaymentService('ORDER_123456');

// 开始支付
paymentButton.addEventListener('click', async () => {
  const paymentData = {
    amount: 100.00,
    method: 'credit_card',
    cardToken: 'tok_123456'
  };
  
  try {
    await paymentService.processPayment(paymentData);
  } catch (error) {
    console.error('支付失败:', error);
  }
});

// 取消支付
cancelButton.addEventListener('click', () => {
  paymentService.cancelPayment();
});

案例3:文件上传队列管理

/**
 * 场景:批量文件上传,需要控制并发和防止重复上传
 * 需求:同一文件不能重复上传,上传任务需要排队
 */
class FileUploadManager {
  constructor() {
    this.uploadQueue = new Map(); // fileId -> upload promise
  }
  
  async uploadFile(file, options = {}) {
    const fileId = this.generateFileId(file);
    const lockName = `upload_${fileId}`;
    
    // 如果已经在队列中,返回已有的Promise
    if (this.uploadQueue.has(fileId)) {
      return this.uploadQueue.get(fileId);
    }
    
    const uploadPromise = asyncLock({
      name: lockName,
      asyncFn: async (signal) => {
        try {
          // 更新UI状态
          this.updateFileStatus(fileId, 'uploading');
          
          // 执行上传
          const result = await this.doUpload(file, signal, options);
          
          // 上传成功
          this.updateFileStatus(fileId, 'success');
          return result;
        } catch (error) {
          // 上传失败
          this.updateFileStatus(fileId, 'error');
          throw error;
        }
      },
      timeout: 5 * 60 * 1000, // 5分钟超时
      retryCount: 3,
      baseRetryDelay: 5000,
      maxRetryDelay: 60000,
      retryCondition: (error) => {
        // 只对网络错误重试
        return error.message.includes('network') || 
               error.message.includes('timeout') ||
               error.message.includes('Network');
      },
      enableQueue: true,
      maxQueueSize: 0, // 同一文件不上传队列
      repeatTip: '文件正在上传中...',
      tipHandler: (msg) => {
        console.log(`文件 ${file.name}: ${msg}`);
      },
      onSuccess: (result) => {
        console.log(`文件 ${file.name} 上传成功:`, result);
        this.uploadQueue.delete(fileId);
      },
      onFail: (error) => {
        console.error(`文件 ${file.name} 上传失败:`, error);
        this.uploadQueue.delete(fileId);
      },
      autoCleanup: false // 手动清理,避免上传完成前锁被清理
    });
    
    // 保存到队列
    this.uploadQueue.set(fileId, uploadPromise);
    
    return uploadPromise;
  }
  
  async doUpload(file, signal, options) {
    const formData = new FormData();
    formData.append('file', file);
    
    // 添加上传进度回调
    const xhr = new XMLHttpRequest();
    
    return new Promise((resolve, reject) => {
      // 监听取消信号
      if (signal.aborted) {
        reject(new Error('上传被取消'));
        return;
      }
      
      const onAbort = () => {
        xhr.abort();
        reject(new Error('上传被取消'));
      };
      
      signal.addEventListener('abort', onAbort);
      
      // 设置上传进度
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable) {
          const percent = Math.round((event.loaded / event.total) * 100);
          this.updateUploadProgress(fileId, percent);
        }
      });
      
      // 完成处理
      xhr.addEventListener('load', () => {
        signal.removeEventListener('abort', onAbort);
        
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`));
        }
      });
      
      xhr.addEventListener('error', () => {
        signal.removeEventListener('abort', onAbort);
        reject(new Error('网络错误,上传失败'));
      });
      
      xhr.addEventListener('abort', () => {
        signal.removeEventListener('abort', onAbort);
        reject(new Error('上传被取消'));
      });
      
      // 开始上传
      xhr.open('POST', '/api/upload');
      xhr.send(formData);
    });
  }
  
  generateFileId(file) {
    // 生成文件唯一ID(实际项目中可能需要更复杂的逻辑)
    return `${file.name}_${file.size}_${file.lastModified}`;
  }
  
  updateFileStatus(fileId, status) {
    // 更新UI显示
    console.log(`文件 ${fileId} 状态: ${status}`);
  }
  
  updateUploadProgress(fileId, percent) {
    // 更新上传进度
    console.log(`文件 ${fileId} 上传进度: ${percent}%`);
  }
  
  // 取消文件上传
  cancelUpload(file) {
    const fileId = this.generateFileId(file);
    const lockName = `upload_${fileId}`;
    
    const cancelled = cancelLockTask(lockName, '用户取消上传');
    if (cancelled) {
      this.uploadQueue.delete(fileId);
      this.updateFileStatus(fileId, 'cancelled');
      console.log(`文件 ${file.name} 上传已取消`);
    }
    
    return cancelled;
  }
  
  // 批量取消所有上传
  cancelAllUploads() {
    releaseAllLocks();
    this.uploadQueue.clear();
    console.log('所有文件上传已取消');
  }
}

// 使用
const uploadManager = new FileUploadManager();

// 上传文件
fileInput.addEventListener('change', async (event) => {
  const files = Array.from(event.target.files);
  
  for (const file of files) {
    try {
      await uploadManager.uploadFile(file);
    } catch (error) {
      console.error(`文件 ${file.name} 上传失败:`, error);
    }
  }
});

// 取消上传
cancelButton.addEventListener('click', () => {
  const file = getSelectedFile();
  uploadManager.cancelUpload(file);
});

案例4:全局配置管理

/**
 * 场景:应用全局配置需要防止并发修改
 * 需求:配置更新需要互斥,多个更新请求需要排队
 */
class ConfigManager {
  constructor() {
    this.config = {};
    this.lockManager = createLockManager({
      timeout: 15000,
      maxLockAge: 2 * 60 * 1000, // 2分钟
      maxQueueSize: 5,
      tipHandler: (msg) => console.log('[ConfigLock]', msg),
      enableStats: true
    });
  }
  
  async updateConfig(key, value, options = {}) {
    const lockName = `config_${key}`;
    
    try {
      const result = await this.lockManager.execute({
        name: lockName,
        asyncFn: async (signal) => {
          // 获取当前配置
          const currentConfig = await this.fetchConfig(key, signal);
          
          // 验证配置
          if (options.validate) {
            const isValid = await options.validate(value, currentConfig, signal);
            if (!isValid) {
              throw new Error('配置验证失败');
            }
          }
          
          // 更新配置
          const updateResult = await this.doUpdateConfig(key, value, signal);
          
          // 更新本地缓存
          this.config[key] = value;
          
          // 触发配置变更事件
          this.emitConfigChange(key, value, currentConfig);
          
          return updateResult;
        },
        timeout: options.timeout || 10000,
        retryCount: options.retryCount || 1,
        baseRetryDelay: 2000,
        retryCondition: (error) => {
          // 只对网络错误重试
          return error.message.includes('network') || 
                 error.message.includes('timeout') ||
                 error.name === 'TypeError'; // fetch错误
        },
        enableQueue: true,
        onSuccess: (result) => {
          console.log(`配置 ${key} 更新成功:`, result);
        },
        onFail: (error) => {
          if (error.code !== 'LOCKED') {
            console.error(`配置 ${key} 更新失败:`, error);
          }
        }
      });
      
      return result;
    } catch (error) {
      console.error(`配置 ${key} 更新异常:`, error);
      throw error;
    }
  }
  
  async fetchConfig(key, signal) {
    const response = await fetch(`/api/config/${key}`, { signal });
    if (!response.ok) {
      throw new Error(`获取配置失败: ${response.status}`);
    }
    return await response.json();
  }
  
  async doUpdateConfig(key, value, signal) {
    const response = await fetch(`/api/config/${key}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ value }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`更新配置失败: ${response.status}`);
    }
    
    return await response.json();
  }
  
  emitConfigChange(key, newValue, oldValue) {
    // 触发配置变更事件
    const event = new CustomEvent('configChange', {
      detail: { key, newValue, oldValue }
    });
    window.dispatchEvent(event);
  }
  
  // 批量更新配置(多个配置项原子更新)
  async batchUpdateConfig(updates, options = {}) {
    const lockName = 'config_batch_update';
    
    return await this.lockManager.execute({
      name: lockName,
      asyncFn: async (signal) => {
        // 开始事务
        const transactionId = await this.beginTransaction(signal);
        
        try {
          const results = {};
          
          // 依次更新每个配置
          for (const [key, value] of Object.entries(updates)) {
            const result = await this.doUpdateConfig(key, value, signal);
            results[key] = result;
            this.config[key] = value;
          }
          
          // 提交事务
          await this.commitTransaction(transactionId, signal);
          
          // 触发批量变更事件
          this.emitBatchConfigChange(updates);
          
          return results;
        } catch (error) {
          // 回滚事务
          await this.rollbackTransaction(transactionId, signal);
          throw error;
        }
      },
      timeout: 30000, // 批量操作需要更长时间
      retryCount: 0, // 批量操作不重试
      enableQueue: true,
      maxQueueSize: 1 // 批量操作只允许一个排队
    });
  }
  
  async beginTransaction(signal) {
    const response = await fetch('/api/config/transaction/begin', { signal });
    if (!response.ok) {
      throw new Error('开始事务失败');
    }
    const data = await response.json();
    return data.transactionId;
  }
  
  async commitTransaction(transactionId, signal) {
    const response = await fetch('/api/config/transaction/commit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transactionId }),
      signal
    });
    
    if (!response.ok) {
      throw new Error('提交事务失败');
    }
  }
  
  async rollbackTransaction(transactionId, signal) {
    await fetch('/api/config/transaction/rollback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transactionId }),
      signal
    }).catch(() => {
      // 回滚失败也继续,不影响主流程
    });
  }
  
  emitBatchConfigChange(updates) {
    const event = new CustomEvent('configBatchChange', {
      detail: { updates }
    });
    window.dispatchEvent(event);
  }
  
  // 获取锁管理器统计信息(用于监控)
  getLockStats() {
    return this.lockManager.getStats();
  }
  
  // 清理所有配置锁
  cleanupConfigLocks() {
    this.lockManager.releaseAllLocks();
  }
}

// 使用
const configManager = new ConfigManager();

// 更新单个配置
async function updateTheme(theme) {
  try {
    await configManager.updateConfig('theme', theme, {
      validate: async (value, current) => {
        // 验证主题是否有效
        const validThemes = ['light', 'dark', 'auto'];
        return validThemes.includes(value);
      },
      retryCount: 2
    });
  } catch (error) {
    if (error.code === 'LOCKED') {
      console.log('配置正在更新中,请稍后');
    } else {
      console.error('更新主题失败:', error);
    }
  }
}

// 批量更新配置
async function updateUserPreferences(prefs) {
  try {
    const results = await configManager.batchUpdateConfig(prefs);
    console.log('偏好设置更新成功:', results);
  } catch (error) {
    console.error('批量更新失败:', error);
  }
}

// 监控锁状态
setInterval(() => {
  const stats = configManager.getLockStats();
  if (stats.activeLocks.length > 0) {
    console.log('活跃的配置锁:', stats.activeLocks);
  }
}, 60000);

🔧 高级配置

自定义重试策略

// 基于错误类型的智能重试策略
const smartRetryCondition = (error) => {
  // 网络错误:重试
  if (error.name === 'NetworkError' || 
      error.name === 'TypeError' || 
      error.message.includes('network')) {
    return true;
  }
  
  // 服务器5xx错误:重试
  if (error.status >= 500 && error.status < 600) {
    return true;
  }
  
  // 服务器4xx错误(除429外):不重试
  if (error.status >= 400 && error.status < 500 && error.status !== 429) {
    return false;
  }
  
  // 429 Too Many Requests:使用退避重试
  if (error.status === 429) {
    return true;
  }
  
  // 默认情况:不重试
  return false;
};

// 使用自定义重试条件
await asyncLock({
  name: 'apiCall',
  asyncFn: apiCallFunction,
  retryCount: 3,
  retryCondition: smartRetryCondition
});

性能监控集成

// 创建带监控的锁管理器
class MonitoredLockManager extends LockManager {
  constructor(options = {}) {
    super(options);
    this.metrics = {
      lockAcquisitionTime: [],
      taskExecutionTime: [],
      queueWaitTime: []
    };
  }
  
  async execute(options) {
    const startTime = performance.now();
    
    try {
      const result = await super.execute(options);
      
      // 记录执行时间
      const endTime = performance.now();
      const executionTime = endTime - startTime;
      this.metrics.taskExecutionTime.push(executionTime);
      
      // 发送性能指标
      this.sendMetrics({
        name: options.name,
        executionTime,
        success: true
      });
      
      return result;
    } catch (error) {
      const endTime = performance.now();
      const executionTime = endTime - startTime;
      
      // 发送错误指标
      this.sendMetrics({
        name: options.name,
        executionTime,
        success: false,
        errorType: error.type,
        errorCode: error.code
      });
      
      throw error;
    }
  }
  
  sendMetrics(metric) {
    // 发送到监控系统
    console.log('[LockMetrics]', metric);
    
    // 实际项目中可以发送到 APM 系统
    // sendToAPM('lock_execution', metric);
  }
  
  getPerformanceMetrics() {
    const calculateStats = (array) => {
      if (array.length === 0) return null;
      
      const sum = array.reduce((a, b) => a + b, 0);
      const avg = sum / array.length;
      const max = Math.max(...array);
      const min = Math.min(...array);
      
      return { count: array.length, avg, min, max, sum };
    };
    
    return {
      taskExecution: calculateStats(this.metrics.taskExecutionTime),
      lockAcquisition: calculateStats(this.metrics.lockAcquisitionTime),
      queueWait: calculateStats(this.metrics.queueWaitTime)
    };
  }
}

// 使用带监控的锁管理器
const monitoredManager = new MonitoredLockManager();

// 定期打印性能指标
setInterval(() => {
  const metrics = monitoredManager.getPerformanceMetrics();
  console.log('锁管理器性能指标:', metrics);
}, 60000);

📊 性能建议

最佳实践

  1. 合理设置超时时间
    • 快速操作:1-5秒
    • 普通操作:5-10秒
    • 长时间操作:10-30秒
    • 文件上传等:1-5分钟
  1. 队列配置建议
    • 关键操作:队列大小 1(确保严格顺序)
    • 普通操作:队列大小 3-5
    • 批量操作:队列大小 10-20
    • 注意:队列越大,内存占用越高
  1. 重试策略建议
    • 网络请求:重试2-3次,基础延迟1-3秒
    • 支付操作:重试1-2次,基础延迟2-5秒
    • 文件操作:重试0-1次,基础延迟5-10秒
  1. 内存管理
    • 定期检查锁状态,避免内存泄漏
    • 页面卸载时调用 destroy() 清理资源
    • 监控队列长度,避免无限增长
  1. 错误处理
    • 区分用户取消和系统错误
    • 对不同的错误类型采取不同的处理策略
    • 记录详细的错误日志以便排查

🐛 常见问题

Q1: 锁会自动释放吗?

A: 是的。锁会在以下情况下自动释放:

  • 任务执行完成(成功或失败)
  • 任务超时
  • 锁过期(超过 maxLockAge 配置)
  • 手动调用 releaseLock() 或 releaseAllLocks()

Q2: 队列中的任务会按顺序执行吗?

A: 是的。队列采用 FIFO(先进先出)原则,任务会按照加入队列的顺序依次执行。

Q3: 如何防止内存泄漏?

A: 锁管理器内置以下防护措施:

  1. 定期清理过期锁(默认60秒一次)
  2. 队列项超时自动清理
  3. 页面卸载时可以调用 destroy() 方法
  4. 所有定时器和事件监听器都有清理逻辑

Q4: 支持分布式环境吗?

A: 当前版本是单机内存锁,适用于单页面应用或单服务器环境。如果需要分布式锁,可以基于此模式扩展,使用 Redis 或其他分布式存储作为锁存储后端。

Q5: 如何监控锁管理器的状态?

A: 可以通过以下方式监控:

  1. 使用 getLockStatus(name) 获取特定锁状态
  2. 使用 getStats() 获取全局统计信息
  3. 继承 LockManager 类添加自定义监控
  4. 监听相关事件(需要自行扩展事件系统)

📈 扩展建议

如果未来需要扩展功能,可以考虑:

  1. 分布式锁支持:集成 Redis 或其他分布式存储
  2. 锁优先级:为队列中的任务添加优先级
  3. 锁续期机制:长时间任务自动续期
  4. 事件系统:锁状态变化时触发事件
  5. 浏览器存储持久化:页面刷新后恢复锁状态
  6. 更复杂的队列算法:支持优先级队列、延迟队列等

📝 总结

LockManager 是一个功能全面、设计优雅的异步任务互斥锁工具,它解决了传统防抖节流方案的诸多痛点,特别适合以下场景:

  • ✅ 表单提交:防止重复提交
  • ✅ 支付操作:防止重复支付
  • ✅ 文件上传:同一文件不上传多次
  • ✅ 配置更新:防止并发修改配置
  • ✅ 关键操作:需要严格顺序执行的操作
  • ✅ 资源竞争:多组件共享资源时的并发控制

通过合理使用这个工具,可以显著提升应用的数据一致性和用户体验,避免因并发操作导致的业务逻辑错误。


📄 完整代码

  1. 默认单例 (asyncLock):适合大多数场景
  2. 自定义实例 (createLockManager):需要不同配置时使用
  3. 类直接使用 (LockManager):需要继承扩展时使用

工具已经过精心设计和测试,可以直接在生产环境中使用。

/**
 * 异步任务互斥锁工具
 * 需求:防抖节流不能防止api接口长时间未返回。如果用户等待一小段时候后重新点击提交,会导致重新触发请求;
 * 解决思路:用互斥锁思路处理异步任务锁定,通过name进行异步任务锁定,防止重入。
 * 核心能力:防止异步任务未完成时重复执行、超时控制、任务取消、资源自动清理
 * 支持:队列机制、指数退避重试、原子操作、错误分类、性能监控
 */
class LockManager {
  constructor(options = {}) {
    // 存储所有锁状态
    this._lockMap = new Map();
    
    // 等待队列
    this._queueMap = new Map();
    
    // 默认配置
    this._defaults = {
      timeout: 10000,
      repeatTip: '操作中,请稍后...',
      throwRepeatError: true,
      autoCleanup: true,
      maxLockAge: 5 * 60 * 1000,
      maxQueueSize: 100,
      enableStats: true,
      tipHandler: () => {}, 
      ...options
    };
    
    // 统计信息
    this._stats = {
      totalExecutions: 0,
      successCount: 0,
      timeoutCount: 0,
      cancelCount: 0,
      repeatRejectCount: 0,
      queueFullCount: 0,
      retryCount: 0
    };
    
    // 定期清理过期锁和队列
    this._cleanupInterval = setInterval(() => this._cleanupExpiredLocks(), 60000);
    
    // 绑定方法,确保在回调中使用正确的this
    this._processNextInQueue = this._processNextInQueue.bind(this);
  }

  /**
   * 原子性地获取锁
   */
  _acquireLock(name) {
    const attemptId = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
    const now = Date.now();
    
    // 第一重检查
    const existing = this._lockMap.get(name);
    if (existing?.locked) {
      return null;
    }
    
    // 创建新的锁对象
    const lockItem = {
      locked: true,
      abortController: new AbortController(),
      timeoutTimer: null,
      createdAt: now,
      taskId: `${name}_${now}_${Math.random().toString(36).slice(2, 10)}`,
      attemptId: attemptId,
      waitingQueue: this._queueMap.get(name) || []
    };
    
    // 第二重检查(原子性保障)
    const current = this._lockMap.get(name);
    if (current?.locked) {
      return null;
    }
    
    // 设置锁(原子操作)
    this._lockMap.set(name, lockItem);
    
    // 最终验证
    const afterSet = this._lockMap.get(name);
    if (afterSet?.attemptId !== attemptId) {
      lockItem.abortController.abort();
      return null;
    }
    
    return lockItem;
  }

  /**
   * 将任务加入等待队列
   */
  _addToQueue(options) {
    const { name, maxQueueSize = this._defaults.maxQueueSize } = options;
    
    let queue = this._queueMap.get(name);
    if (!queue) {
      queue = [];
      this._queueMap.set(name, queue);
    }
    
    if (queue.length >= maxQueueSize) {
      this._stats.queueFullCount++;
      const error = new Error(`任务队列【${name}】已满(最大${maxQueueSize})`);
      error.type = 'queue_full';
      error.code = 'QUEUE_FULL';
      return Promise.reject(error);
    }
    
    return new Promise((resolve, reject) => {
      const queueItem = {
        options,
        resolve,
        reject,
        enqueuedAt: Date.now()
      };
      
      queue.push(queueItem);
      
      if (options.timeout > 0) {
        queueItem.timeoutTimer = setTimeout(() => {
          const index = queue.indexOf(queueItem);
          if (index > -1) {
            queue.splice(index, 1);
            const error = new Error(`任务【${name}】在队列中等待超时`);
            error.type = 'queue_timeout';
            error.code = 'QUEUE_TIMEOUT';
            reject(error);
            
            if (queue.length === 0) {
              this._queueMap.delete(name);
            }
          }
        }, options.timeout);
      }
    });
  }

  /**
   * 处理队列中的下一个任务(使用微任务)
   */
  async _processNextInQueue(name) {
    const queue = this._queueMap.get(name);
    if (!queue || queue.length === 0) {
      this._queueMap.delete(name);
      return;
    }
    
    // 使用微任务处理,避免 setTimeout 的延迟
    await Promise.resolve();
    
    const queueItem = queue.shift();
    
    if (queueItem.timeoutTimer) {
      clearTimeout(queueItem.timeoutTimer);
    }
    
    try {
      const result = await this._executeTask(queueItem.options);
      queueItem.resolve(result);
    } catch (error) {
      queueItem.reject(error);
    } finally {
      // 继续处理下一个(递归)
      if (queue.length > 0) {
        // 再次使用微任务
        Promise.resolve().then(() => this._processNextInQueue(name));
      } else {
        this._queueMap.delete(name);
      }
    }
  }

  /**
   * 执行任务核心逻辑
   */
  async _executeTask(options) {
    const {
      name,
      asyncFn,
      timeout = this._defaults.timeout,
      retryCount = 0,
      baseRetryDelay = 1000,
      maxRetryDelay = 30000,
      retryCondition = null // 自定义重试条件函数
    } = options;
    
    const lockItem = this._acquireLock(name);
    if (!lockItem) {
      const error = new Error(`无法获取锁【${name}】`);
      error.type = 'lock_failed';
      error.code = 'LOCK_FAILED';
      throw error;
    }
    
    let result;
    try {
      if (timeout > 0) {
        lockItem.timeoutTimer = setTimeout(() => {
          const timeoutError = new Error(`任务【${name}】超时(${timeout}ms)`);
          timeoutError.type = 'timeout';
          timeoutError.code = 'TIMEOUT';
          lockItem.abortController.abort(timeoutError);
        }, timeout);
      }
      
      result = await this._executeWithExponentialBackoff(
        () => asyncFn(lockItem.abortController.signal),
        retryCount,
        baseRetryDelay,
        maxRetryDelay,
        lockItem.abortController,
        retryCondition // 传递重试条件
      );
      
      return result;
      
    } catch (error) {
      error.lockName = name;
      error.taskId = lockItem.taskId;
      throw error;
      
    } finally {
      this._cleanupLock(name, lockItem, options.autoCleanup ?? this._defaults.autoCleanup);
      
      // 使用微任务处理下一个队列任务
      Promise.resolve().then(() => this._processNextInQueue(name));
    }
  }

  /**
   * 判断是否应该重试(支持自定义重试条件)
   */
  _shouldRetry(error, retryCondition) {
    // 不重试的错误类型
    const noRetryTypes = ['cancel', 'timeout', 'queue_full', 'queue_timeout', 'lock_failed'];
    if (noRetryTypes.includes(error.type)) {
      return false;
    }
    
    // 如果提供了自定义重试条件函数,使用它
    if (typeof retryCondition === 'function') {
      return retryCondition(error);
    }
    
    // 默认重试条件:非特定错误都重试
    return true;
  }

  /**
   * 指数退避重试执行(支持自定义重试条件)
   */
  async _executeWithExponentialBackoff(fn, maxRetries, baseDelay, maxDelay, abortController, retryCondition) {
    let lastError;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        if (abortController.signal.aborted) {
          const cancelError = new Error('任务已被取消');
          cancelError.type = 'cancel';
          cancelError.code = 'CANCELLED';
          throw cancelError;
        }
        
        if (attempt > 0) {
          const delay = this._calculateExponentialBackoffDelay(
            attempt,
            baseDelay,
            maxDelay
          );
          
          this._stats.retryCount++;
          console.log(`任务重试第${attempt}次,延迟${delay}ms`);
          
          await this._sleep(delay, abortController.signal);
        }
        
        return await fn();
        
      } catch (error) {
        lastError = error;
        
        // 使用统一的判断逻辑决定是否重试
        if (!this._shouldRetry(error, retryCondition)) {
          throw error;
        }
        
        if (attempt === maxRetries) {
          error.retryAttempts = attempt;
          throw error;
        }
      }
    }
    
    throw lastError;
  }

  /**
   * 计算指数退避延迟
   */
  _calculateExponentialBackoffDelay(attempt, baseDelay, maxDelay) {
    const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
    const jitter = exponentialDelay * 0.1 * Math.random();
    return Math.min(exponentialDelay + jitter, maxDelay);
  }

  /**
   * 可中断的延时(安全的事件监听清理)
   */
  _sleep(ms, signal) {
    return new Promise((resolve, reject) => {
      if (signal.aborted) {
        reject(new Error('等待被中断'));
        return;
      }
      
      const timer = setTimeout(() => {
        // 清理事件监听
        signal.removeEventListener('abort', abortHandler);
        resolve();
      }, ms);
      
      const abortHandler = () => {
        clearTimeout(timer);
        const error = new Error('等待被中断');
        error.type = 'cancel';
        error.code = 'SLEEP_CANCELLED';
        reject(error);
      };
      
      signal.addEventListener('abort', abortHandler);
      
      // 确保在Promise settled后清理
      const cleanup = () => {
        clearTimeout(timer);
        signal.removeEventListener('abort', abortHandler);
      };
      
      // 无论成功还是失败都执行清理
      this._safeFinally(() => {
        cleanup();
      }, resolve, reject);
    });
  }

  /**
   * 安全的finally执行,避免影响原始Promise
   */
  _safeFinally(cleanupFn, resolve, reject) {
    const wrappedResolve = (value) => {
      try {
        cleanupFn();
      } finally {
        resolve(value);
      }
    };
    
    const wrappedReject = (error) => {
      try {
        cleanupFn();
      } finally {
        reject(error);
      }
    };
    
    return { resolve: wrappedResolve, reject: wrappedReject };
  }

  /**
   * 清理锁资源
   */
  _cleanupLock(name, lockItem, autoCleanup) {
    if (lockItem.timeoutTimer) {
      clearTimeout(lockItem.timeoutTimer);
      lockItem.timeoutTimer = null;
    }
    
    if (lockItem.abortController) {
      lockItem.abortController = null;
    }
    
    if (autoCleanup) {
      this._lockMap.delete(name);
    } else {
      lockItem.locked = false;
      lockItem.abortController = null;
      lockItem.timeoutTimer = null;
    }
  }

  /**
   * 清理过期锁和队列
   */
  _cleanupExpiredLocks() {
    const now = Date.now();
    const maxAge = this._defaults.maxLockAge;
    
    // 清理过期锁
    for (const [name, lockItem] of this._lockMap.entries()) {
      if (lockItem.locked && (now - lockItem.createdAt) > maxAge) {
        console.warn(`清理过期锁【${name}】,已锁定${now - lockItem.createdAt}ms`);
        
        const error = new Error('锁过期自动清理');
        error.type = 'timeout';
        error.code = 'LOCK_EXPIRED';
        
        if (lockItem.abortController) {
          lockItem.abortController.abort(error);
        }
        
        this._lockMap.delete(name);
      }
    }
    
    // 清理过期队列项
    for (const [name, queue] of this._queueMap.entries()) {
      const expiredItems = [];
      
      for (let i = 0; i < queue.length; i++) {
        const item = queue[i];
        const queueAge = now - item.enqueuedAt;
        const timeout = item.options?.timeout || 30000;
        if (queueAge > timeout) {
          expiredItems.push(i);
        }
      }
      
      for (let i = expiredItems.length - 1; i >= 0; i--) {
        const index = expiredItems[i];
        const item = queue[index];
        
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        
        const error = new Error(`任务【${name}】在队列中过期`);
        error.type = 'queue_timeout';
        error.code = 'QUEUE_TIMEOUT';
        item.reject(error);
        
        queue.splice(index, 1);
      }
      
      if (queue.length === 0) {
        this._queueMap.delete(name);
      }
    }
  }

  /**
   * 执行带锁的异步任务
   */
  async execute(options) {
    const {
      name,
      asyncFn,
      onSuccess,
      onFail,
      repeatTip = this._defaults.repeatTip,
      timeout = this._defaults.timeout,
      throwRepeatError = this._defaults.throwRepeatError,
      tipHandler = this._defaults.tipHandler, // 使用配置的默认值
      enableQueue = false,
      maxQueueSize = this._defaults.maxQueueSize,
      retryCount = 0,
      baseRetryDelay = 1000,
      maxRetryDelay = 30000,
      retryCondition = null, // 自定义重试条件
      autoCleanup = this._defaults.autoCleanup
    } = options;

    this._stats.totalExecutions++;

    try {
      const existingLock = this._lockMap.get(name);
      if (existingLock?.locked) {
        this._stats.repeatRejectCount++;
        
        const repeatError = new Error(repeatTip);
        repeatError.type = 'repeat';
        repeatError.code = 'LOCKED';
        repeatError.lockName = name;
        
        tipHandler(repeatTip);
        
        if (enableQueue) {
          console.log(`任务【${name}】加入等待队列,当前队列长度:${this._queueMap.get(name)?.length || 0}`);
          
          const queueOptions = {
            ...options,
            enableQueue: false,
            maxQueueSize: undefined
          };
          
          const queueResult = await this._addToQueue({
            ...queueOptions,
            name,
            maxQueueSize
          });
          
          onSuccess?.(queueResult);
          return queueResult;
        } else {
          onFail?.(repeatError);
          if (throwRepeatError) throw repeatError;
          return Promise.reject(repeatError);
        }
      }

      const result = await this._executeTask({
        name,
        asyncFn,
        timeout,
        retryCount,
        baseRetryDelay,
        maxRetryDelay,
        retryCondition, // 传递重试条件
        autoCleanup
      });

      this._stats.successCount++;
      onSuccess?.(result);
      return result;
      
    } catch (error) {
      switch (error.type) {
        case 'timeout':
          this._stats.timeoutCount++;
          break;
        case 'cancel':
          this._stats.cancelCount++;
          break;
        case 'queue_full':
          this._stats.queueFullCount++;
          break;
      }
      
      onFail?.(error);
      throw error;
    }
  }

  /**
   * 手动释放指定锁
   */
  releaseLock(name) {
    const lockItem = this._lockMap.get(name);
    if (lockItem) {
      this._cleanupLock(name, lockItem, true);
    }
    
    const queue = this._queueMap.get(name);
    if (queue) {
      queue.forEach(item => {
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        const error = new Error('锁被手动释放,队列任务取消');
        error.type = 'cancel';
        error.code = 'MANUAL_RELEASE';
        item.reject(error);
      });
      this._queueMap.delete(name);
    }
  }

  /**
   * 批量释放所有锁
   */
  releaseAllLocks() {
    this._lockMap.forEach((lockItem, name) => {
      this._cleanupLock(name, lockItem, true);
    });
    this._lockMap.clear();
    
    this._queueMap.forEach((queue, name) => {
      queue.forEach(item => {
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        const error = new Error('所有锁被释放,队列任务取消');
        error.type = 'cancel';
        error.code = 'ALL_RELEASED';
        item.reject(error);
      });
    });
    this._queueMap.clear();
  }

  /**
   * 取消正在执行的任务
   */
  cancelLockTask(name, reason = "用户主动取消") {
    const lockItem = this._lockMap.get(name);
    if (lockItem?.locked && lockItem.abortController) {
      const error = new Error(reason);
      error.type = 'cancel';
      error.code = 'USER_CANCEL';
      lockItem.abortController.abort(error);
      this._cleanupLock(name, lockItem, true);
      return true;
    }
    return false;
  }

  /**
   * 获取指定任务的锁状态
   */
  getLockStatus(name) {
    const lockItem = this._lockMap.get(name);
    const queue = this._queueMap.get(name);
    
    return {
      locked: lockItem?.locked ?? false,
      taskId: lockItem?.taskId,
      createdAt: lockItem?.createdAt,
      age: lockItem ? Date.now() - lockItem.createdAt : 0,
      hasAbortController: !!lockItem?.abortController,
      queueLength: queue?.length || 0,
      queueWaitTimes: queue?.map(item => Date.now() - item.enqueuedAt) || []
    };
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return {
      ...this._stats,
      activeLocks: Array.from(this._lockMap.entries())
        .filter(([_, lock]) => lock.locked)
        .map(([name, lock]) => ({
          name,
          age: Date.now() - lock.createdAt,
          taskId: lock.taskId
        })),
      waitingQueues: Array.from(this._queueMap.entries())
        .map(([name, queue]) => ({
          name,
          length: queue.length,
          oldestWait: queue.length > 0 ? Date.now() - queue[0].enqueuedAt : 0
        }))
    };
  }

  /**
   * 重置统计信息
   */
  resetStats() {
    this._stats = {
      totalExecutions: 0,
      successCount: 0,
      timeoutCount: 0,
      cancelCount: 0,
      repeatRejectCount: 0,
      queueFullCount: 0,
      retryCount: 0
    };
  }

  /**
   * 销毁实例
   */
  destroy() {
    clearInterval(this._cleanupInterval);
    this.releaseAllLocks();
    this._queueMap.clear();
    this._lockMap.clear();
  }
}

// 创建锁管理器的工厂函数
export const createLockManager = (options) => new LockManager(options);

// 默认单例(无默认控制台警告)
export const defaultLockManager = new LockManager({
  tipHandler: () => {} // 明确指定空函数
});

// 带控制台警告的单例(如果需要)
export const verboseLockManager = new LockManager({
  tipHandler: console.warn
});

// 核心方法导出(使用默认单例)
export const asyncLock = (options) => defaultLockManager.execute(options);
export const releaseLock = (name) => defaultLockManager.releaseLock(name);
export const releaseAllLocks = () => defaultLockManager.releaseAllLocks();
export const cancelLockTask = (name, reason) => defaultLockManager.cancelLockTask(name, reason);
export const getLockStatus = (name) => defaultLockManager.getLockStatus(name);
export const getStats = () => defaultLockManager.getStats();
export const resetStats = () => defaultLockManager.resetStats();
export const destroyLockManager = () => defaultLockManager.destroy();

// 导出类本身
export { LockManager };

/*********************************************************************
 * 使用示例
 *********************************************************************/

/*
// 示例1:基础使用(无控制台警告)
import { asyncLock } from './asyncLock';

const submitForm = async () => {
  try {
    const result = await asyncLock({
      name: 'formSubmit',
      asyncFn: async (signal) => {
        if (signal.aborted) throw new Error('任务已被取消');
        return await api.submit(data);
      },
      timeout: 8000,
      retryCount: 2,
      baseRetryDelay: 1000,
      maxRetryDelay: 10000,
      onSuccess: (res) => console.log('提交成功:', res),
      tipHandler: (msg) => console.warn(msg) // 需要时才传入
    });
  } catch (err) {
    console.error('捕获到错误:', err);
  }
};

// 示例2:自定义重试条件
const fetchWithRetry = async () => {
  try {
    const result = await asyncLock({
      name: 'fetchData',
      asyncFn: async (signal) => {
        const response = await fetch('/api/data', { signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
      },
      retryCount: 3,
      retryCondition: (error) => {
        // 只对网络错误和5xx错误重试
        return error.message.includes('Failed to fetch') || 
               error.message.includes('HTTP 5');
      },
      onFail: (error) => {
        if (!error.message.includes('HTTP 4')) {
          console.error('需要重试的错误:', error);
        }
      }
    });
  } catch (err) {
    console.error('最终失败:', err);
  }
};

// 示例3:队列处理
const processWithQueue = async () => {
  try {
    const result = await asyncLock({
      name: 'heavyProcess',
      asyncFn: async (signal) => {
        // 耗时处理
        return await heavyProcessing();
      },
      enableQueue: true,
      maxQueueSize: 10,
      timeout: 30000,
      onSuccess: (res) => {
        console.log('处理完成,结果:', res);
      }
    });
  } catch (err) {
    if (err.code === 'QUEUE_FULL') {
      alert('系统繁忙,请稍后重试');
    }
  }
};

// 示例4:使用verbose版本(需要控制台警告)
import { verboseLockManager } from './asyncLock';

const verboseTask = async () => {
  const result = await verboseLockManager.execute({
    name: 'verboseTask',
    asyncFn: async () => {
      // 任务逻辑
    },
    // 会自动输出控制台警告
  });
};

// 示例5:多个锁管理器实例(隔离环境)
import { createLockManager } from './asyncLock';

const userLockManager = createLockManager({
  maxQueueSize: 5,
  tipHandler: (msg) => Toast.warning(msg)
});

const systemLockManager = createLockManager({
  timeout: 30000,
  tipHandler: console.error
});

// 分别使用
const userTask = async () => {
  await userLockManager.execute({
    name: 'userAction',
    asyncFn: userAction
  });
};

const systemTask = async () => {
  await systemLockManager.execute({
    name: 'systemTask',
    asyncFn: systemTask
  });
};
*/

20张图的保姆级教程,记录使用Verdaccio在Ubuntu服务器上搭建Npm私服

作者 水冗水孚
2025年12月13日 15:59

某些情况下,我们的一些npm包,需要发布到npm上,但是,又不太适合设置成公开的。尽管npm提供了私密包的服务,但是要收钱的,因此,Verdaccio就应运而生了

什么是Verdaccio

简单来说,Verdaccio 是一个轻量级、开源的私有 npm 仓库管理器,就是“自己搭建的 npm 私服”。

核心作用如下:

  1. 替代公共 npm 仓库:你可以把公司内部的私有包、不想公开的代码包发布到这个私服上,只有团队内部能访问;
  2. 可灵活管控权限配置(比如谁能发布 / 下载包)、离线使用,解决公共 npm 访问不稳定、私有代码泄露的问题。

Verdaccio本质是Node.js编写的轻量服务,部署简单,不用依赖复杂的数据库,开箱即用,是中小型团队搭建私有 npm仓库的首选。

0.png

官网:www.verdaccio.org/

搭建记录

乌班图22和node20版本

首先,笔者的服务器是乌班图22,同时node也有是20版本,如下

1.png

笔者查询了一下,node20版本适合6版本的Verdaccio,就直接下载最新版本安装了

全局安装Verdaccio

Ubuntu下加--unsafe-perm避免权限报错

npm install -g verdaccio --unsafe-perm

然后,查看版本号

verdaccio -v
2.png

创建Verdaccio工作目录,并授权

# 创建verdaccio工作目录
mkdir -p /opt/verdaccio/{conf,storage,plugins}

# 授权操作权限
chmod -R 775 /opt/verdaccio
3.png

创建Verdaccio默认配置文件并且编辑

# 进入对应目录
cd /opt/verdaccio/conf/

# 创建配置文件
touch config.yaml

# 查看一下
ls
4.png

然后写入配置

cat > /opt/verdaccio/conf/config.yaml << 'EOF'
# Verdaccio核心配置
storage: /opt/verdaccio/storage
plugins: /opt/verdaccio/plugins

# 日志配置
logs:
  - { type: stdout, format: pretty, level: http }

# 安全配置
security:
  api:
    legacy: false
    jwt:
      sign:
        expiresIn: 29d
  web:
    sign:
      expiresIn: 7d

# 认证配置(密码文件自动生成)
auth:
  htpasswd:
    file: /opt/verdaccio/conf/htpasswd
    max_users: 100

# 上游源,当自己的npm没这个包的时候,往上游找
uplinks:
  npmjs:
    url: https://registry.npmmirror.com/  # 淘宝源
    # url: https://registry.npmjs.org/     # 官方源
    cache: true

# 包权限规则
packages:
  '@*/*':
    access: $all
    publish: $authenticated
    proxy: npmjs
  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs

# 监听所有IP,允许外网访问
listen: 0.0.0.0:4873

# WebUI 配置
web:
  title: 私有NPM仓库
EOF
5.png

顺手给点权限

6.png

启动Verdaccio

verdaccio --config /opt/verdaccio/conf/config.yaml
7.png

输出日志解读如下

日志内容 含义 是否需要处理
root 权限警告 提示不要用 root 运行(安全建议) 可选处理(不影响功能)
logs 配置已废弃 6.x 版本把 logs 字段改名为 log 可选修改(不影响启动)
config file 加载成功 配置文件识别正常 ✅ 无需处理
http address - http://0.0.0.0:4873/ 服务监听在 4873 端口 ✅ 启动成功

防火墙放开4873端口

注意,如果是云服务器,也要在安全组里面放开4873端口

ufw allow 4873/tcp

ufw status

8.png

先通过ip端口方式访问看看

果然是能访问到了,只不过现在仓库是空的

9.png

配置https证书

首先,自然是买了云服务器,就要买对应的https证书,笔者的证书买过了,如下

root@iv-ydy912e3nkay8n6x7ufo:/etc/nginx/certs# ls
ashuai.site.key  ashuai.site.pem

然后到对应目录,修改config.yaml文件,主要是如下修改

# 配置 HTTPS 监听 4873 端口
listen:
  - https://0.0.0.0:4873

# HTTPS 证书配置(用自已有的证书路径)
https:
  key: /etc/nginx/certs/ashuai.site.key    # 私钥
  cert: /etc/nginx/certs/ashuai.site.pem   # 公钥

# 公共 URL(必填,末尾带端口和斜杠)
public_url: https://ashuai.site:4873/

完整配置

# Verdaccio核心配置
storage: /opt/verdaccio/storage
plugins: /opt/verdaccio/plugins

# 日志配置
log:
  - { type: stdout, format: pretty, level: http }

# 安全配置
security:
  api:
    legacy: false
    jwt:
      sign:
        expiresIn: 29d
  web:
    sign:
      expiresIn: 7d

# 认证配置(密码文件自动生成)
auth:
  htpasswd:
    file: /opt/verdaccio/conf/htpasswd
    max_users: 100

# 上游源,当自己的npm没这个包的时候,往上游找
uplinks:
  npmjs:
    url: https://registry.npmmirror.com/  # 淘宝源
    # url: https://registry.npmjs.org/     # 官方源
    cache: true

# 包权限规则
packages:
  '@*/*':
    access: $all
    publish: $authenticated
    proxy: npmjs
  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs

# 配置 HTTPS 监听 4873 端口
listen:
  - https://0.0.0.0:4873

# HTTPS 证书配置(用自已有的证书路径)
https:
  key: /etc/nginx/certs/ashuai.site.key    # 私钥
  cert: /etc/nginx/certs/ashuai.site.pem   # 公钥

# 公共 URL(必填,末尾带端口和斜杠)
public_url: https://ashuai.site:4873/

# WebUI 配置
web:
  title: 私有NPM仓库

注意,如果是普通用户,也要授权一下,笔者是root用户,无妨

chmod 644 /etc/nginx/certs/ashuai.site.key
chmod 644 /etc/nginx/certs/ashuai.site.pem

用https的方式进行访问

先停掉原先的服务

10.png

然后,用pm2进行管理私服npm(强烈推荐)

这里使用pm2启动私服npm(顺手命名为private-npm)

pm2 start verdaccio --name "private-npm" -- --config /opt/verdaccio/conf/config.yaml

然后查看一下状态

pm2 list

如下图

11.png

当然,大家也可以设置为开机自启动,这里不赘述

然后,就可以通过域名+端口的形式进行访问了

12.png

至此,私服npm就搭建成功了(当然,目前还没有包)

接下来,我们简单演示一下使用

私服npm创建用户名和密码,可用于公司同事用户登录

我们知道npm都有对应的账号,所以,我们需要在服务器上,创建对应用户名和密码

首先,安装工具apache2-utils

Apache 提供的一个用于管理 .htpasswd 用户认证文件的工具(常被 Verdaccio、Nginx 等借用)

sudo apt update
sudo apt install apache2-utils

创建新用户,假设名字叫做admin

sudo htpasswd -B -C 10 -c /opt/verdaccio/conf/htpasswd admin

系统会提示我们输入并确认密码,之后就会生成 /opt/verdaccio/conf/htpasswd 文件。

这个时候,用户名和密码都有了,我们后续就可以登录了

root@iv-ydy912e3nkay8n6x7ufo:/opt/verdaccio/conf# ls
config.yaml  htpasswd

顺手查看一下htpasswd,输出安装路径

root@iv-ydy912e3nkay8n6x7ufo:~# which htpasswd
/usr/bin/htpasswd

使用nrm管理源,并登录

这里笔者建议,使用nrm管理一下源,如下,全局安装一下

13.png

添加源自己的私有源,起个名字,叫做self-npm

C:\Users\lss13>nrm add self-npm https://ashuai.site:4873/
SUCCESS  Add registry self-npm success, run nrm use self-npm command to use self-npm registry.

使用自己的源

C:\Users\lss13>nrm use self-npm
SUCCESS  The registry has been changed to 'self-npm'.

使用服务器上,创建的用户名和密码,登录自己的源,再查看当前登录的是谁

14.png

在自己的源里面发布一个测试包

因为,我们先前已经登录过了,现在只需要创建一个包,并直接发布到私服npm上即可

创建如下

15.png

然后,发包

16.png

当然,我们可以在package.json里面写一些我们的信息啥的,不赘述

由上图可以看到发布成功了,接下来,我们到服务器上看看

17.png

到目前为止,我们发布成功了

再创建一个项目,下载使用我们刚刚发布的包

下载

18.png

打开node_modules文件夹看看,有的

19.png

至此,基本搭建完成、可正常发布公司私有包,下载公司私有包.

剩下的,就是一些自由的设置操作了,当然,私服都是在内网,笔者为了给大家呈现效果,特地部署在公网上了,后续会关掉

收益......

A good memory is better than a bad pen. Record it ...

一文带你掌握 JSONP:从 Script 标签到手写实现

作者 shoa_top
2025年12月13日 15:56

一、JSONP 是什么?用来做什么?

JSONP(JSON with Padding)诞生于 CORS 尚未普及的年代,是前端解决 “跨域 GET 请求” 的鼻祖级方案。核心思想:

利用 <script> 标签没有同源限制的特性,让服务器把数据“包”成一段 JavaScript 函数调用返回,浏览器执行后即可拿到数据。

  • 只能发 GET
  • 兼容 IE6+
  • 无需任何浏览器插件或 CORS 配置

在现代前端,JSONP 已逐渐被 CORS 取代,但仍在 老旧系统、第三方统计脚本、CDN 回调 等场景活跃,同时也是 面试常考题


二、Script 标签及其属性回顾

属性 作用 对 JSONP 的影响
src 发起 GET 请求加载外部 JS 核心字段,承载接口地址 + 查询参数
async 异步加载,不保证执行顺序 默认行为,JSONP 无需顺序
defer 异步但 DOM 后再执行 一般不用,防止延迟
crossorigin 开启 CORS 错误详情 JSONP 不需要,否则报错
onload / onerror 监听加载成功/失败 可用来做 超时/异常 处理

关键特性

  1. <script src="xxx"> 不受同源限制
  2. 下载完成后立即在全局作用域执行
  3. 不会把响应文本暴露给 JS,只能靠“执行后的副作用”拿数据

三、Callback 是怎么传递与执行的?

① 传递:前端 → 后端

  1. 前端生成全局唯一函数名(如 jsonp_1710000000000
  2. 把函数名作为 GET 查询参数拼到 script 的 src:
    https://api.example.com/jsonp?callback=jsonp_1710000000000&id=123
    
  3. window 上挂同名函数:
    window[jsonp_1710000000000] = function (data) { /* 处理数据 */ };
    

② 执行:后端 → 浏览器

  1. 服务器读取 req.query.callback(即 jsonp_1710000000000
  2. 把数据包进该函数名,返回一段可执行 JS
    Content-Type: text/javascript
    
    响应体:
    jsonp_1710000000000({"name": "jsonp-demo"});
    
  3. 浏览器下载完后立即在全局作用域执行上述代码 →
    函数被调用,参数即为数据,副作用完成

③ 清理:前端自己

执行完立即 delete window[jsonp_1710000000000] 并移除 <script>,防止堆积。


四、手写一个简洁版 JSONP(含超时 + 错误)

function jsonp(url, data = {}, timeout = 7000) {
  return new Promise((resolve, reject) => {
    const cb = `jp_${Date.now()}`;
    const script = document.createElement('script');
    const timer = setTimeout(() => cleanup(reject('timeout')), timeout);

    window[cb] = (data) => cleanup(resolve(data));

    function cleanup(fn) {
      clearTimeout(timer);
      script.remove();
      delete window[cb];
      fn();
    }

    script.onerror = () => cleanup(reject('script error'));
    script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${cb}&${new URLSearchParams(data)}`;
    document.head.appendChild(script);
  });
}

/* 使用 */
jsonp('https://api.example.com/jsonp', { id: 123 })
  .then(console.log)   // { id: '123', name: 'jsonp-demo' }
  .catch(console.error);

五、常见问题与坑

问题 原因 解决
返回纯 JSON 报语法错 <script> 期望 JS 而非 JSON 服务器务必返回 callback(JSON);
无法捕捉 HTTP 状态码 <script> 只有 onload/onerror onerror + 超时做模糊失败处理
只能 GET <script> 天生 GET 换 CORS 或代理
回调名冲突 全局变量重名 使用时间戳+随机数唯一化

六、今天还用 JSONP 吗?

  • 新项目:优先 CORS,简单、标准、支持所有 HTTP 方法
  • 老系统/统计脚本/CDN:JSONP 仍活跃,零配置跨域不可替代
  • 面试:手写 JSONP 是高频手写题,考察 Promise + Script 加载 + 全局回调 综合功底

七、一句话总结

JSONP = <script> 无同源限制 + 服务器包成 JS 函数调用 + 全局回调收数据
“下载即执行,执行即回调”——掌握它,跨域历史就懂了一半!

如何正确实现圆角渐变边框?为什么 border-radius 对 border-image 不生效?

作者 三十_
2025年12月13日 14:51

在项目中需要实现一个圆角渐变边框效果。

image-20251212075318921.png

我的第一反应是使用 border-radiusborder-image。然而实践后发现 border-radiusborder-image 不生效效果是这样的:

image-20251201193414791.png

给 div 设置了 border-radius,但边框仍然是直角。


为什么 border-radiusborder-image 失效?

两者的工作层级不同

  • border-radius 作用在 div 元素上,它控制的是整个 div 轮廓的圆角。
  • border-image 绘制边框,是独立于 div 之外的,是脱离于 div 的。

所以,看到的效果就是边框依然是直角,而 div 是圆角。

实现方案

主要通过两点来实现:

  • 创建一个稍大于主元素的伪元素,并设置渐变背景。
  • 使用CSS遮罩"挖空"中间部分,只留下边框区域。

代码如下:

.gradient-border-box {
    width: 100px;
    height: 100px;
    border-radius: 6px;
    position: relative;
}

.gradient-border-box::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 6px; /* 和主元素相同的圆角 */
    padding: 1px; /* 边框宽度 */
    
    /* 渐变效果 */
    background: linear-gradient(360deg, 
        rgba(96, 161, 250, 0.5), 
        rgba(96, 161, 250, 1));
    
    /* CSS遮罩 */
    mask: 
        linear-gradient(#fff 0 0) content-box, 
        linear-gradient(#fff 0 0);
    mask-composite: exclude;
}

方案解析

  • 主元素: 负责内容区域和圆角,只设置 border-radius

  • 伪元素: 负责绘制渐变边框,它的位置与大小覆盖主元素,通过:

    • background 绘制渐变
    • padding 控制边框宽度
    • mask 挖空中间区域

伪元素中的遮罩详解

mask: 
    linear-gradient(#fff 0 0) content-box, 
    linear-gradient(#fff 0 0);
mask-composite: exclude;
  • -webkit-mask: 这行代码创建了两个完全相同的白色矩形遮罩,第一个仅作用于内容区域(content-box);第二个作用域整个元素区域(border-box)。

    第一个 linear-gradient(#fff 0 0) 创建一个纯白色矩形(线性渐变,从0%到0%);

    content-box 指定遮罩的参考框,作用域元素的内容区域(不包括padding、border);

    第二个 linear-gradient(#fff 0 0) 创建了一个白色矩形,默认是 border-box(包括内容+padding+border)。

  • mask-composite:exclude: 控制多个遮罩如何组合exclude 代表异或操作

    结合遮罩解释就是: 边框区域 = border-box(整个区域) - content-box(中心区域)

🎉 React 的 JSX 语法与组件思想:开启你的前端‘搭积木’之旅(深度对比 Vue 哲学)

2025年12月13日 14:46

嘿,未来的前端大神们!👋 欢迎来到 React 的世界!如果你正在寻找一个现代、高效、充满乐趣的前端框架,那么恭喜你,你找对地方了!

在 React 中,有两个核心概念你必须掌握:JSX 语法组件化思想。它们不仅是 React 的“招牌”,也是你实现炫酷用户界面的“魔法棒”。

别担心,React 并没有传说中那么“高冷”,只要理解了它的核心哲学,特别是通过与 Vue 等框架的对比,你就会发现这是一个充满乐趣的“搭积木”游戏!

本文将结合一个仿掘金首页的示例,带你一起深入理解 JSX 和组件的魅力,并穿插讲解 React 这种**“All in JavaScript”的纯粹思想与 Vue 的“关注点分离”**哲学有何不同。


🚀 一、现代前端框架的魅力与 React 的“激进”哲学

在深入 JSX 之前,我们先来聊聊 Vue、React 等现代前端框架的共同特点和它们之间的根本区别:

1. 现代框架的共同点

  • 响应式(Reactive):数据(State/状态)发生变化时,UI 界面会自动、高效地同步更新。
  • 数据绑定(Data Binding): 建立数据和 DOM 之间的连接。
  • 组件化(Component-Based):组件为基本开发单位,像搭积木一样来组合页面。

2. React 与 Vue 的哲学差异:关注点如何分离?

尽管都推崇组件化,但在如何组织一个组件内部的职责上,React 和 Vue 采取了截然不同的路径:

特性 React 哲学 (激进) Vue 哲学 (渐进)
关注点分离 技术融合: 关注一个功能/组件的完整性。将模板(UI)、逻辑、样式都写在 JavaScript 文件内。 技术分离: 关注 HTML/CSS/JS 三种技术的传统分工。在一个 .vue 文件中用 <template><script><style> 分块。
模板语法 JSX (XML in JS): 在 JavaScript 中写类似 HTML 的结构。 HTML 扩展:<template> 块中使用标准的 HTML,通过 v-bindv-on 等指令扩展其能力。
入门门槛 相对较高,需要先适应在 JS 中写 UI 的 JSX 语法。 相对较低,接近传统前端开发习惯(HTML 模板)。

🌟 React 的“激进”之处:

React 认为,一个组件的 UI 描述(JSX)、逻辑(JS 代码)和样式(可选的 CSS 模块化)应该紧密地封装在一起,形成一个功能完整的单元。它更推崇**“关注点分离”而非“技术分离”**。实现这一点的核心,就是 JSX


✨ 二、JSX 语法:XML in JavaScript 的魔法

1. 什么是 JSX?(React 的模板语法)

在 React 的世界里,你不再需要一个单独的模板文件来描述 UI。你直接在 JavaScript 代码里写类似 HTML 的结构!这种在 JavaScript 中书写 XML/HTML 结构的语法扩展,就是 JSX (JavaScript XML)

核心定义: JSX 是 React 中用于描述用户界面语法拓展。它本质上是 XML 的一个特定应用,将 UI 描述(原本的 HTML 职责)和逻辑控制(原本的 JS 职责)完美地融合在一起。

在我们的示例代码中,以下两行代码完美地诠释了 JSX 作为语法糖(Syntactic Sugar)的作用:

JavaScript

// 语法糖:用类似 HTML 的 JSX 结构来描述 UI,可读性极高
const element = <h2>JSX 是 React 中用于描述用户界面的语法拓展</h2>;

// 原始写法:使用 React.createElement API,繁琐且可读性差
import { createElement } from 'react'; // 引入 createElement
const element2 = createElement("h2", null, "JSX 是 React 中用于描述用户界面的语法拓展");

结论: JSX 是为了简化模板开发提升代码可读性而生的。在底层,它会被像 Babel 这样的工具编译成 React.createElement() 函数调用,最终创建出 React 元素(Elements)。

2. JSX 与 Vue 模板的对比

特性 React (JSX) Vue (Template)
数据展示 使用 {变量/表达式} 使用 {{ 变量/表达式 }} (双花括号)
类名属性 必须使用 className 使用标准的 class 属性
条件渲染 使用 JavaScript 表达式{condition ? <A/> : <B/>}{condition && <A/>} 使用 特殊指令<p v-if="condition">A</p><p v-else>B</p>
列表渲染 使用 JavaScript 数组方法{list.map((item) => <li key={item.id}>{item.title}</li>)} 使用 特殊指令<li v-for="item in list" :key="item.id">{{ item.title }}</li>

对比总结: Vue 倾向于在 HTML 模板中引入 新的语法和指令(如 v-if, v-for)来实现逻辑控制;而 React (JSX) 则直接利用原生 JavaScript 的强大表现力(如三元运算符、.map() 方法)来实现模板逻辑。这要求开发者对 JavaScript 更加熟悉。

示例:在 JSX 中嵌入 JavaScript 表达式

你可以在 JSX 中使用单花括号 {} 来嵌入任何有效的 JavaScript 表达式

JavaScript

// 在 <span> 标签内嵌入 getname 变量的值
<h1>Hello <span className="title">{getname}!</span></h1>

// 列表渲染:利用 JS 的 .map() 方法和三元表达式进行条件渲染
{
    // 如果 gettodos.length > 0 为真,则渲染 ul 列表,否则渲染 <p>
    gettodos.length > 0 ? (<ul>
        {/* 原生JS react 能不用新语法,就不用。我们直接使用原生 map */}
        {gettodos.map((item) => {
            return (
                // 迭代生成的元素必须要有 key 属性,帮助 React 识别哪些项发生了变化
                <li key={item.id}>
                    {item.title}
                </li>
            )
        })}
    </ul>) : (
        <p>暂无待办事项</p>
    )
}

🏗️ 三、组件化:从“砖头沙子”到“根组件”

1. 以组件为基本单位(组件是函数)

在 React 中,你工作的基本单位不再是孤立的 HTML 元素,而是组件(Component)

简单回答: 返回 JSX 的函数就是组件。

例如,我们的掘金首页示例:

JavaScript

// 函数名(约定以大写字母开头)就是组件名
function JuejinHeader() {
  return (
    // jsx 最外层只能有一个元素
    <div>
      <header>
        <h1>JueJin首页</h1>
      </header>
    </div>
  )
}

// ... 其他子组件
  • App根组件,它渲染了整个页面。
  • 组件树: 这种结构清晰地展示了组件树如何取代传统的 DOM 树,成为我们审查和组织网页结构的主要方式。

2. 根组件的挂载:一切的起点

无论是 React 还是 Vue,应用都需要一个挂载点,即在 HTML 页面中找到一个元素,将整个组件应用渲染进去。React 的现代挂载方式清晰地体现了其纯粹的组件思想

JavaScript

// index.js 或 main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

// 挂载根组件
// react 一开始就是组件思想,非常的纯粹
const root = document.getElementById('root'); // 1. 找到 HTML 中的挂载点 <div id="root"></div>
createRoot(root).render( // 2. 创建 React 根容器,并调用 render 方法渲染
  <StrictMode>
    <App /> {/* 3. 将根组件 <App /> 渲染到根容器中 */}
  </StrictMode>,
)

代码分析:

  1. createRoot(element):这是 React 18+ 的新 API,用于创建 React 应用程序的根容器,它支持并发渲染(Concurrent Mode)等新特性。
  2. App /> 这就是根组件。可以看到,整个应用的起点就是组件App 组件返回的 JSX 结构,将从这里开始,被 React 渲染成最终的 DOM 结构。
  3. <StrictMode> 严格模式,它不会渲染可见的 UI,但会为它内部的组件(包括 App)启用额外的检查和警告,帮助我们在开发过程中及早发现潜在问题。

🧠 四、组件的“数据之魂”:useState 详解

仅仅会写 UI 描述(JSX)是不够的,一个组件必须能够管理自己的数据状态和业务逻辑。在函数式组件中,我们通过 React Hooks 来实现这些功能,其中最基础的就是 useState

1. useState 的作用与结构

useState Hook 的作用就是为函数组件添加“状态”(State)管理的能力。它接收一个参数(状态的初始值),并返回一个包含两个元素的数组

JavaScript

import { useState } from 'react';

function App() {
    // [当前状态值, 更新状态值的函数] = useState(初始值);
    const [getname, setName] = useState("vue"); 
    const [gettodos, setTodos] = useState([
        // ... 待办事项初始数据
    ]);
    const [isloggedIn, setIsLoggedIn] = useState(true);
    // ...
}

① 数组的第一个元素:状态值(如 getname

它是当前时刻的状态值,用于在 JSX 中展示。

② 数组的第二个元素:更新状态的函数(如 setName

这是一个专用的函数,用于更新状态值

⚠️ 关键点:

  1. 不可直接修改状态值:你必须调用更新函数,例如 setName("react")
  2. 触发重新渲染:当调用这个更新函数时,React 会检测到状态变化,重新执行组件函数(即 App 函数),从而实现响应式更新 UI。

2. useState 与 Vue 响应式数据的对比

特性 React (useState) Vue (Composition API / Setup)
数据定义 const [value, setValue] = useState(initial) const value = ref(initial)
数据访问 直接使用 value 需要使用 value.value (在 <script> 块中)
数据更新 调用专用的更新函数:setValue(newValue) 直接赋值:value.value = newValue
更新机制 组件重新执行 (Re-render) :更新状态后,React 重新渲染整个函数组件。 精确追踪 (Proxy) :Vue 通过 Proxy 机制精确追踪哪些地方使用了数据,只重新渲染使用该数据的部分 DOM。

对比总结: Vueref/reactive劫持数据,更新数据就是直接赋值;而 ReactuseState函数调用,更新状态就是调用一个 setter 函数,触发组件重新渲染。这是两者响应式机制的根本区别。

示例:通过事件更新状态

JavaScript

const toggleLogin = () => {
    // 调用 setIsLoggedIn,传入新值(当前值的反面),触发 App 组件重新渲染
    setIsLoggedIn(!isloggedIn);
}

return (
    <>
      {/* 状态依赖 UI 自动更新 */}
      {isloggedIn?<p>已登录</p>:<p>未登录</p>}
      <button onClick={toggleLogin}> {/* onClick 绑定 JS 函数 */}
          {isloggedIn?"退出登录":"登录"}
      </button>
    </>
)

💻 五、JSX 的底层真相与 createElement 的角色

1. JSX 是什么?

JSX 仅仅是语法糖。它需要被编译(通常由 Babel 完成)才能被浏览器识别。

2. createElement 做了什么?

JSX 的编译目标就是 React.createElement() 函数。

  • JSX 形式:

    JavaScript

    const element = <h2>JSX 是 React 中用于描述用户界面的语法拓展</h2>
    
  • 编译后的原始 React API 形式:

    JavaScript

    // createElement(type, props, ...children)
    const element2 = createElement("h2", null, "JSX 是 React 中用于描述用户界面的语法拓展");
    

createElement 函数的职责是创建出一个 React 元素(React Element) ,这是一个轻量级的 JavaScript 对象,它描述了你希望在屏幕上看到什么。它是 React 应用的骨架,是对真实 DOM 元素用户自定义组件的一种抽象描述。React 拿到这个描述对象后,会根据它来构建和维护虚拟 DOM(Virtual DOM) ,最终高效地将其同步到浏览器的真实 DOM 上。


🔍 六、实战总结:纯粹的组件思想

通过掘金首页的示例,我们再次明确了 React 的开发模式:

  • 核心思想: 一个组件就是 JSX (UI) + 逻辑 (Hooks/状态/事件) 的完整封装。
  • 开发模式: 组件是由 js/css/html 组合起来,完成一个相对独立的功能。我们像搭积木一样将它们组装起来,从根组件 <App /> 开始,由 React 负责最终的渲染。

React 的这种纯粹的组件和 JavaScript 驱动的思想,使得它在处理大型、复杂应用时,逻辑清晰、边界分明。虽然它要求开发者对 JavaScript 更加依赖,但它带来的强大灵活性和可维护性是其最大的优势。


结语

恭喜你,现在你已经掌握了 React 开发的两大核心武器:JSX 语法组件化思想,并且理解了它与 Vue 哲学的主要区别!

  1. JSX 让你能够在 JavaScript 中优雅地描述 UI。
  2. 组件 让你以模块化、可复用的方式构建应用,从根组件开始。
  3. useState 为你的函数组件注入了响应式的数据活力。
  4. React 与 Vue 的对比让你更深刻地理解了框架背后的设计哲学。

React 的旅程才刚刚开始,接下来还有 useEffectuseContext 等更多强大的 Hooks 等待你去探索。记住,多写多练,将每一个 UI 模块都看作一个独立的组件,你很快就能成为一名优秀的 React 开发者!

vscode编写vue代码的时候一聚焦就代码块变白?怎么回事如何解决

2025年12月13日 14:37

起因

今天打开vscode编写vue代码的时候 发现我的vscode出现了一个小问题,就是我在 template、script、style里面编写代码的时候,我的vscode会将这些结构体全部变成透明色,这样非常的麻烦也不利于我查看编写的代码,这样写起来也非常的难受

04e1eb05-5c04-43f6-a4f7-16033b06aa9c.jpg

解决方法

原因是编辑器默认的折叠策略或 Vue 插件的配置导致<template><script>等块被自动折叠。可以通过以下步骤解决:

1. 调整 VSCode 全局折叠设置

打开设置(快捷键 Ctrl+,),搜索并修改以下配置:

  • 关闭自动折叠总开关:将 Editor: Folding 取消勾选(或在settings.json中添加 "editor.folding": false)。
  • 或修改折叠策略:将 Editor: Folding Strategy 设为 manual(仅允许手动折叠)。

2. 针对 Vue 文件的插件配置

如果使用 Volar(Vue3 官方插件)或 Vetur,需调整插件的折叠配置:

  • Vetur:在设置中搜索 vue.enableFolding,将其设为 false
  • Volar:确保 editor.foldingStrategy 设为 auto,同时避免其他插件(如旧版 Vetur)冲突。

3. 临时展开所有代码

若需快速恢复当前文件的显示,可使用快捷键:

  • 展开所有代码:Ctrl+K Ctrl+0(Windows/Linux)或 Cmd+K Cmd+0(Mac)。

修改后,Vue 文件的<template><script><style>块就不会在聚焦时自动折叠了。

【基础篇007】GeoGebra工具系列_多边形(Polygon)

2025年12月13日 13:18

@心有矩,行有方;不逾界,自成章。——八荒启

GeoGebra工具系列_多边形(Polygon) @TOC

一. 🚀 引言

1. 背景

在传统教学中,画多边形只是个绘图动作。但在 GeoGebra 中,使用 “多边形”工具(或“向量多边形”工具)的瞬间,你创造的是一个活的数学对象。比如,如果你用的是两点定义多边形,那么这两个点就决定了多边形的长、宽、面积、周长;多边形绘制好后会立即在“代数区”同步生成其方程、所有顶点坐标、几何属性,方便关联其他元素;你可以随时拖动任何一个顶点或边,整个多边形及其所有关联数据(面积、对角线长度…)都会实时、连续地变化。那么本章,我们将从交互的角度一起研究一下多边形工具。

文章路径 公众号:八荒启-交互动画 / 创作中心 / 系列教程 / Geogebra从入门到编程全集 / 基础篇
作者 酷酷的脸脸
官方网址 八荒启-交互动画
更新日期 2025.12.13
资源下载 文章配套文件包,公众号内回复“GGB007B”(注意不要换行)

2. 场景

八荒启专精于制作交互动画,比如GGB、Canvas、H5、Unity,本套GGB系列文章主要是以交互动画为大背景,逐步展开具体知识点的讲解。(官网:八荒启-交互动画) 在这里插入图片描述

二、🛠️GeoGebra工具系列_多边形(Polygon)

1. 基石—重新认识“多边形”工具

核心问题:传统的画多边形与GeoGebra里的“画多边形”,本质区别是什么?

(1)多边形不只是图形

对于传统意义上的多边形绘制,主要突出的特点是:静态图形、纯视觉,比如在纸上画一个多边形,更多是一个描绘动作:

  • 它的边看似平行,但不一定真的平行。
  • 它的角看似直角,但精确度依赖画图工具。
  • 画完之后,它就凝固在那里,无法改变。
  • 学生看到的只是“像个多边形的图形”,而不是一个真正带数学性质的对象。

换句话来说:

传统课堂里,多边形是“图像”。你只能看,不能动,也无法从中提取更多数学信息。

但GeoGebra多边形:一个具有“数学生命”的动态对象

GeoGebra中绘制多边形非常简单,只需要激活多边形工具,然后在画布上点击点即可: polygon工具的使用

GGB的基础操作这里就不过多赘述了,摸索一下就好,都非常简单。

所以在这里,多边形就不只是一个图形了: 它自动具备:

  • 顶点坐标(可实时更新)
  • 边长(随拖动自动计算)
  • 角度(保持直角关系)
  • 平行、垂直等约束(软件自动维持)
  • 面积、周长等属性(动态显示)
  • 拖动后仍保持多边形本质(软件保证性质不被破坏)
在 GeoGebra 里,多边形是“数学对象”。你可以拖、可以测、可以变,它始终保持多边形的数学定义。

这里我汇总一下:

维度 传统画法(纸笔) GeoGebra多边形
本质 静态图形 动态数学对象
精确度 依赖手、尺子、绘图技巧 自动保证精确(平行、垂直、直角)
可操作性 画完即固定,无法改变 可拖动、可变形但保持多边形性质
信息可见性 只能看到轮廓 坐标、边长、角度、面积等实时显示
数学关系 需要人为推理或标注 系统自动维护内部数学关系
探索性 很弱,无法实验 很强,可用于观察、猜想、验证定理
教学价值 展示为主 探究为主,可视化数学思想
(2)动态特性展示

如果我们有一个多边形,然后已经显示出了它的周长、面积、内角和,当我们拖动某个点或者某个边的时候当前多边形的性质是这样的: 动态四边形(V1.0) 注意:

Parameter(q1)  // 获取多边形的周长
Area(q1)  // 获取多边形的面积

看到这里是不是已经可以重新认识了多边形,对的,我们可以让学生逐步理解:几何图形是可以“呼吸”的数学对象,而不是死板的图。

如果说前面讲的点、线等都是一些基础操作,那么从这里开始,互动性就真正开始跃然纸上,比如当前的这个动画,学生通过拖动能看到:

  • 改动一个点,为什么好像整个形状都被“牵动”?
  • 面积为何突然变大?
  • 哪些角的变化与哪些边有关?
  • 周长和面积的变化有没有同步关系?

这种体验,比静止的图形更能帮助学生理解:

图形是由一系列约束与关系共同构成的系统,不只是几条线围起来的“形状”。(配套资源见文章头部表格)

(3)数学对象思维

在 GeoGebra 中,用工具创建的多边形(比如刚才提到的多边形)不只是由几条线围成的图,而是一个具有结构、约束和属性的数学对象。这种“对象思维”是传统纸笔几何较难培养的,但在动态几何环境里可以自然生长。 黑板上的多边形 比如在传统几何课中,学生画一个四边形,往往关注的是:边画直了吗?看起来像不像?有没有闭合?这是典型的“图像思维”。

但在 GeoGebra 中,多边形是由一系列点与点之间的关系定义的,顶点是可以移动的对象,边是由顶点实时决定的线段 ,内角与面积是可计算的量,形状随拖动而变动,但结构关系保持,学生逐渐不再“画图”,而是在操控一个由数学约束构成的系统。

为了方便观察,我这里把动态几何下的“观察 → 猜想 → 验证”科学思维与实验性数学整理成一个表格:

阶段 学生活动 示例问题 目标与体验
观察(Observe) 拖动四边形顶点,测量边长、角度、面积 “四边形的内角和是不是总是 360°?” 直接得到现象,理解图形属性随操作变化
猜想(Hypothesize) 对观察结果提出规律或假设 “如果我把一个角拉成凹角,面积为什么变小?”
“对边的关系会不会一直保持?”
培养预测能力与逻辑思维,形成数学假设
验证(Verify) 通过动态拖动、测量、观察来检验猜想 “是不是所有四边形都能分成两个三角形?” 通过实验验证规律,理解数学对象的稳定性与约束关系

重新认识了一下“多边形”工具,你是否有什么收获呢?这里我没有大篇幅讲解如何操作,但是从交互的角度讲解了一下GeoGebra中“多边形”工具的独特含义。最后,我汇总一下数学作为可实验的对象的表现与意义

特性 表现与意义
可试探 学生可以自由改变图形顶点位置,探索不同形状的性质
可调整 图形属性随操作变化,帮助学生发现规律的条件
可实验 通过动态拖动和测量,进行“数学实验”,验证或反驳假设
可验证 数学性质可以被重复测试和观察,不依赖记忆
可反驳 错误假设可以通过操作立即发现,培养批判性思维
数学不是死知识,而是一套可操控、可验证、可推理的结构系统。多边形是培养“数学对象思维”的最佳入口。
——八荒启

2. 解密—工具背后的代数世界

核心问题:当我拖动多边形时,代数区里发生了什么?

在 GeoGebra 中,拖动并不是“随便动一动图形”,而是一次几何操作驱动代数系统实时重算的过程。理解这一点,是从“会用工具”走向“理解工具”的关键一步。

(1)坐标与约束关系

当你用“多边形工具”创建一个多边形时,GeoGebra 首先做的不是画边,而是定义点。比如我们重新打开一个GGB界面,然后用多边形工具创建一个多边形:

在这里插入图片描述 左边代数区可以发现不是只有一句指令,而是有七行:

A=(-6.0965,6.74676)
B=(-10.84531,2.42577)
C=(-3.35845,1.69848)
t1=Polygon(A,B,C)
a=Segment(B,C,t1)
b=Segment(C,A,t1)
c=Segment(A,B,t1)

仔细观察发现,前三行是点的定义,接着是多边形的定义,最后是三个线段的定义,这个就很好理解了:

当你用“多边形工具”创建一个多边形时,GeoGebra 首先做的不是画边,而是定义点。每一个顶点本质上都是一个有序数对:

A(x1,y1),B(x2,y2),C(x3,y3),A(x1,y1),B(x2,y2),C(x3,y3),…

如果我们拖动其中一个顶点,会发现: 请添加图片描述

对应点的坐标在不停的改变,多边形的面积数值在变化(Polygon指令默认返回的是多边形的面积),多边形对应的边也在发生变化。

汇总一下,当你拖动某一个顶点时:

  • 该点的坐标立即发生变化
  • 与它相连的边随之重算
  • 整个多边形的结构被重新计算

虽然学生看到的只是“图形在动”,而代数区中发生的是:一组变量正在被实时更新,这正是GGB创作的灵魂思路。比如我们可以将这些变化的量作为参数去创建其他图形,就可以实现联动效果,这也正是代数约束在后台持续起作用的结果。

(2)即时反馈机制

GeoGebra 的强大之处在于:代数不是事后计算,而是实时反馈。

当我们拖动多边形的一个顶点时:

  • 每条边的长度自动更新(多边形形成的时候软件就会自动创建好)
  • 各个内角大小重新计算(需要使用Angle指令提取)
  • 周长与面积立即刷新(面积已经有了,周长需要用Perimeter指令提取)
  • 对角线同步变化(需要构造,然后提取长度等信息)

慢慢的你会发现,GGB真正强大的功能不是工具栏,恰恰相反,工具栏只占GGB全部功能的2%,而代数区占了全部功能的28%(剩下的70%是GGB的指令脚本系统)

经过前面几篇的学习和了解,恭喜你逐渐入门GGB,那么这里我总结一下GGB创作的核心思路:

代数区是动画“看不见的引擎”,代数区中不断变化的数值,实际上是多边形内部属性的可视化呈现。
主要体现为:
1. 数值的变化 ↔ 图形形状的变化
2. 数值的稳定 ↔ 图形性质的不变

这也是几何画板根本无法和GGB比较的原因,从软件的设计初衷来看,GGB领先于几何画板好几个时代。

当然,学生也可以意识到一点:几何图形的每一次变化,都有代数层面的对应。

(3)从操作到理解

拖动如果只是“好玩”,意义有限,但当拖动与思考结合,就变成了理解的通道。 多边形工具绘制松树(无约束) 多边形工具的使用,已经可以实现很多教育效果,比如通过拖动观察“哪些变,哪些不变”,教师可以引导学生反复拖动多边形,并引导思考:

  • 顶点坐标变了吗?(变)
  • 边长和角度变了吗?(通常变)
  • 多边形的边数变了吗?(不变)
  • 内角和是否保持不变?(多边形内角和的计算)

这种对比帮助学生区分,变量 vs 不变量,是数学思维中极其重要的一步。(当然,创作GGB的话数学思维必不可少)

当然这里只是一个简单的例子,如果我们对这个松树进行一定的关系约束,比如这种:

多边形工具绘制松树(有约束) 那就可以从几何中的“拖动”,逐渐理解到代数中的“变量变化”,再到几何中的“性质保持”,以及代数中的“关系约束”。(当前演示作品配套文件见文章首部表格,制作不复杂,可以参考)

当你能想到图形之所以没变形,是因为某些代数关系一直成立,说明就已经开始用数学对象的语言在思考问题了。

3. 融合—跨领域的连接与应用

核心问题:多边形工具能用在数学之外的场景吗?

在现实世界中,我们几乎看不到“完美的圆”,却到处都是由直线围成的区域。从设计图纸到工程结构,从城市街区到生活空间,多边形是描述现实世界最自然、最常用的数学语言之一。GeoGebra 的多边形工具,正好为这种“现实—数学”的连接提供了桥梁。

比如这个踏板,就是多边形工具最经典的使用: 多边形工具的使用—动画组件 这里我汇总一下多边形工具在整个交互动画领域的常见使用方式:

专题名称 多边形在交互动画中的核心作用 典型制作场景
画面构图 作为背景块面与画面结构单元 场景搭建、画面比例调整
区域划分 划定功能区与交互区 操作区 / 显示区分离
遮罩与限制 限制显示或操作范围 区域内有效交互
角色轮廓 作为物体或角色的简化外形 轻量级动画角色
结构骨架 作为动画的几何骨架 框架、机构演示
变形结构 拖动顶点引发整体变形 拉伸、压缩动画
运动边界 限制对象的运动范围 防止越界
路径依附 为运动对象提供参考边 沿边移动、贴边动画
参数承载 面积、周长作为动态参数 数值驱动动画变化
间接控制 通过形状变化控制动画状态 减少滑块依赖
状态反馈 颜色、透明度变化提示状态 即时反馈
条件触发 作为几何判断条件 简单交互逻辑
场景建模 构建完整交互场景轮廓 情境化动画
教学演示 可被拖动的演示对象 探索式学习
实验操作 支持反复操作与验证 “试一试”型动画
对象组织 作为多个对象的参考基准 动画结构管理
依赖关系 构建对象间的依赖网络 保持系统稳定
动画系统 作为底层建模单元 完整交互动画

本篇讲解的内容稍微倾向于底层逻辑的梳理与交互动画创作思路的培养,当然,也是因为多边形工具非常重要,且是众多动画根基的缘故。动画工具本身非常简单,但是背后涉及到的理论和思路,值得我们深究。

三. ✨结尾

本文配套文件已上传,资料获取方式见文章首部表格。


本文收录于微信公众号:八荒启-交互动画,可点击扫码关注,更多技术咨询与服务,可直接访问官方网站获取:bahuangqi.com/(电脑打开)

React 入门秘籍:像搭积木一样写网页,JSX 让开发爽到飞起!

2025年12月13日 12:24

前言

还在为原生 JS 写页面 “东拼西凑” 头疼?还在为 HTML 和 JS 交互写一堆繁琐逻辑?好了,我们聊了这么久的 JS,相信大家对 JS 已经有了一定的基础了。接下来我们开始接触前端框架,带大家了解--React

具体JS学习请看我的专栏:你不知道的JavaScript

一、React:让前端开发效率翻倍的 JS 框架

相信学习前端的小伙伴对React这个词并不陌生,但又不知道它具体是个啥。大白话来讲,它就是让 JS 开发 “开挂” 的框架。简单来说,React 就是来 “救场” 的JS 框架!它把网页拆成 “组件”,像搭积木一样拼出整个页面,还能用JSX把 JS 和 HTML 揉在一起写,直接让开发效率起飞~

二、开局第一步:搭建 React 项目环境

想玩转React,先把开发环境搭好!主流有两种方式,按需选择:

1. create-react-app:官方 “傻瓜式” 脚手架

React 官方推出的项目创建工具,无需手动配置 webpack、babel 等底层工具,一行命令就能生成完整的 React 项目:

# 创建项目
npx create-react-app my-react-app

# 进入项目目录
cd my-react-app

# 启动项目
npm start

优点是省心、稳定,适合 React 新手入门;缺点是项目体积较大,启动速度稍慢。

2. Vite:新一代 “极速” 构建工具

如今更推荐的轻量化选择,启动速度、热更新效率远超传统脚手架,创建 React 项目更高效:

# 创建Vite+React项目
npm create vite@latest   # latest是选择最新版本

# 你创建的项目名字
my-vite-react

# 选择 React JavaScript 然后一路Enter

# 进入目录
cd my-vite-react

# 安装依赖
npm install

# 启动项目
npm run dev

image.png

启动后就能看到 Vite 默认的 React 项目结构,核心入口文件就是main.jsx—— 这是整个 React 项目的最外层入口,所有组件最终都会通过它挂载到页面上。

三、JSX:React 的 “语法糖”,让 JS 和 HTML 无缝融合

JSX 的本质是 “JS + XML(HTML)”,看似写 HTML,实则是 JS 的语法扩展,用它写界面比原生 JS 简洁 10 倍!但使用时要遵守核心规则:

1. 核心规则:JSX 里只能放 “表达式”,不能放 “语句”

  • 表达式:有返回值的代码(比如变量、算术运算、数组方法、三元运算),用{}包裹就能嵌入 JSX;
  • 语句:无返回值的执行逻辑(比如 if、for 循环),不能直接写在 JSX 里,需转换为表达式形式。

2. 实战 1:列表循环渲染(核心高频场景)

原生 JS 写列表需要手动创建 DOM、循环追加,代码繁琐:

<ul id="ul">
    <!-- 得写 for 循环 + createElement + appendChild -->
</ul>
<script>
    const arr = ['1','2','3'];
    // 手动循环 + 创建 DOM,代码冗余
    for (let i = 0; i < arr.length; i++) {
        const li = document.createElement('li');
        li.textContent = arr[i];
        document.getElementById('ul').appendChild(li);
    }
</script>

这种原生写法用起来就非常麻烦,而且代码也多。但 React 的JSX + 列表渲染,直接 “声明” 要渲染的内容,React 自动帮你生成 DOM:

export default function App() {
    const arr = ['1', '2', '3'];
    // map是表达式,返回新数组,可直接嵌入JSX
    return (
        <ul id="ul">
            {arr.map((item, index) => (
                <!-- 循环渲染必须加 key,唯一标识每一项 -->
                <li key={index}>{item}</li>
            ))}
        </ul>
    );
}

进阶版:渲染复杂数据列表:

export default function App() {
    const songs = [
        { id: 1, name: '稻香' },
        { id: 2, name: '夜曲' },
        { id: 3, name: '晴天' }
    ];
    return (
        <ul>
            {songs.map((item) => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

image.png

3. 实战 2:条件渲染(按需展示界面)

需求:根据条件展示不同内容,不能直接写 if 语句,用三元表达式(表达式)实现:

export default function App() {
    let flag = true;
    return (
        <div>
            {/* 三元表达式是表达式,可嵌入JSX */}
            <h2>{flag ? '我比他帅' : '他比我帅'}</h2>
            {/* 进阶:逻辑与运算,flag为true时才显示 */}
            <p>{flag && '只有flag为真才显示我'}</p>
        </div>
    );
}

image.png

4. 实战 3:样式处理(三种常用方式)

JSX 中写样式和原生 HTML 有区别,结合图片中样式代码,三种方式全覆盖:

(1)行内样式(对象形式)

原生 HTML 用style="color: red",JSX 需用对象包裹,属性名用小驼峰

export default function App() {
  const styleObj = { 
        color: 'red', 
        fontSize: '20px' // 小驼峰,对应 CSS 的 font-size
    };
    return (
        <div>
            <div style={styleObj}>帅哥</div>
            {/* 也可直接写对象 */}
            <div style={{ color: 'blue', fontWeight: 'bold' }}>帅哥</div>
        </div>
    );
}

这里提一嘴,JSX表达式必须要有一个父元素!

image.png

image.png

(2)类名样式(className)

JSX 中不能用class(保留字),需用className,配合 CSS 文件:

/* index.css */
.home {
    background: #f5f5f5;
    padding: 20px;
}
// App.jsx
import './index.css';
export default function App() {
    return <div className="home">首页</div>; // 对应图片中 className 代码
}

image.png(3)动态类名

结合表达式,按需切换样式:

export default function App() {
    const isActive = true;
    return (
        <div className={`box ${isActive ? 'active' : ''}`}>
            动态样式
        </div>
    );
}

5. 实战 4:事件绑定(交互核心)

原生 HTML 用onclick,JSX 用小驼峰onClick,且绑定的是函数(而非字符串):

(1)基础事件绑定

export default function App() {
    // 定义事件处理函数
    const handleClick = () => {
        console.log('点击了div');
    };
    return (
        // 直接绑定函数,不加()(加()会立即执行)
        <div onClick={handleClick}>hello</div>
    );
}

image.png

(2)事件传参

需用箭头函数包裹,才能传递参数:

export default function App5() {
    const songs = [
        { id: 1, name: '稻香' },
        { id: 2, name: '夜曲' }
    ];
    // 带参数的事件函数
    const handler = (name) => {
        console.log('点击了歌曲:', name);
    };
    return (
        <ul>
            {songs.map((item) => (
                <li 
                    key={item.id} 
                    // 箭头函数传参点击时执行handler并传入歌曲名
                    onClick={() => handler(item.name)}
                >
                    {item.name}
                </li>
            ))}
        </ul>
    );
}

image.png

四、组件化:React 的 “灵魂”,像搭积木一样开发

组件化是 React 单页应用的核心,把页面拆成独立、可复用的组件,比如头部、导航、内容区,再像搭积木一样组合。

1. 定义组件(两种方式)

(1)函数组件(推荐)

// components/Head.jsx(头部组件)
export default function Head() {
    return (
        <header>
            <h1>我的React博客</h1>
            <nav>首页 | 文章 | 关于</nav>
        </header>
    );
}

// components/Main.jsx(主体组件)
export default function Main() {
    const songs = [{ id: 1, name: '稻香' }, { id: 2, name: '夜曲' }];
    return (
        <main>
          <h2>热门歌曲</h2>
          <ul>
            {
              songs.map(item => <li key={item.id}>{item.name}</li>)
            }
          </ul>
        </main>
    );
}

(2)组合组件(拼装页面)

// App.jsx(根组件)
import Head from './components/Head';
import Main from './components/Main';

export default function App6() {
    return (
        <div className="app">
            {/* 引入头部组件 */}
            <Head />
            {/* 引入主体组件 */}
            <Main />
        </div>
    );
}

image.png

2. 组件渲染到页面(入口文件)

所有组件最终要通过main.jsx挂载到 DOM 节点:

// main.jsx(项目最外层入口)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

// 找到页面中的root节点,渲染App根组件
ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

五、JSX 进阶:数组处理小技巧

开发中常需要对数组做转换,比如把数字数组放大 10 倍(比如我们在实战1 中使用的 map):

export default function App() {
    const arr = [1, 2, 3, 4];
    // map返回新数组,可直接渲染或赋值使用
    const newArr = arr.map(item => item * 10);
    return (
        <div>
            <p>原数组:{arr.join(',')}</p>
            <p>放大10倍:{newArr.join(',')}</p>
        </div>
    );
}

放在 JS 里可能更好理解:

const arr = [1, 2, 3, 4];
const newArr = arr.map((item, i, array) => {  // [10, 20, 30, 40]
    return item * 10;
})
console.log(newArr);

image.png

六、总结:React 开发核心流程

1.create-react-appVite创建项目;

2.main.jsx中挂载根组件App

3. 拆分子组件(Head、Main 等),用 JSX 编写组件逻辑;

4. 合表达式实现列表渲染、条件渲染、样式处理、事件绑定;

5. 组合组件,完成整个页面开发。

结语

React 的核心就是 “简单”:用 JSX 简化 HTML 和 JS 的交互,用组件化简化页面结构,用声明式 UI 简化 DOM 操作。把这些基础知识点吃透,再结合代码例子实战反复练习,你就能从 React 新手快速进阶!

【AI 编程实战】第 3 篇:后端小白也能写 API:AI 带我 1 小时搭完 Next.js 服务

作者 HashTang
2025年12月12日 16:09

后端 API 开发是很多前端开发者的"心理阴影"——数据库设计、ORM 操作、JWT 认证、错误处理……每一步都是坑。但有了 AI,这一切都变得简单起来。这是《AI 编程实战:TRAE SOLO 全栈开发指南》专栏的第三篇文章,带你用 AI 快速搭建 Next.js 15 后端 API。

一、开篇:前端开发者的后端焦虑

1.1 后端开发的"噩梦"

还记得小何吗?完成前端架构搭建后,他面临一个更大的挑战:后端 API 开发

作为一个前端开发者,小何对后端并不陌生,但每次写后端代码时,总有一种"如履薄冰"的感觉:

数据库表怎么设计?字段类型怎么选?
ORM 怎么用?Prisma 和 TypeORM 有什么区别?
JWT 怎么生成?Token 过期怎么处理?
接口怎么设计才符合 RESTful 规范?
错误处理怎么统一?日志怎么记录?

传统方式下,小何需要:

任务 预计时间
学习 Next.js API 2 小时
数据库表设计 3 小时
Prisma 配置和学习 4 小时
JWT 认证实现 3 小时
登录注册 API 4 小时
响应格式统一 2 小时
错误处理和日志 2 小时
总计 20 小时

实际上,因为各种问题,可能需要 3-4 天

1.2 AI 辅助的后端开发

有了 TRAE SOLO,小何的体验完全不同:

小何:帮我搭建 Next.js 15 后端 API,需要:
      1. MySQL 数据库,使用 Prisma ORM
      2. JWT 认证
      3. 微信小程序登录
      4. 统一的响应格式
      5. 日志记录

AI:好的,我来帮你搭建完整的后端架构:

    ✓ 分析项目结构,确定 Pages Router 模式
    ✓ 设计数据库表结构(用户、会员、账单等)
    ✓ 配置 Prisma ORM 连接 MySQL
    ✓ 实现 JWT 生成和验证
    ✓ 封装微信登录流程
    ✓ 统一响应格式(responseHelper)
    ✓ 配置 Winston 日志系统

    后端架构已生成,请查看...

时间对比

任务 传统方式 AI 辅助 提升倍数
数据库设计 3 小时 15 分钟 12x
Prisma 配置 4 小时 10 分钟 24x
JWT 认证 3 小时 10 分钟 18x
登录 API 4 小时 20 分钟 12x
响应格式统一 2 小时 5 分钟 24x
总计 ~20 小时 ~1 小时 ~20x

二、技术选型:为什么选择 Next.js 15 + Prisma

2.1 Next.js 作为后端的优势

小何:为什么后端要用 Next.js?不是应该用 Express 或 Koa 吗?

AI:Next.js 作为后端有独特优势:

    1. 全栈一体化
       - 前后端可以共享代码和类型
       - 部署更简单,一个服务搞定

    2. API Routes 开箱即用
       - 文件即路由,无需额外配置
       - 支持中间件、动态路由

    3. TypeScript 原生支持
       - 类型安全,减少运行时错误
       - 与 Prisma 完美配合

    4. 生态完善
       - 丰富的中间件和插件
       - 社区活跃,问题容易解决

2.2 Prisma vs TypeORM

小何:Prisma 和 TypeORM 怎么选?

AI:(通过 MCP 查阅最新文档)

    ┌────────────────┬────────────────┬────────────────┐
    │     维度       │     Prisma     │    TypeORM     │
    ├────────────────┼────────────────┼────────────────┤
    │ 类型安全       │ ⭐⭐⭐⭐⭐     │ ⭐⭐⭐⭐       │
    │ 学习曲线       │ ⭐⭐⭐⭐⭐     │ ⭐⭐⭐         │
    │ 迁移管理       │ ⭐⭐⭐⭐⭐     │ ⭐⭐⭐⭐       │
    │ 查询直观性     │ ⭐⭐⭐⭐⭐     │ ⭐⭐⭐         │
    │ 性能           │ ⭐⭐⭐⭐       │ ⭐⭐⭐⭐       │
    │ 生态系统       │ ⭐⭐⭐⭐       │ ⭐⭐⭐⭐⭐     │
    └────────────────┴────────────────┴────────────────┘

    推荐:Prisma

    理由:
    1. Schema 定义直观,一目了然
    2. 自动生成类型,类型安全
    3. 迁移管理简单,prisma migrate 一键搞定
    4. 查询 API 简洁,链式调用友好

2.3 最终技术栈

后端技术栈:
├── 框架:Next.js 15 (Pages Router)
├── 数据库:MySQL
├── ORM:Prisma 4.11
├── 认证:JWT (jsonwebtoken)
├── 加密:bcryptjs
├── 日志:Winston + daily-rotate-file
├── AI 集成:OpenAI SDK
├── 支付:微信支付 + 支付宝
├── 存储:腾讯云 COS
└── 类型共享:shared-types (workspace)

三、数据库设计:Prisma Schema

3.1 用户表设计

数据库设计是后端开发的第一步,也是最重要的一步。

提示词

帮我设计用户表,需要支持:
1. 微信小程序登录(openid)
2. 手机号登录
3. 性别、年龄段等用户属性
4. 免费使用次数限制
5. 注册来源和渠道追踪

AI 生成的 Prisma Schema

// prisma/schema.prisma

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "darwin", "darwin-arm64", "linux-musl-openssl-3.0.x"]
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  user_id               String    @id @default(uuid())
  username              String
  email                 String?   @unique
  phone_number          String?
  openid                String?   // 微信 openid
  hashed_password       String
  email_verified        Boolean   @default(false)
  phone_verified        Boolean   @default(false)

  // 地理信息
  country               String?
  province              String?
  city                  String?
  isp                   String?

  // 用户属性
  verification_code     String?
  avatar                String?
  gender                Int?      // 1: 男, 2: 女
  age_group             Int?      // 1: 00后, 2: 05后, 3: 90后, 4: 80后, 5: 70后

  // 状态管理
  disabled_status       Int?      @default(0)
  disabled_time         DateTime?

  // 免费额度
  free_reply_total      Int       @default(3)
  free_reply_used       Int       @default(0)
  free_reply_reset_time DateTime?

  // 来源追踪
  register_source       Int       @default(1)  // 注册来源
  register_channel      Int       @default(1)  // 注册渠道
  latest_source         Int       @default(1)  // 最近来源
  latest_channel        Int       @default(1)  // 最近渠道

  // 时间戳
  code_send_time        DateTime?
  create_time           DateTime? @default(now())
  update_time           DateTime  @default(now()) @updatedAt
  operate_time          DateTime?

  // 索引
  @@unique([phone_number, deleted_status])
  @@index([register_source], name: "idx_users_register_source")
  @@index([latest_source], name: "idx_users_latest_source")
  @@map("users")
}

设计要点解析

  1. UUID 主键:使用 @default(uuid()) 而非自增 ID,分布式友好,自己玩的小项目推荐自增 ID
  2. 软删除deleted_status + deleted_time,数据可恢复
  3. 来源追踪register_source/channellatest_source/channel,用于数据分析
  4. 免费额度free_reply_total/used/reset_time,实现每日免费次数限制

3.2 会员表设计

// 会员类型模型
model MembershipType {
  id              Int       @id @default(autoincrement())
  name            String    // 会员名称:月度会员、季度会员、年度会员
  price           Decimal   @db.Decimal(10, 2)
  duration_days   Int       // 会员时长(天)
  description     String?
  is_active       Boolean   @default(true)
  sort_order      Int       @default(0)
  create_time     DateTime  @default(now())
  update_time     DateTime  @default(now()) @updatedAt

  @@map("membership_types")
}

// 用户会员记录
model UserMembership {
  id              Int       @id @default(autoincrement())
  user_id         String
  membership_type Int       // 关联会员类型
  start_time      DateTime
  end_time        DateTime
  is_active       Boolean   @default(true)
  source          String    @default("purchase") // purchase/gift/invite
  order_id        String?   // 关联订单
  create_time     DateTime  @default(now())
  update_time     DateTime  @default(now()) @updatedAt

  @@index([user_id], name: "idx_user_membership_user_id")
  @@index([end_time], name: "idx_user_membership_end_time")
  @@map("user_memberships")
}

3.3 Prisma 初始化和迁移

# 安装 Prisma
pnpm --filter xingdong-server add prisma @prisma/client

# 初始化 Prisma
pnpm --filter xingdong-server prisma init

# 生成 Prisma Client
pnpm --filter xingdong-server prisma generate

# 创建迁移
pnpm --filter xingdong-server prisma migrate dev --name init

# 查看数据库
pnpm --filter xingdong-server prisma studio

四、项目结构:分层架构设计

4.1 目录结构

AI 帮小何设计了清晰的分层架构:

apps/xindong-server/
├── prisma/
│   └── schema.prisma           # 数据库模型定义
├── src/
│   ├── constants/              # 常量定义
│   │   └── index.ts
│   ├── db/                     # 数据访问层 (DAL)
│   │   ├── user.ts             # 用户数据操作
│   │   ├── chat.ts             # 聊天数据操作
│   │   ├── membership.ts       # 会员数据操作
│   │   └── ...
│   ├── helper/                 # 辅助工具
│   │   ├── logger.ts           # 日志工具
│   │   └── responseHelper.ts   # 响应格式化
│   ├── pages/
│   │   └── api/                # API 路由
│   │       ├── auth/           # 认证相关
│   │       │   ├── login.ts
│   │       │   ├── wx-login.ts
│   │       │   └── register.ts
│   │       ├── chat/           # 聊天相关
│   │       ├── membership/     # 会员相关
│   │       └── upload/         # 文件上传
│   ├── service/                # 业务逻辑层
│   │   ├── auth.ts             # 认证服务
│   │   ├── chatService.ts      # 聊天服务
│   │   ├── membershipService.ts
│   │   └── ...
│   ├── type/                   # 类型定义
│   └── utils/                  # 工具函数
│       ├── auth/
│       │   └── auth.ts         # JWT 工具
│       ├── prismaProxy.ts      # Prisma 代理
│       └── logRequest.ts       # 请求日志
├── .env                        # 环境变量
└── package.json

分层职责

  1. pages/api:接收请求、参数验证、调用 Service
  2. service:业务逻辑处理、调用多个 DB 方法
  3. db:纯数据操作、Prisma 查询封装
  4. helper:通用辅助功能(日志、响应格式化)
  5. utils:工具函数(加密、JWT、请求处理)

4.2 Prisma 代理封装

AI 生成了一个 Prisma 代理,自动记录所有数据库操作日志:

// src/utils/prismaProxy.ts
import logger from '@/helper/logger';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

function handlePrismaError(error: Error) {
  logger.error(`Prisma error: ${error.message}`, { error });
}

const handler: ProxyHandler<PrismaClient> = {
  get(target, propKey) {
    const origMethod = target[propKey as keyof PrismaClient];

    if (typeof origMethod === 'function') {
      return function (this: PrismaClient, ...args: unknown[]) {
        const boundMethod = (origMethod as Function).bind(this);
        try {
          // 记录请求日志
          logger.info(`Prisma request: ${propKey.toString()} with args: ${JSON.stringify(args)}`);

          const result = boundMethod(...args);
          if (result instanceof Promise) {
            return result
              .then((res) => {
                // 如果响应包含 count 属性,则记录所影响的行数
                if (res && typeof res.count === 'number') {
                  logger.info(`Prisma response: ${propKey.toString()} affected rows: ${res.count}`);
                }
                return res;
              })
              .catch(handlePrismaError);
          }
          return result;
        } catch (error) {
          return handlePrismaError(error as Error);
        }
      };
    }

    return target[propKey as keyof PrismaClient];
  },
};

const prismaProxy = new Proxy(prisma, handler);
export default prismaProxy;

使用方式

// 在 db 层使用代理后的 prisma
import prisma from '@/utils/prismaProxy';

// 所有操作自动记录日志
const user = await prisma.user.findUnique({
  where: { user_id: userId },
});

五、JWT 认证:从生成到验证

5.1 JWT 工具封装

提示词

帮我封装 JWT 工具,需要:
1. 生成 Token(带过期时间)
2. 验证 Token
3. 密码哈希和验证
4. 使用 bcryptjs 加密

AI 生成的代码

// src/utils/auth/auth.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { TOKEN_EXPIRE_IN } from '@/constants';

// 密码哈希
export const hashPassword = async (password: string): Promise<string> => {
  const salt = await bcrypt.genSalt(10);
  return bcrypt.hash(password, salt);
};

// 密码验证
export const verifyPassword = async (
  password: string,
  hashedPassword: string,
): Promise<boolean> => {
  return bcrypt.compare(password, hashedPassword);
};

// 生成 JWT
export const generateJWT = (userId: string): string => {
  return jwt.sign({ userId }, process.env.JWT_SECRET as string, {
    expiresIn: TOKEN_EXPIRE_IN, // 例如 '7d'
  });
};

// 验证 JWT
export const verifyJWT = (token: string): Promise<any> => {
  return new Promise((resolve, reject) => {
    jwt.verify(token, process.env.JWT_SECRET as string, (err, decoded) => {
      if (err) {
        reject(err);
      } else {
        resolve(decoded);
      }
    });
  });
};

5.2 认证中间件

// src/utils/auth/authMiddleware.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { verifyJWT } from './auth';
import { sendUnauthorizedResponse } from '@/helper/responseHelper';
import { getUserById } from '@/db/user';

export interface AuthenticatedRequest extends NextApiRequest {
  userId: string;
  user: any;
}

export const withAuth = (
  handler: (req: AuthenticatedRequest, res: NextApiResponse) => Promise<void>,
) => {
  return async (req: NextApiRequest, res: NextApiResponse) => {
    try {
      // 从 Header 获取 Token
      const authHeader = req.headers.authorization;
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return sendUnauthorizedResponse(res, '请先登录');
      }

      const token = authHeader.substring(7);

      // 验证 Token
      const decoded = await verifyJWT(token);
      if (!decoded || !decoded.userId) {
        return sendUnauthorizedResponse(res, 'Token 无效');
      }

      // 获取用户信息
      const user = await getUserById(decoded.userId);
      if (!user) {
        return sendUnauthorizedResponse(res, '用户不存在');
      }

      // 注入用户信息
      (req as AuthenticatedRequest).userId = decoded.userId;
      (req as AuthenticatedRequest).user = user;

      // 调用原始处理函数
      return handler(req as AuthenticatedRequest, res);
    } catch (error) {
      return sendUnauthorizedResponse(res, '认证失败,请重新登录');
    }
  };
};

使用方式

// pages/api/user/profile.ts
import { withAuth, AuthenticatedRequest } from '@/utils/auth/authMiddleware';
import { sendSuccessResponse } from '@/helper/responseHelper';
import { NextApiResponse } from 'next';

const handler = async (req: AuthenticatedRequest, res: NextApiResponse) => {
  // req.userId 和 req.user 已经可用
  sendSuccessResponse(res, '获取成功', {
    user_id: req.userId,
    username: req.user.username,
  });
};

export default withAuth(handler);

六、统一响应格式

6.1 响应格式设计

统一的响应格式是 API 开发的基本要求。AI 帮小何设计了清晰的响应结构:

// src/helper/responseHelper.ts
import { NextApiResponse } from 'next';
import logger from './logger';
import { UNAUTHORIZED_TIPS } from '@/constants';

interface JsonResponse {
  code: number;
  message: string;
  data?: any;
}

// 通用响应
export function sendResponse(res: NextApiResponse, code: number, message: string, data: any = null): void {
  const jsonResponse: JsonResponse = {
    code,
    message,
    data,
  };
  res.status(code).json(jsonResponse);
}

// 成功响应
export function sendSuccessResponse(
  res: NextApiResponse,
  message: string = '数据获取成功',
  data: any = null,
): void {
  sendResponse(res, 200, message, data);
}

// 错误响应(服务器错误)
export function sendErrorResponse(res: NextApiResponse, message: string, data: any = null): void {
  sendResponse(res, 500, message, data);
  logger.error(message);
  logger.error(data);
}

// 警告响应(业务错误)
export function sendWarnningResponse(res: NextApiResponse, message: string, data: any = null): void {
  sendResponse(res, 503, message, data);
}

// 未授权响应
export function sendUnauthorizedResponse(
  res: NextApiResponse,
  message: string = UNAUTHORIZED_TIPS,
): void {
  sendResponse(res, 401, message);
}

// 方法不允许
export function sendMethodNotAllowedResponse(
  res: NextApiResponse,
  message: string = 'Method Not Allowed',
): void {
  sendResponse(res, 405, message);
}

6.2 响应码设计

状态码 含义 使用场景
200 成功 请求成功处理
401 未授权 Token 无效或过期
405 方法不允许 GET 接口收到 POST 请求
500 服务器错误 代码异常、数据库错误
503 业务警告 参数错误、业务规则不满足

前端统一处理

// 前端 HTTP 拦截器
const handleResponse = (response: any) => {
  const { code, message, data } = response;

  switch (code) {
    case 200:
      return data;
    case 401:
      // 跳转登录
      uni.reLaunch({ url: '/pages/login/login' });
      throw new Error(message);
    case 503:
      // 业务警告,显示 Toast
      uni.showToast({ title: message, icon: 'none' });
      throw new Error(message);
    case 500:
      // 服务器错误
      uni.showToast({ title: '服务器开小差了', icon: 'none' });
      throw new Error(message);
    default:
      throw new Error(message || '未知错误');
  }
};

七、微信小程序登录实现

7.1 登录流程设计

微信小程序登录是"心动恋聊"的核心功能。AI 帮小何设计了完整的登录流程:

sequenceDiagram
    participant Client as 小程序端
    participant Server as 后端服务
    participant WX as 微信服务器

    Client->>Client: 1. wx.login()
    Client->>Client: 2. 返回 code

    Client->>Server: 3. POST /api/auth/wx-login
    Note right of Client: { code, gender, age_group }

    Server->>WX: 4. jscode2session
    WX-->>Server: 5. 返回 openid

    Server->>Server: 6. 查找/创建用户
    Server->>Server: 7. 生成 JWT

    Server-->>Client: 8. 返回 token + userInfo

7.2 登录 API 实现

提示词

帮我实现微信小程序登录 API,需要:
1. 接收 wx.login 的 code
2. 调用微信服务器获取 openid
3. 新用户需要收集性别和年龄段
4. 老用户直接返回 token
5. 支持多来源(小程序、App)
6. 记录用户来源和渠道

AI 生成的代码

// src/pages/api/auth/wx-login.ts
import { sendErrorResponse, sendSuccessResponse } from '@/helper/responseHelper';
import { generateJWT } from '@/utils/auth/auth';
import { getUserByOpenid, createUserByOpenid } from '@/db/user';
import { NextApiRequest, NextApiResponse } from 'next';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== 'POST') {
    return sendErrorResponse(res, 'Method Not Allowed');
  }

  try {
    const { code, gender, age_group } = req.body;

    // 1. 调用微信服务器获取 openid
    const wxLoginUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${process.env.WX_APPID}&secret=${process.env.WX_SECRET}&js_code=${code}&grant_type=authorization_code`;
    const wxRes = await fetch(wxLoginUrl);
    const { openid } = await wxRes.json();

    // 2. 查找或创建用户
    let user = await getUserByOpenid(openid);
    let isNewUser = false;

    if (!user) {
      // 新用户需要提供性别和年龄
      if (!gender || !age_group) {
        return sendSuccessResponse(res, '需要完善信息', { needsRegistration: true, openid });
      }
      user = await createUserByOpenid(openid, gender, age_group);
      isNewUser = true;
    }

    // 3. 生成 JWT Token
    const token = generateJWT(user.user_id);

    // 4. 返回登录结果
    sendSuccessResponse(res, '登录成功', { token, ...user, isNewUser });
  } catch (error: any) {
    sendErrorResponse(res, '登录失败', error.message);
  }
};

export default handler;

7.3 登录服务层

手机号登录的服务层封装,支持密码和验证码两种方式:

// src/service/auth.ts
import { getUserByPhoneNumber } from '@/db/user';
import { checkCodeValid } from '@/service/checkCodeValid';
import { generateJWT, verifyPassword } from '@/utils/auth/auth';
import { LoginCredentials, LoginResult } from 'shared-types';

export class AuthServiceError extends Error {
  constructor(
    message: string,
    public code: string,
  ) {
    super(message);
    this.name = 'AuthServiceError';
  }
}

export async function loginUser(credentials: LoginCredentials): Promise<LoginResult> {
  const { phone_number, password, verification_code } = credentials;

  // 从数据库获取用户
  const user = await getUserByPhoneNumber(phone_number!);
  if (!user) {
    throw new AuthServiceError('该用户不存在,请先注册', 'USER_NOT_FOUND');
  }

  if (password) {
    // 密码登录
    const decodedPassword = Buffer.from(password, 'base64').toString();
    const isPasswordValid = await verifyPassword(decodedPassword, user.hashed_password);
    if (!isPasswordValid) {
      throw new AuthServiceError('手机号或密码输入有误', 'INVALID_CREDENTIALS');
    }

    if (!user.phone_verified) {
      throw new AuthServiceError(
        '手机号未验证通过,请先使用手机号+验证码的方式登录',
        'PHONE_UNVERIFIED',
      );
    }
  } else {
    // 验证码登录
    if (!verification_code) {
      throw new AuthServiceError('请输入验证码', 'MISSING_VERIFICATION_CODE');
    }
    const { valid, msg } = checkCodeValid({ user, code: verification_code });
    if (!valid) {
      throw new AuthServiceError(msg, 'INVALID_VERIFICATION_CODE');
    }

    if (!user.phone_verified) {
      throw new AuthServiceError('请先注册完成后再登录', 'USER_NOT_REGISTERED');
    }
  }

  // 生成 JWT
  const token = await generateJWT(user.user_id);

  return {
    user,
    token,
  };
}

八、前后端类型共享:shared-types

8.1 为什么需要类型共享

前后端分离开发时,最常见的问题是接口对不上

后端:{ user_id: "xxx" }
前端:{ userId: "xxx" }  // 拿不到数据

后端:{ gender: 1 }
前端:{ gender: "男" }  // 类型不匹配

解决方案:shared-types 包

8.2 类型定义

// packages/shared-types/enums.ts

/**
 * 性别枚举
 */
export enum GenderEnum {
  MALE = 1,
  FEMALE = 2,
}

/**
 * 年龄段枚举
 */
export enum AgeGroupEnum {
  POST_00 = 1,  // 00后
  POST_05 = 2,  // 05后
  POST_90 = 3,  // 90后
  POST_80 = 4,  // 80后
  POST_70 = 5,  // 70后
}

/**
 * 年龄段枚举映射(用于显示)
 */
export const AgeGroupMap = {
  [AgeGroupEnum.POST_00]: '00后',
  [AgeGroupEnum.POST_05]: '05后',
  [AgeGroupEnum.POST_90]: '90后',
  [AgeGroupEnum.POST_80]: '80后',
  [AgeGroupEnum.POST_70]: '70后',
};

/**
 * 客户端来源枚举
 */
export enum ClientSourceEnum {
  MP_WEIXIN = 1,  // 微信小程序
  ANDROID = 2,    // 安卓 App
  HARMONY = 3,    // 鸿蒙
}

8.3 在项目中使用

后端使用

// apps/xingdong-server/src/service/auth.ts
import { LoginCredentials, LoginResult } from 'shared-types';

export async function loginUser(credentials: LoginCredentials): Promise<LoginResult> {
  // 类型自动推导,IDE 智能提示
}

前端使用

// apps/unibest-mp/src/api/auth.ts
import type { WxLoginParams, WxLoginResult } from 'shared-types';

export const apiWxLogin = (params: WxLoginParams): Promise<WxLoginResult> => {
  return http.post('/api/auth/wx-login', params);
};

package.json 配置

{
  "dependencies": {
    "shared-types": "workspace:*"
  }
}

Next.js 配置(重要!):

// next.config.js
const nextConfig = {
  reactStrictMode: true,
  transpilePackages: ['shared-types'], // 编译本地 TypeScript 包
};

九、日志系统:Winston 配置

9.1 日志配置

// src/helper/logger.ts
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import path from 'path';

// 日志目录
const logDir = path.join(process.cwd(), 'logs');

// 日志格式
const logFormat = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.errors({ stack: true }),
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
    let msg = `${timestamp} [${level.toUpperCase()}]: ${message}`;
    if (Object.keys(meta).length > 0) {
      msg += ` ${JSON.stringify(meta)}`;
    }
    return msg;
  }),
);

// 创建 logger
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: logFormat,
  transports: [
    // 控制台输出
    new winston.transports.Console({
      format: winston.format.combine(winston.format.colorize(), logFormat),
    }),
    // 按日期滚动的文件日志
    new DailyRotateFile({
      dirname: logDir,
      filename: 'app-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '14d',
    }),
    // 错误日志单独存放
    new DailyRotateFile({
      dirname: logDir,
      filename: 'error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      level: 'error',
      maxSize: '20m',
      maxFiles: '30d',
    }),
  ],
});

export default logger;

9.2 请求日志中间件

// src/utils/logRequest.ts
import { NextApiRequest, NextApiResponse } from 'next';
import logger from '@/helper/logger';

const logRequest = (req: NextApiRequest, res: NextApiResponse) => {
  const { method, url, body, query } = req;

  logger.info(`API Request: ${method} ${url}`, {
    body: JSON.stringify(body),
    query: JSON.stringify(query),
    ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
    userAgent: req.headers['user-agent'],
  });

  // 记录响应时间
  const startTime = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    logger.info(`API Response: ${method} ${url} - ${res.statusCode} (${duration}ms)`);
  });
};

export default logRequest;

十、总结与下一步

10.1 本篇完成的工作

通过 AI 辅助,我们在 约 1 小时 内完成了:

任务 完成情况
✅ Prisma 数据库设计 用户、会员、账单等核心表
✅ 分层架构 API → Service → DB 清晰分离
✅ JWT 认证 生成、验证、中间件完整实现
✅ 微信登录 支持小程序、App 多端登录
✅ 统一响应格式 responseHelper 标准化
✅ 类型共享 shared-types 前后端类型一致
✅ 日志系统 Winston 按日期滚动、分级存储

10.2 核心提示词模板

数据库设计

帮我设计 [表名] 表,需要支持:
1. [功能点 1]
2. [功能点 2]
3. [功能点 3]
使用 Prisma Schema 语法

API 开发

帮我实现 [功能名称] API,需要:
1. [接口功能]
2. [参数要求]
3. [返回格式]
4. [错误处理]

工具封装

帮我封装 [工具名称],需要:
1. [功能点 1]
2. [功能点 2]
示例用法:[使用场景]

10.3 下一篇预告

《【AI 编程实战】第 4 篇:用 AI 打造原子化 CSS 开发体系 - UnoCSS 实战》

我们将学习:

  • UnoCSS 高级配置
  • 设计稿转代码技巧
  • 主题定制和换肤
  • 小程序端样式优化
  • 响应式布局实践

关注我,不错过每一篇实战干货!


如果这篇文章对你有帮助,请点赞、收藏、转发,让更多人了解 AI 编程的强大!

有任何问题,欢迎在评论区留言,我们一起讨论。

Vue3项目集成monaco-editor实现浏览器IDE代码编辑功能

作者 答案answer
2025年12月12日 10:32

前言

相信大家在做一些低代码平台的项目时,都会涉及到一些在线IDE代码编辑的功能吧,比如通过在线代码编辑后实现在线运行代码效果.

本篇给大家分享一下作者个人在开发低代码平台时如何实现如下图所示的vscode在线代码IDE编辑功能的吧

image.png

一、安装相关插件

pnpm add monaco-editor 
pnpm add monaco-editor-vue3

因为是在Vue3项目中所以这里直接使用 monaco-editor-vue3 这个插件会更加便捷

二、新增一个monaco.ts 配置文件(这个很重要)

在安装完插件后其实我们这样直接在页面中引入就可以使用了,但是这个时候页面其实会有报错的,大概就是提示你monaco-editor 相关配置没有处理

<template>
  <div style="height: 400px; width: 800px">
    <CodeEditor
      v-model:value="code"
      language="javascript"
      theme="vs-dark"
      :height="600"
      :options="editorOptions"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { CodeEditor } from 'monaco-editor-vue3';

const code = ref(`function hello() {
console.log('Hello, Monaco Editor!');
}`);

const editorOptions = {
  fontSize: 14,
  minimap: { enabled: false },
  automaticLayout: true,
};
</script>

这时候我们需要创建一个 monaco.ts 文件并添加以下配置内容

import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import * as monaco from 'monaco-editor';

declare global {
  interface Window {
    MonacoEnvironment?: {
      getWorker: (moduleId: string, label: string) => Worker;
    };
  }
}

(self as Window).MonacoEnvironment = {
  getWorker(_: string, label: string) {
    if (label === 'json') {
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  },
};
 

同时在 main.ts 中引入 monaco.ts

import App from './App.vue';
import '@/utils/monaco'

type AppInstance = AppType<Element>;

const app: AppInstance = createApp(App);
app.mount('#app');

界面:ok配置成功后界面内容大概就是这样

image.png

自定义主题

如果你觉得编辑器默认的主题样式不太好看也可以自定义主题样式,这里简单的配置一下

依旧在monaco.ts中添加代码

// 定义符合项目系统的自定义主题
const customTheme: monaco.editor.IStandaloneThemeData = {
  base: 'vs-dark', // 基于官方暗色主题
  inherit: true, // 继承默认语法高亮规则
  rules: [
    { token: 'comment', foreground: '617b91', fontStyle: 'italic' }, // 注释呈现斜体灰蓝
    { token: 'keyword', foreground: 'c5cceb', fontStyle: 'bold' }, // 关键字加粗淡紫
    { token: 'string', foreground: 'a9b1d6' }, // 字符串淡蓝
    { token: 'number', foreground: 'c5cceb' }, // 数字淡紫
    { token: 'operator', foreground: 'c7cacf' }, // 运算符浅灰
    { token: 'delimiter', foreground: 'c7cacf' }, // 分隔符浅灰
    { token: 'type', foreground: 'c5cceb' }, // 类型标识淡紫
    { token: 'class', foreground: 'c5cceb' }, // 类名淡紫
    { token: 'function', foreground: 'a9b1d6' }, // 函数名淡蓝
    { token: 'variable', foreground: 'c5cceb' }, // 变量名淡紫
  ],
  colors: {
    'editor.background': '#252837', // 编辑器背景
    'editor.foreground': '#c5cceb', // 默认前景文字
    'editor.lineHighlightBackground': '#29344c', // 当前行高亮背景
    'editor.inactiveSelectionBackground': 'rgba(69, 137, 255, 0.15)', // 未激活选区背景
    'editorCursor.foreground': '#c5cceb', // 光标颜色
    'editorWhitespace.foreground': '#535f79', // 空白字符提示色
    'editorIndentGuide.background': '#535f79', // 缩进指示线
    'editorIndentGuide.activeBackground': '#a9b1d6', // 活动缩进指示线
    'editorLineNumber.foreground': '#617b91', // 行号默认颜色
    'editorLineNumber.activeForeground': '#c5cceb', // 当前行号颜色
    'editorGutter.background': '#252837', // 行号区域背景
    'editorWidget.background': '#29344c', // 弹出组件背景
    'editorWidget.border': '#535f79', // 弹出组件边框
    'editorSuggestWidget.background': '#29344c', // 智能提示背景
    'editorSuggestWidget.border': '#535f79', // 智能提示边框
  },
};

// 注册自定义主题
monaco.editor.defineTheme('custom-dark', customTheme);

界面效果:

image.png

monaco.ts 完整的配置

import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import * as monaco from 'monaco-editor';
declare global {
  interface Window {
    MonacoEnvironment?: {
      getWorker: (moduleId: string, label: string) => Worker;
    };
  }
}
(self as Window).MonacoEnvironment = {
  getWorker(_: string, label: string) {
    if (label === 'json') {
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  },
};
// 定义符合项目系统的自定义主题
const customTheme: monaco.editor.IStandaloneThemeData = {
  base: 'vs-dark', // 基于官方暗色主题
  inherit: true, // 继承默认语法高亮规则
  rules: [
    { token: 'comment', foreground: '617b91', fontStyle: 'italic' }, // 注释呈现斜体灰蓝
    { token: 'keyword', foreground: 'c5cceb', fontStyle: 'bold' }, // 关键字加粗淡紫
    { token: 'string', foreground: 'a9b1d6' }, // 字符串淡蓝
    { token: 'number', foreground: 'c5cceb' }, // 数字淡紫
    { token: 'operator', foreground: 'c7cacf' }, // 运算符浅灰
    { token: 'delimiter', foreground: 'c7cacf' }, // 分隔符浅灰
    { token: 'type', foreground: 'c5cceb' }, // 类型标识淡紫
    { token: 'class', foreground: 'c5cceb' }, // 类名淡紫
    { token: 'function', foreground: 'a9b1d6' }, // 函数名淡蓝
    { token: 'variable', foreground: 'c5cceb' }, // 变量名淡紫
  ],
  colors: {
    'editor.background': '#252837', // 编辑器背景
    'editor.foreground': '#c5cceb', // 默认前景文字
    'editor.lineHighlightBackground': '#29344c', // 当前行高亮背景
    'editor.inactiveSelectionBackground': 'rgba(69, 137, 255, 0.15)', // 未激活选区背景
    'editorCursor.foreground': '#c5cceb', // 光标颜色
    'editorWhitespace.foreground': '#535f79', // 空白字符提示色
    'editorIndentGuide.background': '#535f79', // 缩进指示线
    'editorIndentGuide.activeBackground': '#a9b1d6', // 活动缩进指示线
    'editorLineNumber.foreground': '#617b91', // 行号默认颜色
    'editorLineNumber.activeForeground': '#c5cceb', // 当前行号颜色
    'editorGutter.background': '#252837', // 行号区域背景
    'editorWidget.background': '#29344c', // 弹出组件背景
    'editorWidget.border': '#535f79', // 弹出组件边框
    'editorSuggestWidget.background': '#29344c', // 智能提示背景
    'editorSuggestWidget.border': '#535f79', // 智能提示边框
  },
};
// 注册自定义主题
monaco.editor.defineTheme('custom-dark', customTheme);

总结

以上就是作者个人在Vue3项目中集成 monaco.editor 的过程

总体来说也是非常的简单

大概就是分三步流程实现

1.安装 monaco-editormonaco-editor-vue3 插件

2.新增和引入 monaco.ts 文件

3.在页面中使用 CodeEditor

github.com/microsoft/m…

github.com/bazingaedwa…

❌
❌