阅读视图

发现新文章,点击刷新页面。

如何查看、生成 github 开源项目star 图表

声明

重要声明:本应用基于 star-history/star-history 改造升级,我们将持续加入更多数据分析能力,感谢原作者!本文档也在原仓库文档的基础上进行改写与完善。

新的仓库地址:github-data-analysis-

新项目更新了图表实现方式(@visactor/vchart),加入mongodb 进行数据缓存,以减少对 GitHub API 的调用次数,提升性能。 加入了dark 主题,多语言等


在 GitHub README 中加入实时的 Star 历史图

image.png

我们支持把实时的 Star 历史图嵌入到你的 GitHub README 中。上图是我们自己的项目 GitHub 数据分析 的截图。

这个功能非常好用:在站点页面查询仓库后,会生成一段代码片段,你只需要把它复制到你的 README(或任何站点/博客)即可。

image转存失败,建议直接上传图片文件

image.png 下面介绍该功能的设计背景与具体用法。

使用 <iframe /> 方式嵌入

在调研常见的网页嵌入实现后,我们选择使用 <iframe /> 作为嵌入容器:它无需后端即可展示原始图表,并且可以与实时数据交互。

由于 GitHub API 对匿名调用有严格限流,我们需要用户提供自己生成的 Token 来提升限额。

iframe 嵌入的使用步骤

  1. 打开 gitdata.xuanhun520.com 并查询目标仓库;

  2. 点击图表下方的 Embed 按钮;

  3. 输入你的个人访问 Token;

    image转存失败,建议直接上传图片文件

image.png 4. 点击 Copy 按钮,把代码粘贴到你的站点或博客即可;

使用 SVG 静态图片嵌入(用于 README)

iframe 嵌入很强大,但也有两点限制:

  1. GitHub 的 Markdown 风格不允许渲染 <iframe />,因此无法直接把交互图嵌到 README;
  2. 需要提供个人 Token。虽然我们不在服务器端存储 Token,但在网页源码中仍可看到它,这在公共场景下并不理想。

因此,我们提供了基于图片链接的 SVG 方案,适合在公共页面(例如仓库 README)中展示最新星图。

在 GitHub README 中添加图表的步骤

  1. 打开 gitdata.xuanhun520.com 并查询仓库;

  2. 滚动到操作按钮下方的图片嵌入区域;

image.png 3. 点击 Copy 按钮;

  1. 将代码粘贴到你的仓库 README 中;

  2. 搞定 😎

示例链接(按日期模式展示):

gitdata.xuanhun520.com/api/starimg…

image.png

结论

我们提供两种把实时星图嵌入网页的方式:

  • 如果你希望在私有网络页面中放置可自适应且可交互的图表,请使用 <iframe /> 嵌入;
  • 如果你希望在公共页面(例如 GitHub README)中展示最新的星图,请使用 SVG 图片链接方式,例如:
https://gitdata.xuanhun520.com/api/starimg?repos=visactor/vchart&type=Date&theme=dark

下一步

  • 多语言
  • 加入更多数据分析能力,例如:仓库 forks 历史图、贡献者活动图等;
  • 加入用户认证功能,以支持私有仓库的分析;
  • 加入更多可视化形式,例如信息图,动态图表等

欢迎star

star it

image.png

一个看似“送分”的需求为何翻车?——前端状态机实战指南

源码太枯燥?一个人啃不动?关注公众号-「前端小卒」,让我做你的源码领读人。这里没有晦涩的说教,只有清晰的 Vue 3 & React 19 源码拆解。积跬步以至千里,小卒也能进阶为大将,我们一起过河!♟️

故事的开始总是惊人的相似。

上个月,需求评审会上,收到了产品经理的一个需求:“做一个扫码支付弹窗。逻辑很简单:打开弹窗获取二维码,然后每隔 2 秒轮询一次接口,扫码成功就跳转,30 秒超时就让用户刷新”

一拍脑袋,凭经验判断,需求不难。作为一名熟练的“React工程师”,我极其自信地敲下代码,开始写需求代码。

const [isLoading, setIsLoading] = useState(false); // 加载二维码中
const [isPolling, setIsPolling] = useState(false); // 轮询接口中
const [isError, setIsError] = useState(false);     // 出错了
const [retryCount, setRetryCount] = useState(0);   // 重试次数
// ...后面还有一堆 useEffect 和 useRef 用来存定时器ID

当时的我还未意识到,这几个state给我带来了好几个bug单。

隐形的“状态爆炸”

正式提测之后,我就陆陆续续收到了好几个bug单。

“大佬,我就在网络卡顿的时候多点了几下‘刷新’,页面就乱了”

浏览器的Network 面板上,几十个轮询请求在并发执行。UI 界面上,“正在加载”的转圈动画和“网络错误”的红字提示竟然同时存在,甚至还能点击“重试”按钮!!!

按照我的逻辑,isLoadingtrue 时,isError 肯定得是 false 啊。我明明写了 setLoading(true); setError(false),为啥会出现这种问题!

为什么会失控?

问题的根源就在于试图用线性的逻辑,去对抗非线性的现实。

isLoadingisPollingisError 三个布尔值混在一起时,理论上它们能组合出 2^3=8 种状态。而在真实的业务逻辑中,合法的状态可能只有 3 种(加载中、轮询中、失败)。那么剩下的那 5 种“不可能存在的状态”,一不小心没处理就会存在大量的缺陷。

我们花费 80% 的时间,不是在写业务,而是在写防御代码,去堵那些因为逻辑漏洞而产生的混沌状态。

面对那坨膨胀且脆弱的代码,再进行修修补补已经无济于事,必须要大刀阔斧式的推倒重来,和PM和运营沟通,需求进行延期,代码打回去重写。

解决此类问题的最佳方案,不是更高超的 if-else 技巧,而是一个经典的数学模型——有限状态机 。它的核心理念是:系统在任何时刻只能处于一种状态,且状态的流转是确定的。

引入有限状态机 (FSM)

绘制状态流转图

在写代码之前,我先画了一张图。这才是逻辑的核心:

svgviewer-png-output (52).png

流程图清晰的展示出来:

  • 只有在 FAILUREIDLE 状态下,才允许发起 FETCH
  • LOADING 状态下,无论用户怎么点击,都不会触发多余的请求。

引入useReducer

React 的 useReducer 是实现状态机的绝佳工具,通过它可以将状态管理收敛到一个纯函数中。

定义状态与事件

// 状态枚举,只有这五种状态
const STATES = {
  IDLE: 'idle',
  LOADING: 'loading',
  POLLING: 'polling',
  SUCCESS: 'success',
  FAILURE: 'failure',
};

// 事件枚举:用户或系统能做的动作
const EVENTS = {
  FETCH: 'FETCH',      // 触发获取
  RESOLVE: 'RESOLVE',  // 成功拿到二维码
  REJECT: 'REJECT',    // 失败
  DONE: 'DONE',        // 支付完成
  TIMEOUT: 'TIMEOUT',  // 超时
};

编写 Reducer

这里使用双重 Switch 结构:先判断当前处于什么状态,再判断发生了什么事件

const machineReducer = (state, event) => {
  switch (state.status) {
    case STATES.IDLE:
    case STATES.FAILURE:
      // 只有在空闲或失败时,才允许发起 FETCH
      if (event.type === EVENTS.FETCH) {
        return { ...state, status: STATES.LOADING, error: null };
      }
      return state; // 其他事件直接忽略!

    case STATES.LOADING:
      if (event.type === EVENTS.RESOLVE) {
        return { ...state, status: STATES.POLLING, qrCode: event.data };
      }
      if (event.type === EVENTS.REJECT) {
        return { ...state, status: STATES.FAILURE, error: event.error };
      }
      // 🔥 重点:这里没有处理 'FETCH' 事件。
      // 这意味着:在 Loading 状态下,无论用户怎么狂点按钮,代码都不会有任何反应!
      return state;

    case STATES.POLLING:
      if (event.type === EVENTS.DONE) return { ...state, status: STATES.SUCCESS };
      if (event.type === EVENTS.TIMEOUT) return { ...state, status: STATES.FAILURE, error: '超时' };
      return state;

    default:
      return state;
  }
};

组件层调用(分离副作用)

现在,组件层的代码变得异常清爽。我们只需要根据状态去渲染 UI,并利用 useEffect 处理轮询副作用。

const PaymentModal = () => {
  const [state, dispatch] = useReducer(machineReducer, { status: STATES.IDLE });

  // 业务动作:只负责派发意图,不负责判断逻辑
  const handleFetch = () => {
    dispatch({ type: EVENTS.FETCH });
    api.getQrCode()
      .then(data => dispatch({ type: EVENTS.RESOLVE, data }))
      .catch(err => dispatch({ type: EVENTS.REJECT, error: err }));
  };

  // 副作用管理:由状态驱动轮询
  useEffect(() => {
    let timer = null;
    if (state.status === STATES.POLLING) {
      timer = setInterval(async () => {
        const res = await api.checkStatus();
        if (res.success) dispatch({ type: EVENTS.DONE });
      }, 2000);
    }
    return () => clearInterval(timer);
  }, [state.status]); // 状态变了,副作用自动清理或重启

  return (
    <div>
      {state.status === STATES.LOADING && <Spinner />}
      {state.status === STATES.POLLING && <QRCode img={state.qrCode} />}
      {state.status === STATES.FAILURE && <ErrorView onRetry={handleFetch} />}
      {state.status === STATES.SUCCESS && <SuccessView />}
    </div>
  );
};

看懂了吗?我们不再修补漏洞,通过清晰的定义状态和触发事件,收敛业务代码复杂度,消除可能产生的bug。


深度解析状态机

写到这里,可能很多同学会问: “这不就是 Switch-Case 吗?为什么要叫它状态机?” 我们需要把视角拉高,从理论层面彻底理解它,以便应对更复杂的场景。

什么是有限状态机

有限状态机不是一种代码写法,而是一个数学模型。它由五个要素组成:

  • 有限的状态 :比如红绿灯只有红、黄、绿,不可能出现“红绿”。
  • 有限的事件 :比如“倒计时结束”、“按下按钮”。
  • 转换规则 :即 状态 A + 事件 B -> 状态 C
  • 初始状态
  • 最终状态

它的核心理念就是:系统在任何时刻只能处于一种状态,且状态的流转是确定的。

为什么前端需要它

前端早已不是“展示页面”那么简单了,我们是在浏览器里跑 APP。以下场景如果不以状态机思维去写,迟早会崩:

  • 多媒体播放器: 你以为只有 PlayPause?错。 还有 Buffering(缓冲中)、Seeking(拖拽进度条中)、Ended(播放结束)、Error(加载失败)这些场景。
  • 复杂的表单向导 : 第一步 -> 第二步 -> 第三步。 用户在第二步点了“上一步”,数据怎么存?在第三步提交失败了,退回哪一步?
  • TCP/WebSocket 连接管理Connecting -> Connected -> Reconnecting -> Disconnected。 如果在 Reconnecting 期间用户手动点了“断开”,应该去哪?
  • Canvas 游戏/交互可视化: 拖拽模式、绘图模式、选中模式。

读到这里,你可能已经热血沸腾,准备把项目里的所有组件都重写一遍。

且慢。

软件工程的核心在于权衡,如果需求仅仅是控制一个模态框的显示与隐藏,引入状态机只会增加无谓的代码复杂度。

那么,在实际业务开发中,我们应该依据哪些标准来判定是否需要引入状态机?当你的组件出现以下三种特征之一时,就可以结合业务适当的考虑重构了。

存在多个需要“手动同步”的boolean变量

这是最显著的信号。检查一下你的组件 State 定义,如果出现了两个及以上的boolean,且彼此之间存在逻辑关联,就需要警惕,当你需要维护多个布尔值变量的同步关系时,状态机是更优解

例如:

// 典型的高风险代码
const [isLoading, setLoading] = useState(false);
const [isError, setError] = useState(false);
const [hasData, setHasData] = useState(false);

在这种结构下,开发者需要时刻警惕变量间的同步问题。比如在 setLoading(true) 时,必须记得同时重置 setError(false)。这种依赖“人为细心”来维护的代码,随着迭代周期的拉长,几乎必然会产生 Bug。

业务逻辑要求状态必须“互斥”

在很多场景下,业务状态在逻辑上是完全排他的。

比如开头的“扫码轮询”场景:一个请求不可能既在“进行中”又“已失败”。然而,如果使用分散的布尔值控制,代码层面是允许 { isLoading: true, isError: true } 这种非法组合存在的。

这种逻辑上的“排他性”如果不能在代码结构上得到强制保证,就会导致 UI 渲染出错(例如 Loading 动画和错误提示重叠显示)。所以如果UI展示依赖于严格互斥的状态(如 Promise 的 Pending/Resolved/Rejected),请尽可能的使用状态机强制约束。

状态流转路径有严格限制

简单的业务状态流转是自由的,而复杂的业务往往是有向图。

以“退款流程”为例:

  • ✅ 合法路径:申请中 -> 审核通过 -> 打款中
  • ❌ 非法路径:申请中 -> 打款中(跳过审核)

如果使用传统的 if-else 来防御非法跳转,代码中会充斥着大量 if (status === 'AUDIT_PASS') 这样的防御性判断。而状态机可以通过配置 transitions,天然地禁止了所有未定义的跳转路径。

所以当业务逻辑不仅仅关乎“当前是什么状态”,更关乎“当前状态允许变更为哪些状态”时,可以考虑引入状态机。

技术选型对照表

在开始写代码前,可以参考下表进行决策:

业务场景特征 推荐方案
简单的开/关状态 (Toggle) useState(boolean)
简单的互斥状态 (Loading/Success/Error) 字符串枚举 (String Enum)
状态复杂,且存在特定的流转规则 状态机 (Reducer / XState)
涉及定时器、重试、取消等异步竞态问题 状态机 (推荐)

记住,工具是为了解决复杂度而生的。简单留给 useState,复杂留给状态机。

限制即自由

写烂代码的本质,往往是我们试图在混乱中保留过多的“自由度”。而优秀架构的本质,通常在于限制。正如那句话说的:「喜欢就会放肆,但爱就是克制

状态机严格规定了什么状态下能做什么事,这种克制提高了我们的前端稳定性。

最后送给所有的前端的一句话:

所有的 Bug,本质上都是 “非法的状态”或“错误的转换”

TypeScript 帮我们在编译时静态地规避了非法类型,而状态机则帮我们在运行时动态地管理状态流转。

TypeScript + State Machine = 前端逻辑的绝对防御。

深入浅出链表操作:从Dummy节点到快慢指针的实战精要

引言

在数据结构的学习中,链表以其动态灵活的内存管理特性,成为算法设计与面试考察的重点。然而,由于其依赖指针操作、缺乏随机访问能力,许多开发者在处理链表问题时常常陷入边界混乱、逻辑断裂的困境。本文将聚焦三大核心技巧——dummy节点、头插法反转、快慢指针,系统梳理链表操作的本质逻辑,助你构建清晰稳定的解题思维。


一、Dummy节点:统一边界处理的“万能哨兵”

链表操作中最令人头疼的问题之一,是头节点的特殊性。例如删除值为 val 的节点时,若目标恰好是头节点,由于它没有前驱,无法通过“前驱修改 next”的方式删除,必须单独判断。

这种“例外情况”不仅增加代码复杂度,还容易引发空指针异常,尤其在空链表或全匹配场景下极易出错。

解决方案:引入 dummy 节点

dummy 节点(又称哨兵节点)是一个不存储有效数据的人工节点,通常置于链表头部,作为 head 的前驱。它的核心价值在于:

让所有节点都拥有前驱,从而将头节点降级为普通节点,实现操作统一化。

function removeElements(head, val) {
    const dummy = new ListNode(0);
    dummy.next = head;
    let cur = dummy;

    while (cur.next) {
        if (cur.next.val === val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }

    return dummy.next;
}

此时无论原头节点是否被删除,dummy.next 始终指向新的头节点,彻底规避了对 head 的特殊处理,也避免了空链表下的 null 异常。

💡 使用原则

  • 凡涉及节点删除、插入等可能影响头节点的操作,优先考虑添加 dummy。
  • 最终返回 dummy.next,而非原始 head。

二、头插法反转链表:最优雅的就地反转策略

链表反转是经典操作,常见解法有递归法和三指针迭代法。而基于 dummy 的头插法,逻辑最为直观,不易出错。

核心思想

遍历原链表,将每个节点依次“插入”到 dummy 节点之后,形成一个新的逆序链。随着遍历进行,已处理部分自然构成一个反向链表。

实现步骤

  1. 创建 dummy 节点,初始 dummy.next = null
  2. 遍历原链表,对每个节点 cur
    • 保存其后继:const next = cur.next
    • cur 插入 dummy 后:cur.next = dummy.next
    • 更新 dummy 指向新头:dummy.next = cur
    • 继续处理:cur = next
function reverseList(head) {
    const dummy = new ListNode(0);
    let cur = head;

    while (cur) {
        const next = cur.next;     // 保存后续
        cur.next = dummy.next;     // 接到已反转部分
        dummy.next = cur;          // 成为新头
        cur = next;                // 继续遍历
    }

    return dummy.next;
}

优势分析

  • 时间复杂度 O(n),空间复杂度 O(1)
  • 无需额外变量记录 prev 和 next
  • 逻辑清晰,适合快速编码

📌 关键提醒:务必在修改 cur.next 前保存 next,否则原链断裂,遍历中断。


三、快慢指针:高效定位的“双引擎”

当需要在单次遍历中定位特定位置(如中间节点、倒数第 N 个、环入口),快慢指针是最高效的工具。

其基本形式为:

  • slow 每次走 1 步
  • fast 每次走 2 步

利用两者速度差,可巧妙解决多种问题。

应用 1:判断链表是否有环

若链表无环,fast 会先到达末尾(null)。若有环,fast 进入环后会不断追赶 slow,最终相遇。

function hasCycle(head) {
    let slow = head, fast = head;

    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow === fast) return true;
    }

    return false;
}

为何 fast 走两步?因为每轮两者距离缩小 1,必能追上;若走更多步,可能跳过 slow 导致误判。

应用 2:查找中间节点

fast 到达末尾时,slow 正好位于中点。

function middleNode(head) {
    let slow = head, fast = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

此方法适用于奇偶长度链表,返回靠后的中间节点。

应用 3:删除倒数第 N 个节点

思路:让 fast 先走 N 步,再与 slow 同步前进。当 fast 到达末尾时,slow 指向目标节点的前驱。

结合 dummy 节点,可统一处理头节点删除:

function removeNthFromEnd(head, n) {
    const dummy = new ListNode(0);
    dummy.next = head;
    let fast = dummy, slow = dummy;

    while (n--) fast = fast.next;

    while (fast.next) {
        fast = fast.next;
        slow = slow.next;
    }

    slow.next = slow.next.next;
    return dummy.next;
}

四、总结:链表操作的四大心法

  1. Dummy优先原则
    所有涉及结构变更的操作,优先添加 dummy 节点,消除边界差异。

  2. 指针安全第一
    修改任何 next 指针前,务必保存后续节点引用,防止链表断裂。

  3. 快慢指针提速
    涉及“中点”、“倒数”、“环”等问题,首选快慢指针,实现一次遍历定位。

  4. 动手画图验证
    链表逻辑抽象,建议用简单实例(如 3 个节点)手动模拟每一步指针变化,确保理解正确。


掌握这三大技巧,你就拥有了应对绝大多数链表问题的“工具箱”。它们不仅是解题方法,更是一种编程思维:通过引入辅助结构(dummy)、控制相对速度(快慢指针)、重构处理顺序(头插法),将复杂问题转化为可统一处理的模式

链表并不可怕,可怕的是没有章法。当你学会用这些模式去拆解问题,你会发现,所谓的“难题”,不过是基础技巧的组合应用。

从被追问到被点赞:我靠“哨兵+快慢指针”展示了面试官真正想看的代码思维

面试高频链表题精讲:从哨兵节点到快慢指针

在算法面试中,链表是一类非常经典的数据结构题型。它不像数组那样支持随机访问,但正因如此,很多操作都需要我们对指针(引用)有精准的控制。本文将通过四道典型题目,带你系统掌握链表处理中的两大核心技巧:哨兵节点(Dummy Node)快慢指针(Fast & Slow Pointers)


一、删除链表中的指定节点:为什么需要哨兵节点?

题目描述

给定一个单链表的头节点 head 和一个值 val,删除链表中所有值等于 val 的节点,并返回新的头节点。
力扣:LCR 136. 删除链表的节点 - 力扣(LeetCode)

初步解法(不使用哨兵)

function remove(head, val) {
  // 特殊处理头节点
  if (head && head.val === val) {
    return head.next;
  }
  let cur = head;
  while (cur.next) {
    if (cur.next.val === val) {
      cur.next = cur.next.next;
      break; // 假设只删第一个匹配项
    }
    cur = cur.next;
  }
  return head;
}

面试官反问:

“你为什么要单独判断头节点?能不能统一处理?”

这是一个非常关键的问题!因为头节点没有前驱节点,所以当我们想“让前一个节点跳过当前节点”时,头节点就成了特例。

引入哨兵节点(Dummy Node)

哨兵节点是一个人为添加的假节点,通常放在链表头部,其作用是消除边界条件的特殊处理

function remove(head, val) {
  const dummy = new ListNode(0);
  dummy.next = head;
  let cur = dummy;
  while (cur.next) {
    if (cur.next.val === val) {
      cur.next = cur.next.next;
      break;
    }
    cur = cur.next;
  }
  return dummy.next; // 返回真正的头节点
}

优势

  • 不再需要单独判断头节点是否为待删节点;
  • 所有节点都变成了“中间节点”,逻辑统一;
  • 即使链表为空或全被删完,也能安全返回。

二、反转链表:哨兵 + 头插法

题目描述

反转一个单链表。
力扣:206. 反转链表 - 力扣(LeetCode)

解法思路

我们可以利用哨兵节点作为新链表的头,然后遍历原链表,每次把当前节点插入到哨兵之后——这就是头插法

function reverseList(head) {
  const dummy = new ListNode(0); // 哨兵,dummy.next 指向已反转部分的头
  let cur = head;
  while (cur) {
    const next = cur.next;       // 1. 保存下一个节点
    cur.next = dummy.next;       // 2. 当前节点指向已反转部分的头
    dummy.next = cur;            // 3. 更新反转部分的新头
    cur = next;                  // 移动到原链表下一节点
  }
  return dummy.next;
}

🔍 关键理解

  • dummy 始终不动,它的 next 指向当前已反转链表的头
  • 每轮操作把 cur 插到最前面,实现“头插”;
  • 三步操作顺序不能乱,否则会断链。

💡 虽然这道题也可以用递归或双指针(prev/cur)实现,但哨兵+头插法提供了一种清晰、不易出错的迭代思路。


三、判断链表是否有环:从哈希表到快慢指针

题目描述

给定一个链表,判断其中是否存在环。
力扣:141. 环形链表 - 力扣(LeetCode)

解法一:哈希表(空间换时间)

let hash = new Map();
let temp =head;
while(temp)
 {
   if(hash.has(temp))
    return true;
   else
    hash.set(temp,1);
  temp=temp.next;
 }
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

面试官追问:

“能不能不用额外空间?”

解法二:快慢指针(Floyd 判圈算法)

核心思想:如果链表有环,快指针(每次走两步)一定会在环内追上慢指针(每次走一步)。

function hasCycle(head) {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow.next;         // 慢指针走1步
    fast = fast.next.next;    // 快指针走2步
    if (slow === fast) {      // 比较引用地址(同一内存)
      return true;
    }
  }
  return false;
}

