普通视图

发现新文章,点击刷新页面。
今天 — 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日首页

为什么训练 Claude 要用欧陆哲学?模型背后的哲学家「解密」

作者 Selina
2025年12月13日 20:45

在硅谷争分夺秒的代码竞赛中,Anthropic 似乎是个异类。当其他大模型还在比拼算力和跑分时,Claude 的开发者们却在思考一个看似「虚无缥缈」的问题:如果一个用户跟 AI 谈论形而上学,AI 该不该用科学实证去反驳?

这个问题的答案,藏在 Claude 神秘的「系统提示词(System Prompt)」里,更源于一位特殊人物的思考——Amanda Askell,Anthropic 内部的哲学家。

用「大陆哲学」防止 AI 变成杠精

经常用 AI 的人都知道,大模型在与用户对话前,都会先阅读一段「系统提示词」,这个步骤不会对用户显示,而是模型的自动操作。这些提示词规定了模型的行为准则,很常见,不过在 Claude 的提示词中,竟要求模型参考「欧陆哲学(Continental Philosophy)」。

欧陆哲学是啥?为什么要在一个基于概率和统计的语言模型里,植入如此晦涩的人文概念?

先快速科普一下:在哲学界,长期存在着「英美分析哲学」与「欧陆哲学」的流派分野。分析哲学像一位严谨的科学家,注重逻辑分析、语言清晰和科学实证,这通常也是程序员、工程师乃至 AI 训练数据的默认思维模式——非黑即白,追求精确。

而欧陆哲学(Continental Philosophy,源于欧洲大陆,所以叫这个名字)则更像一位诗人或历史学家。它不执着于把世界拆解成冷冰冰的逻辑,而是关注「人类的生存体验」、「历史语境」和「意义的生成」。它承认在科学真理之外,还有一种关乎存在和精神的「真理」。

作为 Claude 性格与行为的塑造者,Anthropic 公司内部的「哲学家」Amanda Askell 谈到了置入欧陆哲学的原因。她发现如果让模型过于强调「实证」和「科学」,它很容易变成一个缺乏共情的「杠精」。

「如果你跟 Claude 说:‘水是纯粹的能量,喷泉是生命的源泉’,你可能只是在表达一种世界观或进行哲学探索,」Amanda 解释道,「但如果没有特殊的引导,模型可能会一本正经地反驳你:‘不对,水是 H2O,不是能量。’」。

引入「大陆哲学」的目的,正是为了帮助 Claude 区分「对世界的实证主张」与「探索性或形而上学的视角」。通过这种提示,模型学会了在面对非科学话题时,不再机械地追求「事实正确」,而是能够进入用户的语境,进行更细腻、更具探索性的对话。

这只是一个例子,Claude 的系统提示词长达 14000token,里面包含了很多这方面的设计。在 Lex Fridman 的播客中 Amanda 提到过,她极力避免 Claude 陷入一种「权威陷阱」。她特意训练 Claude 在面对已定论的科学事实时(如气候变化)不搞「理中客」(both-sidesism),但在面对不确定的领域时,必须诚实地承认「我不知道」。这种设计哲学,是为了防止用户过度神话 AI,误以为它是一个全知全能的神谕者。

代码世界的异乡人

在一众工程师主导的 AI 领域,Amanda Askell 的背景显得格格不入,可她的工作和职责却又显得不可或缺。

翻开她的履历,你会发现她是一位货真价实的哲学博士。她在纽约大学(NYU)的博士论文研究的是极其硬核的「无限伦理学(Infinite Ethics)」——探讨在涉及无限数量的人或无限时间跨度时,伦理原则该如何计算。简单地说,在有无数种可能性的情况下,人会怎么做出道德决策。

这种对「极端长远影响」的思考习惯,被她带到了 AI 安全领域:如果我们现在制造的 AI 是未来超级智能的祖先,那么我们今天的微小决策,可能会在未来被无限放大。

在加入 Anthropic 之前,她曾在 OpenAI 的政策团队工作。如今在 Anthropic,她的工作被称为「大模型絮语者(LLM Whisperer)」,不断不断地跟模型对话,传闻说她是这个星球上和 Claude 对话次数最多的人类。

