普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月21日技术

AI生成功能设计用例|得物技术

作者 得物技术
2025年5月21日 13:31

一、AI背景

人工智能生成内容(AIGC,AI-Generated Content)技术的快速发展正在改变内容生产的方式,并逐渐渗透到各个行业,例如:在自媒体平台自动编写文案并发布,快速分析数据,写小说,画漫画等。强大的文本生成能力已经实现了生产力超过生产资料,提供了更加高效的生产力,将AI引入到工作中成为发展的方向。

目前公司编写测试用例为人工编写,存在手工编写用例的普遍痛点,例如:重新编写,费时费力,边界遗漏,兼容遗漏等。AI拥有自动生成文本并快速整合的能力,以AI辅助功能用例编写成为推动行业创新和效率提升的关键点。

AI编写用例的优点:

效率提升

AI可以快速生成大量测试用例,显著减少人工编写所需的时间,提升整体测试效率。

测试覆盖提升

AI能够自动识别潜在的测试场景和边界条件,从而提高测试覆盖率**,确保更全面的检测。

※  一致性和准确性提升

AI生成测试用例具有较高的一致性和易理解性,减少人为错误,增强测试的可靠性和准确性。

AI热词:

图片

二、设计方案

本部分介绍使用AI编写测试用例的的设计方案,包括使用流程和架构图。

AI编写用例流程图

图片

AI编写用例架构图

图片

三、设计核心介绍

本部分介绍如何使用AI辅助生成功能用例,详细讲解了从PRD文档->测试点->测试用例->Xmind用例->使用采纳,整条链路的核心设计与实现。

PRD文件解析器

平台支持飞书PRD文档中文本、多维表格、电子表格内容的解析,暂不支持对图片、流程图解析。文档读取分为6个步骤,分别为:获取飞书token、获取用户token、获取文件block列表、Table表格解析、电子表格解析、解析结果组装。以下主要介绍解析部分内容:

结构组成设计:

图片

实现方案详情

※  飞书文档读取

图片图片

※  Table的提取与sheet表格的提取

  • Table提取:提取表格过程中需要将表格相关的块与子块关联绑定,递归解析所有的数据。并根据第一行各字段的长度<20做是否为表头判定,默认第一行为表头信息。
  • sheet提取:在飞书表格提取过程中需要使用多个递归,分别获取表格所有内容与元素

图片

※  AI解析PRD文档:

  • PRD解析:通过与AI交互将文本内容解析为:需求关键字、测试背景、测试需求详情三部分,并按照特定字段将数据存储。
  • 结构设计:

PRD解析结构设计

图片

核心代码逻辑:

图片

※  获取关联测试需求业务背景:

  • 根据PRD解析关键字信息匹配最相关的测试用例模块,使用向量和关键字双权重对RAG**模块做测试用例提取:
  1. keyword_weight:0.3
  2. vector_weight:0.7
  3. 同时设置AI模型准确度为0.85
  • 匹配过程中分别针对不同的关键字,从RAG数据中提取热度最高的3个测试模块,合并后提取所有模块中热度最高的三个模块作为业务历史背景。
  • RAG提取架构设计

图片

  • 核心代码逻辑

图片

模型设计

图片

测试点生成器

测试点生成器为AI生成用例的核心,实现PRD到测试点的转换。生成过程中结合需求背景、关键字、需求详情、业务背景、测试分析等信息作为业务背景,以更准确的生成测试用例。核心结构如下:

结构组成设计

图片

实现方案详情

图片图片

模型设计

图片

测试用例生成器

测试用例生成器为AI用例生成器,负责将AI测试点转换为Xmind测试用例,主要实现两个功能,第一步将AI测试点转换为markdown结构的测试用例,包括用例名称、前置条件、执行步骤、期望结果等。第二部负责将第一步测试用例转换为Xmind结构。

实现方案详情

※  测试点解析生成markdown格式用例:

生成markdown格式用例

图片

解析结果

图片

※  AI markdown格式转换为Xmind结构用例

转换Xmind结构

图片

生成结果

图片

模型设计

图片

知识库搭建

LLM大模型有通用的推荐能力,针对公司业务场景是无法准确识别相关功能的,针对“最后一公里”问题,平台使用搭建测试用例知识库的方式,以提升推荐准确度。

平台会以历史测试用例与业务需求文档作为历史业务背景。在推荐功能用例过程中自动匹配历史业务背景,以提升推荐准确度。

知识库搭建

※  知识库涉及范围

图片

※  实现方案详情

  • Xmind测试用例转换知识库

图片

  • 业务文档转换知识库

图片

※  模型设计:

  •   测试用例转换文本AI模型

图片

  •   业务文档转换业务文档模型

图片

四、实现结果展示

图片图片图片图片

五、总结 & 规划

目前平台侧已经实现自动生成功能用例的功能,实现了从 PRD自动解析->测试点生成-> Xmind用例生成->同步平台的完整流程。可以一定程度上提升用户编写用例效率。

后续规划

  1. 支持PRD文档图片/流程图等多模态数据解析
  2. 持续完善RAG模型与测试用例知识库的维护

往期回顾

1.从零实现模块级代码影响面分析方案|得物技术

2.以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术

3.得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

4.得物自研DGraph4.0推荐核心引擎升级之路

5.大语言模型的训练后量化算法综述 | 得物技术

文 / 执一

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

100行代码速通Agent开发

2025年5月21日 13:02

大家好,我卡颂,专注于AI助力程序员转型阅读我的更多思考

自从23年初GPT爆火后,每年都会涌现一些爆火的AI概念。

今年最火的概念无疑是Agent

伴随而来的,是各家都在争夺Agent的解释权

这些繁杂的信息无疑加深了开发者对Agent的理解成本。

实际上,Agent的概念非常简单。本文会用一个100行代码的Agent框架讲解Agent的开发原理

什么是 PocketFlow?

PocketFlow是一款仅有100行代码的Agent框架,作者是Zachary Huang

他的概念非常简单 —— 所有AI应用的核心流程,本质来说是不同复杂度的(graph,一种数据结构),由最基本的数据结构Node组合而成。

AI总结邮件内容这个场景举例:

只让AI总结邮件内容,那么核心流程就是一个Node

如果AI总结邮件后再生成回复草稿,就是两个Node组成的Flow

如果要重复总结多份邮件,就是多个Node重复执行,这些Node组成Batch

如果要同时总结多份邮件,就是多个Node同时执行,这些Node组成Parallel

再考虑一些流程设计,比如:

  • 循环:Node A 与 B 的输出分别作为对方的输入

  • 分支:Node根据条件与不同Node连接

当有了这些基础的结构与流程后,就能组合更复杂的图。

比如循环分支可以组合成Agent

下图是一个总结邮件内容并生成回复Agent,他会分析邮件内容、确定是否需要人工审核、草拟回复并不断迭代优化。

一些常用的AI工具也能组合生成,比如下图是RAG的结构:

有了RAG,再结合循环就能组合成AI聊天应用中的聊天消息记忆模块

到这里你应该能理解为什么PocketFlow代码只有100行 —— 他只实现了构成AI应用所需的基本数据结构。

要开发任何AI应用,只需要:

  1. 了解该应用可以设计为什么结构的图(graph)

  2. PocketFlow描述图结构

  3. 实现图结构、辅助方法、图的状态管理

听起来是不是有种库太简单,以至于需要开发者自己写很多代码的感觉?

在AI时代,这恰恰不是问题。

Agentic Coding

由于PocketFlow代码量极少(100行),这意味着你能将他的源代码、设计理念、使用方法、案例合并为一条提示词给到大模型。

通过这条提示词,现代大模型(比如Claude-sonnet-3.7Gemini2...)可以轻松掌握PocketFlow使用方法的最佳实践。

作者还为上述最佳实践设计了一套称为Agentic CodingAI辅助开发模式

这套开发模式是人类设计AI实现的协作模式 —— 人类负责理解需求、定义高层架构和评估结果,AI负责实现细节和优化。

假设我要基于PocketFlow开发一个mini CursorAgentic Coding的流程如下:

  1. 我去调研mini Cursor需要实现哪些功能

  2. 与AI协作确定这些功能对应什么图结构

对于Cursor应该是Agent结构。

  1. 与AI协作确定工具的类型定义

mini Cursor需要如下工具:

  • codebase_search:语义搜索工具

  • read_file:读取文件内容(或大纲)

  • run_terminal_cmd:代表用户在终端执行命令

  • list_dir:快速列出目录内容

  • 等等

  1. AI使用PocketFlow实现我在步骤2确定的图结构

  2. AI实现我在步骤3确定的工具

  3. 评估、优化效果

就我个人体验,基于Agentic Coding,开发mini Cursor我只用了1天时间,结构如下:

核心代码就是定义不同Node,比如核心Agent各种工具Node

再将各个Node连接形成图。

总结

所有AI应用的核心流程,本质来说是不同复杂度的(graph,一种数据结构),由最基本的数据结构Node组合而成。

PocketFlow提供了最基本的数据结构以及一套人类设计 + AI实现的协作开发模式Agentic Coding(本质是一段可以作为cursor rule的提示词)

你可以在cursor(或其他AI IDE)帮助下,快速开发各种AI应用。

借助AI,我3天开发了一款可视化工作流编辑器

作者 徐小夕
2025年5月21日 12:01

AI着实让人发狂!最近借助AI,我花了不到3天的时间,写了一款原生js实现的工作流编辑器,先上图:图片注意哦,这里是用原生js实现的,也就意味着我们可以轻松将它封装成Vue和React组件,在项目里使用。先总结一下实现这款可视化编辑器,AI的占比:

  • UI界面,80% AI生成
  • 功能逻辑实现,60% AI生成
  • bug和问题修复,60% AI实现
  • 架构和插件设计方案,50% AI实现

也就是说,但凡是懂点技术的人员,都可以借助AI,实现这个可视化流程编辑器需求的50% - 60%。这款可视化流程图,如果不借助AI实现,不借助VUE,REACT等框架,如果纯js生态来写,对于初中级研发来说简直就是灾难。功能详解图片接下来和大家介绍一下这款借助AI实现的工作流编辑器的功能模块。

2.1 节点管理

  • 多种节点类型支持开始、输入、代码、条件、循环等多种节点类型
  • 节点分类将节点按功能分为基础节点、处理节点、流程控制和集成节点
  • 节点搜索支持通过关键词搜索节点
  • 可视化图标每种节点类型配有独特的图标和颜色

2.2 画布操作

  • 拖拽创建从左侧面板拖拽节点到画布创建新节点
  • 节点移动画布中的节点可自由拖动位置
  • 连线创建通过端点连接不同节点
  • 连线样式支持多种连线样式(贝塞尔曲线、直线、流程图、状态机)
  • 缩放控制支持画布的放大、缩小和重置
  • 网格背景可配置的网格背景,支持调整网格大小和颜色
  • 缩略图右下角实时显示画布缩略图,支持点击导航

2.3 属性配置

  • 节点属性配置节点名称、描述、代码等基本属性
  • 节点样式支持多种预设样式(默认、现代、扁平、圆角)
  • 高级设置超时设置、重试机制、优先级、标签等
  • 连接线属性配置连接线标签、颜色、线宽和样式

2.4 导出功能

  • JSON 导出将工作流导出为 JSON 文件
  • 元数据管理配置工作流名称、描述、版本和权限

更让我惊讶的是,AI竟然连节点和边的属性面板都自动帮我实现了,我们还能轻松设置边的样式,比如:

  • 贝塞尔曲线
  • 直线
  • 流程图
  • 状态机

具体demo如下:图片

同时还帮我实现了缩略图的功能:

图片

虽然实现的有点粗糙,但是作为有技术在身的程序员,优化它简直是分分钟的事情。技术实现先和大家分享一下我设计的这款流程图编辑器的技术架构:

图片

技术栈我采用了如下方案:

前端框架:纯 HTML5 + CSS3 + JavaScript (ES6+)
DOM 操作:原生 JavaScript
连线库:jsPlumb (v2.15.6)
样式管理:CSS3 自定义样式
图标:内联 SVG

数据结构分为节点和边的数据结构,下面是节点的数据结构:

image.png

接下来的是边的数据结构:

image.png

最后一个是整个流程图的数据结构:

image.png