优势

  • 空间复杂度 O(1);
  • 时间复杂度 O(n);
  • 是链表环检测的标准解法。

🌟 快慢指针不仅用于判环,还可用于找环入口、求中点、删除倒数第 N 个节点等。


四、删除链表的倒数第 N 个节点:哨兵 + 快慢指针的完美结合

题目描述

给你一个链表,删除倒数第 n 个节点,并返回链表头。
力扣:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

解法思路

  1. 使用哨兵节点避免头节点被删的边界问题;
  2. 快指针先走 n 步
  3. 然后快慢指针同步移动,当快指针到达末尾时,慢指针正好在倒数第 n 个节点的前一个
  4. 执行删除操作。
const removeNthFromEnd = function(head, n) {
  const dummy = new ListNode(0);
  dummy.next = head;
  let fast = dummy;
  let slow = dummy;

  // 快指针先走 n 步
  for (let i = 0; i < n; i++) {
    fast = fast.next;
  }

  // 快慢指针一起走,直到 fast 到达最后一个节点
  while (fast.next) {
    fast = fast.next;
    slow = slow.next;
  }

  // 此时 slow 指向倒数第 n 个节点的前一个
  slow.next = slow.next.next;

  return dummy.next;
};

🎯 为什么需要哨兵?

  • 如果要删除的是头节点(比如链表长度为 5,n=5),那么 slow 需要停在“头节点之前”;
  • 没有哨兵的话,slow 无法指向 null 的前驱;
  • 哨兵确保 slow 始终有效,且 slow.next 可安全删除。

总结:链表面试两大法宝

技巧 适用场景 核心价值
哨兵节点(Dummy) 删除、插入、反转等涉及头节点的操作 消除边界条件,统一逻辑
快慢指针 判环、找中点、倒数第 N 个节点 O(1) 空间解决距离/位置问题

在实际面试中,先写出朴素解法(如哈希表) ,再根据面试官提示优化到快慢指针,能体现你的思维层次;而哨兵节点则是写出健壮、简洁代码的关键技巧。


希望这篇文章能帮你打通链表题的任督二脉!下次遇到链表题,先问自己两个问题:

  1. 要不要加哨兵? → 避免头节点特殊处理
  2. 能不能用快慢指针? → 解决“倒数”、“中点”、“环”等问题

祝你面试顺利,Offer 收割!🎉

Vue 3 动态菜单渲染优化实战:从白屏到“零延迟”体验

背景与问题

在构建中后台管理系统时,动态菜单(Permission Menu)是标准功能。通常的实现流程是:

  1. 用户登录,获取 Token。
  2. 进入主页,调用用户信息接口(/api/user/permissions)。
  3. 后端返回权限列表和菜单数据。
  4. 前端根据数据动态生成路由和菜单树。
  5. 渲染侧边栏组件。

痛点: 在这个流程中,步骤 2 和 3 是异步网络请求。在请求完成前,菜单数据为空,导致侧边栏会出现短暂的空白Loading 状态。 对于用户体验来说,每次刷新页面或重新进入系统,都要忍受 200ms - 1s 的菜单“闪烁”或“白屏”,这极大地影响了系统的流畅感。

优化方案:Cache-First + 增量更新

为了解决这个问题,我们采用了类似 PWA 的 Cache-First(缓存优先) 策略,结合 增量更新(Incremental Update) 机制。

核心策略

  1. 缓存优先渲染 (Cache-First Rendering)

    • 页面初始化时,不等待网络请求,直接从 localStorage 读取上一次缓存的菜单数据进行渲染。
    • 结果:用户看到的菜单是“瞬间”加载的,零延迟。
  2. 静默后台更新 (Silent Background Update)

    • 在缓存渲染完成后,立即在后台发起用户信息请求。
    • 这是一个“静默”操作,用户无感知。
  3. 增量更新检测 (Incremental Update Check)

    • 实现细节:实现 extractMenuEssential 函数提取影响渲染的关键字段(如 id, path, name, icon, children 等),再结合 lodash-esisEqual 进行深度比对。
    • 比对原理
      • 字段筛选:只比对前端关注的 UI 字段,过滤掉后端返回的无关元数据(如 createTime, updateTime 等)。这不仅减少了数据量,也避免了因无关字段变化导致的无效渲染。
      • 结构比较:对筛选后的精简对象树进行深度递归比较。
    • 效率与准确性分析
      • 更高效:相比全量对象比对,剔除无关字段后,比对的数据量大幅减少,性能提升显著。
      • 更精准:只响应业务相关的变更。
      • 对比 Vue 3 Diff:虽然 Vue 的 Diff 已经很快,但在数据层拦截变更(Data-Level Diff)可以完全跳过组件实例的更新流程(Update Cycle),即连 VNode 都不生成,是最高效的优化手段。
    • 执行策略
      • 如果关键指纹一致:直接 return,不执行 webCache.set,也不更新响应式变量 menuList。这彻底切断了后续的 Vue 响应式链路,实现了零重绘
      • 如果指纹变更:更新缓存,并触发 Vue 的响应式更新,视图随之刷新。

技术实现

1. 增量更新逻辑 (CachePermissions.ts)

利用 extractMenuEssential 提取关键特征,结合 lodash-es/isEqual 实现高效的增量检测。

// src/composables/cache/CachePermissions.ts
import { isEqual } from 'lodash-es'

/**
 * 提取菜单关键字段用于比对
 * @description 只保留影响渲染的关键字段,忽略 createTime 等无关字段
 */
function extractMenuEssential(menu: MenuItem): Partial<MenuItem> {
  // 只提取 id, path, name, icon, children 等 UI 相关字段
  const { id, path, name, icon, children, meta, type, enabled, sort } = menu
  const result: Partial<MenuItem> = { id, path, name, icon, meta, type, enabled, sort }

  if (children && children.length > 0) {
    result.children = children.map(extractMenuEssential) as MenuItem[]
  }
  return result
}

export function setCachePermissions(userInfo: UserInfoWithPermissions): void {
  // ... 数据预处理 ...

  // 1. 构建菜单树
  const sortedMenuTree = sortMenuTree(menuTree)

  // 2. 菜单树增量更新检测
  const cachedMenuTree = webCache.get(CACHE_KEY.ROLE_ROUTERS)
  
  // 使用关键特征比对,而非全量比对
  if (isMenuTreeChanged(sortedMenuTree, cachedMenuTree)) {
    webCache.set(CACHE_KEY.ROLE_ROUTERS, sortedMenuTree)
    console.log('[Permission] 菜单数据已更新')
  } else {
    console.log('[Permission] 菜单数据无变更,跳过更新')
  }
}

2. 组件侧渲染策略 (MainMenu.vue)

组件初始化时采用同步读取缓存 + 异步更新的模式。

// src/layout/components/MainMenu/src/MainMenu.vue

/**
 * 从缓存加载并构建菜单
 */
function loadMenusFromCache() {
  const localRouters = webCache.get(CACHE_KEY.ROLE_ROUTERS)
  // ... 构建菜单 ViewModel ...
  
  // Vue 的响应式系统会自动处理 Diff,但这里我们只在数据变动时赋值更好
  // 或者依赖 Vue 3 高效的 Virtual DOM Diff
  menuList.value = finalMenuList
}

/**
 * 初始化用户数据和菜单
 * @description 采用"优先缓存,后台更新"策略
 */
async function initUserStoreAndMenus(): Promise<void> {
  // 1. 【关键】立即从缓存加载菜单,消除白屏
  loadMenusFromCache()

  // 2. 异步获取最新数据 (静默更新)
  try {
    await userStore.setUserInfoAction()
    
    // 3. 数据更新后,重新加载
    // 由于 setCachePermissions 做了增量检测,如果数据没变,
    // webCache.get 获取的引用可能没变(取决于 storage 实现),
    // 即使变了,Vue 的 diff 也能处理,但最重要的是避免了数据抖动
    loadMenusFromCache()
  } catch (e) {
    console.warn('用户信息同步失败,降级使用缓存')
  }
}

// 立即执行
initUserStoreAndMenus()

优化效果与收益分析

1. 核心指标对比

关键指标 (KPI) 优化前 (Baseline) 优化后 (Optimized) 收益 (Gain) 备注
首屏菜单可见耗时 (FMP) 300ms - 1000ms 0ms (即时) ∞ (无限提升) 彻底消除白屏等待
视觉稳定性 (CLS) 存在抖动 (Layout Shift) 极其稳定 100% 无 Loading -> Content 突变
Vue 重绘频率 (Re-render) 100% (每次刷新必重绘) < 1% (仅数据变更时) 降低 99% 节省客户端 CPU/Memory
网络容错率 0% (接口挂=菜单挂) 99.9% (接口挂=用旧菜单) 高可用 离线/弱网可用

2. 流程对比 (Mermaid)

优化前:串行阻塞渲染

sequenceDiagram
    participant U as 用户
    participant P as 页面(Vue)
    participant A as API
    
    U->>P: 进入页面
    P->>P: 渲染框架(无菜单)
    Note right of P: ❌ 此时菜单区域空白
    P->>A: 请求权限接口
    A-->>P: 返回数据 (300ms)
    P->>P: 生成菜单树
    P->>P: 渲染菜单DOM
    Note right of P: ✅ 此时才显示菜单

优化后:并行非阻塞渲染

sequenceDiagram
    participant U as 用户
    participant P as 页面(Vue)
    participant C as 本地缓存
    participant A as API
    
    U->>P: 进入页面
    P->>C: 读取缓存菜单
    C-->>P: 返回旧数据
    P->>P: **立即渲染菜单**
    Note right of P: ✅ 菜单瞬间可见 (0ms)
    
    par 静默更新
        P->>A: 请求最新权限
        A-->>P: 返回新数据
        P->>P: **增量比对(Diff)**
        alt 数据有变更
            P->>C: 更新缓存
            P->>P: 触发Vue更新视图
        else 数据无变更
            P->>P: ⛔ 拦截更新(无重绘)
        end
    end

总结

对于读多写少(Read-heavy, Write-rarely)的数据,如菜单、字典、配置项,“缓存优先 + 增量更新” 是提升用户体验的黄金法则。它将网络延迟从用户感知的关键路径(Critical Path)中移除,让 Web 应用拥有了原生应用般的流畅度。

Vue自定义拖拽指令架构解析:从零到一实现元素自由拖拽

在Vue项目开发中,拖拽功能是提升用户体验的重要交互方式。本文将详细介绍如何基于Vue自定义指令实现一个功能完整的拖拽组件,让你轻松为元素添加拖拽能力。

Tips: 如果时间急迫,可以忽略文中介绍,直接划到代码处,CV代码即可实现拖拽功能

一、拖拽指令的核心原理

拖拽功能的本质是通过监听鼠标事件(mousedownmousemovemouseup)或触摸事件来控制元素的位置变化。在Vue中,我们可以通过自定义指令的方式将这些逻辑封装起来,实现可复用的拖拽功能。 自定义指令的优势在于可以将拖拽逻辑与组件业务逻辑分离,使代码更加清晰和易于维护。Vue指令的生命周期钩子(如bindinsertedunbind)为我们提供了完美的集成点。

二、环境准备与项目设置

在开始之前,确保你已经创建了一个Vue项目。可以使用Vue CLI快速搭建:

# 创建Vue项目
vue create my-drag-project

# 进入项目目录
cd my-drag-project

# 启动开发服务器
npm run serve

三、基础拖拽指令实现

我们先创建一个基本的拖拽指令文件drag.js,实现最核心的拖拽功能:

import Vue from "vue";

// 自定义元素实现弹框拖拽[重点]
Vue.directive("draw", {
  inserted: function (el, bindding, vNode) {
    let left, top, width, height;
    const resetButton = el.querySelector('.reset-button'); // 获取重置按钮
    
    // 设置元素基础样式
    el.setAttribute('style', 'position: fixed; z-index: 9999;');
    el.setAttribute('draggable', true);

    // 记录初始位置
    const styleOrigin = window.getComputedStyle(el, null);
    const defaultPosition = {
      left: el.offsetLeft - parseInt(styleOrigin.marginLeft),
      top: el.offsetTop - parseInt(styleOrigin.marginTop),
    };
    
    // 拖拽开始事件处理
    el._dragstart = function (event) {
      console.log("_dragstart");
      event.stopPropagation();
      left = event.clientX - el.offsetLeft;
      top = event.clientY - el.offsetTop;
      width = el.offsetWidth;
      height = el.offsetHeight;
      
      // 显示重置按钮
      resetButton.style.display = "block";
    };
    
    // 检查位置,防止被拖出边界
    el._checkPosition = function () {
      let width = el.offsetWidth;
      let height = el.offsetHeight;
      let left = Math.min(el.offsetLeft, document.body.clientWidth - width);
      left = Math.max(0, left);
      let top = Math.min(el.offsetTop, document.body.clientHeight - height);
      top = Math.max(0, top);
      
      el.style.left = left + 'px';
      el.style.top = top + 'px';
    };
    
    // 拖拽结束事件处理
    el._dragEnd = function (event) {
      event.stopPropagation();
      left = event.clientX - left;
      top = event.clientY - top;
      
      el.style.left = left + 'px';
      el.style.top = top + 'px';
      el.style.marginTop = '0px';
      
      el._checkPosition(); // 最终位置检查
    };

    // 窗口大小变化时重置位置
    el._resetPosition = function () {
      el.style.marginTop = '0px';
    };

    // 重置按钮点击事件
    resetButton.addEventListener("click", function () {
      // 重置元素到默认位置
      el.style.left = defaultPosition.left + "px";
      el.style.top = defaultPosition.top + "px";
      resetButton.style.display = "none";
    });

    // 允许拖拽放置
    document.body.addEventListener("dragover", function (event) {
      event.preventDefault();
    });

    // 绑定事件监听器
    el.addEventListener('dragstart', el._dragstart);
    el.addEventListener('dragend', el._dragEnd);
    window.addEventListener('resize', el._resetPosition);
  },

  // 指令解绑时清理资源
  unbind: function (el, bindding, vNode) {
    el.removeEventListener("dragstart", el._dragstart);
    el.removeEventListener("dragend", el._dragEnd);
    window.removeEventListener("resize", el._checkPosition);
  },
});

四、注册和使用拖拽指令

1. 全局注册指令

main.js中全局注册指令,使其在整个项目中可用:

import Vue from 'vue';
import App from './App.vue';

// 导入拖拽指令
import './directives/drag';

new Vue({
  render: h => h(App)
}).$mount('#app');

2. 在组件中使用

在Vue组件中,只需简单添加v-draw指令即可使元素可拖拽:

<template>
  <div id="app">
    <div v-draw class="draggable-box">
      <h3>可拖拽的盒子</h3>
      <p>按住并拖动我可以移动位置</p>
      <button class="reset-button" style="display:none;">重置位置</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
.draggable-box {
  width: 300px;
  padding: 20px;
  background-color: #f5f5f5;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  cursor: move;
}