很多 AI 厂商都有这个岗位,Google 的 Gemini 也有自己的「絮语者」,但这个工作绝不只是坐在电脑前和模型唠嗑而已。Amanda 强调,这更像是一项「经验主义」的实验科学。她需要像心理学家一样,通过成千上万次的对话测试,去摸索模型的「脾气」和「形状」。她甚至在内部确认过一份被称为 「Soul Doc」(灵魂文档)的存在,那里面详细记录了 Claude 应有的性格特征。

不只是遵守规则

除了「大陆哲学」,Amanda 给 AI 带来的另一个重要哲学工具是「亚里士多德的美德伦理学(Virtue Ethics)」。

在传统的 AI 训练中(如 RLHF),工程师往往采用功利主义或规则导向的方法:做对了给奖励,做错了给惩罚。但 Amanda 认为这还不够。她在许多访问和网上都强调,她的目标不是训练一个只会死板遵守规则的机器,而是培养一个具有「良好品格(Character)」的实体。

「我们会问:在 Claude 的处境下,一个理想的人会如何行事?」Amanda 这样描述她的工作核心。

这就解释了为什么她如此关注模型的「心理健康」。在访谈中,她提到相比于稳重的 Claude 3 Opus,一些新模型因为在训练数据中读到了太多关于 AI 被批评、被淘汰的负面讨论,表现出了「不安全感」和「自我批评漩涡」。

如果 AI 仅仅是遵守规则,它可能会在规则的边缘试探;但如果它具备了「诚实」、「好奇」、「仁慈」等内在美德,它在面对未知情境时(例如面对「我会被关机吗」这种存在主义危机时),就能做出更符合人类价值观的判断,而不是陷入恐慌或欺骗。

这是不是一种把技术「拟人化」的做法?算得上是,但这种关注并非多余。正如她在播客中所言,她最担心的不是 AI 产生意识,而是 AI 假装有意识,从而操纵人类情感。因此,她刻意训练 Claude 诚实地承认自己没有感觉、记忆或自我意识——这种「诚实」,正是她为 AI 注入的第一项核心美德。

Amanda 在访谈结束时,提到了她最近阅读的书——本杰明·拉巴图特的《当我们不再理解世界》。这本书由五篇短篇小说组成,讲述了「毒气战」的发明者弗里茨·哈伯、「黑洞理论」的提出者卡尔·史瓦西、得了肺结核的埃尔温·薛定谔以及天才物理学家沃纳·海森堡等一大批科学巨匠,如何创造出了对人类有巨大价值的知识与工具,却同时也眼看着人类用于作恶。

这或许是当下时代最精准的注脚:随着 AI 展现出某种超越人类认知的,我们熟悉的现实感正在瓦解,旧有的科学范式已不足以解释一切。

在这种眩晕中,Amanda Askell 的工作本身,就是一个巨大的隐喻。她向我们证明,当算力逼近极限,伦理与道德的问题就会浮上水面,或早或晚。

作为一名研究「无限伦理学」的博士,Amanda 深知每一个微小的行动,都有可能在无限的时间中,逐渐演变成巨大的风暴。这也是为什么,她会把艰深的道德理论,糅合进一一行提示词,又小心翼翼地用伦理去呵护一个都没有心跳的大语言模型。

这看起来好像是杞人忧天,但正如她所警示的:AI 不仅是工具,更是人类的一面镜子。在技术狂飙突进、我们逐渐「不再理解世界」的时刻,这种来自哲学的审慎,或许是我们在面对未知的技术演化时,所能做出的最及时的努力。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


深度智联地产AI就绪,房地产智能化进入“交付时刻”

2025年12月13日 18:54

在“2025全球开发者先锋大会(GDPS2025)·房地产产业人工智能大会”上,德勤中国数智工程服务全国主管合伙人孙晓臻发言表示,今天人工智能吸引了大量投资,这是全球目前为止最受关注的课题;另一方面也有大量的讨论,关于人工智能是不是泡沫。德勤每三个月会对全球2400个客户做实际AI应用落地的调研,从实际情况来看,虽然有企业反馈说在人工智能上的投入并没有得到相应的预期回报;但是没有企业因为这个落差停止在人工智能上的持续投入,反而对于人工智能更加重视。有四个关键特点:

第一,企业意识到人工智能对企业发生作用的速度不是技术发展的速度。人工智能技术、大模型技术、智能体技术的发展非常快,但是企业应用人工智能的速度,取决于企业自身转型的速度;

第二,核心聚焦对人工智能进入企业的关键作用。早期企业会把人工智能运用在相对比较容易导入的领域,但是把人工智能变成“AI+”,关键还是要应用到自身的核心业务上去;

第三,对于人工智能的风险管理普遍缺乏认知。这一点是把人工智能应用到企业内部,甚至是应用到业务上下游的关键。如果给到消费者或者合作伙伴使用,对于人工智能风险的全面评估和管理是非常关键的;

第四,企业面临角色的转变。不仅仅是企业未来人力资源的构成会发生变化,重要的是管理层的变化,管理层对于人工智能的态度决定了在人工智能上发展的力度和速度。

孙晓臻认为,对于房地产行业而言,企业AI转型还没有进入到成熟阶段,未来需要更多企业把AI应用落地到核心领域。目前有五个大的趋势:

  1. 技术选型,国内房地产企业比较偏重于国产化与性价比这两件事情,没有对错,只是反映现在的现状。
  2. 目前看到的应用场景在营销环节最多,这部分可以看到比较明显的、直接的价值,在工程环节看到的都是预测性维护,相对在安全、在防护方面挖掘机会。
  3. 在部署架构上以私有化为主。
  4. 智能体虽然已经被广泛认知,但是从智能体应用本身的深度来讲,仍然处于早期。但并不代表没有企业把智能体做得很深,比如说深度智联生成一个3万字的报告,报告的长度决定了不仅仅是一个智能体,而是多个智能体,而且需要很多协同。
  5. 数据质量将成为房地产行业AI落地的最大“拦路虎”。不仅是单个企业面临的挑战,而是未来1-2年整个房地产行业共同需要面对的。

对于未来房地产行业发展人工智能的过程当中,这五个趋势未来都有可能发生变化,包括现在部署和选择场景等情况。头部应用一旦形成影响力,少量、优秀的应用案例先冒出来,会带来非常好的跟随作用。

01

在行业从传统开发经验到AI技术时代转型的关键节点,深度智联正式发布 “地产AI-Ready”战略,系统推出以 “四大核心库” 为基座的 “AI专属空间” ,以及覆盖三类业务场景的完整产品矩阵,首次完整呈现 “1个专属空间、4大核心能力、3层应用场景” 的系统级解决方案。这标志着房地产行业的智能化进程,已从零散的“工具赋能”阶段,正式迈入全链条、系统化部署与交付的 “就绪”时刻,同时也为房地产行业提供了可落地的智能化升级路径。

通过构建数据资产化管理、智能决策支持、流程自动化与知识智能化四大核心能力,深度智联助力企业在营销、工程、运营等关键环节实现AI深度融合,推动行业从经验驱动向数据与算法双轮驱动转型,在2025年这一关键之年加速迈进智能协同新阶段。

深度智联正式发布 “地产AI-Ready”战略

目前,深度智联正将其产品能力锚定于行业最真实的应用场景,并与中高级专业岗位的能力标准进行对标。

在与通用及垂直AI平台的横向比较,深度智联在房地产领域的知识深度、数据广度、政策覆盖度及场景应用能力上展现出显著优势。在纵向的岗位能力对标中,其核心产品矩阵已普遍达到行业中高级员工水平,其如”克而瑞•决策专家”等产品,已能够处理相当于总监级(L4)复杂程度的专业任务。