数据结构部分也都是AI来实现的,我们只需要来纠错即可。整个原生流程图编辑器采用的是js插件的方式实现,所以我们可以低成本集成到vue和react项目中。AI编程的一些总结其实用AI实现这款编辑器整个过程还是挺顺利的,从效率上对研发人员简直是“大杀器”。接下来我总结几个客观的缺点和事实:

  • 对于业务复杂度高的场景,比如复杂界面的多端适配,AI实现的还不是很友好,需要人为来限定详细的规则,AI才能理解并实现
  • 不同的AI编程工具对语言的熟悉度有差异,所以在使用AI编程工具时,一定要结合自身的技术需求,来选择合适的编程工具和模型
  • AI来生成大型工程时,缺乏“长记忆性”,所以不能把每一个功能细节都帮你实现,80%的AI编程功能更多的还是在UI和交互上来解决效率问题
  • 你的提示词对AI生成的结果至关重要,但是不能给AI太详细的提示词和限制,不然AI生成的效果反而比只用少量提示词生成的效果好
  • 现阶段,AI解决复杂业务问题的前提还是你得有比较扎实和深入的技术能力,否则AI只能帮你实现基础的场景

如果你对这款流程图实现感兴趣,欢迎在趣谈AI公众号留言反馈。

Bash中无法识别node 、npm

2025年5月21日 11:58

使用 nano 编辑器修改 ~/.bashrc 文件

  1. 打开终端
    打开你的 Bash 终端。

  2. 启动 nano 编辑器
    在终端中输入以下命令并按回车:

    bash

    复制

    nano ~/.bashrc
    

    这会打开 ~/.bashrc 文件,如果文件不存在,nano 会创建一个新文件。

  3. 编辑文件
    使用键盘的光标键(上下左右箭头键)移动光标到文件的末尾。然后,输入以下内容(根据你的 Node.js 安装路径修改):

    bash

    复制

    export PATH=$PATH:/c/Program\ Files/nodejs
    

    注意:

    • /c/Program\ Files/nodejs 是 Node.js 的安装路径,确保路径正确。
    • 如果路径中包含空格,需要用反斜杠 `` 转义。
  4. 保存并退出
    完成编辑后,按以下步骤保存并退出:

    • 按下 Ctrl + O 键,保存文件。
    • 按下 Enter 键,确认保存。
    • 按下 Ctrl + X 键,退出 nano 编辑器。

验证修改是否生效

  1. 重新加载配置文件
    在终端中运行以下命令,使修改生效:

    bash

    复制

    source ~/.bashrc
    
  2. 测试 Node.js 是否可用
    在终端中运行以下命令,测试 Node.js 是否可以正常运行:

    bash

    复制

    node -v
    

    如果能够显示 Node.js 的版本号,说明修改成功。

Next.js:一招解决 Hydration 水合时序陷阱

作者 lihainuo
2025年5月21日 11:52

问题描述:

小nuo 在 Next.js 应用中精心设计了路由守卫中间件,当未登录用户访问 /dashboard 时,中间件将其重定向到首页 /?auth=required
首页的客户端组件 AuthCheck 本应根据URL参数弹出登录提示,但首次加载时弹窗神秘消失!更诡异的是,修改代码热更新后弹窗又能正常显示...

 问题复现

核心代码逻辑:
middleware.ts (中间件):

// ...
if (!token) {
  const url = new URL("/", req.url);
  url.searchParams.set("auth", "required");
  return NextResponse.redirect(url); // 重定向到首页带参数
}
// ...

AuthCheck(弹窗提示组件):

'use client'

// ...
const AuthCheck = () => {
  const searchParams = useSearchParams();
  
  useEffect(() => {
    if (searchParams.get("auth") === "required") {
      toast.error("需要登录"); // 预期会弹出提示
    }
  }, [searchParams]);
  
  return null
}

首页:

import { Header } from "@/components/layout/front/Header";
import { AuthCheck } from "@/components/ui/Auth/AuthCheck";

const Home = async () => {
  return (
    <section>
      <AuthCheck />
      <Header />
      <main className="flex justify-center items-center h-[100vh]">
        Hello Next!
      </main>
    </section>
  );
}

export default Home

问题根源:客户端水合(Hydration)的问题

根据小nuo的详细排查,总结如下:
Next.js 采用混合渲染模式(服务端生成静态HTML + 客户端动态注入),这过程中潜藏着一个致命的时间差:

  1. 服务端渲染阶段

    • 生成包含?auth=required参数的静态HTML
    • 但此时的toast组件只是静态DOM,没有"生命力"
  2. 客户端水合阶段

    • React尝试将事件处理器"绑定"到静态HTML
    • useEffect中的弹窗代码抢先执行,此时toast组件尚未准备就绪
  3. 热更新时的差异

    • 页面已经完成水合,组件处于"激活"状态
    • 再次触发useEffect时,toast组件已完全就绪

解放方案:setTimeout

将弹窗提示组件代码修改如下,问题即可解决:

// ...
useEffect(() => {
  const timer = setTimeout(() => { // 延迟执行
    if (searchParams.get("auth") === "required") {
      toast.error("需要登录"); 
    }
  }, 0); // 0ms延迟即可破解

  return () => clearTimeout(timer); // 清理副作用
}, [searchParams]);
// ...

原理解密

  • setTimeout(fn, 0)将代码推入下一个事件循环
  • 给客户端组件争取到50-100ms的水合时间窗口
  • 此时所有组件已完成初始化,弹窗API处于就绪状态

最佳实践指南

  1. 客户端交互统一延迟执行
// 封装自定义hook
const useSafeEffect = (fn: () => void, deps: any[]) => {
  useEffect(() => {
    const timer = setTimeout(fn, 0);
    return () => clearTimeout(timer);
  }, deps);
}
  1. 组件设计原则
  • 将数据获取逻辑放在服务端组件
  • 交互类组件强制添加"use client"指令
  • 使用动态导入延迟加载非关键UI
const DynamicToast = dynamic(() => import('@/components/Toast'), {
  ssr: false, // 禁用服务端渲染
  loading: () => <Skeleton /> 
});

总结:Next.js 的混合渲染虽强大,但也带来了独特的时序挑战。掌握水合机制的核心原理,善用事件循环的延迟技巧,就能轻松驯服这些"薛定谔的弹窗"。现在,让你的交互组件在任何场景下都稳定可靠吧!

qiankun 和 wujie 框架的区别和优缺点 - 汇总总结

作者 千度麒麟
2025年5月21日 11:47

wujieqiankun 都是用于构建微前端(Micro Frontend)架构的 JavaScript 框架,它们都致力于实现多个独立前端子应用的整合与协同工作。不过,它们在设计理念、实现方式以及使用体验等方面有较大区别。

以下是两者的对比,包括主要区别各自优缺点

一、基本概念对比

特性 wujie qiankun
核心理念 iframe + sandbox 的轻量组合 基于 single-spa,注重生命周期管理
应用加载方式 类似 iframe 的嵌入式(隔离性强) Script 注入 + 沙箱实现全局变量隔离
沙箱机制 利用 iframe 的天然隔离 + proxy + snapshot JS 沙箱(proxy + patch)
架构复杂度 较低,接入简单 架构较重,接入和调试略复杂

二、wujie 框架详解

✅ 优点:

  1. 隔离性强:通过 iframe 实现的隔离,使子应用与主应用之间几乎无污染。
  2. 支持任意技术栈:不仅限于 Vue/React 等主流框架,几乎任何前端项目都可以作为子应用接入。
  3. 实现简单:项目接入成本低,无需修改大量子应用逻辑。
  4. 预加载与懒加载:性能优化较好。
  5. 内置多种通信机制:支持 postMessage、全局事件总线、props 等多种通信方式。

❌ 缺点:

  1. 部分功能依赖 iframe,存在 SEO 屏蔽、样式继承困难等问题
  2. 由于隔离性太强,某些主子应用间共享资源(如样式变量、状态)比较麻烦
  3. 文档生态相对较少(比 qiankun 成熟度略低,但社区在成长)。

三、qiankun 框架详解

✅ 优点:

  1. 主流微前端方案,社区活跃度高,资料丰富
  2. 生命周期完整:可以控制子应用的 mount/unmount/init 等,适合大型项目管理。
  3. 沙箱机制成熟:通过 proxy 实现 JS 隔离,较好地解决了全局变量污染问题。
  4. 共享依赖管理:可以在主应用与子应用之间共享包依赖,减少重复加载。

❌ 缺点:

  1. 技术栈耦合偏高:更适用于 Vue/React 子应用,其他技术栈接入可能需要较多改造。
  2. 接入成本高:需要严格遵守 single-spa 生命周期和配置要求。
  3. 沙箱隔离不如 iframe 强:可能存在样式、全局变量泄露问题。
  4. 调试复杂度高:项目一旦大了,调试子应用变得麻烦。

四、适用场景对比

使用场景 推荐框架 理由
快速集成老系统 wujie 接入成本低,几乎不需要对子应用大改动
SEO 要求不高、追求隔离性强 wujie iframe 提供天然隔离,稳定可靠
需要精细控制子应用生命周期 qiankun 生命周期完整,适合大型项目
Vue/React 项目体系为主 qiankun 社区更成熟,框架兼容更好
多技术栈混合(如 Angular + React) wujie iframe 适配各种技术栈,避免冲突

五、总结

对比维度 wujie qiankun
性能 较好(预加载+懒加载) 依赖优化处理后表现良好
安全隔离 强(iframe + 沙箱) 一般(JS 沙箱)
接入复杂度 较低 较高
子应用兼容性 高(技术栈无关) 中(偏向 Vue/React)
生命周期管理 支持但较简单 完善
社区成熟度 一般

除了 qiankun 和 wujie 之外,还有什么其他的微前端框架

1. single-spa

介绍:

  • single-spa 是一个流行的微前端框架,支持多个框架(如 React、Vue、Angular)在同一个页面上共存。它的设计核心是让你能够独立加载和卸载前端应用,而不影响其他应用的运行。

特点:

  • 支持多种前端框架同时运行,允许子应用使用不同的框架(如 React 和 Vue 同时运行)。
  • 通过 生命周期管理 让子应用的加载、卸载、更新都更加灵活。
  • 对微前端有良好的控制,适用于复杂场景。

优点:

  • 灵活性强,可以在一个页面中同时运行多个框架的子应用。
  • 支持子应用生命周期的精细管理。
  • 社区活跃,使用广泛,文档资料丰富。

缺点:

  • 需要一些前提配置,集成起来相对较复杂。
  • 对子应用的开发要求较高,尤其是在框架间的资源共享和通信上。

2. Mosaic

介绍:

  • Mosaic 是一个支持多框架微前端架构的框架,目标是让开发者可以在一个单独的应用中使用多个微前端子应用。它的设计原则是组件化,通过基于 VueReact 的组件容器来实现。

特点:

  • 支持将多个独立的应用通过组件化的方式挂载到一个页面上。
  • 提供了一些方便的工具来管理子应用和主应用之间的通信。

优点:

  • 支持组件级的微前端,而不是应用级,适用于场景较轻量的微前端需求。
  • 易于集成。

缺点:

  • 相比 qiankunsingle-spa,文档和社区支持较少。

3. Module Federation (Webpack 5)

介绍:

  • Module Federation 是 Webpack 5 中新增的功能,旨在让多个 Webpack 构建的模块能够共享和动态加载。
  • 通过模块共享,前端应用可以跨应用共享依赖、组件或功能,不需要每个应用都重复打包相同的内容。

特点:

  • 通过 Module Federation,可以将各个应用的某些部分(如组件、模块、库)共享给其他应用。
  • 支持动态加载和共享应用代码,减少了冗余的依赖。

优点:

  • 解决了微前端中的“依赖共享”问题。
  • 可以按需加载应用,优化性能。
  • 在构建工具层面进行微前端集成,减少了框架间的耦合度。

缺点:

  • 需要 Webpack 5 及以上版本。
  • 配置和调试相对复杂,尤其是对于初学者来说。

4. Luigi Framework

介绍:

  • Luigi 是由 SAP 提供的一个微前端框架,适用于构建企业级的应用。它通过将不同的微前端模块组合到一个单一的用户界面中来创建大规模的应用。

特点:

  • 提供了一个基于 URL 的路由系统,用于管理微前端应用的导航。
  • 支持前后端分离的微前端架构。
  • 提供了一些 UI 组件和功能来帮助开发者实现微前端集成。

优点:

  • 适合企业级应用,尤其是需要多个团队开发的系统。
  • 提供的路由和导航解决方案非常强大。

缺点:

  • 相比于其他框架,社区较小,文档和支持不如其他主流框架。
  • 需要较为复杂的配置,适合复杂的应用场景。

5. FrintJS

介绍:

  • FrintJS 是一个功能强大的微前端框架,支持多个前端应用并且通过模块化的方式来拆分和管理应用。它专注于大规模的微前端架构,能够实现高度解耦的前端组件。