.reset-button {
  margin-top: 10px;
  padding: 5px 10px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.reset-button:hover {
  background-color: #45a049;
}
</style>

五、自定义指令实现拖拽的优势

使用Vue自定义指令实现拖拽功能相比传统方法或第三方库具有多重优势:

1. 代码复用性与维护性

自定义指令允许你将重复的DOM操作封装成一个独立的指令,从而实现代码的复用。这样可以避免同样的代码在多个组件中重复出现,提高代码的可读性和可维护性。

2. 逻辑分离与关注点分离

将DOM操作逻辑从组件的业务逻辑中分离出来,使组件更专注于数据和状态的管理,而不是处理DOM事件。这样可以使组件的代码更加简洁和清晰,同时也使得DOM操作逻辑更容易被测试和调试。

3. 轻量级解决方案

相比于引入完整的拖拽库,自定义指令只需几十行代码就能实现核心功能,减少了项目体积。指令可以应用到任意元素上,不需要改变原有组件结构,维护起来更加方便。

4. 灵活性与可定制性

通过自定义指令,你可以完全控制拖拽的每个细节,包括拖拽手柄、边界限制、动画效果等。这种灵活性是使用第三方库难以达到的。

5. 性能优化

指令内部会正确管理事件监听器的绑定和解绑,避免内存泄漏问题。同时,由于不需要加载外部库,减少了资源请求,提升了页面加载速度。

六、实际应用场景

自定义拖拽指令在实际项目中有广泛的应用场景:

  1. 弹窗拖拽:最常见的应用场景,让用户可以自由移动对话框位置。
  2. 看板应用:在任务看板中拖动卡片到不同列。
  3. 设计工具:让用户可以自由调整设计元素的位置。
  4. 游戏开发:简单游戏中的可拖动元素实现。
  5. 仪表盘定制:允许用户自由排列仪表盘中的各个组件。

七、总结

通过本文的详细介绍,我们实现了一个功能完整的Vue拖拽指令,并深入探讨了其优势和使用注意事项。自定义指令的方式实现拖拽功能,不仅代码简洁、性能优异,而且具有极高的灵活性和可维护性。 相比引入第三方拖拽库,自定义指令方案具有明显的轻量级优势,减少了项目依赖和打包体积。同时,由于代码完全可控,可以根据具体业务需求进行定制化开发,不受第三方API限制。 在实际项目中,建议根据具体需求对示例代码进行扩展,例如添加拖拽手柄、动画效果、拖拽边界限制等高级功能。通过合理利用Vue自定义指令的特性,可以大大提升开发效率和用户体验。

VS Code插件的发布与自动化

本文档总结了如何配置 VS Code 插件的自动化发布流程,自动构建并将插件发布到 VS Code MarketplaceOpen VSX Registry 两个平台。

1. 获取发布 Token

自动化由于需要权限验证,首先需要获取两个平台的 Access Token。

1.1 VS Code Marketplace (Visual Studio Marketplace)

  1. 访问 Azure DevOps 并登录(通常使用微软账号)。
  2. 点击右上角的用户设置图标 -> Personal success tokens
  3. 点击 + New Token
    • Name: 填写描述,如 VS Code Extension
    • Organization: 选择 All accessible organizations.
    • Expiration: 建议选择 All scopes 下较长的时间,或者自定义。
    • Scopes: 找到 Marketplace,勾选 AcquireManage(或者直接选 Full access 简单粗暴,但要注意安全)。
  4. 复制生成的 Token,它只显示一次。

1.2 Open VSX Registry

  1. 访问 Open VSX 并登录(使用 GitHub 账号)。
  2. 点击右上角头像 -> Settings
  3. Access Tokens 区域,生成一个新的 Token。
  4. 复制生成的 Token

2. 配置 GitHub Secrets

为了安全起见,不要将 Token 直接写在代码里。

  1. 进入你的 GitHub 仓库页面。
  2. 点击 Settings -> Secrets and variables -> Actions
  3. 点击 New repository secret,添加以下两个密钥:
    Name Value 描述
    VSCE_TOKEN (Azure DevOps 生成的 Token) 用于发布到 VS Code Marketplace
    OVSX_TOKEN (Open VSX 生成的 Token) 用于发布到 Open VSX

3. 安装工具 (本地测试用)

虽然 CI 会自动安装,但在本地常用这两个工具进行验证或手动发布。

  • vsce: 微软官方的发布工具。
npm install -g @vscode/vsce
  • ovsx: Open VSX 的发布工具。
npm install -g ovsx

常用命令:

  • 打包:vsce package (生成 .vsix 文件)
  • 发布 (VS Code): vsce publish -p <TOKEN>
  • 发布 (Open VSX): ovsx publish -p <TOKEN>

4. 编写 GitHub Actions Workflow

在仓库根目录创建 .github/workflows/publish.yml

这个配置实现了:

  1. 触发条件:当推送 v*.*.* 格式的 tag 时触发(如 git push origin v1.0.5)。
  2. 环境准备:使用 Node.js 20+ (解决部分 npm 兼容性问题)。
  3. 发布流程
    • 自动安装依赖 (vsce, ovsx)。
    • 打包插件。
    • 尝试发布到 VS Code Marketplace。
    • 尝试发布到 Open VSX。
    • 自动创建 GitHub Release 并上传 .vsix 文件。
name: Publish Extension

on:
  push:
    tags:
      - 'v*.*.*'  # 仅在推送版本 tag 时触发
  workflow_dispatch:  # 允许手动触发

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write # 允许创建 Release

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # 使用较新的 Node 版本以避免 "Exit handler never called" 等错误
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      # 安装发布工具
      - name: Install tools
        run: |
          npm install -g @vscode/vsce
          npm install -g ovsx

      # 依赖安装 (如果项目有打包构建步骤)
      - name: Install dependencies
        run: npm ci

      # 打包检查
      - name: Package extension
        run: npx vsce package

      # 发布到 VS Code Marketplace
      # continue-on-error: true 防止因为版本号重复等非致命错误导致中断后后续流程
      - name: Publish to VS Code Marketplace
        run: npx vsce publish -p ${{ secrets.VSCE_TOKEN }}
        continue-on-error: true

      # 发布到 Open VSX Registry
      - name: Publish to Open VSX Registry
        run: ovsx publish -p ${{ secrets.OVSX_TOKEN }}
        continue-on-error: true

      # 获取当前版本号
      - name: Get version
        id: package-version
        # 注意:这里的引号处理很重要,避免 shell 解析错误
        run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

      # 创建 GitHub Release
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          files: '*.vsix'
          generate_release_notes: true
          tag_name: ${{ github.ref_name }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

5. 发布流程总结

  1. 修改代码 并测试通过。

  2. 更新版本号: 修改 package.json 中的 version 字段 (例如 1.0.5 -> 1.0.6)。

  3. 提交代码:

    git add .
    git commit -m "chore: release v1.0.6"
    git push
    
  4. 打 Tag 并推送 (这是触发自动发布的关键):

    git tag v1.0.6
    git push origin v1.0.6
    
  5. 观察 Actions: 去 GitHub 仓库的 Actions 页面查看运行状态。

整个流程大概一分钟左右(取决于你的插件复杂度),你的插件就会发布到 VS Code MarketplaceOpen VSX Registry 两个平台以及在你的Github中创建相关release

当然啦,如果你是内部仓库,这套流程可能就不太适合你,但是大体流程都是一样的,希望能帮到你

微前端架构(二):封装与实现

一、前言

很荣幸由我设计并实现了公司自研的微前端框架,目前该框架已在多个业务项目中广泛应用。

本文是我结合内部培训、产品文档及相关实践梳理而成的总结,旨在对微前端技术体系进行系统性的整理与沉淀。

本篇是系列文章的第二篇,分享一些关于微前端的封装和实践。

二、产品简介

XX微前端是基于qiankun和公司权限管理系统实现的微前端架构解决方案,旨在帮助大家能更简单、无痛地构建一个生产可用微前端架构系统。

它在XX基础模板的基础上,兼容适配了XX已有的用户认证、权限控制、埋点功能,并集成了一系列微前端能力,包括应用加载、应用通信、应用隔离、应用缓存等。

image.png

三、核心封装与实现

3.1 应用缓存

在主应用中使用 v-show 指令控制子应用容器,以实现子应用的切换与缓存。

<!-- 子应用容器 -->
<template v-for="item in apps">
  <div
    class="app-container"
    v-show="currentAppName && currentAppName === item.name"
    :id="`container-${item.name}`"
    :key="item.name"
  ></div>
</template>

3.2 应用加载

采用qiankun的loadMicroApp实现多标签页场景下的应用加载。

image.png

核心逻辑说明:监听路由变化=》根据路由匹配子应用=》loadMicroApp手动加载子应用,给挂载到指定容器。

// 1. 监听路由变化
watch: {
    $route: {
      immediate: false,
      handler(route) {
        this.openMicroApp(route);
      }
    }
  },
// 2. 根据路由信息获取菜单数据及子应用信息
const resource = getMenuByRoute(route);
subApp = resource && this.getAppById(resource.rootResourceId);
// 2.1 菜单资源数据映射关系
{
  id: item.resourceId, // web应用资源id
  name: item.resourceCode, // web应用资源编码
  entry: item.resourceUrl // web应用基础路径
}
// 3. 调用loadMicroApp加载子应用
const { name, entry } = subApp;
const appInstances = loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {}
});

以上代码是简化后的示意代码,实际实现中还包含有对容器节点、路由、loading等细节的处理。

3.3 应用通信

3.3.1 基于props

qiankun的loadMicroApp 方法支持通过props配置选项,传递数据或者方法给子应用。

// 1. 主应用定义并通过props.data传递
const mainAppName = 'app-main'
const appInstances = loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {
    data: {
      mainAppName
    }
  }
});

// 2. 子应用接收
export async function mount(props) {
  render(props);
}
function render(props) {
  const { data = {}, container } = props || {};
  console.log(data.mainAppName)
}

3.3.2 基于window

通过挂载全局属性和方法,供其他应用使用。

// 1. 主应用定义
window.$mainAxxxxxxx = initAxxxxxxx(router);

// 2. 子应用通过window调用
window.$mainAxxxxxxx.xxx();

3.3.3 基于事件

框架选用Node.js的EventEmitter,由主应用集成并对外提供event-bus基于事件总线的应用间通信能力。包含事件的注册、派发和移除功能。

// 1. 主应用实例化事件对象
import { EventEmitter } from 'events';
const eventBus = new EventEmitter();
// 2. 监听事件
eventBus.on('event1', (params1, params2) => {
  // ...do some things
});
// 3. 传递实例给子应用
loadMicroApp({
  props: {
    data: {
      eventBus
    }
  }
});

// 4. 子应用接收并派发事件
function render(props) {
  const { data = {}, container } = props || {};
  if (data.eventBus) {
    data.eventBus.emit('event1', params1, params2);
  }
}
// 5. 子应用也可以 监听其他应用派发的事件
data.eventBus.on('xxx',xxx)

注意事项:

  • 在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。可以用,但不建议大量使用。
  • 删除在其他地方添加的监听器是不好的做法,特别是当 EventEmitter 实例是由其他组件或模块(例如套接字或文件流)创建时。存在较大风险隐患。

3.3.4 基于vuex

通过共享主应用store, 可以快速将基座的vuex注册到微应用自己的vuex实例上。

// 1. 主应用定义并通过props传递store
import qiankunCommonStore from '@/store/modules/common';
loadMicroApp({
  name,
  entry,
  container: `#container-${name}`,
  props: {
    data: {
      store: qiankunCommonStore,
    }
  }
});
// 2. 子应用store注册到自己的vuex实例上
function render(props) {
  const { data = {}, container } = props || {};
  if (store && store.hasModule && data.store) {
    store.registerModule('mainAppStore', data.store);
  }
}
// 3.1 使用:取值
const userInfo = this.$store.state.mainAppStore.userInfo
// 3.2 使用:赋值
this.$store.commit('mainAppStore/setUserInfo', { ...this.userInfo, newData: 'home-new-data' });

3.3.5 基于initGlobalState

qiankun提供的基于全局状态的通信方式。模板中未集成,如需使用,可参看qiankun文档自行扩展。

未集成的原因是,作者并不推荐该API并计划在未来版本中移除。

globalState 不是合理的微前端通信方案,会加剧应用之间的耦合.。 -- qiankun官方issues

3.4 应用跳转

3.4.1 应用内跳转

调用自身路由router的方法

onJump(path) {
  this.$router.push({
    path,
    query
  });
},

3.4.2 应用间跳转

使用主应用路由方法

onJumpOther(path) {
  this.$root.mainAppRouter.push({
    path,
    query
  });
},

3.5 应用隔离

3.5.1 js沙箱

众所周知javaScript的全局作用域存在命名冲突、变量污染等风险,为了解决该问题,qiankun借助 ES6 的 proxy ,创建js代理沙箱,实现了应用间环境的隔离。

qiankun已有实现,默认为开启状态。

参考资料:

3.5.2 样式隔离

为了确保不同组件或模块之间的样式不会相互影响,从而提高代码的可维护性和可复用性。qiankun提供有两种样式隔离方案,strictStyleIsolation和experimentalStyleIsolation。

qiankun默认开启strictStyleIsolation严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。

参考资料:

3.5.3 本地存储隔离

为了防止各微应用间操作本地存储时,数据覆盖、误删除等问题,框架封装并集成了本地存储功能模块。原理是在基座运行时,覆写localStorage、sessionStorage方法,给本地存储的key 统一加上前缀。

我们对此封装了一个组件,代码 和API就不赘述了,这里仅分享,在去覆写的时候遇到的一个卡点问题和解决方案。

问题描述:修改Storage接口的原型方法后,乾坤无法隔离, 导致所有应用(包括主应用)的相关方法均被修改。

解决方案:在引入qiankun之前删除window.localStorage,随后再重新声明,此时的window.localStorage是隔离的,在主子应用分别定义,互不影响。

// 主应用
window.originStorage = window.localStorage;
delete window.localStorage;
window.localStorage = {
 getItem(...ags) {
   console.log("this", this);
   return window.originStorage.getItem(...ags);
},
};

// 子应用
window.localStorage = {
 getItem(key, ...ags) {
   console.log("insub");
   return window.originStorage.getItem(`app-vue${key}`, ...ags);
},
};

原理是qiankun的沙箱(proxySandBox),会从window对象拷贝不可配置的属性并filter掉,不走proxy代理,比如location、localStorage等。现在咱们在qiankun沙箱逻辑执行前,把localStorage属性移除,它拷贝不到,后面咱们再加上的localStorage就会走proxy然后被隔离起来。

3.6 权限控制

整体策略是,各微应用分别进行权限控制,谁的页面谁管控。

在微前端场景下,由于各微应用的路由监听的是同一url,当url切换时,所有微应用的路由守卫都会触发。需要对不是自己应用路由放行。

// 微前端运行时的路由守卫
function microAppHook(to, from, next){
  const isAppNameMatched = to.meta.appName ? to.meta.appName === appName : true,
    isNotMyRoute = !to.path || !to.name || !isAppNameMatched;
  if (isNotMyRoute) {
    // 非当前应用路由,跳过处理
    next();
    return;
  }
  // 权限控制:白名单、角色菜单权限判断
  if (hasPermission(to) || hasPermission(to, false)) {
    next();
  }else{
    next({path: '/404', replace: true});
  }
}

四、后续改进

  1. 性能提升

    1. 内存占用高:在低版本火狐浏览器内存占用经常性超过2G,页面卡顿明显
    2. 加载速度慢:超过5M的子应用资源,请求和加载长达3-5秒
  2. 简化路由、菜单资源的处理

    1. 当前关于菜单资源的处理较复杂且不灵活,有较大提升空间

前端工程化终极指南(Webpack + Gulp + Vite + 实战项目)

📚 目录

  1. 前端工程化概述
  2. Webpack深度解析
  3. Gulp构建实践
  4. Vite现代化构建
  5. 工程化最佳实践
  6. CI/CD与部署自动化
  7. 实战项目搭建
  8. 性能优化与监控

前端工程化概述

什么是前端工程化?

前端工程化是指使用工程化方法和工具来规范前端开发流程、提高开发效率、保证代码质量的综合性解决方案。

核心目标:

  • 标准化:统一的开发规范和流程
  • 自动化:减少重复性手工操作
  • 效率化:提升开发和构建性能
  • 质量化:保证代码质量和项目稳定性

工程化体系:

graph TD
    A[前端工程化] --> B[开发工具链]
    A --> C[构建工具]
    A --> D[代码规范]
    A --> E[自动化测试]
    A --> F[部署流程]
    
    B --> B1[代码编辑器]
    B --> B2[调试工具]
    B --> B3[版本控制]
    
    C --> C1[Webpack]
    C --> C2[gulp]
    C --> C3[Vite]
    
    D --> D1[ESLint]
    D --> D2[Prettier]
    D --> D3[Git Hooks]
    
    E --> E1[单元测试]
    E --> E2[E2E测试]
    E --> E3[性能测试]
    
    F --> F1[CI/CD]
    F --> F2[容器化]
    F --> F3[监控]

工程化发展历程

// 传统开发方式(手工时代)
function manualDevelopment() {
    // 手动下载依赖库
    const jquery = downloadFromCDN('jquery.min.js');
    const bootstrap = downloadFromCDN('bootstrap.min.js');
    
    // 手动合并文件
    const combinedJS = concatenateFiles([jquery, bootstrap, 'app.js']);
    
    // 手动压缩
    const minifiedJS = minify(combinedJS);
    
    // 手动上传到服务器
    uploadToServer(minifiedJS);
}

// 工程化开发方式(自动化)
function modernDevelopment() {
    // 包管理器
    npmInstall();
    
    // 模块化开发
    import { moduleA } from './modules';
    
    // 自动化构建
    webpackBuild();
    
    // 自动化测试
    runTests();
    
    // 自动化部署
    deployToProduction();
}

Webpack深度解析

Webpack核心概念

入口点(Entry):

// webpack.config.js
module.exports = {
    // ========== 单入口配置 ==========
    // 指定Webpack打包的入口文件,这是整个依赖图的起点
    // Webpack会从这个文件开始递归查找所有依赖的模块
    entry: './src/index.js',
    
    // ========== 多入口配置 ==========
    // 应用于多页面应用或需要分离业务代码和第三方库的场景
    // main: 主要的业务逻辑入口
    // vendor: 第三方库/公共模块的入口,便于长期缓存
    entry: {
        main: './src/index.js',           // 主应用入口
        vendor: './src/vendor.js'          // 第三方库入口
    },
    
    // ========== 动态入口配置 ==========
    // 异步返回入口文件路径,适用于需要根据条件动态选择入口的场景
    // 例如:根据环境变量、用户权限等决定加载哪个入口文件
    entry: () => new Promise((resolve) => {
        // 这里可以根据某些条件动态决定入口文件
        // 比如根据环境变量选择不同的入口
        resolve('./src/index.js');
    })
};

出口点(Output):

module.exports = {
    output: {
        // ===== 打包输出目录 =====
        // path.resolve确保得到绝对路径,__dirname是当前文件所在目录
        // 所有打包后的文件都会输出到dist目录下
        path: path.resolve(__dirname, 'dist'),
        
        // ===== 主文件命名规则 =====
        // [name]: 对应entry中的key(如main、vendor)
        // [contenthash]: 根据文件内容生成的hash值,内容变化时hash才会变
        // 作用:利于浏览器缓存,只有文件内容变化时才需要重新下载
        filename: '[name].[contenthash].js',
        
        // ===== 代码分割文件命名规则 =====
        // 用于动态import()或splitChunks分割出来的chunk文件
        // 确保这些异步加载的文件也有正确的缓存策略
        chunkFilename: '[name].[contenthash].chunk.js',
        
        // ===== 公共路径前缀 =====
        // 所有静态资源引用时会添加这个前缀
        // 开发环境:'/static/' 表示资源在静态服务器下
        // 生产环境:'https://cdn.example.com/' 表示使用CDN
        publicPath: '/static/',
        
        // ===== 自动清理输出目录 =====
        // true: 每次构建前自动清理dist目录
        // 避免旧文件残留,确保输出目录的干净
        clean: true,
        
        // ===== 资源文件命名规则 =====
        // 针对图片、字体等静态资源文件的命名规则
        // [hash]: 文件内容的hash值,[ext]: 文件扩展名,[query]: URL查询参数
        assetModuleFilename: 'assets/[hash][ext][query]'
    }
};

加载器(Loaders):

module.exports = {
    module: {
        rules: [
            // ========== JavaScript/TypeScript/JSX/TSX处理 ==========
            {
                // 匹配所有.js, .jsx, .ts, .tsx结尾的文件
                test: /\.(js|jsx|ts|tsx)$/,
                
                // 排除node_modules目录,避免对第三方库进行转译,提高构建速度
                exclude: /node_modules/,
                
                // 使用多个loader,执行顺序从右到左
                use: [
                    // ===== ESLint代码检查 =====
                    // 先进行代码规范检查,在Babel转译之前
                    'eslint-loader',
                    
                    // ===== Babel转译 =====
                    {
                        loader: 'babel-loader',
                        options: {
                            // Babel预设配置
                            presets: [
                                // @babel/preset-env: 根据目标浏览器自动转换ES6+语法
                                ['@babel/preset-env', {
                                    targets: {
                                        // 浏览器兼容性目标
                                        // '> 1%': 全球使用率大于1%的浏览器
                                        // 'last 2 versions': 每个浏览器的最后2个版本
                                        browsers: ['> 1%', 'last 2 versions']
                                    }
                                }]
                            ],
                            // Babel插件配置
                            plugins: [
                                // 转换运行时,避免在每个文件中重复引入helper函数
                                '@babel/plugin-transform-runtime',
                                // 转换类属性语法(class MyClass { prop = value; })
                                '@babel/plugin-proposal-class-properties'
                            ]
                        }
                    }
                ]
            },
            
            // ========== CSS文件处理 ==========
            {
                test: /\.css$/,
                use: [
                    // ===== 将CSS注入到DOM =====
                    // 创建style标签,将CSS插入到页面head中
                    // 开发环境使用,便于热更新
                    'style-loader',
                    
                    // ===== CSS模块化处理 =====
                    // 解析CSS文件中的@import和url()
                    // 支持CSS Modules(配置css-loader.options.modules)
                    'css-loader',
                    
                    // ===== PostCSS处理 =====
                    // 自动添加浏览器前缀(-webkit-, -moz-等)
                    // 使用autoprefixer插件,根据caniuse数据添加前缀
                    'postcss-loader'
                ]
            },
            
            // ========== SCSS/Sass文件处理 ==========
            {
                test: /\.scss$/,
                use: [
                    // 将编译后的CSS注入到DOM
                    'style-loader',
                    // 解析SCSS中的@import和url()
                    'css-loader',
                    // 将SCSS编译为CSS
                    'sass-loader',
                    
                    // ===== 全局变量/混合器注入 =====
                    // 在所有SCSS文件中自动引入全局变量和混合器
                    // 避免每个文件都要手动@import
                    {
                        loader: 'sass-resources-loader',
                        options: {
                            // 全局引入的SCSS文件路径
                            resources: './src/styles/variables.scss'
                        }
                    }
                ]
            },
            
            // ========== 图片资源处理 ==========
            {
                test: /\.(png|jpe?g|gif|webp|svg)$/,
                
                // ===== 资源类型 =====
                // 'asset': Webpack4+的统一资源处理方式
                // 根据文件大小自动选择内联base64或独立文件
                type: 'asset',
                
                // ===== 内联条件设置 =====
                // 当文件小于8KB时,转为base64内联到JS中
                // 优点:减少HTTP请求,缺点:增加JS体积
                // 适用于小图标、小图片等
                parser: {
                    dataUrlCondition: {
                        maxSize: 8 * 1024 // 8KB以下转为base64
                    }
                },
                
                // ===== 文件输出路径 =====
                // 大文件独立输出时的命名规则
                // [name]: 原文件名,[hash]: 内容hash,[ext]: 扩展名
                generator: {
                    filename: 'images/[name].[hash][ext]'
                }
            },
            
            // ========== 字体文件处理 ==========
            {
                test: /\.(woff2?|eot|ttf|otf)$/,
                
                // ===== 字体文件类型 =====
                // 'asset/resource': 始终输出为独立文件,不内联
                // 字体文件通常较大,不适合内联
                type: 'asset/resource',
                
                // ===== 字体文件输出路径 =====
                generator: {
                    filename: 'fonts/[name].[hash][ext]'
                }
            }
        ]
    }
};

插件(Plugins):

// ===== 导入所需的Webpack插件 =====
const HtmlWebpackPlugin = require('html-webpack-plugin');           // HTML模板插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');  // CSS提取插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');    // 清理目录插件
const CopyWebpackPlugin = require('copy-webpack-plugin');          // 复制文件插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // Bundle分析插件