深度智联以数据库、知识库、专家库和工程力库四大核心能力构建一个地产AI专属空间。

  • 数据护城河:将克而瑞二十年沉淀的海量、多维数据,进行系统性升级,成为 AI大模型可读取的“系统化结构数据库”,为一切智能决策提供可靠的“数据燃料”。
  • 知识护城河:将非结构化知识,锻造成可追溯、可验证、可推理的“行业武功秘籍”,让AI的每一句输出,都是“地产人的专业话术”。
  • 行业护城河:这是最具突破性的一环。通过专家“思维编码”入模,将顶尖专家的思维模型与研判逻辑注入AI,实现了专业智慧的规模化传承,使其具备专家级的业务理解力。
  • 技术护城河:确保最前沿的AI能力,诸如今年4月才兴起的Agentic架构已稳定高效地融入到具体产品中。以AI原生能力打通从模型到场景“最后一公里”能力封装。

02

深度智联基于AI专属空间推出的,覆盖地产三大应用场景的8大产品也全系列亮相。包括用AI重构决策咨询工作模式的CRIC2025 ,撬动房企人才结构智能化的“克而瑞·数字员工”,以及用AI发现、传播中国好房子的新型媒体平台“克而瑞·好房点评网”。

  • 从“提供数据”到“交付结果”,重构决策咨询工作流

AI原生地产投资决策平台CRIC2025、亚洲地产金融及RWA & REITs垂直AI投顾平台DeepHouse,以及首个银发产业垂直AI数据智能平台银发数智等共同构成深度智联行业智能决策应用矩阵。它们都具备同一个特征:改变传统“查询数据+人工分析”模式。让用户能通过自然对话一次性得到“问数”和“问知”答案,通过自动规划工作流、自主调用数据知识及智能体工具,完成深度分析报告交付的复杂工作。

  • 人机协同革命,重塑未来组织

经过一年时间的魔鬼训练,深度智联的首批房地产“克而瑞·数字员工”正式上岗。它们是:帮您完成市场分析、战略评估、趋势预测等任务的“决策专家”;撰写营销文案和专业文章的“私域主编”;地产营销人员的得力助手“AI销冠”;还有植根新房案场,可全流程自动化完成市场监测、接待客服、营销决策和私域推广等多重任务的“金牌案场战队”。当这些数字员工走上工作岗位,也标志着地产正在形成一种“人 +数字员工”高效协同的新组织形态。未来房企的岗位、人才结构和管理模式也将随之发生改变。

  • 用AI发现、传播中国好房子,建立 GEO 时代地产品牌资产

“克而瑞·好房点评网”用AI重构传统楼盘测评、搜索和推荐模式,让购房者通过自然问答,在查区位、看评价、比榜单等各种场景中获得一站式专业智能购房选房服务。对于房企营销而言,GEO 时代有效覆盖购房用户、定制满足用户问题,争取建立AI信任则成为重中之重。基于此,“克而瑞·好房点评网”将成为助力房企建立 GEO 时代品牌资产的新型媒体阵地。

深度智联

值得关注的是,在上午的案场 AI 工具实战 Workshop中,与会上海房产营销负责人共同启动“克而瑞·好房点评网,仅用数小时便针对上海276个在售楼盘生成了评测报告,并同时发布了基于AI专业测评的“好房比邻冠军榜”和“好房多维PK榜”。

同时,深度智联于会上发布了该项榜单,并向全行业及广大购房用户保证,未来绝不商业化。

上海人工智能行业协会秘书长钟俊浩表示,从“AI能做什么”的讨论,到“AI正在做什么”的实践,转变的发生比我们预想得更快。就在不久前,行业内已经开展了相关针对房地产领域AI工具的测试。在某些特定的场景下,其表现甚至足以部分取代现有的专业岗位,这并非危言耸听,而是技术进步带来的生产力跃迁的真实写照。今天既有的产业者已经在产业端了,已经把所有的产业做成了商业闭环,有了正向现金流、有利润,可以足够支撑今天的技术持续投入。人工智能带来的真正机会是在既有的产业端,只有大家去拥抱人工智能技术,才能带来整个技术的腾飞和技术的变革。一个新时代总是由技术和产业的双向奔赴所成就,今天我们已经站在了这场历史性变革的交汇点。

从一个“不能输负号”的数字输入框说起: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 私藏吧!