特点:

  • 提供了构建和管理微前端应用的全栈方案,支持应用和模块的动态加载。
  • 使用 FrintJS 构建的应用之间通过框架提供的 API 进行通信,保持良好的解耦性。

优点:

  • 提供了较强的模块化支持,适合开发复杂的微前端架构。
  • 支持多框架、多技术栈共存。

缺点:

  • 文档和社区支持相对较少,开发者需要对框架有较深入的了解。
  • 对入门门槛较高,可能不适合小型项目。

6. MFE (Micro Frontend) 的自定义实现

介绍:

  • 除了现成的框架外,一些团队也选择自行实现微前端架构,通常会基于 iframeWeb ComponentsModule Federation 等技术,定制符合自己需求的微前端解决方案。

特点:

  • 可以根据自己的需求选择最合适的技术栈。
  • 灵活性非常高,但需要较强的技术积累。

优点:

  • 高度定制化,灵活性强。
  • 不受现有框架限制,可以完全按照业务需求实现。

缺点:

  • 开发成本高,需要较多的技术积累。
  • 缺少统一的标准和解决方案,可能导致后期维护困难。

总结

  • qiankunwujie 作为目前较为流行的微前端框架,适用于大多数常见场景。
  • single-spa 更加灵活,适用于不同框架的共存,适合复杂的微前端需求。
  • Module Federation 是 Webpack 5 中的一个特性,可以用于构建微前端,解决了模块共享的问题。
  • 其他框架如 LuigiFrintJS 适合特定需求,如企业级系统或高度模块化的架构。

#JavaScript 中的 const、TDZ、调用栈与调用堆详解

作者 pp_
2025年5月21日 11:22

const 关键字

const 是 ES6 引入的声明常量的关键字,具有以下特性:

基本特性

  • 不可重新赋值:const 声明的变量不能被重新赋值
  • 必须初始化:声明时必须立即赋值
  • 块级作用域:只在声明所在的块级作用域内有效

简单数据类型 vs 复杂数据类型

const age = 18; // 简单数据类型
// age = 20; // 报错,不能重新赋值

const friends = [
  { name: "张三", hometown: '上饶' },
  { name: "李四", hometown: '赣州' }
]; // 复杂数据类型

// 可以修改对象内容
friends.push({ name: "王五", hometown: '九江' }); // 允许
// friends = []; // 报错,不能重新赋值

关键区别

  • 简单数据类型:值不可变(内存栈中的值不变)
  • 复杂数据类型:对象内容可变,但引用地址不可变

TDZ(Temporal Dead Zone,暂时性死区)

TDZ 是 ES6 引入的概念,与 let 和 const 相关:

console.log(a); // undefined (var 声明提升)
var a = 1;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;

TDZ 规则

  1. let/const 声明会提升到块顶部,但不会初始化
  2. 从块开始到声明语句执行前,变量处于 TDZ
  3. 访问 TDZ 中的变量会抛出 ReferenceError
  4. 声明语句执行后,变量离开 TDZ

设计目的

  • 避免 var 的变量提升带来的不可预测行为
  • 使代码更易理解和调试
  • 强制开发者遵循"先声明后使用"的良好实践

调用栈(Call Stack)与调用堆(Heap)

内存模型

调用栈 (Stack) 调用堆 (Heap)
存储内容 原始值、引用地址 对象、数组等复杂数据结构
分配方式 连续内存空间 动态分配的非连续内存
访问速度
大小限制 较小(通常几MB) 较大(可达几GB)
管理方式 自动管理(LIFO) 手动/垃圾回收管理

具体表现

值传递(简单类型)

let a = 10; // 值直接存储在栈中
let b = a;  // 创建新的栈空间,复制值
b = 20;
console.log(a); // 10 (不受影响)

引用式赋值(复杂类型)

const obj1 = { name: "Alice" }; // 对象在堆中,栈存储引用地址
const obj2 = obj1; // 复制引用地址,指向同一个堆对象

obj2.name = "Bob";
console.log(obj1.name); // "Bob" (共享同一对象)

const 与内存

对于 const 变量:

  • 简单类型:栈中的值不可变
  • 复杂类型:栈中的引用地址不可变,堆中的内容可变
const arr = [1, 2, 3];
arr.push(4); // 允许,修改堆内容
// arr = [5, 6]; // 报错,试图改变栈中的引用地址

块级作用域与循环

const 在循环中的表现:

// 每次迭代创建新的块级作用域和新的const变量
for (const i = 0; i < 3; i++) {
  // 报错,因为i++试图修改const变量
}

// 正确用法
for (const value of [1, 2, 3]) {
  console.log(value); // 每次迭代创建新的const value
}

// let 是更合适的循环计数器选择
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0, 1, 2 (每个i有独立作用域)
}

与 var 的区别

  1. 全局属性

    var a = 1;
    console.log(window.a); // 1
    
    const b = 2;
    console.log(window.b); // undefined
    
  2. 重复声明

    var x = 1;
    var x = 2; // 允许
    
    const y = 1;
    // const y = 2; // 报错
    
  3. 作用域

    • var:函数作用域或全局作用域
    • const:块级作用域

最佳实践

  1. 默认使用 const,只有需要重新赋值时才用 let
  2. 避免使用 var,除非有特殊需求
  3. 对于复杂对象,如果确实需要完全不可变,可以使用 Object.freeze()
    const obj = Object.freeze({ prop: 1 });
    // obj.prop = 2; // 静默失败或严格模式下报错
    

理解 const、TDZ、调用栈和调用堆的概念,有助于编写更可预测、更健壮的 JavaScript 代码,特别是在大型应用开发中。

react hooks

作者 娜妹子辣
2025年5月21日 11:21
useRef
  1. 操作dom元素:例如获取文本框焦点、滚动到某元素等。
import React, { useRef } from 'react';

function App() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus(); // 使用 ref 引用 DOM,从而调用 `focus` 方法
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Type something" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

export default App;

  1. 保存一个跨渲染周期的值而不触发重新渲染,比如计数器或一些计算的缓存值
import React, { useRef, useState } from 'react';

function Counter() {
  const count = useRef(0); // 创建一个持久化变量
  const [render, setRender] = useState(false); // 用于触发重新渲染(以演示效果)

  const increment = () => {
    count.current += 1; // 修改 ref 值
    console.log('Current Count:', count.current); // 打印更新后的值(不会重新渲染)
  };

  return (
    <div>
      <p>See console for count: {count.current}</p>
      <button onClick={increment}>Increment Count</button>
      <button onClick={() => setRender(!render)}>Force Re-render</button>
    </div>
  );
}

export default Counter;
import React, { useEffect, useRef, useState } from 'react';

function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCount = useRef();

  useEffect(() => {
    prevCount.current = count; // 保存当前值到 ref 中
  });

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {prevCount.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default PreviousValue;
  1. 当 React.forwardRef 和 useRef 一起使用时,可以将 ref 传递给子组件,可以访问子组件实例和函数
import React, { forwardRef, useRef } from 'react';

// 创建一个可接收 ref 的子组件
const FancyInput = forwardRef((props, ref) => (
  <input ref={ref} type="text" {...props} />
));

function App() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus(); // 调用子组件内的 input 的 focus 方法
  };

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Forward Ref Example" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

export default App;

HTML实现牛马新鞭子 · 实时收入计算器

2025年5月21日 11:07

“打工是不可能打工的,这辈子都不可能打工的……但看着钱一分一秒进账,打工的动力都多了三分”

🐴 产品定位

牛马新鞭子,专为广大打工人(俗称“牛马”)量身打造的精神鞭策神器。它不是普通的收入计算器,而是让你在工位上“看钱如流水”,一边摸鱼一边被工资数字狠狠抽醒的快乐小工具!

  • 你以为你在上班,其实你在“被抽”!
  • 你以为你在加班,其实你在“加鞭”!

🎉 主要功能

  • 实时计时:你上班的每一秒都被记录,摸鱼都能算工龄。
  • 实时收入:工资数字蹭蹭往上涨,老板看了都想加班。
  • 收入速率:每小时、每分钟、每秒钟赚多少,明明白白,绝不糊涂账。

🐂 适用人群

  • 所有打工人、社畜、搬砖侠、摸鱼达人。
  • 想知道自己一秒值多少钱的好奇宝宝。
  • 需要一点“精神鞭策”的上班族。
  • 想体验“被工资支配的恐惧”的朋友。

📝 结语

牛马新鞭子,让你上班不再迷茫,摸鱼不再心虚。
看着数字跳动,感受“鞭子”的力量,体验“钱在眼前,动力无限”!
“工资一秒一秒进账,打工人一秒一秒燃烧!”
“你以为你在上班,其实你在被工资抽打!”
快用牛马新鞭子,做最有“鞭策力”的打工人吧!