module.exports = {
    plugins: [
        // ========== HTML模板处理插件 ==========
        // 自动生成HTML文件,并注入打包后的JS和CSS文件
        // 省去手动管理script标签和link标签的麻烦
        new HtmlWebpackPlugin({
            // HTML模板文件路径
            template: './public/index.html',
            
            // 生成的HTML文件名
            filename: 'index.html',
            
            // 资源注入位置:'head'或'body'
            // 'body': 将script标签放在body底部,推荐做法
            inject: 'body',
            
            // ===== HTML压缩配置 =====
            // 生产环境启用HTML压缩,减少文件体积
            minify: {
                removeComments: true,              // 移除HTML注释
                collapseWhitespace: true,           // 折叠空白字符
                removeRedundantAttributes: true,   // 移除冗余属性(如type="text/javascript")
                useShortDoctype: true,             // 使用短doctype(<!DOCTYPE html>)
                removeEmptyAttributes: true,        // 移除空属性
                removeStyleLinkTypeAttributes: true,// 移除style/link的type属性
                keepClosingSlash: true,             // 自闭合标签保持/
                minifyJS: true,                    // 压缩内联JS
                minifyCSS: true,                   // 压缩内联CSS
                minifyURLs: true                   // 压缩内联URL
            }
        }),
        
        // ========== CSS提取插件 ==========
        // 将CSS从JS中提取出来,生成独立的CSS文件
        // 生产环境使用,便于浏览器并行加载CSS,避免FOUC(无样式内容闪烁)
        new MiniCssExtractPlugin({
            // 主CSS文件命名规则
            filename: 'css/[name].[contenthash].css',
            
            // 异步加载的CSS文件命名规则
            chunkFilename: 'css/[name].[contenthash].chunk.css'
        }),
        
        // ========== 目录清理插件 ==========
        // 在每次构建前清理输出目录
        // 确保每次构建都是全新的,避免旧文件残留
        new CleanWebpackPlugin(),
        
        // ========== 文件复制插件 ==========
        // 将静态文件直接复制到输出目录,不经过Webpack处理
        // 适用于不需要处理的静态资源,如favicon、robots.txt等
        new CopyWebpackPlugin({
            patterns: [
                {
                    // 源目录
                    from: 'public/static',
                    
                    // 目标目录(相对于output.path)
                    to: 'static',
                    
                    // 忽略的文件模式
                    globOptions: {
                        ignore: ['**/index.html'] // 忽略index.html,由HtmlWebpackPlugin处理
                    }
                }
            ]
        }),
        
        // ========== 环境变量定义插件 ==========
        // 在编译时将环境变量注入到代码中
        // 可以在代码中直接使用process.env.NODE_ENV等变量
        new webpack.DefinePlugin({
            'process.env': {
                // 将环境变量转换为字符串字面量
                NODE_ENV: JSON.stringify(process.env.NODE_ENV),
                API_URL: JSON.stringify(process.env.API_URL)
            }
        }),
        
        // ========== Bundle分析插件 ==========
        // 生成Bundle分析报告,帮助优化打包体积
        // analyzerMode: 'static' 生成静态HTML报告文件
        // openAnalyzer: false 构建完成后不自动打开报告
        new BundleAnalyzerPlugin({
            analyzerMode: 'static',
            openAnalyzer: false
        })
    ]
};

Webpack高级配置

环境配置分离:

// webpack.common.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash].js'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: 'babel-loader'
            },
            {
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader']
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            template: './public/index.html'
        })
    ],
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    }
};

// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = merge(common, {
    mode: 'production',
    devtool: 'source-map',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css'
        })
    ],
    optimization: {
        minimizer: [
            new TerserPlugin({
                parallel: true,
                terserOptions: {
                    compress: {
                        drop_console: true // 移除console
                    }
                }
            }),
            new CssMinimizerPlugin()
        ]
    }
});

// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'eval-source-map',
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        port: 3000,
        hot: true,
        overlay: true,
        historyApiFallback: true,
        proxy: {
            '/api': {
                target: 'http://localhost:8080',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
});

性能优化配置:

module.exports = {
    optimization: {
        // 代码分割
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    name(module) {
                        const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                        return `npm.${packageName.replace('@', '')}`;
                    }
                },
                common: {
                    name: 'common',
                    minChunks: 2,
                    chunks: 'initial',
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        },
        
        // 运行时代码分离
        runtimeChunk: {
            name: 'runtime'
        },
        
        // Tree shaking
        usedExports: true,
        sideEffects: false,
        
        // 压缩优化
        minimize: true,
        minimizer: [
            new TerserPlugin({
                parallel: true,
                terserOptions: {
                    compress: {
                        drop_console: true,
                        drop_debugger: true,
                        pure_funcs: ['console.log', 'console.info']
                    },
                    output: {
                        comments: false,
                        ascii_only: true
                    }
                },
                extractComments: false
            })
        ]
    },
    
    // 解析优化
    resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
        alias: {
            '@': path.resolve(__dirname, 'src'),
            '@components': path.resolve(__dirname, 'src/components'),
            '@utils': path.resolve(__dirname, 'src/utils'),
            '@assets': path.resolve(__dirname, 'src/assets')
        },
        modules: ['node_modules', 'src/vendor']
    },
    
    // 缓存配置
    cache: {
        type: 'filesystem',
        buildDependencies: {
            config: [__filename]
        }
    }
};

自定义Loader和Plugin

自定义Loader示例:

// my-custom-loader.js
module.exports = function(source) {
    // 去除console.log
    const noConsoleSource = source.replace(/console\.log\(.*?\);?/g, '');
    
    // 添加文件信息注释
    const filePath = this.resourcePath;
    const fileName = path.basename(filePath);
    const comment = `/* File: ${fileName} */\n`;
    
    return comment + noConsoleSource;
};

// webpack.config.js中使用
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    'babel-loader',
                    {
                        loader: path.resolve(__dirname, 'my-custom-loader.js'),
                        options: {
                            // loader选项
                        }
                    }
                ]
            }
        ]
    }
};

自定义Plugin示例:

// FileListPlugin.js
class FileListPlugin {
    constructor(options = {}) {
        this.filename = options.filename || 'filelist.md';
    }
    
    apply(compiler) {
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
            const filelist = [];
            
            // 遍历所有编译后的资源
            for (const filename in compilation.assets) {
                const source = compilation.assets[filename].source();
                const size = Buffer.byteLength(source, 'utf8');
                filelist.push(`- ${filename} (${size} bytes)`);
            }
            
            // 创建新的资源
            compilation.assets[this.filename] = {
                source: () => filelist.join('\n'),
                size: () => filelist.join('\n').length
            };
            
            callback();
        });
    }
}

// 使用自定义Plugin
module.exports = {
    plugins: [
        new FileListPlugin({
            filename: 'build-files.md'
        })
    ]
};

Gulp构建实践

Gulp基础配置

// gulpfile.js - Gulp构建配置文件

// ===== 导入Gulp核心方法 =====
const { src, dest, watch, series, parallel } = require('gulp');
const gulp = require('gulp');

// ===== 导入各种Gulp插件 =====
const sass = require('gulp-sass')(require('sass'));           // SCSS编译
const postcss = require('gulp-postcss');                    // PostCSS处理
const autoprefixer = require('autoprefixer');                // 浏览器前缀自动添加
const cssnano = require('cssnano');                         // CSS压缩
const babel = require('gulp-babel');                        // ES6+转ES5
const uglify = require('gulp-uglify');                      // JS压缩
const concat = require('gulp-concat');                      // 文件合并
const imagemin = require('gulp-imagemin');                  // 图片优化
const browserSync = require('browser-sync').create();       // 开发服务器
const del = require('del');                                // 文件删除
const plumber = require('gulp-plumber');                   // 错误处理
const sourcemaps = require('gulp-sourcemaps');              // 源码映射

// ===== 文件路径配置 =====
// 统一管理所有源文件和输出目录路径,便于维护
const paths = {
    html: 'src/**/*.html',              // HTML源文件路径
    styles: 'src/scss/**/*.scss',        // SCSS源文件路径
    scripts: 'src/js/**/*.js',           // JS源文件路径
    images: 'src/images/**/*',          // 图片源文件路径
    fonts: 'src/fonts/**/*',            // 字体源文件路径
    dist: 'dist'                        // 输出目录
};

// ===== 清理输出目录 =====
// 在每次构建前删除dist目录,确保输出环境的干净
function clean() {
    return del(paths.dist);
}

// ===== HTML文件处理 =====
// 将HTML文件从src复制到dist目录,不做其他处理
function html() {
    return src(paths.html)              // 读取HTML文件
        .pipe(plumber())                // 添加错误处理,防止错误中断构建流程
        .pipe(dest(paths.dist));         // 输出到dist目录
}

// ===== SCSS编译处理 =====
// 将SCSS编译为CSS,添加浏览器前缀,压缩,并生成源码映射
function styles() {
    return src(paths.styles)            // 读取SCSS文件
        .pipe(plumber())                // 错误处理
        .pipe(sourcemaps.init())        // 初始化源码映射
        .pipe(sass({ 
            outputStyle: 'expanded'     // 输出格式:expanded(展开式,便于调试)
        }).on('error', sass.logError))  // SCSS编译错误处理
        .pipe(postcss([                 // 使用PostCSS处理
            autoprefixer(),              // 自动添加浏览器前缀
            cssnano()                   // CSS压缩
        ]))
        .pipe(sourcemaps.write('.'))     // 写入源码映射文件
        .pipe(dest(`${paths.dist}/css`)) // 输出到dist/css目录
        .pipe(browserSync.stream());     // 触发浏览器热更新
}

// ===== JavaScript处理 =====
// 转译ES6+为ES5,合并文件,压缩,并生成源码映射
function scripts() {
    return src(paths.scripts)           // 读取JS文件
        .pipe(plumber())                // 错误处理
        .pipe(sourcemaps.init())        // 初始化源码映射
        .pipe(babel({                  // Babel转译
            presets: ['@babel/preset-env'] // 使用env预设,根据目标浏览器自动转译
        }))
        .pipe(concat('main.min.js'))     // 合并所有JS文件为单个文件
        .pipe(uglify())                 // 压缩JS代码
        .pipe(sourcemaps.write('.'))     // 写入源码映射
        .pipe(dest(`${paths.dist}/js`)) // 输出到dist/js目录
        .pipe(browserSync.stream());     // 触发浏览器热更新
}

// ===== 图片优化处理 =====
// 压缩和优化各种格式的图片文件
function images() {
    return src(paths.images)            // 读取图片文件
        .pipe(imagemin([               // 图片压缩处理
            // ===== GIF优化 =====
            imagemin.gifsicle({ 
                interlaced: true     // 交错式GIF,渐进加载
            }),
            
            // ===== JPEG优化 =====
            imagemin.mozjpeg({ 
                quality: 80,        // 压缩质量:80%(平衡质量和文件大小)
                progressive: true   // 渐进式JPEG
            }),
            
            // ===== PNG优化 =====
            imagemin.optipng({ 
                optimizationLevel: 5  // 优化级别:1-7,5是较好的平衡点
            }),
            
            // ===== SVG优化 =====
            imagemin.svgo({
                plugins: [
                    { removeViewBox: true },    // 移除viewBox(如果可以)
                    { cleanupIDs: false }       // 保留ID,避免CSS选择器失效
                ]
            })
        ]))
        .pipe(dest(`${paths.dist}/images`)); // 输出到dist/images目录
}

// ===== 字体文件复制 =====
// 将字体文件直接复制到输出目录,不做处理
function fonts() {
    return src(paths.fonts)            // 读取字体文件
        .pipe(dest(`${paths.dist}/fonts`)); // 输出到dist/fonts目录
}

// ===== 开发服务器配置 =====
// 启动本地开发服务器,支持热更新和实时刷新
function serve() {
    browserSync.init({
        server: {
            baseDir: paths.dist     // 服务器根目录
        },
        port: 3000,                // 端口号
        open: true,                 // 自动打开浏览器
        notify: false               // 不显示浏览器通知
    });
    
    // ===== 文件监听配置 =====
    // 监听不同类型的文件变化,执行相应的构建任务
    watch(paths.styles, styles);                      // SCSS文件变化时,重新编译样式
    
    watch(paths.scripts, scripts);                    // JS文件变化时,重新编译脚本
    
    // HTML文件变化时,重新复制并刷新浏览器
    watch(paths.html, html).on('change', browserSync.reload);
    
    // 图片文件变化时,重新优化并刷新浏览器
    watch(paths.images, images).on('change', browserSync.reload);
}

// ===== 构建任务组合 =====
// 使用series(串行)和parallel(并行)组合多个任务
const build = series(
    clean,                           // 首先清理目录
    parallel(                        // 然后并行执行以下任务
        html,                      // HTML处理
        styles,                    // 样式处理
        scripts,                   // 脚本处理
        images,                    // 图片优化
        fonts                      // 字体复制
    )
);

// ===== 开发任务 =====
// 先执行完整构建,然后启动开发服务器
const dev = series(build, serve);

// ===== 任务导出 =====
// 将任务导出为Gulp命令,可在命令行中使用
exports.default = dev;      // 默认任务:npm run gulp 或 npm start
exports.build = build;      // 生产构建:npm run gulp build
exports.serve = serve;      // 仅启动服务:npm run gulp serve

高级Gulp配置

// 复杂的Gulp配置
const gulp = require('gulp');
const $ = require('gulp-load-plugins')();
const webpackStream = require('webpack-stream');
const webpack = require('webpack');
const named = require('vinyl-named');
const rev = require('gulp-rev');
const revRewrite = require('gulp-rev-rewrite');
const gulpsizereport = require('gulp-sizereport');

// 环境配置
const isProduction = process.env.NODE_ENV === 'production';

// Webpack配置
const webpackConfig = {
    mode: isProduction ? 'production' : 'development',
    devtool: isProduction ? 'source-map' : 'eval-source-map',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        plugins: ['@babel/plugin-transform-runtime']
                    }
                }
            }
        ]
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery'
        })
    ],
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    }
};

// 增量构建
function changed(done) {
    return $.changed('dist', { hasChanged: $.changed.compareSha1Digest });
}

// 错误处理
function handleError(error) {
    $.util.log($.util.colors.red('Error: ' + error.message));
    if (isProduction) {
        process.exit(1);
    }
    this.emit('end');
}

// 处理JavaScript
function scripts() {
    return gulp.src(['src/js/**/*.js', '!src/js/**/*.min.js'])
        .pipe(plumber({ errorHandler: handleError }))
        .pipe($.if(!isProduction, $.sourcemaps.init()))
        .pipe(named())
        .pipe(webpackStream(webpackConfig, webpack))
        .pipe($.if(isProduction, $.uglify({
            compress: { drop_console: true }
        })))
        .pipe($.if(!isProduction, $.sourcemaps.write('.')))
        .pipe($.if(isProduction, rev()))
        .pipe(dest('dist/js'))
        .pipe($.if(isProduction, rev.manifest()))
        .pipe(dest('dist/js'));
}

// 处理CSS
function styles() {
    const postcssPlugins = [
        require('autoprefixer')(),
        require('cssnano')()
    ];
    
    return gulp.src('src/scss/**/*.scss')
        .pipe(plumber({ errorHandler: handleError }))
        .pipe($.if(!isProduction, $.sourcemaps.init()))
        .pipe($.sass.sync({ outputStyle: 'expanded' }).on('error', $.sass.logError))
        .pipe($.postcss(postcssPlugins))
        .pipe($.if(isProduction, $.cleanCss()))
        .pipe($.if(isProduction, rev()))
        .pipe(dest('dist/css'))
        .pipe($.if(isProduction, rev.manifest()))
        .pipe(dest('dist/css'));
}

// 版本控制
function revision() {
    const manifest = gulp.src('dist/**/*.json');
    
    return gulp.src(['dist/**/*.html'])
        .pipe($.revRewrite({ manifest }))
        .pipe(gulp.dest('dist'));
}

// 文件大小报告
function sizeReport() {
    return gulp.src('dist/**/*')
        .pipe($.sizereport({
            gzip: true,
            total: true,
            title: 'Build Size Report'
        }));
}

// 压缩检查
function gzip() {
    return gulp.src('dist/**/*.{js,css,html}')
        .pipe($.gzip({ append: true }))
        .pipe(gulp.dest('dist'));
}

// 完整的生产构建
const production = series(
    clean,
    parallel(html, styles, scripts, images),
    revision,
    sizeReport,
    gzip
);

// 导出任务
exports.default = dev;
exports.build = build;
exports.production = production;

Vite现代化构建

Vite基础配置

// vite.config.js - Vite构建配置文件

// ===== 导入Vite相关模块 =====
import { defineConfig } from 'vite';        // Vite配置工具函数
import vue from '@vitejs/plugin-vue';         // Vue单文件组件支持
import { resolve } from 'path';               // Node.js路径解析工具

export default defineConfig({
    // ===== 插件配置 =====
    // Vite使用插件系统来扩展功能
    plugins: [
        vue(),  // 启用Vue单文件组件(.vue文件)支持
    ],
    
    // ===== 路径别名配置 =====
    // 设置模块导入的别名,简化相对路径引用
    resolve: {
        alias: {
            // '@' 指向src目录,便于文件引用
            '@': resolve(__dirname, 'src'),
            
            // 各个子模块的别名,提高代码可读性
            '@components': resolve(__dirname, 'src/components'),
            '@utils': resolve(__dirname, 'src/utils'),
            '@assets': resolve(__dirname, 'src/assets')
        }
    },
    
    // ===== 开发服务器配置 =====
    // 配置Vite的开发服务器行为
    server: {
        host: '0.0.0.0',           // 监听所有网络接口,便于局域网访问
        port: 3000,                // 开发服务器端口号
        open: true,                 // 启动时自动打开浏览器
        cors: true,                 // 启用CORS,允许跨域请求
        
        // ===== 代理配置 =====
        // 将特定路径的请求代理到后端服务器,解决开发环境跨域问题
        proxy: {
            // API请求代理
            '/api': {
                target: 'http://localhost:8080',  // 后端API服务器地址
                changeOrigin: true,                 // 改变请求头的Origin字段
                rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,移除/api前缀
            },
            
            // 文件上传代理
            '/upload': {
                target: 'http://localhost:8080',  // 文件上传服务器地址
                changeOrigin: true                  // 改变请求头Origin
            }
        }
    },
    
    // ===== 构建配置 =====
    // 生产环境打包的相关配置
    build: {
        target: 'es2015',           // 构建目标:支持ES2015语法的浏览器
        outDir: 'dist',             // 输出目录
        assetsDir: 'assets',         // 静态资源输出目录(相对于outDir)
        sourcemap: false,           // 是否生成源码映射文件,生产环境通常关闭
        
        // ===== 压缩配置 =====
        minify: 'terser',           // 使用Terser进行代码压缩(也可以用'esbuild')
        
        // ===== Terser压缩选项 =====
        terserOptions: {
            compress: {
                drop_console: true,     // 移除所有console语句
                drop_debugger: true     // 移除所有debugger语句
            }
        },
        
        // ===== 代码分割配置 =====
        // 使用Rollup的manualChunks进行手动代码分割
        rollupOptions: {
            output: {
                // 手动分割第三方库,优化缓存策略
                manualChunks: {
                    // Vue核心库单独分包
                    'vendor': ['vue', 'vue-router', 'vuex'],
                    
                    // UI组件库单独分包
                    'ui': ['element-plus'],
                    
                    // 工具库单独分包
                    'utils': ['lodash-es', 'axios']
                }
            }
        },
        
        // ===== 构建优化配置 =====
        chunkSizeWarningLimit: 1000,     // Chunk大小警告阈值(KB)
        assetsInlineLimit: 4096          // 小于4KB的资源转为base64内联
    },
    
    // ===== CSS配置 =====
    // CSS处理和预处理器配置
    css: {
        // ===== 预处理器选项 =====
        preprocessorOptions: {
            // SCSS全局变量注入
            scss: {
                // 在每个SCSS文件开头自动导入全局变量文件
                // 避免每个文件都要手动@import
                additionalData: `@import "@/styles/variables.scss";`
            }
        },
        
        // ===== CSS模块配置 =====
        modules: {
            // CSS类名转换规则:使用驼峰命名
            localsConvention: 'camelCaseOnly'
        }
    },
    
    // ===== 环境变量定义 =====
    // 在构建时定义全局常量,可在代码中直接使用
    define: {
        // 应用版本号,从package.json中读取
        __APP_VERSION__: JSON.stringify(process.env.npm_package_version)
    },
    
    // ===== 依赖优化配置 =====
    // Vite在开发时会预构建依赖,优化启动性能
    optimizeDeps: {
        // ===== 强制预构建的依赖 =====
        // 这些依赖会在开发时被预构建,避免按需转换
        include: [
            'vue',                   // Vue核心
            'vue-router',            // 路由库
            'vuex',                  // 状态管理库
            'axios',                 // HTTP客户端
            'lodash-es'              // 工具库
        ],
        
        // ===== 排除预构建的依赖 =====
        // 这些依赖不会被预构建,按需加载
        exclude: ['@babel/polyfill']   // Babel polyfill通常需要按需加载
    }
});

Vite环境配置

// ===== 开发环境变量文件 =====
// .env.development - 开发环境专用配置
VITE_APP_TITLE=My App Development      // 应用标题(开发环境)
VITE_API_BASE_URL=http://localhost:8080/api  // API基础URL(开发环境)
VITE_APP_ENV=development              // 环境标识

// ===== 生产环境变量文件 =====
// .env.production - 生产环境专用配置
VITE_APP_TITLE=My App               // 应用标题(生产环境)
VITE_API_BASE_URL=https://api.example.com  // API基础URL(生产环境)
VITE_APP_ENV=production             // 环境标识

// ===== 预发布环境变量文件 =====
// .env.staging - 预发布环境专用配置
VITE_APP_TITLE=My App Staging        // 应用标题(预发布环境)
VITE_API_BASE_URL=https://staging-api.example.com  // API基础URL(预发布环境)
VITE_APP_ENV=staging                // 环境标识