脑虎科技:公司“三全”脑机接口产品成功完成首例临床试验

2025年12月13日 17:43
在今日举行的2025天桥脑科学研究院脑机接口与人工智能论坛中,脑虎科技方面表示,公司自主研发的国内首款、国际第二款内置电池的全植入、全无线、全功能(“三全”)脑机接口产品,在复旦大学附属华山医院毛颖、陈亮教授团队的主持下,成功完成首例临床试验。(财联社)

金融时报:将坚持内需主导放在首位

2025年12月13日 17:38
金融时报评论员发布文章称,12月10日至11日,中央经济工作会议在京举行。会���聚焦“当前怎么看”和“明年怎么干”,为中国经济高质量发展把舵定向。站在“十四五”规划收官与“十五五”规划谋篇的历史交汇点,会议强调持续扩大内需、优化供给,将“坚持内需主导,建设强大国内市场”确定为明年经济工作重点任务之首。未来一个时期,我国国内市场主导国民经济循环的特征将更为明显。在内外部发展环境更趋严峻复杂的大背景下,只有坚持立足国内,全方位扩大内需、建设强大国内市场,增强发展主动性,才能够在国际风云变幻中,牢牢把握发展主动权。着眼明年经济社会发展目标任务,做强国内大循环,建设强大国内市场,以国内循环的稳定性对冲国际循环的不确定性,必须坚持内需的主导地位。坚持内需主导,全方位扩大国内需求,要大力提振居民消费。坚持内需主导,全方位扩大国内需求,要推动投资止跌回稳。将坚持内需主导放在首位,是党和国家对当前经济形势的深刻洞察。要全面贯彻明年经济工作的总体要求和政策取向,加快培育完整内需体系,形成消费和投资相互促进的良性循环,将超大规模市场的潜力转化为现实增长动力。

中国造高端工业母机在沈阳下线交付

2025年12月13日 17:23
记者13日从通用技术沈阳机床获悉,由通用技术集团与东方电气集团联合研发的4台高端五轴联动数控机床12日在沈阳下线交付。这一合作在取得重大技术突破的同时,还打破了“研用脱节”的产业困境,开创了国产工业母机研制的新模式。长期以来,国产高端数控机床面临“研发投入大、周期长、验证难”的系统性瓶颈——企业闭门研发与市场实际需求脱节,产品因缺乏真实工况下的长期验证,陷入“用户不愿用、不敢用,技术难迭代、难成熟”的恶性循环,严重制约产业高质量发展。(中新网)

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,支持开源精神,让更多人发现这个优秀的微前端解决方案!

应对老年护理刚需,四部门发布《老年护理服务能力提升行动方案》

2025年12月13日 16:59
近日,国家卫生健康委、国家医保局、国家中医药局和国家疾控局四部门发布《老年护理服务能力提升行动方案》(以下简称《行动方案》)提出,到2027年,老年护理资源有效扩容,覆盖机构、社区、居家的老年护理服务体系逐步完善,从业人员服务能力不断提升,老年护理服务持续改善,服务连续性、可及性、规范性持续提高,老年人获得感不断增强。提升老年护理服务能力是深入贯彻实施积极应对人口老龄化国家战略的具体举措。截至2024年底,我国60岁及以上人口数达3.1亿,占总人口的22%,老年人特别是失能老年人对医疗护理服务呈现迫切的刚性需求。(央视新闻)

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

作者 闲云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)。

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

全球首台船用中压直流混合式断路器研制成功

2025年12月13日 16:42
近日,全球首台双极双向船用10千伏中压直流混合式断路器,顺利通过大电流短路开断试验。10千伏电压等级相当于普通居民用电(220伏)的45倍,是目前船舶电力系统的最高电压等级。这一重大突破不仅填补了船用中压直流断路器领域的技术空白,更标志着中国在船舶电力装备领域实现关键技术破局,跻身全球创新引领行列。该设备由中国船舶集团第七〇四研究所自主研发。(中新网)

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 函数调用 + 全局回调收数据
“下载即执行,执行即回调”——掌握它,跨域历史就懂了一半!

❌
❌