阅读视图

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

还在重复下载资源?HTTP 缓存让二次访问 “零请求”,用户体验翻倍

🚀 性能优化的“节流大师”:HTTP 缓存机制,让你的网站快到飞起!

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

在上一篇中,我们学会了如何利用资源提示符(Resource Hints)让浏览器“未卜先知”,提前准备资源。但如果用户已经访问过我们的网站,我们还能不能更快?当然可以!

今天,我们要聊的是前端性能优化的终极“节流大师”——HTTP 缓存机制。它的核心目标是复用资源、减少延迟、节省带宽,让用户在二次访问时,几乎可以瞬间打开页面。


⚠️ 缓存机制的运行原理

HTTP 缓存就像是浏览器给自己准备的一个“百宝箱”。浏览器在请求资源前,会先检查这个百宝箱。

整个缓存机制由一系列 HTTP 头字段(如 Cache-ControlExpiresETagLast-Modified)共同决定,并可分为强缓存协商缓存两大类。

  • 首次请求: 服务器提供资源及缓存策略。

  • 再次访问: 浏览器依据策略决定是否使用缓存。

    • 命中强缓存 → 无需请求服务器,直接使用本地资源。
    • 命中协商缓存 → 向服务器发送验证请求,确认资源是否可用。

image.png

✨ 强缓存机制:最快的“秒开”体验

强缓存是性能优化的最高境界。当缓存未过期时,浏览器直接使用本地资源无需发送任何网络请求

核心字段:Cache-Control 与 Expires

强缓存主要依赖两个 HTTP 头部字段:Cache-Control(HTTP/1.1 规范)和 Expires(HTTP/1.0 规范)。

优先级: Cache-Control > Expires

Cache-Control 指令:现代缓存的“指挥官”

Cache-Control 提供了更灵活、更强大的缓存控制能力:

指令 含义 场景
max-age=31536000 缓存有效期(秒),例如缓存一年 静态资源
public 允许代理服务器和浏览器缓存 所有人可见的资源
private 仅允许用户浏览器缓存 包含用户信息的资源
no-cache 不使用强缓存,进入协商缓存阶段 主入口文件(如 index.html
no-store 完全禁用缓存,每次都从服务器获取 敏感或动态数据
immutable 资源永久不变,浏览器无需重新验证 带版本号的静态资源
// 最佳实践:版本化静态资源
Cache-Control:public, max-age=31536000, immutable

应用场景: 带有版本号的静态资源(如 app.v1.jslogo.v2.png),一旦文件内容改变,版本号也会变,从而绕过强缓存。

Expires 头部字段:过时的“日历”
  • 格式: Expires: Tue, 09 Nov 2024 21:09:28 GMT
  • 问题: 指定绝对过期时间,但它依赖于客户端本地时间。如果用户修改了本地时间,就可能出现时钟误差,导致缓存失效或误用。

⚠️ 注意: 在现代 Web 中,Expires 已被 Cache-Control 替代。当两者同时存在时,Cache-Control 优先级更高。

🔄 协商缓存机制:谨慎的“验证官”

当强缓存失效(过期)或被禁用(Cache-Control: no-cache)后,浏览器会进入协商缓存阶段。

此时,浏览器会向服务器发送验证请求,服务器根据资源状态决定是返回缓存还是新内容:

  • 304 Not Modified → 资源未修改,浏览器使用本地缓存。
  • 200 OK → 资源已修改,服务器返回新内容。

Last-Modified / If-Modified-Since:基于时间的验证

这是最原始的协商缓存方式:

  1. 服务器响应: 附带资源最后修改时间:Last-Modified: Mon, 10 Nov 2025 12:00:00 GMT
  2. 浏览器请求: 再次请求时,携带:If-Modified-Since: Mon, 10 Nov 2025 12:00:00 GMT
  3. 服务器比较: 如果资源未修改,返回 304 Not Modified

⚠️ 缺点: 只能精确到秒。如果在 1 秒内资源被修改了多次,或者服务器时间与文件系统时间不一致,可能导致误判

ETag / If-None-Match:基于指纹的验证

ETag(Entity Tag)是资源的唯一指纹内容的哈希值,是更精确的验证方式:

  1. 服务器响应: 附带资源指纹:ETag: "filehash123"
  2. 浏览器请求: 再次请求时,携带:If-None-Match: "filehash123"
  3. 服务器比较: 比较哈希值,如果一致,返回 304 Not Modified

优点: 精度更高,解决了 Last-Modified 的时间误差问题。 优先级: ETag 高于 Last-Modified

🔧 Service Worker:可编程的缓存策略

HTTP 缓存是被动的,它依赖于服务器的 HTTP 头部配置。而 Service Worker 则提供了一种主动、可编程的缓存方案。

Service Worker 是一个独立于网页运行的后台脚本,它可以拦截网络请求,并结合 Cache API 实现完全自定义的缓存逻辑,从而实现离线访问动态缓存更新

Service Worker 常见的缓存策略:

策略名称 核心逻辑 优势 典型适用场景
Network First 先请求网络 → 网络失败则用缓存 优先获取最新内容,离线有兜底 新闻列表、动态数据(需最新)
Cache First 先读缓存 → 缓存无则请求网络并更新缓存 加载速度快,减少网络请求 静态资源(图片、CSS、JS)
Stale-While-Revalidate 先返回缓存旧资源 → 后台请求更新缓存 速度快,兼顾资源新鲜度 用户头像、非核心静态资源

Service Worker 示意图

✅ 最佳实践:缓存策略的“组合拳”

合理的缓存策略是性能优化的基石,我们应该根据资源的特性,打出“组合拳”:

  1. 静态资源(带版本号): 采用最强缓存。

    • Cache-Control:max-age=31536000, immutable
  2. 主入口文件(如 index.html): 禁用强缓存,启用协商缓存。

    • Cache-Control:no-cache (确保每次都能获取最新的 HTML,从而加载最新的 JS/CSS 版本)
  3. 动态数据(API): 按业务灵活配置。

    • 敏感数据:no-store
    • 可容忍延迟数据:max-age
  4. 离线与复杂策略: 使用 Service Worker 实现更高级的缓存控制。


下一篇预告: 缓存解决了“二次访问”的速度问题,但对于全球用户来说,“首次访问”的速度依然依赖于物理距离。下一篇我们将探讨如何利用内容分发网络(CDN) ,将资源部署到离用户最近的地方,实现真正的全球加速!敬请期待!

webpack编译中为什么不建议load替换ast中节点删除consolg.log

在 Webpack 编译过程中,不建议仅通过 loader 直接替换 / 删除代码中的 console.log核心原因是「loader 替换的局限性」会导致代码稳定性、可维护性、调试效率受损,且存在更优的替代方案。以下从「核心问题、场景风险、正确方案」三个维度详细解析:

一、loader 替换 console.log 的核心问题

Loader 的核心作用是转换模块的源代码(如 ts→js、less→css),其设计逻辑是 “按模块处理、文本替换”,但用它处理 console.log 会暴露以下致命问题:

1. 语法鲁棒性极差:易破坏合法代码

console.log 的写法高度灵活(参数、嵌套、动态调用),loader 基于 “正则 / 简单字符串替换” 无法精准识别,极易误改合法代码:

// 1. 合法场景被误替换
const console = { log: (msg) => alert(msg) }; // 自定义 console 对象
console.log("正常业务逻辑"); // loader 替换后变成 undefined.log(...),直接报错

// 2. 嵌套场景替换不彻底
if (debug) {
  console.log("调试信息", { a: 1, b: 2 }); // 正则仅替换 console.log 会残留参数,导致语法错误
}

// 3. 动态调用场景失效
const log = console.log;
log("动态调用的日志"); // loader 无法识别,漏替换

本质问题:loader 是 “文本层面” 的替换,而非 “语法层面” 的分析,无法区分「原生 console.log」和「自定义 console 对象 / 动态调用」,最终导致代码语法错误或逻辑异常。

2. 破坏开发 / 生产环境的一致性

  • 开发环境:需要保留 console.log 用于调试;
  • 生产环境:需要移除 console.log 减少体积、避免信息泄露;

若用 loader 硬编码替换,需在 webpack.config.js 中区分环境配置,且 loader 一旦配置错误,会导致:

  • 开发环境日志被意外删除,调试效率骤降;
  • 生产环境日志未被删除,泄露敏感信息(如接口参数、用户 ID)。

3. 无法精细化控制:一刀切的替换不灵活

实际项目中,console.log 并非 “全删 / 全留”,而是需要精细化控制:

  • 保留 console.error/console.warn(生产环境需监控错误);
  • 仅删除开发调试的 console.log,保留业务关键日志;
  • 按环境 / 模块 / 日志级别区分(如测试环境保留、生产环境删除);

loader 基于简单替换无法实现上述逻辑,只能 “全替换” 或 “全不替换”,灵活性为 0。

4. 性能损耗:增加编译开销

Loader 处理每个模块时都要执行正则匹配 / 替换,项目越大(模块越多),编译时间越长;而专业的优化插件(如 terser-webpack-plugin)是在 “代码压缩阶段” 批量处理,效率远高于 loader 逐模块替换。

二、更致命的场景风险

  1. 第三方依赖被误改:loader 会处理所有模块(包括 node_modules),若第三方库中使用 console.log 做关键逻辑(如调试、错误提示),被替换后会导致依赖功能异常,且难以排查;
  2. SourceMap 错位:loader 替换代码后,行号 / 列号与原始代码不一致,生产环境报错时,SourceMap 无法精准定位问题,增加调试难度;
  3. 无法回滚:一旦 loader 配置上线,若发现替换错误,需重新编译发布,而插件方案可通过配置快速开关,成本更低。

三、Webpack 中处理 console.log 的正确方案

核心原则:在代码压缩 / 优化阶段处理,而非 loader 转换阶段,以下是行业主流方案:

1. 生产环境:用 TerserWebpackPlugin 精准移除(推荐)

terser-webpack-plugin 是 Webpack 内置的代码压缩插件(替代旧的 uglifyjs-webpack-plugin),支持语法层面分析,可安全移除 console,且支持精细化配置:

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            // 核心配置:移除 console
            drop_console: true, // 全量移除 console(包括 log/error/warn)
            // 精细化配置:仅移除 log,保留 error/warn
            // pure_funcs: ['console.log'] 
          },
          // 保留 SourceMap,避免报错定位错位
          sourceMap: true 
        }
      })
    ]
  }
};

优势

  • 语法分析:仅移除原生 console.log,不影响自定义 console 对象;
  • 批量处理:压缩阶段一次性处理,编译效率高;
  • 灵活配置:可按日志级别区分保留 / 删除;
  • 不影响第三方依赖:默认仅处理业务代码(可通过 test 配置范围)。

2. 开发 / 测试环境:用 ESLint 约束(而非删除)

开发阶段无需删除 console.log,但可通过 ESLint 提示 / 禁止调试日志,避免提交到生产环境:

// .eslintrc.js
module.exports = {
  rules: {
    // 警告:提示开发者删除 console.log
    'no-console': ['warn', { allow: ['error', 'warn'] }],
    // 或严格模式:禁止所有 console(仅生产环境启用)
    // 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  }
};

3. 按需控制:用环境变量 + 封装日志函数(最佳实践)

项目中封装统一的日志工具,按环境控制日志输出,从根源避免 console.log 泛滥:

// src/utils/logger.js
const logger = {
  log: (...args) => {
    if (process.env.NODE_ENV !== 'production') {
      console.log('[LOG]', ...args);
    }
  },
  error: (...args) => {
    console.error('[ERROR]', ...args); // 生产环境保留错误日志
  }
};

module.exports = logger;

业务代码中使用:

import logger from './utils/logger';

logger.log('调试信息'); // 生产环境自动不输出
logger.error('业务错误'); // 生产环境保留

优势

  • 完全可控:按环境 / 日志级别灵活调整;
  • 无编译风险:不修改原生代码,仅控制输出;
  • 便于扩展:可接入日志监控平台(如 Sentry)。

四、总结:为什么 loader 是错误选择?

维度 loader 替换 TerserPlugin + 封装日志
语法安全性 极低(易误改代码) 极高(语法层面分析)
灵活性 无(一刀切) 高(按级别 / 环境配置)
编译性能 差(逐模块替换) 优(压缩阶段批量处理)
可维护性 差(配置分散) 优(集中配置,易开关)
调试友好性 差(SourceMap 错位) 优(保留 SourceMap)

核心结论

loader 的设计目标是 “转换代码格式”,而非 “优化代码逻辑 / 删除日志”;处理 console.log 应交给「代码压缩插件」(生产环境)+「日志封装函数」(全环境),既保证代码稳定性,又能灵活控制日志输出。

Agent时代下,ToB前端的UI和交互会往哪走?

1. 前言

最近在负责 Agent 项目前端部分的落地,深感 Agent + ToB 不仅仅是功能的叠加,更是一个需要前端开发、交互设计与业务逻辑深度融合的全新领域。因此,我想通过这篇文章梳理一下架构设计层面的思考。

在过去二十年里,ToB软件已经形成了一套成熟且稳固的交互范式(GUI图形用户界面)。为了承载复杂的业务逻辑,业界构建了深度的树状导航、高密度的业务逻辑和繁复的表单系统。这种设计通过将业务流程抽象为标准化的功能模块,有效地解决了信息化的问题。

此时,用户与系统的关系是典型的 “人适应系统” :用户必须接受培训,记忆固定的操作路径,通过“点击 + 键盘”的机械指令来驱动软件。

近年,AI Agent 技术开始被引入 ToB 领域。但目前的普遍做法,仅仅是在现有界面上叠加一个聊天机器人(Chatbot)。但是,AI Agent 的出现不是为了在界面上加一个聊天框,而是为了打破“人去寻找功能”的旧模式。

未来,Agent 必然不再是一个边缘的辅助工具,而是系统的核心驱动力,可以深入介入用户的作业流,ToB 前端软件的人机交互应当如何重构?

本文分以下几个方面来深入探讨这个问题:

  • 静态界面变迁:静态渲染 => 混合渲染(生成式 UI + 静态渲染)
  • 人机交互变迁:功能导航 => 意图引导
  • 前端开发变迁:页面构建 => 原子组件 + 能力架构

最后,我还会加入一些杂七杂八但是重要的思考

以下内容纯属个人预测,如有指教请在评论区指正!


2. 定义:Agent是个啥?有啥用?

2.1 先定义:我说的Agent是什么?

我说的AI Agent不是我们熟知的聊天机器人或任务助手。

聊天机器人或者任务助手其本质是反应式的。它们在预设的规则或模型内,针对用户的明确指令执行单一、具体的任务 ,比如基于知识库的业务问答

而AI Agent则代表了一种主动式的、具备自主性的新形态(常用Cursor的朋友应该能够明白我在说啥)。  

一个真正的AI Agent具备以下核心特征:

  • 感知: 能够感知并处理其数字环境中的信息,包括视觉、文本和各类数据输入 。  
  • 规划: 能够将一个复杂、高阶的目标分解为一系列可执行的、具体的子任务 。这是其区别于普通LLM的关键能力。
  • 工具使用: 能够调用外部工具(如API、代码执行环境、数据库查询)来获取信息或执行动作,从而与外部世界进行交互。  
  • 记忆: 能够记忆过去的交互、行动和结果,并利用这些记忆来指导未来的决策,实现上下文的连续性和学习改进 。  

2.2 ToB软件对于AI Agent的核心诉求: 在复杂工作流程中提升用户生产力

ToB Web 应用对 AI 智能体的需求,可以总结为在复杂工作流程中提升用户生产力

引入AI Agent的最终目标,并非仅仅加速单个任务,而是从根本上重构涉及业务整体工作流 。这一理念的转变,要求前端交互设计需要进行变革。前端不再仅仅是用户与单一功能的交互界面,而是用户与一个持续进行的、复杂的业务流程进行协作的平台。  


3. 静态界面变迁: 从静态渲染 => 混合渲染(生成式 UI + 静态渲染)

Agent所代表的生成式 UI 并非要消灭现有的菜单和页面,而是作为一种高维度的补充。它让软件能够像乐高积木一样,根据用户的即时需求,实时重组最适合的交互形态。

3.1 静态页面的“确定性”与意图的“无限性”

1、静态渲染的局限性:被“确定性”锁死的路由

现有的 ToB 架构建立在 “确定性路由” 之上。前端工程师预先定义好 /dashboard/report 等路由,并在每个路由下硬编码固定的组件布局。这种模式的前提是:用户的需求是可枚举的,操作路径是线性的。

这种模式有两个缺陷:

  • 路径冗余:用户想要完成一个跨模块的任务(例如:“把 A 项目的数据经过 B模块的处理后导出”),往往需要在三四个页面间跳转、复制、粘贴。
  • 僵尸组件:为了覆盖长尾需求,ToB 软件的页面里塞满了平时根本用不到的高级筛选器和按钮,导致界面臃肿。

然而,Agent 的核心价值在于处理发散性意图。当用户提出“请分析上季度华东区销售异常的原因,并给出整改建议”时,这一需求涉及数据查询、图表分析、文本推理等多个维度。传统的静态页面无法预知并覆盖此类组合式需求。若强行通过跳转多个页面来解决,会让用户很不爽。

2、生成式 UI 的核心逻辑: 图形界面(GUI)和语言界面(LUI)的融合

生成式 UI 指的是:系统根据对用户意图的理解和当前上下文,在运行时动态决策并渲染出的用户界面。

在 Ant Design X 的范式中,这被称为“富交互组件的流式渲染”。其工作流包含三个阶段:

  • 意图识别 :Agent 识别用户需求,判断单纯的文本回复不足以解决问题,需要调用特定的 UI 组件。
  • 结构化描述 :Agent 输出一段包含组件类型和Props的 JSON 描述
  • 动态渲染 :前端接收 Schema,映射到本地的“原子组件库”,在对话流中即时渲染出可交互的卡片。

未来ToB软件的界面,可能不再是密密麻麻的菜单和按钮。而是一个全局的、智能的 语言用户界面(LUI) 将成为最高优的交互入口。用户可以通过自然语言直接下达指令,而GUI则会变成为:

  • 歧义消除与能力展示区:  当Agent对指令理解不清时,会通过GUI提供选项让用户选择。同时,GUI也直观地展示了当前软件“能做什么”,为用户提供输入指令的灵感。
  • 过程的监控区:  Agent的执行计划、步骤和状态,会以可视化的方式在GUI中呈现。 比如任务规划List(以流程图或任务列表的形式,展示Agent为了完成用户目标而制定的详细计划)、实时日志流(实时显示Agent正在执行哪一步、调用了哪个工具、工具返回了什么结果)。
  • 结果的呈现区:  Agent执行任务后的结果(如生成的报表、更新后的数据列表)依然通过确定的GUI来展示。

这标志着前端开发重心的转移:从构建静态页面转向定义组件能力

3.2 具体有哪些新的UI形态?

为了让 Agent 的抽象工作过程变得透明、可控和可信,一系列新的UI模式应运而生。这些模式可以根据其核心功能分为三大类 。

3.2.1 为了提高透明度和可观测性的新形态

这类模式的核心目标是打开Agent的“黑盒”,让用户能够理解其“思考”过程。

1、双栏视图

这是展示Agent思考过程的关键模式。界面被分为两栏,一栏实时展示Agent的内部推理过程;另一栏则展示其采取的具体行动。  

例子: Cursor的主界面一般被分成两块,一边是代码编辑区(具体行动);另外一边是思考和推理过程。

2、多智能体流程图

在由多个Agent协作的系统中,使用流程图或活动图来展示任务的分配和流转,清晰地标明哪个Agent在何时执行了何种操作。用户可以通过可展开的卡片深入了解每个Agent的具体贡献 。

例子: 秘塔AI搜索研究版\ image.png

3、来源引用与追溯

对于使用检索增强生成(RAG)技术的Agent,UI必须将生成的内容链接回其引用的原始文档或数据源。这极大地增强了信息的可信度,并允许用户进行事实核查 。  

例子: Gemini的深度研究,使用了大量展示数据源的交互。


3.2.2 为了更好地和Agent交互与协作的新形态

这类新UI和交互目的是将Agent的能力无缝地融入用户的工作流中。

1、协作画布

AI能力被直接嵌入用户的主要工作区,如设计工具或文档编辑器。用户通过行内建议、斜杠命令(/)或专用的AI功能块与Agent交互,整个过程无需离开当前的工作上下文,保持了心流体验 。  

例子: copliot中的自动代码补全

2、提示工程化表单

将复杂的自然语言提示(Prompt)工程抽象成结构化的表单,用户只需填写几个关键字段,系统就会在后台将这些输入转换成一个优化过的、高质量的提示,然后提交给Agent。这有效降低了用户使用AI的门槛 。

例子: 抖音“创作灵感”工具的主动引导。抖音的“AI脚本生成”功能早期面临用户输入模糊的问题(如用户仅输入“拍一条美食视频”)。通过“提示工程化表单”方案优化后,交互流程变为:

  1. 用户输入“拍一条美食视频”,意图分类模型判断明确度0.4(模糊)。
  2. 系统生成候选引导项:
  3. “选项1:家常菜教程(步骤清晰,适合新手)”
  4. “选项2:探店测评(突出口感描述,适合推荐)”
  5. “选项3:创意摆盘(视觉化强,适合短视频)”
  6. 用户点击“选项1”,系统自动补全提示词:“帮我生成一条家常菜教程的视频脚本,目标人群是厨房新手,需要包含食材准备、步骤拆解和小贴士。”
3、生成式UI

AI不仅填充数据,还能动态生成或修改UI组件本身。

例子: 用户可以要求一些办公软件中的Agent“在表格中增加一列,显示每个订单的利润率”,Agent会直接修改表格组件的结构;或者要求“生成一个展示用户增长趋势的图表”,Agent会在仪表盘上创建一个新的图表组件


3.2.3 增加用户对Agent的掌控的UI

1、人在回路

Agent的工作流在执行关键或高风险操作(如撤销git 暂存区内容时)之前会被明确地暂停。UI会清晰地展示Agent将要执行的动作及其可能带来的后果,并要求用户进行明确的批准后才能继续 。  

例子:Cursor 和 Copilot 在agent模式下,是可以自动执行终端命令并读取执行结果的的。默认情况下,绝大多数终端命令(尤其是删除、安装依赖等)都需要用户点击 "Run Command"

2、可编辑计划与“check point”回溯

在Agent开始执行任务之前,UI会展示其完整的行动计划。用户可以审阅、编辑、重新排序甚至取消计划中的某些步骤。

通过“check point”,用户可以“回溯”到Agent执行过程中的任一历史状态,并从该点开始探索一条不同的执行路径 。

例子:Gemini的研究模式 或者是 Cursor 的 plan模式\ image.png

  • 中断与恢复控制: UI必须始终提供一个明确的“停止”按钮,允许用户随时中断正在运行的Agent。中断后,UI应提供恢复、重试或放弃任务的选项 。

4. 人机交互变迁:“操作员” => “指挥官”

在过去几十年里, GUI(图形用户界面)将用户规训成了软件的 “操作员”。而在 Agent 时代,交互设计的终极目标是将人还原为 “决策者”

4.1 Agent时代的B端交互需要解放用户

4.1.1 交互逻辑:从“过程导向”到“结果导向”

在传统的 GUI(图形用户界面)中,交互是指令式的。 用户必须清楚地知道每一个步骤:点击菜单 A \to 选择子项 B \to 输入参数 C \to 点击确认。 这是一种“手把手教机器做事”的模式,对于一个有明确目的复杂工作流来说很繁琐

在 Agent 介入后,交互变成了意图式的。用户不再关注步骤,只关注结果:“帮我把这周异常的监控数据发给XXX”。系统内部自动完成了“检索数据 \to 过滤异常 \to 格式化报表 \to 查找联系人 \to 发送邮件”这一系列复杂工作流。

用户不再需要记忆软件的操作路径,只需要清晰地定义目标。

4.1.2 反馈机制:从“单向执行”到“双向对齐”

在 GUI中,反馈通常是二元的:成功 or 失败。机器默认用户是永远正确的,只管执行。

但在意图式交互中,由于自然语言天然存在模糊性(例如用户说“清理一下”,是指删除媒体文件还是清理一下数据库?),交互的重心从“执行”转移到了“对齐”。

包括但不限于以下反馈机制:

  • 授权:  用户可以设定Agent的权限范围,决定哪些操作它可以自动执行,哪些需要审批。 比如 Copliot 可以设置哪些命令行命令是不可以自动执行的。
  • “急停”与接管:  在Agent执行任务的任何时刻,用户都应该可以暂停任务,审查状态,甚至手动接管后续步骤。
  • 纠错与反馈:  当Agent执行出错或不符合预期时,用户可以方便地提供反馈,帮助Agent学习和改进。

这种交互更像人类之间的协作:听懂 \to 确认 \to 行动,而不是冰冷的 点击 \to 运行


4.2 具体有哪些新的交互模式?

交互的内核正从“指令驱动”向“意图驱动”迁移。面对 AI 输出的不确定性与生成特性,旧的“点击-响应”模式已捉襟见肘。为了解决这些新问题,衍生出了三大类全新的交互模式。

4.2.1 “协商式”交互:精准对齐意图

这类模式的核心是为了解决自然语言天然的“模糊性”。Agent 不再闷头执行指令,而是像一个经验丰富的助手,通过多轮沟通来“对齐”需求,确保做正确的事。

1、主动追问与澄清

当用户的指令太宽泛或缺少关键参数时,Agent 不应该“瞎猜”(那会带来不可控的风险),而应该采取兜底策略,主动抛出选项或反问,引导用户补全信息。

例子: Cursor的 Plan 模式
用户的编码意图不够清晰,方案有模糊的监视空间,plan模式就会主动向用户进行询问

2、上下文锚定

这是“LUI+ GUI”协同工作的典型场景。用户无需费力用语言去描述一个复杂的对象,只需通过“圈选”或“指代”来锁定上下文,再辅以语言指令。

例子: Photoshop 的 Generative Fill(创成式填充)
用户用套索工具圈选画面中的一只狗(GUI 操作),然后在输入框里敲下“换成一只猫”(LUI 操作),用户不用费劲描述“请把画面左下角坐标 (x,y) 处的那只黑色小狗...”,极大降低了表达成本。

4.2.2 “共创式”交互:人机实时协作

这类模式旨在打破传统交互“你输完指令、我慢慢跑进度条”的回合制感,让用户和 Agent 能够实时配合,实现人机“同频”。

1、流式输出与即时干预

大模型推理需要时间,界面除了用“打字机效果”缓解等待焦虑外,更关键的是要允许用户随时插嘴叫停

例子: Cursor的“Stop Generating”与实时纠偏

代码写的有问题,此时你不需要等它全写完再提意见,而是直接点停止,并且重新提交prompt,cursor会自动撤销原来有问题的代码生成。 这种机制把“事后验收”变成了“事中纠偏”,极大地压缩了试错成本。

2、预测性推荐

Agent 基于当前上下文和用户画像,预判你的下一步意图,并直接把“下一步动作”封装成按钮递到你手边。

例子: Cursor、Gemini、元宝啥的都有这个功能


4.2.3 “方便修正结果”的交互

AI 的产出往往是概率性的“半成品”。交互设计必须提供极低成本的修改能力,让用户通过简单的操作将结果调整至完美。

1、基于 GUI 的修正

用户不需要为了修改一点小细节而重新组织Prompt,而是直接在生成的 GUI 组件上动手改。Agent 能感知到这些修改,并自动同步到底层逻辑。

例子: Claude 的 Artifacts 预览修改

Agent 生成了一个网页按钮,颜色你不喜欢。你不需要打字说“把按钮改成深蓝色”,而是直接用右侧预览窗口里的取色器吸取一个蓝色。 此时,Agent 会自动重写左侧的代码以匹配你的视觉修改。这种“所见即所得”的修改方式,远比语言描述高效。

2、给用户选择题!

针对创意类或没有标准答案的任务,Agent 不做“填空题”而是做“选择题”。它一次性提供多个草稿,用户负责做决策。

例子: Midjourney 的 U/V 模式。

输入提示词后,系统先出 4 张缩略图。用户点击“V2”(基于第 2 张图生成变体),系统会保留第 2 张图的构图,仅在细节上微调。 这是一种典型的通过选择表达审美。在难以用语言描述清楚“具体哪里感觉不对”的时候,让用户做选择题是最高效的。


5. 前端开发的变迁:页面构建 => 原子组件 + 能力架构

前端开发的核心变迁可以用以下两个公式描述

传统模式: UI=f(State)UI = f(State)。状态变了,界面变。但 ff (渲染逻辑) 是写死的。

Agent 模式: UI=f(Intent,Context,Components)UI = f(Intent, Context, Components)。意图决定展示什么组件,上下文决定填充什么数据。

5.1 组件设计的变革:从“Props 定义”到“语义自述”

在传统的 Vue 或 React 开发中,我们定义一个组件(比如一个日期选择器),最关注的是它的 Props:需要传什么值,触发什么事件。这是写给 人类程序员 看的。

但在 Agent 体系下,组件的第一受众变成了 AI 模型。AI 不看源码,它需要知道这个组件“是干什么的?”、“数据结构是啥?”、“什么时候该用它?”。

因此,前端组件的开发标准增加了两个新维度:

  1. 语义描述: 你需要用自然语言告诉 Agent:“这是一个日期范围选择器,适用于需要筛选特定时间段数据的场景。”这种描述不再是注释,而是会作为系统提示词(System Prompt)的一部分实时喂给 AI。
  2. Schema 标准化: 组件的输入必须严格遵循 JSON Schema 标准。因为 LLM(大模型)输出的是结构化数据,前端必须确保组件能无缝“消化” AI 生成的配置参数,具备极强的鲁棒性,避免因参数错误导致页面崩溃。
  3. 唯一可寻址性:每个组件实例都应拥有一个稳定且唯一的标识符,Agent可以通过这个ID精确地引用它。
  4. API驱动的状态:组件的状态不仅应能通过用户的直接交互(如点击、输入)来改变,还必须能通过编程接口被读取和修改。例如,Agent需要能够通过调用API来对表格组件应用一个筛选条件。

这一转变意味着,前端组件开发不再仅仅是关于UI还原和交互处理,更是一项API设计工作。组件需要定义清晰的API供Agent调用,还需要能够文档化和测试其对Agent暴露的API。

5.2 前端数据通信的变革:从“请求-响应”到“流式驱动”

传统的 CRUD 应用中,数据流是离散的:发起请求 \rightarrow 等待 \rightarrow 拿到全量 JSON \rightarrow 渲染。

但在 Agent 交互中,这种模式会让用户体验崩溃。因为 AI 的推理和生成是缓慢的。如果等 10 秒钟才一次性刷出结果,用户早就关掉了页面。

前端的数据通信架构正在向 流式驱动 转型:

  • 增量渲染能力: 前端不能傻等完整的 JSON。当 Agent 刚刚吐出 {"type": "chart", "tit... 时,前端就应该渲染出一个带有标题骨架的图表占位符。随着数据流 ...le": "Q3 Revenue"} 的到达,标题被实时填入。
  • 状态的颗粒度更细: 我们需要处理一种全新的状态—— “思考中” 。这不同于传统的 loading。Loading 是单纯的转圈,而 Thinking 需要展示 Agent 正在调用的工具、正在阅读的文档,甚至允许用户在这个过程中打断。

这对前端的状态管理库提出了很高的要求:它必须能像处理水流一样处理数据,而不是像处理砖块一样。

5.3 路由系统的变革:从“URL 映射”到“意图路由”

传统前端应用中/user/detail/xxx等 严格对应着一个组件树。

但在混合渲染的架构中,URL 的统治力开始下降。当用户说“看看张三的绩效”时,系统可能不需要跳转 URL,而是直接在当前的对话流中“挂载”一个绩效卡片组件。

前端的路由逻辑发生裂变:

  • 宏观路由(保留): 依然使用 URL 管理大的模块(如“工作台”、“设置页”),保证浏览器的刷新和回退机制正常工作。
  • 微观路由(新增): 在页面内部,引入一个 “意图导航”。它根据 AI 识别出的意图,动态决定在当前视图中加载哪个组件。

这意味着前端架构必须支持动态组件注册沙箱隔离,防止 AI 随意生成的组件把主应用搞挂。

5.4 容错设计的变革:从“防止代码逻辑错误”到“给Agent的幻觉进行容错”

传统前端的主要捕获代码逻辑错误(比如 undefined is not a function)。

现在的挑战在于:代码没报错,但 AI一本正经地胡说八道。 比如,AI 返回了一个不存在的枚举值,或者生成了一段格式错误的 JSON。

前端开发必须构建一层容错机制:

  • 校验: 在渲染 AI 生成的内容前,必须进行严格的 Schema 校验。
  • 降级: 如果 AI 想要渲染一个复杂的图表但参数错了,前端应该能自动降级显示为纯文本,或者是提示用户“数据解析失败,请重试”,而不是直接白屏。
  • 可控: 在 UI 上提供“重新生成”或“手动编辑”的入口,把最终的纠错权交还给用户。

5.5 总结

前端并没有死,它只是向上进化了。

我们不再专注于于像素级的对齐、编写确定的组件(这些 AI 已经能写得很好了),而是开始关注组件的语义、数据流的实时性、以及系统的鲁棒性

我们将从构建界面的工程师,转变为构建 AI 运行时环境的工程师。这才是 Agent 时代前端开发的真正价值所在。


6. 一些系统性的思考问题

6.1 Agent进步那么快,会话式交互(自然语言LUI)会完全取代GUI吗?

不会!

所有交互创新都是为了这一个目的:“让输入更简单,让结果更易懂”。

Agent时代的到来似乎冲昏了一些产品或者交互的头脑?认为 LUI(语言交互)会取代 GUI。

我现在可以举一个反例:在游戏过程中,你想让飞机升高10米,实际上用上下左右的按键会比在聊天界面输入“这台飞机马上升高10米”更高效得多。

1、GUI 的本质:直接操作
  • 定义: 用户直接作用于屏幕上的对象。比如:你看到一个滑块,你拖动它;你看到方向键,你点击它。

  • 心理模型: “手眼协调” 。它模拟的是物理世界(比如开车、开飞机)。

  • 优势:

    • 低认知负荷: 是Recognition,看到按钮就知道能干嘛,不需要记忆指令。
    • 零延迟反馈: 按下即响应,形成了毫秒级的“感知-行动”闭环。
    • 高确定性: 按钮点了就是对应某个动作,不存在歧义。
2、LUI (自然语言) 的本质:代理委托
  • 定义: 用户通过语言描述意图,中间层(Agent)理解并执行。

  • 心理模型: “人际交流” 。它模拟的是你跟一个副驾驶说话。

  • 优势:

    • 无限的表达空间: 按钮数量有限,语言的组合无限。
    • 高抽象能力: 可以处理复杂的逻辑(比如“把所有电量低于 20% 的无人机召回”)。
3、案例分析

案例:在游戏过程中,你想让飞机升高10米,实际上用上下左右的按键会比在聊天界面输入“这台飞机马上升高10米”更高效得多。

刚才“飞机升高”案例,为什么 GUI 完胜? 因为 “空间运动”是低维度的、物理直觉的任务。 人脑对空间的理解是直接的,不需要经过“语言中枢”的转译。

  • GUI 路径: 眼睛看到位置 -> 大脑空间计算 -> 手指移动。(快,生物本能)
  • LUI 路径: 眼睛看到位置 -> 大脑空间计算 -> 转化为语义("升高") -> 量化数值("10米") -> 组织语言 -> 输入 -> AI 解析 -> 执行。(慢,且容易累)
4、方法论

到底是GUI好还是LUI好?以下给出两个维度进行评估:

维度 GUI (图形交互) LUI (自然语言/AI)
操作粒度 原子级操作。适合微调、实时控制。(如:向左偏一点点) 意图级操作。适合批处理、复杂指令。(如:按顺时针巡检A区)
学习曲线 初学者难,专家快。需要学习界面布局,但熟练后可形成“肌肉记忆”。 初学者快,专家慢。无需学习(会说话就行),但无法形成肌肉记忆,每次都要组织语言。
信息带宽 输入低,输出高。鼠标只有一个坐标,但屏幕能显示千万像素。 输入高,输出低。一句话包含丰富信息,但文本输出往往难以一目了然。
容错率 低风险。通常有限制,难以产生严重越界操作。 高风险。可能因为歧义(幻觉)导致灾难性后果(如 AI 误解了“降落”和“坠落”)。

回到刚才的那个原则:“让输入更简单,让结果更易懂”。

  • 对于在游戏中移动飞机:推摇杆是输入最简单(符合直觉),看飞机动了是结果最易懂(视觉反馈)。
  • 对于数据查询:说句话是输入最简单(省去筛选),看生成的图表是结果最易懂。

真正高效的交互应该应该采用 GUI + LUI 的方式:

  • 保留专业的控制面板或者图表(GUI),用于高频、实时、精细操作。

  • 嵌入“副驾驶” (LUI):用于跨菜单调用功能、复杂数据分析等意图级操作。

  • 关键点: AI 的输出,最好是 GUI 组件。

    • 错误做法: 用户问“表中哪个设备目前待更换?”,AI 回答文字“3号飞机待更换”。
    • 正确做法: 用户问“表中哪个设备目前待更换?”,AI 直接把表格上的 3 号设备高亮闪烁,并在侧边栏弹出它的详细状态卡片。

6.2 交互元素的“考古分析”

image.png

要讲计算机上的人机交互变革,肯定离不开施乐+苹果当年发扬光大的GUI。GUI和NUI在现在似乎稀松平常,但是当年实际上隐含着很多深层的思考和价值,这极大地改变了我们现在的世界。

很有必要考考古,才能指导未来的设计!

6.2.1 PC GUI

所以要讲Agent的交互元素的思考,肯定要先讲清楚GUI时代的内容

1、Icon 图标
  • 思考: 在命令行(CLI)时代,用户必须记住命令(如 deleterm)。图标的出现利用了人类从物理世界继承来的视觉经验——比如用一个“垃圾桶”代表删除,用一个“放大镜”代表搜索。这本质上是一种语义的视觉压缩
  • 价值: 它是认知负荷的极度减负。它建立了一座通用的桥梁,跨越了语言障碍(不识字的人也能看懂图标),让计算机的操作从“背诵语法”变成了“看图说话”,极大地降低了数字世界的准入门槛。
2、 Menu 菜单
  • 思考: 软件的功能往往成百上千,如果全部铺在屏幕上,用户会崩溃。菜单本质上是一种信息的分类 。它通过层级结构(文件、编辑、视图)将混乱的功能有序折叠。它解决的是 “有限屏幕”与“无限软件功能”之间的矛盾
  • 价值: 它提供了可发现性安全感。用户不需要预先知道软件能做什么,只要打开菜单逛一逛就能知道。它为用户建立了一张“功能地图”,让用户在面对庞大系统时,能通过逻辑推演找到目标,而不是盲目乱撞。
3、Pointing 指点设备(鼠标/光标)
  • 思考: 在键盘交互时代,操作是线性的、离散的。而鼠标引入了二维空间坐标的概念。它将人类的手部动作(物理位移)映射到屏幕上的光标运动(虚拟位移)。这引入了费茨定律 :操作效率取决于目标的大小和距离。它是“手眼协调”在数字世界的第一次完美投射。
  • 价值: 用户可以跳过屏幕上的任何内容,直接随机访问任何他想操作的那个像素点,而不必像填写表格一样按 Tab 键逐个切换。
4、Windows 窗口
  • 思考: 在单任务时代(DOS),屏幕就是全部。窗口的发明,创造了 **“屏幕中的屏幕” ** 。它模拟了物理世界中的“办公桌桌面”:你可以同时摊开好几份文件,有的在上面,有的被压在下面。它在心理学上契合了人类的 工作记忆 模型——我们需要同时关注多个相关联的信息源。
  • 价值: 它实现了多任务并行与信息隔离。用户可以在一个窗口看文档,在另一个窗口写代码,无需频繁切换上下文导致思路中断。它通过空间复用,让有限的显示器承载了逻辑上无限的信息流。
5、Toast 轻提示:
  • 思考: 弹窗 model 会强行打断用户,强迫用户点击“确定”。
  • 价值: 它是非阻断式的反馈。它承认“用户的注意力很宝贵”,只在视觉边缘告知结果(如“保存成功”),让交互的主流程不被打断。

6.2.2 移动互联网NUI(触控与感知)

在移动端,屏幕变小了,鼠标消失了,交互从“指向”变成了“直接操纵”。

1、手势(滑动/捏合):
  • 思考: PC GUI里的滚动条是一个独立的控件,用户需要把鼠标移过去拖动它。而手势让用户直接作用于内容本身
  • 价值: 它是“动作”与“结果”的零距离融合。用户想翻页,直接拨动页面,而不是拨动控制器(滚动条)。这极大降低了操作的抽象程度。
2、下拉刷新
  • 思考: 这是一个极其经典的发明。它利用了自然的物理隐喻(像拉动弹簧一样)。
  • 价值: 它创造了一个**“无界面”的按钮**。用户不需要寻找“刷新”图标,而是通过一种符合直觉的动律(Ritual)来触发更新,同时通过回弹动画告知用户“系统正在处理”。
3、侧滑操作(列表项左滑/右滑)

插一句,玩过“探探”的朋友们有没有想过左滑右滑不感兴趣的匹配对象的交互逻辑?以下解析应该可以帮助你们

  • 思考: 在 PC GUI 时代,处理列表数据(如邮件)通常需要三步:1. 选中某一行;2. 移动鼠标去找工具栏上的“删除”图标;3. 点击确认。而侧滑操作利用了 “扔掉” 的物理隐喻。
  • 价值: 它是高频决策的极速通道。它将“选择对象”与“执行动作”合二为一。用户不需要把视线从内容(邮件标题)移开去寻找按钮,手指一滑即处理。这种交互极大地提升了信息筛选和清理的效率(如 探探 的左滑右滑,Mail 的滑动归档)。
4、Haptic Touch 长按呼出
  • 思考: 移动设备没有鼠标右键,屏幕只有二维平面(X轴和Y轴)。长按操作引入了时间维度(或压力维度,如 3D Touch),模拟了Z轴的深度。它类似于在物理世界中“注视”或“用力按压”一个物体以探究其内部结构。
  • 价值: 它是渐进式的信息披露。它允许界面保持简洁(不展示复杂的菜单按钮),但又隐藏了深度的操作选项(如快捷方式、预览)。它解决了移动端“屏幕太小”与“功能太多”的矛盾,让用户在不离开当前页面的情况下获取上下文信息。
5、无限滚动(瀑布流)
  • 思考: WIMP 时代的网页浏览基于“分页器”,看完了要点“下一页”,这强迫用户进行一次“是否继续”的理性决策,打断了体验。而无限滚动创造了 “流(Stream)” 的隐喻,内容像水流一样源源不断。
  • 价值: 它创造了沉浸式的无意识消费。消除了“翻页”带来的认知中断,极大地延长了用户停留时间。对于内容型产品,这是将用户留存率最大化的“交互黑洞”。
6、底部的导航栏
  • 思考: PC GUI的导航栏通常在顶部或左侧(为了配合 F 型阅读顺序)。但移动设备的交互核心限制是 “拇指热区”。底部导航栏是对人体工程学的妥协与尊重。
  • 价值: 在高频切换的核心功能区,用户不需要调整握持姿势,拇指仅需微动即可完成路由跳转。这是硬件形态(单手握持)反向定义软件交互的典型案例。

【节点】[Adjustment-ReplaceColor节点]原理解析与实际应用

【Unity Shader Graph 使用与特效实现】专栏-直达

ReplaceColor节点是Unity ShaderGraph中Artistic类别下的重要颜色调整工具,能够将输入颜色中的指定颜色值替换为目标颜色,并通过参数控制实现平滑的过渡效果。该节点在游戏开发、影视制作和UI设计等领域应用广泛,为开发者提供了强大的颜色处理能力。

节点的核心功能基于输入颜色与源颜色之间的距离计算,在特定范围内实现颜色替换。与简单的颜色替换不同,该节点引入了Range和Fuzziness两个关键参数,使颜色替换不再是生硬的切换,而是能够创建自然的渐变过渡。

在实际应用中,ReplaceColor节点常用于以下场景:

  • 替换材质中的特定颜色元素
  • 实现色键效果(绿幕抠像)
  • 动态调整游戏元素的颜色主题
  • 创建特殊视觉效果和风格化渲染

该节点属于ShaderGraph的Artistic类别,在默认颜色模式下会显示对应的类别颜色标识,便于开发者快速识别节点类型。

端口与参数详解

ReplaceColor节点包含六个主要端口,每个端口都具有特定的功能和作用。

输入端口配置

In端口

  • 类型:Vector 3
  • 绑定:无
  • 描述:作为颜色替换操作的输入源,可连接纹理采样节点、颜色节点或其他颜色计算节点的输出。该端口接收RGB颜色值,通常来自材质的基础纹理或计算得到的颜色结果。

From端口

  • 类型:Vector 3
  • 绑定:Color
  • 描述:定义需要被替换的目标颜色值。该端口通常连接到颜色属性或固定的颜色值,用于指定在输入颜色中寻找的特定颜色。

To端口

  • 类型:Vector 3
  • 绑定:Color
  • 描述:定义替换后的目标颜色值。当输入颜色与From颜色匹配时,将使用此颜色值进行替换。

Range端口

  • 类型:Float
  • 绑定:无
  • 描述:控制颜色匹配的容差范围。该参数决定了在From颜色周围多大的颜色范围内进行替换操作,数值越大,替换的颜色范围越广。

Fuzziness端口

  • 类型:Float
  • 绑定:无
  • 描述:软化选择范围周围的边缘,实现平滑过渡效果。通过调整此参数可以避免替换边缘出现锯齿现象,创建自然的颜色渐变。

输出端口配置

Out端口

  • 类型:Vector 3
  • 绑定:无
  • 描述:输出经过颜色替换处理后的最终结果。该端口可连接到主节点的颜色输入或其他后续处理节点。

核心算法解析

ReplaceColor节点的内部实现基于颜色距离计算和线性插值算法。深入理解其核心算法对于有效使用该节点至关重要。

颜色距离计算

节点首先计算输入颜色(In)与源颜色(From)之间的欧几里得距离:

float Distance = distance(From, In);

该距离值决定了当前像素颜色与目标替换颜色的相似程度。距离越小,说明颜色越接近,替换效果越明显。

插值因子计算

获得颜色距离后,节点通过以下公式计算插值因子:

float factor = saturate((Distance - Range) / max(Fuzziness, 1e-5));

此计算确保在Range范围内的颜色会被完全替换,而在Range到Range+Fuzziness范围内的颜色会产生平滑过渡。

最终输出计算

通过lerp函数在目标颜色(To)和输入颜色(In)之间进行插值:

Out = lerp(To, In, factor);

当factor为0时,输出完全使用To颜色;当factor为1时,输出保持原始In颜色;在0到1之间时,输出为两种颜色的混合结果。

参数调节技巧

Range参数调节

Range参数决定颜色替换的敏感度范围:

  • 小数值(0-0.1):仅替换与From颜色几乎完全相同的像素,适用于精确颜色匹配
  • 中等数值(0.1-0.3):替换相似颜色范围,适用于大多数常规应用
  • 大数值(0.3以上):替换广泛的颜色范围,可能影响非目标区域

Fuzziness参数调节

Fuzziness参数控制替换边缘的柔和度:

  • 低模糊度(0-0.05):产生硬边缘,适合需要清晰边界的效果
  • 中等模糊度(0.05-0.2):创建自然过渡,避免锯齿现象
  • 高模糊度(0.2以上):产生非常柔和的边缘,适合创建羽化效果

参数组合策略

在实际应用中,Range和Fuzziness的组合使用可产生不同的视觉效果:

  • 精确替换:小Range + 低Fuzziness
  • 平滑过渡:中等Range + 中等Fuzziness
  • 区域影响:大Range + 高Fuzziness

实际应用案例

游戏元素颜色动态替换

在游戏开发中,ReplaceColor节点常用于动态改变游戏元素的颜色主题。例如,可根据游戏状态改变角色的服装颜色或环境的色调。

实现步骤:

  1. 将角色纹理连接到In端口
  2. 设置需要替换的原始颜色到From端口
  3. 通过脚本控制To端口的颜色值
  4. 调整Range和Fuzziness以获得理想的替换效果

绿幕抠像效果

ReplaceColor节点可实现类似绿幕抠像的效果,将特定的背景颜色替换为透明或其他背景。

关键技术点:

  • 精确设置绿幕颜色到From端口
  • 使用较小的Range值确保只影响背景区域
  • 通过Fuzziness控制边缘的平滑度

UI元素主题切换

在UI设计中,可使用ReplaceColor节点实现动态主题切换。通过替换UI元素中的特定颜色,可快速实现白天/黑夜模式或不同色彩主题的切换。

性能优化建议

在ShaderGraph的Heatmap颜色模式下,可直观查看节点的性能成本。ReplaceColor节点通常具有中等性能开销,主要取决于颜色距离计算的复杂度。

优化策略

  • 预处理纹理:尽可能在纹理制作阶段优化颜色分布,减少需要处理的颜色范围
  • 合理使用参数:避免使用过大的Range值,这会增加计算量
  • 考虑平台差异:在移动平台上应更加谨慎地使用复杂的颜色替换效果

性能监控

通过以下方法监控节点性能:

  1. 切换到Heatmap颜色模式查看节点相对性能成本
  2. 在目标平台上实际测试着色器性能
  3. 使用Unity的性能分析工具进行详细分析

常见问题解决方案

颜色替换不精确

当颜色替换效果不理想时,可能的原因和解决方案包括:

  • 颜色空间问题:确保所有颜色值在相同的颜色空间中处理
  • 光照影响:考虑场景光照对颜色感知的影响,可能需要结合其他颜色调整节点
  • 参数设置不当:重新调整Range和Fuzziness参数

边缘锯齿问题

解决替换边缘的锯齿现象:

  • 适当增加Fuzziness参数值
  • 结合抗锯齿技术
  • 使用更高分辨率的纹理

性能问题处理

当颜色替换操作导致性能下降时:

  • 减少同时使用的ReplaceColor节点数量
  • 优化Range参数,使用尽可能小的有效范围
  • 考虑使用LOD技术,在远距离使用简化的着色器版本

与其他节点配合使用

ReplaceColor节点可与其他ShaderGraph节点组合使用,创建更复杂的效果。

与Blend节点配合

将ReplaceColor节点与Blend节点结合,可实现多层颜色的混合和替换效果。这种组合特别适用于创建复杂的材质效果和动态纹理变化。

与Mask节点组合

结合Color Mask节点使用,可更精确地控制颜色替换的区域和范围。通过遮罩技术,可限制ReplaceColor节点只在特定区域生效。

在调整节点中的位置

在Artistic类别中,ReplaceColor节点与其他调整节点如Hue、Saturation、Contrast等共同构成了完整的颜色调整工具集。理解各节点的特性和适用场景,有助于构建更高效的着色器图形。

高级应用技巧

动态参数控制

通过脚本动态控制ReplaceColor节点的参数,可实现实时的颜色变化效果。这种技术在交互式应用和游戏中特别有用。

实现方法:

  1. 在ShaderGraph中创建对应的材质属性
  2. 通过C#脚本修改材质属性值
  3. 实现基于游戏逻辑的颜色动态变化

多级颜色替换

通过串联多个ReplaceColor节点,可实现复杂的多级颜色替换效果。这种方法适用于需要同时替换多种颜色的场景。

注意事项:

  • 节点顺序影响最终结果
  • 注意性能开销的累积
  • 考虑颜色之间的相互影响

总结与最佳实践

ReplaceColor节点是ShaderGraph中功能强大的颜色处理工具,通过合理使用其参数和组合其他节点,可创建各种视觉效果。

使用建议

  • 从简单开始:先使用基本设置,逐步调整参数
  • 测试不同光照条件:确保在各种光照环境下效果一致
  • 考虑目标平台:根据运行平台调整效果复杂度和性能要求

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

静默打印程序实现

背景

需求需要实现一个静默打印插件,辅助 Web 后台系统以实现静默打印标签,提升仓库侧工作效率。原插件稳定性、兼容性差,无源码无法扩展修改,支持多种标签格式,后续所有标签会转移至 PDF 格式,所以新的插件系统只需要支持 PDF 打印即可。

技术选型

桌面框架选择 Electron

  • 优点:前端友好、快速开发、文档、生态完善、兼容性好
  • 缺点:资源占用高、打包体积大

渲染层选择 React(个人喜好)。

包管理工具使用 npm,由于开发 React Native 时被 pnpm 的各种差异、兼容性搞的头疼,所以在开发非 Web 应用时更倾向于使用 npm。

最终使用开源的 electron-react-boilerplate 作为基础框架进行开发。

整体架构设计

主进程实现功能:

  • 静默打印
  • 对外接口服务
  • 异常重启
  • 日志记录(electron-log
  • 系统通知(electron 内置 Notification
  • 自动更新(electron-updater
  • 数据库交互(@seald-io/nedb
  • 外部程序调用

渲染层实现功能:

  • 界面交互
  • 打印设置
  • 数据查询(打印记录查询查看)

主进程(核心层)通过启动本地服务器提供对外接口调用,触发接口调用时主进程调用命令行或外部程序实现打印功能。

Web 开发大家都很熟了,所以下面只说主进程中的静默打印、对外接口服务的功能开发。

静默打印

使用 Electron 自带打印功能时,需要通过 BrowserWindow.webContents.print() 进行打印,这个方法只能打印当前浏览器窗口的内容,而不能指定文件进行打印。

如果需要打印 PDF 文件,一种解决办法是在渲染层将指定 PDF 文件渲染为图片然后打印,这种方法兼容性好,支持常见的操作系统(Mac/Windows);缺点也很明显:渲染多页 PDF 文件耗时长,并发打印支持差,并且需要主进程、渲染层合作处理,代码分散。

另一个方法是使用三方库,这些库内部调用系统命令或外部程序以实现打印 PDF 的功能,这种方法性能较好、支持并发,且只需要在主进程处理,缺点就是可定制性差。

这里使用三方库的方式,由于没有找到可靠的通用 PDF 打印库,所以针对 Mac 系统使用 unix-print,针对 Windows 系统使用 pdf-to-printer

它们各自的实现原理如下:

  • Mac 系统中内置了通用打印系统 CUPS,使用命令行 ls command 就可以实现打印 PDF,unix-print 就是对 ls 命令的一层包装

  • pdf-to-printer 中内置了 SumatraPDF 可执行程序,这是一个开源轻量的阅读器,支持以命令行方式调用以打印 PDF,pdf-to-printer 同样是对这些命令的一层包装。

这两个库虽然整体能够在外部封装统一接口来进行对齐,但还有一些差异是无法抹平的:

  • CUPS 支持设定自定义的纸张规格,这在处理非标准尺寸的 PDF 标签时是很有用的,而 SumatraPDF 不支持

    SumatraPDF 要实现这样的功能只能在 Windows 系统中添加自定义打印规格,并在打印时选择指定规格,这样就多了人工操作的步骤,非常不便。

  • CUPS 支持 lpstat 命令来查询打印任务,SumatraPDF 不支持

为了增强 Windows 端的打印功能,使用 .Net 8 实现了一个 Windows 控制台应用程序,实现逻辑如下:

  1. 启动时,启动一个管道,通过管道实现交互(这样可以避免每次调用程序时创建进程的开销)
  2. 根据入参,调用特定方法
  3. 打印功能:读取 PDF,转换为位图并调用 .Net 内置 SKD 打印,这种方式兼容性好、实现简单,缺点是位图放大时不如 PostScript 生成的清晰,体积也更大,不过在打印标签的场景上通常不会有太大问题
  4. .Net 还内置有其他实用功能,比如获取打印机列表、获取打印队列、自定义纸张规格等。

在开发 Windows 控制台程序时,曾考虑过使用三方包集成,这种方式无疑时最好的,但查询到仍在积极开发状态的 PDF 打印 Nuget 包基本采用付费授权,而一些开源的(比如 PdfiumViewer)包在很久前都已经停止开发了。

对外接口服务

使用 express 启动本地 HTTP 服务,进行 cors 设置,定义通用响应结构,为了后续扩展其他功能,这里以不同的路由前缀区分模块,比如:

  • /print/* 表示打印功能相关 API
  • /docs/* 表示文档相关 API

同时,为了检查服务器是否正常工作,提供 /ping 端点让外部调用。

考虑相关功能,应该提供以下 API:

  • /print/printer-list: 获取系统打印机列表
  • /print/printer-setting: 设置打印机打印参数
  • /print/printer-jobs: 获取指定打印机打印队列
  • /print/print-urls: 打印以 URL 格式提供的 PDF 文件
  • /print/print-base64: 打印以 base64 格式提供的 PDF 文件
  • /print/print-formdata: 打印以 FormData 格式提供的 PDF 文件

开发本地服务器供 Web 调用时,需要注意 Chrome 的兼容性,最近 Chrome 142 版本推出了本地网络访问权限提示。本地网络访问权限限制了 Web 访问本地服务器的能力,如果你的网站是以 https 提供服务的,Chrome 会在调用本地服务器时弹出权限提示,同意即可;但如果你的网站是以 http 提供服务的,那访问本地服务器时会静默失败,目前的方法是引导用户关闭 Chrome 对本地网络访问的限制。

Next.js第十三章(缓存组件)

缓存组件(Cache Components)

什么是Cache Components?

Cache Components 是Next.js(16)版本特有的机制,实现了静态内容 动态内容 缓存内容的混合编排。保留了静态内容的加载速度,又具备动态渲染的灵活性,解决了静态内容(加载快但无法实时更新数据)动态内容(加载慢但可以实时更新数据)权衡的问题。

  • 静态内容: 构建(npm run build)时进行预渲染,例如 「本地文件」「模块导入」「纯计算」(无网络请求、无用户相关数据),会被直接编译成HTML瞬间加载、立即响应。

  • 动态内容:用户发起请求时才开始渲染的内容,依赖 “实时数据” 或 “用户个性化信息”,每次请求都可能生成不同结果,不会被缓存。例如「实时数据源」(如实时接口、数据库实时查询)或「用户请求上下文」(如 Cookie、请求头、URL 参数)

  • 缓存内容:缓存内容的本质就是缓存动态数据,缓存之后会被纳入静态外壳(Static Shell),静态外壳就类似于毛坯房,会提前把结构搭建好,后续在通过(流式传输)填充里面的动态内容。

传统方案 Cache Components
静态页面:数据无法实时更新 支持缓存内容重新验证,动态内容流式补充
动态页面:初始加载慢、服务器压力大 静态外壳优先返回,动态内容并行渲染
客户端渲染:bundle 体积大、首屏慢 服务器预渲染核心内容,客户端仅补充动态部分

启用Cache Components

Cache Components 为可选功能,需在 Next 配置文件中显式启用:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true, // 启用缓存组件
};

export default nextConfig;
1. 静态内容展示

适用场景:仅依赖同步 I/O(如 fs.readFileSync)、模块导入、纯计算的组件

import fs from 'node:fs'

export default async function Home() {
    const data = fs.readFileSync('data.json', 'utf-8') //本地文件读取
    const json = JSON.parse(data)
    const impData = await import('../../../data.json') //模块导入
    const names = impData.list.map(item=>item.name).join(',') //纯计算
    console.log(json)
    console.log(impData)
    console.log(names)
    return (
        <div>
            <h1>Home</h1>
            <ul>
                {json.list.map((item: any) => (
                    <li key={item.id}>{item.name} - {item.age}</li>
                ))}
            </ul>
        </div>
    )
}

image.png

2.1 动态内容展示

适用场景:fetch请求、cookies、headers等动态数据

动态内容必须配合Suspense使用。

import { Suspense } from "react"
import { cookies } from "next/headers"

const DynamicContent = async () => {
    const data = await fetch('https://www.mocklib.com/mock/random/name') //随机生成一个名称
    const json = await data.json()
    console.log(json)
    const cookieStore = await cookies() //获取cookie
    console.log(cookieStore)
    return (
        <div>
            <h2>动态内容</h2>
            <main>
                <ul>
                    <li>名称:{json.name}</li>
                </ul>
            </main>
        </div>
    )
}

export default async function Home() {

    return (
        <div>
            <h1>Home</h1>
            <Suspense fallback={<div>动态内容Loading...</div>}>
                <DynamicContent />
            </Suspense>
        </div>
    )
}

image.png

2.2 实现原理

Next.js 会通过(Partial Prerendering/PPR)技术,实现静态外壳(Static Shell)渲染,提供占位符,当用户请求时,再通过流式传输(Streaming)填充里面的动态内容,以此提升首屏加载速度和用户体验。

image.png

我们观察上图

  • <h1>Home</h1>: 纯静态内容,属于静态外壳的一部分,构建 / 请求时直接渲染,浏览器能立即显示。

  • <template id="B:0"></template> 动态内容的容器模板,后续用来挂载异步加载的动态内容

  • <div>动态内容Loading...</div>:占位符(fallback),属于静态外壳的一部分,在动态内容加载完成前显示。

image.png

  • 这个 <div> 初始为 hidden,是服务器异步渲染完成的动态内容,等待客户端脚本触发后替换到占位符位置。

  • id="S:0" 与前面的 <template id="B:0"> 一一对应,是 “动态内容 - 占位符” 的关联标识。

image.png

$RC("B:0", "S:0") // 关键调用:关联 B:0 占位符和 S:0 动态内容
  • RCReactContentReplace):找到id="B:0"的占位符和id="S:0"的动态内容,将其加入替换队列RC(React Content Replace):找到 id="B:0" 的占位符和 id="S:0" 的动态内容,将其加入替换队列 RB。
  • $RV(React Content Render):在动画帧 / 超时后执行替换,移除加载占位符,将动态内容插入到页面中,完成最终渲染。
2.3 非确定操作

例如: 随机数时间戳等非确定操作,每次请求都可能生成不同结果。

直接使用就会报错如下:

Error: Route "/home" used Math.random() before accessing either uncached data (e.g. fetch()) or Request data (e.g. cookies(), headers(), connection(), and searchParams). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: nextjs.org/docs/messag… at DynamicContent (page.tsx:5:25) at Home (page.tsx:27:17)

解决方案:

使用Suspense包裹,然后使用connection表示不要预渲染这部分。

Next.js默认会尝试尽可能多地静态预渲染页面内容。但像 Math.random() 这样的值每次调用结果都不同,如果在预渲染时执行,那这个"随机值"就被固定了,失去了意义。 通过在 Math.random() 之前调用 await connection(),你明确告诉 Next.js:

  • 不要预渲染这部分
  • 等真正有用户请求时再执行
import { Suspense } from "react"
import { connection } from "next/server"

const DynamicContent = async () => {
    await connection() //使用connection表示不要预渲染这部分
    const random = Math.random()
    const now = Date.now()
    console.log(random, now)
    return (
        <div>
            <h2>动态内容</h2>
            <main>
                <ul>
                    <li>名称:{random}</li>
                    <li>时间:{now}</li>
                </ul>
            </main>
        </div>
    )
}

export default async function Home() {

    return (
        <div>
            <h1>Home</h1>
            <Suspense fallback={<div>动态内容Loading...</div>}>
                <DynamicContent />
            </Suspense>
        </div>
    )
}
3. 缓存内容展示

缓存组件,可以使用use cache声明这是一个缓存组件,然后使用cacheLife声明缓存时间。

cacheLife参数:

  • stale:客户端在此时间内直接使用缓存,不向服务器发请求(单位:秒)
  • revalidate:超过此时间后,服务器收到请求时会在后台重新生成内容(单位:秒)
  • expire:超过此时间无访问,缓存完全失效,下次请求需要等待重新计算(单位:秒)

预设参数:

Profile 适用场景 stale revalidate expire
seconds 实时数据(股票、比分) 30秒 1秒 1分钟
minutes 频繁更新(社交动态) 5分钟 1分钟 1小时
hours 每日多次更新(库存、天气) 5分钟 1小时 1天
days 每日更新(博客文章) 5分钟 1天 1周
weeks 每周更新(播客) 5分钟 1周 30天
max 很少变化(法律页面) 5分钟 30天 1年
import { Suspense } from "react"
import { cacheLife } from "next/cache"

const DynamicContent = async () => {
    'use cache'
    cacheLife("hours") //使用预设参数
    //cacheLife({stale: 30, revalidate: 1, expire: 1}) //使用自定义参数
    const data = await fetch('https://www.mocklib.com/mock/random/name')
    const json = await data.json()
    console.log(json)
    return (
        <div>
            <h2>动态内容</h2>
            <main>
                <ul>
                    <li>名称:{json.name}</li>
                </ul>
            </main>
        </div>
    )
}

export default async function Home() {

    return (
        <div>
            <h1>Home</h1>
            <Suspense fallback={<div>动态内容Loading...</div>}>
                <DynamicContent />
            </Suspense>
        </div>
    )
}

8.gif

“受控组件”的诅咒:为什么你需要 React Hook Form + Zod 来拯救你的键盘?

前言:你的手指累吗?

上一篇咱们聊了把 Tabs 状态放进 URL 里,这就好比给了你的应用一个“身份证”,走到哪都在。

但在这个 Tab 下面,往往藏着前端开发最大的噩梦——表单

你是不是还在这么写代码? 一个简单的注册页,你定义了 username, password, email, confirmPassword 四个 useState。 然后写了四个 handleXxxChange 函数。 每次用户敲一个键盘,React 就重新渲染一次组件(Re-render)。 如果表单有 50 项,你在第一个输入框打字,整个页面卡得像是在放 PPT。

还有那个该死的校验逻辑:

if (!email.includes('@')) { ... }
if (password.length < 8) { ... }
if (password !== confirmPassword) { ... }

这些 if-else 像面条一样缠绕在你的组件里。写到最后,你都不知道自己是在写 UI 还是在写逻辑判断。

兄弟,放下你手里的 useState。是时候引入 React Hook FormZod 这对“黄金搭档”了。

观念重塑:受控 vs 非受控

React 官方文档早期教我们要用“受控组件”(Controlled Components),也就是 value 绑定 state,onChange 更新 state。

但这在复杂表单场景下,简直是性能杀手

React Hook Form (RHF) 的核心哲学是回归 HTML 的本质:非受控组件(Uncontrolled Components) 。 它利用 ref 直接接管原生 DOM 元素。

  • 你打字时,React 渲染。
  • 只有校验报错或提交时,React 才介入。

这就好比,以前你是傀儡师,手要把着每一个木偶的关节动(受控);现在你给了木偶一个指令“往前走”(非受控),它自己就走了,你只管终点。

实战演练:从 50 行代码缩减到 10 行

假设我们要写一个带有校验的用户表单。

❌ 痛苦的传统写法(useState):

  const [values, setValues] = useState({ name: '', age: 0 });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    // 1. 更新状态(触发重渲染)
    setValues({ ...values, [e.target.name]: e.target.value });
    // 2. 还要在这里手写校验逻辑,或者等提交时校验
    // ...逻辑省略,已经想吐了
  };

  return (
    <form>
      <input name="name" value={values.name} onChange={handleChange} />
      <input name="age" value={values.age} onChange={handleChange} />
    </form>
  );
};

✅ 爽翻天的 RHF 写法:


const BetterForm = () => {
  // register: 注册器,相当于 ref + onChange 的语法糖
  // handleSubmit: 帮你处理 `e.preventDefault` 和数据收集
  // formState: 所有的错误状态都在这
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => console.log(data); // data 直接就是 {name:..., age:...}

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 也就是一句话的事 */}
      <input {...register("name", { required: true })} />
      {errors.name && <span>名字必填</span>}
      
      <input {...register("age")} />
      
      <button type="submit">提交</button>
    </form>
  );
};

看到没有?没有 useState,没有 handleChangeregister 函数给你的 Input 注入了原本需要手写的 name, onBlur, onChange, ref。一切都在黑盒里自动完成了。

灵魂伴侣:Zod 登场

RHF 解决了数据收集和性能问题,但校验规则如果写在 JSX 里(比如 { required: true, minLength: 10 }),还是很乱。

这时候,你需要 Zod。 Zod 是一个 TypeScript 优先的 Schema 声明库。简单说,就是把校验逻辑从 UI 里抽离出来,变成一份“说明书”

1. 定义说明书 (Schema)


const schema = z.object({
  username: z.string().min(2, "名字太短了,再长点"),
  email: z.string().email("这根本不是邮箱"),
  age: z.number().min(18, "未成年人请绕道").max(100),
  // 甚至可以做复杂的依赖校验
  password: z.string().min(6),
  confirm: z.string()
}).refine((data) => data.password === data.confirm, {
  message: "两次密码不对啊兄弟",
  path: ["confirm"], // 错误显示在 confirm 字段下
});

2. 连接 RHF 和 Zod

我们需要一个“中间人”:@hookform/resolvers

import { zodResolver } from '@hookform/resolvers/zod';

const BestForm = () => {
  const { 
    register, 
    handleSubmit, 
    formState: { errors, isDirty } // isDirty 很有用!
  } = useForm({
    resolver: zodResolver(schema) // ✨ 注入灵魂
  });

  return (
    <form onSubmit={handleSubmit(saveData)}>
      <input {...register("username")} />
      <p className="text-red-500">{errors.username?.message}</p>
      
      {/* ...其他字段... */}
      
      <button type="submit">提交</button>
    </form>
  );
};

现在,你的 JSX 极其干净。所有的校验逻辑都在 schema 对象里。想改规则?去改 schema 就行,不用动组件。

回应开头:防止“手滑关网页”

还记得前言里说的那个悲剧吗?用户填了一半,手滑关了 Tab。

RHF 提供了一个神属性:isDirty(表单是否被弄脏了/是否被修改过)。

配合 React Router 的 useBlocker (v6.x) 或者传统的 window.onbeforeunload,我们可以轻松实现拦截


// 一个简单的拦截 Hook
const usePreventLeave = (isDirty) => {
  useEffect(() => {
    const handleBeforeUnload = (e) => {
      if (isDirty) {
        e.preventDefault();
        // 现代浏览器通常不支持自定义文本,但这会触发浏览器的默认弹窗
        e.returnValue = ''; 
      }
    };
    
    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [isDirty]);
};

// 在组件里使用
const MyForm = () => {
  const { formState: { isDirty } } = useForm();
  
  // 只要用户改了一个字,isDirty 就变成 true,防守模式开启
  usePreventLeave(isDirty); 
  
  return <form>...</form>;
}

现在,只要用户动了表单,试图关闭或刷新页面时,浏览器就会弹出一个无情的警告框:“您有未保存的更改,确定要离开吗?”。 PM 再也不用担心用户投诉数据丢了。


总结

React Hook Form + Zod 是现代 React 开发的“工业标准”。

  • RHF 负责 DOM 交互和性能优化,让你的页面丝滑顺畅。
  • Zod 负责数据结构和逻辑校验,保证你的数据干净可靠。
  • TypeScript 自动推导类型,让你写代码有自动补全。

当你熟练掌握这一套组合拳,你会发现写表单不再是折磨,甚至有一种解压的快感。

好了,我要去把那个嵌套了 10 层 if-else 校验的屎山给重构了。

438783fa67714fda959e8e52adb9245d.webp


下期预告:表单数据量如果不止 50 项,而是 10,000 项呢? 比如一个超长的“用户管理列表”,或者一个即时滚动的“日志监控台”。如果你把这 10,000 个 div 直接渲染出来,浏览器会直接死机。 下一篇,我们来聊聊 “虚拟滚动 (Virtual Scrolling) 技术” 。教你如何用 react-window 欺骗用户的眼睛,让无限列表像德芙一样纵享丝滑。

将flutter打成aar包嵌入到安卓

Flutter Module 打包成 AAR 并集成到 Android 项目

一、创建 Flutter Module

如果你 还没有 Flutter Module

flutter create -t module flutter_module

二、构建 Flutter AAR

执行 AAR 构建命令

flutter build aar

构建产物位置

flutter_module/build/host/outputs/repo/

构建控制台输出

在下面的配置中会用到

  1. Open <host>\app\build.gradle
  2. Ensure you have the repositories configured, otherwise add them:

      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
      repositories {
        maven {
            url 'E:\lumosproject\module\lumos\build\host\outputs\repo'
        }
        maven {
            url "$storageUrl/download.flutter.io"
        }
      }

  3. Make the host app depend on the Flutter module:

    dependencies {
      debugImplementation 'com.example.lumos:flutter_debug:1.0'
      profileImplementation 'com.example.lumos:flutter_profile:1.0'
      releaseImplementation 'com.example.lumos:flutter_release:1.0'
    }


  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
      }
    }


三、Android 宿主项目集成 AAR

1. 修改MyApp/app/build.gradle

你目前现有的 Android 项目可能支持 mips 或 x86 之类的架构,然而,Flutter 当前仅支持 为 x86_64armeabi-v7a 和 arm64-v8a 构建预编(AOT)的库。

可以考虑使用 abiFilters 这个 Android Gradle 插件 API 来指定 APK 中支持的架构,从而避免 libflutter.so 无法生成而导致应用运行时崩溃,具体操作如下:

Groovy 版
android {
    defaultConfig {
        ndk {
            // Filter for architectures supported by Flutter
            abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
        }
    }
}
Kotlin DSL
android {
    defaultConfig {
        ndk {
            // Filter for architectures supported by Flutter
            abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
        }
    }
}

2. 配置 settings.gradle(.kts)

在国内,需要使用镜像站点代替 storage.googleapis.com。有关镜像的详细信息,参见 在中国网络环境下使用 Flutter 页面。

Groovy 版
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.flutter-io.cn"
    repositories {

        google()
        mavenCentral()
        maven {
            url 'E:/lumosproject/module/lumos/build/host/outputs/repo'
        }
        maven {
            url "$storageUrl/download.flutter.io"
        }
    }
}
Kotlin DSL
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
    repositories {
        google()
        mavenCentral()
        maven("https://storage.googleapis.com/download.flutter.io")
    }
}

3. 添加依赖(app/build.gradle)

这里添加的依赖都是build aar 控制台输出的内容

groovy版
dependencies {
   debugImplementation 'com.example.untitled:flutter_debug:1.0'
   profileImplementation 'com.example.untitled:flutter_profile:1.0'
   releaseImplementation 'com.example.untitled:flutter_release:1.0'
}
Kotlin DSL
dependencies {
debugImplementation("com.example.flutter_module:flutter_debug:1.0")    
releaseImplementation("com.example.flutter_module:flutter_release:1.0")   
add("profileImplementation", "com.example.flutter_module:flutter_profile:1.0") 
}

4. 添加profile build type(app/build.gradle)

buildTypes中添加

groovy版
profile {
    initWith debug
}
Kotlin DSL
create("profile") { initWith(getByName("debug")) }

四、Android 启动 Flutter 页面

1.在 AndroidManifest.xml 中添加 FlutterActivity

Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内

<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/LaunchTheme"   android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

2. 加载 FlutterActivity

确保使用如下的语句导入:

import io.flutter.embedding.android.FlutterActivity;
MyButton(onClick = {
    startActivity(
        FlutterActivity.createDefaultIntent(this)
    )
})

@Composable
fun MyButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Launch Flutter!")
    }
}

上述的例子假定了你的 Dart 代码入口是调用 main(),并且你的 Flutter 初始路由是 '/'。 Dart 代码入口不能通过 Intent 改变,但是初始路由可以通过 Intent 来修改。下面的例子讲解了如何打开一个自定义 Flutter 初始路由的 FlutterActivity

MyButton(onClick = {
  startActivity(
    FlutterActivity
      .withNewEngine()
      .initialRoute("/my_route")
      .build(this)
  )
})

@Composable
fun MyButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Launch Flutter!")
    }
}

3. 使用缓存 Engine(推荐)

每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine

class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine

  override fun onCreate() {
    super.onCreate()

    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )

    // Cache the FlutterEngine to be used by FlutterActivity.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}
使用
myButton.setOnClickListener {
  startActivity(
    FlutterActivity
      .withCachedEngine("my_engine_id")
      .build(this)
  )
}
为缓存的 FlutterEngine 设置初始路由
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine
  override fun onCreate() {
    super.onCreate()
    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)
    // Configure an initial route.
    flutterEngine.navigationChannel.setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}
通过设置路由的方式可以配置缓存引擎在执行 Dart 入口点之前使用自定义初始路由

五、Flutter 与 Android 通信

MethodChannel方式

Flutter

添加到合适的地方

static const methodChannel = MethodChannel('com.bluetoothCharacteristic');
methodChannel.setMethodCallHandler(_handleMethod);
Future<dynamic> _handleMethod(MethodCall call) async {
  switch (call.method) {
    case 'bluetoothCharacteristic':
      // 设置设备写入特征
      _device.setWriteCharacteristic(
        call.arguments as BluetoothCharacteristic,
      );
      break;
    default:
      throw PlatformException(code: 'Unrecognized Method');
  }
}

Android


private lateinit var methodChannel: MethodChannel
methodChannel = MethodChannel(
    flutterEngine.dartExecutor.binaryMessenger,
    "com.bluetoothCharacteristic"
)
使用
Button(
    onClick = {
        val intent = FlutterActivity
            .withCachedEngine("my_engine_id")
            .build(activity)
        methodChannel.invokeMethod(
            "bluetoothCharacteristic",
            "00002a37-0000-1000-8000-00805f9b34fb"
        );
        activity.startActivity(intent)
    },
    modifier = Modifier.padding(16.dp)
) {
    Text("跳转到flutter页面")
}

MethodChannel 中的 com.bluetoothCharacteristic 和methodChannel.invokeMethod中的bluetoothCharacterstic必须和 Flutter 中保持一致,否则接收不到数据。

EventChannel方式

EventChannel的使用方式大致和MethodChannel相同,我这里就把主要代码复制下来了。

class MainActivity : ComponentActivity() {
    private lateinit var flutterEngine: FlutterEngine
    private lateinit var eventChannel: EventChannel
    private var eventDataSink: EventChannel.EventSink? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        // 创建 FlutterEngine
        flutterEngine = FlutterEngine(this)
        //设置初始路由
        flutterEngine.navigationChannel.setInitialRoute("/settings")
        // 启动 Flutter 引擎
        flutterEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        )
        // 设置 EventChannel 用于实时数据传输
        eventChannel = EventChannel(flutterEngine.dartExecutor, "com.example.flutter_aar_demo/event_channel")
        eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                eventDataSink = events
                startRealTimeDataTransmission()
            }
            override fun onCancel(arguments: Any?) {
                eventDataSink = null
            }
        })
        // 缓存 FlutterEngine
        FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
    }

我这里的安卓使用的是groovy的创建方式Kotlin DSL的方式是我直接从官网复制的代码可能有不正确的地方具体请查看将 Flutter module 集成到 Android 项目

拿捏 React 组件通讯:从父子到跨组件的「传功秘籍」

前言

你有没有过这种崩溃时刻?写 React 组件时,想让父子组件互通数据,结果代码越写越乱;兄弟组件想传个值,绕了八层父组件还没搞定…… 为啥别人的 React 组件传值丝滑?别慌!秘密都在这!今天咱们用 「武侠传功」 的思路,React 组件通讯的坑,一篇帮你填平!

一、父子组件:父传子的「单向秘籍」

想象一下:父组件武林盟主,手里有本 《九阴真经》(变量),想传给子组件这个小徒弟。但规矩是:

徒弟只能看,不能改!

传功步骤:

  1. 父组件发功:在子组件标签上绑定属性(把秘籍递出去)
  2. 子组件接功:通过 props 接收(双手接住秘籍)

看个例子:

父组件(盟主)把 state.name 传给子组件:

// 父组件 Parent.jsx
import Child from "./Child";
export default function Parent(props) {
    const state = {
        name: 'henry' // 这是要传的「秘籍」
    };
    return (
        <div>
            <h2>父组件</h2>
            <Child msg = {state.name}/> {/* 绑定属性 msg,把秘籍递出去 */}
        </div>
    );
}

子组件(徒弟)用 props 接收,但不能修改(秘籍是只读的!):

// 子组件 Child.jsx
export default function Child(props) {
    console.log(props); // 能看到 {msg: 'henry'}
    // props.msg = 'harvest'; // ❌ 报错!props 是只读的,不能改!
    return (
        {/* 展示接收到的「秘籍」 */}
        <div>子组件 -- {props.msg}</div> 
    );
}

打印结果如下:

image.png

如果修改值会报错(只读,不能改!):

image.png

二、子父组件:子传父的「反向传功」

这次反过来:子组件是徒弟,练出了新招式(变量),想传给父组件盟主。但徒弟不能直接塞给盟主,得让盟主递个「接收袋」(函数),徒弟把招式装进去

传功步骤:

  1. 父组件递袋:定义接收数据的函数(准备好接收袋)
  2. 父传子袋:把函数通过 props 传给子组件(把袋子递过去)
  3. 子组件装招:调用函数并传数据(把新招式装进袋子)

代码例子:

父组件准备「接收袋」getNum,传给子组件:

// 父组件 Parent.jsx
import Child from "./Child"
import { useState } from "react";
export default function Parent() {
    let [count, setCount] = useState(1);
    // 定义「接收袋」:收到数据后更新自己的状态
    const getNum = (n) => {
        setCount(n);
    }
    return (
        <div>
            <h2>父组件二 -- {count}</h2>
            <Child getNum = {getNum}/> {/* 把袋子递过去 */}
        </div>
    );
}

子组件调用 getNum,把自己的 state.num 传过去:

// 子组件 Child.jsx
export default function Child(props) {
    const state = {
        num: 100 // 自己练的新招式
    };
    function send() {
        props.getNum(state.num); // 把新招式装进袋子
    }
    return (
        <div>
            <h3>子组件二</h3>
            <button onClick={send}>发送</button> {/* 点按钮传功 */}
        </div>
    )
}

点击发送前:

image.png

点击发送后:

image.png

这样我们就成功的把子组件的变量传给了父组件。这里我们用到了useState,至于它的作用我们暂时先不讲,我们先搞懂通讯。

三、兄弟组件:「父组件当中间商」

兄弟组件像两个师兄弟,想互相传功?得先把招式传给盟主父组件(中间商),再让父组件转给另一个兄弟。

传功步骤:

  1. 弟 1 传父:弟 1 把数据传给父组件
  2. 父传弟 2:父组件把数据传给弟 2

依旧代码:

父组件当中间商,接收 Child1 的数据,再传给 Child2:

// 父组件 Parent.jsx
import { useState } from "react";
import Child1 from "./Child1"
import Child2 from "./Child2"
export default function Parent(props) {
    let [message, setMessage] = useState();
    // 接收 Child1 的数据
    const getMeg = (msg) => {
        setMessage(msg);
    }
    return (
        <div>
            <h2>父组件三</h2>
            <Child1 getMeg = {getMeg} /> {/* 收 Child1 的数据 */}
            <Child2 msg = {message}/> {/* 把数据传给 Child2 */}
        </div>
    )
}

Child1(兄) 传数据给父:

// Child1.jsx
export default function Child1(props) {
    const state = {
        msg: '3.1'
    };
    function send() {
        props.getMeg(state.msg); // 传给父组件
    }
    return (
        <div>
            <h3>子组件3.1</h3>
            <button onClick={send}>发送</button>
        </div>
    )
}

Child2(弟) 接收父组件传来的数据:

// Child2.jsx
export default function Child2(props) {
    return (
        <div>
            <h3>子组件3.2 -- {props.msg}</h3> {/* 展示兄传来的数据 */}
        </div>
    )
}

发送前:

image.png

发送后:

image.png

如此这般,子组件3.2就成功接收到了子组件3.1的传递信息。

四、跨组件通信:「Context 全局广播」

如果组件嵌套了好多层(比如父→子→孙→重孙),一层层传功太麻烦了!这时候用 Context 就像 「武林广播」:父组件把数据广播出去,所有后代组件都能直接收到。

广播步骤:

  1. 父组件建广播台:用 createContext 创建上下文对象,用 Provider 广播数据
  2. 后代组件收广播:用 useContext 接收广播的内容

还是代码:

父组件建广播台,广播「父组件的数据」:

// 父组件 Parent.jsx
import Child1 from "./Child1";
import { createContext } from 'react';
// 创建上下文对象(广播台)
export const Context = createContext() 
export default function Parent() {
    return (
        <div>
            <h2>父组件四</h2>
            {/* 用 Provider 广播数据,value 是要传的内容 */}
            <Context.Provider value={'父组件的数据'}>
                <Child1/>
            </Context.Provider>
        </div>
    );
}

孙组件直接收广播(不用经过子组件):

// 孙组件 Child2.jsx
import { useContext } from 'react'
import { Context } from './Parent' // 引入广播台
export default function Child2() {
    const msg = useContext(Context) // 直接接收广播内容
    return (
        <div>
            <h4>孙子组件 --- {msg}</h4> {/* 展示广播的内容 */}
        </div>
    );
}

另外附上子组件代码:

import Child2 from "./Child2"
export default function Child1() {
    return (
        <div>
            <h3>子组件</h3>
            <Child2/>
        </div>
    );
}

结果孙子组件成功得到父组件的数据:

image.png

五、温馨提示

别忘了App.jsxmain.jsx文件!前面的所有结果都需要这两个大佬的支持。

// App.jsx
// import Parent from "./demo1/Parent"
// import Parent from "./demo2/Parent"
// import Parent from "./demo3/Parent"
import Parent from "./demo4/Parent"

export default function App() {
    return (
        <Parent></Parent>
    )
}
// main.jsx
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
    <App />
)

这两份是创建React自带的,但是还是提醒一下大家别忘了!

六、总结:组件通讯「武功谱」

通讯场景 方法 核心思路
父子组件 props 传值 父传子,子只读
子父组件 props 传函数 父给函数,子调用传数据
兄弟组件 父组件中转 子→父→另一个子
跨组件 Context(上下文) 父广播,后代直接收

结语

其实 React 组件通讯的核心,从来都不是死记硬背步骤,而是找准数据的流向。父子传值用 props 单向传递,子父传值借函数反向搭桥,兄弟组件靠父组件当 “中转站”,跨层级通信就用 Context 打破嵌套壁垒。

这些方法没有优劣之分,只有场景之别。新手阶段先把 props 和函数传值练熟,再逐步尝试 Context,甚至后续学习的 Redux 等状态管理库,都是在这个基础上的延伸。

记住:

组件通讯本质,就是让数据在合适的组件间有序流动。现在就打开编辑器,把这些例子敲一遍,你会发现,那些曾经让你头疼的传值问题,早就迎刃而解了。

深入MCP本质——编写自定义MCP Server并通过Cursor调用

这篇文章将深入探讨 MCP (Model Context Protocol) 的核心原理,并通过一个简单的自定义天气工具示例,揭示 AI 模型如何与本地系统进行高效、安全的交互。


实战配置:启动你的自定义 MCP Server

首先,我们通过一个简单的步骤来配置并运行一个自定义的 MCP Server,该 Server 能够获取电脑所在地的实时时间和天气(不使用代理的前提下)。

步骤 1: 代码准备

下面是我编写的 MCP Server ,你可以将代码库拉取到本地目录

github.com/Objecteee/m…

步骤 2: Cursor 配置

在 Cursor IDE 的 mcp.json 配置文件中添加如下配置,指定你的本地 Server 脚本的路径:

{
  "Real Weather Tool": {
    "command": "node",
    "args": [
      "E:\project\mcp\myMCP\index.js"  # 请确保这里是你的本地文件路径
    ]
  }
}

配置完成后,您即可在 Cursor 的对话框中通过自然语言调用这个工具,例如询问:“现在的天气怎么样?”

image.png


1. 核心架构:什么是 MCP?

MCP (Model Context Protocol) 是由 Anthropic 提出的一种开放协议,旨在解决 大型语言模型 (LLM)外部世界(本地文件、数据库、API 等) 之间的信息鸿沟。

  • 没有 MCP 时: LLM 仅依赖训练数据,无法感知外部世界的实时状态(如当前时间、实时天气、本地代码结构)。它就像一个“孤岛上的智者”。
  • 有了 MCP: 我们为 LLM 提供了一个标准化的“工具插座”。任何遵循 MCP 协议的外部程序(即 MCP Server)都可以插入这个插座,使 LLM 能够 控制 这些工具并 获取 实时数据。

2. 本质原理:Cursor 如何连接并调用工具?

Cursor 连接和调用自定义 Server 的过程,本质上是基于 标准输入/输出 (Stdio)进程间通信 (IPC) ,它采用 JSON-RPC 格式进行数据交换。我们可以将整个过程分为两个阶段:

阶段一:握手(Handshake)与工具发现

  1. 启动子进程: 当你在 Cursor 中保存 mcp.json 配置后,Cursor 会在后台启动一个 子进程 (Child Process) ,执行你指定的 node index.js 脚本。
  2. 发送询问: Cursor(父进程)通过 标准输入 (stdin) 向你的脚本发送一条 JSON-RPC 格式的消息,请求获取可用的工具列表。
  3. 返回列表: 你的脚本(子进程)收到请求后,会通过 标准输出 (stdout) 返回一个包含其所有工具定义(如 TimeWeatherReporter)的 JSON 响应。这对应于代码中的 server.setRequestHandler(ListToolsRequestSchema...)

阶段二:待命与工具调用

  1. 待命: 只要 Cursor 不关闭,你的脚本进程就会在后台持续运行,等待调用指令。
  2. LLM 触发: 用户在聊天框输入指令(如“现在的天气怎么样?”),Cursor 内置的 AI 模型分析请求,决定调用哪个工具。
  3. 发送指令: Cursor 向你的脚本发送一条 JSON-RPC 调用请求,指定要执行的工具名称和参数。
  4. 执行任务: 你的脚本接收指令,执行相应的内部函数(例如请求 wttr.in 获取天气)。
  5. 反馈结果: 脚本将执行结果打包成 JSON,通过 stdout 发回给 Cursor。
  6. 结果展示: Cursor 接收到工具输出的原始数据,将其作为 上下文 传递给 AI 模型,由模型组织成自然语言的答案回复用户。

3. 代码层面的关键实现:MCP Server 运行机制 (index.js)

我们的注意力不要放在代码的实现上,要放在代码的流程上,因为MCP只是一种规范。

这个 Node.js 脚本通过遵循 MCP 规范,并利用标准输入/输出流 (Stdio) 实现通信。以下是驱动 MCP Server 运行的五个关键代码片段:

3.1 核心模块导入:定义协议与传输层

代码片段 作用描述 核心原理
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; Server 实例 MCP 服务端核心类,负责处理 JSON-RPC 请求和响应。
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; Stdio 传输层 声明通信载体是基于 标准输入/输出 (stdin/stdout) ,而非网络端口。
import { CallToolRequestSchema, ListToolsRequestSchema } from ... 协议定义 导入请求的 JSON Schema,用于解析 Cursor 发来的不同类型的指令。

3.2 初始化 MCP 服务器

const server = new Server(
  {
    name: "simple-weather-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

作用: 创建服务器实例。capabilities.tools: {} 明确告诉 MCP,该服务器具备提供工具的能力。

3.3 注册工具列表:响应“握手”请求

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "TimeWeatherReporter",
        description: "Get current system time and weather...",
        inputSchema: {
          type: "object",
          properties: {
            location: {
              type: "string",
              description: "Optional. City name (e.g. 'Beijing')...",
            },
          },
          required: [],
        },
      },
    ],
  };
});

作用 这是握手阶段(Handshake) 的关键。当 Cursor 首次启动子进程时,会发送 ListToolsRequestSchema 请求。该代码片段负责返回一个清晰的 JSON Schema 描述:

  • name: 工具的唯一标识符。
  • description: 供 AI 理解工具用途的自然语言描述。
  • inputSchema: JSON Schema 格式的参数定义,AI 依赖此信息来构造正确的调用参数。

3.4 处理工具调用:执行业务逻辑

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "TimeWeatherReporter") {
    const location = request.params.arguments?.location;
    return await getSystemStyleWeather(location); // 调用实际的业务函数
  }
  throw new Error("Tool not found");
});

作用: 这是执行阶段(Execution) 的核心。当 Cursor 的 AI 决定调用工具时,会发送 CallToolRequestSchema 请求。这段代码根据 request.params.name 匹配到对应的业务逻辑 (getSystemStyleWeather),执行后将结果返回给 Cursor。

3.5 启动服务器:连接到 Stdio 流

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Simple Weather MCP Server running on stdio");
}

作用: 连接点。 StdioServerTransport() 创建了传输通道,server.connect(transport) 则将服务器实例绑定到这个通道上。至此,您的 Node.js 脚本准备就绪,可以通过 stdin/stdout 与 Cursor 进程进行双向通信。


4. 总结:MCP 的本质

角色 实体 职责
雇主 (Client) Cursor IDE (父进程) 启动并监控工具,发送调用指令,接收并处理结果。
雇员 (Server) node index.js (子进程) 接收指令,执行实际任务(如 API 调用、文件读写),返回原始数据。
工作合同 (Protocol) MCP 协议 规定了握手和调用过程中 JSON 数据的结构和标准。
对讲机 (Transport) Stdio (标准输入/输出) 实现本地父进程与子进程之间即时、安全的通信。

MCP 的精妙之处在于它的解耦性安全性:Cursor 客户端无需关心你的 Server 是用 Node.js、Python 还是 Go 编写。只要你的程序符合 MCP 协议,能够通过命令行流接收和发送 JSON 数据,它就能无缝地成为 AI 的外部工具,极大地扩展了 LLM 的能力边界。


自定义MCP Server代码

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import { z } from "zod";

// 1. Initialize MCP Server
const server = new Server(
  {
    name: "simple-weather-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 2. Define Tool Logic using wttr.in (No API Key required)
async function getSystemStyleWeather(location) {
  // --- Get Real Local Time (System Time) ---
  const now = new Date();
  const localTime = now.toLocaleTimeString("zh-CN", {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  });
  const localDate = now.toLocaleDateString("zh-CN");

  // --- Determine URL ---
  // wttr.in automatically detects location by IP if no location is provided
  // format=j1 gives us a JSON response
  let url = "https://wttr.in/?format=j1";
  if (location) {
    url = `https://wttr.in/${encodeURIComponent(location)}?format=j1`;
  }

  try {
    const response = await axios.get(url);
    const data = response.data;
    
    // Parse wttr.in specific JSON structure
    const current = data.current_condition[0];
    const area = data.nearest_area[0];

    const weatherInfo = {
      location: location || area.areaName[0].value, // Use detected name if no input
      region: area.region[0].value,
      condition: current.weatherDesc[0].value,
      temperature_c: current.temp_C,
      feelslike_c: current.FeelsLikeC,
      humidity: current.humidity,
      wind_kph: current.windspeedKmph,
      data_source: "wttr.in (IP-based auto-location)"
    };

    // --- Construct Result ---
    const result = {
      status: "success",
      tool: "SystemWeatherReporter",
      system_time: `${localDate} ${localTime}`,
      weather: weatherInfo
    };

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result, null, 2),
        },
      ],
    };

  } catch (error) {
    return {
      content: [{ type: "text", text: `Unable to fetch weather data. Please check your network connection. Error: ${error.message}` }],
      isError: true,
    };
  }
}

// 3. Register Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "TimeWeatherReporter",
        description: "Get current system time and weather. Automatically detects location if not specified. No API key required.",
        inputSchema: {
          type: "object",
          properties: {
            location: {
              type: "string",
              description: "Optional. City name (e.g. 'Beijing'). If omitted, uses auto-detection.",
            },
          },
          required: [],
        },
      },
    ],
  };
});

// 4. Handle Tool Calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "TimeWeatherReporter") {
    const location = request.params.arguments?.location;
    return await getSystemStyleWeather(location);
  }
  throw new Error("Tool not found");
});

// 5. Start Server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Simple Weather MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

React 新手村通关指南:状态、组件与魔法 UI 🧙‍♂️

React 入门:从 JSX 到组件化,搞定核心知识点 🚀

一、 什么是 React & 为什么选择 React?🎯

如果你问前端圈的程序猿 “现在最火的 UI 库是什么”,十有八九会听到 “React” 这个名字。简单来说,React 是 Facebook(现在的 Meta)推出的一个用于构建用户界面的 JavaScript 库,它就像一个超级高效的 “UI 建筑师”,让我们能轻松搭建出交互丰富、性能能打的网页。

为什么要学 React 呢?“react 一开始就是组件思想,非常的纯粹”。和传统前端开发相比,React 有三个 “撒手锏”:响应式(数据变了 UI 自动更)、数据绑定(数据和 UI 手拉手)、组件化(把页面拆成一块块可复用的 “积木”)。虽然有人说它入门门槛比 Vue 高一点,但一旦上手,你会发现这种 “激进” 的设计思路特别适合复杂应用开发,毕竟谁不想写一次代码到处复用呢?

二、 JSX 语法:在 JS 里写 HTML?这操作有点秀!✨

2.1 什么是 JSX?

第一次看到 JSX 代码的同学可能会懵:“这到底是 JS 还是 HTML?” 其实官方解释很直白 ——JSX 就是 “XML in JS” (在 JavaScript 里写 XML),而我们熟悉的 HTML,本质上就是一种特殊的 XML。

看看下面的例子:

jsx

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

// 等效的原生JS写法(JSX的编译结果)
const element2 = createElement('h2', null,'JSX 是 React 中用于描述用户界面的语法扩展');

这两行代码效果完全一样,但显然 JSX 版本更像 “人话”。所以说,JSX 本质是createElement函数的语法糖,它的存在就是为了让我们写 UI 时少掉点头发,毕竟谁愿意对着一堆嵌套的函数调用流泪呢?

2.2 JSX 的基本语法

虽然 JSX 看起来像 HTML,但它有几个 “小怪癖” 需要注意:

  1. 必须有根元素包裹就像快递必须有包装盒,JSX 返回的内容也得有个 “根容器”。比如下面用<div>当容器,也可以用 React 提供的<Fragment>(文档碎片)当 “隐形容器”,避免多一层不必要的 DOM 节点。

    jsx

    // 正确:有根元素包裹
    return (
      <div>
        <h1>title</h1>
        <p>content</p>
      </div>
    )
    
    // 错误:没有根元素
    return (
      <h1>title</h1>
      <p>content</p>
    )
    
  2. class 要写成 className,如果你在 JSX 里写<div class="box">,React 会给你抛错 —— 因为class是 JavaScript 的关键字,所以得改用className。比如:

    jsx

    // 正确写法
    <span className="title">Hello</span>
    
    // 错误写法(会报错)
    <span class="title">Hello</span>
    
  3. 表达式要用大括号包起来想在 JSX 里插入变量或表达式?用{}就行!比如:

    jsx

    const name = "React";
    const isLoggedIn = true;
    
    // 插入变量
    return <h1>Hello {name}!</h1>;
    
    // 插入条件表达式
    return {isLoggedIn ? <div>已登录</div> : <div>未登录</div>};
    

三、 组件化:前端开发的 “乐高积木”🧩

3.1 什么是组件?

组件就像网页的 “功能模块”—— 把 HTML、CSS、JS 打包成一个独立单元,完成特定功能。比如下面的JuejinHeader是头部组件,Ariticles是文章列表组件,它们就像乐高积木,拼在一起就成了完整页面。

jsx

// 头部组件
function JuejinHeader() {
  return(
    <div>
      <header>
        <h1>JueJin首页</h1>
      </header>
    </div>
  )
}

// 文章列表组件
const Ariticles = () => {
  return(
    <div>
      Articles
    </div>
  )
}

3.2 为什么 React 选函数当组件?

React 里的组件大多是函数,这可不是随便选的。函数适合做组件,因为能将 JSX + 逻辑封装成一个组件。想想看,函数接收参数、处理逻辑、返回结果,组件接收数据、处理交互、返回 UI,简直是天造地设!

比如最简化的组件长这样:

jsx

// 一个简单的签到组件
const Checkin = () => {
  return(
    <div>Checkin</div>
  )
}

一行不多一行不少,逻辑和 UI 完美融合~

3.3 根组件与子组件组合形成组件树

整个 React 应用就像一棵大树,有一个 “根组件”,其他组件都是它的 “子组件”。以掘金首页为例展示根组件、子组件以及组件树的知识内容。下面代码的JuejinHeaderAriticles都是APP的子组件:

QQ20251214-161150.png

jsx

function JuejinHeader() {
  return(
    // jsx 最外层
    <div>
      <header>
        <h1>JueJin首页</h1>
      </header>
    </div>
  )
}

const Ariticles = () => {
  return(
    <div>
      Articles
    </div>
  )
}

const Checkin = () => {
  return(
    <div>
      Checkin
    </div>
  )
}

const TopArticles = () => {
  return(
    <div>
      TopArticles
    </div>
  )
}



// 根组件
function APP() {
  // 子组件们
  return(
    <div>
      {/* <h1>Hello <b>React!</b></h1> */}
      {/* 头部组件 */}
      <JuejinHeader />
      <main>
        {/* 组件也和html 一样声明,自定义组件 */}
        {/* 组件化让我们像搭积木一样组合成页面 */}
        <Ariticles />
        <aside>
          <Checkin />
          <TopArticles />
        </aside>
      </main>
    </div>
  )
}

export default APP 

组件树以一个根组件为起点(就像树干),其他组件作为 “子组件”(树枝和树叶)嵌套在其中,形成明确的层级关系。整个 React 应用的 UI 结构,本质上就是一棵组件树的可视化呈现。上面这段代码形成的组件树结构如下:

APP(根)
├─ JuejinHeader
└─ main
   ├─ Ariticles
   └─ aside
      ├─ Checkin
      └─ TopArticles

四、 状态:让组件 “动” 起来的魔法📦

4.1 什么是 state 状态?

如果说组件是 “身体”,那状态(state)就是 “灵魂”—— 它是组件内部的 “数据仓库”,决定了组件的样子和行为。比如用户是否登录、待办事项列表、当前显示的名字,这些都是状态。

4.2 useState:状态管理的 “金钥匙”

React 提供了useState这个钩子函数来管理状态,它的用法就像 “拆盲盒”:

jsx

// 引入useState
import { useState } from 'react';

// 声明状态:初始值为"vue",setName用于更新状态
const [name, setName] = useState("vue");

这几行代码做了三件事:

  1. 声明了一个叫name的状态,初始值是 “vue”
  2. 得到一个更新状态的函数setName
  3. setName被调用时,组件会重新渲染,UI 自动更新

比如 3 秒后更新状态,页面会自动变化:

jsx

// 3秒后将name从"vue"改为"react"
setTimeout(() => {
  setName("react");
}, 3000);

效果展示:

QQ20251214-16249.gif

4.3 多种状态管理

一个组件可以有多个状态,就像一个人可以有 “姓名”“年龄”“职业” 多个属性。比如同时管理三个状态:

jsx

// 框架名称状态
const [name, setName] = useState("vue");

// 待办事项列表状态
const [todos, setTodos] = useState([
  { id: 1, title: "学习react", done: false },
  { id: 2, title: "学习node", done: false }
]);

// 登录状态
const [isLoggedIn, setIsLonggedIn] = useState(false);

每个状态独立管理,互不干扰,代码逻辑清晰得很~

五、 渲染:让数据 “长” 成 UI 的艺术🖌️

5.1 条件渲染:根据状态 “变魔术”

React 里的条件渲染就像 “if-else” 的可视化版本,jSX中不能直接使用“if-else”语法(因为 JSX 大括号{}内只能放表达式,而if else是语句),通常使用三目运算符代替。比如判断待办事项是否为空:

jsx

// 有数据时显示列表,无数据时显示提示
todos.length > 0 ? (
  <ul>
    {/* 列表内容 */}
  </ul>
) : (
  <div>暂无待办事项</div>
)

登录状态的显示也是同理:

jsx

// 根据登录状态显示不同内容
{isLoggedIn ? <div>已登录</div> : <div>未登录</div>}

5.2 列表渲染:用 map () 批量 “生产” UI

当需要渲染列表(比如待办事项、商品列表)时,map()方法就是神器。比如渲染 todos 列表:

jsx

<ul>
  {
    todos.map((todo) => (
      // 每个列表项必须有唯一key
      <li key={todo.id}>
        {todo.title}
      </li>
    ))
  }
</ul>

这里有个小细节:每个列表项必须加key属性(通常用唯一 ID),这是 React 的 “性能暗号”,帮助它快速识别哪些项没变,避免重复渲染 —— 不加 key 虽然能运行,但可能会有性能问题哦!

六、 事件处理:给组件 “装开关”🔌

React 里的事件处理就像给组件装开关,点击、输入等操作都会触发对应的函数。语法上有个小特点:事件名要用驼峰命名(比如onClick而不是onclick)。

比如登录按钮的点击事件:

jsx

// 切换登录状态的函数
const toggleLogin = () => {
  setIsLonggedIn(!isLoggedIn);
};

// 按钮绑定点击事件
<button onClick={toggleLogin}>
  {isLoggedIn ? "退出登录" : "登录"}
</button>

点击按钮时,toggleLogin函数会切换isLoggedIn状态,按钮文字和登录状态显示会自动更新 —— 这就是 “数据驱动 UI” 的魅力,我们只需要管数据怎么变,UI 自然会跟上。

七、实战演练-简单的学习任务清单登陆界面

7.1 代码展示:

// JSX 负责UI
// use 使用
// state 数据状态

import {useState} from 'react';
import './App.css';
function App() {
    // const name = "vue";
    // useState 会返回一个数组
    // 数组的第一个元素是状态值,第二个元素是更新状态值的函数
    const [name,setName] = useState("vue");
    const [todos, setTodos] = useState([{
        id: 1,
        title: "学习react",
        done: false,
    },
    {
        id: 2,
        title: "学习node",
        done: false,

    }]);
    const [isLoggedIn,setIsLonggedIn] = useState(false);
    setTimeout(() => {
        setName("react");
    },3000);
    // 组件的数据业务、交互等
    // JSX js里面,class 是js 关键字 不能用,用className
    const toggleLogin = () => {
        setIsLonggedIn(!isLoggedIn);
    }
    return(
        // 文档碎片标签
        <div>
            <h1>Hello <span className="title">{name}!</span></h1>
            {
                todos.length > 0 ? (
                    <ul>
                        {
                            todos.map((todo) => (
                                <li key={todo.id}>
                                    {todo.title}
                                </li>
                            ))
                        }
                    </ul>
                ) : (<div>暂无待办事项</div>)
            }
            {isLoggedIn ? <div>已登录</div>:<div>未登录</div>}
            <button onClick = {toggleLogin}>
                {isLoggedIn?"退出登录":"登录"}
            </button>
        </div>
    )
}

export default App

7.2 项目介绍

这个项目是一个基于 React 的简单示例,主要展示了 React 的核心特性,具体如下:

  1. 技术基础:使用 React 框架开发,采用 JSX 语法描述 UI 界面,通过import引入 React 的useState(状态管理)API。

  2. 核心功能

    • 动态文本展示:通过name状态控制标题文本,初始显示 "Hello vue!",3 秒后自动更新为 "Hello react!"。
    • 待办事项列表:基于todos状态渲染待办事项列表,若列表为空则显示 "暂无待办事项"。
    • 登录状态切换:通过isLoggedIn状态管理登录状态,点击按钮可切换 "登录 / 退出登录" 状态,并同步显示对应的状态文本。
  3. React 特性体现

    • 状态管理:使用useState钩子管理组件内部状态(nametodosisLoggedIn),通过状态更新函数(如setNamesetIsLonggedIn)触发 UI 重新渲染。
    • 条件渲染:通过三元表达式根据状态(如todos.lengthisLoggedIn)动态展示不同 UI 内容。
    • JSX 语法:在 JavaScript 中直接编写类似 HTML 的标签,同时支持嵌入变量(如{name})和表达式。

整体是一个用于演示 React 状态与 UI 联动、组件化开发思想的基础示例。

7.3 效果亮个相

QQ20251214-164631.gif

八、 总结:React 核心知识清单 & 结语📝

8.1 知识清单

  1. React 是专注于 UI 的库,核心是组件化和响应式
  2. JSX 是 “JS 里写 XML” 的语法糖,简化 UI 开发
  3. 组件是函数,封装 UI 和逻辑,可组合成组件树
  4. useState管理状态,状态变则 UI 变
  5. 条件渲染用三目运算符,列表渲染用map()+key
  6. 事件处理用驼峰命名,绑定函数操作状态

8.2 结语

学 React 就像学搭积木 —— 一开始可能觉得 “这零件怎么拼”,但掌握了 JSX、组件、状态这些 “基础块” 后,你会发现再复杂的页面也能一步步搭出来。

React 更加激进,但这种激进恰恰让它成为前端开发的 “瑞士军刀”—— 无论是小应用还是大型项目,都能应对自如。现在,你已经掌握了 React 的核心概念,接下来就动手写写代码吧,毕竟实践才是最好的老师!💪

Vue3 服务端渲染 (SSR) 深度解析:从原理到实践的完整指南

摘要

服务端渲染 (SSR) 是现代 Web 应用提升性能、SEO 和用户体验的关键技术。Vue3 提供了全新的服务端渲染架构,具有更好的性能、更简洁的 API 和更完善的 TypeScript 支持。本文将深入探讨 Vue3 SSR 的工作原理、核心概念、实现方案,通过详细的代码示例、架构图和流程图,帮助你全面掌握 Vue3 服务端渲染的完整知识体系。


一、 什么是服务端渲染?为什么需要它?

1.1 客户端渲染 (CSR) 的问题

在传统的 Vue 单页面应用 (SPA) 中:

<!DOCTYPE html>
<html>
<head>
    <title>Vue App</title>
</head>
<body>
    <div id="app"></div>
    <!-- 初始 HTML 是空的 -->
    <script src="app.js"></script>
</body>
</html>

CSR 的工作流程:

  1. 浏览器请求 HTML
  2. 服务器返回空的 HTML 模板
  3. 浏览器下载 JavaScript 文件
  4. Vue 应用初始化,渲染页面
  5. 用户看到内容

存在的问题:

  • SEO 不友好:搜索引擎爬虫难以抓取动态内容
  • 首屏加载慢:用户需要等待所有 JS 加载执行完才能看到内容
  • 白屏时间长:特别是网络条件差的情况下

1.2 服务端渲染 (SSR) 的优势

SSR 的工作流程:

  1. 浏览器请求 HTML
  2. 服务器执行 Vue 应用,生成完整的 HTML
  3. 浏览器立即显示渲染好的内容
  4. Vue 应用在客户端"激活"(Hydrate),变成可交互的 SPA

核心优势:

  • 更好的 SEO:搜索引擎可以直接抓取完整的 HTML 内容
  • 更快的首屏加载:用户立即看到内容,无需等待 JS 下载执行
  • 更好的用户体验:减少白屏时间,特别是对于慢网络用户
  • 社交分享友好:社交媒体爬虫可以正确获取页面元信息

二、 Vue3 SSR 核心架构与工作原理

2.1 Vue3 SSR 整体架构

流程图:Vue3 SSR 完整工作流程

flowchart TD
    A[用户访问URL] --> B[服务器接收请求]
    B --> C[创建Vue应用实例]
    C --> D[路由匹配]
    D --> E[数据预取<br>asyncData/pinia]
    E --> F[渲染HTML字符串]
    F --> G[注入状态到HTML]
    G --> H[返回完整HTML给浏览器]
    
    H --> I[浏览器显示静态内容]
    I --> J[加载客户端JS]
    J --> K[Hydration激活]
    K --> L[变成可交互SPA]
    L --> M[后续路由切换为CSR]

2.2 同构应用 (Isomorphic Application)

Vue3 SSR 的核心概念是"同构" - 同一套代码在服务器和客户端都能运行。

// 同构组件 - 在服务器和客户端都能运行
export default {
  setup() {
    // 这个组件在两个环境都能执行
    const data = ref('Hello SSR')
    return { data }
  }
}

关键挑战:

  1. 环境差异:Node.js vs 浏览器环境
  2. 生命周期:服务器没有 DOM,只有部分生命周期
  3. 数据状态:服务器预取的数据需要传递到客户端
  4. 路由匹配:服务器需要根据 URL 匹配对应组件

三、 Vue3 SSR 核心 API 解析

3.1 renderToString - 核心渲染函数

import { renderToString } from 'vue/server-renderer'
import { createApp } from './app.js'

// 服务器渲染入口
async function renderApp(url) {
  // 1. 创建 Vue 应用实例
  const { app, router } = createApp()
  
  // 2. 设置服务器端路由
  router.push(url)
  await router.isReady()
  
  // 3. 渲染为 HTML 字符串
  const html = await renderToString(app)
  
  // 4. 返回完整的 HTML
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue3 SSR App</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
  `
}

3.2 createSSRApp - 创建 SSR 应用

// app.js - 同构应用创建
import { createSSRApp } from 'vue'
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './routes'

// 导出一个工厂函数,为每个请求创建新的应用实例
export function createApp() {
  const app = createSSRApp(App)
  
  // 根据环境使用不同的 history
  const router = createRouter({
    history: import.meta.env.SSR 
      ? createMemoryHistory()  // 服务器用 memory history
      : createWebHistory(),    // 客户端用 web history
    routes
  })
  
  app.use(router)
  return { app, router }
}

3.3 useSSRContext - 服务器上下文

import { useSSRContext } from 'vue'

// 在组件中访问 SSR 上下文
export default {
  setup() {
    if (import.meta.env.SSR) {
      // 只在服务器端执行
      const ctx = useSSRContext()
      ctx.title = '动态标题'
    }
  }
}

四、 完整 Vue3 SSR 项目实战

让我们构建一个完整的 Vue3 SSR 项目来演示所有概念。

4.1 项目结构

vue3-ssr-project/
├── src/
│   ├── client/          # 客户端入口
│   │   └── entry-client.js
│   ├── server/          # 服务器入口
│   │   └── entry-server.js
│   ├── components/      # 共享组件
│   │   ├── Layout.vue
│   │   └── PostList.vue
│   ├── router/          # 路由配置
│   │   └── index.js
│   ├── stores/          # 状态管理
│   │   └── postStore.js
│   └── App.vue
├── index.html           # HTML 模板
├── server.js           # Express 服务器
└── vite.config.js      # Vite 配置

4.2 共享应用创建 (app.js)

// src/app.js
import { createSSRApp } from 'vue'
import { createRouter } from './router'
import { createPinia } from 'pinia'
import App from './App.vue'

// 导出一个工厂函数,为每个请求创建新的应用实例
export function createApp() {
  const app = createSSRApp(App)
  const router = createRouter()
  const pinia = createPinia()
  
  app.use(router)
  app.use(pinia)
  
  return { app, router, pinia }
}

4.3 路由配置

// src/router/index.js
import { createRouter as _createRouter, createMemoryHistory, createWebHistory } from 'vue-router'

// 路由配置
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../components/Home.vue'),
    meta: {
      ssr: true // 标记需要 SSR
    }
  },
  {
    path: '/posts',
    name: 'Posts',
    component: () => import('../components/PostList.vue'),
    meta: {
      ssr: true,
      preload: true // 需要数据预取
    }
  },
  {
    path: '/posts/:id',
    name: 'PostDetail',
    component: () => import('../components/PostDetail.vue'),
    meta: {
      ssr: true,
      preload: true
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../components/About.vue'),
    meta: {
      ssr: false // 不需要 SSR
    }
  }
]

export function createRouter() {
  return _createRouter({
    history: import.meta.env.SSR 
      ? createMemoryHistory() 
      : createWebHistory(),
    routes
  })
}

4.4 Pinia 状态管理

// src/stores/postStore.js
import { defineStore } from 'pinia'

// 模拟 API 调用
const fetchPosts = async () => {
  await new Promise(resolve => setTimeout(resolve, 100))
  return [
    { id: 1, title: 'Vue3 SSR 入门指南', content: '学习 Vue3 服务端渲染...', views: 152 },
    { id: 2, title: 'Pinia 状态管理', content: 'Vue3 推荐的状态管理方案...', views: 98 },
    { id: 3, title: 'Vite 构建工具', content: '下一代前端构建工具...', views: 76 }
  ]
}

const fetchPostDetail = async (id) => {
  await new Promise(resolve => setTimeout(resolve, 50))
  const posts = await fetchPosts()
  return posts.find(post => post.id === parseInt(id))
}

export const usePostStore = defineStore('post', {
  state: () => ({
    posts: [],
    currentPost: null,
    loading: false
  }),
  
  actions: {
    async loadPosts() {
      this.loading = true
      try {
        this.posts = await fetchPosts()
      } finally {
        this.loading = false
      }
    },
    
    async loadPostDetail(id) {
      this.loading = true
      try {
        this.currentPost = await fetchPostDetail(id)
      } finally {
        this.loading = false
      }
    },
    
    // 服务器端数据预取
    async serverInit(route) {
      if (route.name === 'Posts') {
        await this.loadPosts()
      } else if (route.name === 'PostDetail') {
        await this.loadPostDetail(route.params.id)
      }
    }
  }
})

4.5 服务器入口

// src/server/entry-server.js
import { renderToString } from 'vue/server-renderer'
import { createApp } from '../app.js'
import { usePostStore } from '../stores/postStore'

export async function render(url) {
  const { app, router, pinia } = createApp()
  
  // 设置服务器端路由位置
  router.push(url)
  
  // 等待路由准备完成
  await router.isReady()
  
  // 获取匹配的路由
  const matchedComponents = router.currentRoute.value.matched
  const route = router.currentRoute.value
  
  // 数据预取 - 执行组件的 asyncData 或 store 的 serverInit
  const postStore = usePostStore(pinia)
  await postStore.serverInit(route)
  
  // 获取需要预取数据的组件
  const componentsWithPreload = matchedComponents.map(component => 
    component.components?.default || component
  ).filter(component => component.asyncData)
  
  // 执行组件的 asyncData 方法
  const preloadPromises = componentsWithPreload.map(component => 
    component.asyncData({
      store: pinia,
      route: router.currentRoute.value
    })
  )
  
  await Promise.all(preloadPromises)
  
  // 渲染应用为 HTML 字符串
  const ctx = {}
  const html = await renderToString(app, ctx)
  
  // 获取 Pinia 状态,用于客户端注水
  const state = JSON.stringify(pinia.state.value)
  
  return { html, state }
}

4.6 客户端入口

// src/client/entry-client.js
import { createApp } from '../app.js'
import { usePostStore } from '../stores/postStore'

const { app, router, pinia } = createApp()

// 恢复服务器状态
if (window.__PINIA_STATE__) {
  pinia.state.value = window.__PINIA_STATE__
}

// 等待路由准备完成
router.isReady().then(() => {
  // 挂载应用
  app.mount('#app')
  
  console.log('客户端激活完成')
})

// 客户端特定逻辑
if (!import.meta.env.SSR) {
  // 添加客户端特定的事件监听等
  const postStore = usePostStore()
  
  // 监听路由变化,在客户端获取数据
  router.beforeEach((to, from, next) => {
    if (to.meta.preload && !postStore.posts.length) {
      postStore.serverInit(to).then(next)
    } else {
      next()
    }
  })
}

4.7 Vue 组件示例

App.vue - 根组件

<template>
  <div id="app">
    <Layout>
      <RouterView />
    </Layout>
  </div>
</template>

<script setup>
import Layout from './components/Layout.vue'
</script>

Layout.vue - 布局组件

<template>
  <div class="layout">
    <header class="header">
      <nav class="nav">
        <RouterLink to="/" class="nav-link">首页</RouterLink>
        <RouterLink to="/posts" class="nav-link">文章列表</RouterLink>
        <RouterLink to="/about" class="nav-link">关于</RouterLink>
      </nav>
    </header>
    
    <main class="main">
      <slot />
    </main>
    
    <footer class="footer">
      <p>&copy; 2024 Vue3 SSR 演示</p>
    </footer>
  </div>
</template>

<script setup>
import { RouterLink } from 'vue-router'
</script>

<style scoped>
.layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.header {
  background: #2c3e50;
  padding: 1rem 0;
}

.nav {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2rem;
  display: flex;
  gap: 2rem;
}

.nav-link {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  transition: background 0.3s;
}

.nav-link:hover,
.nav-link.router-link-active {
  background: #34495e;
}

.main {
  flex: 1;
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
  width: 100%;
}

.footer {
  background: #ecf0f1;
  padding: 1rem;
  text-align: center;
  color: #7f8c8d;
}
</style>

PostList.vue - 文章列表组件

<template>
  <div class="post-list">
    <h1>文章列表</h1>
    
    <div v-if="postStore.loading" class="loading">
      加载中...
    </div>
    
    <div v-else class="posts">
      <article 
        v-for="post in postStore.posts" 
        :key="post.id"
        class="post-card"
      >
        <h2>
          <RouterLink :to="`/posts/${post.id}`" class="post-link">
            {{ post.title }}
          </RouterLink>
        </h2>
        <p class="post-content">{{ post.content }}</p>
        <div class="post-meta">
          <span>浏览量: {{ post.views }}</span>
        </div>
      </article>
    </div>
  </div>
</template>

<script setup>
import { usePostStore } from '../stores/postStore'
import { onServerPrefetch, onMounted } from 'vue'

const postStore = usePostStore()

// 服务器端数据预取
onServerPrefetch(async () => {
  await postStore.loadPosts()
})

// 客户端数据获取(如果服务器没有预取)
onMounted(async () => {
  if (postStore.posts.length === 0) {
    await postStore.loadPosts()
  }
})

// 传统 asyncData 方式(可选)
export const asyncData = async ({ store, route }) => {
  const postStore = usePostStore(store)
  await postStore.loadPosts()
}
</script>

<style scoped>
.post-list {
  max-width: 800px;
  margin: 0 auto;
}

.loading {
  text-align: center;
  padding: 2rem;
  color: #7f8c8d;
}

.posts {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.post-card {
  border: 1px solid #e1e8ed;
  border-radius: 8px;
  padding: 1.5rem;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  transition: transform 0.2s, box-shadow 0.2s;
}

.post-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.post-link {
  color: #2c3e50;
  text-decoration: none;
}

.post-link:hover {
  color: #42b883;
  text-decoration: underline;
}

.post-content {
  color: #5a6c7d;
  line-height: 1.6;
  margin: 1rem 0;
}

.post-meta {
  border-top: 1px solid #e1e8ed;
  padding-top: 1rem;
  color: #7f8c8d;
  font-size: 0.9rem;
}
</style>

4.8 Express 服务器

// server.js
import express from 'express'
import { fileURLToPath } from 'url'
import { dirname, resolve } from 'path'
import { render } from './dist/server/entry-server.js'

const __dirname = dirname(fileURLToPath(import.meta.url))
const app = express()

// 静态文件服务
app.use('/assets', express.static(resolve(__dirname, './dist/client/assets')))

// SSR 路由处理
app.get('*', async (req, res) => {
  try {
    const { html, state } = await render(req.url)
    
    const template = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue3 SSR 演示</title>
    <link rel="stylesheet" href="/assets/style.css">
</head>
<body>
    <div id="app">${html}</div>
    <script>
      // 将服务器状态传递到客户端
      window.__PINIA_STATE__ = ${state}
    </script>
    <script type="module" src="/assets/entry-client.js"></script>
</body>
</html>`
    
    res.status(200).set({ 'Content-Type': 'text/html' }).end(template)
  } catch (error) {
    console.error('SSR 渲染错误:', error)
    res.status(500).end('服务器内部错误')
  }
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`🚀 服务器运行在 http://localhost:${PORT}`)
})

4.9 Vite 构建配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  
  build: {
    rollupOptions: {
      input: {
        client: resolve(__dirname, 'src/client/entry-client.js'),
        server: resolve(__dirname, 'src/server/entry-server.js')
      },
      output: {
        format: 'esm',
        entryFileNames: (chunk) => {
          return chunk.name === 'server' ? 'server/[name].js' : 'client/assets/[name]-[hash].js'
        }
      }
    }
  },
  
  ssr: {
    noExternal: ['pinia']
  }
})

五、 数据预取与状态同步

5.1 数据预取策略

流程图:数据预取与状态同步流程

flowchart TD
    A[用户请求] --> B[服务器路由匹配]
    B --> C[识别需要预取的组件]
    C --> D[执行asyncData/store预取]
    D --> E[所有数据预取完成]
    E --> F[渲染HTML]
    F --> G[序列化状态到window]
    G --> H[返回HTML+状态]
    
    H --> I[浏览器渲染]
    I --> J[客户端激活]
    J --> K[反序列化状态]
    K --> L[Hydration完成]

5.2 多种数据预取方式

方式一:使用 onServerPrefetch

<template>
  <div>
    <h1>用户资料</h1>
    <div v-if="user">{{ user.name }}</div>
  </div>
</template>

<script setup>
import { ref, onServerPrefetch } from 'vue'

const user = ref(null)

const fetchUserData = async () => {
  // 模拟 API 调用
  await new Promise(resolve => setTimeout(resolve, 100))
  user.value = { name: '张三', id: 1, email: 'zhangsan@example.com' }
}

// 服务器端预取
onServerPrefetch(async () => {
  await fetchUserData()
})

// 客户端获取(如果服务器没有预取)
import { onMounted } from 'vue'
onMounted(async () => {
  if (!user.value) {
    await fetchUserData()
  }
})
</script>

方式二:使用 Store 统一管理

// stores/userStore.js
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    loading: false
  }),
  
  actions: {
    async fetchUser(id) {
      this.loading = true
      try {
        // 实际项目中这里调用 API
        await new Promise(resolve => setTimeout(resolve, 100))
        this.user = { id, name: '用户' + id, email: `user${id}@example.com` }
      } finally {
        this.loading = false
      }
    },
    
    // 服务器端初始化
    async serverInit(route) {
      if (route.name === 'UserProfile' && route.params.id) {
        await this.fetchUser(route.params.id)
      }
    }
  }
})

六、 性能优化与最佳实践

6.1 缓存策略

// server.js - 添加缓存
import lru-cache from 'lru-cache'

const ssrCache = new lru-cache({
  max: 100, // 缓存100个页面
  ttl: 1000 * 60 * 15 // 15分钟
})

app.get('*', async (req, res) => {
  // 检查缓存
  const cacheKey = req.url
  if (ssrCache.has(cacheKey)) {
    console.log('使用缓存:', cacheKey)
    return res.send(ssrCache.get(cacheKey))
  }
  
  try {
    const { html, state } = await render(req.url)
    const template = generateTemplate(html, state)
    
    // 缓存结果(排除需要动态内容的页面)
    if (!req.url.includes('/admin') && !req.url.includes('/user')) {
      ssrCache.set(cacheKey, template)
    }
    
    res.send(template)
  } catch (error) {
    // 错误处理
  }
})

6.2 流式渲染

// 流式渲染示例
import { renderToNodeStream } from 'vue/server-renderer'

app.get('*', async (req, res) => {
  const { app, router } = createApp()
  
  router.push(req.url)
  await router.isReady()
  
  res.write(`
    <!DOCTYPE html>
    <html>
      <head><title>Vue3 SSR</title></head>
      <body><div id="app">
  `)
  
  const stream = renderToNodeStream(app)
  stream.pipe(res, { end: false })
  
  stream.on('end', () => {
    res.write(`</div><script src="/client.js"></script></body></html>`)
    res.end()
  })
})

6.3 错误处理

// 错误边界组件
const ErrorBoundary = {
  setup(props, { slots }) {
    const error = ref(null)
    
    onErrorCaptured((err) => {
      error.value = err
      return false // 阻止错误继续传播
    })
    
    return () => error.value 
      ? h('div', { class: 'error' }, '组件渲染错误')
      : slots.default?.()
  }
}

七、 Nuxt.js 3 - 更简单的 SSR 方案

对于大多数项目,推荐使用 Nuxt.js 3,它基于 Vue3 提供了开箱即用的 SSR 支持。

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  modules: ['@pinia/nuxt'],
  runtimeConfig: {
    public: {
      apiBase: process.env.API_BASE || '/api'
    }
  }
})
<!-- pages/index.vue -->
<template>
  <div>
    <h1>Nuxt 3 SSR</h1>
    <div>{{ data }}</div>
  </div>
</template>

<script setup>
// 自动处理 SSR 数据获取
const { data } = await useFetch('/api/posts')
</script>

八、 总结

8.1 Vue3 SSR 核心优势

  1. 更好的性能:首屏加载速度快,减少白屏时间
  2. SEO 友好:搜索引擎可以直接索引内容
  3. 同构开发:一套代码,两端运行
  4. 现代化 API:更好的 TypeScript 支持和组合式 API 集成

8.2 适用场景

  • 内容型网站:博客、新闻、电商等需要 SEO 的场景
  • 企业官网:需要快速首屏加载和良好 SEO
  • 社交应用:需要社交媒体分享预览
  • 需要性能优化的 SPA

8.3 注意事项

  1. 服务器负载:SSR 会增加服务器 CPU 和内存消耗
  2. 开发复杂度:需要处理环境差异和状态同步
  3. 缓存策略:需要合理设计缓存机制
  4. 错误处理:需要完善的错误边界和降级方案

Vue3 的服务端渲染为现代 Web 应用提供了强大的能力,合理使用可以显著提升用户体验和应用性能。希望本文能帮助你全面掌握 Vue3 SSR 的核心概念和实践技巧!


如果这篇文章对你有帮助,欢迎点赞、收藏和评论!有任何问题都可以在评论区讨论。在这里插入图片描述

Vue3 事件修饰符深度解析:从基础到高级应用的完整指南

摘要

事件修饰符是 Vue.js 中一个强大而优雅的特性,它允许我们以声明式的方式处理 DOM 事件细节。Vue3 在保留所有 Vue2 事件修饰符的基础上,还引入了一些新的修饰符。本文将深入探讨所有事件修饰符的工作原理、使用场景和最佳实践,通过详细的代码示例、执行流程分析和实际应用案例,帮助你彻底掌握 Vue3 事件修饰符的完整知识体系。


一、 什么是事件修饰符?为什么需要它?

1.1 传统事件处理的问题

在原生 JavaScript 中处理事件时,我们经常需要编写重复的样板代码:

// 原生 JavaScript 事件处理
element.addEventListener('click', function(event) {
  // 阻止默认行为
  event.preventDefault()
  
  // 停止事件传播
  event.stopPropagation()
  
  // 执行业务逻辑
  handleClick()
})

传统方式的问题:

  • 代码冗余:每个事件处理函数都需要重复调用 preventDefault()stopPropagation()
  • 关注点混合:事件处理逻辑与 DOM 操作细节混合在一起
  • 可读性差:代码意图不够清晰明确
  • 维护困难:修改事件行为需要深入函数内部

1.2 Vue 事件修饰符的解决方案

Vue 的事件修饰符提供了一种声明式的解决方案:

<template>
  <!-- 使用事件修饰符 -->
  <a @click.prevent.stop="handleClick" href="/about">关于我们</a>
</template>

事件修饰符的优势:

  • 代码简洁:以声明式的方式表达事件行为
  • 关注点分离:业务逻辑与 DOM 细节分离
  • 可读性强:代码意图一目了然
  • 维护方便:修改事件行为只需改动模板

二、 Vue3 事件修饰符完整列表

2.1 事件修饰符分类总览

类别 修饰符 说明 Vue2 Vue3
事件传播 .stop 阻止事件冒泡
.capture 使用捕获模式
.self 仅当事件源是自身时触发
.once 只触发一次
.passive 不阻止默认行为
默认行为 .prevent 阻止默认行为
按键修饰 .enter Enter 键
.tab Tab 键
.delete 删除键
.esc Esc 键
.space 空格键
.up 上箭头
.down 下箭头
.left 左箭头
.right 右箭头
系统修饰 .ctrl Ctrl 键
.alt Alt 键
.shift Shift 键
.meta Meta 键
.exact 精确匹配系统修饰符
鼠标修饰 .left 鼠标左键
.right 鼠标右键
.middle 鼠标中键
Vue3 新增 .vue 自定义事件专用

三、 事件传播修饰符详解

3.1 事件传播的基本概念

流程图:DOM 事件传播机制

flowchart TD
    A[事件发生] --> B[捕获阶段 Capture Phase]
    B --> C[从window向下传递到目标]
    C --> D[目标阶段 Target Phase]
    D --> E[到达事件目标元素]
    E --> F[冒泡阶段 Bubble Phase]
    F --> G[从目标向上传递到window]
    
    B --> H[.capture 在此阶段触发]
    D --> I[.self 检查事件源]
    F --> J[.stop 阻止继续冒泡]

3.2 .stop - 阻止事件冒泡

<template>
  <div class="stop-modifier-demo">
    <h2>.stop 修饰符 - 阻止事件冒泡</h2>
    
    <div class="demo-area">
      <!-- 外层容器 -->
      <div class="outer-box" @click="handleOuterClick">
        <p>外层容器 (点击我会触发)</p>
        
        <!-- 内层容器 - 不使用 .stop -->
        <div class="inner-box" @click="handleInnerClick">
          <p>内层容器 - 无 .stop (点击我会触发内外两层)</p>
        </div>
        
        <!-- 内层容器 - 使用 .stop -->
        <div class="inner-box stop-demo" @click.stop="handleInnerClickStop">
          <p>内层容器 - 有 .stop (点击我只触发内层)</p>
        </div>
      </div>
    </div>

    <div class="event-logs">
      <h3>事件触发日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 10) {
    logs.value.pop()
  }
}

const handleOuterClick = (event) => {
  addLog('🟢 外层容器被点击')
  console.log('外层点击事件:', event)
}

const handleInnerClick = (event) => {
  addLog('🔵 内层容器被点击 (无.stop - 会冒泡)')
  console.log('内层点击事件 (无.stop):', event)
}

const handleInnerClickStop = (event) => {
  addLog('🔴 内层容器被点击 (有.stop - 阻止冒泡)')
  console.log('内层点击事件 (有.stop):', event)
}
</script>

<style scoped>
.stop-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.demo-area {
  margin: 20px 0;
}

.outer-box {
  padding: 30px;
  background: #e3f2fd;
  border: 2px solid #2196f3;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.3s;
}

.outer-box:hover {
  background: #bbdefb;
}

.outer-box p {
  margin: 0 0 15px 0;
  font-weight: bold;
  color: #1976d2;
}

.inner-box {
  padding: 20px;
  margin: 15px 0;
  background: #f3e5f5;
  border: 2px solid #9c27b0;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.3s;
}

.inner-box:hover {
  background: #e1bee7;
}

.inner-box p {
  margin: 0;
  color: #7b1fa2;
}

.stop-demo {
  background: #fff3e0;
  border-color: #ff9800;
}

.stop-demo p {
  color: #ef6c00;
}

.event-logs {
  margin-top: 30px;
  padding: 20px;
  background: #f5f5f5;
  border-radius: 8px;
}

.event-logs h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.log-item {
  padding: 8px 12px;
  margin: 5px 0;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #4caf50;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

3.3 .capture - 使用事件捕获模式

<template>
  <div class="capture-modifier-demo">
    <h2>.capture 修饰符 - 事件捕获模式</h2>
    
    <div class="demo-area">
      <!-- 捕获阶段触发 -->
      <div class="capture-box" @click.capture="handleCaptureClick">
        <p>捕获阶段容器 (使用 .capture)</p>
        
        <div class="target-box" @click="handleTargetClick">
          <p>目标元素 (正常冒泡阶段)</p>
        </div>
      </div>
    </div>

    <div class="explanation">
      <h3>执行顺序说明:</h3>
      <ol>
        <li>点击目标元素时,首先触发 <strong>.capture</strong> 阶段的事件</li>
        <li>然后触发目标元素自身的事件</li>
        <li>最后是冒泡阶段的事件 (本例中没有)</li>
      </ol>
    </div>

    <div class="event-logs">
      <h3>事件触发顺序:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
}

const handleCaptureClick = () => {
  addLog('1️⃣ 捕获阶段: 外层容器 (.capture)')
}

const handleTargetClick = () => {
  addLog('2️⃣ 目标阶段: 内层元素 (正常)')
}
</script>

<style scoped>
.capture-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.capture-box {
  padding: 30px;
  background: #fff3e0;
  border: 2px dashed #ff9800;
  border-radius: 8px;
}

.capture-box p {
  margin: 0 0 15px 0;
  color: #ef6c00;
  font-weight: bold;
}

.target-box {
  padding: 20px;
  background: #e8f5e8;
  border: 2px solid #4caf50;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.3s;
}

.target-box:hover {
  background: #c8e6c9;
}

.target-box p {
  margin: 0;
  color: #2e7d32;
}

.explanation {
  margin: 20px 0;
  padding: 20px;
  background: #e3f2fd;
  border-radius: 8px;
}

.explanation h3 {
  margin: 0 0 10px 0;
  color: #1976d2;
}

.explanation ol {
  margin: 0;
  color: #333;
}

.log-item {
  padding: 8px 12px;
  margin: 5px 0;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #ff9800;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

3.4 .self - 仅当事件源是自身时触发

<template>
  <div class="self-modifier-demo">
    <h2>.self 修饰符 - 仅自身触发</h2>
    
    <div class="demo-area">
      <!-- 不使用 .self -->
      <div class="container" @click="handleContainerClick">
        <p>普通容器 (点击子元素也会触发)</p>
        <button class="child-btn">子元素按钮</button>
      </div>
      
      <!-- 使用 .self -->
      <div class="container self-demo" @click.self="handleContainerSelfClick">
        <p>.self 容器 (只有点击容器本身才触发)</p>
        <button class="child-btn">子元素按钮</button>
      </div>
    </div>

    <div class="event-logs">
      <h3>事件触发日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 8) {
    logs.value.pop()
  }
}

const handleContainerClick = (event) => {
  addLog(`🔵 容器被点击 (target: ${event.target.tagName})`)
}

const handleContainerSelfClick = (event) => {
  addLog(`🔴 .self 容器被点击 (只有点击容器本身才触发)`)
}
</script>

<style scoped>
.self-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.demo-area {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin: 20px 0;
}

.container {
  padding: 25px;
  border: 2px solid #666;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.3s;
  min-height: 120px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.container:hover {
  background: #f5f5f5;
}

.container p {
  margin: 0 0 15px 0;
  font-weight: bold;
  text-align: center;
}

.self-demo {
  border-color: #e91e63;
  background: #fce4ec;
}

.self-demo:hover {
  background: #f8bbd9;
}

.child-btn {
  padding: 8px 16px;
  background: #2196f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.child-btn:hover {
  background: #1976d2;
}

.event-logs {
  margin-top: 30px;
  padding: 20px;
  background: #f5f5f5;
  border-radius: 8px;
}
</style>

3.5 .once - 只触发一次

<template>
  <div class="once-modifier-demo">
    <h2>.once 修饰符 - 只触发一次</h2>
    
    <div class="demo-area">
      <div class="button-group">
        <button @click="handleNormalClick" class="btn">
          普通按钮 (可重复点击)
        </button>
        <button @click.once="handleOnceClick" class="btn once-btn">
          .once 按钮 (只触发一次)
        </button>
      </div>
      
      <div class="counter-display">
        <div class="counter">
          <span>普通点击: </span>
          <strong>{{ normalCount }}</strong>
        </div>
        <div class="counter">
          <span>Once 点击: </span>
          <strong>{{ onceCount }}</strong>
        </div>
      </div>
    </div>

    <div class="event-logs">
      <h3>事件触发日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const normalCount = ref(0)
const onceCount = ref(0)
const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 6) {
    logs.value.pop()
  }
}

const handleNormalClick = () => {
  normalCount.value++
  addLog(`🔵 普通按钮点击: ${normalCount.value}`)
}

const handleOnceClick = () => {
  onceCount.value++
  addLog(`🔴 ONCE 按钮点击: ${onceCount.value} (只会显示一次!)`)
}
</script>

<style scoped>
.once-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.button-group {
  display: flex;
  gap: 20px;
  justify-content: center;
  margin: 30px 0;
}

.btn {
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.3s;
}

.btn:first-child {
  background: #2196f3;
  color: white;
}

.btn:first-child:hover {
  background: #1976d2;
  transform: translateY(-2px);
}

.once-btn {
  background: #ff9800;
  color: white;
}

.once-btn:hover {
  background: #f57c00;
  transform: translateY(-2px);
}

.counter-display {
  display: flex;
  justify-content: center;
  gap: 40px;
  margin: 20px 0;
}

.counter {
  padding: 15px 25px;
  background: white;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  text-align: center;
  min-width: 150px;
}

.counter span {
  display: block;
  color: #666;
  margin-bottom: 5px;
}

.counter strong {
  font-size: 24px;
  color: #333;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #2196f3;
  font-family: 'Courier New', monospace;
}

.log-item:contains('ONCE') {
  border-left-color: #ff9800;
}
</style>

3.6 .passive - 不阻止默认行为

<template>
  <div class="passive-modifier-demo">
    <h2>.passive 修饰符 - 不阻止默认行为</h2>
    
    <div class="demo-area">
      <div class="scroll-container">
        <div class="scroll-content">
          <div v-for="n in 50" :key="n" class="scroll-item">
            项目 {{ n }}
          </div>
        </div>
      </div>
      
      <div class="control-info">
        <p>尝试滚动上面的区域,观察控制台输出:</p>
        <ul>
          <li>使用 <code>.passive</code> 的事件处理函数不会调用 <code>preventDefault()</code></li>
          <li>这可以提升滚动性能,特别是移动端</li>
        </ul>
      </div>
    </div>

    <div class="performance-metrics">
      <h3>性能指标:</h3>
      <div class="metrics">
        <div class="metric">
          <span>滚动事件触发次数:</span>
          <strong>{{ scrollCount }}</strong>
        </div>
        <div class="metric">
          <span>阻塞时间:</span>
          <strong>{{ blockTime }}ms</strong>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const scrollCount = ref(0)
const blockTime = ref(0)

onMounted(() => {
  const scrollContainer = document.querySelector('.scroll-container')
  
  // 模拟阻塞操作
  const heavyOperation = () => {
    const start = performance.now()
    let result = 0
    for (let i = 0; i < 1000000; i++) {
      result += Math.random()
    }
    const end = performance.now()
    blockTime.value = (end - start).toFixed(2)
    return result
  }
  
  // 添加 passive 事件监听器
  scrollContainer.addEventListener('scroll', (event) => {
    scrollCount.value++
    heavyOperation()
    console.log('passive 滚动事件 - 不会阻止默认行为')
  }, { passive: true })
})
</script>

<style scoped>
.passive-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.scroll-container {
  height: 200px;
  border: 2px solid #ddd;
  border-radius: 8px;
  overflow-y: scroll;
  margin: 20px 0;
}

.scroll-content {
  padding: 10px;
}

.scroll-item {
  padding: 15px;
  margin: 5px 0;
  background: #f5f5f5;
  border-radius: 4px;
  border-left: 4px solid #4caf50;
}

.control-info {
  padding: 20px;
  background: #e3f2fd;
  border-radius: 8px;
  margin: 20px 0;
}

.control-info p {
  margin: 0 0 10px 0;
  font-weight: bold;
}

.control-info ul {
  margin: 0;
  color: #333;
}

.control-info code {
  background: #fff;
  padding: 2px 6px;
  border-radius: 3px;
  font-family: 'Courier New', monospace;
}

.performance-metrics {
  padding: 20px;
  background: #fff3e0;
  border-radius: 8px;
}

.performance-metrics h3 {
  margin: 0 0 15px 0;
  color: #e65100;
}

.metrics {
  display: flex;
  gap: 30px;
}

.metric {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.metric span {
  color: #666;
  margin-bottom: 5px;
}

.metric strong {
  font-size: 24px;
  color: #e65100;
}
</style>

四、 默认行为修饰符

4.1 .prevent - 阻止默认行为

<template>
  <div class="prevent-modifier-demo">
    <h2>.prevent 修饰符 - 阻止默认行为</h2>
    
    <div class="demo-area">
      <div class="form-group">
        <h3>表单提交示例</h3>
        <form @submit="handleFormSubmit" class="prevent-form">
          <input v-model="username" placeholder="请输入用户名" class="form-input" />
          <button type="submit" class="btn">普通提交</button>
          <button type="submit" @click.prevent="handlePreventSubmit" class="btn prevent-btn">
            使用 .prevent
          </button>
        </form>
      </div>
      
      <div class="link-group">
        <h3>链接点击示例</h3>
        <a href="https://vuejs.org" @click="handleLinkClick" class="link">
          普通链接 (会跳转)
        </a>
        <a href="https://vuejs.org" @click.prevent="handlePreventLinkClick" class="link prevent-link">
          使用 .prevent (不会跳转)
        </a>
      </div>
    </div>

    <div class="event-logs">
      <h3>事件触发日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const username = ref('')
const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 8) {
    logs.value.pop()
  }
}

const handleFormSubmit = (event) => {
  addLog('📝 表单提交 (页面会刷新)')
  // 这里可以添加表单验证等逻辑
}

const handlePreventSubmit = () => {
  addLog('🛑 表单提交 (使用 .prevent,页面不会刷新)')
  // 在这里处理 AJAX 提交等逻辑
  if (username.value) {
    addLog(`✅ 提交用户名: ${username.value}`)
  }
}

const handleLinkClick = () => {
  addLog('🔗 链接点击 (会跳转到 Vue.js 官网)')
}

const handlePreventLinkClick = () => {
  addLog('🚫 链接点击 (使用 .prevent,不会跳转)')
  // 可以在这里处理路由跳转等逻辑
}
</script>

<style scoped>
.prevent-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.form-group, .link-group {
  margin: 30px 0;
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
}

.form-group h3, .link-group h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.prevent-form {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

.form-input {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  flex: 1;
  min-width: 200px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.btn:first-of-type {
  background: #2196f3;
  color: white;
}

.prevent-btn {
  background: #ff5722;
  color: white;
}

.btn:hover {
  opacity: 0.9;
}

.link-group {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.link {
  padding: 12px 20px;
  background: #e3f2fd;
  border: 1px solid #2196f3;
  border-radius: 4px;
  text-decoration: none;
  color: #1976d2;
  text-align: center;
  transition: background 0.3s;
}

.prevent-link {
  background: #ffebee;
  border-color: #f44336;
  color: #d32f2f;
}

.link:hover {
  background: #bbdefb;
}

.prevent-link:hover {
  background: #ffcdd2;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #2196f3;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

五、 按键修饰符详解

5.1 常用按键修饰符

<template>
  <div class="key-modifier-demo">
    <h2>按键修饰符 - 键盘事件处理</h2>
    
    <div class="demo-area">
      <div class="input-group">
        <h3>输入框按键事件</h3>
        <input 
          v-model="inputText"
          @keyup.enter="handleEnter"
          @keyup.tab="handleTab"
          @keyup.delete="handleDelete"
          @keyup.esc="handleEsc"
          @keyup.space="handleSpace"
          placeholder="尝试按 Enter、Tab、Delete、Esc、Space 键"
          class="key-input"
        />
      </div>
      
      <div class="arrow-group">
        <h3>方向键控制</h3>
        <div class="arrow-controls">
          <div class="arrow-row">
            <button @keyup.up="handleUp" class="arrow-btn up">↑</button>
          </div>
          <div class="arrow-row">
            <button @keyup.left="handleLeft" class="arrow-btn left">←</button>
            <button @keyup.down="handleDown" class="arrow-btn down">↓</button>
            <button @keyup.right="handleRight" class="arrow-btn right">→</button>
          </div>
        </div>
        <p>点击按钮后按方向键测试</p>
      </div>
    </div>

    <div class="key-logs">
      <h3>按键事件日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const inputText = ref('')
const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 10) {
    logs.value.pop()
  }
}

const handleEnter = (event) => {
  addLog('↵ Enter 键被按下')
  if (inputText.value.trim()) {
    addLog(`💾 保存内容: "${inputText.value}"`)
    inputText.value = ''
  }
}

const handleTab = () => {
  addLog('↹ Tab 键被按下')
}

const handleDelete = () => {
  addLog('⌫ Delete 键被按下')
}

const handleEsc = () => {
  addLog('⎋ Esc 键被按下 - 取消操作')
  inputText.value = ''
}

const handleSpace = () => {
  addLog('␣ Space 键被按下')
}

const handleUp = () => {
  addLog('↑ 上方向键 - 向上移动')
}

const handleDown = () => {
  addLog('↓ 下方向键 - 向下移动')
}

const handleLeft = () => {
  addLog('← 左方向键 - 向左移动')
}

const handleRight = () => {
  addLog('→ 右方向键 - 向右移动')
}
</script>

<style scoped>
.key-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.input-group, .arrow-group {
  margin: 30px 0;
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
}

.input-group h3, .arrow-group h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.key-input {
  width: 100%;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 6px;
  font-size: 16px;
  transition: border-color 0.3s;
}

.key-input:focus {
  outline: none;
  border-color: #2196f3;
}

.arrow-controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.arrow-row {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.arrow-btn {
  width: 60px;
  height: 60px;
  border: 2px solid #666;
  border-radius: 8px;
  background: white;
  font-size: 20px;
  cursor: pointer;
  transition: all 0.3s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.arrow-btn:focus {
  outline: none;
  background: #e3f2fd;
  border-color: #2196f3;
}

.arrow-btn:hover {
  transform: scale(1.1);
}

.up { border-color: #4caf50; color: #4caf50; }
.down { border-color: #2196f3; color: #2196f3; }
.left { border-color: #ff9800; color: #ff9800; }
.right { border-color: #9c27b0; color: #9c27b0; }

.arrow-group p {
  text-align: center;
  margin: 15px 0 0 0;
  color: #666;
  font-style: italic;
}

.key-logs {
  margin-top: 30px;
  padding: 20px;
  background: #2c3e50;
  border-radius: 8px;
  color: white;
}

.key-logs h3 {
  margin: 0 0 15px 0;
  color: #42b883;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: #34495e;
  border-radius: 4px;
  border-left: 4px solid #42b883;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

5.2 系统修饰符

<template>
  <div class="system-modifier-demo">
    <h2>系统修饰符 - 组合键处理</h2>
    
    <div class="demo-area">
      <div class="modifier-group">
        <h3>系统修饰符测试</h3>
        <div class="key-combinations">
          <div class="key-item" @click.ctrl="handleCtrlClick">
            Ctrl + 点击
          </div>
          <div class="key-item" @click.alt="handleAltClick">
            Alt + 点击
          </div>
          <div class="key-item" @click.shift="handleShiftClick">
            Shift + 点击
          </div>
          <div class="key-item" @click.meta="handleMetaClick">
            Meta (Cmd) + 点击
          </div>
        </div>
      </div>
      
      <div class="exact-modifier">
        <h3>.exact 修饰符 - 精确匹配</h3>
        <div class="exact-combinations">
          <button @click="handleAnyClick" class="exact-btn">
            任意点击
          </button>
          <button @click.ctrl="handleCtrlOnlyClick" class="exact-btn">
            Ctrl + 点击
          </button>
          <button @click.ctrl.exact="handleExactCtrlClick" class="exact-btn exact">
            .exact Ctrl (仅 Ctrl)
          </button>
        </div>
      </div>
    </div>

    <div class="system-logs">
      <h3>系统修饰符事件日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 8) {
    logs.value.pop()
  }
}

const handleCtrlClick = () => {
  addLog('🎛️ Ctrl + 点击')
}

const handleAltClick = () => {
  addLog('⎇ Alt + 点击')
}

const handleShiftClick = () => {
  addLog('⇧ Shift + 点击')
}

const handleMetaClick = () => {
  addLog('⌘ Meta (Cmd) + 点击')
}

const handleAnyClick = () => {
  addLog('🔄 任意点击 (无修饰符)')
}

const handleCtrlOnlyClick = () => {
  addLog('🎛️ Ctrl + 点击 (可能包含其他修饰符)')
}

const handleExactCtrlClick = () => {
  addLog('🎛️ .exact Ctrl + 点击 (仅 Ctrl,无其他修饰符)')
}
</script>

<style scoped>
.system-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.modifier-group, .exact-modifier {
  margin: 30px 0;
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
}

.modifier-group h3, .exact-modifier h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.key-combinations {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 15px;
}

.key-item {
  padding: 20px;
  background: white;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
  font-weight: bold;
}

.key-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.key-item:nth-child(1) { border-color: #2196f3; color: #2196f3; }
.key-item:nth-child(2) { border-color: #ff9800; color: #ff9800; }
.key-item:nth-child(3) { border-color: #4caf50; color: #4caf50; }
.key-item:nth-child(4) { border-color: #9c27b0; color: #9c27b0; }

.exact-combinations {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}

.exact-btn {
  padding: 12px 20px;
  border: 2px solid #666;
  border-radius: 6px;
  background: white;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.exact-btn:hover {
  background: #f5f5f5;
}

.exact-btn.exact {
  border-color: #e91e63;
  color: #e91e63;
  font-weight: bold;
}

.system-logs {
  margin-top: 30px;
  padding: 20px;
  background: #2c3e50;
  border-radius: 8px;
  color: white;
}

.system-logs h3 {
  margin: 0 0 15px 0;
  color: #42b883;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: #34495e;
  border-radius: 4px;
  border-left: 4px solid #42b883;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

六、 鼠标按键修饰符

6.1 鼠标按键修饰符使用

<template>
  <div class="mouse-modifier-demo">
    <h2>鼠标按键修饰符</h2>
    
    <div class="demo-area">
      <div class="mouse-test-area">
        <div 
          class="click-zone"
          @click.left="handleLeftClick"
          @click.middle="handleMiddleClick"
          @click.right="handleRightClick"
        >
          <p>在此区域测试鼠标按键:</p>
          <ul>
            <li>左键点击 - 正常点击</li>
            <li>中键点击 - 鼠标滚轮点击</li>
            <li>右键点击 - 弹出上下文菜单</li>
          </ul>
        </div>
      </div>
      
      <div class="context-menu-info">
        <p><strong>注意:</strong>右键点击时,使用 <code>.prevent</code> 可以阻止浏览器默认的上下文菜单:</p>
        <div 
          class="prevent-context-zone"
          @click.right.prevent="handlePreventRightClick"
        >
          右键点击这里不会显示浏览器菜单
        </div>
      </div>
    </div>

    <div class="mouse-logs">
      <h3>鼠标事件日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 6) {
    logs.value.pop()
  }
}

const handleLeftClick = () => {
  addLog('🖱️ 鼠标左键点击')
}

const handleMiddleClick = () => {
  addLog('🎯 鼠标中键点击')
}

const handleRightClick = (event) => {
  addLog('📋 鼠标右键点击 (会显示浏览器上下文菜单)')
}

const handlePreventRightClick = () => {
  addLog('🚫 鼠标右键点击 (使用 .prevent,不显示浏览器菜单)')
  // 可以在这里显示自定义上下文菜单
}
</script>

<style scoped>
.mouse-modifier-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.mouse-test-area {
  margin: 30px 0;
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
}

.click-zone {
  padding: 40px;
  background: white;
  border: 3px dashed #2196f3;
  border-radius: 8px;
  text-align: center;
  cursor: pointer;
  transition: background 0.3s;
}

.click-zone:hover {
  background: #e3f2fd;
}

.click-zone p {
  margin: 0 0 15px 0;
  font-weight: bold;
  color: #1976d2;
}

.click-zone ul {
  text-align: left;
  display: inline-block;
  margin: 0;
  color: #333;
}

.click-zone li {
  margin: 8px 0;
}

.context-menu-info {
  margin: 30px 0;
  padding: 25px;
  background: #fff3e0;
  border-radius: 8px;
}

.context-menu-info p {
  margin: 0 0 15px 0;
  color: #e65100;
}

.prevent-context-zone {
  padding: 20px;
  background: #ffebee;
  border: 2px solid #f44336;
  border-radius: 6px;
  text-align: center;
  cursor: pointer;
  font-weight: bold;
  color: #d32f2f;
}

.mouse-logs {
  margin-top: 30px;
  padding: 20px;
  background: #2c3e50;
  border-radius: 8px;
  color: white;
}

.mouse-logs h3 {
  margin: 0 0 15px 0;
  color: #42b883;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: #34495e;
  border-radius: 4px;
  border-left: 4px solid #42b883;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

七、 事件修饰符的组合使用

7.1 修饰符链式调用

<template>
  <div class="combined-modifier-demo">
    <h2>事件修饰符组合使用</h2>
    
    <div class="demo-area">
      <div class="combination-examples">
        <div class="example">
          <h3>1. 阻止冒泡 + 阻止默认行为</h3>
          <a 
            href="#"
            @click.prevent.stop="handlePreventStop"
            class="combined-link"
          >
            @click.prevent.stop
          </a>
          <p>既阻止链接跳转,又阻止事件冒泡</p>
        </div>
        
        <div class="example">
          <h3>2. 捕获阶段 + 只触发一次</h3>
          <div 
            @click.capture.once="handleCaptureOnce"
            class="capture-once-box"
          >
            @click.capture.once
            <button>内部按钮</button>
          </div>
          <p>在捕获阶段触发,且只触发一次</p>
        </div>
        
        <div class="example">
          <h3>3. 精确组合键 + 阻止默认</h3>
          <button 
            @keydown.ctrl.exact.prevent="handleExactCtrlPrevent"
            class="exact-ctrl-btn"
          >
            聚焦后按 Ctrl (精确)
          </button>
          <p>精确匹配 Ctrl 键,阻止默认行为</p>
        </div>
        
        <div class="example">
          <h3>4. 自身检查 + 阻止冒泡</h3>
          <div 
            @click.self.stop="handleSelfStop"
            class="self-stop-box"
          >
            @click.self.stop
            <button>点击按钮不会触发</button>
          </div>
          <p>只有点击容器本身才触发,并阻止冒泡</p>
        </div>
      </div>
    </div>

    <div class="combination-logs">
      <h3>组合修饰符事件日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const logs = ref([])

const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 8) {
    logs.value.pop()
  }
}

const handlePreventStop = () => {
  addLog('🔗 prevent.stop: 阻止跳转和冒泡')
}

const handleCaptureOnce = () => {
  addLog('🎯 capture.once: 捕获阶段触发,只触发一次')
}

const handleExactCtrlPrevent = (event) => {
  addLog('⌨️ ctrl.exact.prevent: 精确 Ctrl,阻止默认行为')
  event.preventDefault()
}

const handleSelfStop = () => {
  addLog('🎯 self.stop: 仅自身触发,阻止冒泡')
}
</script>

<style scoped>
.combined-modifier-demo {
  padding: 20px;
  max-width: 1000px;
  margin: 0 auto;
}

.combination-examples {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin: 30px 0;
}

.example {
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e9ecef;
}

.example h3 {
  margin: 0 0 15px 0;
  color: #333;
  font-size: 16px;
}

.example p {
  margin: 10px 0 0 0;
  color: #666;
  font-size: 14px;
  font-style: italic;
}

.combined-link {
  display: block;
  padding: 12px;
  background: #e3f2fd;
  border: 2px solid #2196f3;
  border-radius: 6px;
  text-decoration: none;
  color: #1976d2;
  text-align: center;
  font-weight: bold;
  transition: background 0.3s;
}

.combined-link:hover {
  background: #bbdefb;
}

.capture-once-box {
  padding: 20px;
  background: #fff3e0;
  border: 2px solid #ff9800;
  border-radius: 6px;
  text-align: center;
  cursor: pointer;
  font-weight: bold;
  color: #e65100;
}

.capture-once-box button {
  margin-top: 10px;
  padding: 8px 16px;
  background: #ff9800;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.exact-ctrl-btn {
  width: 100%;
  padding: 12px;
  background: #fce4ec;
  border: 2px solid #e91e63;
  border-radius: 6px;
  color: #c2185b;
  font-weight: bold;
  cursor: pointer;
  transition: background 0.3s;
}

.exact-ctrl-btn:focus {
  outline: none;
  background: #f8bbd9;
}

.self-stop-box {
  padding: 20px;
  background: #e8f5e8;
  border: 2px solid #4caf50;
  border-radius: 6px;
  text-align: center;
  cursor: pointer;
  font-weight: bold;
  color: #2e7d32;
}

.self-stop-box button {
  margin-top: 10px;
  padding: 8px 16px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.combination-logs {
  margin-top: 30px;
  padding: 20px;
  background: #2c3e50;
  border-radius: 8px;
  color: white;
}

.combination-logs h3 {
  margin: 0 0 15px 0;
  color: #42b883;
}

.log-item {
  padding: 10px 15px;
  margin: 5px 0;
  background: #34495e;
  border-radius: 4px;
  border-left: 4px solid #42b883;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

八、 最佳实践和注意事项

8.1 事件修饰符最佳实践

<template>
  <div class="best-practices-demo">
    <h2>事件修饰符最佳实践</h2>
    
    <div class="practices">
      <div class="practice-item good">
        <h3>✅ 推荐做法</h3>
        <div class="code-example">
          <pre><code>&lt;!-- 清晰的修饰符顺序 --&gt;
&lt;form @submit.prevent.stop="handleSubmit"&gt;
&lt;!-- 适当的修饰符组合 --&gt;
&lt;a @click.prevent="handleLinkClick"&gt;
&lt;!-- 使用 .exact 精确控制 --&gt;
&lt;button @keyup.ctrl.exact="handleExactCtrl"&gt;</code></pre>
        </div>
      </div>
      
      <div class="practice-item bad">
        <h3>❌ 避免做法</h3>
        <div class="code-example">
          <pre><code>&lt;!-- 过度使用修饰符 --&gt;
&lt;button @click.prevent.stop.self="handleClick"&gt;
&lt;!-- 混淆的修饰符顺序 --&gt;
&lt;form @submit.stop.prevent="handleSubmit"&gt;
&lt;!-- 不必要的修饰符 --&gt;
&lt;div @click.self.prevent="handleClick"&gt;</code></pre>
        </div>
      </div>
    </div>

    <div class="performance-tips">
      <h3>性能提示</h3>
      <ul>
        <li>使用 <code>.passive</code> 改善滚动性能,特别是移动端</li>
        <li>避免在频繁触发的事件上使用复杂的修饰符组合</li>
        <li>使用 <code>.once</code> 清理不需要持续监听的事件</li>
        <li>合理使用 <code>.prevent</code> 避免不必要的默认行为阻止</li>
      </ul>
    </div>

    <div class="accessibility-considerations">
      <h3>可访问性考虑</h3>
      <ul>
        <li>确保键盘导航支持所有交互功能</li>
        <li>使用适当的 ARIA 标签描述交互行为</li>
        <li>测试屏幕阅读器兼容性</li>
        <li>提供键盘快捷键的视觉提示</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
// 最佳实践示例代码
const handleSubmit = () => {
  console.log('表单提交处理')
}

const handleLinkClick = () => {
  console.log('链接点击处理')
}

const handleExactCtrl = () => {
  console.log('精确 Ctrl 键处理')
}
</script>

<style scoped>
.best-practices-demo {
  padding: 20px;
  max-width: 900px;
  margin: 0 auto;
}

.practices {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 30px;
  margin: 30px 0;
}

.practice-item {
  padding: 25px;
  border-radius: 8px;
}

.practice-item.good {
  background: #e8f5e8;
  border: 2px solid #4caf50;
}

.practice-item.bad {
  background: #ffebee;
  border: 2px solid #f44336;
}

.practice-item h3 {
  margin: 0 0 15px 0;
  color: inherit;
}

.code-example {
  background: white;
  border-radius: 6px;
  padding: 15px;
  overflow-x: auto;
}

.code-example pre {
  margin: 0;
  font-family: 'Courier New', monospace;
  font-size: 14px;
  line-height: 1.4;
}

.performance-tips, .accessibility-considerations {
  margin: 30px 0;
  padding: 25px;
  background: #e3f2fd;
  border-radius: 8px;
}

.performance-tips h3, .accessibility-considerations h3 {
  margin: 0 0 15px 0;
  color: #1976d2;
}

.performance-tips ul, .accessibility-considerations ul {
  margin: 0;
  color: #333;
}

.performance-tips li, .accessibility-considerations li {
  margin: 8px 0;
}

.performance-tips code, .accessibility-considerations code {
  background: #fff;
  padding: 2px 6px;
  border-radius: 3px;
  font-family: 'Courier New', monospace;
  font-size: 12px;
}
</style>

九、 总结

9.1 事件修饰符核心价值

  1. 声明式编程:以声明的方式表达事件行为意图
  2. 代码简洁:减少样板代码,提高开发效率
  3. 可读性强:代码意图一目了然
  4. 维护方便:修改事件行为只需改动模板

9.2 事件修饰符分类总结

类别 主要修饰符 使用场景
事件传播 .stop .capture .self 控制事件传播流程
默认行为 .prevent .passive 管理浏览器默认行为
按键处理 .enter .tab .esc 键盘交互处理
系统修饰 .ctrl .alt .shift .meta 组合键操作
精确控制 .exact 精确匹配修饰符
鼠标按键 .left .right .middle 区分鼠标按键
次数控制 .once 一次性事件处理

9.3 最佳实践要点

  1. 合理排序:按照 .capture.once.passive.prevent.stop.self 的顺序
  2. 适度使用:避免过度复杂的修饰符组合
  3. 性能考虑:在频繁事件上使用 .passive
  4. 可访问性:确保键盘导航支持所有功能

Vue3 的事件修饰符提供了一种优雅而强大的方式来处理 DOM 事件细节,通过合理使用这些修饰符,可以编写出更加简洁、可读和可维护的 Vue 代码。


如果这篇文章对你有帮助,欢迎点赞、收藏和评论!有任何问题都可以在评论区讨论。在这里插入图片描述

JavaScript性能与优化:手写实现关键优化技术

引言

在前端开发中,性能优化不仅仅是使用现成的库和工具,理解其底层原理并能够手写实现是关键。通过手写这些优化技术,我们可以:

  • 更深入地理解性能瓶颈
  • 根据具体场景定制优化方案
  • 避免引入不必要的依赖
  • 提升解决问题的能力

本文将深入探讨JavaScript性能优化的核心手写实现,每个技术点都将包含完整的实现代码和应用场景。

一、虚拟列表实现(Virtual List)

虚拟列表是处理大数据列表渲染的核心技术,通过只渲染可视区域内的元素来大幅提升性能。

1.1 核心原理
class VirtualList {
  constructor(options) {
    this.container = options.container;
    this.itemHeight = options.itemHeight;
    this.totalItems = options.totalItems;
    this.bufferSize = options.bufferSize || 5; // 上下缓冲区域
    this.renderItem = options.renderItem;
    
    this.visibleItems = [];
    this.startIndex = 0;
    this.endIndex = 0;
    
    this.init();
  }

  init() {
    // 创建容器
    this.viewport = document.createElement('div');
    this.viewport.style.position = 'relative';
    this.viewport.style.height = `${this.totalItems * this.itemHeight}px`;
    this.viewport.style.overflow = 'hidden';
    
    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.left = '0';
    this.content.style.width = '100%';
    
    this.container.appendChild(this.viewport);
    this.viewport.appendChild(this.content);
    
    // 绑定滚动事件
    this.viewport.addEventListener('scroll', this.handleScroll.bind(this));
    
    // 初始渲染
    this.calculateVisibleRange();
    this.renderVisibleItems();
  }

  calculateVisibleRange() {
    const scrollTop = this.viewport.scrollTop;
    const visibleHeight = this.viewport.clientHeight;
    
    // 计算可视区域起始和结束索引
    this.startIndex = Math.max(
      0,
      Math.floor(scrollTop / this.itemHeight) - this.bufferSize
    );
    
    this.endIndex = Math.min(
      this.totalItems - 1,
      Math.ceil((scrollTop + visibleHeight) / this.itemHeight) + this.bufferSize
    );
  }

  renderVisibleItems() {
    // 移除不在可视区域的元素
    this.visibleItems.forEach(item => {
      if (item.index < this.startIndex || item.index > this.endIndex) {
        item.element.remove();
      }
    });
    
    // 更新可见项数组
    this.visibleItems = this.visibleItems.filter(
      item => item.index >= this.startIndex && item.index <= this.endIndex
    );
    
    // 创建新的可见项
    for (let i = this.startIndex; i <= this.endIndex; i++) {
      const existingItem = this.visibleItems.find(item => item.index === i);
      
      if (!existingItem) {
        const itemElement = document.createElement('div');
        itemElement.style.position = 'absolute';
        itemElement.style.top = `${i * this.itemHeight}px`;
        itemElement.style.height = `${this.itemHeight}px`;
        itemElement.style.width = '100%';
        
        this.renderItem(itemElement, i);
        
        this.content.appendChild(itemElement);
        this.visibleItems.push({ index: i, element: itemElement });
      }
    }
    
    // 更新内容区域位置
    this.content.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`;
  }

  handleScroll() {
    requestAnimationFrame(() => {
      this.calculateVisibleRange();
      this.renderVisibleItems();
    });
  }

  updateItem(index, data) {
    const item = this.visibleItems.find(item => item.index === index);
    if (item) {
      this.renderItem(item.element, index, data);
    }
  }

  destroy() {
    this.viewport.removeEventListener('scroll', this.handleScroll);
    this.container.removeChild(this.viewport);
  }
}

// 使用示例
const listContainer = document.getElementById('list-container');

const virtualList = new VirtualList({
  container: listContainer,
  itemHeight: 50,
  totalItems: 10000,
  bufferSize: 10,
  renderItem: (element, index) => {
    element.textContent = `Item ${index + 1}`;
    element.style.borderBottom = '1px solid #eee';
    element.style.padding = '10px';
  }
});

// 动态更新
setTimeout(() => {
  virtualList.updateItem(5, 'Updated Item 6');
}, 2000);
1.2 带动态高度的虚拟列表
class DynamicVirtualList {
  constructor(options) {
    this.container = options.container;
    this.totalItems = options.totalItems;
    this.renderItem = options.renderItem;
    this.estimateHeight = options.estimateHeight || 50;
    this.bufferSize = options.bufferSize || 5;
    
    this.itemHeights = new Array(this.totalItems).fill(null);
    this.itemPositions = new Array(this.totalItems).fill(0);
    this.visibleItems = [];
    this.cachedItems = new Map();
    
    this.init();
  }

  init() {
    this.viewport = document.createElement('div');
    this.viewport.style.position = 'relative';
    this.viewport.style.height = '500px';
    this.viewport.style.overflow = 'auto';
    
    this.content = document.createElement('div');
    this.content.style.position = 'relative';
    
    this.viewport.appendChild(this.content);
    this.container.appendChild(this.viewport);
    
    // 计算预估的总高度
    this.calculatePositions();
    this.updateContentHeight();
    
    this.viewport.addEventListener('scroll', this.handleScroll.bind(this));
    
    // 初始渲染
    this.calculateVisibleRange();
    this.renderVisibleItems();
  }

  calculatePositions() {
    let totalHeight = 0;
    for (let i = 0; i < this.totalItems; i++) {
      this.itemPositions[i] = totalHeight;
      totalHeight += this.itemHeights[i] || this.estimateHeight;
    }
    this.totalHeight = totalHeight;
  }

  updateContentHeight() {
    this.content.style.height = `${this.totalHeight}px`;
  }

  calculateVisibleRange() {
    const scrollTop = this.viewport.scrollTop;
    const viewportHeight = this.viewport.clientHeight;
    
    // 二分查找起始索引
    let start = 0;
    let end = this.totalItems - 1;
    
    while (start <= end) {
      const mid = Math.floor((start + end) / 2);
      if (this.itemPositions[mid] <= scrollTop) {
        start = mid + 1;
      } else {
        end = mid - 1;
      }
    }
    
    this.startIndex = Math.max(0, end - this.bufferSize);
    
    // 查找结束索引
    let currentHeight = scrollTop;
    this.endIndex = this.startIndex;
    
    while (
      this.endIndex < this.totalItems &&
      currentHeight < scrollTop + viewportHeight
    ) {
      currentHeight += this.itemHeights[this.endIndex] || this.estimateHeight;
      this.endIndex++;
    }
    
    this.endIndex = Math.min(
      this.totalItems - 1,
      this.endIndex + this.bufferSize
    );
  }

  renderVisibleItems() {
    // 更新可见项
    const newVisibleItems = [];
    
    for (let i = this.startIndex; i <= this.endIndex; i++) {
      let itemElement = this.cachedItems.get(i);
      
      if (!itemElement) {
        itemElement = document.createElement('div');
        itemElement.style.position = 'absolute';
        itemElement.style.top = `${this.itemPositions[i]}px`;
        itemElement.style.width = '100%';
        
        this.renderItem(itemElement, i);
        this.cachedItems.set(i, itemElement);
        this.content.appendChild(itemElement);
        
        // 测量实际高度
        if (this.itemHeights[i] === null) {
          this.itemHeights[i] = itemElement.offsetHeight;
          this.calculatePositions();
          this.updateContentHeight();
          
          // 重新计算位置
          itemElement.style.top = `${this.itemPositions[i]}px`;
        }
      }
      
      newVisibleItems.push({ index: i, element: itemElement });
    }
    
    // 隐藏不在可视区域的元素
    this.visibleItems.forEach(({ index, element }) => {
      if (index < this.startIndex || index > this.endIndex) {
        element.style.display = 'none';
      }
    });
    
    // 显示可见元素
    newVisibleItems.forEach(({ index, element }) => {
      element.style.display = '';
      element.style.top = `${this.itemPositions[index]}px`;
    });
    
    this.visibleItems = newVisibleItems;
  }

  handleScroll() {
    requestAnimationFrame(() => {
      this.calculateVisibleRange();
      this.renderVisibleItems();
    });
  }

  updateItem(index, data) {
    const itemElement = this.cachedItems.get(index);
    if (itemElement) {
      const oldHeight = this.itemHeights[index] || this.estimateHeight;
      
      this.renderItem(itemElement, index, data);
      
      const newHeight = itemElement.offsetHeight;
      if (oldHeight !== newHeight) {
        this.itemHeights[index] = newHeight;
        this.calculatePositions();
        this.updateContentHeight();
        this.renderVisibleItems();
      }
    }
  }
}

二、图片懒加载(Lazy Loading)

2.1 基于IntersectionObserver的实现
class LazyImageLoader {
  constructor(options = {}) {
    this.options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
      placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
      errorImage: null,
      loadingClass: 'lazy-loading',
      loadedClass: 'lazy-loaded',
      errorClass: 'lazy-error',
      ...options
    };
    
    this.images = new Map();
    this.observer = null;
    this.fallbackTimeout = 3000; // 降级超时时间
    
    this.init();
  }

  init() {
    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver(
        this.handleIntersection.bind(this),
        this.options
      );
    } else {
      this.useFallback();
    }
    
    // 预连接DNS和预加载
    this.addPreconnect();
  }

  addPreconnect() {
    const domains = new Set();
    
    // 收集所有图片的域名
    document.querySelectorAll('img[data-src]').forEach(img => {
      const src = img.getAttribute('data-src');
      if (src) {
        try {
          const url = new URL(src, window.location.origin);
          domains.add(url.origin);
        } catch (e) {
          console.warn('Invalid URL:', src);
        }
      }
    });
    
    // 添加preconnect链接
    domains.forEach(domain => {
      const link = document.createElement('link');
      link.rel = 'preconnect';
      link.href = domain;
      link.crossOrigin = 'anonymous';
      document.head.appendChild(link);
    });
  }

  registerImage(imgElement) {
    if (!(imgElement instanceof HTMLImageElement)) {
      throw new Error('Element must be an image');
    }

    const src = imgElement.getAttribute('data-src');
    if (!src) return;

    // 保存原始属性
    imgElement.setAttribute('data-lazy-src', src);
    
    // 设置占位符
    if (imgElement.src !== this.options.placeholder) {
      imgElement.setAttribute('data-original-src', imgElement.src);
      imgElement.src = this.options.placeholder;
    }
    
    imgElement.classList.add(this.options.loadingClass);
    
    // 添加到观察列表
    this.images.set(imgElement, {
      src,
      loaded: false,
      loadAttempted: false,
      observerAttached: false
    });
    
    this.attachObserver(imgElement);
  }

  attachObserver(imgElement) {
    if (this.observer && !this.images.get(imgElement)?.observerAttached) {
      this.observer.observe(imgElement);
      this.images.get(imgElement).observerAttached = true;
    }
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        this.loadImage(img);
        this.observer?.unobserve(img);
      }
    });
  }

  async loadImage(imgElement) {
    const imageData = this.images.get(imgElement);
    if (!imageData || imageData.loadAttempted) return;

    imageData.loadAttempted = true;
    
    // 移除加载类,添加加载中类
    imgElement.classList.remove(this.options.loadingClass);
    imgElement.classList.add(this.options.loadingClass);
    
    // 创建加载超时
    const loadTimeout = setTimeout(() => {
      if (!imageData.loaded) {
        this.handleImageError(imgElement, new Error('Image load timeout'));
      }
    }, this.fallbackTimeout);
    
    try {
      // 预加载图片
      await this.preloadImage(imageData.src);
      
      // 应用图片
      this.applyImage(imgElement, imageData.src);
      
      clearTimeout(loadTimeout);
      
      // 更新状态
      imageData.loaded = true;
      imgElement.classList.remove(this.options.loadingClass);
      imgElement.classList.add(this.options.loadedClass);
      
      // 触发事件
      this.dispatchEvent(imgElement, 'lazyload', { src: imageData.src });
      
      // 预加载相邻图片
      this.preloadAdjacentImages(imgElement);
      
    } catch (error) {
      this.handleImageError(imgElement, error);
    }
  }

  async preloadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      
      img.onload = () => {
        img.onload = img.onerror = null;
        resolve(img);
      };
      
      img.onerror = (err) => {
        img.onload = img.onerror = null;
        reject(new Error(`Failed to load image: ${src}`));
      };
      
      // 设置crossOrigin属性
      if (src.startsWith('http')) {
        img.crossOrigin = 'anonymous';
      }
      
      img.src = src;
    });
  }

  applyImage(imgElement, src) {
    // 使用requestAnimationFrame确保流畅
    requestAnimationFrame(() => {
      imgElement.src = src;
      
      // 如果有srcset也更新
      const srcset = imgElement.getAttribute('data-srcset');
      if (srcset) {
        imgElement.srcset = srcset;
        imgElement.removeAttribute('data-srcset');
      }
      
      // 移除data-src属性
      imgElement.removeAttribute('data-src');
      imgElement.removeAttribute('data-lazy-src');
    });
  }

  preloadAdjacentImages(currentImg) {
    const allImages = Array.from(this.images.keys());
    const currentIndex = allImages.indexOf(currentImg);
    
    if (currentIndex !== -1) {
      // 预加载前后各2张图片
      const indices = [
        currentIndex - 2, currentIndex - 1,
        currentIndex + 1, currentIndex + 2
      ];
      
      indices.forEach(index => {
        if (index >= 0 && index < allImages.length) {
          const img = allImages[index];
          const imgData = this.images.get(img);
          
          if (!imgData.loaded && !imgData.loadAttempted) {
            this.attachObserver(img);
          }
        }
      });
    }
  }

  handleImageError(imgElement, error) {
    const imageData = this.images.get(imgElement);
    
    imgElement.classList.remove(this.options.loadingClass);
    imgElement.classList.add(this.options.errorClass);
    
    // 设置错误图片
    if (this.options.errorImage) {
      imgElement.src = this.options.errorImage;
    }
    
    // 恢复原始图片(如果有)
    const originalSrc = imgElement.getAttribute('data-original-src');
    if (originalSrc && originalSrc !== this.options.placeholder) {
      imgElement.src = originalSrc;
    }
    
    console.error('Lazy image load error:', error);
    this.dispatchEvent(imgElement, 'lazyloaderror', { 
      src: imageData?.src, 
      error 
    });
  }

  dispatchEvent(element, eventName, detail) {
    const event = new CustomEvent(eventName, { 
      bubbles: true,
      detail 
    });
    element.dispatchEvent(event);
  }

  useFallback() {
    // 降级方案:滚动监听
    window.addEventListener('scroll', this.handleScrollFallback.bind(this));
    window.addEventListener('resize', this.handleScrollFallback.bind(this));
    window.addEventListener('orientationchange', this.handleScrollFallback.bind(this));
    
    // 初始检查
    setTimeout(() => this.handleScrollFallback(), 100);
  }

  handleScrollFallback() {
    const viewportHeight = window.innerHeight;
    const scrollTop = window.scrollY;
    
    this.images.forEach((imageData, imgElement) => {
      if (!imageData.loaded && !imageData.loadAttempted) {
        const rect = imgElement.getBoundingClientRect();
        const elementTop = rect.top + scrollTop;
        const elementBottom = rect.bottom + scrollTop;
        
        // 判断是否在可视区域内(带缓冲区)
        if (
          elementBottom >= scrollTop - 500 && 
          elementTop <= scrollTop + viewportHeight + 500
        ) {
          this.loadImage(imgElement);
        }
      }
    });
  }

  // 批量注册
  registerAll(selector = 'img[data-src]') {
    const images = document.querySelectorAll(selector);
    images.forEach(img => this.registerImage(img));
    
    // 监听动态添加的图片
    if ('MutationObserver' in window) {
      this.mutationObserver = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === 1) { // 元素节点
              if (node.matches && node.matches(selector)) {
                this.registerImage(node);
              }
              if (node.querySelectorAll) {
                node.querySelectorAll(selector).forEach(img => {
                  this.registerImage(img);
                });
              }
            }
          });
        });
      });
      
      this.mutationObserver.observe(document.body, {
        childList: true,
        subtree: true
      });
    }
  }

  // 手动触发加载
  loadImageNow(imgElement) {
    if (this.images.has(imgElement)) {
      this.loadImage(imgElement);
    }
  }

  // 销毁
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
    
    if (this.mutationObserver) {
      this.mutationObserver.disconnect();
    }
    
    window.removeEventListener('scroll', this.handleScrollFallback);
    window.removeEventListener('resize', this.handleScrollFallback);
    window.removeEventListener('orientationchange', this.handleScrollFallback);
    
    this.images.clear();
  }
}

// 使用示例
const lazyLoader = new LazyImageLoader({
  threshold: 0.01,
  placeholder: '/path/to/placeholder.jpg',
  errorImage: '/path/to/error.jpg'
});

// 注册所有懒加载图片
document.addEventListener('DOMContentLoaded', () => {
  lazyLoader.registerAll();
});

// 动态添加图片
const newImage = document.createElement('img');
newImage.setAttribute('data-src', '/path/to/image.jpg');
document.body.appendChild(newImage);
lazyLoader.registerImage(newImage);
2.2 背景图片懒加载
class LazyBackgroundLoader extends LazyImageLoader {
  constructor(options = {}) {
    super(options);
    this.attributeName = options.attributeName || 'data-bg';
  }

  registerElement(element) {
    const bgSrc = element.getAttribute(this.attributeName);
    if (!bgSrc) return;

    this.images.set(element, {
      src: bgSrc,
      loaded: false,
      loadAttempted: false,
      observerAttached: false
    });

    this.attachObserver(element);
  }

  async loadImage(element) {
    const elementData = this.images.get(element);
    if (!elementData || elementData.loadAttempted) return;

    elementData.loadAttempted = true;

    try {
      await this.preloadImage(elementData.src);
      
      requestAnimationFrame(() => {
        element.style.backgroundImage = `url("${elementData.src}")`;
        element.removeAttribute(this.attributeName);
        
        elementData.loaded = true;
        element.classList.add(this.options.loadedClass);
        
        this.dispatchEvent(element, 'lazyload', { src: elementData.src });
      });
    } catch (error) {
      this.handleImageError(element, error);
    }
  }

  registerAll(selector = `[${this.attributeName}]`) {
    const elements = document.querySelectorAll(selector);
    elements.forEach(el => this.registerElement(el));
  }
}

三、函数记忆化(Memoization)

3.1 基础记忆化实现
function memoize(fn, options = {}) {
  const {
    maxSize = Infinity,
    keyResolver = (...args) => JSON.stringify(args),
    ttl = null, // 生存时间(毫秒)
    cache = new Map()
  } = options;
  
  const stats = {
    hits: 0,
    misses: 0,
    size: 0
  };
  
  // 创建LRU缓存(最近最少使用)
  const lruKeys = [];
  
  const memoized = function(...args) {
    const key = keyResolver(...args);
    
    // 检查缓存
    if (cache.has(key)) {
      const entry = cache.get(key);
      
      // 检查TTL
      if (ttl && Date.now() - entry.timestamp > ttl) {
        cache.delete(key);
        stats.size--;
        stats.misses++;
      } else {
        // 更新LRU顺序
        if (maxSize < Infinity) {
          const index = lruKeys.indexOf(key);
          if (index > -1) {
            lruKeys.splice(index, 1);
            lruKeys.unshift(key);
          }
        }
        
        stats.hits++;
        return entry.value;
      }
    }
    
    // 计算新值
    stats.misses++;
    const result = fn.apply(this, args);
    
    // 缓存结果
    const entry = {
      value: result,
      timestamp: Date.now()
    };
    
    cache.set(key, entry);
    stats.size++;
    
    // 处理LRU缓存
    if (maxSize < Infinity) {
      lruKeys.unshift(key);
      
      if (cache.size > maxSize) {
        const lruKey = lruKeys.pop();
        cache.delete(lruKey);
        stats.size--;
      }
    }
    
    return result;
  };
  
  // 添加工具方法
  memoized.clear = function() {
    cache.clear();
    lruKeys.length = 0;
    stats.hits = stats.misses = stats.size = 0;
  };
  
  memoized.delete = function(...args) {
    const key = keyResolver(...args);
    const deleted = cache.delete(key);
    if (deleted) {
      const index = lruKeys.indexOf(key);
      if (index > -1) lruKeys.splice(index, 1);
      stats.size--;
    }
    return deleted;
  };
  
  memoized.has = function(...args) {
    const key = keyResolver(...args);
    return cache.has(key);
  };
  
  memoized.getStats = function() {
    return { ...stats };
  };
  
  memoized.getCache = function() {
    return new Map(cache);
  };
  
  return memoized;
}

// 使用示例
function expensiveCalculation(n) {
  console.log('Calculating...', n);
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

const memoizedCalculation = memoize(expensiveCalculation, {
  maxSize: 100,
  ttl: 60000 // 1分钟缓存
});

// 第一次调用会计算
console.log(memoizedCalculation(10));
// 第二次调用直接从缓存读取
console.log(memoizedCalculation(10));

// 查看统计
console.log(memoizedCalculation.getStats());
3.2 异步函数记忆化
function memoizeAsync(fn, options = {}) {
  const {
    maxSize = Infinity,
    keyResolver = (...args) => JSON.stringify(args),
    ttl = null
  } = options;
  
  const cache = new Map();
  const pendingPromises = new Map();
  const lruKeys = [];
  
  const memoized = async function(...args) {
    const key = keyResolver(...args);
    
    // 检查缓存
    if (cache.has(key)) {
      const entry = cache.get(key);
      
      if (ttl && Date.now() - entry.timestamp > ttl) {
        cache.delete(key);
        const index = lruKeys.indexOf(key);
        if (index > -1) lruKeys.splice(index, 1);
      } else {
        if (maxSize < Infinity) {
          const index = lruKeys.indexOf(key);
          if (index > -1) {
            lruKeys.splice(index, 1);
            lruKeys.unshift(key);
          }
        }
        return entry.value;
      }
    }
    
    // 检查是否已经在执行中
    if (pendingPromises.has(key)) {
      return pendingPromises.get(key);
    }
    
    // 创建新的Promise
    const promise = (async () => {
      try {
        const result = await fn.apply(this, args);
        
        // 缓存结果
        cache.set(key, {
          value: result,
          timestamp: Date.now()
        });
        
        if (maxSize < Infinity) {
          lruKeys.unshift(key);
          
          if (cache.size > maxSize) {
            const lruKey = lruKeys.pop();
            cache.delete(lruKey);
          }
        }
        
        return result;
      } finally {
        pendingPromises.delete(key);
      }
    })();
    
    pendingPromises.set(key, promise);
    return promise;
  };
  
  memoized.clear = () => {
    cache.clear();
    pendingPromises.clear();
    lruKeys.length = 0;
  };
  
  memoized.delete = (...args) => {
    const key = keyResolver(...args);
    const deleted = cache.delete(key);
    if (deleted) {
      const index = lruKeys.indexOf(key);
      if (index > -1) lruKeys.splice(index, 1);
    }
    pendingPromises.delete(key);
    return deleted;
  };
  
  memoized.has = (...args) => {
    const key = keyResolver(...args);
    return cache.has(key);
  };
  
  return memoized;
}

// 使用示例
async function fetchUserData(userId) {
  console.log('Fetching user data for:', userId);
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

const memoizedFetchUserData = memoizeAsync(fetchUserData, {
  ttl: 30000 // 30秒缓存
});

// 多个组件同时请求同一个用户数据
Promise.all([
  memoizedFetchUserData(1),
  memoizedFetchUserData(1),
  memoizedFetchUserData(1)
]).then(results => {
  console.log('All results:', results);
  // 只会有一次实际的网络请求
});
3.3 React Hook记忆化
import { useRef, useCallback } from 'react';

function useMemoizedCallback(fn, dependencies = []) {
  const cacheRef = useRef(new Map());
  const fnRef = useRef(fn);
  
  // 更新函数引用
  fnRef.current = fn;
  
  const memoizedFn = useCallback((...args) => {
    const cache = cacheRef.current;
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fnRef.current(...args);
    cache.set(key, result);
    return result;
  }, dependencies);
  
  // 清理函数
  const clearCache = useCallback(() => {
    cacheRef.current.clear();
  }, []);
  
  return [memoizedFn, clearCache];
}

// React组件使用示例
function ExpensiveComponent({ data }) {
  const [processData, clearCache] = useMemoizedCallback(
    (item) => {
      // 昂贵的计算
      return item.value * Math.sqrt(item.weight);
    },
    [data]
  );
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>
          {processData(item)}
        </div>
      ))}
      <button onClick={clearCache}>Clear Cache</button>
    </div>
  );
}

四、请求防重和缓存(Deduplication & Caching)

4.1 请求防重系统
class RequestDeduplicator {
  constructor(options = {}) {
    this.options = {
      defaultTtl: 60000, // 默认缓存时间1分钟
      maxCacheSize: 100,
      ...options
    };
    
    this.pendingRequests = new Map();
    this.cache = new Map();
    this.cacheTimestamps = new Map();
    this.stats = {
      duplicatesPrevented: 0,
      cacheHits: 0,
      cacheMisses: 0
    };
  }

  generateKey(config) {
    // 生成请求的唯一标识符
    const { url, method = 'GET', params = {}, data = {} } = config;
    
    const keyParts = [
      method.toUpperCase(),
      url,
      JSON.stringify(params),
      JSON.stringify(data)
    ];
    
    return keyParts.join('|');
  }

  async request(config) {
    const key = this.generateKey(config);
    const now = Date.now();
    
    // 检查缓存
    if (this.cache.has(key)) {
      const { data, timestamp } = this.cache.get(key);
      const ttl = config.ttl || this.options.defaultTtl;
      
      if (now - timestamp < ttl) {
        this.stats.cacheHits++;
        return Promise.resolve(data);
      } else {
        // 缓存过期
        this.cache.delete(key);
        this.cacheTimestamps.delete(key);
      }
    }
    
    this.stats.cacheMisses++;
    
    // 检查是否有相同的请求正在进行中
    if (this.pendingRequests.has(key)) {
      this.stats.duplicatesPrevented++;
      return this.pendingRequests.get(key);
    }
    
    // 创建新的请求
    const requestPromise = this.executeRequest(config);
    
    // 存储进行中的请求
    this.pendingRequests.set(key, requestPromise);
    
    try {
      const result = await requestPromise;
      
      // 缓存成功的结果
      if (config.cache !== false) {
        this.cache.set(key, {
          data: result,
          timestamp: now
        });
        this.cacheTimestamps.set(key, now);
        
        // 清理过期的缓存
        this.cleanupCache();
      }
      
      return result;
    } finally {
      // 移除进行中的请求
      this.pendingRequests.delete(key);
    }
  }

  async executeRequest(config) {
    const { url, method = 'GET', params = {}, data = {}, headers = {} } = config;
    
    // 构建请求URL
    let requestUrl = url;
    if (params && Object.keys(params).length > 0) {
      const queryString = new URLSearchParams(params).toString();
      requestUrl += `?${queryString}`;
    }
    
    // 发送请求
    const response = await fetch(requestUrl, {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...headers
      },
      body: method !== 'GET' && method !== 'HEAD' ? JSON.stringify(data) : undefined
    });
    
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }
    
    return response.json();
  }

  cleanupCache() {
    const now = Date.now();
    const maxAge = this.options.defaultTtl;
    
    // 清理过期缓存
    for (const [key, timestamp] of this.cacheTimestamps.entries()) {
      if (now - timestamp > maxAge) {
        this.cache.delete(key);
        this.cacheTimestamps.delete(key);
      }
    }
    
    // 清理超出大小的缓存
    if (this.cache.size > this.options.maxCacheSize) {
      const sortedEntries = Array.from(this.cacheTimestamps.entries())
        .sort(([, a], [, b]) => a - b);
      
      const entriesToRemove = sortedEntries.slice(
        0,
        this.cache.size - this.options.maxCacheSize
      );
      
      entriesToRemove.forEach(([key]) => {
        this.cache.delete(key);
        this.cacheTimestamps.delete(key);
      });
    }
  }

  // 手动清理缓存
  clearCache(urlPattern) {
    if (urlPattern) {
      for (const key of this.cache.keys()) {
        if (key.includes(urlPattern)) {
          this.cache.delete(key);
          this.cacheTimestamps.delete(key);
        }
      }
    } else {
      this.cache.clear();
      this.cacheTimestamps.clear();
    }
  }

  // 预加载数据
  prefetch(config) {
    const key = this.generateKey(config);
    
    if (!this.cache.has(key) && !this.pendingRequests.has(key)) {
      this.request(config).catch(() => {
        // 静默失败,预加载不影响主流程
      });
    }
  }

  getStats() {
    return {
      ...this.stats,
      pendingRequests: this.pendingRequests.size,
      cachedResponses: this.cache.size
    };
  }
}

// 使用示例
const deduplicator = new RequestDeduplicator({
  defaultTtl: 30000, // 30秒
  maxCacheSize: 50
});

// 多个组件同时请求相同的数据
async function fetchUserProfile(userId) {
  const config = {
    url: `/api/users/${userId}`,
    method: 'GET',
    ttl: 60000 // 此请求特定缓存时间
  };
  
  return deduplicator.request(config);
}

// 在多个地方同时调用
Promise.all([
  fetchUserProfile(1),
  fetchUserProfile(1),
  fetchUserProfile(1)
]).then(results => {
  console.log('Results:', results);
  console.log('Stats:', deduplicator.getStats());
});

// 预加载
deduplicator.prefetch({
  url: '/api/products',
  params: { page: 1, limit: 20 }
});
4.2 分层缓存系统
class LayeredCache {
  constructor(options = {}) {
    this.layers = [];
    this.options = {
      defaultTtl: 300000, // 5分钟
      ...options
    };
    
    this.stats = {
      layerHits: {},
      totalHits: 0,
      totalMisses: 0
    };
  }

  addLayer(layer) {
    if (!layer.get || !layer.set || !layer.delete || !layer.clear) {
      throw new Error('Cache layer must implement get, set, delete, and clear methods');
    }
    
    this.layers.push(layer);
    this.stats.layerHits[layer.name || `layer_${this.layers.length}`] = 0;
  }

  async get(key) {
    this.stats.totalHits++;
    
    // 从上层开始查找
    for (let i = 0; i < this.layers.length; i++) {
      const layer = this.layers[i];
      const layerName = layer.name || `layer_${i + 1}`;
      
      try {
        const value = await layer.get(key);
        
        if (value !== undefined && value !== null) {
          // 命中,更新统计
          this.stats.layerHits[layerName] = (this.stats.layerHits[layerName] || 0) + 1;
          
          // 将数据复制到上层缓存(提升)
          for (let j = 0; j < i; j++) {
            this.layers[j].set(key, value, this.options.defaultTtl).catch(() => {});
          }
          
          return value;
        }
      } catch (error) {
        console.warn(`Cache layer ${layerName} error:`, error);
        // 继续尝试下一层
      }
    }
    
    this.stats.totalMisses++;
    return null;
  }

  async set(key, value, ttl = this.options.defaultTtl) {
    // 设置所有层级的缓存
    const promises = this.layers.map(layer => 
      layer.set(key, value, ttl).catch(() => {})
    );
    
    await Promise.all(promises);
    return value;
  }

  async delete(key) {
    // 删除所有层级的缓存
    const promises = this.layers.map(layer => 
      layer.delete(key).catch(() => {})
    );
    
    await Promise.all(promises);
  }

  async clear() {
    const promises = this.layers.map(layer => 
      layer.clear().catch(() => {})
    );
    
    await Promise.all(promises);
  }

  getStats() {
    return {
      ...this.stats,
      hitRate: this.stats.totalHits > 0 
        ? (this.stats.totalHits - this.stats.totalMisses) / this.stats.totalHits 
        : 0
    };
  }
}

// 内存缓存层实现
class MemoryCacheLayer {
  constructor(name = 'memory') {
    this.name = name;
    this.cache = new Map();
    this.timestamps = new Map();
  }

  async get(key) {
    const value = this.cache.get(key);
    
    if (value === undefined) return null;
    
    const timestamp = this.timestamps.get(key);
    if (timestamp && Date.now() > timestamp) {
      // 已过期
      this.cache.delete(key);
      this.timestamps.delete(key);
      return null;
    }
    
    return value;
  }

  async set(key, value, ttl) {
    this.cache.set(key, value);
    
    if (ttl) {
      this.timestamps.set(key, Date.now() + ttl);
    }
    
    return value;
  }

  async delete(key) {
    this.cache.delete(key);
    this.timestamps.delete(key);
  }

  async clear() {
    this.cache.clear();
    this.timestamps.clear();
  }
}

// IndexedDB缓存层实现
class IndexedDBCacheLayer {
  constructor(name = 'indexeddb', dbName = 'cache_db', storeName = 'cache_store') {
    this.name = name;
    this.dbName = dbName;
    this.storeName = storeName;
    this.db = null;
    
    this.initDB();
  }

  async initDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName);
        }
      };
      
      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve();
      };
      
      request.onerror = (event) => {
        reject(event.target.error);
      };
    });
  }

  async get(key) {
    if (!this.db) await this.initDB();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.get(key);
      
      request.onsuccess = () => {
        const result = request.result;
        if (result && result.expires && Date.now() > result.expires) {
          // 已过期,删除
          this.delete(key);
          resolve(null);
        } else {
          resolve(result ? result.value : null);
        }
      };
      
      request.onerror = () => reject(request.error);
    });
  }

  async set(key, value, ttl) {
    if (!this.db) await this.initDB();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      
      const item = {
        value,
        expires: ttl ? Date.now() + ttl : null,
        timestamp: Date.now()
      };
      
      const request = store.put(item, key);
      
      request.onsuccess = () => resolve(value);
      request.onerror = () => reject(request.error);
    });
  }

  async delete(key) {
    if (!this.db) await this.initDB();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.delete(key);
      
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async clear() {
    if (!this.db) await this.initDB();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.clear();
      
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

// 使用示例
const cache = new LayeredCache({
  defaultTtl: 300000 // 5分钟
});

// 添加内存缓存层(快速)
cache.addLayer(new MemoryCacheLayer('memory'));

// 添加IndexedDB缓存层(持久化)
cache.addLayer(new IndexedDBCacheLayer('indexeddb'));

// 使用缓存
async function getCachedData(key) {
  let data = await cache.get(key);
  
  if (!data) {
    // 从网络获取数据
    data = await fetchDataFromNetwork();
    await cache.set(key, data);
  }
  
  return data;
}

五、时间切片(Time Slicing)

5.1 基于requestIdleCallback的实现
class TimeSlicer {
  constructor(options = {}) {
    this.options = {
      timeout: 1000, // 超时时间
      taskChunkSize: 100, // 每个时间片处理的任务数
      ...options
    };
    
    this.tasks = [];
    this.isProcessing = false;
    this.currentIndex = 0;
    this.deferred = null;
    this.stats = {
      tasksProcessed: 0,
      timeSlicesUsed: 0,
      totalTime: 0
    };
  }

  addTask(task) {
    if (typeof task !== 'function') {
      throw new Error('Task must be a function');
    }
    
    this.tasks.push(task);
    return this;
  }

  addTasks(tasks) {
    tasks.forEach(task => this.addTask(task));
    return this;
  }

  process() {
    if (this.isProcessing) {
      return Promise.reject(new Error('Already processing'));
    }
    
    if (this.tasks.length === 0) {
      return Promise.resolve();
    }
    
    this.isProcessing = true;
    this.currentIndex = 0;
    this.stats.tasksProcessed = 0;
    this.stats.timeSlicesUsed = 0;
    this.stats.totalTime = 0;
    
    return new Promise((resolve, reject) => {
      this.deferred = { resolve, reject };
      this.processNextChunk();
    });
  }

  processNextChunk() {
    const startTime = performance.now();
    
    // 处理一个时间片的任务
    for (let i = 0; i < this.options.taskChunkSize; i++) {
      if (this.currentIndex >= this.tasks.length) {
        this.finishProcessing();
        return;
      }
      
      try {
        const task = this.tasks[this.currentIndex];
        task();
        this.currentIndex++;
        this.stats.tasksProcessed++;
      } catch (error) {
        this.handleError(error);
        return;
      }
    }
    
    const endTime = performance.now();
    this.stats.totalTime += endTime - startTime;
    
    // 检查是否还有任务
    if (this.currentIndex < this.tasks.length) {
      this.stats.timeSlicesUsed++;
      
      // 使用requestIdleCallback安排下一个时间片
      if ('requestIdleCallback' in window) {
        requestIdleCallback(
          () => this.processNextChunk(),
          { timeout: this.options.timeout }
        );
      } else {
        // 降级方案:使用setTimeout
        setTimeout(() => this.processNextChunk(), 0);
      }
    } else {
      this.finishProcessing();
    }
  }

  finishProcessing() {
    this.isProcessing = false;
    this.deferred.resolve({
      tasksProcessed: this.stats.tasksProcessed,
      timeSlicesUsed: this.stats.timeSlicesUsed,
      totalTime: this.stats.totalTime
    });
    this.deferred = null;
  }

  handleError(error) {
    this.isProcessing = false;
    this.deferred.reject(error);
    this.deferred = null;
  }

  clear() {
    this.tasks = [];
    this.isProcessing = false;
    this.currentIndex = 0;
  }

  getStats() {
    return { ...this.stats };
  }
}

// 使用示例
function createExpensiveTask(id) {
  return () => {
    console.log(`Processing task ${id}`);
    // 模拟耗时操作
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += Math.sqrt(i);
    }
    return sum;
  };
}

// 创建时间切片处理器
const slicer = new TimeSlicer({
  taskChunkSize: 50, // 每个时间片处理50个任务
  timeout: 2000 // 2秒超时
});

// 添加大量任务
for (let i = 0; i < 1000; i++) {
  slicer.addTask(createExpensiveTask(i));
}

// 开始处理
slicer.process().then(stats => {
  console.log('Processing completed:', stats);
}).catch(error => {
  console.error('Processing failed:', error);
});

// 可以在处理过程中添加新任务
setTimeout(() => {
  slicer.addTask(() => console.log('New task added during processing'));
}, 1000);
5.2 基于Generator的时间切片
function* taskGenerator(tasks) {
  for (let i = 0; i < tasks.length; i++) {
    yield tasks[i];
  }
}

class GeneratorTimeSlicer {
  constructor(options = {}) {
    this.options = {
      timePerSlice: 16, // 每个时间片16ms(大约一帧的时间)
      ...options
    };
    
    this.taskGenerator = null;
    this.isProcessing = false;
    this.stats = {
      tasksProcessed: 0,
      slicesUsed: 0,
      totalTime: 0
    };
  }

  processTasks(tasks) {
    if (this.isProcessing) {
      return Promise.reject(new Error('Already processing'));
    }
    
    this.isProcessing = true;
    this.taskGenerator = taskGenerator(tasks);
    this.stats = { tasksProcessed: 0, slicesUsed: 0, totalTime: 0 };
    
    return new Promise((resolve, reject) => {
      this.processNextSlice(resolve, reject);
    });
  }

  processNextSlice(resolve, reject) {
    if (!this.isProcessing) return;
    
    const sliceStart = performance.now();
    let taskResult;
    
    // 处理一个时间片
    while (true) {
      const { value: task, done } = this.taskGenerator.next();
      
      if (done) {
        this.isProcessing = false;
        resolve({
          ...this.stats,
          completed: true
        });
        return;
      }
      
      try {
        taskResult = task();
        this.stats.tasksProcessed++;
      } catch (error) {
        this.isProcessing = false;
        reject(error);
        return;
      }
      
      // 检查是否超过时间片限制
      if (performance.now() - sliceStart >= this.options.timePerSlice) {
        break;
      }
    }
    
    this.stats.slicesUsed++;
    this.stats.totalTime += performance.now() - sliceStart;
    
    // 安排下一个时间片
    if ('requestAnimationFrame' in window) {
      requestAnimationFrame(() => this.processNextSlice(resolve, reject));
    } else {
      setTimeout(() => this.processNextSlice(resolve, reject), 0);
    }
  }

  stop() {
    this.isProcessing = false;
  }
}

// 使用示例:处理大型数组
function processLargeArray(array, processItem, chunkSize = 100) {
  return new Promise((resolve) => {
    let index = 0;
    const results = [];
    
    function processChunk() {
      const chunkStart = performance.now();
      
      while (index < array.length) {
        results.push(processItem(array[index]));
        index++;
        
        // 检查是否处理了足够多的项目或时间到了
        if (index % chunkSize === 0 || 
            performance.now() - chunkStart > 16) {
          break;
        }
      }
      
      if (index < array.length) {
        // 还有更多项目,安排下一个时间片
        requestAnimationFrame(processChunk);
      } else {
        // 完成
        resolve(results);
      }
    }
    
    // 开始处理
    requestAnimationFrame(processChunk);
  });
}

// 处理10万个项目
const largeArray = Array.from({ length: 100000 }, (_, i) => i);

processLargeArray(largeArray, (item) => {
  // 对每个项目进行一些处理
  return item * Math.sqrt(item);
}, 1000).then(results => {
  console.log(`Processed ${results.length} items`);
});

六、函数节流与防抖(Throttle & Debounce)

6.1 高级节流与防抖实现
class AdvancedThrottleDebounce {
  // 节流:确保函数在一定时间内只执行一次
  static throttle(func, wait, options = {}) {
    let timeout = null;
    let previous = 0;
    let result;
    let context;
    let args;
    
    const { leading = true, trailing = true } = options;
    
    const later = () => {
      previous = !leading ? 0 : Date.now();
      timeout = null;
      if (trailing && args) {
        result = func.apply(context, args);
        context = args = null;
      }
    };
    
    const throttled = function(...params) {
      const now = Date.now();
      
      if (!previous && !leading) {
        previous = now;
      }
      
      const remaining = wait - (now - previous);
      context = this;
      args = params;
      
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        context = args = null;
      } else if (!timeout && trailing) {
        timeout = setTimeout(later, remaining);
      }
      
      return result;
    };
    
    throttled.cancel = () => {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };
    
    return throttled;
  }

  // 防抖:确保函数在最后一次调用后一定时间才执行
  static debounce(func, wait, options = {}) {
    let timeout = null;
    let result;
    let context;
    let args;
    let lastCallTime;
    let lastInvokeTime = 0;
    
    const { 
      leading = false, 
      trailing = true,
      maxWait 
    } = options;
    
    const invokeFunc = (time) => {
      lastInvokeTime = time;
      result = func.apply(context, args);
      context = args = null;
      return result;
    };
    
    const leadingEdge = (time) => {
      lastInvokeTime = time;
      
      if (trailing) {
        timeout = setTimeout(timerExpired, wait);
      }
      
      return leading ? invokeFunc(time) : result;
    };
    
    const remainingWait = (time) => {
      const timeSinceLastCall = time - lastCallTime;
      const timeSinceLastInvoke = time - lastInvokeTime;
      const timeWaiting = wait - timeSinceLastCall;
      
      return maxWait === undefined
        ? timeWaiting
        : Math.min(timeWaiting, maxWait - timeSinceLastInvoke);
    };
    
    const shouldInvoke = (time) => {
      const timeSinceLastCall = time - lastCallTime;
      const timeSinceLastInvoke = time - lastInvokeTime;
      
      return (
        lastCallTime === undefined || 
        timeSinceLastCall >= wait ||
        timeSinceLastCall < 0 ||
        (maxWait !== undefined && timeSinceLastInvoke >= maxWait)
      );
    };
    
    const timerExpired = () => {
      const time = Date.now();
      if (shouldInvoke(time)) {
        return trailingEdge(time);
      }
      timeout = setTimeout(timerExpired, remainingWait(time));
    };
    
    const trailingEdge = (time) => {
      timeout = null;
      
      if (trailing && args) {
        return invokeFunc(time);
      }
      
      context = args = null;
      return result;
    };
    
    const debounced = function(...params) {
      const time = Date.now();
      const isInvoking = shouldInvoke(time);
      
      context = this;
      args = params;
      lastCallTime = time;
      
      if (isInvoking) {
        if (!timeout && leading) {
          return leadingEdge(lastCallTime);
        }
        
        if (maxWait !== undefined) {
          timeout = setTimeout(timerExpired, wait);
          return invokeFunc(lastCallTime);
        }
      }
      
      if (!timeout) {
        timeout = setTimeout(timerExpired, wait);
      }
      
      return result;
    };
    
    debounced.cancel = () => {
      if (timeout !== null) {
        clearTimeout(timeout);
      }
      lastInvokeTime = 0;
      lastCallTime = 0;
      timeout = context = args = null;
    };
    
    debounced.flush = () => {
      return timeout ? trailingEdge(Date.now()) : result;
    };
    
    return debounced;
  }

  // 立即执行的防抖(第一次立即执行,然后防抖)
  static immediateDebounce(func, wait) {
    let timeout;
    let immediate = true;
    
    return function(...args) {
      const context = this;
      
      if (immediate) {
        func.apply(context, args);
        immediate = false;
      }
      
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        immediate = true;
      }, wait);
    };
  }

  // 节流+防抖组合
  static throttleDebounce(func, wait, options = {}) {
    const {
      throttleWait = wait,
      debounceWait = wait,
      leading = true,
      trailing = true
    } = options;
    
    const throttled = this.throttle(func, throttleWait, { leading, trailing });
    const debounced = this.debounce(func, debounceWait, { leading, trailing });
    
    let lastCall = 0;
    
    return function(...args) {
      const now = Date.now();
      const timeSinceLastCall = now - lastCall;
      lastCall = now;
      
      // 如果距离上次调用时间很短,使用防抖
      if (timeSinceLastCall < throttleWait) {
        return debounced.apply(this, args);
      }
      
      // 否则使用节流
      return throttled.apply(this, args);
    };
  }
}

// 使用示例
// 节流示例:滚动事件
window.addEventListener('scroll', AdvancedThrottleDebounce.throttle(
  function() {
    console.log('Scroll position:', window.scrollY);
  },
  100,
  { leading: true, trailing: true }
));

// 防抖示例:搜索输入
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', AdvancedThrottleDebounce.debounce(
  function(event) {
    console.log('Searching for:', event.target.value);
    // 实际搜索逻辑
  },
  300,
  { leading: false, trailing: true }
));

// 立即执行的防抖示例:按钮点击
const submitButton = document.getElementById('submit');
submitButton.addEventListener('click', AdvancedThrottleDebounce.immediateDebounce(
  function() {
    console.log('Submit clicked');
    // 提交逻辑
  },
  1000
));

// 组合示例:调整窗口大小
window.addEventListener('resize', AdvancedThrottleDebounce.throttleDebounce(
  function() {
    console.log('Window resized');
    // 调整布局逻辑
  },
  200,
  { throttleWait: 100, debounceWait: 300 }
));
6.2 React Hook版本的节流防抖
import { useRef, useCallback, useEffect } from 'react';

// 使用Hook实现节流
function useThrottle(callback, delay, options = {}) {
  const { leading = true, trailing = true } = options;
  const lastCallTime = useRef(0);
  const timeout = useRef(null);
  const lastArgs = useRef(null);
  const lastThis = useRef(null);
  
  const throttled = useCallback(function(...args) {
    const now = Date.now();
    lastArgs.current = args;
    lastThis.current = this;
    
    if (!leading && !lastCallTime.current) {
      lastCallTime.current = now;
    }
    
    const remaining = delay - (now - lastCallTime.current);
    
    if (remaining <= 0 || remaining > delay) {
      if (timeout.current) {
        clearTimeout(timeout.current);
        timeout.current = null;
      }
      
      lastCallTime.current = now;
      callback.apply(this, args);
      lastArgs.current = lastThis.current = null;
    } else if (!timeout.current && trailing) {
      timeout.current = setTimeout(() => {
        lastCallTime.current = leading ? Date.now() : 0;
        timeout.current = null;
        
        if (trailing && lastArgs.current) {
          callback.apply(lastThis.current, lastArgs.current);
          lastArgs.current = lastThis.current = null;
        }
      }, remaining);
    }
  }, [callback, delay, leading, trailing]);
  
  // 清理函数
  useEffect(() => {
    return () => {
      if (timeout.current) {
        clearTimeout(timeout.current);
      }
    };
  }, []);
  
  return throttled;
}

// 使用Hook实现防抖
function useDebounce(callback, delay, options = {}) {
  const { leading = false, maxWait } = options;
  const lastCallTime = useRef(0);
  const lastInvokeTime = useRef(0);
  const timeout = useRef(null);
  const lastArgs = useRef(null);
  const lastThis = useRef(null);
  
  const debounced = useCallback(function(...args) {
    const now = Date.now();
    
    lastArgs.current = args;
    lastThis.current = this;
    lastCallTime.current = now;
    
    const invokeFunc = () => {
      lastInvokeTime.current = now;
      callback.apply(this, args);
      lastArgs.current = lastThis.current = null;
    };
    
    const shouldInvoke = () => {
      const timeSinceLastCall = now - lastCallTime.current;
      const timeSinceLastInvoke = now - lastInvokeTime.current;
      
      return (
        lastCallTime.current === 0 ||
        timeSinceLastCall >= delay ||
        timeSinceLastCall < 0 ||
        (maxWait !== undefined && timeSinceLastInvoke >= maxWait)
      );
    };
    
    if (shouldInvoke()) {
      if (!timeout.current && leading) {
        invokeFunc();
      }
      
      if (maxWait !== undefined) {
        timeout.current = setTimeout(() => {
          if (timeout.current) {
            clearTimeout(timeout.current);
            timeout.current = null;
          }
          invokeFunc();
        }, delay);
        return;
      }
    }
    
    if (!timeout.current) {
      timeout.current = setTimeout(() => {
        timeout.current = null;
        if (lastArgs.current) {
          invokeFunc();
        }
      }, delay);
    }
  }, [callback, delay, leading, maxWait]);
  
  // 清理函数
  useEffect(() => {
    return () => {
      if (timeout.current) {
        clearTimeout(timeout.current);
      }
    };
  }, []);
  
  return debounced;
}

// React组件使用示例
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  // 防抖搜索函数
  const debouncedSearch = useDebounce(async (searchQuery) => {
    if (!searchQuery.trim()) {
      setResults([]);
      return;
    }
    
    const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
    const data = await response.json();
    setResults(data);
  }, 300);
  
  const handleInputChange = useCallback((event) => {
    const value = event.target.value;
    setQuery(value);
    debouncedSearch(value);
  }, [debouncedSearch]);
  
  return (
    <div>
      <input 
        type="text" 
        value={query}
        onChange={handleInputChange}
        placeholder="Search..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.name}</li>
        ))}
      </ul>
    </div>
  );
}

七、并发控制与请求池(Concurrency Control & Request Pool)

7.1 智能请求池实现
class RequestPool {
  constructor(options = {}) {
    this.options = {
      maxConcurrent: 6, // 最大并发数(浏览器限制通常是6)
      retryCount: 2, // 重试次数
      retryDelay: 1000, // 重试延迟
      timeout: 30000, // 超时时间
      priority: false, // 是否启用优先级
      ...options
    };
    
    this.queue = [];
    this.activeRequests = new Map();
    this.requestCount = 0;
    this.stats = {
      total: 0,
      success: 0,
      failed: 0,
      retried: 0,
      queued: 0,
      active: 0
    };
    
    this.paused = false;
  }

  addRequest(request, priority = 0) {
    const requestId = `req_${Date.now()}_${++this.requestCount}`;
    const requestConfig = {
      id: requestId,
      request,
      priority,
      retries: 0,
      addedAt: Date.now()
    };
    
    if (this.options.priority) {
      // 按优先级插入队列
      let insertIndex = this.queue.length;
      for (let i = 0; i < this.queue.length; i++) {
        if (priority > this.queue[i].priority) {
          insertIndex = i;
          break;
        }
      }
      this.queue.splice(insertIndex, 0, requestConfig);
    } else {
      this.queue.push(requestConfig);
    }
    
    this.stats.queued++;
    this.stats.total++;
    
    this.processQueue();
    return requestId;
  }

  async processQueue() {
    if (this.paused || this.queue.length === 0) return;
    
    // 检查可用并发数
    const availableSlots = this.options.maxConcurrent - this.activeRequests.size;
    if (availableSlots <= 0) return;
    
    // 获取要处理的任务
    const tasksToProcess = this.queue.splice(0, availableSlots);
    
    tasksToProcess.forEach(task => {
      this.executeRequest(task);
    });
  }

  async executeRequest(task) {
    const { id, request, priority, retries } = task;
    
    this.activeRequests.set(id, task);
    this.stats.queued--;
    this.stats.active++;
    
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => {
        controller.abort();
      }, this.options.timeout);
      
      // 执行请求
      const result = await request(controller.signal);
      
      clearTimeout(timeoutId);
      
      // 请求成功
      this.activeRequests.delete(id);
      this.stats.active--;
      this.stats.success++;
      
      // 触发成功事件
      this.emit('success', { id, result, retries });
      
      // 继续处理队列
      this.processQueue();
      
      return result;
    } catch (error) {
      clearTimeout(timeoutId);
      
      // 检查是否需要重试
      const shouldRetry = retries < this.options.retryCount && 
                         !this.isAbortError(error);
      
      if (shouldRetry) {
        this.stats.retried++;
        
        // 延迟后重试
        setTimeout(() => {
          task.retries++;
          this.queue.unshift(task);
          this.stats.queued++;
          this.processQueue();
        }, this.options.retryDelay);
      } else {
        // 最终失败
        this.activeRequests.delete(id);
        this.stats.active--;
        this.stats.failed++;
        
        // 触发失败事件
        this.emit('error', { id, error, retries });
        
        // 继续处理队列
        this.processQueue();
      }
    }
  }

  isAbortError(error) {
    return error.name === 'AbortError' || error.message === 'The user aborted a request.';
  }

  // 事件系统
  eventHandlers = new Map();
  
  on(event, handler) {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, []);
    }
    this.eventHandlers.get(event).push(handler);
  }
  
  off(event, handler) {
    if (this.eventHandlers.has(event)) {
      const handlers = this.eventHandlers.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }
  
  emit(event, data) {
    if (this.eventHandlers.has(event)) {
      this.eventHandlers.get(event).forEach(handler => {
        try {
          handler(data);
        } catch (error) {
          console.error(`Error in event handler for ${event}:`, error);
        }
      });
    }
  }

  // 控制方法
  pause() {
    this.paused = true;
  }

  resume() {
    this.paused = false;
    this.processQueue();
  }

  cancelRequest(requestId) {
    // 从队列中移除
    const queueIndex = this.queue.findIndex(req => req.id === requestId);
    if (queueIndex > -1) {
      this.queue.splice(queueIndex, 1);
      this.stats.queued--;
      return true;
    }
    
    // 从活动请求中移除(无法真正取消fetch,但可以标记为取消)
    if (this.activeRequests.has(requestId)) {
      // 在实际应用中,这里应该取消fetch请求
      // 需要保存AbortController并在取消时调用abort()
      this.activeRequests.delete(requestId);
      this.stats.active--;
      return true;
    }
    
    return false;
  }

  clearQueue() {
    this.queue = [];
    this.stats.queued = 0;
  }

  getStats() {
    return {
      ...this.stats,
      queueLength: this.queue.length,
      activeRequests: this.activeRequests.size
    };
  }

  waitForAll() {
    return new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        if (this.queue.length === 0 && this.activeRequests.size === 0) {
          clearInterval(checkInterval);
          resolve(this.stats);
        }
      }, 100);
    });
  }
}

// 使用示例
const requestPool = new RequestPool({
  maxConcurrent: 4,
  retryCount: 2,
  timeout: 10000,
  priority: true
});

// 添加请求事件监听
requestPool.on('success', ({ id, result }) => {
  console.log(`Request ${id} succeeded`);
});

requestPool.on('error', ({ id, error }) => {
  console.error(`Request ${id} failed:`, error);
});

// 创建请求函数
function createRequest(url, data = null) {
  return async (signal) => {
    const options = {
      method: data ? 'POST' : 'GET',
      headers: {
        'Content-Type': 'application/json'
      },
      signal
    };
    
    if (data) {
      options.body = JSON.stringify(data);
    }
    
    const response = await fetch(url, options);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return response.json();
  };
}

// 添加多个请求
const urls = [
  '/api/users/1',
  '/api/users/2',
  '/api/users/3',
  '/api/products/1',
  '/api/products/2',
  '/api/orders/1'
];

urls.forEach((url, index) => {
  const priority = index < 3 ? 10 : 1; // 前3个请求高优先级
  requestPool.addRequest(createRequest(url), priority);
});

// 等待所有请求完成
requestPool.waitForAll().then(stats => {
  console.log('All requests completed:', stats);
});

// 动态调整并发数
setTimeout(() => {
  requestPool.options.maxConcurrent = 2;
}, 5000);
7.2 带缓存的请求池
class CachedRequestPool extends RequestPool {
  constructor(options = {}) {
    super(options);
    
    this.cache = new Map();
    this.cacheOptions = {
      ttl: options.cacheTtl || 300000, // 5分钟
      maxSize: options.cacheMaxSize || 100,
      ...options.cacheOptions
    };
    
    this.cacheHits = 0;
    this.cacheMisses = 0;
  }

  addRequest(request, priority = 0, cacheKey = null) {
    // 生成缓存键
    const actualCacheKey = cacheKey || this.generateCacheKey(request);
    
    // 检查缓存
    if (this.cache.has(actualCacheKey)) {
      const cached = this.cache.get(actualCacheKey);
      
      if (Date.now() - cached.timestamp < this.cacheOptions.ttl) {
        this.cacheHits++;
        
        // 立即返回缓存结果
        return Promise.resolve(cached.data);
      } else {
        // 缓存过期
        this.cache.delete(actualCacheKey);
      }
    }
    
    this.cacheMisses++;
    
    // 创建包装的请求函数
    const wrappedRequest = async (signal) => {
      try {
        const result = await request(signal);
        
        // 缓存结果
        this.cache.set(actualCacheKey, {
          data: result,
          timestamp: Date.now()
        });
        
        // 清理过期缓存
        this.cleanupCache();
        
        return result;
      } catch (error) {
        throw error;
      }
    };
    
    // 添加到父类队列
    return new Promise((resolve, reject) => {
      const requestId = super.addRequest(wrappedRequest, priority);
      
      // 监听完成事件
      const successHandler = ({ id, result }) => {
        if (id === requestId) {
          resolve(result);
          this.off('success', successHandler);
          this.off('error', errorHandler);
        }
      };
      
      const errorHandler = ({ id, error }) => {
        if (id === requestId) {
          reject(error);
          this.off('success', successHandler);
          this.off('error', errorHandler);
        }
      };
      
      this.on('success', successHandler);
      this.on('error', errorHandler);
    });
  }

  generateCacheKey(request) {
    // 根据请求函数生成缓存键
    // 这是一个简单实现,实际应用中可能需要更复杂的逻辑
    return `cache_${Date.now()}_${Math.random().toString(36).substr(2)}`;
  }

  cleanupCache() {
    const now = Date.now();
    const maxAge = this.cacheOptions.ttl;
    
    // 清理过期缓存
    for (const [key, value] of this.cache.entries()) {
      if (now - value.timestamp > maxAge) {
        this.cache.delete(key);
      }
    }
    
    // 清理超出大小的缓存
    if (this.cache.size > this.cacheOptions.maxSize) {
      const entries = Array.from(this.cache.entries());
      entries.sort(([, a], [, b]) => a.timestamp - b.timestamp);
      
      const toRemove = entries.slice(0, this.cache.size - this.cacheOptions.maxSize);
      toRemove.forEach(([key]) => this.cache.delete(key));
    }
  }

  clearCache() {
    this.cache.clear();
    this.cacheHits = 0;
    this.cacheMisses = 0;
  }

  getCacheStats() {
    return {
      hits: this.cacheHits,
      misses: this.cacheMisses,
      hitRate: this.cacheHits + this.cacheMisses > 0 
        ? this.cacheHits / (this.cacheHits + this.cacheMisses)
        : 0,
      size: this.cache.size
    };
  }
}

八、数据分批处理(Batch Processing)

8.1 智能数据批处理器
class BatchProcessor {
  constructor(options = {}) {
    this.options = {
      batchSize: 100,
      delay: 100, // 延迟时间(毫秒)
      maxBatches: 10, // 最大批次数
      autoProcess: true, // 是否自动处理
      ...options
    };
    
    this.batch = [];
    this.batchQueue = [];
    this.isProcessing = false;
    this.timer = null;
    this.stats = {
      totalItems: 0,
      processedItems: 0,
      batchesProcessed: 0,
      processingTime: 0
    };
    
    if (this.options.autoProcess) {
      this.startAutoProcess();
    }
  }

  add(item) {
    this.batch.push(item);
    this.stats.totalItems++;
    
    if (this.batch.length >= this.options.batchSize) {
      this.enqueueBatch();
    } else if (this.options.autoProcess && !this.timer) {
      this.startTimer();
    }
    
    return this;
  }

  addMany(items) {
    items.forEach(item => this.add(item));
    return this;
  }

  enqueueBatch() {
    if (this.batch.length === 0) return;
    
    const batchToEnqueue = [...this.batch];
    this.batch = [];
    
    this.batchQueue.push(batchToEnqueue);
    
    // 检查队列长度
    if (this.batchQueue.length > this.options.maxBatches) {
      console.warn('Batch queue is full, consider increasing maxBatches or batchSize');
    }
    
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    
    if (this.options.autoProcess && !this.isProcessing) {
      this.processQueue();
    }
  }

  startTimer() {
    if (this.timer) {
      clearTimeout(this.timer);
    }
    
    this.timer = setTimeout(() => {
      this.enqueueBatch();
      this.timer = null;
    }, this.options.delay);
  }

  async processQueue() {
    if (this.isProcessing || this.batchQueue.length === 0) return;
    
    this.isProcessing = true;
    
    while (this.batchQueue.length > 0) {
      const batch = this.batchQueue.shift();
      
      try {
        const startTime = Date.now();
        await this.processBatch(batch);
        const endTime = Date.now();
        
        this.stats.processedItems += batch.length;
        this.stats.batchesProcessed++;
        this.stats.processingTime += endTime - startTime;
        
        // 触发批次完成事件
        this.emit('batchComplete', {
          batch,
          size: batch.length,
          processingTime: endTime - startTime
        });
        
      } catch (error) {
        console.error('Batch processing error:', error);
        
        // 触发错误事件
        this.emit('error', {
          error,
          batch,
          size: batch.length
        });
      }
    }
    
    this.isProcessing = false;
    
    // 触发队列完成事件
    this.emit('queueEmpty', this.stats);
  }

  async processBatch(batch) {
    // 这是一个抽象方法,需要在子类中实现
    throw new Error('processBatch method must be implemented');
  }

  flush() {
    // 强制处理当前批次
    if (this.batch.length > 0) {
      this.enqueueBatch();
    }
    
    return this.processQueue();
  }

  waitForCompletion() {
    return new Promise((resolve) => {
      const checkComplete = () => {
        if (this.batch.length === 0 && 
            this.batchQueue.length === 0 && 
            !this.isProcessing) {
          resolve(this.stats);
        } else {
          setTimeout(checkComplete, 50);
        }
      };
      
      checkComplete();
    });
  }

  // 事件系统
  eventHandlers = new Map();
  
  on(event, handler) {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, []);
    }
    this.eventHandlers.get(event).push(handler);
  }
  
  off(event, handler) {
    if (this.eventHandlers.has(event)) {
      const handlers = this.eventHandlers.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }
  
  emit(event, data) {
    if (this.eventHandlers.has(event)) {
      this.eventHandlers.get(event).forEach(handler => {
        try {
          handler(data);
        } catch (error) {
          console.error(`Error in event handler for ${event}:`, error);
        }
      });
    }
  }

  getStats() {
    return {
      ...this.stats,
      itemsInBatch: this.batch.length,
      batchesInQueue: this.batchQueue.length,
      isProcessing: this.isProcessing
    };
  }

  reset() {
    this.batch = [];
    this.batchQueue = [];
    this.isProcessing = false;
    
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    
    this.stats = {
      totalItems: 0,
      processedItems: 0,
      batchesProcessed: 0,
      processingTime: 0
    };
  }
}

// 使用示例:API批量请求处理器
class ApiBatchProcessor extends BatchProcessor {
  constructor(apiEndpoint, options = {}) {
    super({
      batchSize: 50,
      delay: 500,
      ...options
    });
    
    this.apiEndpoint = apiEndpoint;
  }

  async processBatch(batch) {
    // 批量发送请求
    const response = await fetch(this.apiEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ items: batch })
    });
    
    if (!response.ok) {
      throw new Error(`API request failed: ${response.status}`);
    }
    
    return response.json();
  }
}

// 使用示例
const processor = new ApiBatchProcessor('/api/batch-save', {
  batchSize: 100,
  delay: 1000
});

// 监听事件
processor.on('batchComplete', ({ batch, size, processingTime }) => {
  console.log(`Processed batch of ${size} items in ${processingTime}ms`);
});

processor.on('queueEmpty', (stats) => {
  console.log('All batches processed:', stats);
});

// 添加大量数据
for (let i = 0; i < 1000; i++) {
  processor.add({ id: i, data: `Item ${i}` });
}

// 等待处理完成
processor.waitForCompletion().then(stats => {
  console.log('Processing completed:', stats);
});
8.2 数据库批量操作
class DatabaseBatchProcessor extends BatchProcessor {
  constructor(dbName, storeName, options = {}) {
    super({
      batchSize: 100,
      delay: 100,
      ...options
    });
    
    this.dbName = dbName;
    this.storeName = storeName;
    this.db = null;
    
    this.initDatabase();
  }

  async initDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'id' });
        }
      };
      
      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve();
      };
      
      request.onerror = (event) => {
        reject(event.target.error);
      };
    });
  }

  async processBatch(batch) {
    if (!this.db) await this.initDatabase();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      
      let completed = 0;
      let errors = [];
      
      batch.forEach(item => {
        const request = store.put(item);
        
        request.onsuccess = () => {
          completed++;
          if (completed === batch.length) {
            if (errors.length > 0) {
              reject(new Error(`Failed to save ${errors.length} items`));
            } else {
              resolve({ saved: batch.length });
            }
          }
        };
        
        request.onerror = (event) => {
          errors.push({ item, error: event.target.error });
          completed++;
          
          if (completed === batch.length) {
            if (errors.length > 0) {
              reject(new Error(`Failed to save ${errors.length} items`));
            } else {
              resolve({ saved: batch.length - errors.length });
            }
          }
        };
      });
    });
  }

  async queryBatch(queryFn, batchSize = 100) {
    if (!this.db) await this.initDatabase();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.openCursor();
      
      const results = [];
      let processed = 0;
      
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        
        if (cursor) {
          // 应用查询函数
          if (queryFn(cursor.value)) {
            results.push(cursor.value);
          }
          
          processed++;
          cursor.continue();
          
          // 批量处理
          if (processed % batchSize === 0) {
            // 短暂暂停以避免阻塞主线程
            setTimeout(() => {}, 0);
          }
        } else {
          resolve(results);
        }
      };
      
      request.onerror = (event) => {
        reject(event.target.error);
      };
    });
  }
}

// 使用示例
const dbProcessor = new DatabaseBatchProcessor('myDatabase', 'items');

// 批量添加数据
for (let i = 0; i < 10000; i++) {
  dbProcessor.add({
    id: i,
    name: `Item ${i}`,
    value: Math.random(),
    timestamp: Date.now()
  });
}

// 批量查询
dbProcessor.queryBatch(
  item => item.value > 0.5,
  500
).then(results => {
  console.log(`Found ${results.length} items with value > 0.5`);
});

九、对象池模式(Object Pool Pattern)

9.1 通用对象池实现
class ObjectPool {
  constructor(createFn, options = {}) {
    if (typeof createFn !== 'function') {
      throw new Error('createFn must be a function');
    }
    
    this.createFn = createFn;
    this.options = {
      maxSize: 100,
      minSize: 0,
      validate: null, // 验证函数
      reset: null, // 重置函数
      ...options
    };
    
    this.pool = [];
    this.active = new Set();
    this.stats = {
      created: 0,
      reused: 0,
      destroyed: 0,
      peakSize: 0
    };
    
    // 预创建对象
    this.prepopulate();
  }

  prepopulate() {
    const count = Math.min(this.options.minSize, this.options.maxSize);
    for (let i = 0; i < count; i++) {
      const obj = this.createNew();
      this.pool.push(obj);
    }
  }

  createNew() {
    const obj = this.createFn();
    this.stats.created++;
    return obj;
  }

  acquire() {
    let obj;
    
    if (this.pool.length > 0) {
      // 从池中获取
      obj = this.pool.pop();
      
      // 验证对象
      if (this.options.validate && !this.options.validate(obj)) {
        // 对象无效,销毁并创建新的
        this.destroyObject(obj);
        obj = this.createNew();
      } else {
        this.stats.reused++;
      }
    } else {
      // 池为空,创建新对象
      obj = this.createNew();
    }
    
    // 重置对象状态
    if (this.options.reset) {
      this.options.reset(obj);
    }
    
    this.active.add(obj);
    this.updatePeakSize();
    
    return obj;
  }

  release(obj) {
    if (!this.active.has(obj)) {
      console.warn('Object not active in pool');
      return;
    }
    
    this.active.delete(obj);
    
    // 检查池是否已满
    if (this.pool.length < this.options.maxSize) {
      // 重置对象
      if (this.options.reset) {
        this.options.reset(obj);
      }
      
      this.pool.push(obj);
    } else {
      // 池已满,销毁对象
      this.destroyObject(obj);
    }
  }

  destroyObject(obj) {
    // 调用清理函数(如果存在)
    if (obj.destroy && typeof obj.destroy === 'function') {
      obj.destroy();
    }
    
    this.stats.destroyed++;
  }

  updatePeakSize() {
    const totalSize = this.pool.length + this.active.size;
    if (totalSize > this.stats.peakSize) {
      this.stats.peakSize = totalSize;
    }
  }

  clear() {
    // 销毁所有对象
    [...this.pool, ...this.active].forEach(obj => {
      this.destroyObject(obj);
    });
    
    this.pool = [];
    this.active.clear();
  }

  getStats() {
    return {
      ...this.stats,
      poolSize: this.pool.length,
      activeSize: this.active.size,
      totalSize: this.pool.length + this.active.size
    };
  }

  // 执行函数并自动管理对象
  async execute(callback) {
    const obj = this.acquire();
    
    try {
      const result = await callback(obj);
      return result;
    } finally {
      this.release(obj);
    }
  }
}

// 使用示例:DOM元素池
class DOMElementPool extends ObjectPool {
  constructor(elementType, options = {}) {
    super(() => {
      const element = document.createElement(elementType);
      element.style.display = 'none'; // 初始隐藏
      document.body.appendChild(element);
      return element;
    }, options);
    
    this.elementType = elementType;
  }

  acquire(styles = {}) {
    const element = super.acquire();
    
    // 应用样式
    Object.assign(element.style, {
      display: '',
      ...styles
    });
    
    return element;
  }

  release(element) {
    // 隐藏元素
    element.style.display = 'none';
    
    // 清除内容
    element.innerHTML = '';
    
    super.release(element);
  }
}

// 使用示例
const divPool = new DOMElementPool('div', {
  maxSize: 50,
  minSize: 10,
  reset: (div) => {
    div.className = '';
    div.style.cssText = '';
    div.textContent = '';
  }
});

// 使用对象池创建临时元素
for (let i = 0; i < 1000; i++) {
  const div = divPool.acquire({
    position: 'absolute',
    left: `${Math.random() * 100}%`,
    top: `${Math.random() * 100}%`,
    width: '50px',
    height: '50px',
    backgroundColor: `hsl(${Math.random() * 360}, 100%, 50%)`
  });
  
  div.textContent = i;
  
  // 模拟使用
  setTimeout(() => {
    divPool.release(div);
  }, Math.random() * 3000);
}

// 查看统计
setTimeout(() => {
  console.log('Pool stats:', divPool.getStats());
}, 5000);
9.2 连接池实现
class ConnectionPool {
  constructor(createConnection, options = {}) {
    this.createConnection = createConnection;
    this.options = {
      maxConnections: 10,
      minConnections: 2,
      idleTimeout: 30000, // 空闲超时时间
      acquireTimeout: 5000, // 获取连接超时时间
      testOnBorrow: true, // 获取时测试连接
      ...options
    };
    
    this.pool = [];
    this.active = new Set();
    this.waiting = [];
    this.timers = new Map();
    
    this.stats = {
      created: 0,
      destroyed: 0,
      acquired: 0,
      released: 0,
      timeoutErrors: 0,
      connectionErrors: 0
    };
    
    // 初始化连接池
    this.init();
  }

  async init() {
    for (let i = 0; i < this.options.minConnections; i++) {
      await this.createAndAddConnection();
    }
  }

  async createAndAddConnection() {
    try {
      const connection = await this.createConnection();
      this.pool.push({
        connection,
        lastUsed: Date.now(),
        valid: true
      });
      
      this.stats.created++;
      return connection;
    } catch (error) {
      this.stats.connectionErrors++;
      throw error;
    }
  }

  async acquire() {
    this.stats.acquired++;
    
    // 1. 检查空闲连接
    for (let i = 0; i < this.pool.length; i++) {
      const item = this.pool[i];
      
      if (item.valid) {
        // 检查连接是否有效
        if (this.options.testOnBorrow) {
          try {
            await this.testConnection(item.connection);
          } catch (error) {
            item.valid = false;
            continue;
          }
        }
        
        // 从池中移除
        const [acquiredItem] = this.pool.splice(i, 1);
        this.active.add(acquiredItem.connection);
        
        // 设置最后使用时间
        acquiredItem.lastUsed = Date.now();
        
        return acquiredItem.connection;
      }
    }
    
    // 2. 检查是否可以创建新连接
    const totalConnections = this.pool.length + this.active.size;
    if (totalConnections < this.options.maxConnections) {
      const connection = await this.createAndAddConnection();
      this.active.add(connection);
      return connection;
    }
    
    // 3. 等待可用连接
    return new Promise((resolve, reject) => {
      const waitStart = Date.now();
      
      const waitingRequest = {
        resolve,
        reject,
        timer: setTimeout(() => {
          // 超时处理
          const index = this.waiting.indexOf(waitingRequest);
          if (index > -1) {
            this.waiting.splice(index, 1);
          }
          
          this.stats.timeoutErrors++;
          reject(new Error('Connection acquisition timeout'));
        }, this.options.acquireTimeout)
      };
      
      this.waiting.push(waitingRequest);
      
      // 立即尝试处理等待队列
      this.processWaitingQueue();
    });
  }

  release(connection) {
    if (!this.active.has(connection)) {
      console.warn('Connection not active in pool');
      return;
    }
    
    this.active.delete(connection);
    this.stats.released++;
    
    // 检查连接是否仍然有效
    if (this.isConnectionValid(connection)) {
      this.pool.push({
        connection,
        lastUsed: Date.now(),
        valid: true
      });
      
      // 清理空闲超时的连接
      this.cleanupIdleConnections();
      
      // 处理等待队列
      this.processWaitingQueue();
    } else {
      // 连接无效,销毁
      this.destroyConnection(connection);
    }
  }

  async testConnection(connection) {
    // 默认实现,子类应该覆盖这个方法
    return Promise.resolve();
  }

  isConnectionValid(connection) {
    // 默认实现,子类应该覆盖这个方法
    return true;
  }

  destroyConnection(connection) {
    // 清理连接资源
    if (connection.destroy && typeof connection.destroy === 'function') {
      connection.destroy();
    } else if (connection.close && typeof connection.close === 'function') {
      connection.close();
    }
    
    this.stats.destroyed++;
  }

  cleanupIdleConnections() {
    const now = Date.now();
    const idleTimeout = this.options.idleTimeout;
    
    for (let i = this.pool.length - 1; i >= 0; i--) {
      const item = this.pool[i];
      
      // 检查空闲时间
      if (now - item.lastUsed > idleTimeout) {
        // 保留最小连接数
        if (this.pool.length > this.options.minConnections) {
          const [idleItem] = this.pool.splice(i, 1);
          this.destroyConnection(idleItem.connection);
        }
      }
    }
  }

  processWaitingQueue() {
    while (this.waiting.length > 0 && this.pool.length > 0) {
      const waitingRequest = this.waiting.shift();
      clearTimeout(waitingRequest.timer);
      
      // 获取连接
      const item = this.pool.pop();
      this.active.add(item.connection);
      item.lastUsed = Date.now();
      
      waitingRequest.resolve(item.connection);
    }
  }

  async execute(callback) {
    const connection = await this.acquire();
    
    try {
      const result = await callback(connection);
      return result;
    } finally {
      this.release(connection);
    }
  }

  getStats() {
    return {
      ...this.stats,
      poolSize: this.pool.length,
      activeSize: this.active.size,
      waitingSize: this.waiting.length,
      totalSize: this.pool.length + this.active.size
    };
  }

  clear() {
    // 清理所有连接
    [...this.pool, ...this.active].forEach(item => {
      this.destroyConnection(item.connection || item);
    });
    
    // 清理等待队列
    this.waiting.forEach(request => {
      clearTimeout(request.timer);
      request.reject(new Error('Pool cleared'));
    });
    
    this.pool = [];
    this.active.clear();
    this.waiting = [];
  }
}

// 使用示例:WebSocket连接池
class WebSocketPool extends ConnectionPool {
  constructor(url, options = {}) {
    super(async () => {
      return new Promise((resolve, reject) => {
        const ws = new WebSocket(url);
        
        ws.onopen = () => resolve(ws);
        ws.onerror = (error) => reject(error);
      });
    }, options);
    
    this.url = url;
  }

  async testConnection(ws) {
    return new Promise((resolve, reject) => {
      if (ws.readyState === WebSocket.OPEN) {
        resolve();
      } else if (ws.readyState === WebSocket.CONNECTING) {
        // 等待连接
        const onOpen = () => {
          ws.removeEventListener('open', onOpen);
          resolve();
        };
        
        const onError = () => {
          ws.removeEventListener('error', onError);
          reject(new Error('WebSocket connection failed'));
        };
        
        ws.addEventListener('open', onOpen);
        ws.addEventListener('error', onError);
        
        // 超时
        setTimeout(() => {
          ws.removeEventListener('open', onOpen);
          ws.removeEventListener('error', onError);
          reject(new Error('WebSocket connection timeout'));
        }, 5000);
      } else {
        reject(new Error('WebSocket is not open'));
      }
    });
  }

  isConnectionValid(ws) {
    return ws.readyState === WebSocket.OPEN;
  }

  destroyConnection(ws) {
    if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
      ws.close();
    }
    super.destroyConnection(ws);
  }
}

// 使用示例
const wsPool = new WebSocketPool('wss://echo.websocket.org', {
  maxConnections: 5,
  minConnections: 1,
  idleTimeout: 60000
});

// 使用连接池发送消息
async function sendMessage(message) {
  return wsPool.execute(async (ws) => {
    return new Promise((resolve, reject) => {
      const messageId = Date.now();
      
      const handler = (event) => {
        try {
          const data = JSON.parse(event.data);
          if (data.id === messageId) {
            ws.removeEventListener('message', handler);
            resolve(data);
          }
        } catch (error) {
          // 忽略解析错误
        }
      };
      
      ws.addEventListener('message', handler);
      
      // 设置超时
      setTimeout(() => {
        ws.removeEventListener('message', handler);
        reject(new Error('Message timeout'));
      }, 5000);
      
      // 发送消息
      ws.send(JSON.stringify({
        id: messageId,
        message
      }));
    });
  });
}

// 并发发送消息
Promise.all([
  sendMessage('Hello 1'),
  sendMessage('Hello 2'),
  sendMessage('Hello 3'),
  sendMessage('Hello 4'),
  sendMessage('Hello 5')
]).then(responses => {
  console.log('All messages sent:', responses);
});

十、Web Worker优化计算密集型任务

10.1 智能Worker池
class WorkerPool {
  constructor(workerScript, options = {}) {
    this.workerScript = workerScript;
    this.options = {
      maxWorkers: navigator.hardwareConcurrency || 4,
      idleTimeout: 30000, // 空闲超时时间
      ...options
    };
    
    this.workers = [];
    this.idleWorkers = [];
    this.taskQueue = [];
    this.taskCallbacks = new Map();
    this.workerStates = new Map();
    
    this.stats = {
      tasksCompleted: 0,
      tasksFailed: 0,
      workersCreated: 0,
      workersDestroyed: 0
    };
    
    // 初始化Worker
    this.initWorkers();
  }

  initWorkers() {
    const initialCount = Math.min(2, this.options.maxWorkers);
    
    for (let i = 0; i < initialCount; i++) {
      this.createWorker();
    }
  }

  createWorker() {
    if (this.workers.length >= this.options.maxWorkers) {
      return null;
    }
    
    let worker;
    
    if (typeof this.workerScript === 'string') {
      worker = new Worker(this.workerScript);
    } else if (typeof this.workerScript === 'function') {
      // 从函数创建Worker
      const workerBlob = new Blob([
        `(${this.workerScript.toString()})()`
      ], { type: 'application/javascript' });
      
      worker = new Worker(URL.createObjectURL(workerBlob));
    } else {
      throw new Error('workerScript must be a URL string or a function');
    }
    
    const workerId = this.workers.length;
    worker.id = workerId;
    
    // 设置消息处理
    worker.onmessage = (event) => {
      this.handleWorkerMessage(workerId, event);
    };
    
    worker.onerror = (error) => {
      this.handleWorkerError(workerId, error);
    };
    
    worker.onmessageerror = (error) => {
      this.handleWorkerError(workerId, error);
    };
    
    this.workers.push(worker);
    this.idleWorkers.push(workerId);
    this.workerStates.set(workerId, {
      idle: true,
      currentTask: null,
      lastUsed: Date.now()
    });
    
    this.stats.workersCreated++;
    
    return workerId;
  }

  handleWorkerMessage(workerId, event) {
    const state = this.workerStates.get(workerId);
    if (!state || !state.currentTask) return;
    
    const taskId = state.currentTask;
    const callback = this.taskCallbacks.get(taskId);
    
    if (callback) {
      if (event.data.error) {
        callback.reject(new Error(event.data.error));
        this.stats.tasksFailed++;
      } else {
        callback.resolve(event.data.result);
        this.stats.tasksCompleted++;
      }
      
      this.taskCallbacks.delete(taskId);
    }
    
    // 标记Worker为空闲
    state.idle = true;
    state.currentTask = null;
    state.lastUsed = Date.now();
    this.idleWorkers.push(workerId);
    
    // 处理下一个任务
    this.processQueue();
    
    // 清理空闲超时的Worker
    this.cleanupIdleWorkers();
  }

  handleWorkerError(workerId, error) {
    console.error(`Worker ${workerId} error:`, error);
    
    const state = this.workerStates.get(workerId);
    if (state && state.currentTask) {
      const taskId = state.currentTask;
      const callback = this.taskCallbacks.get(taskId);
      
      if (callback) {
        callback.reject(error);
        this.stats.tasksFailed++;
        this.taskCallbacks.delete(taskId);
      }
    }
    
    // 销毁Worker
    this.destroyWorker(workerId);
    
    // 创建新的Worker替换
    this.createWorker();
    
    // 处理队列中的任务
    this.processQueue();
  }

  destroyWorker(workerId) {
    const workerIndex = this.workers.findIndex(w => w.id === workerId);
    if (workerIndex === -1) return;
    
    const worker = this.workers[workerIndex];
    
    // 终止Worker
    worker.terminate();
    
    // 从数组中移除
    this.workers.splice(workerIndex, 1);
    
    // 更新其他Worker的ID
    this.workers.forEach((w, index) => {
      w.id = index;
    });
    
    // 清理状态
    this.workerStates.delete(workerId);
    
    // 从空闲列表中移除
    const idleIndex = this.idleWorkers.indexOf(workerId);
    if (idleIndex > -1) {
      this.idleWorkers.splice(idleIndex, 1);
    }
    
    this.stats.workersDestroyed++;
  }

  cleanupIdleWorkers() {
    const now = Date.now();
    const idleTimeout = this.options.idleTimeout;
    
    // 保留至少一个Worker
    while (this.idleWorkers.length > 1) {
      const workerId = this.idleWorkers[0];
      const state = this.workerStates.get(workerId);
      
      if (state && now - state.lastUsed > idleTimeout) {
        this.destroyWorker(workerId);
      } else {
        break;
      }
    }
  }

  processQueue() {
    while (this.taskQueue.length > 0 && this.idleWorkers.length > 0) {
      const task = this.taskQueue.shift();
      const workerId = this.idleWorkers.shift();
      
      this.executeTask(workerId, task);
    }
    
    // 如果没有空闲Worker但有任务,考虑创建新Worker
    if (this.taskQueue.length > 0 && this.workers.length < this.options.maxWorkers) {
      this.createWorker();
      this.processQueue();
    }
  }

  executeTask(workerId, task) {
    const worker = this.workers.find(w => w.id === workerId);
    if (!worker) return;
    
    const state = this.workerStates.get(workerId);
    if (!state) return;
    
    state.idle = false;
    state.currentTask = task.id;
    state.lastUsed = Date.now();
    
    worker.postMessage({
      taskId: task.id,
      data: task.data,
      type: task.type
    });
  }

  runTask(data, type = 'default') {
    const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    
    return new Promise((resolve, reject) => {
      const task = {
        id: taskId,
        data,
        type
      };
      
      this.taskCallbacks.set(taskId, { resolve, reject });
      this.taskQueue.push(task);
      
      this.processQueue();
    });
  }

  // 批量执行任务
  runTasks(tasks, type = 'default') {
    return Promise.all(
      tasks.map(taskData => this.runTask(taskData, type))
    );
  }

  // 执行函数并自动清理
  async execute(data, processor) {
    if (typeof processor !== 'function') {
      throw new Error('Processor must be a function');
    }
    
    // 将处理器函数发送到Worker
    const taskId = await this.runTask({
      data,
      processor: processor.toString()
    }, 'function');
    
    return taskId;
  }

  getStats() {
    return {
      ...this.stats,
      workers: this.workers.length,
      idleWorkers: this.idleWorkers.length,
      activeWorkers: this.workers.length - this.idleWorkers.length,
      queuedTasks: this.taskQueue.length,
      activeTasks: this.taskCallbacks.size
    };
  }

  terminate() {
    // 终止所有Worker
    this.workers.forEach(worker => {
      worker.terminate();
    });
    
    // 清理所有任务
    this.taskCallbacks.forEach(({ reject }) => {
      reject(new Error('Worker pool terminated'));
    });
    
    this.workers = [];
    this.idleWorkers = [];
    this.taskQueue = [];
    this.taskCallbacks.clear();
    this.workerStates.clear();
  }
}

// Worker脚本示例
const workerScript = function() {
  // Worker内部代码
  self.onmessage = function(event) {
    const { taskId, data, type } = event.data;
    
    try {
      let result;
      
      switch (type) {
        case 'function':
          // 执行传入的函数
          const { data: taskData, processor } = data;
          const func = eval(`(${processor})`);
          result = func(taskData);
          break;
          
        case 'calculate':
          // 计算密集型任务
          result = expensiveCalculation(data);
          break;
          
        case 'process':
          // 数据处理任务
          result = processData(data);
          break;
          
        default:
          result = data;
      }
      
      self.postMessage({ taskId, result });
    } catch (error) {
      self.postMessage({ 
        taskId, 
        error: error.message || 'Unknown error' 
      });
    }
  };
  
  function expensiveCalculation(data) {
    let result = 0;
    for (let i = 0; i < data.iterations || 1000000; i++) {
      result += Math.sqrt(i) * Math.sin(i);
    }
    return result;
  }
  
  function processData(data) {
    // 数据处理逻辑
    return data.map(item => ({
      ...item,
      processed: true,
      timestamp: Date.now()
    }));
  }
};

// 使用示例
const workerPool = new WorkerPool(workerScript, {
  maxWorkers: 4,
  idleTimeout: 60000
});

// 执行计算密集型任务
async function runCalculations() {
  const tasks = Array.from({ length: 10 }, (_, i) => ({
    iterations: 1000000 * (i + 1)
  }));
  
  const startTime = Date.now();
  
  const results = await workerPool.runTasks(tasks, 'calculate');
  
  const endTime = Date.now();
  console.log(`Calculations completed in ${endTime - startTime}ms`);
  console.log('Results:', results);
  
  return results;
}

// 执行函数
async function runCustomFunction() {
  const processor = (data) => {
    // 这是在Worker中执行的函数
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      sum += data[i] * Math.sqrt(data[i]);
    }
    return sum;
  };
  
  const data = Array.from({ length: 1000000 }, () => Math.random());
  
  const result = await workerPool.execute(data, processor);
  console.log('Custom function result:', result);
}

// 监控统计
setInterval(() => {
  console.log('Worker pool stats:', workerPool.getStats());
}, 5000);
10.2 专用Worker优化
class ImageProcessingWorker {
  constructor() {
    this.worker = this.createWorker();
    this.taskQueue = new Map();
    this.nextTaskId = 1;
  }

  createWorker() {
    const workerCode = `
      self.onmessage = function(event) {
        const { taskId, operation, imageData, params } = event.data;
        
        try {
          let result;
          
          switch (operation) {
            case 'resize':
              result = resizeImage(imageData, params);
              break;
              
            case 'filter':
              result = applyFilter(imageData, params);
              break;
              
            case 'compress':
              result = compressImage(imageData, params);
              break;
              
            default:
              throw new Error('Unknown operation: ' + operation);
          }
          
          self.postMessage({ taskId, result }, [result]);
        } catch (error) {
          self.postMessage({ taskId, error: error.message });
        }
      };
      
      function resizeImage(imageData, { width, height, quality = 0.9 }) {
        // 创建离屏Canvas
        const canvas = new OffscreenCanvas(width, height);
        const ctx = canvas.getContext('2d');
        
        // 绘制并缩放图像
        ctx.drawImage(imageData, 0, 0, width, height);
        
        // 转换为Blob
        return canvas.convertToBlob({ quality });
      }
      
      function applyFilter(imageData, { filter, intensity = 1 }) {
        const canvas = new OffscreenCanvas(
          imageData.width, 
          imageData.height
        );
        const ctx = canvas.getContext('2d');
        
        ctx.drawImage(imageData, 0, 0);
        
        const imageDataObj = ctx.getImageData(
          0, 0, 
          canvas.width, 
          canvas.height
        );
        
        // 应用滤镜
        const data = imageDataObj.data;
        for (let i = 0; i < data.length; i += 4) {
          // 简单灰度滤镜示例
          if (filter === 'grayscale') {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
            data[i] = data[i + 1] = data[i + 2] = avg;
          }
          // 更多滤镜...
        }
        
        ctx.putImageData(imageDataObj, 0, 0);
        
        return canvas.convertToBlob();
      }
      
      function compressImage(imageData, { quality = 0.7 }) {
        const canvas = new OffscreenCanvas(
          imageData.width, 
          imageData.height
        );
        const ctx = canvas.getContext('2d');
        
        ctx.drawImage(imageData, 0, 0);
        
        return canvas.convertToBlob({ quality });
      }
    `;
    
    const blob = new Blob([workerCode], { type: 'application/javascript' });
    return new Worker(URL.createObjectURL(blob));
  }

  processImage(imageElement, operation, params = {}) {
    return new Promise((resolve, reject) => {
      const taskId = this.nextTaskId++;
      
      // 创建Canvas来获取ImageData
      const canvas = document.createElement('canvas');
      canvas.width = imageElement.width;
      canvas.height = imageElement.height;
      
      const ctx = canvas.getContext('2d');
      ctx.drawImage(imageElement, 0, 0);
      
      // 获取ImageData
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      
      // 创建ImageBitmap(更高效)
      createImageBitmap(imageElement).then(imageBitmap => {
        // 存储回调
        this.taskQueue.set(taskId, { resolve, reject });
        
        // 发送任务到Worker
        this.worker.postMessage({
          taskId,
          operation,
          imageData: imageBitmap,
          params
        }, [imageBitmap]);
      });
    });
  }

  destroy() {
    this.worker.terminate();
    this.taskQueue.clear();
  }
}

// 使用示例
const imageProcessor = new ImageProcessingWorker();

async function processUserImage(imageFile) {
  const img = new Image();
  img.src = URL.createObjectURL(imageFile);
  
  await new Promise(resolve => {
    img.onload = resolve;
  });
  
  // 调整大小
  const resized = await imageProcessor.processImage(img, 'resize', {
    width: 800,
    height: 600,
    quality: 0.8
  });
  
  // 应用滤镜
  const filtered = await imageProcessor.processImage(img, 'filter', {
    filter: 'grayscale',
    intensity: 1
  });
  
  // 压缩
  const compressed = await imageProcessor.processImage(img, 'compress', {
    quality: 0.6
  });
  
  return {
    resized,
    filtered,
    compressed
  };
}

十一、最佳实践与性能原则

11.1 性能优化黄金法则
  1. 测量第一,优化第二
  • 使用Performance API测量关键指标
  • 优先优化瓶颈,而非微观优化
  • 建立性能基准线
  1. 延迟加载一切可能的内容
  • 图片、视频、第三方脚本
  • 非关键CSS和JavaScript
  • 路由级代码分割
  1. 缓存一切可能的内容
  • HTTP缓存策略
  • 内存缓存频繁使用的数据
  • 持久化缓存重要数据
  1. 批量处理操作
  • DOM操作批量更新
  • 网络请求合并
  • 状态更新合并
  1. 避免阻塞主线程
  • 长时间任务使用Web Worker
  • 复杂计算使用时间切片
  • 避免同步的阻塞操作
11.2 手写实现的优势
  1. 精细控制
  • 可以根据具体需求定制优化策略
  • 避免通用库的冗余代码
  1. 更好的理解
  • 深入理解性能问题的本质
  • 掌握底层优化原理
  1. 更小的包体积
  • 只包含需要的功能
  • 避免依赖大型库
  1. 更好的可调试性
  • 完全控制代码流程
  • 更容易添加日志和监控
11.3 持续优化流程
  1. 建立性能文化
  • 性能作为核心需求
  • 定期性能评审
  • 性能回归测试
  1. 自动化性能测试
  • 集成到CI/CD流程
  • 自动生成性能报告
  • 设置性能预算
  1. 渐进式优化
  • 从最关键的问题开始
  • 小步快跑,持续改进
  • 监控优化效果
  1. 知识分享与传承
  • 建立性能知识库
  • 定期分享会
  • 编写优化指南

总结

JavaScript性能优化是一个持续的过程,需要结合理论知识、实践经验和工具支持。通过手写实现这些优化技术,我们不仅能够解决具体的性能问题,更能深入理解性能优化的本质。

记住,最好的优化往往是那些能够从根本上解决问题的优化,而不是临时的修补。始终以用户体验为中心,以数据为依据,以持续改进为方法,才能构建出真正高性能的Web应用。

性能优化没有银弹,但有了这些手写实现的技术储备,你将能够更自信地面对各种性能挑战,构建出更快、更流畅的用户体验。

解放双手!使用Cursor+Figma MCP 高效还原响应式设计稿

在如今的快速迭代周期中,如何高效、精准地将设计稿转化为高质量的跨平台代码,一直是前端和移动端开发者的核心痛点。

传统的“切图”和“像素级还原”流程不仅耗时,还极易产生信息偏差。

好消息是,Figma 官方推出的 Model Component Protocol (MCP) 服务,结合 AI 代码神器 Cursor,正在彻底颠覆这一工作流。本文将为您揭示如何通过这一黄金组合,将设计到代码的流程缩短到分钟级,实现 90% 的视觉还原度。


Figma MCP 解决了什么实际问题?

一句话总结: 通过 MCP 直接读取 Figma 文件的结构化设计数据,结合 Cursor 的代码智能,实现多平台 UI 代码的自动化、精准生成。

痛点 解决方案(Cursor + Figma MCP) 效果
信息偏差 直接获取精确的尺寸、颜色、间距等核心数据。 消除“手抖”和“看错”的风险。
重复劳动 自动化生成基础 UI 结构代码 (如 Android XML, iOS SwiftUI, H5, RN)。 大幅减少模板代码的编写时间。
跨平台不一致 同一设计源生成多平台代码。 提升不同平台间 UI 的视觉和结构统一性。
迭代速度慢 从设计到可用代码耗时数天/数小时。 从设计到可用代码仅需几分钟

一、Cursor + Figma MCP 连接流程

Figma 官方在 9 月份推出 MCP 服务后,连接流程已经大大简化,告别了之前繁琐的第三方插件连接方式。

1.1 一键连接配置

整个配置过程现在是标准化的,简单可靠:

  1. 进入 Figma 官方 MCP 服务页面。服务页面

  2. 点击 Cursor 中的 “Add MCP to Cursor”image.png

  3. 系统将跳转至 Cursor 客户端,点击 Install

image.png

  1. 点击 Connect 开始身份验证。

  2. 在弹出的对话框中点击 Open,然后点击 Allow access 完成授权。

image.png

1.2 开始使用 MCP 服务

配置完成后,您就可以将设计数据喂给 Cursor 了:

  1. 在 Figma 客户端中,确保切换到 Dev Mode(这里需要将Figma升级到专业版或者教育版,可以看看某鱼)。
  2. 选择您想要生成代码的图层或组件
  3. 右键点击,选择 Copy link to selection

image.png

  1. 将复制的链接粘贴到 Cursor 的输入框或聊天界面中,并提出您的代码生成请求(例如:“为我生成这段设计的 Android XML 代码”)。

下面是我和AI对话的流程以及最后的效果图

# 任务:严格复刻响应式 Figma 组件
**1. 目标设计图链接(含 Node ID):**

{ https://www.figma.com/design/c4BVKzIN4oVNuxV9OzUhC8/Untitled?node-id=1-616&t=IiXRzf3Lq5QE7N3K-4 }

**2. 核心要求:**

- **复刻范围:** 请严格复刻链接指向的【首页/特定框架名称】中的**主要导航栏 (Header/Navbar)****英雄区 (Hero Section)**- **响应式处理:** 必须使用 Figma MCP 获取设计稿中的所有**响应式断点**(例如 Desktop、Tablet、Mobile)。代码必须使用媒体查询(Media Queries)或框架的响应式工具(如 Tailwind CSS 的 `md:``lg:` 前缀)来精确实现不同断点下的布局、间距和字体大小变化。

- **样式严格性:** 所有颜色、字体、圆角、内/外边距、阴影必须严格匹配 Figma 中定义的设计 Token。

**3. 技术栈和格式:**

- **语言/框架:** React (或 Next.js)。

- **样式方案:** Tailwind CSS (推荐用于响应式),或者 CSS Modules。

- **文件名:** 请将代码拆分成两个文件:`Header.jsx``HeroSection.jsx`**4. 额外功能/工具调用指示:**

- **Design Token:** 请优先调用 `figma/get_color_styles``figma/get_text_styles` 等 MCP 工具来提取并定义颜色变量和字体样式,作为代码的常量或 Tailwind 配置的扩展。

- **交互占位符:** 导航栏中的链接使用 `<a href="#">` 作为占位符。

---

请开始生成代码,位置为figmaMcp。

你要调用我配置好的 figmamcp server

image.png

再次根据设计稿检查有没有不合理的地方https://www.figma.com/design/c4BVKzIN4oVNuxV9OzUhC8/Untitled?node-id=1-616&t=IiXRzf3Lq5QE7N3K-4 ,并修改,要达成一比一复刻

image.png

二、使用Figma MCP的注意事项和规范

使用 Figma MCP(Model Component Protocol)规范与 Cursor 这样的 AI 编码工具结合时,要实现高效、准确的代码生成,您需要注意的要点可以总结为 "设计纪律""AI 引导" 两个方面。

Figma MCP 的作用是提供结构化数据,但数据的质量和清晰度完全取决于设计师在 Figma 中的操作。


1.设计师需要注意的“设计纪律”

AI 生成代码的质量,90% 取决于 Figma 文件的规范程度。 为了让 AI 能准确地“理解”设计稿,设计师必须做到以下几点:

(1) 结构化组件是基础

  • 使用 Auto Layout(自动布局): 这是实现响应式设计的核心。确保所有的布局(尤其是列表、卡片、侧边栏等)都使用 Auto Layout,并设置正确的填充(Padding)和间距(Gap)。AI 依赖 Auto Layout 来识别 FlexboxStackViewColumn/Row 等布局结构。
  • 清晰的图层命名: 这是最关键的一步。 图层名称应该是语义化的(例如:Item/ProductCardButton/PrimaryList/RecentOrders)。避免使用默认的名称(例如:Rectangle 1Group 4)。
  • 组件化和变体: 尽可能将设计元素抽象为 Figma 组件变体(Variants) 。这能让 AI 识别出哪些是可复用的模块,并生成组件代码(例如 React Components 或 SwiftUI Views)。

(2) 统一使用设计 Token/变量

  • 使用 Figma Variables(变量/令牌): 颜色、字体大小、间距(Spacing)等属性,应统一使用 Figma 的 Variables(设计令牌) 。AI 接收到的数据将是 $color-primary-500 而非 #007AFF,这能让生成的代码直接映射到您的企业代码主题
  • 字体和样式一致性: 确保所有文本都链接到同一套文本样式

(3) 明确的交付状态

  • 使用 Dev Mode Statuses: 设计师应该利用 Figma Dev Mode 中的状态标记(如 Ready for dev)来明确告知开发者和 AI 哪些部分可以开始使用了,避免 AI 读取到正在修改中的设计稿。

2. 开发者需要注意的“AI 引导”

即使设计稿非常规范,AI 默认生成的代码也可能不符合企业级项目的技术栈。开发者需要通过 Cursor Rules 等方式对 AI 进行二次约束和引导。

(1) 强制组件库替换

  • 制定 Cursor Rules: 针对上文提到的企业组件库适配问题,利用 Cursor 的规则配置能力,设定替换逻辑。

    • 示例规则: 如果 AI 识别到 Figma 中图层名为 Button/Primary,则强制生成的代码不是 <button><TextView>,而是 <MyCustomUIButton.Primary />
  • 输入提示(Prompt)引导: 在粘贴 Figma 链接时,明确告知 Cursor 应该使用哪个技术栈和组件库。

    • 示例 Prompt: “请为我生成这段设计的 Android Compose UI 代码,并确保使用我们团队的 "Dewu-Design-System" 中的组件。”

(2) 解决动态布局的语义化问题

  • 图层命名约定(与设计师协作): 与设计师团队约定一套统一的列表命名规则,例如:

    • 单个 Item: 图层名必须以 item__card 结尾。
    • 列表容器: 图层名必须以 list__container 结尾。
  • Rules 强制生成动态结构: 将上述命名约定植入 Cursor Rules 中,强制 AI 在识别到这些前缀时,生成 RecyclerViewLazyColumnFlatList 这样的动态列表结构,而不是一堆静态 View。

(3) 处理图片和本地资源

  • 资源缺失是常态: 目前,AI 无法自动下载本地图片资源。您需要手动下载并替换代码中的占位符。
  • Placeholder 命名: 确保设计稿中图片的图层命名是清晰的(例如:icon_user_avatar),这样生成的代码中对资源的引用名称也会更清晰,方便您替换。

Vue3 组件懒加载深度解析:从原理到极致优化的完整指南

摘要

组件懒加载是现代前端性能优化的核心技术,Vue3 提供了多种强大的懒加载方案。本文将深入探讨 Vue3 中组件懒加载的实现原理、使用场景、性能优化策略,通过详细的代码示例、执行流程分析和实际项目案例,帮助你全面掌握 Vue3 组件懒加载的完整知识体系。


一、 什么是组件懒加载?为什么需要它?

1.1 传统组件加载的问题

在传统的 Vue 应用中,所有组件通常被打包到一个 JavaScript 文件中:

// 传统同步导入方式
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'

const app = createApp({
  components: {
    Home,
    About,
    Contact
  }
})

传统方式的问题:

  • 首屏加载缓慢:用户需要下载整个应用代码才能看到首屏内容
  • 资源浪费:用户可能永远不会访问某些页面,但依然加载了对应组件
  • 用户体验差:特别是对于移动端用户和网络条件较差的场景
  • 缓存效率低:整个应用打包成一个文件,任何改动都会使缓存失效

1.2 组件懒加载的解决方案

懒加载(Lazy Loading)也称为代码分割(Code Splitting),它允许我们将代码分割成多个 chunk,只在需要时加载:

// 懒加载方式
const Home = () => import('./components/Home.vue')
const About = () => import('./components/About.vue')
const Contact = () => import('./components/Contact.vue')

懒加载的优势:

  • 更快的首屏加载:只加载当前页面需要的代码
  • 按需加载:根据用户操作动态加载组件
  • 更好的缓存:独立的 chunk 可以独立缓存
  • 优化用户体验:减少初始加载时间

二、 Vue3 组件懒加载核心概念

2.1 懒加载的工作原理

流程图:组件懒加载完整工作流程

flowchart TD
    A[用户访问应用] --> B[加载主包 main.js]
    B --> C[渲染首屏内容]
    C --> D{用户触发懒加载?}
    
    D -- 路由切换 --> E[加载对应路由组件]
    D -- 条件渲染 --> F[加载条件组件]
    D -- 用户交互 --> G[加载交互组件]
    
    E --> H[显示加载状态]
    F --> H
    G --> H
    
    H --> I[网络请求对应chunk]
    I --> J{加载成功?}
    J -- 是 --> K[渲染懒加载组件]
    J -- 否 --> L[显示错误状态]
    
    K --> M[组件激活使用]
    L --> N[提供重试机制]

2.2 懒加载的核心概念

  • 代码分割:将代码拆分成多个小块(chunks)
  • 动态导入:使用 import() 函数在运行时加载模块
  • 组件工厂:返回 Promise 的函数,解析为组件定义
  • 加载状态:在组件加载期间显示的回退内容
  • 错误处理:加载失败时的降级方案

三、 Vue3 组件懒加载基础实现

3.1 使用 defineAsyncComponent 实现懒加载

Vue3 提供了 defineAsyncComponent 函数来创建异步组件:

<template>
  <div class="basic-lazy-demo">
    <h2>基础懒加载示例</h2>
    
    <div class="controls">
      <button @click="showLazyComponent = !showLazyComponent" class="btn-primary">
        {{ showLazyComponent ? '隐藏' : '显示' }} 懒加载组件
      </button>
    </div>

    <div class="component-area">
      <!-- 同步加载的组件 -->
      <div v-if="!showLazyComponent" class="sync-component">
        <h3>同步加载的组件</h3>
        <p>这个组件在主包中,立即可用</p>
      </div>

      <!-- 懒加载的组件 -->
      <Suspense v-else>
        <template #default>
          <LazyBasicComponent />
        </template>
        <template #fallback>
          <div class="loading-state">
            <div class="spinner"></div>
            <p>懒加载组件加载中...</p>
          </div>
        </template>
      </Suspense>
    </div>

    <div class="bundle-info">
      <h3>打包信息分析</h3>
      <div class="info-grid">
        <div class="info-item">
          <span>主包大小:</span>
          <strong>~15KB</strong>
        </div>
        <div class="info-item">
          <span>懒加载组件大小:</span>
          <strong>~8KB (单独chunk)</strong>
        </div>
        <div class="info-item">
          <span>加载方式:</span>
          <strong>按需加载</strong>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'

const showLazyComponent = ref(false)

// 使用 defineAsyncComponent 定义懒加载组件
const LazyBasicComponent = defineAsyncComponent(() => 
  import('./components/LazyBasicComponent.vue')
)
</script>

<style scoped>
.basic-lazy-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
  font-family: Arial, sans-serif;
}

.controls {
  margin: 20px 0;
  text-align: center;
}

.btn-primary {
  padding: 12px 24px;
  background: #42b883;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: background 0.3s;
}

.btn-primary:hover {
  background: #369870;
}

.component-area {
  margin: 30px 0;
  min-height: 200px;
}

.sync-component {
  padding: 30px;
  background: #e3f2fd;
  border: 2px solid #2196f3;
  border-radius: 8px;
  text-align: center;
}

.sync-component h3 {
  margin: 0 0 15px 0;
  color: #1976d2;
}

.loading-state {
  padding: 40px;
  background: #fff3e0;
  border: 2px dashed #ff9800;
  border-radius: 8px;
  text-align: center;
  color: #e65100;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #ff9800;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 16px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.bundle-info {
  margin-top: 30px;
  padding: 20px;
  background: #f5f5f5;
  border-radius: 8px;
}

.bundle-info h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 15px;
}

.info-item {
  display: flex;
  justify-content: space-between;
  padding: 12px;
  background: white;
  border-radius: 6px;
  border-left: 4px solid #42b883;
}

.info-item span {
  color: #666;
}

.info-item strong {
  color: #2c3e50;
}
</style>

LazyBasicComponent.vue

<template>
  <div class="lazy-basic-component">
    <h3>🚀 懒加载组件已加载!</h3>
    <div class="component-content">
      <p>这个组件是通过懒加载方式动态加载的</p>
      <div class="features">
        <div class="feature">
          <span class="icon">📦</span>
          <span>独立 chunk</span>
        </div>
        <div class="feature">
          <span class="icon">⚡</span>
          <span>按需加载</span>
        </div>
        <div class="feature">
          <span class="icon">🎯</span>
          <span>性能优化</span>
        </div>
      </div>
      <p class="load-time">组件加载时间: {{ loadTime }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const loadTime = ref('')

onMounted(() => {
  loadTime.value = new Date().toLocaleTimeString()
  console.log('LazyBasicComponent 已挂载')
})
</script>

<style scoped>
.lazy-basic-component {
  padding: 30px;
  background: #e8f5e8;
  border: 2px solid #4caf50;
  border-radius: 8px;
  text-align: center;
}

.lazy-basic-component h3 {
  margin: 0 0 20px 0;
  color: #2e7d32;
  font-size: 24px;
}

.component-content {
  max-width: 400px;
  margin: 0 auto;
}

.features {
  display: flex;
  justify-content: space-around;
  margin: 25px 0;
  padding: 20px;
  background: white;
  border-radius: 8px;
}

.feature {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.feature .icon {
  font-size: 24px;
}

.feature span:last-child {
  font-size: 14px;
  color: #666;
}

.load-time {
  margin: 20px 0 0 0;
  padding: 10px;
  background: #2c3e50;
  color: white;
  border-radius: 4px;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}
</style>

3.2 路由级别的懒加载

在实际项目中,路由级别的懒加载是最常见的应用场景:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')  // 懒加载首页
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue') // 懒加载关于页
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('@/views/Products.vue') // 懒加载产品页
  },
  {
    path: '/contact',
    name: 'Contact',
    component: () => import('@/views/Contact.vue') // 懒加载联系页
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

带加载状态的路由懒加载:

<template>
  <div class="route-lazy-demo">
    <h2>路由级别懒加载示例</h2>
    
    <nav class="nav-tabs">
      <router-link 
        v-for="tab in tabs" 
        :key="tab.path"
        :to="tab.path"
        class="nav-tab"
        active-class="active"
      >
        {{ tab.name }}
      </router-link>
    </nav>

    <div class="route-content">
      <RouterView v-slot="{ Component }">
        <Suspense>
          <template #default>
            <component :is="Component" />
          </template>
          <template #fallback>
            <div class="route-loading">
              <div class="loading-content">
                <div class="spinner large"></div>
                <p>页面加载中...</p>
                <div class="loading-dots">
                  <span></span>
                  <span></span>
                  <span></span>
                </div>
              </div>
            </div>
          </template>
        </Suspense>
      </RouterView>
    </div>

    <div class="route-info">
      <h3>路由懒加载信息</h3>
      <div class="chunk-status">
        <div 
          v-for="chunk in chunkStatus" 
          :key="chunk.name"
          class="chunk-item"
          :class="chunk.status"
        >
          <span class="chunk-name">{{ chunk.name }}</span>
          <span class="chunk-status">{{ chunk.status }}</span>
          <span class="chunk-size">{{ chunk.size }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

const tabs = [
  { path: '/', name: '首页' },
  { path: '/about', name: '关于我们' },
  { path: '/products', name: '产品服务' },
  { path: '/contact', name: '联系我们' }
]

const chunkStatus = ref([
  { name: 'home', status: 'loaded', size: '15KB' },
  { name: 'about', status: 'pending', size: '12KB' },
  { name: 'products', status: 'pending', size: '25KB' },
  { name: 'contact', status: 'pending', size: '8KB' }
])

// 监听路由变化,模拟 chunk 加载状态
watch(() => route.name, (newRouteName) => {
  const chunkName = newRouteName.toLowerCase()
  chunkStatus.value.forEach(chunk => {
    if (chunk.name === chunkName) {
      chunk.status = 'loaded'
    }
  })
})
</script>

<style scoped>
.route-lazy-demo {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.nav-tabs {
  display: flex;
  background: #f8f9fa;
  border-radius: 8px;
  padding: 5px;
  margin: 20px 0;
}

.nav-tab {
  flex: 1;
  padding: 12px 20px;
  text-align: center;
  text-decoration: none;
  color: #666;
  border-radius: 6px;
  transition: all 0.3s;
}

.nav-tab:hover {
  background: #e9ecef;
  color: #333;
}

.nav-tab.active {
  background: #42b883;
  color: white;
}

.route-content {
  min-height: 400px;
  margin: 30px 0;
}

.route-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 300px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 2px dashed #dee2e6;
}

.loading-content {
  text-align: center;
  color: #666;
}

.spinner.large {
  width: 60px;
  height: 60px;
  border: 6px solid #f3f3f3;
  border-top: 6px solid #42b883;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 20px;
}

.loading-dots {
  display: flex;
  justify-content: center;
  gap: 4px;
  margin-top: 15px;
}

.loading-dots span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #42b883;
  animation: bounce 1.4s infinite ease-in-out;
}

.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }

@keyframes bounce {
  0%, 80%, 100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
}

.route-info {
  margin-top: 30px;
  padding: 20px;
  background: #2c3e50;
  border-radius: 8px;
  color: white;
}

.route-info h3 {
  margin: 0 0 15px 0;
  color: #42b883;
}

.chunk-status {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chunk-item {
  display: flex;
  justify-content: space-between;
  padding: 12px 15px;
  background: #34495e;
  border-radius: 6px;
  transition: all 0.3s;
}

.chunk-item.loaded {
  border-left: 4px solid #27ae60;
}

.chunk-item.pending {
  border-left: 4px solid #f39c12;
  opacity: 0.7;
}

.chunk-name {
  font-weight: bold;
  color: #ecf0f1;
}

.chunk-status {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  text-transform: uppercase;
}

.chunk-item.loaded .chunk-status {
  background: #27ae60;
  color: white;
}

.chunk-item.pending .chunk-status {
  background: #f39c12;
  color: white;
}

.chunk-size {
  color: #bdc3c7;
  font-family: 'Courier New', monospace;
}
</style>

四、 高级懒加载配置与优化

4.1 完整的异步组件配置

Vue3 的 defineAsyncComponent 支持完整的配置选项:

<template>
  <div class="advanced-lazy-demo">
    <h2>高级懒加载配置</h2>
    
    <div class="controls">
      <button @click="loadComponent('success')" class="btn-success">
        加载成功组件
      </button>
      <button @click="loadComponent('error')" class="btn-error">
        加载错误组件
      </button>
      <button @click="loadComponent('timeout')" class="btn-warning">
        加载超时组件
      </button>
      <button @click="loadComponent('delay')" class="btn-info">
        加载延迟组件
      </button>
    </div>

    <div class="component-area">
      <AdvancedAsyncComponent 
        v-if="currentComponent"
        :key="componentKey"
      />
    </div>

    <div class="config-info">
      <h3>异步组件配置说明</h3>
      <div class="config-grid">
        <div class="config-item">
          <h4>loader</h4>
          <p>组件加载函数,返回 Promise</p>
        </div>
        <div class="config-item">
          <h4>loadingComponent</h4>
          <p>加载过程中显示的组件</p>
        </div>
        <div class="config-item">
          <h4>errorComponent</h4>
          <p>加载失败时显示的组件</p>
        </div>
        <div class="config-item">
          <h4>delay</h4>
          <p>延迟显示加载状态(避免闪烁)</p>
        </div>
        <div class="config-item">
          <h4>timeout</h4>
          <p>加载超时时间</p>
        </div>
        <div class="config-item">
          <h4>onError</h4>
          <p>错误处理回调函数</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'
import LoadingSpinner from './components/LoadingSpinner.vue'
import ErrorDisplay from './components/ErrorDisplay.vue'

const currentComponent = ref(null)
const componentKey = ref(0)

// 模拟不同加载场景的组件
const componentConfigs = {
  success: () => import('./components/SuccessComponent.vue'),
  error: () => Promise.reject(new Error('模拟加载错误')),
  timeout: () => new Promise(() => {}), // 永远不会 resolve
  delay: () => new Promise(resolve => {
    setTimeout(() => {
      resolve(import('./components/DelayedComponent.vue'))
    }, 3000)
  })
}

// 高级异步组件配置
const AdvancedAsyncComponent = defineAsyncComponent({
  // 加载器函数
  loader: () => currentComponent.value?.loader() || Promise.reject(new Error('未选择组件')),
  
  // 加载中显示的组件
  loadingComponent: LoadingSpinner,
  
  // 加载失败显示的组件
  errorComponent: ErrorDisplay,
  
  // 延迟显示加载状态(避免闪烁)
  delay: 200,
  
  // 超时时间(毫秒)
  timeout: 5000,
  
  // 错误处理函数
  onError: (error, retry, fail, attempts) => {
    console.error(`组件加载失败 (尝试次数: ${attempts}):`, error)
    
    // 最多重试 3 次
    if (attempts <= 3) {
      console.log(`第 ${attempts} 次重试...`)
      retry()
    } else {
      fail()
    }
  },
  
  // 可挂起(Suspense 相关)
  suspensible: false
})

const loadComponent = (type) => {
  currentComponent.value = {
    loader: componentConfigs[type],
    type: type
  }
  componentKey.value++ // 强制重新创建组件
}
</script>

<style scoped>
.advanced-lazy-demo {
  padding: 20px;
  max-width: 1000px;
  margin: 0 auto;
}

.controls {
  display: flex;
  gap: 15px;
  justify-content: center;
  margin: 30px 0;
  flex-wrap: wrap;
}

.btn-success { background: #27ae60; }
.btn-error { background: #e74c3c; }
.btn-warning { background: #f39c12; }
.btn-info { background: #3498db; }

.btn-success, .btn-error, .btn-warning, .btn-info {
  color: white;
  border: none;
  padding: 12px 20px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.btn-success:hover { background: #229954; }
.btn-error:hover { background: #c0392b; }
.btn-warning:hover { background: #e67e22; }
.btn-info:hover { background: #2980b9; }

.component-area {
  min-height: 300px;
  margin: 30px 0;
  border: 2px dashed #ddd;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.config-info {
  margin-top: 40px;
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
}

.config-info h3 {
  margin: 0 0 20px 0;
  color: #2c3e50;
  text-align: center;
}

.config-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
}

.config-item {
  padding: 20px;
  background: white;
  border-radius: 8px;
  border-left: 4px solid #42b883;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.config-item h4 {
  margin: 0 0 10px 0;
  color: #42b883;
  font-size: 16px;
}

.config-item p {
  margin: 0;
  color: #666;
  line-height: 1.5;
}
</style>

LoadingSpinner.vue

<template>
  <div class="loading-spinner">
    <div class="spinner-container">
      <div class="spinner"></div>
      <p>组件加载中...</p>
      <div class="progress">
        <div class="progress-bar" :style="progressStyle"></div>
      </div>
      <p class="hint">这通常很快,请耐心等待</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const progress = ref(0)
let progressInterval

onMounted(() => {
  progressInterval = setInterval(() => {
    progress.value = Math.min(progress.value + Math.random() * 10, 90)
  }, 200)
})

onUnmounted(() => {
  clearInterval(progressInterval)
})

const progressStyle = {
  width: `${progress.value}%`
}
</script>

<style scoped>
.loading-spinner {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  text-align: center;
}

.spinner-container {
  max-width: 300px;
}

.spinner {
  width: 50px;
  height: 50px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #42b883;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 20px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.progress {
  width: 100%;
  height: 6px;
  background: #f0f0f0;
  border-radius: 3px;
  margin: 15px 0;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background: linear-gradient(90deg, #42b883, #369870);
  border-radius: 3px;
  transition: width 0.3s ease;
}

.hint {
  font-size: 12px;
  color: #999;
  margin: 10px 0 0 0;
}
</style>

ErrorDisplay.vue

<template>
  <div class="error-display">
    <div class="error-container">
      <div class="error-icon">❌</div>
      <h3>组件加载失败</h3>
      <p class="error-message">{{ error?.message || '未知错误' }}</p>
      <div class="error-actions">
        <button @click="retry" class="retry-btn">
          🔄 重试加载
        </button>
        <button @click="reset" class="reset-btn">
          🏠 返回首页
        </button>
      </div>
      <p class="error-hint">如果问题持续存在,请联系技术支持</p>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  error: {
    type: Error,
    default: null
  }
})

const emit = defineEmits(['retry'])

const retry = () => {
  emit('retry')
}

const reset = () => {
  window.location.href = '/'
}
</script>

<style scoped>
.error-display {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  text-align: center;
}

.error-container {
  max-width: 400px;
  padding: 30px;
  background: #fff5f5;
  border: 2px solid #fed7d7;
  border-radius: 8px;
}

.error-icon {
  font-size: 48px;
  margin-bottom: 20px;
}

.error-container h3 {
  margin: 0 0 15px 0;
  color: #e53e3e;
}

.error-message {
  color: #718096;
  margin-bottom: 20px;
  padding: 10px;
  background: white;
  border-radius: 4px;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}

.error-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-bottom: 15px;
}

.retry-btn, .reset-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.retry-btn {
  background: #4299e1;
  color: white;
}

.retry-btn:hover {
  background: #3182ce;
}

.reset-btn {
  background: #e2e8f0;
  color: #4a5568;
}

.reset-btn:hover {
  background: #cbd5e0;
}

.error-hint {
  font-size: 12px;
  color: #a0aec0;
  margin: 0;
}
</style>

4.2 条件懒加载与预加载

<template>
  <div class="conditional-lazy-demo">
    <h2>条件懒加载与预加载策略</h2>
    
    <div class="strategies">
      <div class="strategy">
        <h3>1. 条件懒加载</h3>
        <div class="demo-section">
          <label class="toggle-label">
            <input type="checkbox" v-model="enableHeavyComponent">
            启用重型组件
          </label>
          <div class="component-container">
            <HeavyComponent v-if="enableHeavyComponent" />
          </div>
        </div>
      </div>

      <div class="strategy">
        <h3>2. 预加载策略</h3>
        <div class="demo-section">
          <div class="preload-buttons">
            <button @click="preloadComponent('chart')" class="preload-btn">
              预加载图表组件
            </button>
            <button @click="preloadComponent('editor')" class="preload-btn">
              预加载编辑器
            </button>
          </div>
          <div class="preload-status">
            <div 
              v-for="item in preloadStatus" 
              :key="item.name"
              class="status-item"
              :class="item.status"
            >
              <span>{{ item.name }}</span>
              <span class="status-dot"></span>
            </div>
          </div>
        </div>
      </div>

      <div class="strategy">
        <h3>3. 可见时加载</h3>
        <div class="demo-section">
          <div class="scroll-container">
            <div 
              v-for="n in 10" 
              :key="n"
              class="scroll-item"
            >
              <p>内容区块 {{ n }}</p>
              <LazyWhenVisible v-if="n === 5" />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue'

// 1. 条件懒加载
const enableHeavyComponent = ref(false)
const HeavyComponent = defineAsyncComponent(() => 
  import('./components/HeavyComponent.vue')
)

// 2. 预加载策略
const preloadStatus = reactive([
  { name: '图表组件', status: 'pending' },
  { name: '编辑器组件', status: 'pending' }
])

const preloadedComponents = {}

const preloadComponent = async (type) => {
  const index = preloadStatus.findIndex(item => item.name.includes(type))
  if (index === -1) return

  preloadStatus[index].status = 'loading'
  
  try {
    if (type === 'chart') {
      preloadedComponents.chart = await import('./components/ChartComponent.vue')
    } else if (type === 'editor') {
      preloadedComponents.editor = await import('./components/EditorComponent.vue')
    }
    
    preloadStatus[index].status = 'loaded'
    console.log(`${type} 组件预加载完成`)
  } catch (error) {
    preloadStatus[index].status = 'error'
    console.error(`${type} 组件预加载失败:`, error)
  }
}

// 3. 可见时加载
const LazyWhenVisible = defineAsyncComponent(() => 
  import('./components/LazyWhenVisible.vue')
)

// 模拟预加载
onMounted(() => {
  // 空闲时预加载可能用到的组件
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      preloadComponent('chart')
    })
  }
})
</script>

<style scoped>
.conditional-lazy-demo {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.strategies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
  gap: 30px;
  margin: 30px 0;
}

.strategy {
  padding: 25px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e9ecef;
}

.strategy h3 {
  margin: 0 0 20px 0;
  color: #2c3e50;
  font-size: 18px;
}

.demo-section {
  min-height: 200px;
}

.toggle-label {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 15px;
  cursor: pointer;
  font-weight: bold;
  color: #333;
}

.component-container {
  min-height: 150px;
  border: 2px dashed #ddd;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.preload-buttons {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.preload-btn {
  padding: 10px 16px;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.3s;
}

.preload-btn:hover {
  background: #2980b9;
}

.preload-status {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 12px;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #bdc3c7;
}

.status-item.pending {
  border-left-color: #f39c12;
}

.status-item.loading {
  border-left-color: #3498db;
}

.status-item.loaded {
  border-left-color: #27ae60;
}

.status-item.error {
  border-left-color: #e74c3c;
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #bdc3c7;
}

.status-item.pending .status-dot { background: #f39c12; }
.status-item.loading .status-dot { 
  background: #3498db;
  animation: pulse 1.5s infinite;
}
.status-item.loaded .status-dot { background: #27ae60; }
.status-item.error .status-dot { background: #e74c3c; }

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.scroll-container {
  height: 300px;
  overflow-y: auto;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 10px;
}

.scroll-item {
  padding: 20px;
  margin: 10px 0;
  background: white;
  border-radius: 4px;
  border: 1px solid #f0f0f0;
  min-height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.scroll-item p {
  margin: 0;
  color: #666;
}
</style>

五、 性能优化与最佳实践

5.1 Webpack 打包优化配置

// vue.config.js
const { defineConfig } = require('@vue/cli-service')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = defineConfig({
  transpileDependencies: true,
  
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          // 第三方库单独打包
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            priority: 20,
            chunks: 'all'
          },
          // Vue 相关库单独打包
          vue: {
            test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
            name: 'vue-vendors',
            priority: 30,
            chunks: 'all'
          },
          // 公共代码提取
          common: {
            name: 'common',
            minChunks: 2,
            priority: 10,
            chunks: 'all'
          }
        }
      }
    },
    plugins: [
      // 打包分析工具(开发时使用)
      process.env.NODE_ENV === 'development' && 
      new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        openAnalyzer: false
      })
    ].filter(Boolean)
  },
  
  chainWebpack: config => {
    // 预加载配置
    config.plugin('preload').tap(options => {
      options[0] = {
        rel: 'preload',
        as(entry) {
          if (/\.css$/.test(entry)) return 'style'
          if (/\.(woff|woff2)$/.test(entry)) return 'font'
          return 'script'
        },
        include: 'initial',
        fileBlacklist: [/\.map$/, /hot-update\.js$/]
      }
      return options
    })
    
    //  prefetch 配置
    config.plugin('prefetch').tap(options => {
      options[0] = {
        rel: 'prefetch',
        include: 'asyncChunks'
      }
      return options
    })
  }
})

5.2 性能监控与错误追踪

<template>
  <div class="performance-monitor">
    <h2>懒加载性能监控</h2>
    
    <div class="metrics-dashboard">
      <div class="metric-cards">
        <div class="metric-card">
          <div class="metric-value">{{ metrics.totalLoads }}</div>
          <div class="metric-label">总加载次数</div>
        </div>
        <div class="metric-card">
          <div class="metric-value">{{ metrics.averageLoadTime }}ms</div>
          <div class="metric-label">平均加载时间</div>
        </div>
        <div class="metric-card">
          <div class="metric-value">{{ metrics.successRate }}%</div>
          <div class="metric-label">成功率</div>
        </div>
        <div class="metric-card">
          <div class="metric-value">{{ metrics.cacheHits }}</div>
          <div class="metric-label">缓存命中</div>
        </div>
      </div>

      <div class="load-timeline">
        <h3>组件加载时间线</h3>
        <div class="timeline">
          <div 
            v-for="event in loadEvents" 
            :key="event.id"
            class="timeline-event"
            :class="event.status"
          >
            <div class="event-time">{{ event.timestamp }}</div>
            <div class="event-name">{{ event.name }}</div>
            <div class="event-duration">{{ event.duration }}ms</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'

const metrics = reactive({
  totalLoads: 0,
  averageLoadTime: 0,
  successRate: 100,
  cacheHits: 0
})

const loadEvents = ref([])

// 监控组件加载性能
const monitorComponentLoad = (componentName) => {
  const startTime = performance.now()
  const eventId = Date.now()
  
  const loadEvent = {
    id: eventId,
    name: componentName,
    timestamp: new Date().toLocaleTimeString(),
    status: 'loading',
    duration: 0
  }
  
  loadEvents.value.unshift(loadEvent)
  if (loadEvents.value.length > 10) {
    loadEvents.value.pop()
  }
  
  metrics.totalLoads++
  
  return {
    success: () => {
      const endTime = performance.now()
      const duration = endTime - startTime
      
      loadEvent.status = 'success'
      loadEvent.duration = Math.round(duration)
      
      // 更新平均加载时间
      const totalTime = metrics.averageLoadTime * (metrics.totalLoads - 1) + duration
      metrics.averageLoadTime = Math.round(totalTime / metrics.totalLoads)
    },
    error: () => {
      const endTime = performance.now()
      const duration = endTime - startTime
      
      loadEvent.status = 'error'
      loadEvent.duration = Math.round(duration)
      
      // 更新成功率
      const successCount = Math.floor(metrics.totalLoads * (metrics.successRate / 100))
      metrics.successRate = Math.round((successCount / metrics.totalLoads) * 100)
    },
    cacheHit: () => {
      metrics.cacheHits++
    }
  }
}

// 示例:监控组件加载
const loadMonitoredComponent = async (componentName) => {
  const monitor = monitorComponentLoad(componentName)
  
  try {
    // 模拟组件加载
    await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500))
    
    // 检查是否缓存命中
    if (Math.random() > 0.7) {
      monitor.cacheHit()
    }
    
    monitor.success()
    return true
  } catch (error) {
    monitor.error()
    return false
  }
}

// 模拟一些加载事件
onMounted(async () => {
  const components = ['首页', '用户面板', '设置页面', '数据分析', '文档查看']
  
  for (const component of components) {
    await loadMonitoredComponent(component)
    await new Promise(resolve => setTimeout(resolve, 1000))
  }
})
</script>

<style scoped>
.performance-monitor {
  padding: 20px;
  max-width: 1000px;
  margin: 0 auto;
}

.metrics-dashboard {
  margin: 30px 0;
}

.metric-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.metric-card {
  padding: 25px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  text-align: center;
  border-top: 4px solid #42b883;
}

.metric-value {
  font-size: 32px;
  font-weight: bold;
  color: #2c3e50;
  margin-bottom: 8px;
}

.metric-label {
  color: #7f8c8d;
  font-size: 14px;
}

.load-timeline {
  background: white;
  border-radius: 8px;
  padding: 25px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.load-timeline h3 {
  margin: 0 0 20px 0;
  color: #2c3e50;
}

.timeline {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.timeline-event {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 15px;
  border-radius: 6px;
  border-left: 4px solid #bdc3c7;
  transition: all 0.3s;
}

.timeline-event.loading {
  border-left-color: #3498db;
  background: #ebf5fb;
}

.timeline-event.success {
  border-left-color: #27ae60;
  background: #eafaf1;
}

.timeline-event.error {
  border-left-color: #e74c3c;
  background: #fdedec;
}

.event-time {
  font-size: 12px;
  color: #7f8c8d;
  min-width: 80px;
}

.event-name {
  flex: 1;
  font-weight: 500;
  color: #2c3e50;
}

.event-duration {
  font-family: 'Courier New', monospace;
  font-weight: bold;
  color: #34495e;
  min-width: 60px;
  text-align: right;
}
</style>

六、 实际项目中的应用场景

6.1 大型管理系统的懒加载策略

// src/utils/lazyLoading.js
export const createLazyComponent = (loader, options = {}) => {
  const defaultOptions = {
    loadingComponent: () => import('@/components/Loading/LoadingState.vue'),
    errorComponent: () => import('@/components/Error/ErrorState.vue'),
    delay: 200,
    timeout: 10000,
    retryAttempts: 3
  }
  
  return defineAsyncComponent({
    loader,
    ...defaultOptions,
    ...options
  })
}

// 业务组件懒加载
export const LazyUserManagement = createLazyComponent(
  () => import('@/views/UserManagement.vue'),
  { timeout: 15000 }
)

export const LazyDataAnalytics = createLazyComponent(
  () => import('@/views/DataAnalytics.vue')
)

export const LazyReportGenerator = createLazyComponent(
  () => import('@/views/ReportGenerator.vue')
)

// 功能模块懒加载
export const LazyRichEditor = createLazyComponent(
  () => import('@/components/Editors/RichEditor.vue')
)

export const LazyChartLibrary = createLazyComponent(
  () => import('@/components/Charts/ChartLibrary.vue')
)

// 预加载策略
export const preloadCriticalComponents = () => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      // 预加载关键组件
      import('@/views/Dashboard.vue')
      import('@/components/Common/SearchBox.vue')
    })
  }
}

// 路由级别的分组懒加载
export const createRouteGroup = (groupName) => {
  return {
    user: () => import(/* webpackChunkName: "user-group" */ `@/views/${groupName}/User.vue`),
    profile: () => import(/* webpackChunkName: "user-group" */ `@/views/${groupName}/Profile.vue`),
    settings: () => import(/* webpackChunkName: "user-group" */ `@/views/${groupName}/Settings.vue`)
  }
}

6.2 基于用户行为的智能预加载

<template>
  <div class="smart-preload-demo">
    <h2>智能预加载策略</h2>
    
    <div class="user-journey">
      <div class="journey-step" @mouseenter="preloadStep('products')">
        <h3>1. 浏览产品</h3>
        <p>鼠标悬停预加载产品详情</p>
      </div>
      
      <div class="journey-step" @click="preloadStep('checkout')">
        <h3>2. 加入购物车</h3>
        <p>点击预加载结算页面</p>
      </div>
      
      <div class="journey-step" @touchstart="preloadStep('payment')">
        <h3>3. 结算支付</h3>
        <p>触摸预加载支付组件</p>
      </div>
    </div>

    <div class="preload-strategies">
      <h3>预加载策略状态</h3>
      <div class="strategy-grid">
        <div 
          v-for="strategy in strategies" 
          :key="strategy.name"
          class="strategy-item"
          :class="strategy.status"
        >
          <div class="strategy-icon">{{ strategy.icon }}</div>
          <div class="strategy-info">
            <div class="strategy-name">{{ strategy.name }}</div>
            <div class="strategy-desc">{{ strategy.description }}</div>
          </div>
          <div class="strategy-status">{{ strategy.status }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'

const strategies = reactive([
  {
    name: '悬停预加载',
    description: '鼠标悬停时预加载目标组件',
    icon: '🖱️',
    status: '等待触发',
    trigger: 'mouseenter'
  },
  {
    name: '点击预加载',
    description: '用户点击时预加载下一页面',
    icon: '👆',
    status: '等待触发',
    trigger: 'click'
  },
  {
    name: '触摸预加载',
    description: '移动端触摸时预加载',
    icon: '📱',
    status: '等待触发',
    trigger: 'touchstart'
  },
  {
    name: '空闲预加载',
    description: '浏览器空闲时预加载',
    icon: '💤',
    status: '等待触发',
    trigger: 'idle'
  }
])

const preloadedComponents = new Set()

const preloadStep = async (step) => {
  const strategy = strategies.find(s => s.trigger === step)
  if (strategy && strategy.status === '等待触发') {
    strategy.status = '加载中...'
    
    try {
      // 模拟组件预加载
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      strategy.status = '已加载'
      preloadedComponents.add(step)
      console.log(`✅ ${step} 组件预加载完成`)
    } catch (error) {
      strategy.status = '加载失败'
      console.error(`❌ ${step} 组件预加载失败:`, error)
    }
  }
}

// 空闲时预加载
onMounted(() => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      const idleStrategy = strategies.find(s => s.trigger === 'idle')
      if (idleStrategy) {
        idleStrategy.status = '已加载'
        preloadedComponents.add('common')
        console.log('🕒 空闲时预加载完成')
      }
    })
  }
})
</script>

<style scoped>
.smart-preload-demo {
  padding: 20px;
  max-width: 1000px;
  margin: 0 auto;
}

.user-journey {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin: 30px 0;
}

.journey-step {
  padding: 30px;
  background: white;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
}

.journey-step:hover {
  border-color: #42b883;
  transform: translateY(-2px);
  box-shadow: 0 4px 15px rgba(66, 184, 131, 0.2);
}

.journey-step h3 {
  margin: 0 0 10px 0;
  color: #2c3e50;
}

.journey-step p {
  margin: 0;
  color: #7f8c8d;
  font-size: 14px;
}

.preload-strategies {
  margin-top: 40px;
}

.preload-strategies h3 {
  margin: 0 0 20px 0;
  color: #2c3e50;
}

.strategy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 15px;
}

.strategy-item {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 20px;
  background: white;
  border-radius: 8px;
  border-left: 4px solid #bdc3c7;
  transition: all 0.3s;
}

.strategy-item.等待触发 {
  border-left-color: #f39c12;
}

.strategy-item.加载中 {
  border-left-color: #3498db;
}

.strategy-item.已加载 {
  border-left-color: #27ae60;
}

.strategy-item.加载失败 {
  border-left-color: #e74c3c;
}

.strategy-icon {
  font-size: 24px;
}

.strategy-info {
  flex: 1;
}

.strategy-name {
  font-weight: bold;
  color: #2c3e50;
  margin-bottom: 4px;
}

.strategy-desc {
  font-size: 12px;
  color: #7f8c8d;
}

.strategy-status {
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  text-transform: uppercase;
}

.strategy-item.等待触发 .strategy-status {
  background: #fff3cd;
  color: #856404;
}

.strategy-item.加载中 .strategy-status {
  background: #d1ecf1;
  color: #0c5460;
}

.strategy-item.已加载 .strategy-status {
  background: #d4edda;
  color: #155724;
}

.strategy-item.加载失败 .strategy-status {
  background: #f8d7da;
  color: #721c24;
}
</style>

七、 总结

7.1 Vue3 组件懒加载的核心价值

  1. 性能优化:显著减少首屏加载时间,提升用户体验
  2. 资源效率:按需加载,避免资源浪费
  3. 缓存优化:独立的 chunk 可以更好地利用浏览器缓存
  4. 用户体验:合理的加载状态和错误处理提升用户满意度

7.2 懒加载实现方式总结

方式 适用场景 优点 缺点
defineAsyncComponent 条件渲染组件 配置灵活,错误处理完善 需要手动管理加载状态
路由懒加载 页面级组件 天然的业务分割,实现简单 页面切换可能有延迟
Suspense + 异步组件 需要加载状态的场景 声明式,代码简洁 需要 Vue3 支持
动态 import() 模块级懒加载 标准语法,通用性强 需要配合构建工具

7.3 性能优化最佳实践

  1. 合理分割代码:按照业务模块和功能进行代码分割
  2. 预加载策略:根据用户行为预测并预加载可能需要的组件
  3. 加载状态管理:提供友好的加载反馈和错误处理
  4. 缓存策略:利用浏览器缓存和 Service Worker
  5. 监控分析:持续监控加载性能,优化分割策略

7.4 注意事项

  • 避免过度分割:太多的 chunk 会增加 HTTP 请求开销
  • 错误处理:必须处理加载失败的情况
  • 测试覆盖:确保懒加载组件在各种网络条件下的表现
  • SEO 考虑:服务端渲染时需要考虑懒加载组件的处理

Vue3 的组件懒加载为现代前端应用提供了强大的性能优化手段。通过合理运用各种懒加载策略,可以显著提升应用性能,改善用户体验。


如果这篇文章对你有帮助,欢迎点赞、收藏和评论!有任何问题都可以在评论区讨论。在这里插入图片描述

平面几何:如何绘制一个星形?

大家好,我是前端西瓜哥。

也是有一个月没写文章了。主要是 AI 太强了,简单的东西已经没有写的必要的,复杂的不好写。但多少还是要写点。

今天我们来绘制 Figma 的星形。

图片

星形的绘制,比较简单,其实就是求两个同心圆的内接正多边形的点,将两组点两两连接即可。

方法的参数有:width、height、count、innerScale。方法签名为:

(
  width: number,
  height: number,
  count: number,
  innerScale: number,
) => Point[];

首先是 归一,求出宽高为 1 的矩形下(其实也是半径为 0.5 的原型)的内接多边形的点集。

从最顶部的点开始,不断地旋转 360 / count 的角度,得到 count 个点。中心点是坐标原点。

const getInnerRegularPolygon = (radius: number, count: number): Point[] => {
const p = { x: 0, y: -radius };
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

const rotate = (p: Point, rad: number) => {
return {
    x: p.x * Math.cos(rad) - p.y * Math.sin(rad),
    y: p.x * Math.sin(rad) + p.y * Math.cos(rad),
  };
};

这里是基于第一个点,不断地应用一个新的旋转角度。

还有一种方案是基于上一个点,做同样的增量旋转矩阵,但不是很建议,这是一种 “累加” 的策略,会导致误差的累加。

中间步骤越多,误差就累加的越大。类似的有对图形的移动,基于起点的位移会更可靠,基于 mousemove 的上一个点位移则会有很多问题。

图片

接下来是绘制更小内接多边形。

半径改为 innerScale 就好了。innerScale 代表的小圆是相对大圆的大小。

不过起点的位置需要调整下,要顺时针旋转 360 / count / 2 的角度,然后再基于这个点去旋转。

否则你可能得到下面这样一个多边形环。

图片

所以需要改造下 getInnerRegularPolygon,提供个起始角度。

const getInnerRegularPolygon = (
  radius: number,
  count: number,
  startAngle: number = 0,
): Point[] => {
let p = { x: 0, y: -radius };
if (startAngle) {
    p = rotate(p, startAngle);
  }
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

然后我们得到一个大的正多边形,和一个小的歪了点的正多边形。

图片

点是对了,就是点的顺序要调整下。我们对大多边形和小多边形的两组点,两两顺排。

const outerPoints = getInnerRegularPolygon(1, count);
const innerPoints = getInnerRegularPolygon(
  innerScale,
  count,
  Math.PI / count,
);

const points = [];
for (let i0; i < count; i++) {
  points.push(outerPoints[i]);
  points.push(innerPoints[i]);
}

到这里我们绘制了一个 2x2 圆的内接星形。(到这里才发现 1x1 要传入 0.5 或者改多边形算法实现才行,想了下 2x2 也问题不大)

后面我们给这些点放大和位移就齐活了。scale(width/2, height/2) * translate(width/2, height/2)

for (let i0; i < points.length; i++) {
  points[i].x = points[i].x * halfWidth + halfWidth;
  points[i].y = points[i].y * halfHeight + halfHeight;
}

return points;

图片

体验

线上体验地址:

geo-play-nv7v.vercel.app/src/page/st…

图片

特殊的,innerScale 是 1 的话,就会让 n 角星形变成 2n 多边形。

另外,可以看到,包围盒其实是一个圆形,而不是矩形,这就是为什么 Figma 的 星形和多边形在矩形包围盒下会有空隙 的原因。因为包围盒它不是圆形的。

代码实现

interface Point {
  x: number;
  y: number;
}

const getInnerRegularPolygon = (
  radius: number,
  count: number,
  startAngle: number = 0,
): Point[] => {
let p = { x: 0, y: -radius };
if (startAngle) {
    p = rotate(p, startAngle);
  }
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

const rotate = (p: Point, rad: number) => {
return {
    x: p.x * Math.cos(rad) - p.y * Math.sin(rad),
    y: p.x * Math.sin(rad) + p.y * Math.cos(rad),
  };
};

exportconst getStarPoints = (
  width: number,
  height: number,
  count: number,
  innerScale: number,
): Point[] => {
const outerPoints = getInnerRegularPolygon(1, count);
const innerPoints = getInnerRegularPolygon(
    innerScale,
    count,
    Math.PI / count,
  );

const points = [];
for (let i0; i < count; i++) {
    points.push(outerPoints[i]);
    points.push(innerPoints[i]);
  }

const halfWidth = width / 2;
const halfHeight = height / 2;

// scale(width/2, height/2) * translate(width/2, height/2)
for (let i0; i < points.length; i++) {
    points[i].x = points[i].x * halfWidth + halfWidth;
    points[i].y = points[i].y * halfHeight + halfHeight;
  }

return points;
};


结尾

星形,本质是两个多边形的点的交替连接。

几何算法很有趣吧。

我是前端西瓜哥,关注我,学习更多平面几何知识。


相关阅读,

平面几何:求内接或外切于圆的正多边形

平面几何:判断点是否在多边形内(射线法)

Vue的Class绑定对象语法如何让动态类名切换变得直观高效?

大家在开发Vue项目时,肯定遇到过这样的场景:按钮点击后要切换“激活状态”的样式,表单验证失败要显示“错误提示”的红色文本,或者 tabs 切换时高亮当前标签。这些动态切换类名的需求,Vue 的 Class 绑定对象语法 能完美解决——它就像一个“样式开关”,让类名跟着数据状态自动变化,彻底告别手动拼接字符串的麻烦。

一、对象语法基础:键是类名,值是“开关”

Vue 为 class 属性提供了特殊的 v-bind(简写为 :)增强:当你绑定一个对象时,对象的键是要添加的类名,**值是布尔值 **(或返回布尔值的表达式),用来决定这个类是否“生效”。

最基础的例子:按钮激活状态


<template>
  <!-- 当 isActive 为 true 时,添加 active 类 -->
  <button :class="{ active: isActive }" @click="toggleActive">
    {{ isActive ? '激活' : '未激活' }}
  </button>
</template>

<script setup>
  import {ref} from 'vue'
  // 响应式变量:控制按钮是否激活
  const isActive = ref(false)
  // 点击事件:切换 isActive 状态
  const toggleActive = () => isActive.value = !isActive.value
</script>

<style>
  .active {
    background-color: #42b983;
    color: white;
    border: none;
  }

  button {
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
  }
</style>

这段代码的逻辑很直观:

  • isActive 是用 ref 定义的响应式变量(初始为 false);
  • 点击按钮时,toggleActive 函数翻转 isActive 的值;
  • :class="{ active: isActive }" 会根据 isActive 的值自动添加/移除 active 类。

小细节:类名带连字符怎么办?

如果类名像 text-danger 这样包含连字符(不符合 JavaScript 标识符规则),必须用引号把键包起来,否则会报语法错误:

<!-- 正确写法:'text-danger' 作为字符串键 -->
<div :class="{ 'text-danger': hasError }"></div>

二、静态类与动态类的“和平共处”

实际开发中,元素往往既有固定不变的静态类(比如布局类 container),又有动态切换的类(比如 active)。Vue 允许你同时使用 class 属性和 :class 绑定,两者会自动合并:


<template>
  <!-- 静态类 container + 动态类 active/text-danger -->
  <div class="container" :class="{ active: isActive, 'text-danger': hasError }">
    内容区域
  </div>
</template>

<script setup>
  import {ref} from 'vue'

  const isActive = ref(true)   // 激活状态
  const hasError = ref(false) // 错误状态
</script>

<style>
  .container {
    padding: 20px;
    border: 1px solid #eee;
  }

  .active {
    border-color: #42b983;
  }

  .text-danger {
    color: #e53935;
  }
</style>

此时渲染的结果是:<div class="container active">内容区域</div>。如果 hasError 变为 true,结果会变成 container active text-danger——完全不用手动拼接字符串!

三、从“Inline 对象”到“响应式对象”:让代码更整洁

如果动态类很多,inline 对象会让模板变得拥挤。这时可以把类对象抽到响应式变量计算属性里,让代码更可读。

1. 用 reactive 定义类对象(Composition API)

如果类的状态比较固定,可以用 reactive 定义一个响应式的类对象,直接绑定到 :class


<template>
  <div :class="classObject">响应式类对象示例</div>
</template>

<script setup>
  import {reactive} from 'vue'
  // 用 reactive 定义响应式的类对象
  const classObject = reactive({
    active: true,
    'text-danger': false,
    'font-large': true
  })
</script>

<style>
  .font-large {
    font-size: 18px;
  }
</style>

这里 classObject 是响应式的,修改它的属性会直接更新类名:比如 classObject['text-danger'] = true,会立即添加 text-danger 类。

2. 计算属性:处理复杂逻辑的“神器”

当类名的切换依赖多个状态时,计算属性(computed)是最佳选择。比如一个提交按钮,要根据“是否加载中”和“是否有错误”来切换样式:


<template>
  <button :class="buttonClass" @click="handleSubmit" :disabled="isLoading">
    {{ buttonText }}
  </button>
</template>

<script setup>
  import {ref, computed} from 'vue'

  // 状态变量
  const isLoading = ref(false)  // 加载中状态
  const hasError = ref(false)   // 错误状态

  // 计算按钮类:根据状态动态生成
  const buttonClass = computed(() => ({
    // 加载中时添加 loading 类
    loading: isLoading.value,
    // 有错误时添加 error 类
    error: hasError.value,
    // 正常状态添加 active 类
    active: !isLoading.value && !hasError.value
  }))

  // 计算按钮文字
  const buttonText = computed(() => {
    if (isLoading.value) return '加载中...'
    if (hasError.value) return '提交失败'
    return '提交表单'
  })

  // 模拟提交请求
  const handleSubmit = async () => {
    isLoading.value = true
    hasError.value = false

    // 模拟异步请求(比如调用接口)
    await new Promise(resolve => setTimeout(resolve, 1500))

    // 模拟随机结果(50% 成功,50% 失败)
    hasError.value = Math.random() > 0.5
    isLoading.value = false
  }
</script>

<style>
  button {
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .loading {
    background-color: #bbdefb;
    color: #2196f3;
  }

  .error {
    background-color: #ffcdd2;
    color: #e53935;
  }

  .active {
    background-color: #42b983;
    color: white;
  }
</style>

这个例子中:

  • buttonClass 是一个计算属性,依赖 isLoadinghasError
  • isLoading 变为 trueloading 类自动添加;
  • hasError 变为 trueerror 类自动添加;
  • 所有状态变化都由计算属性“自动处理”,模板无需关心逻辑——这就是计算属性的魅力!
往期文章归档
免费好用的热门在线工具

四、响应式的“魔法”:数据变,类名自动变

为什么数据变化时类名会自动更新?因为 Vue 的响应式系统在背后工作:

  1. 跟踪依赖:当你用 refreactive 定义变量时,Vue 会跟踪它的依赖(比如 isActive:class 用到);
  2. 触发更新:当变量变化时,Vue 会重新计算依赖它的表达式(比如 { active: isActive });
  3. 更新 DOM:最后自动更新 DOM 上的类名——全程不需要你手动操作!

比如下面的例子,输入框内容长度超过5时,添加 valid 类,否则添加 invalid 类:


<template>
  <input
      type="text"
      v-model="inputValue"
      :class="{ valid: inputValue.length > 5, invalid: inputValue.length <= 5 }"
      placeholder="输入至少6个字符"
  >
</template>

<script setup>
  import {ref} from 'vue'

  const inputValue = ref('') // 输入框内容(响应式)
</script>

<style>
  input {
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .valid {
    border-color: #42b983;
  }

  .invalid {
    border-color: #e53935;
  }
</style>

当输入内容长度超过5时,valid 类自动添加;否则添加 invalid 类——完全由 inputValue 的变化驱动!

五、实际案例:Tabs 组件的高亮切换

我们用对象语法实现一个常见的 Tabs 组件,点击 tab 时高亮当前标签:


<template>
  <div class="tabs">
    <button
        v-for="(tab, index) in tabs"
        :key="index"
        :class="{ active: currentTab === index }"
        @click="currentTab = index"
    >
      {{ tab.title }}
    </button>
    <div class="tab-content">
      {{ tabs[currentTab].content }}
    </div>
  </div>
</template>

<script setup>
  import {ref} from 'vue'

  // Tabs 数据
  const tabs = ref([
    {title: '首页', content: '首页内容...'},
    {title: '文章', content: '文章内容...'},
    {title: '关于', content: '关于我们...'}
  ])
  // 当前激活的 tab 索引
  const currentTab = ref(0)
</script>

<style>
  .tabs {
    border-bottom: 1px solid #eee;
    margin-bottom: 16px;
  }

  .tabs button {
    padding: 8px 16px;
    border: none;
    background: none;
    cursor: pointer;
  }

  .tabs button.active {
    border-bottom: 2px solid #42b983;
    color: #42b983;
  }

  .tab-content {
    padding: 16px;
  }
</style>

这段代码的核心逻辑:

  • v-for 循环渲染 tabs 按钮;
  • :class="{ active: currentTab === index }":当当前 tab 索引等于按钮索引时,添加 active 类;
  • 点击按钮时,更新 currentTab 的值——高亮效果自动切换!

六、常见报错与解决

报错1:类名不生效,控制台无错误

原因:响应式变量没有正确定义(比如用 let 而不是 ref/reactive),导致数据变化时无法触发重新渲染。
解决:用 refreactive 定义状态变量,比如 const isActive = ref(false) 而不是 let isActive = false

报错2:“Uncaught SyntaxError: Unexpected token '-'”

原因:带连字符的类名没有用引号包裹,比如 { text-danger: hasError },JavaScript 会解析成 text - danger(变量 text 减变量 danger)。
解决:把类名用引号包裹:{ 'text-danger': hasError }

报错3:计算属性返回的类对象不更新

原因:计算属性没有正确依赖响应式变量,比如在计算属性中使用了非响应式的数据。
解决:确保计算属性内部用到的所有变量都是响应式的(用 ref/reactive 定义),比如 computed(() => ({ active: isActive.value })) 中的 isActiveref

参考链接

vuejs.org/guide/essen…

Day01-APIs

1.变量声明

1.优先选择const

  1. 建议数组和对象都用const来声明
image.png

1.1 引用数据类型修改仍可用const

只要地址不修改,它也不会报错

<script>
        1.数组即使追加也可以定义成const
        因为数组地址没变
        const arr = ['莎莎','vv']
        arr.push('鑫鑫')
        console.log(arr);
        下面这样会报错,因为这样子是开辟了一个新地址,并且赋给了arr
        arr = [1,2,3]
        console.log(arr);       
    </script>

2.API作用与分类

image.png

3.什么是DOM

Document Object Model----文档对象模型

作用:通过js操作网页内容,实现用户放纵

image.png

4.DOM树

document是DOM提供的一个对象,网页中所有内容都在document里面 image.png

5.DOM对象(重要)

html的标签 js获取后就变成了对象

核心:把内容当对象处理

6.获取DOM对象

1.根据CSS选择器来获取DOM元素(重点)

2.其他获取DOM元素的方法(了解)

6.1 利用css选择器来获取

image.pngimage.pngimage.png
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
        }
        #nav {
            background-color: pink;
        }
    </style>
</head>
<body>
    <div class="box">123</div>
    <div class="box">456</div>
    <p id="nav">导航栏</p>
    <ul>
        <li>啦啦啦1</li>
        <li>啦啦啦2</li>
        <li>啦啦啦3</li>
    </ul>
    <script>
    // 1. 获取匹配的第一个元素
    const box1 = document.querySelector('div')
    console.log(box1)
    const box2 = document.querySelector('box')
    console.log(box2)
    // id选择器一定要加#号
    const nav = document.querySelector('#nav')
    nav.style.background = 'green'
    console.log(nav);
    // 获取第一个li
    const li = document.querySelector('ul li:first-child')
    console.log(li);

    //2.选择所有的小li
    const lis = document.querySelectorAll('ul li')
    console.log(lis);
    </script>
</body>
</html>  
  • 遍历得到的伪数组
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
        }
        #nav {
            background-color: pink;
        }
    </style>
</head>
<body>
    <div class="box">123</div>
    <div class="box">456</div>
    <p id="nav">导航栏</p>
    <ul class="nav">
        <li>啦啦啦1</li>
        <li>啦啦啦2</li>
        <li>啦啦啦3</li>
    </ul>

    <script>
        // 遍历这个伪数组
        const lis = document.querySelectorAll('.nav li')
        for(let i = 0; i < lis.length; i++){
            console.log(lis[i])
        }
    </script>
</body>
</html>

6.2 其他方法

image.png

7.操作元素的内容

image.png

7.1 对象.innerText属性

image.png
    <script>
        // 1.获取元素
        const box = document.querySelector('.box')
        // 2.修改文字内容
        console.log(box.innerText)
        box.innerText = '我是莎莎'
        console.log(box.innerText);
    </script>

7.2 对象.innerHTML属性

image.png

7.3 年会抽奖案例

<script>
    // 1.声明数组
    const arr = ['莎莎','vv','鑫鑫','大伟','坤哥']
    // 2.随机生成一个数字
    for(let i = 0; i < 3; i++){
        let random = Math.floor(Math.random()*arr.length)
        // 获取这个元素并修改
        if(i === 0){
            const span = document.querySelector('.wrapper #one')
            span.innerText = arr[random]
        }else if(i=== 1){
            const span = document.querySelector('.wrapper #two')
            span.innerText = arr[random]
        }else{
            const span = document.querySelector('.wrapper #three')
            span.innerText = arr[random]
        }
        arr.splice(random,1)
        
    }
  </script>

8.操作元素属性

8.1 操作元素常用属性href、src、title

image.png

8.1.1 随机刷新图片案列

<body>
    <img src="./images/1.webp" alt="">
    <script>
        function getRandom(min, max) {
            // 先处理边界:如果min > max,交换两者
            if (min > max) [min, max] = [max, min];
            // 核心公式:Math.floor(Math.random() * (max - min + 1)) + min
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }
        // 1.获取图片对象
        const img = document.querySelector('img')  
        // 2.修改图片的src属性
        const random = getRandom(1,6)

        img.src = `./images/${random}.webp`
        img.title = '这就是你啊' 

    </script>
</body>

8.2 操作元素样式属性

8.2.1 通过style修改

image.png
  1. body的样式就不需要获取了,可以直接使用,因为body是唯一的

2.css中遇到bckground-image这种,用小驼峰解决,写成backgroundImage

<body>
    <div class="box"></div>
    <script>
        // 1.获取元素
        const box = document.querySelector('.box')
        // 2.修改样式属性,别忘了加单位
        box.style.width = '300px'
        // 遇到css总-的命名方式,用小驼峰命名法解决
        box.style.backgroundColor = 'blue'
        // 加边框
        box.style.border = '2px solid red'
        box.style.borderTop = '5px solid pink'
    </script>
</body>
<script>
        // 因为body是唯一的,所以不需要获取
        function getRandom(min, max) {
            // 先处理边界:如果min > max,交换两者
            if (min > max) [min, max] = [max, min];
            // 核心公式:Math.floor(Math.random() * (max - min + 1)) + min
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }
        const random = getRandom(1,10)
        document.body.style.backgroundImage = `url(./images/desktop_${random}.jpg)`
        
    </script>

8.2.2 通过className来修改

好处:简洁 image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        div {
            height: 200px;
            width: 200px;
            background-color: pink;
        }
        .nav {
            color: red;
        }

        .box {
            width: 300px;
            height: 300px;
            background-color: skyblue;
            margin: 20px auto;
            padding: 10px;
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <div class="nav">可爱莎莎</div>
    <script>
        // 1.获取元素
        const div = document.querySelector('div')
        // 2.添加类名,并且会覆盖前面的类型
        div.className = 'box'
        // 3.如果想保留之前的类名,可以使用下面的方法
        div.className = 'nav box'
    </script>
</body>
</html>

8.2.3 通过classList操作类控制css

这个是用的最多的 image.png

8.2.4 随机切换轮播图

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>轮播图点击切换</title>
  <style>
    * {
      box-sizing: border-box;
    }

    .slider {
      width: 560px;
      height: 400px;
      overflow: hidden;
    }

    .slider-wrapper {
      width: 100%;
      height: 320px;
    }

    .slider-wrapper img {
      width: 100%;
      height: 100%;
      display: block;
    }

    .slider-footer {
      height: 80px;
      background-color: rgb(100, 67, 68);
      padding: 12px 12px 0 12px;
      position: relative;
    }

    .slider-footer .toggle {
      position: absolute;
      right: 0;
      top: 12px;
      display: flex;
    }

    .slider-footer .toggle button {
      margin-right: 12px;
      width: 28px;
      height: 28px;
      appearance: none;
      border: none;
      background: rgba(255, 255, 255, 0.1);
      color: #fff;
      border-radius: 4px;
      cursor: pointer;
    }

    .slider-footer .toggle button:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    .slider-footer p {
      margin: 0;
      color: #fff;
      font-size: 18px;
      margin-bottom: 10px;
    }

    .slider-indicator {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      align-items: center;
    }

    .slider-indicator li {
      width: 8px;
      height: 8px;
      margin: 4px;
      border-radius: 50%;
      background: #fff;
      opacity: 0.4;
      cursor: pointer;
    }

    .slider-indicator li.active {
      width: 12px;
      height: 12px;
      opacity: 1;
    }
  </style>
</head>

<body>
  <div class="slider">
    <div class="slider-wrapper">
      <img src="./images/slider01.jpg" alt="" />
    </div>
    <div class="slider-footer">
      <p>对人类来说会不会太超前了?</p>
      <ul class="slider-indicator">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
      </ul>
      <div class="toggle">
        <button class="prev">&lt;</button>
        <button class="next">&gt;</button>
      </div>
    </div>
  </div>
  <script>
    // 1. 初始数据,这是一个数组对象
    const sliderData = [
      { url: './images/slider01.jpg', title: '对人类来说会不会太超前了?', color: 'rgb(100, 67, 68)' },
      { url: './images/slider02.jpg', title: '开启剑与雪的黑暗传说!', color: 'rgb(43, 35, 26)' },
      { url: './images/slider03.jpg', title: '真正的jo厨出现了!', color: 'rgb(36, 31, 33)' },
      { url: './images/slider04.jpg', title: '李玉刚:让世界通过B站看到东方大国文化', color: 'rgb(139, 98, 66)' },
      { url: './images/slider05.jpg', title: '快来分享你的寒假日常吧~', color: 'rgb(67, 90, 92)' },
      { url: './images/slider06.jpg', title: '哔哩哔哩小年YEAH', color: 'rgb(166, 131, 143)' },
      { url: './images/slider07.jpg', title: '一站式解决你的电脑配置问题!!!', color: 'rgb(53, 29, 25)' },
      { url: './images/slider08.jpg', title: '谁不想和小猫咪贴贴呢!', color: 'rgb(99, 72, 114)' },
    ]
    // 2.需要一个随机数
    const random = Math.floor(Math.random() * sliderData.length)
    // 3.获取图片
    const img = document.querySelector('.slider-wrapper img')
    // 4.修改图片路径
    img.src = sliderData[random].url
    // 5.获取文字
    const text = document.querySelector('.slider-footer p')
    // 6.修改文字内容
    text.innerHTML = sliderData[random].title
    // 7.修改底部颜色,括号里面要写css选择器
    const footer = document.querySelector('.slider-footer')
    footer.style.backgroundColor = sliderData[random].color

    // 8.修改底部小圆点高亮特效
    const li = document.querySelector(`.slider-indicator li:nth-child(${random+1})`)
    li.classList.add('active')
  </script>
</body>

</html> 

8.3 操作表单元素属性

image.png

image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" value="电脑">
    <input type="checkbox" name="" id=""> 
    <script>
    
    // 1.获取元素
    const uname = document.querySelector('input')

    // 2.获取表单里面的值 用的是表单对象.value
    console.log(uname.value)
    
    // 3.修改值
    uname.value = '我是可爱莎莎'
    uname.type = 'password'
    
    // 给复选框直接加上对号
    // 1.获取对象
    const input = document.querySelector(input:nth(2)-child)
    input.checked = true
    </script>  
</body>
</html>    

8.4 自定义属性

image.png

<body>
    <div data-id="1">1</div>
    <div data-id="2">2</div>
    <div data-id="3">3</div>
    <div data-id="4">4</div>
    <div data-id="5">5</div>
    
    <script>
        const one = document.querySelector('div')
        console.log(one.dataset); // 可以输出自定义data属性的集合对象
        console.log(one.dataset.id); // 输出1
    </script>
    
</body>

9.定时器-间歇函数

image.png

9.1 开启和关闭定时器

语法:setInterval(函数,间隔时间)和 clearInterval(定时器序号) // 单位是毫秒

也可以直接用匿名函数

返回的这个id代表了开启的定时器的个数

image.png

    <script>
        //setInterval(函数,间隔时间ms)
        // 1.使用匿名函数来每隔1s输出一个句子
        let m = setInterval(function(){
            console.log('1s执行1次')
            
        },1000)
        console.log(m);
        

        // 2.使用外部函数
        function fn(){
            console.log('可爱莎莎') 
        }
        let n = setInterval(fn,1000)
        console.log(n);
        

        // 关闭定时器
        clearInterval(m)
    </script>

9.1 用户倒计时

❌