// ===== 在Vite配置中使用环境变量 =====
// vite.config.js - 根据不同环境动态配置
export default defineConfig(({ mode }) => {
    // 根据命令行参数判断当前环境
    const isProduction = mode === 'production';
    const isStaging = mode === 'staging';
    
    return {
        // ===== 环境变量定义 =====
        // 将环境变量注入到代码中,可在构建时使用
        define: {
            // 是否为生产环境标识
            __IS_PROD__: isProduction,
            
            // 环境标识字符串
            __APP_ENV__: JSON.stringify(process.env.VITE_APP_ENV)
        },
        
        // ===== 根据环境配置构建选项 =====
        build: {
            // 生产环境启用压缩,开发环境不压缩便于调试
            minify: isProduction ? 'terser' : false,
            
            // 生产环境关闭源码映射,开发环境启用内联源码映射
            sourcemap: isProduction ? false : 'inline'
        },
        
        // ===== 根据环境配置服务器 =====
        server: {
            // 不同环境使用不同的代理配置
            proxy: {
                '/api': {
                    // 根据环境选择不同的API地址
                    target: isProduction 
                        ? 'https://api.example.com' 
                        : isStaging 
                            ? 'https://staging-api.example.com' 
                            : 'http://localhost:8080',
                    changeOrigin: true,
                    rewrite: (path) => path.replace(/^\/api/, '')
                }
            }
        }
    };
});

Vite插件开发

// vite-plugin-custom.js - 自定义Vite插件示例

// ===== 插件主函数 =====
export function customPlugin(options = {}) {
    return {
        // ===== 插件名称 =====
        // 用于在日志和调试中识别插件
        name: 'vite-plugin-custom',
        
        // ===== 配置钩子 =====
        // 在Vite配置解析之前被调用,可以修改配置
        config(config, { command }) {
            // command: 'serve' | 'build',表示当前是开发还是构建模式
            
            // 如果是开发模式,添加开发服务器配置
            if (command === 'serve') {
                config.server = {
                    ...config.server,
                    ...options.devServer     // 合并插件传入的服务器配置
                };
            }
            
            return config; // 返回修改后的配置
        },
        
        // ===== 配置解析完成钩子 =====
        // 在Vite配置解析完成后被调用
        configResolved(config) {
            console.log('Vite config resolved:', config);
            // 这里可以验证配置或进行额外的初始化工作
        },
        
        // ===== 开发服务器配置钩子 =====
        // 在开发服务器创建后被调用,可以添加自定义中间件
        configureServer(server) {
            // 添加自定义API路由
            server.middlewares.use('/api/custom', (req, res, next) => {
                // 自定义API响应
                res.setHeader('Content-Type', 'application/json');
                res.end(JSON.stringify({
                    message: 'Custom middleware response',
                    timestamp: Date.now()
                }));
            });
        },
        
        // ===== 代码转换钩子 =====
        // 对模块的源码进行转换
        transform(code, id) {
            // code: 文件内容
            // id: 文件路径(含查询参数)
            
            // 只处理.custom.js文件
            if (id.endsWith('.custom.js')) {
                // 替换代码中的占位符
                const transformedCode = code.replace(
                    /__VERSION__/g, 
                    options.version || '1.0.0'
                );
                
                return {
                    code: transformedCode,     // 转换后的代码
                    map: null                 // 源码映射(这里不需要)
                };
            }
            
            // 返回null表示不处理此文件
            return null;
        },
        
        // ===== 构建开始钩子 =====
        // 在构建过程开始时被调用
        buildStart() {
            console.log('🚀 Build started...');
            
            // 可以在这里进行构建前的准备工作
            // 比如清理临时文件、检查依赖等
        },
        
        // ===== 构建结束钩子 =====
        // 在构建过程结束时被调用
        buildEnd() {
            console.log('✅ Build completed!');
            
            // 可以在这里进行构建后的清理工作
            // 比如发送构建通知、生成构建报告等
        },
        
        // ===== 生成钩子 =====
        // 在构建完成后,生成最终文件时被调用
        generateBundle(options, bundle) {
            // options: 生成选项
            // bundle: 包含所有生成文件的信息
            
            console.log(`Generated ${Object.keys(bundle).length} files`);
            
            // 可以在这里分析打包结果或生成额外文件
        }
    };
}

// ===== 使用自定义插件 =====
// vite.config.js
import { defineConfig } from 'vite';
import { customPlugin } from './vite-plugin-custom';

export default defineConfig({
    plugins: [
        // 使用自定义插件,传入配置选项
        customPlugin({
            version: '1.0.0',          // 插件版本号
            devServer: {                // 开发服务器配置
                port: 3001,            // 自定义端口
                open: false             // 不自动打开浏览器
            }
        })
    ]
});

工程化最佳实践

代码规范和质量控制

ESLint配置:

// .eslintrc.js - ESLint代码检查配置文件

module.exports = {
    // ===== 运行环境配置 =====
    // 指定代码运行的环境,ESLint会根据环境自动确定全局变量
    env: {
        browser: true,    // 浏览器环境:支持window、document等全局变量
        es2021: true,     // ES2021语法环境:支持Promise、Set等
        node: true        // Node.js环境:支持require、process等全局变量
    },
    
    // ===== 继承的规则集 =====
    // 从已有的规则集继承,避免重复配置
    extends: [
        'eslint:recommended',           // ESLint推荐规则
        '@vue/standard',               // Vue.js标准规则
        '@vue/typescript/recommended',  // Vue + TypeScript推荐规则
        'prettier'                    // Prettier规则集成,避免格式冲突
    ],
    
    // ===== 解析器选项 =====
    parserOptions: {
        ecmaVersion: 'latest',     // 支持最新的ECMAScript语法
        sourceType: 'module'        // 使用ES模块语法(import/export)
    },
    
    // ===== 自定义规则 =====
    rules: {
        // ===== 调试语句规则 =====
        // 根据环境严格程度控制调试语句
        'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
        
        // ===== 代码质量规则 =====
        'no-unused-vars': 'warn',     // 未使用的变量警告(不报错,允许临时调试)
        'prefer-const': 'error',       // 优先使用const而不是let
        'no-var': 'error',             // 禁止使用var,强制使用let/const
        
        // ===== 代码风格规则 =====
        'object-shorthand': 'error',    // 对象方法使用简写语法
        'prefer-arrow-callback': 'error' // 回调函数优先使用箭头函数
    },
    
    // ===== 文件覆盖规则 =====
    // 针对特定文件类型使用不同的规则配置
    overrides: [
        {
            // Vue单文件组件的特殊规则
            files: ['*.vue'],
            rules: {
                // Vue属性每行数量限制
                'vue/max-attributes-per-line': ['error', {
                    singleline: 3,    // 单行最多3个属性
                    multiline: 1      // 多行每行1个属性
                }]
            }
        }
    ]
};

Prettier配置:

// .prettierrc - Prettier代码格式化配置文件

{
    // ===== 分号配置 =====
    // false: 不使用分号
    // true: 使用分号
    "semi": false,
    
    // ===== 引号配置 =====
    // true: 优先使用单引号
    // false: 优先使用双引号
    "singleQuote": true,
    
    // ===== 缩进配置 =====
    "tabWidth": 2,        // 缩进空格数
    "useTabs": false,      // 使用空格而不是Tab进行缩进
    
    // ===== 行宽配置 =====
    "printWidth": 100,     // 每行最大字符数
    
    // ===== 括号配置 =====
    "bracketSpacing": true, // 对象字面量括号内添加空格
    "bracketSameLine": false, // JSX标签的>不与属性在同一行
    
    // ===== 箭头函数配置 =====
    // "avoid": 单参数时不加括号 (x) => {} 改为 x => {}
    // "always": 总是加括号
    "arrowParens": "avoid",
    
    // ===== 行尾配置 =====
    "endOfLine": "lf",   // 使用LF换行符(Unix风格)
    
    // ===== 尾随逗号配置 =====
    // "es5": ES5支持的对象/数组最后元素加逗号
    // "none": 不加尾随逗号
    // "all": 所有地方都加尾随逗号
    "trailingComma": "es5"
}

Husky和lint-staged:

// package.json - Git钩子和代码质量检查配置

{
    "scripts": {
        // ===== 代码检查脚本 =====
        "lint": "eslint src --ext .js,.vue,.ts",
        
        // ===== 代码修复脚本 =====
        "lint:fix": "eslint src --ext .js,.vue,.ts --fix",
        
        // ===== 代码格式化脚本 =====
        "format": "prettier --write src/**/*.{js,vue,ts,scss}",
        
        // ===== Pre-commit钩子脚本 =====
        "pre-commit": "lint-staged"
    },
    
    // ===== Git钩子配置 =====
    "husky": {
        "hooks": {
            // 提交前执行代码检查
            "pre-commit": "lint-staged",
            
            // 提交信息格式检查
            "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
        }
    },
    
    // ===== 暂存文件处理配置 =====
    // 只对Git暂存区的文件执行检查,提高性能
    "lint-staged": {
        // JavaScript/Vue/TypeScript文件处理
        "*.{js,vue,ts}": [
            "eslint --fix",        // 自动修复ESLint错误
            "prettier --write",    // 格式化代码
            "git add"            // 将修复后的文件重新添加到暂存区
        ],
        
        // 样式文件处理
        "*.{scss,css}": [
            "prettier --write",    // 格式化样式
            "git add"            // 重新添加到暂存区
        ]
    }
}

Git工作流规范

# .github/workflows/ci.yml - GitHub Actions CI/CD配置文件

# ===== 流水线名称 =====
name: CI/CD Pipeline

# ===== 触发条件 =====
# 定义什么时候触发此工作流
on:
  push:
    # 推送到以下分支时触发
    branches: [main, develop]
  pull_request:
    # 创建针对以下分支的PR时触发
    branches: [main]

# ===== 工作定义 =====
jobs:
  # ===== 测试任务 =====
  test:
    runs-on: ubuntu-latest  # 使用Ubuntu最新版作为运行环境
    
    # ===== 矩阵策略 =====
    # 在多个Node.js版本上并行测试
    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x]  # 测试多个Node.js版本
    
    # ===== 执行步骤 =====
    steps:
      # 步骤1:检出代码
      - uses: actions/checkout@v3
      
      # 步骤2:设置Node.js环境
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'  # 启用npm缓存加速
      
      # 步骤3:安装依赖
      - name: Install dependencies
        run: npm ci  # 使用npm ci进行快速、可靠的安装
      
      # 步骤4:运行代码检查
      - name: Run lint
        run: npm run lint
      
      # 步骤5:运行测试并生成覆盖率报告
      - name: Run tests
        run: npm run test:coverage
      
      # 步骤6:上传覆盖率报告
      - name: Upload coverage
        uses: codecov/codecov-action@v3
      
      # 步骤7:构建项目
      - name: Build
        run: npm run build
      
      # 步骤8:上传构建产物
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ matrix.node-version }}
          path: dist/

  # ===== 部署任务 =====
  deploy:
    # 依赖测试任务完成
    needs: test
    runs-on: ubuntu-latest
    
    # ===== 条件部署 =====
    # 只有推送到main分支才执行部署
    if: github.ref == 'refs/heads/main'
    
    steps:
      # 步骤1:检出代码
      - uses: actions/checkout@v3
      
      # 步骤2:部署到生产环境
      - name: Deploy to production
        run: |
          echo "Deploying to production..."
          # 这里可以是实际的部署脚本
          # 例如:上传到CDN、部署到服务器等
          # docker build && docker push
          # kubectl apply -f deployment.yaml

CI/CD与部署自动化

Docker化部署

# Dockerfile
# 构建阶段
FROM node:18-alpine as build-stage

WORKDIR /app

# 复制package文件
COPY package*.json ./
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产阶段
FROM nginx:alpine as production-stage

# 复制构建产物
COPY --from=build-stage /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    
    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json;
    
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;
        
        # 启用缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
        
        # SPA路由
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # API代理
        location /api/ {
            proxy_pass http://backend:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Kubernetes部署

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-app
  labels:
    app: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: my-frontend-app:latest
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        env:
        - name: API_URL
          value: "https://api.example.com"
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80

实战项目搭建

脚手架工具开发

// create-vue-app.js
#!/usr/bin/env node

const { Command } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');

const program = new Command();

program
    .name('create-vue-app')
    .description('Create a new Vue.js project')
    .version('1.0.0')
    .argument('[project-name]', 'Project name')
    .action(async (projectName) => {
        if (!projectName) {
            const { name } = await inquirer.prompt([
                {
                    type: 'input',
                    name: 'name',
                    message: 'Project name:',
                    validate: input => input.trim() ? true : 'Project name is required'
                }
            ]);
            projectName = name;
        }
        
        const targetDir = path.resolve(process.cwd(), projectName);
        
        if (fs.existsSync(targetDir)) {
            const { overwrite } = await inquirer.prompt([
                {
                    type: 'confirm',
                    name: 'overwrite',
                    message: 'Directory already exists. Overwrite?',
                    default: false
                }
            ]);
            
            if (!overwrite) {
                console.log(chalk.yellow('Project creation cancelled.'));
                process.exit(0);
            }
            
            await fs.remove(targetDir);
        }
        
        const answers = await inquirer.prompt([
            {
                type: 'list',
                name: 'preset',
                message: 'Please pick a preset:',
                choices: [
                    {
                        name: 'Default (Vue 3 + Vite)',
                        value: 'default'
                    },
                    {
                        name: 'Default Plus (Vue 3 + Vite + TypeScript + Router + Pinia)',
                        value: 'default-plus'
                    },
                    {
                        name: 'Manually select features',
                        value: 'manual'
                    }
                ]
            }
        ]);
        
        if (answers.preset === 'manual') {
            const manualAnswers = await inquirer.prompt([
                {
                    type: 'checkbox',
                    name: 'features',
                    message: 'Check the features needed for your project:',
                    choices: [
                        { name: 'TypeScript', value: 'typescript' },
                        { name: 'Router', value: 'router' },
                        { name: 'Pinia (状态管理)', value: 'pinia' },
                        { name: 'ESLint', value: 'eslint' },
                        { name: 'Prettier', value: 'prettier' },
                        { name: 'Unit Testing', value: 'testing' },
                        { name: 'E2E Testing', value: 'e2e' }
                    ]
                }
            ]);
            
            answers.features = manualAnswers.features;
        }
        
        console.log(chalk.blue('\nCreating project...'));
        
        await createProject(projectName, targetDir, answers);
        
        console.log(chalk.green('\n✨ Project created successfully!'));
        console.log(chalk.cyan('\nNext steps:'));
        console.log(chalk.white(`  cd ${projectName}`));
        console.log(chalk.white('  npm install'));
        console.log(chalk.white('  npm run dev'));
    });

async function createProject(name, targetDir, answers) {
    // 创建项目目录
    await fs.ensureDir(targetDir);
    
    // 生成package.json
    const packageJson = generatePackageJson(name, answers);
    await fs.writeJSON(path.join(targetDir, 'package.json'), packageJson, { spaces: 2 });
    
    // 生成配置文件
    await generateConfigFiles(targetDir, answers);
    
    // 生成源代码
    await generateSourceFiles(targetDir, answers);
    
    // 安装依赖
    console.log(chalk.blue('Installing dependencies...'));
    await installDependencies(targetDir, answers);
}

function generatePackageJson(name, answers) {
    const base = {
        name,
        version: '0.0.1',
        private: true,
        scripts: {
            dev: 'vite',
            build: 'vite build',
            preview: 'vite preview'
        }
    };
    
    const dependencies = [];
    const devDependencies = ['vite'];
    
    if (answers.preset === 'default' || answers.preset === 'default-plus' || answers.features?.includes('typescript')) {
        devDependencies.push('@vitejs/plugin-vue');
        dependencies.push('vue');
    }
    
    if (answers.preset === 'default-plus' || answers.features?.includes('router')) {
        dependencies.push('vue-router');
    }
    
    if (answers.preset === 'default-plus' || answers.features?.includes('pinia')) {
        dependencies.push('pinia');
    }
    
    if (answers.features?.includes('typescript')) {
        devDependencies.push('typescript', 'vue-tsc');
        base.scripts.typecheck = 'vue-tsc --noEmit';
    }
    
    return {
        ...base,
        dependencies: dependencies.length ? dependencies : undefined,
        devDependencies
    };
}

async function installDependencies(targetDir, answers) {
    return new Promise((resolve, reject) => {
        const child = spawn('npm', ['install'], {
            cwd: targetDir,
            stdio: 'inherit'
        });
        
        child.on('close', (code) => {
            if (code !== 0) {
                reject(new Error('npm install failed'));
            } else {
                resolve();
            }
        });
    });
}

program.parse();

项目结构模板

my-project/
├── public/                 # 静态资源
│   ├── favicon.ico
│   └── index.html
├── src/                   # 源代码
│   ├── api/              # API接口
│   ├── assets/           # 静态资源
│   ├── components/       # 组件
│   │   ├── common/       # 公共组件
│   │   └── business/     # 业务组件
│   ├── composables/      # 组合式函数
│   ├── layouts/          # 布局组件
│   ├── pages/            # 页面
│   ├── router/           # 路由配置
│   ├── stores/           # 状态管理
│   ├── styles/           # 样式文件
│   ├── utils/            # 工具函数
│   ├── types/            # TypeScript类型
│   ├── App.vue           # 根组件
│   └── main.ts           # 入口文件
├── tests/                # 测试文件
│   ├── unit/             # 单元测试
│   └── e2e/              # E2E测试
├── .env                  # 环境变量
├── .env.development
├── .env.production
├── .eslintrc.js          # ESLint配置
├── .prettierrc           # Prettier配置
├── package.json
├── tsconfig.json         # TypeScript配置
├── vite.config.ts        # Vite配置
└── README.md

性能优化与监控

性能监控配置

// 性能监控工具
class PerformanceMonitor {
    constructor() {
        this.metrics = {
            navigation: {},
            resources: [],
            paint: {},
            memory: {}
        };
        
        this.init();
    }
    
    init() {
        this.observeNavigation();
        this.observeResources();
        this.observePaint();
        this.observeMemory();
        this.observeLayoutShift();
    }
    
    observeNavigation() {
        if (performance.timing) {
            const timing = performance.timing;
            this.metrics.navigation = {
                dns: timing.domainLookupEnd - timing.domainLookupStart,
                tcp: timing.connectEnd - timing.connectStart,
                request: timing.responseStart - timing.requestStart,
                response: timing.responseEnd - timing.responseStart,
                dom: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart,
                load: timing.loadEventEnd - timing.loadEventStart,
                total: timing.loadEventEnd - timing.navigationStart
            };
        }
    }
    
    observeResources() {
        const resources = performance.getEntriesByType('resource');
        this.metrics.resources = resources.map(resource => ({
            name: resource.name,
            type: this.getResourceType(resource.name),
            duration: resource.duration,
            size: resource.transferSize || 0
        }));
    }
    
    observePaint() {
        const paintEntries = performance.getEntriesByType('paint');
        paintEntries.forEach(entry => {
            this.metrics.paint[entry.name] = entry.startTime;
        });
    }
    
    observeMemory() {
        if (performance.memory) {
            this.metrics.memory = {
                used: Math.round(performance.memory.usedJSHeapSize / 1048576),
                total: Math.round(performance.memory.totalJSHeapSize / 1048576),
                limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576)
            };
        }
    }
    
    observeLayoutShift() {
        let cumulativeLayoutShift = 0;
        
        const observer = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                if (!entry.hadRecentInput) {
                    cumulativeLayoutShift += entry.value;
                }
            }
            
            this.metrics.cls = cumulativeLayoutShift;
        });
        
        observer.observe({ entryTypes: ['layout-shift'] });
    }
    
    getResourceType(url) {
        if (url.includes('.css')) return 'css';
        if (url.includes('.js')) return 'javascript';
        if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image';
        if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font';
        return 'other';
    }
    
    getReport() {
        return {
            ...this.metrics,
            timestamp: Date.now(),
            userAgent: navigator.userAgent,
            url: window.location.href
        };
    }
    
    sendReport() {
        const report = this.getReport();
        
        // 发送到监控系统
        fetch('/api/performance', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(report)
        }).catch(error => {
            console.error('Failed to send performance report:', error);
        });
    }
}

// 初始化监控
const monitor = new PerformanceMonitor();

// 页面加载完成后发送报告
window.addEventListener('load', () => {
    setTimeout(() => {
        monitor.sendReport();
    }, 1000);
});

Bundle分析工具

// webpack-bundle-analyzer集成
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin({
            analyzerMode: 'static',
            openAnalyzer: false,
            reportFilename: 'bundle-report.html',
            defaultSizes: 'parsed',
            generateStatsFile: true,
            statsFilename: 'bundle-stats.json',
            statsOptions: {
                source: false,
                modules: true,
                chunks: true,
                chunkModules: true
            }
        })
    ]
};

🚀 落地实战项目案例

微前端架构实践

// 主应用配置 - qiankun
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vue-app',
    entry: '//localhost:8081',
    container: '#vue-app',
    activeRule: '/vue',
    props: { data: 'main-app-data' }
  },
  {
    name: 'react-app',
    entry: '//localhost:8082',
    container: '#react-app',
    activeRule: '/react'
  }
]);

start({
  sandbox: {
    experimentalStyleIsolation: true,
    strictStyleIsolation: false
  },
  prefetch: true
});

// 微应用配置
// vue-app/src/main.js
import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue-app] bootstrap');
}

export async function mount(props) {
  console.log('[vue-app] mount', props);
  render(props);
}