源码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实时收入计算器</title>
  <style>
    body {
      background: linear-gradient(120deg, #e0e7ef 0%, #f8fafc 100%);
      min-height: 100vh;
      margin: 0;
      font-family: 'Segoe UI', 'PingFang SC', Arial, sans-serif;
      color: #222;
    }
    .container {
      max-width: 440px;
      margin: 60px auto;
      background: #fff;
      border-radius: 18px;
      box-shadow: 0 4px 24px #b0c4de44;
      padding: 36px 32px 28px 32px;
      text-align: center;
    }
    h2 {
      color: #1976d2;
      margin-bottom: 22px;
      letter-spacing: 2px;
    }
    .input-row {
      display: flex;
      gap: 10px;
      margin-bottom: 18px;
      justify-content: center;
    }
    .input-row label {
      flex: 1;
      text-align: right;
      color: #555;
      font-size: 16px;
      margin-right: 6px;
    }
    .input-row input {
      flex: 2;
      padding: 8px 10px;
      border: 1px solid #ddd;
      border-radius: 6px;
      font-size: 17px;
      outline: none;
      transition: border 0.2s;
    }
    .input-row input:focus {
      border: 1.5px solid #1976d2;
    }
    button {
      padding: 10px 28px;
      background: #1976d2;
      color: #fff;
      border: none;
      border-radius: 6px;
      font-size: 18px;
      cursor: pointer;
      margin-top: 10px;
      margin-bottom: 18px;
      transition: background 0.2s;
    }
    button:active {
      background: #0d47a1;
    }
    .result-area {
      background: #f1f8fe;
      border-radius: 10px;
      box-shadow: 0 1px 6px #1976d211;
      padding: 22px 10px 18px 10px;
      margin-top: 18px;
      font-size: 18px;
      color: #1976d2;
      min-height: 120px;
    }
    .result-area strong {
      font-size: 22px;
      color: #d32f2f;
    }
    .tips {
      background: #fffde7;
      color: #b26a00;
      border-radius: 6px;
      padding: 10px 14px;
      margin-bottom: 18px;
      font-size: 15px;
      text-align: left;
      box-shadow: 0 1px 4px #ffe0b244;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>实时收入计算器</h2>
    <div class="tips">
      请输入你的每月工资、每月上班天数、每天工作小时数,点击“开始上班”即可实时看到你的收入增长!
    </div>
    <div class="input-row">
      <label for="salary">每月工资(元):</label>
      <input type="number" id="salary" min="0" placeholder="如 8000">
    </div>
    <div class="input-row">
      <label for="days">每月上班天数:</label>
      <input type="number" id="days" min="1" max="31" placeholder="如 22">
    </div>
    <div class="input-row">
      <label for="hours">每天工作小时:</label>
      <input type="number" id="hours" min="1" max="24" placeholder="如 8">
    </div>
    <button id="startBtn" onclick="startWork()">开始上班</button>
    <div class="result-area" id="resultArea">
      <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
        <span>已上班时间:</span>
        <strong id="workTime">00:00:00</strong>
      </div>
      <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
        <span>已赚到:</span>
        <span>
            <strong id="earned">0.0000</strong>
            <span style="margin-left: 2px; color: #888; font-size: 15px;"></span>
        </span>
      </div>
      <div style="border-top: 1px dashed #b0c4de; margin: 12px 0 10px 0;"></div>
      <div>
        <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px;"><span style="color:#1976d2;">每小时收入速率:</span><strong id="rateHour" style="margin-left:8px; color:#d32f2f; font-size:18px;">0.0000 元</strong></div>
        <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px;"><span style="color:#1976d2;">每分钟收入速率:</span><strong id="rateMin" style="margin-left:8px; color:#d32f2f; font-size:18px;">0.0000 元</strong></div>
        <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px;"><span style="color:#1976d2;">每秒钟收入速率:</span><strong id="rateSec" style="margin-left:8px; color:#d32f2f; font-size:18px;">0.0000 元</strong></div>
      </div>
    </div>
  </div>
  <script>
    let timer = null, startTimestamp = null;
    let salary = 0, days = 0, hours = 0;
    let rateHour = 0, rateMin = 0, rateSec = 0;
    let isWorking = false;
    function startWork() {
      if (!isWorking) {
        salary = parseFloat(document.getElementById('salary').value);
        days = parseInt(document.getElementById('days').value);
        hours = parseFloat(document.getElementById('hours').value);
        if (!salary || !days || !hours) {
          alert('请填写完整且有效的工资、天数和小时数!');
          return;
        }
        // 计算速率
        const totalSeconds = days * hours * 3600;
        rateHour = salary / (days * hours);
        rateMin = rateHour / 60;
        rateSec = rateHour / 3600;
        document.getElementById('rateHour').innerHTML = `<strong style="margin-left:8px; color:#d32f2f; font-size:18px;">${rateHour.toFixed(4)} 元</strong>`;
        document.getElementById('rateMin').innerHTML = `<strong style="margin-left:8px; color:#d32f2f; font-size:18px;">${rateMin.toFixed(4)} 元</strong>`;
        document.getElementById('rateSec').innerHTML = `<strong style="margin-left:8px; color:#d32f2f; font-size:18px;">${rateSec.toFixed(4)} 元</strong>`;
        document.getElementById('startBtn').textContent = '上班中...';
        isWorking = true;
        startTimestamp = Date.now();
        if (timer) clearInterval(timer);
        timer = setInterval(updateIncome, 100);
      } else {
        // 结束计时
        isWorking = false;
        if (timer) clearInterval(timer);
        document.getElementById('startBtn').textContent = '开始上班';
      }
    }
    function updateIncome() {
      const elapsed = (Date.now() - startTimestamp) / 1000;
      const h = Math.floor(elapsed / 3600);
      const m = Math.floor((elapsed % 3600) / 60);
      const s = Math.floor(elapsed % 60);
      document.getElementById('workTime').textContent = `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
      const earned = elapsed * rateSec;
      document.getElementById('earned').textContent = earned.toFixed(4);
    }
  </script>
</body>
</html>

【CodeBuddy】三分钟开发一个实用小功能之:折叠面板手风琴效果

作者 Homi
2025年5月21日 10:59

前言

在数字化浪潮席卷的今天,编程已然成为推动科技进步的关键力量。然而,对于许多开发者,尤其是初学者来说,编写代码的过程并非一帆风顺,常常会面临各种难题和挑战。不久前,我就遇到了这样的困扰。我想要实现一个手风琴折叠面板的网页效果,这个效果在很多网站上都能看到,能够以简洁美观的方式展示大量信息。但我在构思页面布局、设计样式以及实现交互逻辑等方面都遇到了阻碍,不知从何下手。就在我一筹莫展的时候,我遇到了 CodeBuddy。我向它详细描述了我的需求,包括页面的整体风格、交互效果以及兼容性要求等,没想到 CodeBuddy 迅速给出了一套完整的解决方案,让我惊叹不已。


以下是实际操作中的开发界面与最终呈现效果(文末附完整代码):


应用场景

快速原型开发

在项目的初期阶段,需要快速验证想法和概念。CodeBuddy 可以帮助开发者迅速生成基础代码,搭建起项目的框架。例如,在我开发手风琴折叠面板的过程中,它能快速给出 HTML 结构、CSS 样式和 JavaScript 交互代码,大大缩短了原型开发的时间。

学习编程

对于初学者来说,CodeBuddy 是一个很好的学习伙伴。它可以根据用户的需求生成代码,并详细解释代码的逻辑和实现方式,帮助初学者理解编程的基本概念和技巧。通过观察 CodeBuddy 生成的代码,初学者可以学习到如何合理布局页面、如何实现交互效果等知识。

解决编程难题

当开发者在编写代码时遇到难题,如某个功能无法实现、代码出现错误等,CodeBuddy 可以提供解决方案。它可以分析问题的原因,并给出相应的代码修改建议,帮助开发者快速解决问题。

核心功能

智能代码生成

CodeBuddy 能够根据用户输入的需求,自动生成高质量的代码。它可以理解用户的意图,结合相关的编程知识和经验,生成符合要求的代码。在我开发手风琴折叠面板时,它生成的代码结构清晰、逻辑严谨,不仅实现了基本的折叠和展开功能,还添加了平滑的动画效果和良好的用户交互体验。

代码优化

CodeBuddy 可以对已有的代码进行优化,提高代码的性能和可读性。它可以识别代码中的冗余部分,提出优化建议,并生成优化后的代码。这对于提高代码的质量和可维护性非常有帮助。

知识解释

CodeBuddy 不仅能生成代码,还能对代码进行详细的解释。它会说明代码的每一部分的作用和实现原理,帮助用户理解代码的逻辑。这对于初学者来说非常有用,可以让他们在学习代码的过程中少走弯路。

将来可以优化升级的地方

支持更多编程语言和框架

虽然 CodeBuddy 目前已经支持多种常见的编程语言和框架,但随着技术的不断发展,新的编程语言和框架不断涌现。未来可以进一步扩大支持范围,让更多的开发者受益。

更精准的需求理解

尽管 CodeBuddy 在理解用户需求方面表现出色,但在一些复杂的需求场景下,可能还存在一定的偏差。可以通过不断优化算法和模型,提高对用户需求的理解能力,生成更加精准的代码。

与开发工具的深度集成

未来可以将 CodeBuddy 与常见的开发工具(如 Visual Studio Code、IntelliJ IDEA 等)进行深度集成,让开发者在开发过程中更加便捷地使用 CodeBuddy 的功能。

总结感悟

通过这次与 CodeBuddy 的接触,我深刻体会到了 AI 编程的魅力。它不仅为我解决了实际的编程难题,还让我对编程有了更深入的理解。CodeBuddy 就像一个无所不知的编程导师,能够随时为我提供帮助和支持。在未来的编程道路上,我相信 CodeBuddy 会成为我不可或缺的伙伴。同时,我也期待 CodeBuddy 能够不断优化和升级,为开发者带来更多的惊喜和便利。AI 编程的时代已经来临,让我们一起拥抱这个充满无限可能的新时代!

附:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手风琴折叠面板</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="accordion-container">
        <div class="accordion-item">
            <div class="accordion-header">
                <span>面板 1</span>
                <i class="fas fa-chevron-down"></i>
            </div>
            <div class="accordion-content">
                <p>这里是第一个面板的内容。点击标题可以展开或折叠这个区域。</p>
            </div>
        </div>
        
        <div class="accordion-item">
            <div class="accordion-header">
                <span>面板 2</span>
                <i class="fas fa-chevron-down"></i>
            </div>
            <div class="accordion-content">
                <p>第二个面板包含更多内容。手风琴效果会自动关闭其他已展开的面板。</p>
                <p>这是一个多行内容的示例。</p>
            </div>
        </div>
        
        <div class="accordion-item">
            <div class="accordion-header">
                <span>面板 3</span>
                <i class="fas fa-chevron-down"></i>
            </div>
            <div class="accordion-content">
                <p>第三个面板的内容。所有面板都有平滑的展开/折叠动画效果。</p>
                <ul>
                    <li>列表项 1</li>
                    <li>列表项 2</li>
                    <li>列表项 3</li>
                </ul>
            </div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

style.css

body {
    margin: 0;
    padding: 0;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #9c27b0, #673ab7);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    color: #333;
}

.accordion-container {
    width: 90%;
    max-width: 800px;
    margin: 20px auto;
}

.accordion-item {
    background-color: white;
    border-radius: 8px;
    margin-bottom: 15px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    transition: all 0.3s ease;
}

.accordion-item:hover {
    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}

.accordion-header {
    padding: 18px 20px;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-weight: 600;
    font-size: 18px;
    background-color: #f8f9fa;
    transition: background-color 0.3s;
}

.accordion-header:hover {
    background-color: #e9ecef;
}

.accordion-header i {
    transition: transform 0.3s ease;
}

.accordion-item.active .accordion-header i {
    transform: rotate(180deg);
}

.accordion-content {
    padding: 0;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease, padding 0.3s ease;
}

.accordion-item.active .accordion-content {
    padding: 20px;
    max-height: 500px;
}

.accordion-content p, .accordion-content ul {
    margin: 0 0 10px 0;
}

.accordion-content ul {
    padding-left: 20px;
}

.accordion-content li {
    margin-bottom: 5px;
}

@media (max-width: 600px) {
    .accordion-header {
        padding: 15px;
        font-size: 16px;
    }
}

script.js

document.addEventListener('DOMContentLoaded', function() {
    const accordionContainer = document.querySelector('.accordion-container');
    
    // 使用事件委托处理点击
    accordionContainer.addEventListener('click', function(e) {
        const header = e.target.closest('.accordion-header');
        if (!header) return;
        
        const item = header.parentElement;
        const isActive = item.classList.contains('active');
        
        // 关闭所有面板
        document.querySelectorAll('.accordion-item.active').forEach(activeItem => {
            if (activeItem !== item) {
                activeItem.classList.remove('active');
            }
        });
        
        // 切换当前面板状态
        item.classList.toggle('active', !isActive);
    });
    
    // 初始化第一个面板为展开状态
    document.querySelector('.accordion-item').classList.add('active');
});



🌟 让技术经验流动起来

▌▍▎▏ 你的每个互动都在为技术社区蓄能 ▏▎▍▌
点赞 → 让优质经验被更多人看见
📥 收藏 → 构建你的专属知识库
🔄 转发 → 与技术伙伴共享避坑指南

点赞 ➕ 收藏 ➕ 转发,助力更多小伙伴一起成长!💪

💌 深度连接
点击 「头像」→「+关注」
每周解锁:
🔥 一线架构实录 | 💡 故障排查手册 | 🚀 效能提升秘籍

深入解析 View Transitions API 的 DOM 快照差分算法

作者 ErpanOmer
2025年5月21日 10:50

现代 Web UI 越来越强调流畅、连贯的用户体验。然而传统的 DOM 操作导致内容切换生硬,缺乏过渡动画。为此,Chrome 团队提出了一项划时代的标准草案 —— View Transitions API。它让页面在 DOM 状态变更前后实现平滑过渡,而无需显式管理动画元素或状态同步

本文将深度解析这项 API 的核心:DOM 快照差分算法(DOM Snapshot Diffing) 。这背后的算法思想并非简单的 diff,而是融合了性能优化、节点映射、样式跟踪的综合设计。我们将通过真实案例,揭示它是如何在瞬间捕捉视觉状态并生成过渡动画的。


什么是 View Transitions API?

View Transitions API 是一种浏览器级别的页面变更过渡机制。

其核心理念是:

  • 捕获变更前后页面 DOM 的视觉快照
  • 通过 diff 算法匹配关键元素
  • 浏览器自动生成补间动画,实现平滑过渡

它不仅适用于 SPA 页面局部更新,也支持多页应用(MPA)的全页面跳转过渡。


DOM 快照差分的基本原理

View Transitions API 的流程可以概括为以下几步:

1. 捕获变更前的 DOM 快照(before snapshot)
2. 执行用户定义的 DOM 更新
3. 捕获变更后的 DOM 快照(after snapshot)
4. 计算两次快照之间的差异(DOM diff)
5. 渲染补间动画(transition)

其中,步骤 4 的差分算法是整个机制的灵魂。它负责决定:

  • 哪些 DOM 元素发生了位移、缩放、透明度变化
  • 哪些元素是“新增”、“删除”或“保留”
  • 哪些元素在视觉上是“相同”的(通过 view-transition-name 标记)

快照差分算法核心逻辑

浏览器内部的算法设计无法开源查看,但我们可根据官方规范和实验行为推断其大致过程:

✅ 1. 节点识别:基于 view-transition-name

在 DOM 中标记关键元素:

<div style="view-transition-name: profile-avatar"></div>

浏览器在快照时会记录这些节点,并使用名称作为匹配键。

如果某个元素在旧 DOM 和新 DOM 中都存在 view-transition-name: profile-avatar,则认为这是同一个视觉实体,可以执行平滑动画。

✅ 2. 样式快照:计算视觉特征

对于匹配的节点,浏览器会记录它们的:

  • 边界框(bounding box)
  • 样式(transform、opacity、clip、border-radius 等)
  • 可见性
  • 位图缓存(用于过渡期间离屏绘制)

这些构成了节点的“视觉快照”。

✅ 3. 快照比对:空间 diff + 样式 diff

浏览器在两个快照中对比这些信息:

  • 位置是否发生变化?→ 位移动画
  • 尺寸是否变化?→ 缩放动画
  • 样式是否渐变?→ 淡入淡出等动画

如果两个快照之间的差异无法可视化,就不会做动画。例如完全重排结构、父节点不同、元素消失等。

✅ 4. Transition layer 渲染

最终,浏览器将这些差分构建成一个 过渡图层(transition layer) ,在动画过程中显示旧状态到新状态的补间效果。


实战案例:头像变更平滑过渡

我们以一个切换用户资料页的例子来说明实际应用:

HTML 结构

<!-- 页面 A -->
<img src="avatar.jpg" style="view-transition-name: avatar">

<!-- 页面 B -->
<img src="avatar-large.jpg" style="view-transition-name: avatar">

在 JavaScript 中触发 View Transition:

document.startViewTransition(() => {
  // 切换页面内容
  renderProfilePage();
});

浏览器会:

  • 捕捉当前 avatar 的位置与样式
  • 渲染 B 页面后,获取新的 avatar 元素
  • 自动插入 transition 图层,将旧图片平移、缩放到新位置
  • 页面平滑过渡,无需手动管理动画

动画控制:自定义样式和中断逻辑

可通过 @keyframes 控制样式细节:

::view-transition-old(avatar) {
  animation: fade-out 0.3s ease;
}
::view-transition-new(avatar) {
  animation: fade-in 0.3s ease;
}

还可通过 viewTransition.updateCallbackDone.then(...) 控制下一步逻辑,保证动画完成后再执行下一阶段操作。


差分算法的局限与注意事项

虽然该算法简化了开发者负担,但其内部仍有一定约束:

  • 必须明确标记 key 元素(用 view-transition-name)
  • 不能跨文档跳转(仅在支持的 SPA 中有效)
  • 不支持渲染时延迟组件,否则快照获取不到预期元素
  • 不适用于复杂结构重排,例如 DOM 架构层级变更严重时,无法识别同一元素

浏览器兼容性与渐进增强

View Transitions API 当前仅在 Chromium 111+ 浏览器中支持,Firefox、Safari 尚未实现。

为了兼容性,可采用渐进式方案:

if (document.startViewTransition) {
  document.startViewTransition(() => {
    // 新 DOM 更新逻辑
  });
} else {
  // Fallback:直接更新页面
  renderPageWithoutTransition();
}

总结

View Transitions API 让前端开发进入了“原生动画无痛过渡”的新时代。其核心的 DOM 快照差分算法,本质上是浏览器的一种轻量级“动画 reconciliation”机制。它通过元素命名 + 样式 diff 实现视觉同步,使页面更新更自然、更平滑。

在实际开发中,善用 view-transition-name、理解快照捕获时机、避免非结构性突变,将使你掌控这项技术如臂使指。


参考链接

如何在微前端中实现全局通信

作者 mm_t
2025年5月21日 10:50

微前端中Host与Remote的通信

静态数据的全局通信

import {Injectable} from '@angular/core';
import _ from 'lodash';

@Injectable({providedIn: 'platform'})

export class DataStoreService {
    sharedData = {};
    
    private _clone(object) { 
        return ({..object }
    };
    
   get data(){
        this.sharedData = this._clone(this.sharedData):
        return this.sharedData;
    }
    
    set data(value: any) {
        throw new Error('do not mutate the " data" directly');
       }

    get(prop: any) {
        const data = this.data;
        return data.has0wnProperty(prop) ? data[prop] : null;
    }

    set(prop: string, value: any){
        this.sharedData[prop] = value;
        return this.sharedData[prop];
    }

    clear (exceptionkeys?: string[]) {
        if(_.isEmpty(exemptionKeys)) {
            this.sharedData = {};
            return;
        }
        this.shareData = _.pick(this.shareData, exceptionkeys); //. 保留指定属性以对象形式返回
        }
    }

全局数据的实时通信

通过封装RXJS实现全局的实时通信, 首先封装一下通信的service,让其可以在remote和host中可以更好的使用

import { Injectable } from "@angular/core";
import { Subject } from "rxjs";

@Injectable({
  providedIn: "platform", // 跨平台共享,适用于微前端架构
})
export class CommunicationService {

subMap: Map<string, Subject<unknown>> = new Map<string, Subject<unknown>>();

createAction (actionName: string) {
    const actionSub = new Subject<unknown>();
    this.subMap.set(actionName, actionSub);
}

dispatchAction<T>(actionName: string, payload?: T) {
    this.createActionIfNotPresent(actionName);
    this.subMap.get(actionName).next(payload);
 }
 
createEffect<T, U>(actionName: string, effectToRun: (...args: T[]) => U) {
    this.createActionIfNotPresent(actionName);
    this.subMap.get(actionName).subscribe(effectToRun);
}

destroyAction(actionName: string) {
    if (this.subMap.has(actionName)) {
        this.subMap.get(actionName).unsubscribe();
        this.subMap?.delete(actionName);
    }
}

destroyAll(){
    this.subMap?.forEach((sub) => sub?.unsubscribe?.());
    this.subMap?.clear ();
}

private createActionIfNotPresent (actionName: string) {
    if (!this.subMap.has(actionName)) {
      this.createAction(actionName);
    }
}

数据更改的地方如何使用 communication service 去通知监听的地方数据的指变更了?

# 1. 首先使用的地方先provide这个service
xxfunction(){
    const data = 'test';
    this.communicationService.dispatchAction('CustomKey', data)
}

监听数据变更,并执行相对应的操作

 # 1. 首先使用的地方先provide这个service
 ngOnInit(){
  this.communicationService.destroyAction('CustomKey');
  this.communicationService.createEffect('CustomKey', (data: any) => {
    if(data) {
      // if we listen data change we should do some relevant logic in this callback
    }
  })
 }
 
 # 2. 当前组件销毁时记得取消订阅
 ngOnDestroy(){
     this.communicationService.destroyAction('CustomKey');
 }

Mac 配置 Vue 项目端口转发(1024 -> 80)

2025年5月21日 10:49

Mac 配置 Vue 项目端口转发(1024 -> 80)

背景

在 Mac 上,Vue CLI 项目默认使用高位端口(如 1024),但有时需要将请求转发到 80 端口。由于 80 是特权端口,需要特殊配置。

配置步骤

1. 配置 Vue 项目端口

vue.config.js 中设置开发服务器端口为 1024:

devServer: {
  port: 1024,
  host: '0.0.0.0',
  // ... 其他配置
}

2. 设置端口转发

使用 pfctl 命令设置端口转发(将 80 端口转发到 1024):

  1. 创建端口转发规则文件:
echo "
rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 1024
" | sudo tee /etc/pf.anchors/portforward
  1. 启用端口转发:
sudo pfctl -ef /etc/pf.anchors/portforward
  1. 如果遇到权限问题,先启用 pf:
sudo pfctl -e
  1. 验证规则是否生效:
sudo pfctl -s nat

3. 取消端口转发

如果需要取消端口转发:

sudo pfctl -d

验证

配置完成后,可以通过以下地址访问:

  • 本地访问:http://localhost/xxx
  • 局域网访问:http://<你的IP地址>/xxx

注意事项

  1. 确保 80 端口没有被其他程序占用
  2. 如果遇到权限问题,检查是否使用了 sudo 命令
  3. 修改配置后需要重启开发服务器
  4. 如果使用 nginx,需要确保 nginx 配置正确

常见问题

  1. 如果出现 "command not found",确保使用的是 pfctl 而不是已废弃的 ipfw
  2. 如果转发不生效,检查防火墙设置
  3. 如果访问出现 403 错误,检查代理配置和请求头设置

相关命令

  • 查看端口占用:sudo lsof -i :80
  • 查看转发规则:sudo pfctl -s nat
  • 重启开发服务器:npm run serve

Import Maps 对模块解析算法的扩展影响与实战应用

作者 ErpanOmer
2025年5月21日 10:47

随着前端模块化的发展,ES Modules(简称 ES 模块)逐渐成为现代 JavaScript 代码组织的主流方式。浏览器原生支持 ES 模块后,如何高效管理模块依赖路径成为关键挑战。尤其是浏览器默认仅支持通过相对路径或绝对路径引用模块,而不能识别裸模块名(如 lodashreact)直接导入。

为解决这一痛点,Import Maps 规范应运而生。它为浏览器引入了一种“模块路径重写”机制,通过静态或动态声明映射关系,扩展了模块解析算法,使得裸模块名与自定义路径能够被正确解析加载。

本文将深入剖析 Import Maps 对浏览器模块解析算法的影响机制,结合实战案例展示如何利用 Import Maps 优化项目依赖管理,最后讨论其在兼容性及未来生态中的应用前景。


1. 浏览器模块解析的基本机制

浏览器对 ES 模块的加载遵循 ECMAScript 规范定义的模块解析算法:

  • 绝对路径(以协议开头,如 https://)直接请求资源。
  • 相对路径./../)根据当前模块地址计算请求资源地址。
  • 裸模块名(如 lodash)默认无法被识别,导致加载失败。

传统前端构建工具(Webpack、Rollup)通过打包将裸模块解析成路径引用,从而适配浏览器环境。Import Maps 的出现,打破了这个限制,让浏览器能够识别裸模块名并重写为实际路径。


2. Import Maps的设计原理与解析流程扩展

Import Maps 通过在页面中注入 <script type="importmap"> 标签,声明模块名称与 URL 的映射关系,浏览器在解析模块请求时优先查询 Import Maps 进行路径替换。

扩展点体现在模块解析算法中的“映射查找”步骤:

  1. 检查模块标识符(specifier)是否与 Import Maps 中的某个映射匹配。
  2. 匹配成功,则将模块标识符替换为映射的完整 URL。
  3. 未匹配则继续走浏览器默认解析规则。

这种机制让开发者可以在不依赖构建工具的情况下,直接通过裸模块名或自定义路径别名引用模块。


3. Import Maps 的映射规则与匹配优先级

Import Maps 的映射关系包含:

  • 精确匹配(如 "react": "https://cdn.../react.js"
  • 前缀匹配(如 "utils/": "/src/utils/",匹配以 utils/ 开头的模块)

浏览器解析时优先匹配最长且最具体的前缀。例如:

{
  "imports": {
    "utils/": "/src/utils/",
    "utils/helpers/": "/src/utils/helpers/"
  }
}

utils/helpers/date.js 会匹配更具体的 "utils/helpers/" 路径。


4. 实战应用

4.1 无构建裸模块加载

不依赖构建工具,直接在浏览器使用裸模块名:

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es/lodash.js"
  }
}
</script>
<script type="module">
import _ from 'lodash';
console.log(_.chunk([1,2,3,4], 2));
</script>

4.2 路径别名管理

大型项目中目录层级深,使用路径别名:

<script type="importmap">
{
  "imports": {
    "components/": "/src/components/"
  }
}
</script>
<script type="module">
import Button from 'components/Button.js';
</script>

有效提升代码可读性及重构灵活度。

4.3 微前端依赖隔离

通过动态更新 Import Maps,实现不同子应用独立依赖:

const importMap = {
  imports: {
    'react': 'https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js'
  }
};
const script = document.createElement('script');
script.type = 'importmap';
script.textContent = JSON.stringify(importMap);
document.currentScript.after(script);

动态修改 Import Maps 便于灰度发布和版本切换。


5. 兼容性及注意事项

  • 浏览器支持:Chrome 89+、Edge 89+ 完整支持,Firefox 和 Safari 支持度有限,需检测环境。
  • 缓存问题:Import Maps 内容变更可能被缓存,生产环境需合理设计缓存策略。
  • 安全性:通过 Import Maps 加载的第三方模块需注意来源安全,防止供应链攻击。
  • 动态修改限制:浏览器允许动态插入新的 Import Maps,但对修改已有映射有一定限制。

6. 未来展望

随着浏览器原生模块生态的发展,Import Maps 有望成为前端模块管理的基础设施之一。结合 Service Worker、动态模块加载等技术,可打造无打包、极速响应的现代前端架构。


7. 结语

Import Maps 通过对模块解析算法的扩展,实现了裸模块名的浏览器友好加载,大大简化了依赖管理流程。理解其工作机制与应用场景,将帮助开发者更高效地构建现代前端应用。


参考资料

初识 Vue3 生命周期 | 手把手带你玩转全生命周期钩子

2025年5月21日 10:39

初识 Vue3 生命周期 | 手把手带你玩转全生命周期钩子

作为一名前端老司机,我经常看到新同学在 Vue 生命周期上栽跟头。今天咱们就来扒一扒 Vue3 的生命周期到底怎么玩,附上超详细的 JavaScript 代码示例!


为什么 Vue 生命周期这么重要?

想象你正在搭建一座房子:

  • 地基阶段:得先规划结构(对应 beforeCreate
  • 框架搭建:开始搭钢架(对应 created
  • 内外装修:粉刷墙壁安装灯具(对应 mounted
  • 后期维护:修补漏水更换家具(对应 updated/unmounted

Vue 生命周期就是这套精准的施工指南,帮你把控组件从出生到消亡的每一个关键节点。


Vue3 生命周期全景图

graph TD
    A[beforeCreate] --> B[created]
    B --> C[beforeMount]
    C --> D[mounted]
    D --> E[beforeUpdate]
    E --> F[updated]
    F --> G[beforeUnmount]
    G --> H[unmounted]

小贴士:Vue3 把很多钩子搬到了组合式 API 里,我们主要关注 setup() 里的生命周期


动手实操 | 从零开始搭建计时器组件

让我们通过构建一个简单的数字时钟组件,感受各个生命周期的魔法时刻

// 完整代码文件:CounterTimer.vue
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    let timerId = null

    // 1. beforeCreate 替代方案 - 使用 ref 初始化前
    onBeforeMount(() => {
      console.log('准备开始挂载组件')
    })

    // 2. created 替代方案 - setup 执行时
    console.log('组件刚创建完成,count 初始值:', count.value)

    // 3. mounted 核心逻辑
    onMounted(() => {
      timerId = setInterval(() => {
        count.value += 1
      }, 1000)
      console.log('组件已挂载,开启计时器')
    })

    // 4. beforeUpdate 前置操作
    onBeforeUpdate(() => {
      console.log('检测到数据变更,旧值:', count.value)
    })

    // 5. updated 后置操作
    onUpdated(() => {
      console.log('数据更新完成,新值:', count.value)
    })

    // 6. unmounted 清理工作
    onUnmounted(() => {
      clearInterval(timerId)
      console.log('组件即将销毁,清除计时器')
    })

    return {
      count
    }
  }
}

这个组件会经历完整的生命周期流程:

  1. 初始化时打印 count 初始值
  2. 挂载后启动计时器
  3. 每次数值更新前后打印日志
  4. 销毁时清理定时器

生命周期实战技巧

获取真实 DOM 的最佳时机

import { ref, onMounted } from 'vue'

export default {
  setup() {
    const inputRef = ref(null)

    onMounted(() => {
      inputRef.value.focus() // 确保 DOM 已经渲染
    })

    return { inputRef }
  }
}

警惕异步操作陷阱

// 错误示范:在生命周期中进行异步操作
onMounted(async () => {
  const data = await fetchData()
  console.log(data) // 可能在组件卸载后才执行
})

复杂场景解决方案

当需要根据环境选择生命周期逻辑时:

if (import.meta.env.SSR) {
  // 服务端渲染的特殊处理
} else {
  onMounted(() => {
    // 仅在客户端执行
  })
}

高级玩法 | keep-alive 缓存的秘密

<template>
  <div v-if="false"></div>
</template>
// 配合 keep-alive 使用的激活钩子
export default {
  setup() {
    onActivated(() => {
      console.log('组件重新显示')
    })
    onDeactivated(() => {
      console.log('组件即将隐藏')
    })
  }
}

新手常犯错误清单

危险行为 后果描述 解决方案
在 serverPrefetch 中操作 DOM SSR 环境下报错 判断平台后再执行
mounted 中直接修改 $props 触发无限更新循环 使用 computed 处理属性
生命周期中使用箭头函数 this 指向错误 改用普通函数定义

学习路径建议

  1. 第一阶段:对照官方文档实现所有生命周期钩子
  2. 第二阶段:重构现有项目,将所有 option API 转换为组合式 API
  3. 第三阶段:尝试在 SSR 环境中测试生命周期执行情况

现在你已经掌握了 Vue3 生命周期的核心用法!快去改造你的项目吧~ 如果遇到问题,欢迎在评论区交流讨论哦!

前端发展史:从静态页面到现代Web应用的演进之路

作者 哎呦喂205
2025年5月21日 10:24

引言:前端技术的起源

前端开发的历史可以追溯到互联网的早期阶段。1990年,Tim Berners-Lee发明了第一个网页浏览器WorldWideWeb(后来改名为Nexus),同时创造了HTML(超文本标记语言)。这标志着前端技术的诞生,当时的"前端"仅仅是静态文本和简单链接的组合。

第一阶段:静态网页时代(1990-1995)

  • HTML的诞生:1991年,HTML1.0发布,只包含18个标签
  • Mosaic浏览器:1993年发布,首次支持图片内联显示
  • 基础技术栈:纯HTML,无CSS和JavaScript
  • 开发方式:手工编写HTML文件,通过FTP上传
<!-- 典型的早期HTML页面 -->
<html>
<head>
    <title>我的第一个网页</title>
</head>
<body>
    <h1>欢迎来到我的网站</h1>
    <p>这是一个段落。</p>
    <a href="another.html">点击这里</a>转到另一个页面。
</body>
</html>

第二阶段:动态交互的萌芽(1995-2005)

JavaScript的革命

1995年,Netscape公司的Brendan Eich在10天内创造了JavaScript(最初叫Mocha,后改名LiveScript,最终定为JavaScript)。这一发明彻底改变了前端的可能性。

  • 1996年:微软推出JScript,与Netscape竞争
  • 1997年:ECMAScript标准确立(ECMA-262)
  • DOM Level 1:1998年发布,为动态页面操作提供标准

CSS的引入

1996年,CSS1成为W3C推荐标准,实现了内容与表现的分离。

/* 早期CSS示例 */
body {
    font-family: Arial;
    background-color: white;
}
h1 {
    color: blue;
}

浏览器大战

这一时期,Netscape Navigator和Internet Explorer展开了激烈的"浏览器大战",虽然带来了创新,但也导致了严重的兼容性问题。

第三阶段:Ajax与Web 2.0(2005-2010)

2005年,Jesse James Garrett发表文章《Ajax: A New Approach to Web Applications》,标志着Web 2.0时代的开始。

  • 关键技术
    • XMLHttpRequest(尽管2005年才被广泛关注,但早在1999年就已存在)
    • JSON逐渐取代XML成为数据交换格式
  • 代表性产品
    • Google Maps(2005)
    • Gmail(2004)
    • Flickr(2004)
// 典型的Ajax请求
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        var data = JSON.parse(xhr.responseText);
        // 更新DOM
    }
};
xhr.send();

jQuery的崛起(2006)

John Resig发布jQuery,解决了浏览器兼容性问题,简化了DOM操作和Ajax请求。

// 使用jQuery实现同样功能
$.get('/api/data', function(data) {
    // 更新DOM
});

第四阶段:前端工程化(2010-2015)

MVC框架的出现

  • Backbone.js(2010):首个流行的前端MVC框架
  • AngularJS(2010):Google推出的双向数据绑定框架
  • Ember.js(2011):约定优于配置的框架

Node.js与前端工具链

2009年,Ryan Dahl发布Node.js,使JavaScript可以运行在服务器端,同时带来了npm(Node Package Manager),彻底改变了前端开发的工作流。

  • 构建工具:Grunt(2012)、Gulp(2013)
  • 模块打包:Browserify(2011)、Webpack(2012)
  • 转译器:Babel(2014),使开发者可以使用ES6+特性

响应式设计与移动优先

2010年,Ethan Marcotte提出"响应式网页设计"概念,随着智能手机普及,移动端适配成为必须。

/* 媒体查询示例 */
@media screen and (max-width: 600px) {
    body {
        font-size: 14px;
    }
}

第五阶段:现代前端开发(2015-至今)

React、Vue和Angular的三足鼎立

  • React(2013年发布,2015年后流行):虚拟DOM和组件化思想
  • Vue(2014):渐进式框架,易上手
  • Angular(2016年重写为Angular 2+):完整的MVC解决方案
// React组件示例
function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}

ES6+与现代JavaScript

ECMAScript 2015(ES6)带来了重大更新:

  • 类与模块
  • 箭头函数
  • Promise
  • let/const
  • 解构赋值
  • 等等...

前端工程化的深入

  • 状态管理:Redux(2015)、Vuex
  • TypeScript:微软推出的类型化JavaScript超集
  • SSR/SSG:Next.js(2016)、Nuxt.js(2017)等框架
  • 微前端:解决大型应用的前端架构问题
  • WebAssembly:高性能Web应用的新选择

现代CSS解决方案

  • CSS预处理器:Sass、Less
  • CSS-in-JS:Styled-components、Emotion
  • CSS框架:Tailwind CSS、UnoCSS等实用工具优先方案

未来趋势

  1. Web Components:原生组件化方案
  2. Progressive Web Apps (PWA):接近原生应用的体验
  3. WebAssembly:高性能计算在浏览器中的实现
  4. Serverless与边缘计算:前后端界限进一步模糊
  5. AI与前端结合:如GitHub Copilot等工具改变开发方式

结语

前端开发在短短30年间经历了惊人的变革,从简单的静态页面发展到如今复杂的应用程序。技术的快速迭代要求前端开发者保持持续学习的态度。正如JavaScript之父Brendan Eich所说:"Always bet on JavaScript",前端技术的未来仍充满无限可能。


延伸阅读推荐

  1. 《JavaScript高级程序设计》(红宝书)
  2. 《你不知道的JavaScript》系列
  3. MDN Web Docs
  4. 掘金小册《前端技术演进史》

相关社区

  • 稀土掘金前端板块
  • GitHub前端热门仓库
  • Stack Overflow前端标签

希望这篇文章能帮助你理解前端技术的发展脉络,欢迎在评论区分享你的前端学习经历或对未来的预测!

红宝书第三十二讲:零基础学会模块打包器:Webpack、Parcel、Rollup

作者 kovli
2025年5月21日 10:19

红宝书第三十二讲:零基础学会模块打包器:Webpack、Parcel、Rollup

资料取自《JavaScript高级程序设计(第5版)》。 查看总目录:红宝书学习大纲


一、模块打包器是什么?

把分散的HTML/CSS/JS文件 组合成浏览器可加载的单个/少量文件。解决三大问题 1

  1. 依赖管理(如import语法)
  2. 语法转换(ES6→ES5)
  3. 文件优化(压缩、图片转base64)
flowchart LR
    网页组件模块 --> JS文件 --> 打包器处理 --> 单个优化文件
    CSS/HTML文件 --> 打包器处理 --> 单个优化文件

二、Webpack:配置灵活的全能选手 21

主流打包工具,适合复杂项目(如React/Vue)。

核心特点
  1. 插件丰富:支持代码分割、热更新等
  2. Loader机制:不同类型文件转换(如.scss→.css
典型配置文件(webpack.config.js)
module.exports = {
  entry: './src/index.js',    // 入口文件
  output: {
    filename: 'bundle.js',    // 输出文件
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] } // CSS处理规则
    ]
  }
};
常用rules

以下是 Webpack 最常用的 rules 配置列表,以表格形式呈现:

规则名称 描述 示例
test 用于匹配文件扩展名的正则表达式,决定哪些文件需要被处理 test: /\.js$/ 匹配所有 .js 文件
include 包含哪些文件或目录 include: [path.resolve(__dirname, 'src')]
exclude 排除哪些文件或目录 exclude: /node_modules/ 排除 node_modules 目录
use 指定使用的 Loader,可以是字符串或对象数组 use: ['style-loader', 'css-loader']
loader use 中指定单个 Loader loader: 'babel-loader'
options 为 Loader 传递选项 options: { presets: ['@babel/preset-env'] }
enforce 强制执行 Loader 的顺序,可选值为 prepost enforce: 'pre' 强制在其他 Loader 之前执行
示例配置

以下是一个常见的 Webpack rules 配置示例:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192 // 小于 8KB 的图片会被转为 base64
            }
          }
        ]
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  }
};

这些规则涵盖了常见的文件类型处理,如 JavaScript、CSS、图片等,适用于大多数 Webpack 项目。

常用loder配置

以下是 Webpack 中最常用的 Loader 及其 options 配置的表格总结:

Loader 名称 常用 options 配置 描述
url-loader limit
name
outputPath
esModule
小于 limit 的文件会被转为 Base64 编码,否则使用 file-loader 处理。
file-loader name
outputPath
esModule
将文件复制到输出目录,并返回文件的公共 URL。
css-loader modules
importLoaders
sourceMap
解析 CSS 文件中的 @importurl(),支持 CSS 模块化。
style-loader insert
singleton
attributes
将 CSS 插入到 HTML 的 <style> 标签中。
sass-loader implementation
sourceMap
将 Sass 文件编译为普通 CSS 文件。
babel-loader presets
plugins
用于编译 ES6+ 代码到 ES5。
eslint-loader fix
configFile
对 JavaScript 文件进行代码检查,fix 可自动修复部分问题。
postcss-loader postcssOptions
sourceMap
使用 PostCSS 插件处理 CSS,如 autoprefixer
image-webpack-loader mozjpeg
pngquant
gifsicle
webp
对图片进行优化,如压缩、格式转换。
html-loader minimize
attributes
处理 HTML 文件,支持压缩和图片引用处理。

这些配置项是 Webpack 项目中常用的 Loader 配置,可以根据项目需求进行调整。


三、Parcel:开箱即用 2

零配置工具,适合新手快速上手小项目。

使用步骤
  1. 安装工具
npm install -g parcel-bundler
  1. 直接运行
parcel index.html  # 自动处理HTML中的JS/CSS引用
优点演示

项目结构:

src/
  index.html
  style.css
  app.js

直接运行后生成优化后的 dist 文件夹,自动处理所有依赖和编译!


四、Rollup:轻量高效的库打包器 23

专为JS库设计,提供 Tree Shaking 移除未使用代码。

示例:打包一个数学库

源码(math.js):

export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }

主文件(index.js):

import { add } from './math.js';
console.log(add(2,3));

打包后:仅保留 add 函数代码!3

快速安装使用
npm install rollup --save-dev
npx rollup index.js --file bundle.js --format umd

五、横向对比:该怎么选?

工具 特点 适用场景
Webpack ✔️配置灵活
✔️生态强大
❌上手复杂
企业级复杂项目(React/Vue)
Parcel ✔️无需配置
✔️自动优化
❌定制性差
简单页面原型开发
Rollup ✔️极致精简
✔️打包效率高
❌插件少
JS库或框架开发(如Vue3)

目录:总目录 上篇文章:红宝书第三十一讲:通俗易懂的包管理器指南:npm 与 Yarn


脚注

Footnotes

  1. 《JavaScript高级程序设计(第5版)》说明Webpack支持多模块格式和插件扩展 2

  2. 《JavaScript高级程序设计(第5版)》介绍Webpack、Parcel和Rollup的基本特性 2 3

  3. 《JavaScript高级程序设计(第5版)》指出Rollup具备Tree Shaking功能 2

基于pnpm monorepo 项目工程化设计

作者 岩柏
2025年5月21日 10:15

传统架构概述

什么是传统架构?

  • 独立项目结构:每个项目作为独立的单元开发,维护和部署。
  • 技术栈独立:不同的项目可能使用不同的技术栈和工具链。
  • 依赖管理:每个项目都有自己独立的 node_modules 和依赖配置。
  • 部署策略:各自独立部署和上线,通常依赖 CI/CD 工具进行自动化部署。

传统架构的优缺点

优点

  • 开发独立性:各团队或者项目相互隔离,独立开发,互不干扰。
  • 灵活性:可以自由选择和切换技术栈和工具链。
  • 简单性:每个项目都有自己独立的依赖和配置,不需要与其他项目进行复杂的依赖管理。

缺点

  • 代码重复性:多个项目中可能存在重复代码或者模块,难以统一管理和复用。
  • 依赖版本冲突:多个项目维护相同依赖的不同版本,可能导致版本冲突和兼容性问题。
  • 协作成本高:跨项目的联调和功能共享需要额外的沟通和管理。
  • 构建效率低:每个项目都需要单独构建和部署,效率低下。

PNPM Monorepo 架构概述

monorepo 架构

  • 混合项目结构,所有相关的工程形成子包进行管理。
  • 技术栈高度统一,团队基建项目、业务项目、子服务、技术栈
  • 规范化、自动化、流程化项目之间共享
  • 依赖管理,版本统一管理
  • 部署策略,docker、compose、自动化脚本统一部署流程

pnpm 的优势

  • 链接机制(软链接)
  • 缓存机制, 寻址
  • 原生支持 workspace
  • 依赖扁平化管理
  • 磁盘占用少
  • 速度快

用一句话总结:中心化思想解决依赖复用问题

一、什么是Monorepo 定义与概念

Monorepo 单个仓库多项目管理,是一种项目代码管理方式, 将多个项目或模块放在一个仓库中进行管理,这种方式有助于代码共享、依赖管理、版本控制、构建和部署等方面的复杂性,并提供更好的可重用和协作性。

需求背景

在大型项目中,通常会有多个子项目或模块,这些子项目或模块之间可能存在依赖关系,如果每个子项目或模块都单独管理,那么在依赖管理和版本控制方面可能会变得非常复杂。而Monorepo通过将所有子项目或模块放在一个仓库中进行管理,可以简化依赖管理和版本控制,提高开发效率。

1.多项目整合与独立运行:

  • 将多个项目整合在一个仓库中,方便管理和维护。
  • 提供灵活的运行机制,允许一次运行多个项目或者运行其中的某一个项目。

2.共享样式与组件:

  • 提取多个项目中共有的样式和组件,避免重复编写代码,降低维护的成本, 将他们放置在 workspace的根目录下。
  • 通过 pnpm workspace 的依赖管理机制,使得其他项目可以方便地引用这些共享的样式和组件。

实现步骤

一、多项目搭建-配置-运行

1.创建项目目录
mkdir my-monorepo
cd my-monorepo
2.初始化项目
  • 执行 pnpm init 来初始化一个 package.json的文件,这个文件将作为整个 Monorepo 目录的配置文件。
pnpm init
  • 初始化完成后,会在 my-monorepo 目录下生成一个 package.json 文件。
{
  "name": "my-monorepo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.9.0"
}
3. 配置 package.json 的文件
  • 删除 package.json 文件中的 main、test 字段,因为 Monorepo 中通常不需要这个字段。
  • 添加 private 字段,将其设置为 true,以防止意外发布到 npm 上。
{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.9.0"
}
4. 创建 pnpm-workspace.yaml 文件
  • my-monorepo 目录下创建一个 pnpm-workspace.yaml 文件,这个文件将用于配置 Monorepo 的工作区。
touch pnpm-workspace.yaml
  • pnpm-workspace.yaml 文件中添加以下内容:

  • 这将告诉 pnpmpackages 目录下的所有子目录都作为工作区的一部分。

packages:
  # 这样写相当于可以访问packages目录下的所有子目录
  - "packages/*"
5. 创建目录 packages 这个文件用来放你的项目,可以放一个或者多个
  • 这里的目录名字可以自定义,但通常使用 packages 作为目录名。
  • 但是无论自定义什么名字,在 pnpm-workspace.yaml 文件中都需要进行相应的配置。
  • 譬如:你的名字叫 my-workspace,那么在 pnpm-workspace.yaml 文件中就需要这样写:
packages:
  - "my-workspace/*"
6. 创建子项目
  • packages 目录下创建一个子项目,例如 my-project-vue
pnpm  create  vite my-project-vue  --template  vue
  • 再创建一个子项目 my-project-vue-ts
pnpm  create  vite my-project-vue-ts  --template  vue-ts
  • 创建成功后的目录结构如下:
my-monorepo
├── pnpm-workspace.yaml
├── packages
│   ├── my-project-vue
│   └── my-project-vue-ts
└── package.json
7. 避免端口冲突,设置子项目启动端口
  • 分别在 my-project-vuemy-project-vue-ts 两个项目目录下的 vite.config.js/vite.config.ts 中,设置不同的端口,避免端口冲突。
server: {
  port: 8080,
}
8. 在 my-monorepo 目录下安装依赖
pnpm  i
  • 安装完成依赖后的目录结构如下:
my-monorepo
├── node_modules
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── packages
│   ├── my-project-vue
│   └── my-project-vue-ts
└── package.json
9. 在根目录下的 package.json 文件中添加脚本(用于运行子项目的命令)
  • my-monorepo 目录下的 package.json 文件中添加以下脚本:
  • -C 参数用于指定要运行的子项目目录。
{
  "scripts": {
    "dev:vue": "pnpm  run  -C packages/my-project-vue dev",
    "dev:vue-ts": "pnpm  run  -C packages/my-project-vue-ts dev",
    "build:vue": "pnpm  run  -C packages/my-project-vue build",
    "build:vue-ts": "pnpm  run  -C packages/my-project-vue-ts build"
  }
}
10. 运行启动项目
  • 在运行的命令都配置好了,结果运行会报错,因为 pnpm 还没有识别到 Monorepo 的配置,需要重新安装依赖。 注意: 要再安装一遍依赖

    pnpm  i
    
  • 运行 pnpm run dev:vue 启动 my-project-vue 项目。

  • 到目前两个项目都可以启动和运行了。

11. 配置: 一个命令启动所有项目
  • my-monorepo 目录下的 package.json 文件中
  • 我们已经为各个子项目配置了启动命令和构建命令,现在我们希望有一个命令可以同时启动所有子项目。
  • 如果想要一次性启动所有子项目的开发服务器
  • 可以使用一个能够并行运行多个任务的工具,比如 npm-run-all 或者 concurrently。 这里使用 concurrently 来实现这个需求
11.1 需要安装 concurrently
pnpm  i  concurrently  -D
11.2 在根目录下的 package.json 文件中添加脚本
"scripts": {
  "dev:all": "concurrently \"pnpm run -C packages/my-project-vue dev\" \"pnpm run -C packages/my-project-vue-ts dev\""
}

注意,这里我们使用了双引号来包围每个  pnpm run  命令,并且整个  concurrently  命令也被双引号包围。这是因为在 JSON 中,字符串内部的特殊字符(如空格)需要被转义,而使用双引号是最简单的方法。此外,由于 Windows 命令提示符对引号的处理有所不同,这种方法在 Unix/Linux/macOS 和 Windows 的 Git Bash 或 PowerShell 中都应该能正常工作。

11.3 运行命令
pnpm dev:all
  • 运行结果: c3ca2024-f332-4665-ad9f-d14db8006f58.png

全局公共样式

  • 当多个项目都需要一样的样式,那么我们可以把这个样式抽离出来,定义在全局,定义一次,所有项目都可以直接使用
1. 在 pnpm-workspace.yaml 配置公共样式
packages:
  # 这样写相当于可以访问packages目录下的所有子目录
  - "packages/*"
  - "common/*"
2. 在 my-monorepo 根目录下创建 common 文件夹,并在 common 文件夹下创建 common-styles 文件夹,并在 common-styles 文件夹下创建 index.css 文件
.common-button {
  background-color: #6b9cd0;
  color: white;
  border: none;
  padding: 10px 20px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
}

.common-button:hover {
  background-color: #0056b3;
}
  • 文件结构如下:
my-monorepo
├── common
│   └── common-styles
│       └── index.css
├── pnpm-workspace.yaml
├── packages
│   ├── my-project-vue
│   └── my-project-vue-ts
└── package.json
3.初始化 common-styles 包
  • cd 到 common/common-styles 目录下,初始化 common-styles
  • 运行 pnpm init 初始化一个 package.json文件
pnpm init -y
{
  "name": "common-styles",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.9.0"
}
  • 文件结构如下:
my-monorepo
├── common
│   ├── styles
│   │   ├── index.css
│   │   └── package.json
├── pnpm-workspace.yaml
├── packages
│   ├── my-project-vue
│   └── my-project-vue-ts
└── package.json
4. 在项目的 package.json 中添加依赖
  • my-project-vuemy-project-vue-ts 两个项目的 package.json 文件中添加依赖
  • dependencies 部分添加对 common-styles 的依赖。由于 common-styles 是一个本地包,需要使用文件路径来指定它的位置。
  • 譬如: 在项目 my-project-vue 中添加依赖
{
  "dependencies": {
    "vue": "^3.5.13",
    "common-styles": "file:../../common/common-styles"
  }
}

注意:这里的路径是相对路径,它指向项目根目录下的 common/common-styles 文件夹。如果你的项目结构不同,请调整路径以匹配你的实际情况。

5. 安装依赖
  • 回到 my-project-vue 和 ``my-project-vue-ts两个项目的根目录下,运行pnpm i` 命令来安装依赖。
6. 在项目中引入公共样式
  • my-project-vuemy-project-vue-ts 两个项目的入口文件中引入公共样式。
import { createApp } from "vue";
import "./style.css";
import "common-styles/index.css";
import App from "./App.vue";

createApp(App).mount("#app");

注意:由于使用的相对路径依赖,并且没有将 common-styles 发布到 npm 仓库,因此不需要(也不能)在 node_modules 中找到它。Vite 或你的构建工具应该能够解析这个本地依赖并正确地引入样式文件。

7. 使用全局样式
  • 现在,你可以在 my-project-vuemy-project-vue-ts 两个项目的任何地方使用 common-styles 中定义的样式了。

1297b243-da76-4386-8651-75e42f2c928f.png

【CodeBuddy】三分钟开发一个实用小功能之:万花筒图案生成器

作者 Homi
2025年5月21日 10:12

前言:与AI的对话,从需求开始

想象一下,当你有一个创意想法时,如何将它转化为现实?在传统的开发过程中,你需要手动编写每一行代码,调试每一个细节。然而,现在有了CodeBuddy,这种体验变得截然不同。

假设你想要一个互动式的万花筒图案生成器,用户可以通过滑块调整对称数量、动画速度,甚至随机化颜色。你可以简单地告诉CodeBuddy:“我需要一个交互式万花筒应用,支持鼠标和触摸操作,并且可以动态改变颜色和对称性。”短短几分钟内,CodeBuddy就能为你生成完整的前端代码,涵盖HTML结构、CSS样式以及JavaScript逻辑。

这不仅仅是代码生成,而是一次与智能助手的深度沟通——你的需求被准确理解,创意被迅速实现,无需繁琐的手动编码。


以下是实际操作中的开发界面与最终呈现效果(文末附完整代码):


应用场景:互动艺术与教育工具

这个由CodeBuddy生成的万花筒图案生成器,不仅是一个视觉艺术品,也是一个极具潜力的互动工具:

  • 数字艺术创作:艺术家可以利用该程序作为灵感来源,通过不同的对称模式和色彩组合探索新的视觉效果。
  • 教育辅助工具:教师可以用它讲解几何对称、颜色理论、动画原理等概念,学生通过直观的操作加深理解。
  • 网页互动元素:作为一个轻量级的Canvas应用,它可以轻松嵌入到网站中,提升用户体验。
  • 儿童娱乐与创造力培养:孩子可以通过拖动和绘制发现图案变化的乐趣,激发他们的想象力。

这一切都得益于CodeBuddy对需求的理解能力,它不仅能生成功能完备的代码,还能确保良好的用户体验和跨设备兼容性(如支持鼠标和触摸事件)。


核心功能:AI驱动的智能编码能力

CodeBuddy展现出的强大功能,让它在众多编程辅助工具中脱颖而出:

1. 自然语言理解与代码生成

只需描述功能需求,CodeBuddy即可生成结构清晰、模块化的代码。例如:

  • Canvas渲染与响应式布局
  • 颜色渐变与HSL调色系统
  • 动画循环与自动旋转机制
  • 多点触控支持

这些复杂功能在没有AI帮助的情况下可能需要数小时甚至更长时间才能完成,而CodeBuddy将其压缩为几秒钟的过程。

2. 交互设计与用户体验优化

除了基础功能外,CodeBuddy还考虑了用户交互体验:

  • 控件布局美观且响应式
  • 滑块实时更新数值显示
  • 自动淡出背景保持画面流动感
  • 按钮反馈动画增强交互感

3. 可扩展性与维护友好

生成的代码具有良好的模块结构,便于后续修改和扩展。比如添加新功能(如导出图片、保存配置)或接入后端服务都非常方便。


未来优化方向:让AI更懂“人”

尽管CodeBuddy已经非常强大,但它仍有进一步进化空间:

1. 个性化风格适配

当前版本提供了一套默认UI样式,未来可以根据用户的审美偏好自动生成主题风格(如极简风、复古风、科技风),甚至集成流行的设计系统(如Tailwind CSS、Material Design)。

2. 智能性能优化

AI可以在生成代码时自动评估性能瓶颈,比如Canvas重绘频率、内存占用、动画帧率控制等,并给出优化建议或直接生成优化后的版本。

3. 多语言/框架支持

目前主要面向Web前端,未来可拓展至移动端(React Native)、桌面端(Electron)、游戏引擎(Unity)等领域,满足更广泛的应用需求。

4. 错误预防与容错机制

在生成代码前进行逻辑检查,避免常见错误(如未定义变量、类型不匹配),并自动生成单元测试或边界条件处理代码。


总结感悟:AI不是取代,而是赋能

CodeBuddy的出现,并不是为了取代程序员,而是为了让我们从重复劳动中解放出来,专注于更高层次的创意和架构设计。它像一位沉默却高效的搭档,默默承担起基础搭建工作,让我们得以专注于创新与优化。

在这个案例中,我们看到一个复杂的交互式图形应用被快速构建,背后是AI对需求的精准理解与技术实现的无缝衔接。CodeBuddy不仅提升了开发效率,也降低了编程门槛,让更多非专业开发者也能轻松实现自己的创意。

附:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>万花筒图案生成器</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <canvas id="kaleidoscope"></canvas>
        <div class="controls">
            <div class="control-group">
                <label for="symmetry">对称数量:</label>
                <input type="range" id="symmetry" min="3" max="12" value="6">
                <span id="symmetry-value">6</span>
            </div>
            <div class="control-group">
                <label for="speed">动画速度:</label>
                <input type="range" id="speed" min="1" max="10" value="5">
            </div>
            <div class="control-group">
                <button id="randomize">随机颜色</button>
                <button id="reset">重置</button>
            </div>
        </div>
    </div>
    <script src="main.js"></script>
</body>
</html>

style.css

:root {
    --primary-color: #6a11cb;
    --secondary-color: #2575fc;
    --dark-bg: linear-gradient(to right, #0f2027, #203a43, #2c5364);
    --control-bg: rgba(255, 255, 255, 0.1);
    --text-color: rgba(255, 255, 255, 0.8);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Arial', sans-serif;
    background: var(--dark-bg);
    color: var(--text-color);
    height: 100vh;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
}

.container {
    width: 95%;
    max-width: 1000px;
    height: 95vh;
    display: flex;
    flex-direction: column;
    gap: 20px;
}

#kaleidoscope {
    flex: 1;
    width: 100%;
    border-radius: 10px;
    box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
    background-color: rgba(0, 0, 0, 0.2);
}

.controls {
    background: var(--control-bg);
    backdrop-filter: blur(5px);
    padding: 15px;
    border-radius: 10px;
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    justify-content: center;
}

.control-group {
    display: flex;
    align-items: center;
    gap: 10px;
}

label {
    font-size: 14px;
    font-weight: bold;
}

input[type="range"] {
    width: 100px;
    height: 5px;
    -webkit-appearance: none;
    background: rgba(255, 255, 255, 0.2);
    border-radius: 5px;
    outline: none;
}

input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background: var(--secondary-color);
    cursor: pointer;
}

button {
    padding: 8px 15px;
    border: none;
    border-radius: 5px;
    background: var(--primary-color);
    color: white;
    font-weight: bold;
    cursor: pointer;
    transition: all 0.3s;
}

button:hover {
    background: var(--secondary-color);
    transform: translateY(-2px);
}

@media (max-width: 600px) {
    .controls {
        flex-direction: column;
        align-items: stretch;
    }
    
    .control-group {
        flex-direction: column;
        align-items: flex-start;
    }
}

script.js

// 初始化Canvas和上下文
const canvas = document.getElementById('kaleidoscope');
const ctx = canvas.getContext('2d');
let width, height;

// 控制元素
const symmetrySlider = document.getElementById('symmetry');
const symmetryValue = document.getElementById('symmetry-value');
const speedSlider = document.getElementById('speed');
const randomizeBtn = document.getElementById('randomize');
const resetBtn = document.getElementById('reset');

// 状态变量
let segments = 6;
let speed = 5;
let colors = [];
let angle = 0;
let isDrawing = false;
let lastPoint = { x: 0, y: 0 };
let hue = 0;

// 初始化函数
function init() {
    resizeCanvas();
    generateRandomColors();
    setupEventListeners();
    animate();
}

// 调整Canvas大小
function resizeCanvas() {
    width = canvas.width = canvas.offsetWidth;
    height = canvas.height = canvas.offsetHeight;
    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
    ctx.fillRect(0, 0, width, height);
}

// 生成随机颜色
function generateRandomColors() {
    colors = [];
    const baseHue = Math.random() * 360;
    for (let i = 0; i < 3; i++) {
        colors.push(`hsl(${(baseHue + i * 120) % 360}, 80%, 60%)`);
    }
}

// 设置事件监听器
function setupEventListeners() {
    window.addEventListener('resize', resizeCanvas);
    
    canvas.addEventListener('mousedown', (e) => {
        isDrawing = true;
        lastPoint = getCanvasPoint(e);
    });
    
    canvas.addEventListener('mousemove', (e) => {
        if (!isDrawing) return;
        const currentPoint = getCanvasPoint(e);
        drawSegment(lastPoint, currentPoint);
        lastPoint = currentPoint;
    });
    
    canvas.addEventListener('mouseup', () => isDrawing = false);
    canvas.addEventListener('mouseout', () => isDrawing = false);
    
    // 触摸支持
    canvas.addEventListener('touchstart', (e) => {
        e.preventDefault();
        isDrawing = true;
        lastPoint = getCanvasPoint(e.touches[0]);
    });
    
    canvas.addEventListener('touchmove', (e) => {
        e.preventDefault();
        if (!isDrawing) return;
        const currentPoint = getCanvasPoint(e.touches[0]);
        drawSegment(lastPoint, currentPoint);
        lastPoint = currentPoint;
    });
    
    canvas.addEventListener('touchend', () => isDrawing = false);
    
    // 控制面板事件
    symmetrySlider.addEventListener('input', () => {
        segments = parseInt(symmetrySlider.value);
        symmetryValue.textContent = segments;
    });
    
    speedSlider.addEventListener('input', () => {
        speed = parseInt(speedSlider.value);
    });
    
    randomizeBtn.addEventListener('click', generateRandomColors);
    
    resetBtn.addEventListener('click', () => {
        ctx.fillStyle = 'rgba(0, 0, 0, 1)';
        ctx.fillRect(0, 0, width, height);
    });
}

// 获取Canvas坐标点
function getCanvasPoint(e) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
    };
}