export async function unmount() {
  console.log('[vue-app] unmount');
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

Monorepo工程架构

// package.json (根目录)
{
  "name": "frontend-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^1.6.3",
    "@changesets/cli": "^2.24.4"
  }
}

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", "build/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    }
  }
}
frontend-monorepo/
├── packages/
│   ├── ui/              # UI组件库
│   ├── utils/           # 工具库
│   ├── hooks/           # 自定义hooks
│   └── types/           # 类型定义
├── apps/
│   ├── admin/           # 管理后台
│   ├── mobile/          # 移动端应用
│   └── docs/            # 文档站点
├── tools/
│   ├── build/           # 构建工具
│   └── scripts/         # 脚本工具
└── shared/              # 共享配置
    ├── eslint-config/
    ├── tsconfig/
    └── webpack-config/

组件库工程化

// packages/ui/build.js
const { build } = require('esbuild');
const { glob } = require('glob');
const { resolve } = require('path');

async function buildLibrary() {
  // 入口文件
  const entryPoints = await glob('./src/**/*.{js,ts,jsx,tsx}');
  
  // 构建ESM
  await build({
    entryPoints,
    bundle: false,
    outdir: 'dist/es',
    format: 'esm',
    target: ['es2018'],
    sourcemap: true,
    tsconfig: './tsconfig.json'
  });
  
  // 构建CJS
  await build({
    entryPoints,
    bundle: false,
    outdir: 'dist/lib',
    format: 'cjs',
    target: ['es2018'],
    sourcemap: true,
    tsconfig: './tsconfig.json'
  });
  
  // 生成类型声明
  await build({
    entryPoints: './src/index.ts',
    bundle: false,
    outdir: 'dist/types',
    format: 'esm',
    target: ['es2018'],
    tsconfig: './tsconfig.json',
    jsxFactory: 'React.createElement',
    jsxFragment: 'React.Fragment'
  });
}

buildLibrary();

// packages/ui/.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-docs'
  ],
  framework: {
    name: '@storybook/react-webpack5',
    options: {}
  },
  webpackFinal: async (config) => {
    // 自定义webpack配置
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': resolve(__dirname, '../src')
    };
    
    return config;
  }
};

低代码平台工程化

// 低代码引擎核心
class LowCodeEngine {
  constructor() {
    this.components = new Map();
    this.schemas = new Map();
    this.plugins = [];
  }
  
  // 注册组件
  registerComponent(name, component) {
    this.components.set(name, component);
  }
  
  // 注册插件
  registerPlugin(plugin) {
    this.plugins.push(plugin);
    plugin.init(this);
  }
  
  // 解析schema并渲染
  render(schema) {
    const Component = this.components.get(schema.type);
    if (!Component) {
      throw new Error(`Component ${schema.type} not found`);
    }
    
    const props = {
      ...schema.props,
      children: schema.children?.map(child => this.render(child))
    };
    
    return React.createElement(Component, props, props.children);
  }
  
  // 页面构建器
  buildPage(schema) {
    return this.render(schema);
  }
}

// 拖拽系统
class DragDropSystem {
  constructor(container) {
    this.container = container;
    this.draggedElement = null;
    this.dropZones = [];
    this.init();
  }
  
  init() {
    this.container.addEventListener('dragstart', this.handleDragStart.bind(this));
    this.container.addEventListener('dragover', this.handleDragOver.bind(this));
    this.container.addEventListener('drop', this.handleDrop.bind(this));
  }
  
  handleDragStart(e) {
    this.draggedElement = e.target;
    e.dataTransfer.effectAllowed = 'move';
  }
  
  handleDragOver(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  }
  
  handleDrop(e) {
    e.preventDefault();
    if (this.draggedElement && e.target.classList.contains('drop-zone')) {
      e.target.appendChild(this.draggedElement);
      this.onDrop(this.draggedElement, e.target);
    }
  }
}

📊 企业级监控体系

错误监控系统

// 错误监控SDK - 前端错误采集和上报系统

class ErrorMonitor {
  constructor(config) {
    // ===== 配置初始化 =====
    // 合并用户配置和默认配置
    this.config = {
      apiUrl: '/api/errors',           // 错误上报API地址
      maxErrors: 50,                 // 本地最大错误缓存数
      sampleRate: 1,                  // 采样率(1=100%,0.1=10%)
      ...config                       // 合并用户自定义配置
    };
    
    this.errors = [];                 // 本地错误缓存数组
    this.init();                      // 初始化监控
  }
  
  // ===== 初始化方法 =====
  // 启动所有错误监听机制
  init() {
    this.captureGlobalErrors();        // 监听全局JavaScript错误
    this.captureUnhandledRejections(); // 监听未处理的Promise rejection
    this.captureResourceErrors();     // 监听资源加载错误
    this.startReporting();            // 启动定时上报机制
  }
  
  // ===== 全局JavaScript错误监听 =====
  // 捕获所有未处理的JavaScript运行时错误
  captureGlobalErrors() {
    window.addEventListener('error', (event) => {
      // 构建错误对象,包含详细的错误信息
      this.captureError({
        type: 'javascript',           // 错误类型:JavaScript运行时错误
        message: event.message,        // 错误消息
        filename: event.filename,      // 出错的文件名
        lineno: event.lineno,          // 错误行号
        colno: event.colno,           // 错误列号
        stack: event.error?.stack     // 错误堆栈信息
      });
    });
  }
  
  // ===== Promise rejection监听 =====
  // 捕获所有未处理的Promise rejection
  captureUnhandledRejections() {
    window.addEventListener('unhandledrejection', (event) => {
      this.captureError({
        type: 'promise',                           // 错误类型:Promise错误
        message: event.reason?.message || 'Unhandled Promise Rejection', // 错误消息
        stack: event.reason?.stack                  // 错误堆栈
      });
    });
  }
  
  // ===== 资源加载错误监听 =====
  // 捕获图片、脚本、样式等资源加载失败
  captureResourceErrors() {
    // 使用捕获阶段监听,能获取到资源加载错误
    window.addEventListener('error', (event) => {
      // event.target !== window 确保是资源错误,不是JS错误
      if (event.target !== window) {
        this.captureError({
          type: 'resource',                                      // 错误类型:资源加载错误
          message: `Failed to load ${event.target.tagName}`,         // 错误描述
          resource: event.target.src || event.target.href,          // 失败的资源URL
          type: event.target.tagName.toLowerCase()                   // 资源类型(img, script等)
        });
      }
    }, true);  // true表示使用捕获阶段
  }
  
  // ===== 错误捕获核心方法 =====
  // 统一处理所有类型的错误
  captureError(error) {
    // ===== 采样控制 =====
    // 根据采样率决定是否处理此错误,减少上报量
    if (Math.random() > this.config.sampleRate) {
      return;
    }
    
    // ===== 错误信息增强 =====
    // 添加环境信息和上下文信息
    const enrichedError = {
      ...error,                           // 原始错误信息
      timestamp: Date.now(),               // 错误发生时间戳
      url: window.location.href,           // 当前页面URL
      userAgent: navigator.userAgent,       // 浏览器用户代理
      sessionId: this.getSessionId(),      // 会话ID(用于关联同一用户的错误)
      userId: this.getUserId()            // 用户ID(需要用户自己实现)
    };
    
    // ===== 错误缓存 =====
    // 将错误添加到本地缓存
    this.errors.push(enrichedError);
    
    // ===== 缓存大小控制 =====
    // 如果缓存超过最大值,移除最旧的错误
    if (this.errors.length > this.config.maxErrors) {
      this.errors.shift();
    }
  }
  
  // ===== 错误上报方法 =====
  // 将缓存中的错误批量发送到服务器
  async reportErrors() {
    // 如果没有错误,直接返回
    if (this.errors.length === 0) return;
    
    // 复制错误数组并清空缓存
    const errorsToSend = [...this.errors];
    this.errors = [];
    
    try {
      // 发送错误到监控服务器
      await fetch(this.config.apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          errors: errorsToSend,          // 错误数组
          timestamp: Date.now()          // 上报时间戳
        })
      });
    } catch (error) {
      // ===== 上报失败处理 =====
      // 将错误重新添加到缓存头部,下次重试
      console.error('Failed to report errors:', error);
      this.errors.unshift(...errorsToSend);
    }
  }
  
  // ===== 启动定时上报 =====
  startReporting() {
    // 定时上报:每30秒上报一次
    setInterval(() => this.reportErrors(), 30000);
    
    // 页面卸载时上报:确保用户离开页面前上报所有错误
    window.addEventListener('beforeunload', () => {
      this.reportErrors();
    });
  }
  
  // ===== 会话ID生成 =====
  // 生成唯一的会话标识符
  getSessionId() {
    // 从sessionStorage获取或生成新的
    if (!sessionStorage.getItem('sessionId')) {
      const sessionId = 'session_' + 
        Math.random().toString(36).substr(2, 9) + 
        Date.now();
      sessionStorage.setItem('sessionId', sessionId);
      return sessionId;
    }
    return sessionStorage.getItem('sessionId');
  }
  
  // ===== 用户ID获取 =====
  // 获取当前用户ID(需要根据具体业务实现)
  getUserId() {
    // 可以从cookie、localStorage、全局变量等获取
    return localStorage.getItem('userId') || 'anonymous';
  }
}

// ===== 初始化错误监控 =====
// 在应用入口处初始化监控系统
new ErrorMonitor({
  apiUrl: 'https://monitor.example.com/api/errors',  // 监控API地址
  sampleRate: 0.1                                    // 10%采样率,减少上报量
});

性能监控系统

// Web Vitals监控 - 核心Web性能指标监控

// 导入web-vitals库的核心指标函数
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

class PerformanceMonitor {
  constructor() {
    this.metrics = {};           // 存储性能指标
    this.init();                // 初始化监控
  }
  
  // ===== 初始化方法 =====
  init() {
    this.observeWebVitals();           // 监控核心Web性能指标
    this.observeUserInteractions();    // 监控用户交互性能
    this.observeAPIPerformance();     // 监控API请求性能
  }
  
  // ===== Web Vitals指标监控 =====
  // 监控Google推荐的核心Web性能指标
  observeWebVitals() {
    // CLS - Cumulative Layout Shift(累积布局偏移)
    // 衡量页面视觉稳定性,值越小越好(<0.1为良好)
    getCLS((metric) => this.recordMetric('CLS', metric));
    
    // FID - First Input Delay(首次输入延迟)
    // 衡量页面交互性,值越小越好(<100ms为良好)
    getFID((metric) => this.recordMetric('FID', metric));
    
    // FCP - First Contentful Paint(首次内容绘制)
    // 衡量页面加载速度,值越小越好(<1.8s为良好)
    getFCP((metric) => this.recordMetric('FCP', metric));
    
    // LCP - Largest Contentful Paint(最大内容绘制)
    // 衡量页面主要内容加载速度,值越小越好(<2.5s为良好)
    getLCP((metric) => this.recordMetric('LCP', metric));
    
    // TTFB - Time to First Byte(首字节时间)
    // 衡量服务器响应速度,值越小越好(<800ms为良好)
    getTTFB((metric) => this.recordMetric('TTFB', metric));
  }
  
  // ===== 用户交互性能监控 =====
  // 监控用户的点击、输入等交互的响应延迟
  observeUserInteractions() {
    // 监控点击响应延迟
    document.addEventListener('click', (event) => {
      const startTime = performance.now();  // 记录点击开始时间
      
      // 使用requestAnimationFrame确保在下一帧测量
      requestAnimationFrame(() => {
        const clickDelay = performance.now() - startTime;
        this.recordMetric('ClickDelay', { value: clickDelay });
      });
    });
    
    // 监控键盘输入响应延迟
    document.addEventListener('keydown', (event) => {
      const startTime = performance.now();
      
      requestAnimationFrame(() => {
        const keyDelay = performance.now() - startTime;
        this.recordMetric('KeyDelay', { value: keyDelay });
      });
    });
  }
  
  // ===== API性能监控 =====
  // 拦截fetch API,监控所有网络请求的性能
  observeAPIPerformance() {
    // 保存原始的fetch函数
    const originalFetch = window.fetch;
    
    // 重写fetch函数,添加性能监控
    window.fetch = async (...args) => {
      const startTime = performance.now();  // 记录请求开始时间
      
      try {
        // 执行原始fetch请求
        const response = await originalFetch(...args);
        const endTime = performance.now();   // 记录请求结束时间
        
        // 记录成功的API请求指标
        this.recordMetric('APIRequest', {
          url: args[0],                           // 请求URL
          method: args[1]?.method || 'GET',        // 请求方法
          status: response.status,                   // 响应状态码
          duration: endTime - startTime,            // 请求耗时
          size: response.headers.get('content-length') // 响应大小
        });
        
        return response;
      } catch (error) {
        const endTime = performance.now();
        
        // 记录失败的API请求指标
        this.recordMetric('APIError', {
          url: args[0],                           // 请求URL
          error: error.message,                    // 错误信息
          duration: endTime - startTime             // 请求耗时
        });
        
        // 继续抛出原始错误
        throw error;
      }
    };
  }
  
  // ===== 指标记录方法 =====
  // 统一记录各种性能指标
  recordMetric(name, metric) {
    // 存储指标数据
    this.metrics[name] = {
      ...metric,                   // 原始指标数据
      timestamp: Date.now(),       // 记录时间
      url: window.location.href    // 页面URL
    };
    
    // 立即发送指标到服务器
    this.sendMetric(name, metric);
  }
  
  // ===== 指标上报方法 =====
  // 将性能指标发送到监控服务器
  async sendMetric(name, metric) {
    try {
      await fetch('/api/metrics', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          name: name,                        // 指标名称
          metric: metric,                    // 指标数据
          url: window.location.href,          // 页面URL
          userAgent: navigator.userAgent,       // 浏览器信息
          timestamp: Date.now()               // 上报时间
        })
      });
    } catch (error) {
      console.error('Failed to send metric:', error);
      // 可以考虑添加重试逻辑或本地缓存
    }
  }
  
  // ===== 性能报告生成 =====
  // 生成性能报告摘要
  getPerformanceReport() {
    return {
      // 核心Web Vitals指标
      vitals: {
        cls: this.metrics.CLS?.value || 0,
        fid: this.metrics.FID?.value || 0,
        fcp: this.metrics.FCP?.value || 0,
        lcp: this.metrics.LCP?.value || 0,
        ttfb: this.metrics.TTFB?.value || 0
      },
      
      // 交互性能指标
      interactions: {
        clickDelay: this.metrics.ClickDelay?.value || 0,
        keyDelay: this.metrics.KeyDelay?.value || 0
      },
      
      // 网络请求性能
      network: {
        avgResponseTime: this.calculateAverageResponseTime(),
        errorRate: this.calculateErrorRate()
      },
      
      // 页面信息
      page: {
        url: window.location.href,
        timestamp: Date.now()
      }
    };
  }
  
  // ===== 计算平均响应时间 =====
  calculateAverageResponseTime() {
    const requests = Object.values(this.metrics)
      .filter(metric => metric.name === 'APIRequest');
    
    if (requests.length === 0) return 0;
    
    const totalTime = requests.reduce((sum, req) => sum + req.duration, 0);
    return totalTime / requests.length;
  }
  
  // ===== 计算错误率 =====
  calculateErrorRate() {
    const allRequests = Object.values(this.metrics)
      .filter(metric => metric.name.startsWith('API'));
    
    if (allRequests.length === 0) return 0;
    
    const errors = allRequests.filter(metric => metric.name === 'APIError');
    return (errors.length / allRequests.length) * 100;
  }
}

// ===== 初始化性能监控 =====
// 在应用入口处初始化性能监控系统
new PerformanceMonitor();

用户行为分析

// 用户行为追踪 - 用户交互行为数据采集系统

class UserBehaviorTracker {
  constructor() {
    this.events = [];                          // 本地事件缓存
    this.sessionId = this.generateSessionId();   // 会话唯一标识
    this.init();                               // 初始化追踪
  }
  
  // ===== 初始化方法 =====
  init() {
    this.trackPageViews();          // 页面访问追踪
    this.trackClicks();            // 点击行为追踪
    this.trackScrolls();           // 滚动行为追踪
    this.trackFormInteractions();   // 表单交互追踪
    this.startBatching();          // 启动批量上报
  }
  
  // ===== 页面访问追踪 =====
  trackPageViews() {
    // 页面首次加载时的访问记录
    this.track('page_view', {
      path: window.location.pathname,        // 页面路径
      referrer: document.referrer,          // 来源页面
      title: document.title,               // 页面标题
      timestamp: Date.now(),               // 访问时间
      userAgent: navigator.userAgent         // 浏览器信息
    });
    
    // SPA路由变化监听(监听浏览器历史记录变化)
    window.addEventListener('popstate', () => {
      this.track('page_view', {
        path: window.location.pathname,    // 新页面路径
        type: 'spa_navigation'            // 标记为SPA路由跳转
      });
    });
    
    // 监听pushState和replaceState(大多数SPA框架的路由方法)
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;
    
    history.pushState = function(...args) {
      originalPushState.apply(this, args);
      window.dispatchEvent(new Event('popstate'));
    };
    
    history.replaceState = function(...args) {
      originalReplaceState.apply(this, args);
      window.dispatchEvent(new Event('popstate'));
    };
  }
  
  // ===== 点击行为追踪 =====
  trackClicks() {
    document.addEventListener('click', (event) => {
      const target = event.target;
      
      // 构建点击事件数据
      this.track('click', {
        elementType: target.tagName.toLowerCase(),    // 元素类型
        elementClass: target.className,            // 元素类名
        elementId: target.id,                     // 元素ID
        text: target.textContent?.slice(0, 100),  // 元素文本(截取前100字符)
        attributes: this.getElementAttributes(target), // 元素属性
        coordinates: {                             // 点击坐标
          x: event.clientX,
          y: event.clientY
        },
        viewportSize: {                            // 视窗大小
          width: window.innerWidth,
          height: window.innerHeight
        }
      });
    });
  }
  
  // ===== 滚动行为追踪 =====
  trackScrolls() {
    let lastScrollDepth = 0;     // 记录上次滚动深度
    let scrollTimeout = null;      // 防抖定时器
    
    window.addEventListener('scroll', () => {
      // 防抖处理,避免频繁触发
      clearTimeout(scrollTimeout);
      
      scrollTimeout = setTimeout(() => {
        // 计算当前滚动深度百分比
        const scrollDepth = Math.round(
          (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
        );
        
        // 只有在滚动深度增加时才记录(避免重复记录同一深度)
        if (scrollDepth > lastScrollDepth) {
          this.track('scroll', {
            depth: scrollDepth,                          // 滚动深度百分比
            scrollPosition: window.scrollY,               // 滚动位置
            pageHeight: document.body.scrollHeight,        // 页面总高度
            viewportHeight: window.innerHeight,            // 视窗高度
            timestamp: Date.now()                       // 滚动时间
          });
          lastScrollDepth = scrollDepth;
        }
      }, 100); // 100ms防抖
    });
  }
  
  // ===== 表单交互追踪 =====
  trackFormInteractions() {
    // 表单元素变化追踪(input、select、textarea)
    document.addEventListener('change', (event) => {
      const target = event.target;
      
      if (target.tagName === 'INPUT' || target.tagName === 'SELECT' || target.tagName === 'TEXTAREA') {
        this.track('form_interaction', {
          fieldType: target.type,                       // 字段类型
          fieldName: target.name,                       // 字段名称
          fieldValue: this.sanitizeValue(target.value),    // 字段值(脱敏处理)
          elementId: target.id,                         // 元素ID
          formId: target.form?.id,                     // 表单ID
          timestamp: Date.now()                          // 交互时间
        });
      }
    });
    
    // 表单提交追踪
    document.addEventListener('submit', (event) => {
      const form = event.target;
      
      this.track('form_submit', {
        formId: form.id,                              // 表单ID
        formName: form.name,                            // 表单名称
        formAction: form.action,                        // 表单提交地址
        formMethod: form.method,                        // 提交方法
        fieldCount: form.elements.length,                // 字段数量
        timestamp: Date.now()                           // 提交时间
      });
    });
    
    // 表单聚焦/失焦追踪(可选)
    document.addEventListener('focus', (event) => {
      if (['INPUT', 'SELECT', 'TEXTAREA'].includes(event.target.tagName)) {
        this.track('form_focus', {
          fieldName: event.target.name,
          fieldType: event.target.type,
          timestamp: Date.now()
        });
      }
    }, true);
  }
  
  // ===== 事件追踪核心方法 =====
  // 统一处理所有用户行为事件
  track(eventName, data) {
    // 构建完整的事件对象
    const event = {
      eventName,                              // 事件名称
      data,                                   // 事件数据
      timestamp: Date.now(),                   // 事件时间戳
      sessionId: this.sessionId,               // 会话ID
      userId: this.getUserId(),                // 用户ID
      url: window.location.href,              // 当前页面URL
      userAgent: navigator.userAgent,          // 浏览器信息
      screenResolution: {                     // 屏幕分辨率
        width: screen.width,
        height: screen.height
      }
    };
    
    // 添加到本地缓存
    this.events.push(event);
  }
  
  // ===== 批量上报机制 =====
  startBatching() {
    // 定时批量上报:每30秒上报一次
    const batchInterval = setInterval(() => {
      this.sendBatch();
    }, 30000);
    
    // 页面可见性变化时上报(用户切换标签页)
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.sendBatch();
      }
    });
    
    // 页面卸载时上报:确保数据不丢失
    window.addEventListener('beforeunload', () => {
      this.sendBatch();
      clearInterval(batchInterval);
    });
  }
  
  // ===== 事件上报方法 =====
  async sendBatch() {
    // 如果没有事件,直接返回
    if (this.events.length === 0) return;
    
    // 复制事件数组并清空缓存
    const eventsToSend = [...this.events];
    this.events = [];
    
    try {
      // 发送批量事件到服务器
      await fetch('/api/events', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          events: eventsToSend,                 // 事件数组
          batchId: this.generateBatchId(),      // 批次ID
          timestamp: Date.now()                 // 上报时间
        })
      });
    } catch (error) {
      // 上报失败处理:将事件重新添加到缓存头部
      console.error('Failed to send events:', error);
      this.events.unshift(...eventsToSend);
    }
  }
  
  // ===== 工具方法 =====
  
  // 生成会话ID
  generateSessionId() {
    return 'session_' + 
      Math.random().toString(36).substr(2, 9) +  // 随机字符串
      Date.now();                                 // 时间戳
  }
  
  // 生成批次ID
  generateBatchId() {
    return 'batch_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
  }
  
  // 获取用户ID
  getUserId() {
    return localStorage.getItem('userId') || 'anonymous';
  }
  
  // 获取元素属性
  getElementAttributes(element) {
    const attributes = {};
    for (let attr of element.attributes) {
      if (['id', 'class', 'href', 'src', 'alt', 'title'].includes(attr.name)) {
        attributes[attr.name] = attr.value;
      }
    }
    return attributes;
  }
  
  // 数据脱敏处理
  sanitizeValue(value) {
    if (typeof value !== 'string') return value;
    
    // 密码字段完全隐藏
    if (value.length > 0 && ['password', 'pwd', 'pass'].some(keyword => 
        this.currentFieldName?.toLowerCase().includes(keyword))) {
      return '******';
    }
    
    // 邮箱部分脱敏
    if (value.includes('@')) {
      const [username, domain] = value.split('@');
      return username.substring(0, 2) + '***@' + domain;
    }
    
    // 手机号脱敏
    if (/^\d{11}$/.test(value)) {
      return value.substring(0, 3) + '****' + value.substring(7);
    }
    
    // 其他长文本截断
    return value.length > 50 ? value.substring(0, 50) + '...' : value;
  }
}

// ===== 初始化用户行为追踪 =====
// 在应用入口处初始化行为追踪系统
const behaviorTracker = new UserBehaviorTracker();

// 暴露全局接口,供业务代码调用自定义事件
window.trackEvent = (eventName, data) => {
  behaviorTracker.track(eventName, data);
};

window.trackCustomGoal = (goalName, value) => {
  behaviorTracker.track('goal_conversion', {
    goalName,
    value,
    timestamp: Date.now()
  });
};

🎯 总结

这个前端工程化终极指南提供了从基础到高级的完整解决方案:

📋 核心覆盖内容

  1. 构建工具深入

    • Webpack:核心概念、高级配置、性能优化
    • Gulp:任务流管理、自动化构建
    • Vite:现代构建、开发体验优化
  2. 工程化最佳实践

    • 代码规范:ESLint、Prettier、Git Hooks
    • 工作流规范:分支管理、提交规范
    • Monorepo架构:多项目管理
  3. CI/CD与部署

    • Docker容器化部署
    • Kubernetes集群管理
    • 自动化流水线
  4. 实战项目落地

    • 微前端架构实现
    • 组件库工程化
    • 低代码平台搭建
  5. 监控与优化

    • 错误监控系统
    • 性能监控体系
    • 用户行为分析

🚀 技术亮点

  • 深度技术解析:每个工具都有原理解释和实战配置
  • 企业级方案:包含Monorepo、微前端等企业架构
  • 完整代码示例:所有配置和工具都可以直接使用
  • 性能优化专项:从构建到运行的全链路优化
  • 监控体系建设:错误、性能、行为的全方位监控

教你快速从Vue 开发者 → React开发者转变!

前言

工作这么多年,一直用的都是vue,对vue框架也最熟悉,但最近想深入学习react,之前也学过,只懂一点皮毛,对很多写法还是不理解,我就在想既然我比较熟悉vue,那能不能设计一份react和vue的转化总结,这样用理解vue的方式来学习react那就事半功倍了。

现在AI这么方便,我就把我的需求说给chatGPT了,他帮我设计了一份Vue 开发者 → React 转化总结与对照表和一份学习计划。

真的一目了然,特别好理解,在上周末我根据这两份计划很快的上手了react框架,也顺利完成一个小型的react项目,这次没有一知半解!

虽然AI可以帮我写代码,但我们自己还是得掌握,不然可能连代码都看不懂,怎么去进行调试呢?

AI帮我们写90%,那剩下10%就要自己来了!

AI真的很好用,它可以帮助我们快速学习任何我们想学习的,还是使用最简单易上手的方式!

以下就是对照表以及学习计划了,希望对想快速上手react的小伙伴有点借鉴意义!

核心思想对比

Vue

  • 使用模板(HTML-like template)
  • 自动依赖追踪的响应式系统
  • 指令系统(v-if / v-for / v-model)
  • 双向绑定常见
  • 更“框架化”,封装度高

React

  • UI = f(state) 的函数式思想
  • 使用 JSX(模板+JS融合)
  • 没有自动依赖追踪,需要手动声明(useEffect)
  • 单向数据流
  • 更灵活、更偏底层

API 对照表

核心 API 对照

Vue React 说明
data() useState 声明组件状态
computed useMemo 计算属性
watch useEffect 监听值变化或模拟生命周期
方法(methods) 普通函数 不需要特殊 API
ref() useRef DOM 或变量引用
provide / inject createContext + useContext 跨组件传递数据
v-model onChange + useState 双向绑定自己实现
v-if JS 表达式(条件渲染) { condition && <Component /> }
v-for arr.map() 列表渲染
组件通信(props) props 完全一致
子组件事件(emit) 父传入回调函数 回调作为 props

生命周期迁移

Vue → React 生命周期对照表

Vue React
onMounted useEffect(() => {}, [])
onUpdated useEffect(() => {})
onUnmounted useEffect(() => return cleanup, [])
onActivated 无,需要自定义
onDeactivated 无,需要自定义
beforeMount / beforeUpdate 无(通常不需要)

示例:Vue → React 生命周期对比

Vue
<script setup>
onMounted(() => console.log("mounted"))
onUnmounted(() => console.log("unmounted"))
</script>
React
useEffect(() => {
  console.log("mounted");
  return () => console.log("unmounted");
}, []);

状态管理迁移

Vue → React 状态管理选择

Vue React 等价方案 特点
Pinia Zustand 写法最像,轻量好用(推荐)
Vuex Redux Toolkit 官方推荐、企业级
composables 自定义 Hooks 逻辑复用方式几乎一样
reactive() useState/useReducer 响应式状态

Zustand(最适合 Vue 开发者)

Zustand 使用方式类似 Pinia,推荐入门 React 状态管理就用它。

示例:Zustand vs Pinia

Pinia

export const useUserStore = defineStore('user', {
  state: () => ({ name: 'Echo' }),
  actions: { setName(name) { this.name = name } }
})

Zustand

const useUserStore = create((set) => ({
  name: "Echo",
  setName: (name) => set({ name }),
}));

几乎一模一样。


路由迁移

Vue Router → React Router 对照表

Vue Router React Router
routes 数组 <Routes><Route/></Routes>
router.push() useNavigate()
获取参数 useParams()
路由守卫 自定义鉴权组件包裹 Route

Vue 路由

const routes = [
  { path: '/user/:id', component: User }
]

React 路由

<Routes>
  <Route path="/user/:id" element={<User />} />
</Routes>

读取参数:

const { id } = useParams()

项目结构对照

Vue 项目结构

src/
  components/
  views/
  store/
  router/
  composables/
  assets/

React 推荐结构

src/
  components/     # 公共组件
  pages/          # 页面级组件
  hooks/          # 自定义 Hooks
  store/          # 状态管理(Zustand / Redux)
  router/         # 路由定义
  services/       # API 请求封装
  assets/

思维模型转化总结

1. 模板 → JSX(最大的差异)

Vue:

<v-if="ok">hello</v-if>

React:

{ok && <div>hello</div>}

2. 自动响应式 → 显式依赖声明

Vue 自动追踪
React 必须写依赖数组

3. 逻辑复用方式变化

Vue compositions → React 自定义 Hooks

4. 组件通信完全一致(props)

但事件改为父传回调函数

5. React 更纯粹、更 JavaScript 化

少魔法、少黑盒、更多掌控权。


学习计划(适合 Vue 开发者)

📘 阶段 1:核心理念与基础 JSX

⭐ 核心目标

  • 理解 React 哲学(UI = f(state))
  • 能用 JSX 编写组件、列表、事件

✅ 实战 1:Hello React + 计数器

1. 新建项目(Vite)

npm create vite@latest react-basic --template react
cd react-basic
npm install
npm run dev

2. 编写一个计数器(App.jsx)

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: 20 }}>
      <h1>计数器</h1>
      <p>当前值:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

📝 Vue 对照:

<script setup>
const count = ref(0)
</script>

<button @click="count++">{{ count }}</button>

🌱 实践任务

  • 写一个 从 1 增加到 100 的进度条动画
  • 写一个 列表渲染组件(todoList)

📘 阶段 2:Hooks 深入

⭐ 核心要点

  • useState
  • useEffect(替代 watch / 生命周期)
  • useMemo / useCallback(性能)
  • useRef(替代 Vue的模板 ref)
  • 自定义 hooks(对 Vue 用户最重要)

✅ 实战 2:useEffect 生命周期模拟

Vue → React 生命周期对照

Vue React
onMounted useEffect(() => {}, [])
onUpdated useEffect(() => {})
onUnmounted return () => {}

例子:监听窗口大小

import { useState, useEffect } from "react";

export default function WindowSize() {
  const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight });

  useEffect(() => {
    const onResize = () => {
      setSize({ w: window.innerWidth, h: window.innerHeight });
    };
    window.addEventListener("resize", onResize);

    return () => window.removeEventListener("resize", onResize);
  }, []);

  return <div>窗口大小:{size.w} x {size.h}</div>;
}

✅ 实战 3:自定义 Hook(重要)

Vue 组合式 API 与 React 自定义 hook 是一样的概念。

useFetch.js

import { useState, useEffect } from "react";

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then((json) => {
        setData(json);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
}

使用它

import { useFetch } from "./useFetch";

export default function UserList() {
  const { data, loading } = useFetch("https://jsonplaceholder.typicode.com/users");

  if (loading) return <p>加载中...</p>;
  return (
    <ul>
      {data.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

🌱 实践任务

  • 写一个 useLocalStorage(key, defaultValue)
  • 写一个 useCountdown(秒)
  • 写一个 useDebounce(value, delay)

📘 阶段 3:React Router


✅ 项目结构

src/
  pages/
    Home.jsx
    Detail.jsx
  main.jsx
  App.jsx

安装

npm install react-router-dom

✅ 实战 4:配置基础路由

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/detail/:id" element={<Detail />} />
      </Routes>
    </BrowserRouter>
  );
}

Detail.jsx

import { useParams } from "react-router-dom";

export default function Detail() {
  const { id } = useParams();
  return <h2>详情页面 - ID: {id}</h2>;
}

🌱 实践任务

  • 写一个 商品列表页 + 商品详情页
  • 点击列表项跳转到详情页

📘 阶段 4:状态管理

为了更像 Vue 的 Pinia,你会更喜欢 Zustand


👉 为什么不用 Redux?

  • Redux 心智负担大(action/reducer/immutable)
  • Zustand 更像 Pinia:简单、轻巧、API优雅

📦 安装 Zustand

npm install zustand

✅ 实战 5:创建用户 store(类似 Pinia)

src/store/user.js

import { create } from "zustand";

export const useUserStore = create((set) => ({
  name: "Echo",
  setName: (name) => set({ name }),
}));

在组件里使用

import { useUserStore } from "../store/user";

export default function Profile() {
  const { name, setName } = useUserStore();

  return (
    <div>
      <p>用户名:{name}</p>
      <input onChange={(e) => setName(e.target.value)} />
    </div>
  );
}

🌱 实践任务

  • 给 store 增加:token / userInfo / logout
  • 全局 Layout 读取用户信息

📘 阶段 5:接口请求与工程化


推荐:axios 封装 + useRequest Hook


✅ 实战 6:axios 封装

/src/api/request.js

import axios from "axios";

const request = axios.create({
  baseURL: "https://api.example.com",
  timeout: 8000,
});

request.interceptors.response.use((res) => res.data);

export default request;

使用 useRequest Hook(可复用)

import { useEffect, useState } from "react";
import request from "../api/request";

export function useRequest(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    request(url).then((res) => {
      setData(res);
      setLoading(false);
    });
  }, [url]);

  return { data, loading };
}

📘 阶段 6:综合项目

做一个 后台管理系统模版(React)


功能组成

  • 登录页
  • Layout 布局(侧边栏 + Header)
  • react-router 路由守卫
  • 表格 CRUD(增删改查)
  • Zustand 全局状态(保存用户)

没 CDN = 用户等半天?四大核心机制:就近、分流、提速、容错全搞定

🚀 性能优化的“任意门”:CDN 如何实现全球加速,让用户“瞬间移动”到你的网站?

前端性能优化专栏 - 第六篇

在上一篇中,我们探讨了 HTTP 缓存机制,它解决了用户“二次访问”的速度问题。但对于全球用户来说,“首次访问”的速度依然是一个挑战,因为网络延迟与物理距离成正比。

想象一下,你的网站服务器在北京,一个远在伦敦的用户访问你的网站,数据需要跨越欧亚大陆。这个漫长的旅程,就是延迟的根源。

今天,我们将揭秘一个真正能让你的网站实现“瞬间移动”的秘密武器——内容分发网络(CDN,Content Delivery Network)


💡 CDN 是什么?内容仓库的“分店”模式

CDN 是由分布在全球各地的高性能服务器节点组成的网络。它的核心作用是将网站内容更靠近用户,以实现更快的访问体验。

你可以把 CDN 理解为一个拥有全球“分店”的内容仓库体系:

  • 源站(Origin): 存储网站的原始内容(总部)。
  • CDN 节点(Edge Node): 分布在全球各地的“分店”,负责缓存和分发内容。
  • 智能调度系统: 负责指挥用户去最近的“分店”取货。

核心理念:就近响应

CDN 的核心理念就是内容仓库的“分店”模式,通过三个步骤实现就近响应:

  1. 缓存内容: 提前在各地节点存储静态资源。
  2. 智能调度: 通过智能 DNS 分配最佳节点。
  3. 就近响应: 从最近节点直接返回内容,大幅缩短传输距离。

专业名词解释:智能 DNS 是 CDN 实现就近访问的关键技术。它能根据用户的地理位置、网络运营商、以及 CDN 节点的负载情况,动态地将用户的请求解析到最优的 CDN 边缘节点 IP 地址。

下方图片展示了 CDN 的核心理念: img

🚀 CDN 提升性能的四大核心机制

CDN 不仅仅是简单的缓存,它通过四大机制全方位提升网站性能和稳定性:

🌐1. 就近访问 — 降低网络延迟

这是 CDN 最直观的优势。网络延迟与物理距离成正比。

CDN 通过在全球部署大量的边缘节点,让用户“就近取数据”,从而:

  • 减少物理距离带来的延迟: 用户的请求不再需要绕远路到达遥远的源站。
  • 消除重复的网络握手: 边缘节点与用户之间的连接速度更快,且通常已经建立了连接。

下方图片进一步说明了就近访问的优势: img

下方示意图也清晰地展示了全球分发和就近响应的流程: CDN 全球分发示意图

🛡️2. 减轻源站压力 — 分担流量与请求

CDN 承担了绝大部分静态资源(图片、脚本、CSS)的分发工作,这极大地减轻了源站的压力,让源站可以专注于核心业务。

  • 分流: 静态资源(通常占网站流量的 80% 以上)由 CDN 处理。
  • 专注: 源站只需应对动态业务(登录、查询、支付等)。
  • 稳定: 避免因并发量过高导致源站服务器崩溃,保障高峰期访问稳定性。
  • 效率: 提高整体吞吐量与服务效率。

我们生成的对比图形象地展示了 CDN 的负载均衡能力: 源站过载与 CDN 负载均衡对比图

⚡3. 优化资源调度 — 提升带宽与并发性能

CDN 服务商通常拥有更高质量的网络基础设施,这为性能优化提供了坚实的基础:

  • 海量带宽储备: 支持突发流量,应对大促或热点事件。
  • 动态负载均衡: 实时分配请求到负载最低、响应最快的节点。
  • 多协议优化: 支持 HTTP/2、QUIC 等先进协议,提升传输效率。

💡 结论: CDN 提供了连接少、带宽大、资源调度智能的网络环境,页面加载自然更快。

🔄4. 提升可用性 — 负载均衡与自动容错

CDN 内置的负载均衡自动容错机制是网站高可用的重要保障,让网站“永不宕机”:

  • 负载均衡: 实时分配最优节点,确保用户体验。
  • 自动容错: 边缘节点异常时,请求会自动切换到健康的节点。
  • 全局冗余: 保证服务连续性,即使部分节点宕机,服务也不会中断。

下方图片展示了 CDN 的高可用性: img

✅ 核心机制总结与最佳实践

CDN 的核心价值可以概括为以下四点:

机制 核心作用 性能提升点
地理就近 减少物理距离 降低网络延迟(TTFB)
流量分担 静态资源分流 稳定源站,提高并发处理能力
资源优化 高质量网络 提升带宽利用率,支持先进协议
智能调度 负载均衡与容错 保证高可用性,避免单点故障

最佳实践:

  • 动静分离: 只有静态资源(图片、CSS、JS)才应该走 CDN。动态内容(API 接口)应由源站处理。
  • 合理设置缓存头: 结合上一篇的知识,在 CDN 上配置合理的 Cache-Control,确保资源在 CDN 节点和用户浏览器都能被高效缓存。

下一篇预告: 在所有的前端资源中,图片往往是体积最大、最影响加载性能的“罪魁祸首”。下一篇我们将聚焦于如何对图片进行精细化管理,探讨各种图片加载策略,让你的页面既高清又秒开!敬请期待!

JWorker——一套简单易用的基于鸿蒙 Worker 的双向 RPC 通讯机制

零、JWorker

JWorker 是一套简单易用的基于鸿蒙 Worker 的双向 RPC 通讯机制。

structure.png

传统的 Worker 通讯基于事件监听和消息传递,缺乏原生的 Promise/async-await 支持,导致逻辑割裂。JWorker 通过双向 RPC 机制,让主 Worker 可以 await 子 Worker 的执行结果,子 Worker 也可以 await 主 Worker 的响应,将跨 Worker 通讯简化为像调用本地异步函数,消除回调嵌套,保持代码线性流畅。

一、安装

运行 ohpm install jworker 安装 JWorker 库

二、常规使用

JWorker 是基于鸿蒙 Worker 封装的一套 RPC 通讯机制,所以在正式使用之前需要先添加和配置 Worker 的 ets 文件。可以按照鸿蒙官方 Worker 的使用文档进行添加配置,这里就不再赘述。

“常规使用” 示例完整代码 传送门

1、创建 JWorker

主 Worker 中使用 createJWorker(workerPath: string) 创建 JWorker 实例,然后调用 JWorker.start() 启动 JWorker 。 完整代码如下:

JWorker.start() 内部会启动 Worker 文件,并关联消息接收、退出接收等回调。

// 将 Worker 的文件路径传给 createJWorker 方法,会返回 JWorker 实例
this.worker = createJWorker("sample/ets/worker/simple/SimpleWorker.ets")
// 启动 JWorker
this.worker.start()

子 Worker 中使用 initJWorker() 获取 SubWorker 实例。完整代码如下:

initJWorker() 内部会让 SubWorker 关联子 Worker 的消息接收等回调。

const worker = initJWorker()

2、双向 RPC 通讯

JWorker 的通讯是基于 Channel ,所以主子 Worker 的通讯需要先添加相同名称的 Channel

主 Worker 通过 JWorker.addChannel(channelName: string, channel: Channel) 方法进行添加通讯 Channel 。

// 创建通讯渠道 MainSimpleChannel ,需要继承 Channel 
this.simpleWorkerChannel = new MainSimpleChannel()
// 添加渠道名为 “SimpleWorkerChannel” 的通讯 Channel 
this.worker.addChannel("SimpleWorkerChannel", this.simpleWorkerChannel)

子 Worker 通过 JWorkerChannel(channelName: string, channel: Channel) 方法进行添加通讯 Channel 。

// 添加渠道名为 “SimpleWorkerChannel” 的通讯 Channel
// 同样 SubSimpleChannel 也需要继承 Channel
JWorkerChannel("SimpleWorkerChannel", new SubSimpleChannel(worker))

主 Worker 和子 Worker 通过相同的渠道名称建立通讯通道MainSimpleChannelSubSimpleChannel 通讯规则如下:

  • 通过 handleMessage(methodName: string, data: any): Promise<any> 接收对方的调用消息,返回值会返回到调用点;
  • 通过 send(methodName: string, data?: any, transfer?: ArrayBuffer[]) => Promise<any> 可以主动调用对方方法并携带参数,对方处理完的返回值会以 Promise<any> 类型返回到调用点。