// 绘制对称线段
function drawSegment(start, end) {
    const segmentAngle = (Math.PI * 2) / segments;
    const center = { x: width / 2, y: height / 2 };
    
    ctx.lineWidth = 2;
    ctx.lineCap = 'round';
    
    for (let i = 0; i < segments; i++) {
        const currentAngle = segmentAngle * i;
        
        // 旋转起点
        const rotatedStart = rotatePoint(start, center, currentAngle);
        const rotatedEnd = rotatePoint(end, center, currentAngle);
        
        // 绘制线段
        const gradient = ctx.createLinearGradient(
            rotatedStart.x, rotatedStart.y, 
            rotatedEnd.x, rotatedEnd.y
        );
        
        gradient.addColorStop(0, colors[0]);
        gradient.addColorStop(0.5, colors[1]);
        gradient.addColorStop(1, colors[2]);
        
        ctx.strokeStyle = gradient;
        ctx.beginPath();
        ctx.moveTo(rotatedStart.x, rotatedStart.y);
        ctx.lineTo(rotatedEnd.x, rotatedEnd.y);
        ctx.stroke();
    }
}

// 旋转点
function rotatePoint(point, center, angle) {
    const x = point.x - center.x;
    const y = point.y - center.y;
    
    const rotatedX = x * Math.cos(angle) - y * Math.sin(angle);
    const rotatedY = x * Math.sin(angle) + y * Math.cos(angle);
    
    return {
        x: rotatedX + center.x,
        y: rotatedY + center.y
    };
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    
    // 淡出效果
    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
    ctx.fillRect(0, 0, width, height);
    
    // 自动旋转
    if (!isDrawing && speed > 0) {
        angle += 0.002 * speed;
        const center = { x: width / 2, y: height / 2 };
        const point = { x: center.x + 100, y: center.y };
        const rotatedPoint = rotatePoint(point, center, angle);
        
        hue = (hue + 0.5) % 360;
        colors = [
            `hsl(${hue}, 80%, 60%)`,
            `hsl(${(hue + 120) % 360}, 80%, 60%)`,
            `hsl(${(hue + 240) % 360}, 80%, 60%)`
        ];
        
        drawSegment(center, rotatedPoint);
    }
}

// 启动应用
init();



🌟 让技术经验流动起来

▌▍▎▏ 你的每个互动都在为技术社区蓄能 ▏▎▍▌
点赞 → 让优质经验被更多人看见
📥 收藏 → 构建你的专属知识库
🔄 转发 → 与技术伙伴共享避坑指南

点赞 ➕ 收藏 ➕ 转发,助力更多小伙伴一起成长!💪

💌 深度连接
点击 「头像」→「+关注」
每周解锁:
🔥 一线架构实录 | 💡 故障排查手册 | 🚀 效能提升秘籍

❌
❌