主 Worker 调用子 Worker 的逻辑,通过注册的 simpleWorkerChannel 调用 send 方法发送即可。

// ============== 主 Worker 中进行发送 ==============
const user = {
  "name": "jiangpengyong",
  "year": 1994,
  "height": 170.0,
  "address": {
    "country": "China",
    "province": "GuangDong",
    "city": "Guangzhou",
  },
} as User
// 第一个参数为调用方法名称,第二个参数为调用方法的参数
// response 为子 Worker 处理的结果
const response = (await this.simpleWorkerChannel?.send("sayHello", user)) as Any
Log.i(TAG, `【发送有处理的消息】子 Worker 回复 response=${JSON.stringify(response)}`)

// ============== 子 Worker 中进行接收处理 ==============
// 通过 SubSimpleChannel 接收调用方法名称和参数,处理后返回结果
export class SubSimpleChannel extends Channel {
  async handleMessage(methodName: string, data: Any): Promise<Any> {
    switch (methodName) {
      // 处理主 Worker 调用的 “sayHello” 方法,将 data 转为 User 类型并获取对应数据,返回一个 string 结果
      case "sayHello": {
        const user = data as User
        return `Hello, ${user.name}. I'm replying to you from the sub-worker.`
      }
      // 省略其他方法
    }
  }
}

// ============== 最终会在 Log 中看到以下输出 ==============
// 【发送有处理的消息】子 Worker 回复 response="Hello, jiangpengyong. I'm replying to you from the sub-worker."

子 Worker 调用主 Worker 的逻辑,也是同样的流程,通过注册的 SubSimpleChannel 调用 send 方法发送即可。

// ============== 子 Worker 调用主 Worker 的逻辑 ==============
export class SubSimpleChannel extends Channel {
  async handleMessage(methodName: string, data: Any): Promise<Any> {
    switch (methodName) {
      case "getUserDes": {
        // 调用主 Worker 的 “getUserInfo” 方法,此处没有携带参数,会返回 User 类型
        const user = await this.send("getUserInfo") as User
        return `name: ${user.name}, height: ${user.height}`
      }
      // 省略其他逻辑
    }
  }
}

// ============== 主 Worker 处理逻辑 ==============
export class MainSimpleChannel extends Channel {
  async handleMessage(methodName: string, data: Any): Promise<Any> {
    switch (methodName) {
      // 接收到子 Worker 的请求处理,处理完之后返回数据
      case "getUserInfo": {
        return {
          "name": "江澎涌",
          "year": 1994,
          "height": 170.0,
          "address": {
            "country": "中国",
            "province": "广东",
            "city": "普宁",
          },
        } as User
      }
    }
  }
}

Channel 中包含了 send(methodName: string, data?: any, transfer?: ArrayBuffer[]) => Promise<any> 方法,可以在 “ Channel 内部主动调用” 或是 “外部代码通过 Channel 实例主动调用”,await 数据返回即可。

3、传递 ArrayBuffer 数据

Worker 在传递 ArrayBuffer 时,为了不拷贝 ArrayBuffer 数据,可以考虑将 ArrayBuffer 使用权移交给对方,JWorker 也同样提供这一能力。

transfer_data.png

上图则是一个完整的移交 ArrayBuffer 使用权的全流程

调用点传递 ArrayBuffer 类型数据

无论是 “主 Worker 主动调用子 Worker 方法”,还是 “子 Worker 主动调用主 Worker 方法”,都是使用 Channel 的 send 方法。

send(methodName: string, data?: any, transfer?: ArrayBuffer[]) => Promise<any>

send 的第三个参数 transfer 持有第二个参数 data 中需要移交使用权的 ArrayBuffer 对象,JWorker 会负责移交使用权。

// ============== 发送方代码(此处为主 Worker ) ==============
const uint8Array = await getContext(this).resourceManager.getRawFileContent("image1.jpeg")
const arrayBuffer = uint8Array.buffer
// 此处将 arrayBuffer 使用权移交给子 Worker ,所以将 arrayBuffer 放置到了第三个参数
// 值得注意,移交后的 arrayBuffer ,主 Worker 不可再使用,否则会报错
const response = await this.simpleWorkerChannel.send("cropImage", arrayBuffer, [arrayBuffer]) as ArrayBuffer | undefined

// ============== 接收方代码(此处为子 Worker ) ==============
export class SubSimpleChannel extends Channel {
  async handleMessage(methodName: string, data: Any): Promise<Any> {
    switch (methodName) {
      case "cropImage": {
        // 接收 ArrayBuffer 的代码没有特别的要求,和接收普通类型的逻辑一样,只需要转为对应的类型进行处理即可
        const arrayBuffer = data as ArrayBuffer
        const cropPixelMap = await this.cropImage(arrayBuffer)
        const cropArrayBuffer = await PixelMapConverter.pixelMapToArrayBuffer(cropPixelMap)
        // 省略其他逻辑
      }
    }
  }
}

返回值传递 ArrayBuffer 类型数据

在处理完逻辑后,返回数据给调用方,此时存在返回数据携带 ArrayBuffer 类型数据的场景。为此 JWorker 提供了 TransferData 类型,支持该场景的数据传递,具体操作如下:

// ============== 继续上面的代码 ==============
// ============== 接收方代码(此处为子 Worker ) ==============
export class SubSimpleChannel extends Channel {
  async handleMessage(methodName: string, data: Any): Promise<Any> {
    switch (methodName) {
      case "cropImage": {
        // 省略重复代码
        // 返回值如果需要移交 ArrayBuffer 使用权,则使用 TransferData 类进行包裹
        // 第一个参数为返回数据,第二个参数为需要移交使用权的 ArrayBuffer 列表
        return new TransferData(cropArrayBuffer, [cropArrayBuffer])
      }
    }
  }
}

// ============== 发送方代码(此处为主 Worker ) ==============
// 调用点接收到的数据类型是已经去掉 TransferData 包裹的真实数据
const response = await this.simpleWorkerChannel.send("cropImage", arrayBuffer, [arrayBuffer]) as ArrayBuffer | undefined
if (response) {
  this.cropPixelMap = await PixelMapConverter.arrayBufferToPixelMap(response)
}

4、关闭 JWorker

在 JWorker 中提供了两种关闭 Worker 的方式,分别为 主 Worker 进行关闭子 Worker 进行关闭推荐使用子 Worker 进行关闭,因为项目可以更好控制子 Worker 的生命周期和释放相应资源。

主 Worker 进行关闭

通过调用 JWorker 实例的 release() 方法进行释放。

// 创建 JWorker 对象
this.worker = createJWorker("sample/ets/worker/simple/SimpleWorker.ets")
// 进行开启 JWorker 、添加 Channel 等操作

// 关闭 JWorker
this.worker?.release()

子 Worker 进行关闭

通过调用 SubWorker 对象的 release() 方法进行释放。

// 在子 Worker 中构建 subWorker
const worker = initJWorker()
// 添加需要的 Channel 操作

// 在需要释放的时候调用
// 1、可以将 worker 传递给 Channel ,Channel 内部可以根据需要进行调用释放
// 2、可以全局持有,在需要的时候进行释放
worker.release()

5、值得注意

如果 JWorker 对象未开启(即未调用 JWorker.start() 方法或已关闭),此时使用添加在该 JWorker 的 Channel 进行发送消息会立马得到一个 undefined 数据。

如果通过 JWorker 的 Channel 发送了消息,在未得到回复前对该 JWorker 进行关闭,则会让调用点立马得到一个 undefined 数据。

所以为了程序的健壮,调用点的类型转换最好增加对 undefined 的判断。

三、多个 Worker

1、项目主 Worker 开多个子 Worker

JWorker 项目支持开启多个 Worker ,使用 createJWorker(workerPath: string) 方法传入不同的路径,管理好返回 JWorker 对象即可。

“项目主 Worker 开多个子 Worker” 示例完整代码 传送门

main_multi_worker.png

假设项目需要构建上图的使用场景,可以通过以下代码创建 JWorker 实例。

  • 可以使用不同的 Worker ets 文件,也可以使用同一个 Worker ets 文件可以开启多个 JWorker 实例。
  • 通过管理好 JWorker 实例,添加渠道后进行各自 Worker 通讯。
// worker0 和 worker1、worker2 使用不同的 Worker ets 文件进行开启不同的 JWorker 实例
this.worker0 = createJWorker("sample/ets/worker/simple/SimpleWorker.ets")
this.worker0.start()
this.simpleWorkerChannel = new MainSimpleChannel()
this.worker0.addChannel("SimpleWorkerChannel", this.simpleWorkerChannel)

// worker1 和 worker2 使用相同的 Worker ets 文件进行开启不同的 JWorker 实例 
this.worker1 = createJWorker("sample/ets/worker/mainmultiworker/MainMultiWorker.ets")
this.worker1Channel = new MainMultiChannel()
this.worker1.addChannel("multiChannel", this.worker1Channel)
this.worker1.start()

this.worker2 = createJWorker("sample/ets/worker/mainmultiworker/MainMultiWorker.ets")
this.worker2Channel = new MainMultiChannel()
this.worker2.addChannel("multiChannel", this.worker2Channel)
this.worker2.start()

2、子 Worker 开多个子 Worker

JWorker 同样支持在子 Worker 中开启多个 JWorker ,可以进行如下图所示的创建和管理。

“子 Worker 开多个子 Worker” 示例完整代码 传送门

sub_multi_worker.png

可以在子 Worker 需要创建子 Worker 的地方调用 createJWorker 方法创建 JWorker ,然后进行启动和添加相应 Channel 进行通讯。使用方式和之前的完全相同。

export class ParentSubChannel extends Channel {
  // 省略其他逻辑

  constructor(worker: SubWorker) {
    // 省略其他逻辑
    this.startChildrenWorker()
  }

  private startChildrenWorker() {
    // 创建三个 JWorker 并开启,添加对应 Channel 
    if (this.childWorker1 == undefined) {
      this.childWorker1 = createJWorker("sample/ets/worker/submultiworker/ChildWorker.ets")
      this.childWorker1Channel = new ChildMainChannel()
      this.childWorker1.addChannel("childChannel", this.childWorker1Channel)
      this.childWorker1.start()
    }
    if (this.childWorker2 == undefined) {
      this.childWorker2 = createJWorker("sample/ets/worker/submultiworker/ChildWorker.ets")
      this.childWorker2Channel = new ChildMainChannel()
      this.childWorker2.addChannel("childChannel", this.childWorker2Channel)
      this.childWorker2.start()
    }
    if (this.childWorker3 == undefined) {
      this.childWorker3 = createJWorker("sample/ets/worker/submultiworker/ChildWorker.ets")
      this.childWorker3Channel = new ChildMainChannel()
      this.childWorker3.addChannel("childChannel", this.childWorker3Channel)
      this.childWorker3.start()
    }
  }
}

值得注意

这种情况下需要控制好 Worker 的关闭顺序,应该让项目的主 Worker 通知子 Worker 进行关闭他创建的子 Worker ,然后在关闭自身。具体操作如下:

// 项目主 Worker 调用子 Worker 的 exit 方法
this.workerChannel?.send("exit")

// 子 Worker 接收到主 Worker 的 “exit” 调用,则调用子 Worker 创建的子 Worker 的 “exit” 方法进行退出,并等待所有的子 Worker 处理完再退出自身
export class ParentSubChannel extends Channel {
  // 省略其他逻辑
  async handleMessage(methodName: string, data: Any) {
    switch (methodName) {
      case "exit": {
        // 等待所有子 Worker 退出完成
        await Promise.all([this.childWorker1Channel?.send("exit"), this.childWorker2Channel?.send("exit"), this.childWorker3Channel?.send("exit")])
        Log.i(TAG, "【exit】")
        await this.worker?.release()
        this.worker = undefined
        this.childWorker1 = undefined
        this.childWorker1Channel = undefined
        this.childWorker2 = undefined
        this.childWorker2Channel = undefined
        this.childWorker3 = undefined
        this.childWorker3Channel = undefined
        return undefined
      }
      default: {
        return undefined
      }
    }
  }
}

// 子 Worker 的子 Worker 接收到 “exit” 的调用,退出自身
export class ChildSubChannel extends Channel {
  // 省略其他逻辑
  async handleMessage(methodName: string, data: Any) {
    if (methodName == "exit") {
      this.worker.release()
      return undefined
    }
    return undefined
  }
}

四、作者博客

掘金:juejin.im/user/5c3033…

csdn:blog.csdn.net/weixin_3762…

公众号:微信搜索 "江澎涌"

Hello 算法:以“快”著称的哈希

每个系列一本前端好书,帮你轻松学重点。

本系列来自上海交通大学硕士,华为高级算法工程师 靳宇栋《Hello,算法》

你一定不止一次开始学算法,然而,因为很多人都会有的“知难而退”和“持久性差”,每次都半途而废,有些算法就因为排在了学习计划中靠后的位置,从来不曾触碰过,既不知道是什么,也不知道可以用来干什么。

本篇文章开始,小匠会带大家一起,用最少的时间,揭开常见“高级”算法的神秘面纱。

什么是“哈希”?

哈希,从萌芽时期到现在已有70多年历史,音译自“hash”,原意是“切碎并搅拌”。即将一个任意长度的输入(),通过特定的算法(切碎并搅拌),转换成一个固定长度的、看似随机的输出(哈希值) 。

听起来很玄乎,实际想做的,就是能够像数组一样,实现对数据的快速访问,且键可以是非整型数字。

微信图片_2025-12-14_175842_460.jpg

那么在编程语言中,是否包含了这种数据结构呢,答案是:有。

JavaScript的早期版本是没有的,ES6后才加入,它就是Map

Map

Map大家并不陌生,常用来存储键值对,就像这样:

/* 初始化 */
const map = new Map();

// 添加键值对 (key, value)
map.set(12836'小哈');
map.set(15937'小啰');

// 根据 key 做查询
let name = map.get(15937);

// 删除
map.delete(12836);

看起来并不复杂,而且如果只是存储键值对,还可以用Object,甚至更“方便”。

那为什么要多创造出来一种数据结构,它们的区别是什么?

Object,意为对象,面向对象编程的精髓是将数据做抽象,定义它所具备的属性能力

Map,意为映射,主要用于存储、查找数据,单从这点就很容易区分了。

同时,Map有专门的增、删、迭代方法,且严格按照插入顺序迭代。

此外,Object的优化倾向是静态存储和快速访问。Map的优化倾向是频繁增删。

这样对比下来,它们的特点和应用场景差异还是挺大的。

接下来,深入介绍一下哈希表有哪些特点、用途和痛点。

用数组实现哈希

了解一种数据结构的最佳途径就是亲手实现它。

通常,笔者在这个节点都会比较慌:一来不知从何下手,二来涉及逻辑细节就发懵。

但这次不用慌,我们只看最简的“数组实现”。

微信图片_2025-12-14_181201_100.png

映射关系的建立

我们知道,数组本身自带索引位,本书作者将这些索引位称为“桶”(bucket),你想叫别的也行,本质就是容器

每个桶可存储一个键值对(记住这里的“一个”),因此,查询操作就是找到 key 对应的桶,并在桶中获取 value 。

前面提到,哈希表需要实现的是“非整型数字”作为 key,既然要放弃天然的数字索引,兼容其他类型,如何基于 key 定位对应的桶呢?这就要涉及它的核心能力—哈希函数(hash function)。

哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间

为什么这么做?因为哈希表的输入是不确定的,但能使用的存储空间是有限的。

本次实现中,输入一个 key ,哈希函数的计算过程有两步:

  • 通过哈希算法 hash() 计算得到哈希值。

  • 将哈希值对桶数量(数组长度)capacity 取模,从而获取该 key 对应的桶(数组索引)index

    index = hash(key) % capacity

随后,就可以利用 index 在哈希表中访问对应的桶,从而获取 value 。

实现逻辑如下。

准备好一个创建键值对的类:

/* 键值对 Number -> String */
class Pair {
    constructor(key, val) {
        this.key = key;
        this.val = val;
    }
}

创建哈希类:

/* 基于数组实现的哈希表 */
class ArrayHashMap {
    #buckets;
    constructor() {
        // 初始化数组,包含 100 个桶
        this.#buckets = new Array(100).fill(null);
    }
}

计算 value 的哈希函数:

 /* 哈希函数 */
    #hashFunc(key) {
        return key % 100;
    }

增、删、查方法:

    /* 查询操作 */
    get(key) {
        let index = this.#hashFunc(key);
        let pair = this.#buckets[index];
        if (pair === nullreturn null;
        return pair.val;
    }

    /* 添加操作 */
    set(key, val) {
        let index = this.#hashFunc(key);
        this.#buckets[index] = new Pair(key, val);
    }

    /* 删除操作 */
    delete(key) {
        let index = this.#hashFunc(key);
        // 置为 null ,代表删除
        this.#buckets[index] = null;
    }

遍历方法:

    /* 获取所有键值对 */
    entries() {
        let arr = [];
        for (let i = 0; i < this.#buckets.length; i++) {
            if (this.#buckets[i]) {
                arr.push(this.#buckets[i]);
            }
        }
        return arr;
    }

    /* 获取所有键 */
    keys() {
        let arr = [];
        for (let i = 0; i < this.#buckets.length; i++) {
            if (this.#buckets[i]) {
                arr.push(this.#buckets[i].key);
            }
        }
        return arr;
    }

    /* 获取所有值 */
    values() {
        let arr = [];
        for (let i = 0; i < this.#buckets.length; i++) {
            if (this.#buckets[i]) {
                arr.push(this.#buckets[i].val);
            }
        }
        return arr;
    }

整体比较容易理解,一个是基于哈希函数的value计算,一个是数组遍历。

可以看出,哈希函数是整个过程的关键点,但,也是“痛点”。

哈希的痛

在哈希表中,键的存储不是挨个放的,是经过哈希函数计算得来。

前面提到,哈希表的输入远大于输出,且一个key对应的位置只存放一个value,那么有可能发生什么情况?

不同的 key 经过计算落到了同一个位置上,就与预期相违背了,这种情况称作“哈希冲突”。

微信图片_2025-12-14_181639_311.png

产生冲突后,会对操作产生直接影响,哈希表的优势就大打折扣。怎么办?

比较容易想到的是,哈希表容量越大,不同 key得到同一个结果的概率就越低,冲突就越少,所以,可以通过扩容来减少冲突。

但是这种方法不提倡轻易使用,因为哈希表扩容需要进行大量的数据搬运与值计算,效率太低。

需要采取一些策略来弥补效率上的损失:

1、仅在必要的时候进行扩容。

2、改良数据结构,使哈希表在出现冲突时仍能正常工作。

问题来了,什么是必要的时候?

负载因子与动态扩容

负载因子(load factor)的定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度

简单理解:当负载因子超过某个阈值(通常是 0.5 到 0.8 之间),表示哈希表要“超重”了,需要创建一个更大的哈希表(通常是当前大小的 2 倍),然后重新插入所有现有的键值对,这个过程称为 “重哈希”

这种方式相比“立即扩容”进行了一定的优化,不过结果还是造成了内存占用的增加。 那有没有可以不扩容,原地优化的方法?

链式地址与开放寻址

听起来高大上,其实简单,想想日常,我们有东西要存到柜子里,但柜子不是空的,怎么办?

1、放得下就继续放,一个柜子塞多个

2、从它旁边再找一个空柜子

链式地址对应方法1,将原本一个位置只存储一个值的方式,改为一个地址存储一串值,即一个“链表”。

开放寻址对应方法2,不引入新的数据结构,而是开放整个存储空间,发现冲突后重新找一个能存放的地址。

这两种方案都有多种实现策略,但它们也不是完美的,有各自的优缺点,同时,它们的存在是为了保证哈希表在发生冲突时仍正常工作,其实是“绕开”了问题,而不是解决问题。

那么,怎样能解决?

哈希算法

既然冲突的来源是“计算”,解决问题的根本也就是“计算”。

优秀的哈希算法,应具备以下特点:

  • 确定性:相同的输入始终产生相同的输出。
  • 效率高:计算过程足够快,开销足够小。
  • 均匀分布:键值对尽可能地均匀分布。

在实际中,我们通常不会自己再开发一套哈希算法,而是用一些标准算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。

MD5 常用于校验文件完整性,比如软件安装包的哈希值;SHA-2 常用于安全应用与协议,比如生成数字签名;Git使用SHA-1哈希来唯一标识每一次提交。

近一个世纪以来,研发人员都在对哈希算法进行不断地升级优化,要么提升性能,要么提升安全性,那么编程语言都是怎么选择的呢。

编程语言的选择

各种编程语言采取了不同的哈希表实现策略。

  • Python 采用开放寻址。字典 dict 使用伪随机数进行探测。
  • Java 采用链式地址。当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会转换为红黑树以提升查找性能。
  • Go 采用链式地址。规定每个桶最多存储 8 个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。
  • JavaScript 引擎 V8 中的 Map 并非使用简单的传统哈希表,而是使用了一个称为 OrderedHashMap 的数据结构。

小结

越高级的算法,涉及的细节就越多,同时离实际应用场景也越近。

笔者本文所述尽量全面易懂,但不一定全部准确,需要注意,这是你学习哈希算法的起点,而不是终点,欢迎留言交流探讨。

更多好文第一时间接收,可关注公众号:“前端说书匠”

❌