普通视图

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

弃用html2pdf.js,这个html转pdf方案能力是它的几十倍

作者 刘发财
2026年3月3日 02:06

欢迎转载文章

在前端开发中,“把网页变成 PDF”是个老生常谈的需求。无论是生成发票、报告还是简历,用户总希望点一下按钮就能带走一份格式完美的文档。 目前主流的前端html转pdf方案是通过html2canvas将网页渲染成canvas,再通过jsPDF将canvas转换为pdf。代表方案就是 html2pdf.js,npm包周下载量达到了80万,为广大开发者所接受。但是因为它基于html2canvas和jsPDF,会有一些无法解决的问题,比如:

  • 生成速度慢
  • 生成的pdf文件体积大
  • 生成的pdf内容会模糊,打印时无法达到清晰度要求
  • 文字无法被搜索,选中,编辑,因为它生成的pdf是图片式的,而非矢量pdf

而现在,有一种全新的解决思路,完美的解决了这些问题,那就是作者开源的前端pdf生成库dompdf.js,具体的实现和说明可以查看我上一篇文章 https://juejin.cn/post/7583912637470769203

在线体验

dompdfjs.lisky.com.cn

Git 仓库地址 (欢迎 Star⭐⭐⭐)

github.com/lmn1919/dom…

gitee.com/liu-facai/d…

dompdf.js的大致原理

1.解析 html 页面,生成一个包含节点位置信息,样式,层级,内容等信息的 DOM 树。

2.递归 DOM 树,根据节点据顶部的高度和生成页面规格的高度,将节点分配到不同的页面。

3.调用 jspdf.js 的 api,将节点绘制到 PDF 文件上。

可以看出,dompdf.js 跳过了html转图片的步骤,直接将 DOM 树转换为矢量 PDF 文件,避免了图片转换导致的模糊问题,同时也解决了文字无法被搜索,选中,编辑的问题。

下面,我们从pdf生成速度,生成质量,生成数量等方面对两种方案进行对比

测试的内容为生成包含文本和表格的pdf文件

1.文件生成速度对比

同样的内容,dompdf.js 生成速度更快,耗时基本上只有 html2pdf.js 的 1/2。

微信截图_20260303012415.png

2.文件体积对比

dompdf.js 生成的 pdf 文件体积更小,同样的内容页数,dompdf.js 生成的 pdf 文件体积是 html2pdf.js 的 1/5左右。

微信截图_20260303012435.png

3.清晰度对比

在放大到500%后,html2pdf.js 生成的 pdf 文字会出现明显的锯齿,而 dompdf.js 生成的 pdf 文字则完全没有压力。

html2pdf.png

html2pdf.js生成的pdf文件,放大后会有锯齿

微信截图_20260303013333.png

dompdf.js生成的矢量文件,不会出现模糊的情况

4.生成数量对比

html2pdf.js在30页左右,由于canvas高度限制,就会出现空白页,而 dompdf.js 轻松可以生成数百上千页的pdf。

微信截图_20260303014415.png

html2pdf.js生成的pdf文件,内容过多会出现空白页

微信截图_20260303015837.png

dompdf.js轻松可以生成数百上千页pdf

总结

通过上述对比可以看出,dompdf.js 在各项指标上都完胜传统的 html2pdf.js 方案。它不仅解决了 html2canvas 带来的模糊、体积大、无法选中文字等痛点,还大幅提升了生成速度和页面承载能力。

对于需要高质量、可编辑、且对性能有要求的前端 PDF 生成场景,dompdf.js 无疑是目前更优的选择。

如果你也被前端生成 PDF 的各种坑所困扰,不妨试一试这个库,希望能够帮助到你!

别忘了去 GitHub 点个 Star 支持一下作者哦!⭐⭐⭐

GitHub: github.com/lmn1919/dom…

2026年大模型怎么选?前端人实用对比

作者 牛奶
2026年3月2日 23:57

2026年大模型怎么选?前端人实用对比

这是系列第三篇。02篇我们聊完基础概念,这篇来看看怎么选对大模型和开发工具。


你有没有过这样的经历?

打开一个AI编程工具,纠结半天该选哪个模型。有人说Claude最强,有人说GPT好用,还有人说免费的DeepSeek足够用了。你花了半小时研究,最后还是随便选了一个。

结果用起来才发现:这个模型写代码总是漏这漏那,那个模型响应太慢,还有一个模型连中文都理解不好。

如果你有这样的经历,说明你和我一样,曾经被困在「选择困难」里。

这篇文章,我帮你把这件事彻底讲清楚。


原文地址

墨渊书肆/2026年大模型怎么选?前端人实用对比


先说结论

不想看长文的记住这几点:

  • 免费首选:Trae国内版(完全免费,Claude 3.5)
  • 想要最强:Cursor Pro($20/月,Claude Opus 4.6)
  • 性价比之选:Windsurf($15/月,Claude Sonnet)
  • 国产之光:智谱GLM-5(开源最强,逼近Claude)
  • 开源白嫖:OpenCode + 免费API

核心问题:模型到底差在哪?

选工具之前,先搞明白一个问题:这些模型都能写代码,到底差在哪?

根据2026年2月Coding Arena的真实投票数据(17万+开发者票选),核心差异就三点:

复杂任务处理能力

面对一个模糊的需求,顶级模型会先问清楚再做,差的模型会直接开写,然后写错。

面对跨文件重构,顶级模型能理解整个代码库的结构,差的模型只能看到当前文件。

举一个我自己的例子:

有一次我要重构一个React项目的老组件,大概3000行代码。我分别用了三个模型:

  • Claude Opus 4.5:先问我「这个组件的数据流是什么」「有没有单元测试」「目标是用Class还是Function」,然后才开始写
  • GPT-5.1:直接开始写,写到一半发现数据结构不对,又从头改了一遍
  • DeepSeek V3.2:写倒是能写,但细节处理不完善,后面我自己修了半小时

这就是差距。复杂任务面前,顶级模型不是在「写代码」,而是在「解决问题」。

思考深度

Thinking模式(推理模式)比普通模式平均强5-10%。

但Thinking模式响应慢3-8秒。

简单任务不需要Thinking,复杂任务必须开。

我的经验是:

  • 写个简单函数、开个API接口 → 普通模式就够了
  • 面对复杂需求、跨文件重构、疑难Bug → 必须开Thinking

上下文理解

有的模型看 3000 行代码就开始「失忆」,给你的代码前后矛盾。

有的模型能理解200K token,整个项目丢进去都不是问题。

前端项目越大,上下文能力越重要。特别是你要让AI帮你理解一个老项目的时候。


2026年模型排名(基于Coding Arena)

这是2026年2月的真实排名,17万开发者投票得出。数据来源:Arena.ai

第一梯队:最强王者

排名 模型 得分 适合场景
1 Claude Opus 4.6 1560 通用最强,新版无需Thinking
2 Claude Opus 4.6 Thinking 1553 架构设计、复杂重构
3 Claude Sonnet 4.6 1531 性价比最高的顶级模型

为什么强:这三兄弟是 Anthropic 家的,特点是「想清楚了再写代码」。当你面对一个复杂需求,它们会先分析问题、考虑边界情况、规划实现方案,然后才动手。

第二梯队:实用之选

排名 模型 得分 适合场景
5 GPT 5.1 High 1471 快速原型、速度优先
7 Gemini 3.1 Pro PreView 1461 多语言切换、前后端通吃
8 GLM-5 1451 开源最强,200K上下文

为什么实用:GPT 5.1 High在速度上有优势,适合快速迭代;Gemini 3.1 Pro在多语言支持上表现出色,适合全栈开发者;GLM-5虽然是国产模型,但表现已经逼近国际顶级水平,特别是在中文场景下。

第三梯队:国产新势力

排名 模型 得分 适合场景
12 kimi k2.5 thinking 1436 长文本处理、中文对话、文档分析
13 minimax m2.5 1436 多模态理解、长文本总结
17 qwen3.5 1396 阿里生态、中文优化、高性价比

为什么值得关注:国产模型正在快速追赶国际顶级水平。Kimi在长上下文和多轮对话上有优势,MiniMax在多模态领域表现出色,Qwen3.5背靠阿里云生态,性价比极高。对于国内开发者,这三个模型是很好的替代选择,特别是中文场景下体验不输国际大厂。


开发工具到底选哪个?

对于前端开发者,工具比模型更重要。因为工具已经把模型封装好了,还加了文件管理、终端操作这些功能。

1. Cursor(推荐给不差钱的)

价格:$20/月

包含模型:Claude Opus 4.6 + GPT-5.1 High + Gemini 3.1 系列

优点

  • 目前集成度最高的AI IDE
  • Tab补全、Ctrl+I提问、Ctrl+K改代码,三种模式无缝切换
  • Agent模式可以自己跑命令、改文件
  • 理解项目结构,能跨文件分析

缺点

  • 贵,$20/月
  • 国内访问不稳定

适合:预算充足,追求最强体验

我的建议:如果你只能选一个,选 Cursor。它的体验是目前最好的,特别是Agent模式,真的能帮你减少很多机械劳动。


2. Trae(国内免费首选)

价格:国内版完全免费

包含模型:Claude 3.5 Sonnet + 豆包

亮点功能

  • 国内直达:无需翻墙,直接访问
  • 中文优化:对中文Prompt理解更准确
  • 智能补全:类似Cursor的Tab补全
  • Agent模式:支持自动执行开发任务

优点

  • 免费!国内直达,不用翻墙
  • 中文体验最好
  • Claude 3.5 Sonnet足够强
  • 界面简洁,上手快

缺点

  • 相比 Cursor,集成度稍低
  • Agent 能力不如 Cursor
  • 插件生态不如 Cursor 丰富

适合:国内用户,预算0元,日常开发

我的建议:国内开发者的福音。免费且够用,夫复何求?如果你之前没用过AI编程工具,从Trae开始是最省心的选择。


3. Windsurf(性价比之选)

价格:$15/月

包含模型:Claude Sonnet系列

亮点功能

  • Flow模式:类似Cursor的Agent模式,可以自动执行多步骤任务
  • Cascade:新一代AI编程架构,任务拆解能力更强
  • 上下文保持:长时间对话中保持项目上下文

优点

  • 比Cursor便宜$5
  • 能力接近Cursor
  • Flow模式也能自动执行任务
  • 对Mac/Windows/Linux支持都很完善

缺点

  • 略逊于Cursor(主要在Agent的智能化程度)
  • 生态没Cursor成熟(插件少一些)
  • 中文优化不如Trae

适合:预算有限,但想要好体验

我的建议:如果$20觉得贵,Windsurf是完美的替代品。能力足够,价钱友好。特别是Cascade模式发布后,整体体验提升明显。


4. Google Antigravity(AI原生开发平台)

价格:免费(目前)

包含模型:Gemini 3 Pro / Flash

亮点功能

  • Agent Manager:可以同时管理多个AI Agent协同工作
  • 浏览器自动化:支持浏览器内的自动化任务执行
  • Workspace概念:支持创建多个独立的工作空间
  • Google生态集成:深度整合Google Cloud和开发工具链

优点

  • Google原生,AI Agent能力强大
  • Agent Manager可以同时管理多个AI协同工作
  • 支持浏览器内自动化任务
  • 免费!目前对开发者免费开放
  • Gemini 3在多模态理解上优势明显

缺点

  • 2025年11月才发布,还比较新
  • 生态还在建设中(插件少、功能在快速迭代)
  • 国内访问可能不稳定

适合:喜欢Google生态,想尝试最新AI编程方式的开发者

我的建议:这是Google在AI编程领域的大招。虽然还年轻,但Google的投入力度很大,未来值得关注。特别是它的「Agent Manager」概念很有意思——你可以同时让多个AI帮你干活。如果你是Google全家桶用户,强烈建议试试。


5. OpenCode(开源白嫖)

价格:完全免费(开源项目)

支持模型:75+模型,包括Claude、GPT、Gemini、DeepSeek、MiniMax M2.5

亮点功能

  • MCP扩展:支持Model Context Protocol,可以扩展各种功能
  • 灵活配置:可以自定义模型参数、API端点
  • 隐私优先:所有数据本地处理,不上传云端
  • 多模型切换:同一个对话中随时切换不同模型

优点

  • 完全免费
  • 灵活,想用啥模型用啥模型
  • 隐私优先,数据本地处理
  • 支持MCP扩展
  • 社区活跃,插件丰富
  • 支持MiniMax M2.5免费模型,国内访问稳定

缺点

  • 终端操作,有学习成本
  • 没有图形界面(纯命令行)
  • 需要自己配置API Key
  • 没有内置的代码编辑器功能

适合:开发者,有技术背景,想自己掌控

使用技巧

  • 配合VS Code的Dev Container使用效果更好
  • 推荐使用MiniMax M2.5免费模型,国内直达,无需翻墙
  • 适合需要高度定制化的专业开发者

我的建议:如果你愿意折腾,OpenCode + MiniMax M2.5是性价比最高的组合。完全免费,工具免费+模型免费,夫复何求?适合有一定技术基础、喜欢折腾的开发者。


6. Z Code(智谱官方)

价格:免费/付费

包含模型:GLM-5系列

亮点功能

  • AutoDev模式:自动完成整个开发流程(写代码→执行→测试→提交)
  • 200K超长上下文:可以一次性理解整个大型项目
  • 多模态支持:支持图片、代码、文档等多种输入形式
  • 国产化部署:支持私有化部署,适合企业用户

优点

  • 智谱官方,GLM-5体验最完整
  • 自动完成整个开发流程(写代码、执行、测试、提交)
  • 200K超长上下文
  • 中文理解能力极强
  • 国内访问稳定

缺点

  • 刚发布,生态还在建设中
  • 插件和第三方集成不如Cursor丰富
  • Agent能力还在持续优化中

适合:想体验国产最强模型、喜欢尝鲜的开发者

我的建议:GLM-5确实强,但配套工具还需要时间完善。适合想支持国产的朋友。特别是200K上下文对于大型项目非常友好,如果你需要处理大型老项目,Z Code值得一试。


预算方案推荐

预算0元:Trae + OpenCode

  • 日常开发:Trae国内版
  • 查问题:OpenCode + MiniMax M2.5免费模型
  • 尝鲜:Z Code(GLM-5)

效果:80%的日常开发够用,国产模型崛起


预算15元/月:Windsurf Pro

  • 工具:Windsurf Pro($15/月)
  • 模型:Claude Sonnet

效果:比Cursor便宜,能力足够


预算20元/月:Cursor Pro

  • 工具:Cursor Pro($20/月)
  • 模型:Claude Opus 4.6

效果:目前前端开发最强组合


想要国产最强:Z Code + GLM-5

  • 工具:Z Code(免费)
  • 模型:GLM-5(开源最强)

效果:支持国产,能力逼近Claude


我的建议

  1. 先用起来:别纠结,Trae直接下载先用
  2. 从免费开始:觉得不够再升级
  3. 按需付费:每个工具都有免费额度,先试试
  4. 组合使用:不同场景用不同工具
  5. 关注国产:GLM-5的崛起值得关注

写在最后

AI工具更新快,这篇写的是2026年2月的格局。

有一点特别想说的是:这两年国产模型的进步速度超出了所有人的预期。从2024年的「能用」,到2025年的「够用」,再到2026年的「逼近最强」——GLM-5、Kimi K2.5这些国产模型正在快速追赶。

作为前端开发者,这是最好的时代。我们有更多的选择,也有更大的空间。

下篇我们聊《Prompt怎么写才有效》——同样工具不同人用,效果差十倍。

感兴趣下篇见。

前端人为什么要学AI?

作者 牛奶
2026年3月2日 23:50

前端人为什么要学AI?

系列开篇,写给想要真正掌握未来的前端开发者。


你有没有过这样的经历?

写一个登录表单,花了半小时调样式。产品说交互要改一下,你又花了半小时。类似的功能做了无数遍,感觉自己就是个「Ctrl+C / Ctrl+V」工程师。

遇到一个复杂的正则表达式或者是算法题,对着Google搜了半小时,结果复制过来的代码自己都看不懂,最后只能硬着头皮问同事。

接手别人的代码,看着满满一屏幕的useEffectuseState,完全不知道数据是怎么流的,想改又不敢改。

如果你有过类似的经历,说明你和我一样,曾经被困在某种「技术舒适区」里。

前端会React,会写样式,会调API,但面对一些「重复性的工作」和「棘手的问题」,总是要花大量时间。

我想聊聊这件事。


原文地址

墨渊书肆/前端人为什么要学AI?


前端这件事,也被误解了很多年

一提到「前端工程师」,很多人脑海里浮现的是这样一个形象:每天跟样式打交道,调调组件,写写页面,看起来没什么技术含量。

这种理解,该过时了。

现在的Web应用越来越复杂。前端不再只是「画界面」,而是要处理复杂的交互、状态管理、性能优化、工程化建设。ReactVueNext.js......框架越来越强大,需要学的越来越多。

但问题是:

  • 前端的工作边界在扩大:以前只管页面,现在要做SSR、做SEO、做动画、做可视化......一个人要学的东西越来越多
  • 重复劳动越来越多:同样的组件改改参数就是一个新的,同样的交互换换逻辑又要重新写
  • 沟通成本越来越高:和产品经理、设计师、后端工程师来来回回确认需求,代码反而没写多少

我们变成了「高级CV工程师」——不是Copy Vector,是Copy and Paste。

这不是前端的问题,这是整个行业的痛点。


AI来了,情况不一样了

2023年开始,AI的爆发让一切变得不同。

以前我们需要自己写的代码,现在AI可以帮我们写。以前我们需要自己查的文档,现在AI可以直接读给我们听。以前我们需要自己调试的bug,现在AI可以直接帮我们定位。

但我发现一个有趣的现象:很多前端开发者对AI的态度是两个极端——

要么觉得AI没用,「生成的代码一堆bug还得我自己改」;要么觉得AI太厉害,「迟早要取代我」。

这两种观点,都不对。

AI不会取代前端,但它会重新定义「前端」这个岗位。

就像计算器没有取代数学家,但数学家必须会用计算器。AI工具不会取代前端开发者,但前端开发者必须会用AI。


AI到底能帮前端做什么?

说几个我自己的真实经历。

1. 写代码更快了

以前我要写一个日期选择器组件,从头写到尾要半小时。现在我告诉AI我的需求——「需要一个支持范围选择、禁用特定日期、暗色模式的主题适配」——它能给我一个可以直接用的版本,我只需要根据业务需求微调。

这不是「替代」,是「放大」。我原本半小时只能做一个组件,现在十分钟做出来,剩下二十分钟可以去喝杯咖啡。

2. 读代码更快了

接手别人的项目,最头疼的就是看不懂代码。现在我可以直接把代码丢给AI,让它帮我解释:「这个组件的数据流是怎么走的?为什么要用useMemo?」

它不仅能解释代码,还能帮我梳理逻辑,告诉我哪里可能有性能问题。

3. 查文档更快了

以前遇到问题,我先去Google搜,然后看Stack Overflow,最后实在不行才去翻文档。

现在我直接问AI:「Next.js 15怎么做密码重置?」它能直接给我答案,虽然不一定完全准确,但足够让我快速上手。

4. 做项目更有底气了

以前做一个带AI功能的项目,光是调研要用什么API、怎么接入、怎么管理上下文,就能劝退一半的人。

现在这些都有现成的方案。Vercel AI SDK几分钟就能搭一个聊天界面,LangChain帮我管理AI的工作流,我只需要专注于业务逻辑。


但AI不是万能的

我知道有人要问了:AI这么厉害,那我们还学什么?

这是个好问题。

我用了一年多AI辅助开发,发现它有几个明显的短板:

  • 第一,AI不懂你的业务

你告诉AI「帮我写个用户列表」,它能给你写。但你的产品里用户列表要显示会员等级、要按活跃时间排序、要支持导出Excel——这些AI不知道。

你得自己把需求翻译成AI能理解的形式。

  • 第二,AI会犯错

AI生成的代码有bug是常态,不是例外。它能帮你写70%的代码,剩下30%你得自己改、自己调。

如果你没有判断代码对不对的能力,AI帮你的可能还没有坑你的多。

  • 第三,AI不知道什么是「好」

代码能跑和代码好是两回事。AI可以写出能跑的代码,但不一定符合性能要求、安全规范、可维护性标准。

这些都需要你有一定的技术判断力。

所以,AI时代更需要学习,只是学习的内容变了。

以前我们学的是「怎么实现」,以后我们学的是「怎么整合」。

以前我们学的是「这个API怎么用」,以后我们学的是「这个需求怎么拆」。

以前我们学的是「怎么写代码」,以后我们学的是「怎么用AI写代码」。


前端学AI,有什么优势?

说了这么多,你可能会问:为什么是前端先学AI?而不是后端、不是移动端?

我的答案是:前端天然离用户最近,天然是AI落地的最佳场景。

你想做一个智能助手,第一个要做的就是界面。一个聊天窗口、一个语音按钮、一个输入框——这些是前端最擅长的。

你想做一个AI生成图片的应用,第一个要做的还是界面。用户上传图片、选择风格、预览结果——这些也是前端最擅长的。

而且前端开发者有几个天然优势:

  • 对交互敏感:我们知道什么是好的用户体验AI生成的内容需要什么样的交互来呈现
  • 对视觉敏感:我们知道怎么把AI生成的内容美化、适配不同的屏幕
  • 对技术敏感:我们天天跟API打交道,接入AI服务对我们来说轻车熟路

2026年了,如果前端还只把自己定位在「画界面」,那确实危险。但如果前端把自己定位在「用户与AI的桥梁」,那前景无限。


这个系列想带你做什么

市面上不缺AI教程。Prompt工程大模型原理LangChain实战——这种内容一搜一大把。

但我发现很多前端开发者看完这些教程,还是不知道怎么做。

因为大部分教程要么太偏理论(全是数学公式),要么太偏后端(全是Python代码),跟前端开发者的日常工作没关系。

这个系列我想带你做的事情很简单:从零开始,让AI真正成为你的开发助手。

不是demo,不是练习,而是真实的、能用到日常工作中的技能。

我会分成这几个阶段:

  • 阶段零:认知重建

    先理解AI到底能帮我们什么(就是这篇)。

  • 阶段一:Prompt工程与AI应用基础

    真正开始用AI工具。学怎么写有效的Prompt,怎么让AI帮我们写代码、查文档、修bug。

  • 阶段二:AI功能接入与网页开发

    开始做项目。把AI功能接入到自己的网页里,做出能展示的Demo。

  • 阶段三:AI原理与进阶应用

    从「会用」到「理解」。不求能自己训练模型,但求知道AI为什么有时候聪明有时候犯傻。

  • 阶段四:本地部署与生产实践

    接近实际生产。LangChain、本地模型、浏览器端运行——怎么让AI不依赖云服务也能跑。

在这个过程中,你会看到我踩过的坑,做过的错误决策,总结出的经验。我不是为了告诉你「这个技术怎么用」,而是告诉你「这个AI能力该怎么学」。


写在最后

回到开头的问题。

你是不是经常感觉写了很多代码,但真正用到的时候还是那些老东西?

这很正常。

技术本身不是目的,解决问题才是。

2026年了,AI可以帮你写代码,但不能帮你判断什么是好的代码。能做到这一点的人,永远有市场。

而这,就是我们这个系列要一起做的事情。

下一篇文章,我会讲讲《AI辅助开发的基础概念》介绍一些向量、Token、大模型的基本概念,以及前端视角怎么理解这些概念。

感兴趣的话,下一篇见。

昨天 — 2026年3月2日技术

🎉OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用!

2026年3月2日 21:25

AI Agent 时代,人们已经不满足只是与 AI 进行问答交互,而是希望 AI 能直接帮人干活。

目前 AI 帮人干活的场景越来越丰富,最常见的就是 AI 帮人写代码、做视频、做 PPT、做设计稿。

你有没有想过 AI 能帮人操作网页?

这就是 OpenTiny NEXT-SDK 做的事情。

1 简介

OpenTiny NEXT‑SDK 是一套面向前端智能应用的开发工具包,核心是基于 MCP(Model Context Protocol) 协议,让前端应用快速接入 AI Agent,实现前端界面可被智能体直接操控的能力。

OpenTiny NEXT‑SDK 可以帮助开发者:

  • 把普通前端应用快速改造为 MCP Server,对外暴露界面操作能力

  • 让 AI Agent(WebAgent)通过标准 MCP 协议读取界面、调用功能、执行操作

  • 快速集成 AI 对话组件(如 TinyRobot),构建智能交互前端

2 项目优势

NEXT‑SDK 基于 MCP 协议实现,将 MCP 的能力扩展到了 Web 端,让 Web 应用也能被 AI 操控,以下是项目优势:

  • 扩大 MCP 工具范围:为 Agent 智能体提供更多的 MCP 工具,实现当前现有的本地/云服务 MCP 工具所不具备的能力,即操控前端应用的能力。这种能力比 RPA 方案(Browser Use / Computer Use)更快(可通过后面的演示视频感受 AI 操作的效率)、更准更经济(消耗更少 Token)

  • 完全兼容 MCP 生态:所有的前端应用都采用标准的 MCP 协议声明 MCP Server,并且基于标准的 MCP 通讯方式进行连接,比如 Streamable HTTP,意味着能完全融入现有的 MCP 生态,兼容现有乃至未来的 MCP Host 应用

  • 支持智能体交互范式:当前的前端应用主要还是人机交互,即人手动操作前端界面上的 UI 组件。引入 OpenTiny NEXT-SDK 之后,Agent 智能体可以借助 MCP 工具读取前端界面的信息、调用前端界面的功能,配合生成式 UI 实现新的智能体交互范式

  • 多样的前端智能化方案:不仅支持 Web 应用的前端智能化改造,还全面覆盖 AI 应用(对话框)的多端部署场景——无论是浏览器扩展、Web 页面集成,还是各终端内置的 AI 助手,均可直接或间接调用前端应用中的 MCP 工具

3 演示动画

我们一起来看一个演示动画(无剪辑、无加速,AI 操作页面的真实速度),直观感受下 NEXT-SDK 的能力吧!

AI创建用户.gif

接入 NEXT-SDK 的前端应用,右下角会出现一个机器人图标,点击这个图标会从侧边弹出 AI 对话框,我们可以使用自然语言与 AI 对话,让 AI 帮我们操作前端应用。

比如我们可以输入以下内容:

帮我创建以下用户,用户信息如下:
邮箱:zhangsan@sina.com
密码:Abc123456
用户名:zhangsan

这时 AI 会调用页面中定义的名为 add-user 的 MCP 工具,帮我们创建 zhangsan 这个用户。

我们提供了一个 Playground 代码演练场,你可以在线体验 NEXT-SDK 的能力。

NEXT-SDK Playground:playground.opentiny.design/next-sdk

4 快速接入

使用 OpenTiny NEXT-SDK,只需要以下四步,就可以把你的前端应用变成智能应用。

第一步:安装依赖


npm install @opentiny/next-sdk

第二步:创建 MCP Client

在 Web 应用的主入口(比如:Vue 项目的 App.vue 文件)定义 WebMcpClient。


import { onMounted, provide } from 'vue'
import { WebMcpClient, createMessageChannelPairTransport } from '@opentiny/next-sdk'

onMounted(async () => {
  // 创建通信通道
  const [serverTransport, clientTransport] = createMessageChannelPairTransport()
  provide('serverTransport', serverTransport)

  // 创建 MCP Client
  const client = new WebMcpClient()
  await client.connect(clientTransport)
  // 这个 sessionId 是 Web 应用与 WebAgent 服务建立连接后,由 WebAgent 服务生成的,用来唯一标识被操控的 Web 应用(被控端)
  const { sessionId } = await client.connect({
    agent: true,
    url: 'https://agent.opentiny.design/api/v1/webmcp-trial/mcp'
  })
})

第三步:创建 MCP Server

在 Web 应用的子页面(比如:views/page1.vue)中定义 WebMcpServer,每个页面可以定义自己的 WebMcpServer,页面切换时,MCP Client 会与当前页面的 MCP Server 建立连接,并丢弃与之前页面的连接。


import { onMounted, inject } from 'vue'
import { WebMcpServer, z } from '@opentiny/next-sdk'

onMounted(async () => {
  const serverTransport = inject('serverTransport')
  // 创建 MCP Server
  const server = new WebMcpServer({
    name: 'mcp-server-page1',
    version: '1.0.0'
  })

  // 定义 MCP 工具
  server.registerTool(
    'demo-tool',
    {
      title: '演示工具',
      description: '一个简单工具',
      inputSchema: { foo: z.string() }
    },
    async (params) => {
      console.log('params:', params)
      return { content: [{ type: 'text', text: `收到: ${params.foo}` }] }
    }
  )

  await server.connect(serverTransport)
})

完成!现在你的前端应用已经变成智能应用,可以被 AI 操控了,你可以通过各类 MCP Host 来操控智能应用。

第四步:添加 AI 遥控器

我们提供了一个开箱即用的 AI 对话框组件,支持 PC 端和移动端,就像一个遥控器,可以通过对话方式操控你的前端应用。

安装遥控器组件:


npm install @opentiny/next-remoter

在 Vue 项目中使用:


<script setup lang="ts">
import { TinyRemoter } from '@opentiny/next-remoter'
import '@opentiny/next-remoter/dist/style.css'

// 使用第二步获取的 sessionId
const sessionId = 'your-session-id'
</script>

<template>
  <tiny-remoter 
    :session-id="sessionId" 
    title="我的智能助手"
  />
</template>

遥控器会在你的应用右下角显示一个图标,悬浮后可以选择:

  • 弹出 AI 对话框:在应用侧边打开 AI 对话界面

  • 显示二维码:手机扫码后打开移动端遥控器

不管是 PC 端还是移动端,都可以通过自然语言对话的方式让 AI 帮你操作应用,极大提升工作效率!

如果你想了解更多 NEXT-SDK 的用法,请参考 NEXT-SDK 官网文档:docs.opentiny.design/next-sdk

5 立即行动

在 AI 技术快速迭代的今天,前端智能化不再是“高端需求”,而是提升产品竞争力、提升操作效率的核心能力和必选项。

OpenTiny NEXT-SDK 让前端 AI 集成,从“复杂踩坑”到“5分钟上手”,让你的应用瞬间拥有 AI 能力,领跑行业智能化创新!

立即行动,解锁前端智能化新可能:

  • 执行 npm install @opentiny/next-sdk 安装 OpenTiny NEXT-SDK,5分钟上手实操,快速体验 AI 操控效果

  • 前往 OpenTiny NEXT-SDK 官网:opentiny.design/next-sdk,查看详细的项目介绍、API 文档和进阶用法

  • 访问 OpenTiny NEXT-SDK 代码演练场:playground.opentiny.design/next-sdk,在线体验 AI 自动操作前端应用

  • 添加 OpenTiny 微信小助手:opentiny-official,加入 OpenTiny 技术交流群,获取一对一集成指导,解决实操难题,与同行交流 AI 前端集成经验

如果你有任何问题,欢迎在评论区留言交流!

ArcGIS Pro 中的 notebook 初识

作者 GIS之路
2026年3月2日 20:13

^ 关注我,带你一起学GIS ^

notebook中文翻译为笔记本,既然是笔记本,那就具有添加、修改、删除、保存等功能。ArcGIS Pro中的 notebook其实也是这意思。

区别就是ArcGIS Notebooks是一个基于JupyterLab构建的开源 web 应用程序 ,可用于创建和共享包含实时 Python 代码、可视化效果和叙事文本的文档(名为 Notebooks)。

将 ArcGIS Notebooks 集成到 ArcGIS Pro 后,可以执行分析并在地理环境中立即查看结果,与新兴数据进行交互,记录并自动化工作流,以及将其保存以供稍后使用或共享。ArcGIS Notebooks 用途包括数据清理和转换、数值模拟、统计建模、计算机学习、管理任务等。

并且ArcGIS Pro 中的所有 Python 功能均可通过 ArcGIS Notebooks 使用,其中包括核心 Python 功能、Python 标准库、ArcPyArcGIS API for Python 以及ArcGIS Pro 所随附的众多第三方库,例如 NumPy 和 pandas

ArcGIS Pro 可以使用 ArcGIS Pro 包管理器通过开源库进行扩展。

当开源Jupyter NotebooksArcGIS Pro 应用程序中本地运行时,Esri集成 Jupyter Notebook 体验也可用于ArcGIS OnlineArcGIS Enterprise门户。

1. ArcGIS Notebooks 使用

1.1. 创建一个新的笔记本。

方式一:

点击插入选项卡,在工程窗口中选择New Notebook下拉菜单,然后点击New Notebook。或者存在保存过的笔记本的话,也可以通过Add and Open Notebook打开。

方式二:

点击分析选项卡,选择Python下拉菜单,点击Python Notebook

打开notebook笔记本窗口显示如下,由标题栏、工具栏和代码区组成,主要包括保存、新建、剪切、复制、运行等工具。

1.2. 运行 Python 代码

在单元格中输入代码后,点击三角形按钮运行代码。

也可以通过按住[CTRL+ENTER]运行选定行,代码显示如下。可通过在每一行后按 Enter 键,在单个单元格内添加多行代码。 如果您习惯于在 Python 窗口或 Python 编辑器的交互式窗口中运行代码,这可能会与您的习惯不符,因为在上述两个窗口中按 Enter 键的结果是运行代码行。

2. 查看ArcGIS Notebooks

已添加到工程中的ArcGIS Notebooks将在目录窗格的 Notebooks 文件夹下列出。 使用 ArcGIS Pro 创建的 Notebook 会自动添加到您的工程中。

要将现有的笔记本添加到工程中,请右键单击Notebooks文件夹,然后选择添加笔记本,或者单击插入功能区上添加笔记本按钮旁边的下拉箭头,然后选择添加笔记本。

3. 查看代码帮助

ArcGIS Notebooks中输入代码后,可通过按下tab键打开帮助窗口查看具体方法或者属性,具有代码提示和代码补全功能。

显示列表后,还可以输入内容进行再次过滤。 从列表中选择合适的方法后,按 Enter 键即可使用该方法。

Python工具、模块、函数、类和关键字都会存储可提供有关其使用信息的文档。 通过按Shift+Tab 可以激活指针处的文档。以下是针对缓冲区工具显示的文档:

或者,也可以使用内置Python help方法访问帮助文档。以下是针对 arcpy.analysis.Clip显示的帮助文档:

4. 参考资料

  • ArcGIS Pro 中的 notebook:https://pro.arcgis.com/zh-cn/pro-app/latest/arcpy/get-started/pro-notebooks.htm
  • ArcGIS Pro 提取分析工具:https://pro.arcgis.com/zh-cn/pro-app/latest/tool-reference/analysis/clip.htm

GIS之路-开发示例数据下载,请在公众号后台回复:vector

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集(全)

GDAL 开发合集(全)

GIS 影像数据源介绍

GeoJSON 数据源介绍

GIS 名词解释

ArcPy,一个基于 Python 的 GIS 开发库简介

GIS 开发库 Turf 介绍

GIS 开发库 GeoTools 介绍

GIS 开发库 GDAL 介绍

地图网站大全

从微信指数看当前GIS框架的趋势

Landsat 卫星数据介绍

OGC:开放地理空间联盟简介

中国地图 GeoJSON 数据集网站介绍

看完就懂 useSyncExternalStore

作者 ssshooter
2026年3月2日 19:42

功能

React 引入 useSyncExternalStore 也很长一段时间了,但是存在感还不太强。简而言之,它专门用来搞定那些不受 React 内部生命周期控制的外部数据源

过去最大的问题其实是 React 渲染时的 「撕裂」,这是 React 为了优化页面响应速度引入的并发渲染机制带来的副作用。

简单来说就是 React 为了防止在渲染时长时间无法响应用户输入,把渲染过程拆分成多个可中断的小任务,这就能小任务的间隙中插入用户响应,从而模拟出「并发」的感觉。更完整的前因后果可以参考《React 的设计哲学》

在 React 并发渲染机制下,如果用普通的 useEffect 去同步外部数据,可能会出现渲染进行到一半时数据突然发生变化,导致同一份页面中,一半的组件拿着老数据,另一半拿着新数据的灵异现象(但是实际上出现这个问题的几率其实非常小,大家都忽略了,这就导致了 useSyncExternalStore 的存在感很低)。使用 useSyncExternalStore 后,如果在渲染过程中快照发生变化,React 会丢弃当前渲染并重新开始,从而保证同一次提交中的所有组件看到的是同一个版本的数据。

使用场景

订阅浏览器 API

拿监听网络状态来说。不使用这个 Hook 之前,我们通常得在组件里写个包含完整挂载和清理逻辑的 useEffect 去监听 onlineoffline 事件。

function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

// 组件里直接这么用
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

监听媒体查询(Media Queries)响应式布局也是同样的套路:

const query = window.matchMedia("(max-width: 600px)");

function subscribe(callback) {
  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

const isMobile = useSyncExternalStore(subscribe, () => query.matches);

轻量级全局状态

如果你接手了一个极小的项目,不想引入 Redux 或 Zustand 这样繁琐的包,但又迫切需要在几个跨层级的组件间共享某部分状态。这时候你可以直接手搓一个简易的 Store:

// 丢在 React 外面的状态中心
let internalState = { count: 0 };
const listeners = new Set();

const store = {
  increment() {
    internalState = { count: internalState.count + 1 };
    listeners.forEach((l) => l());
  },
  subscribe(callback) {
    listeners.add(callback);
    return () => listeners.delete(callback);
  },
  getSnapshot() {
    return internalState;
  },
};

// 任何组件里都可以直接同步获取状态
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

注意:useSyncExternalStore 内部用 Object.is 比较前后快照,如果 getSnapshot 在数据未变的情况下每次都返回新对象,会导致无限循环重渲染。

只要把这段代码看懂,你就掌握了 Zustand 这种现代状态管理库的核心原理

竞品 API

useEffect + setState

曾经大家都习惯在 useEffect 里监听外部变化,如果变了,再跑一下 setState 触发更新。

这就又到了日常批判 useEffect 的时候了。

useEffect 带来重复渲染和闪烁问题。如果你的外部状态和页面初始计算的状态不对齐,页面渲染就会经历「旧值 -> 闪烁 -> 新值」这三步。而 useSyncExternalStore 在渲染中途就能直接取走最新的正确值。

另外,在处理服务端渲染时,用副作用很容易抛出水合(Hydration)错误,因为服务端和客户端首次生成的 HTML 大概率因为外部数据对不上。useSyncExternalStore 为此专门开了一个叫 getServerSnapshot 的参数,让你传能兜底服务端的静态快照。

Context

很多人滥用 Context 做全局状态,但如果是频繁变动的数据,Context 的广播机制简直是一场灾难。只要 Provider 提供的值发生了变动,它底下所有的子组件也会跟着无脑重跑 Render,除非你给每个组件层级套一层 React.memo(当然现在有 compiler,但也不是毫无代价)。

相比之下,useSyncExternalStore 实现了高精度的按需订阅——只有从 Store 取出的快照真的有了变化,关联的组件才会再次渲染。在这里还是顺便强调一下,没事别用 Context。

总结

要判断何时使用 useSyncExternalStore 其实很简单,只要你的数据依然在 React 的生命周期里流转(例如表单实时输入、控制弹窗开闭的布尔值),那就老老实实用回你的 useStateuseReducer

一旦数据满足游离于 React 管理之外、会随时间变化、且你要让 UI 能自动响应这种变化这三个条件,就毫不犹豫上 useSyncExternalStore。日常写前端页面也许碰不到几次,但之后你要是去造底层 Hook 库,或者需要硬啃第三方库内部暴露出的状态时,useSyncExternalStore 绝对好使~

相关链接

别再乱用 ref 和 reactive 了!Vue 3 响应式最佳实践,90% 的人都踩过坑

作者 前端Hardy
2026年3月2日 18:14

上周 Code Review,我看到同事写了这样一段代码:

const state = reactive({
  user: null,
  loading: false,
  error: '',
  list: []
});

// 后面又单独定义
const currentPage = ref(1);
const pageSize = ref(10);

乍看没问题,但一运行**——页面卡顿、watch 失效、调试器里数据对不上……**

问题出在哪?
不是逻辑错,而是响应式对象的“组合方式”错了

今天,我就用3 条黄金法则 + 2 个实战模板,帮你彻底搞懂 Vue 3 响应式怎么写才高效、安全、可维护。

法则 1:简单值用 ref,复杂对象用 reactive —— 但别混用!

很多教程说:“primitive 用 ref,object 用 reactive”,这没错,但忽略了“解构陷阱”。

错误示范:

const { user, loading } = reactive({ user: null, loading: false });
// 解构后失去响应性!

正确做法:

// 方案 A:全部用 ref(推荐新手)
const user = ref(null);
const loading = ref(false);

// 方案 B:用 toRefs 保持响应性
const state = reactive({ user: null, loading: false });
const { user, loading } = toRefs(state); // ✅ 响应式保留

经验公式:

  • 如果你要频繁解构 or 传递单个属性 → 优先用 ref
  • 如果是完整状态模块(如表单、列表配置)→ 用 reactive + toRefs

法则 2:别把 ref 套进 reactive,除非你真的需要

见过这种写法吗?

const state = reactive({
  count: ref(0), // ❌ 不要!
  name: 'Vue'
});

这会导致:

  • 访问时必须写 state.count.value(破坏一致性)
  • 模板中虽然自动 unwrap,但逻辑层混乱
  • 容易引发“value 嵌套地狱”

正确做法:统一层级

// 要么全 ref
const count = ref(0);
const name = ref('Vue');

// 要么全 reactive(count 直接是 number)
const state = reactive({
  count: 0,
  name: 'Vue'
});

小技巧:在 setup() 返回时,用 ...toRefs(state) 一键暴露所有属性。

法则 3:大型组件,用“状态模块化”代替巨型 reactive

当组件状态超过 5 个字段,别堆在一个 reactive 里!

反面教材:

const state = reactive({
  // 用户信息
  userId, userName, userAvatar,
  // 分页
  page, size, total,
  // 搜索条件
  keyword, status, dateRange,
  // UI 状态
  showDrawer, loading, errorMsg...
});

推荐拆分:

// 按功能拆成多个小状态块
const userState = reactive({ id: '', name: '', avatar: '' });
const pagination = reactive({ page: 1, size: 10, total: 0 });
const uiState = reactive({ loading: false, drawerVisible: false });

// 或封装成 composable
const { userState } = useUserStore();
const { pagination, fetchList } = usePagination();

这样不仅逻辑清晰,还天然支持 逻辑复用(比如分页逻辑抽成 usePagination)。

实战模板:两种主流写法对比

模板 A:全 ref 风格(适合中小型组件)

export default {
  setup() {
    const loading = ref(false);
    const list = ref([]);
    const keyword = ref('');

    const search = async () => {
      loading.value = true;
      list.value = await api.search(keyword.value);
      loading.value = false;
    };

    return { loading, list, keyword, search };
  }
}

优点:直观、无解构风险、TS 类型推导友好
注意:返回时别漏写 .value

模板 B:reactive + toRefs(适合状态密集型组件)

export default {
  setup() {
    const state = reactive({
      loading: false,
      list: [] as Item[],
      keyword: ''
    });

    const search = async () => {
      state.loading = true;
      state.list = await api.search(state.keyword);
      state.loading = false;
    };

    return { ...toRefs(state), search };
  }
}

优点:状态聚合、减少变量声明、模板中直接用 list
注意:内部操作用 state.xxx,别解构!

高阶建议:结合 更清爽

如果你用 Vue 3.3+,直接上 :

import { ref } from 'vue'

const loading = ref(false)
const list = ref([])
const keyword = ref('')

const search = async () => {
  loading.value = true
  list.value = await api.search(keyword.value)
  loading.value = false
}

没有 return,没有 setup(),变量自动暴露——这才是 Vue 3 的终极舒适区。

最后说两句

Vue 3 的响应式系统很强大,但自由也意味着责任。
用对了,代码清爽如诗;用错了,bug 隐蔽如鬼。

记住三句话:

  1. 简单用 ref,复杂用 reactive
  2. 别混用,别嵌套,别解构裸对象
  3. 大组件,拆状态,抽 composable

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

使用Ai从零开发智慧水利态势感知大屏(开源)

作者 柳杉
2026年3月2日 18:09

基于 React + autofit.js 打造的全屏自适应数据可视化大屏系统


📌 系统概述

智慧水利态势感知系统 是一套专为水利防汛设计的实时监控与数据可视化平台。系统采用现代化的前端技术栈,结合智能自适应方案,能够在任意分辨率的大屏设备上完美呈现,为水利防汛指挥提供全方位的数据支撑。

🎯 核心特性

  • 全屏自适应:基于 autofit.js 实现任意屏幕完美适配
  • 实时数据监控:天气、降雨、河道水情实时更新
  • 地理信息可视化:河南省地图 + 区域预警标注
  • 交互式图表:ECharts 驱动的多维度数据展示
  • 响应式布局:左中右三栏式科技感界面
  • 动态视觉效果:渐变、光晕、动画营造沉浸体验

🎨 界面布局设计

ScreenShot_2026-03-02_164325_575.png 系统采用经典的三栏式大屏布局,设计分辨率为 1920×1080px

┌─────────────────────────────────────────────────────────┐
│                    智慧水利态势感知系统                     │  ← 头部导航
├──────────┬──────────────────────────┬──────────┤
│          │                          │          │
│  左侧面板 │      中心地图可视化       │  右侧面板  │
│          │                          │          │
│ 实时天气  │    河南省地图 + 预警面板   │ 河道水情  │
│ 降雨监控  │                          │ 水位变化  │
│ 降雨统计  │    底部模式切换控制       │ 趋势图表  │
│          │                          │          │
└──────────┴──────────────────────────┴──────────┘

区域功能划分

区域 宽度占比 主要功能
左侧面板 25% 实时天气情况、实时降雨情况、降雨统计
中心区域 50% 河南省地图、暴风雨预警、模式切换
右侧面板 25% 河道实时水情、水情变化、水位趋势

🛠️ 技术架构

核心技术栈

{
  "前端框架": "React 19.2.3",
  "构建工具": "Vite 7.2.4",
  "UI框架": "Tailwind CSS 4.1.17",
  "图表库": "ECharts 6.0.0 + echarts-for-react",
  "自适应方案": "autofit.js 3.2.8",
  "时间处理": "Day.js 1.11.19",
  "类型支持": "TypeScript 5.9.3",
  "图标库": "lucide-react 0.575.0"
}

项目结构

dashboard-autofit-setup/
├── src/
│   ├── components/          # 组件目录
│   │   ├── ui/             
│   │   │   └── Panel.tsx   # 通用面板容器
│   │   ├── Header.tsx      # 顶部导航栏
│   │   ├── LeftPanel.tsx   # 左侧数据面板
│   │   ├── CenterMap.tsx   # 中心地图区域
│   │   └── RightPanel.tsx  # 右侧数据面板
│   ├── hooks/              # 自定义 Hooks
│   │   ├── useData.ts      # 数据获取 Hook
│   │   └── useTime.ts      # 实时时间 Hook
│   ├── api/                # API 接口层
│   │   ├── index.ts        # API 统一导出
│   │   └── mock/          
│   │       └── data.ts     # Mock 数据
│   ├── utils/              # 工具函数
│   │   └── cn.ts           # 类名合并工具
│   ├── assets/             # 静态资源
│   ├── App.tsx             # 根组件
│   ├── autofit.d.ts        # autofit.js 类型声明
│   └── index.css           # 全局样式
└── package.json

🎯 核心功能详解

1️⃣ 全屏自适应解决方案

技术原理

系统采用 autofit.js 作为核心自适应引擎,通过 CSS3 Transform Scale 实现等比例缩放:

关键配置代码:

// App.tsx
useEffect(() => {
  autofit.init({
    dw: 1920,        // 设计稿宽度
    dh: 1080,        // 设计稿高度
    el: '.dashboard', // 缩放目标元素
    resize: true,     // 监听窗口变化
  });

  return () => {
    autofit.off();
  };
}, []);

CSS 样式支持:

/* index.css */
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
}

.dashboard {
  transform-origin: center center;
  width: 1920px;
  height: 1080px;
}

工作流程

1. 页面加载 → autofit.js 获取窗口尺寸
2. 计算缩放比例 = min(窗口宽/1920, 窗口高/1080)
3. 对 .dashboard 应用 transform: scale(比例)
4. 监听窗口 resize 事件,动态调整

适配效果

  • ✅ 支持 1366×7684K 任意分辨率
  • ✅ 保持 16:9 宽高比不变形
  • ✅ 自动居中对齐,始终撑满屏幕
  • ✅ 无需编写媒体查询代码

2️⃣ 实时天气与降雨监控

功能模块

左侧面板 - 实时天气情况

<Panel title="实时天气情况">
  {/* 实时时间 + 天气状态 */}
  <div className="flex justify-between">
    <div>
      <Clock /> 实时时间
      {time} {date}
    </div>
    <div>
      <CloudRain /> 实时天气
      {weather.weather} {weather.temp}
    </div>
  </div>
  
  {/* 降雨概率趋势图 */}
  <ReactECharts option={rainProbOption} />
</Panel>

数据展示:

  • 📍 实时时钟(基于 Day.js 每秒更新)
  • 🌡️ 温度范围:17~28°C
  • ☀️ 天气状态:晴/多云/雨
  • 📊 24小时降雨概率曲线图

左侧面板 - 实时降雨情况

<Panel title="实时降雨情况">
  {/* 累计降雨量数据 */}
  <div className="flex space-x-6">
    <div>当日累计降雨量: {rainfall.daily} mm</div>
    <div>近3日累计降雨量: {rainfall.threeDay} mm</div>
  </div>
  
  {/* 逐小时降雨量柱状图 */}
  <ReactECharts option={rainfallHoursOption} />
</Panel>

技术亮点:

  • 📈 ECharts 渐变色柱状图
  • 🔄 自动刷新数据(通过 useData Hook)
  • 🎨 动态高亮当前时段

左侧面板 - 降雨统计

展示各行政区划的降雨数据表格,支持:

  • 📋 当日/三日/当月降雨量对比
  • 🔀 可切换流域、水库维度
  • 📜 虚拟滚动加载(处理大量数据)

3️⃣ 地理信息可视化

河南省地图

数据来源: DataV.GeoAtlas(阿里云地理数据服务)

// CenterMap.tsx
useEffect(() => {
  fetch('https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json')
    .then(res => res.json())
    .then(data => {
      echarts.registerMap('henan', data);
      setGeoJson(data);
    });
}, []);

地图特性:

  • 🗺️ 支持缩放、拖拽交互
  • 📍 标注重点城市降雨量
  • ✨ 特效散点标记高风险区域
  • 🌊 动态波纹效果(effectScatter)

暴风雨预警面板

叠加在地图左下角的实时预警卡片:

<div className="absolute left-[6%] bottom-[10%] panel-bg">
  <div className="title">
    🌧️ 暴风雨预警
    <span className="orange-alert">Ⅲ级橙色预警</span>
  </div>
  
  <div className="content">
    <div>预警区域: 郑州 · 南阳市</div>
    <div>1小时最大雨强: 48mm</div>
    <div>未来3小时累计: 96mm</div>
    <div>风险上升: ▲ 32%</div>
    
    {/* 微型趋势柱状图 */}
    <div className="mini-chart">
      {[18, 26, 32, 40, 48, 38].map(v => (
        <div className="bar" style={{height: `${v/52*22}px`}} />
      ))}
    </div>
    
    <button>预案详情</button>
  </div>
</div>

设计亮点:

  • 🎯 橙色预警级别标识
  • 📊 实时数据大字号突出
  • 📈 渐变柱状图可视化趋势
  • 💡 操作建议 + 预案链接

4️⃣ 河道水情监控

右侧面板 - 河道实时水情

7列数据表格展示各站点详细信息:

站点 实时水位 实时雨量 设防水位 防洪高水位 警戒水位 保证水位
伊洛河 40m 20m 20m 20m 20m 20m
卫河 60m 20m 20m 20m 20m 20m

颜色标识:

  • 🟢 实时水位:青色加粗
  • 🟡 警戒水位:黄色
  • 🔴 保证水位:红色

右侧面板 - 河道水情变化

对比上一时段的水位变化:

{riverChanges.map(item => (
  <div className="grid-cols-4">
    <div>{item.station}</div>
    <div>{item.realtime}</div>
    <div>{item.previous}</div>
    <div>
      {item.trend === 'up' ? 
        <ArrowUp className="text-red-500" /> : 
        <ArrowDown className="text-green-500" />
      }
      {item.change}
    </div>
  </div>
))}

交互体验:

  • ⬆️ 上升趋势:红色箭头
  • ⬇️ 下降趋势:绿色箭头
  • 🔄 Hover 高亮当前行

右侧面板 - 水位实时变化趋势

折线图可视化:

series: [{
  type: 'line',
  smooth: true,
  data: waterTrends.data,
  markLine: {
    data: [
      { yAxis: waterTrends.safe, label: '保证水位' },
      { yAxis: waterTrends.warning, label: '警戒水位' }
    ]
  }
}]

技术细节:

  • 📏 警戒/保证水位标线
  • 🎨 渐变填充区域
  • 🔍 Tooltip 悬浮提示
  • 📊 支持流域切换(下拉选择器)

5️⃣ 自定义 Hooks 设计

useTime - 实时时钟

// hooks/useTime.ts
export function useTime() {
  const [time, setTime] = useState('');
  const [date, setDate] = useState('');

  useEffect(() => {
    const timer = setInterval(() => {
      const now = dayjs();
      setTime(now.format('HH:mm:ss'));
      setDate(now.format('YYYY-MM-DD dddd'));
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return { time, date };
}

应用场景:

  • 头部导航栏右侧时间显示
  • 左侧天气面板实时时钟

useData - 数据获取与缓存

// hooks/useData.ts
export function useData<T>(
  fetcher: () => Promise<T>, 
  initialData: T
) {
  const [data, setData] = useState(initialData);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetcher()
      .then(setData)
      .finally(() => setLoading(false));
  }, []);

  return { data, loading };
}

使用示例:

const { data: weather } = useData(
  () => getWeatherData(), 
  { temp: '', weather: '', rainProb: [] }
);

6️⃣ 响应式图表处理

ECharts 自适应问题

问题: autofit.js 的 transform: scale() 会导致 ECharts 图表内部不感知真实容器尺寸。

解决方案:

// CenterMap.tsx
useEffect(() => {
  const handleResize = () => {
    setTimeout(() => {
      if (chartRef.current) {
        const chart = chartRef.current.getEchartsInstance();
        chart?.resize();
      }
    }, 100);
  };

  window.addEventListener('resize', handleResize);
  handleResize(); // 初始化时调用

  return () => window.removeEventListener('resize', handleResize);
}, [geoJson]);

关键点:

  • ⏱️ 延迟 100ms 确保 transform 完成
  • 📐 手动调用 chart.resize() 更新尺寸
  • 🔄 监听 window resize 事件

🎨 视觉设计系统

色彩方案

:root {
  --color-primary: #00ffcc;        /* 主题青色 */
  --color-primary-dark: #00b38f;   /* 深青色 */
  --color-bg-dark: #020b18;        /* 深蓝黑背景 */
  --color-bg-panel: rgba(2,16,32,0.7); /* 面板半透明 */
  --color-border: #00e5ff;         /* 边框青色 */
}

视觉特效

1. 面板样式

.panel-bg {
  background: linear-gradient(
    180deg, 
    rgba(3,26,45,0.8) 0%, 
    rgba(2,14,25,0.8) 100%
  );
  border: 1px solid rgba(0,229,255,0.3);
  box-shadow: inset 0 0 20px rgba(0,229,255,0.1);
}

2. 渐变文字

.text-gradient {
  background: linear-gradient(180deg, #ffffff 0%, #00e5ff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

3. 光晕背景

<div className="absolute inset-0">
  <div className="w-[800px] h-[800px] bg-[radial-gradient(
    circle_at_center,
    rgba(0,229,255,0.1)_0,
    transparent_60%
  )]" />
</div>

📦 部署与优化

构建配置

// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

生产构建

# 安装依赖
npm install

# 开发环境运行
npm run dev

# 生产构建(输出到 dist 目录)
npm run build

# 预览生产构建
npm run preview

性能优化建议

  1. 代码分割

    • Vite 自动进行 chunk 分割
    • React 组件懒加载(React.lazy)
  2. 图片资源

    • 使用 WebP 格式
    • 压缩静态资源
  3. ECharts 按需加载

    import { LineChart, BarChart } from 'echarts/charts';
    import { GridComponent } from 'echarts/components';
    echarts.use([LineChart, BarChart, GridComponent]);
    
  4. 数据请求优化

    • 实现请求缓存
    • 增量数据更新
    • WebSocket 实时推送

🚀 扩展方向

功能增强

  • 多屏联动:支持多大屏同步显示
  • 历史数据回放:时间轴拖拽查看历史
  • 告警推送:WebSocket 实时预警通知
  • 3D 地形:Three.js 立体地形可视化
  • AI 预测:机器学习预测降雨趋势

技术升级

  • TypeScript 完善:增强类型安全
  • 单元测试:Vitest 测试覆盖
  • Docker 部署:容器化部署方案
  • 微前端改造:qiankun/Module Federation
  • 性能监控:接入 Sentry/性能埋点

💡 技术亮点总结

技术点 实现方案 优势
屏幕自适应 autofit.js 零配置、高性能、兼容性强
数据可视化 ECharts 功能强大、交互丰富、文档完善
状态管理 React Hooks 轻量级、易维护、TypeScript 友好
样式方案 Tailwind CSS 原子化、响应式、开发效率高
构建工具 Vite 快速启动、热更新、现代化
时间处理 Day.js 轻量级、国际化、链式调用

📚 参考资料


👨‍💻 开发者信息

项目名称: 智慧水利态势感知系统
技术栈: React + TypeScript + Vite + autofit.js
设计分辨率: 1920×1080
开发时间: 2026年


🎉 结语

本系统综合运用了现代前端技术,实现了高性能、强交互、全适配的数据可视化大屏解决方案。通过 autofit.js 自适应引擎,完美解决了传统大屏开发中的分辨率适配难题,为水利防汛指挥提供了强有力的技术支撑。

核心价值:

  • ✅ 开箱即用的自适应方案
  • ✅ 模块化组件设计易于维护
  • ✅ 丰富的视觉效果提升体验
  • ✅ 完整的技术栈可复用性强

希望这套系统能够为智慧水利建设贡献一份力量!🚀


我放在公众号(柳杉前端) 回复 智慧水利态势感知大屏 获取源码

从高阶函数到 Hooks:React 如何减轻开发者的心智负担(含 Demo + ahooks 推荐)

作者 兆子龙
2026年3月2日 17:55

从高阶函数到 Hooks:React 如何减轻开发者的心智负担(含 Demo + ahooks 推荐)

对比 HOC/render props 与 Hooks,用具体 demo 展示「按功能组织、无 this、复用逻辑」的减负效果,并推荐 ahooks 库。


一、高阶函数时代的心智负担

在 Hooks 之前,React 里复用「带状态的逻辑」主要靠两类手段:高阶组件(HOC)render props。二者本质都是「高阶函数」——接收组件或函数,返回增强后的组件或新的渲染方式。它们能解决问题,但会带来明显的心智负担。

1. 嵌套地狱,难以追踪

多个 HOC 叠加时,组件树会变成一层套一层:withAuth(withTheme(withWindowSize(MyPage)))。DevTools 里看到的是一串 WithAuth(WithTheme(WithWindowSize(...)))数据从哪一层来、props 叫什么,都要一层层往上找,调试和阅读成本都很高。

2. this 与生命周期分散逻辑

Class 组件里,this 的绑定(bind 或类字段)是常见坑;同一块逻辑还经常被拆到 componentDidMountcomponentDidUpdate 两处,「根据 A 同步 B」 的代码散落在不同生命周期里,难以按「功能」理解。

3. 命名与透传的样板代码

HOC 要透传 props({...this.props}),还要小心 refdisplayName;render props 则要多写一层函数和命名(如 render={({ x, y }) => ...})。这些都是在解决「逻辑复用」时多出来的心智开销。

下面先用一个具体 demo 对比「HOC 写法」和「自定义 Hook 写法」,直观感受 Hooks 如何减负。


二、Demo 1:窗口尺寸 —— HOC 与 Hook 对比

需求:多个组件需要用到「当前窗口宽高」,并在 resize 时更新。

用 HOC 实现(心智负担大)

// 高阶组件:包装一层 Class,把 width/height 通过 props 注入
function withWindowSize(WrappedComponent) {
    return class WithWindowSize extends React.Component {
        state = { width: window.innerWidth, height: window.innerHeight };
        componentDidMount() {
            this.handler = () => this.setState({
                width: window.innerWidth,
                height: window.innerHeight,
            });
            window.addEventListener('resize', this.handler);
        }
        componentWillUnmount() {
            window.removeEventListener('resize', this.handler);
        }
        render() {
            return (
                <WrappedComponent
                    width={this.state.width}
                    height={this.state.height}
                    {...this.props}
                />
            );
        }
    };
}

// 使用:组件被包一层,DevTools 里多一个 WithWindowSize
const MyPanel = withWindowSize(function MyPanel({ width, height }) {
    return <div>当前宽度:{width}px,高度:{height}px</div>;
});

你要关心:HOC 的 displayNameref 透传(若需要)、以及「数据从哪个 HOC 来」。多个 HOC 叠加时,问题成倍增加。

用自定义 Hook 实现(减负)

// 自定义 Hook:按「一块逻辑」组织,无 Class、无 this
function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight,
    });
    useEffect(() => {
        const handler = () => setSize({
            width: window.innerWidth,
            height: window.innerHeight,
        });
        window.addEventListener('resize', handler);
        return () => window.removeEventListener('resize', handler);
    }, []);
    return size;
}

// 使用:直接调用,无包装、无嵌套
function MyPanel() {
    const { width, height } = useWindowSize();
    return <div>当前宽度:{width}px,高度:{height}px</div>;
}

减负体现:逻辑集中在 useWindowSize 里,按「功能」一块块组织;组件树扁平,没有多余的包装组件;没有 this,没有生命周期命名,读代码时「用到什么就调什么 Hook」。


三、Demo 2:请求数据 + loading —— 手写 vs ahooks useRequest

需求:请求用户列表,展示 loading、错误和重试。

手写 useEffect(容易漏依赖、重复逻辑)

function UserList() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        let cancelled = false;
        setLoading(true);
        setError(null);
        fetch('/api/users')
            .then((res) => res.json())
            .then((json) => {
                if (!cancelled) setData(json);
            })
            .catch((e) => {
                if (!cancelled) setError(e);
            })
            .finally(() => {
                if (!cancelled) setLoading(false);
            });
        return () => { cancelled = true; };
    }, []);

    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误:{error.message}</div>;
    return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

你要自己处理:竞态取消、loading/error 状态、重试逻辑若再加一层,代码更长、心智负担更大。

用 ahooks 的 useRequest(减负)

import { useRequest } from 'ahooks';

function UserList() {
    const { data, loading, error, refresh } = useRequest(() =>
        fetch('/api/users').then((res) => res.json())
    );

    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误:{error.message} <button onClick={refresh}>重试</button></div>;
    return (
        <ul>
            {data?.map((u) => <li key={u.id}>{u.name}</li>)}
            <button onClick={refresh}>刷新</button>
        </ul>
    );
}

减负体现竞态、loading、error、重试 都由 useRequest 管,你只关心「发什么请求」和「怎么渲染」;代码更短,逻辑更清晰,心智负担明显下降。


四、Demo 3:防抖输入 —— 手写 vs ahooks useDebounce

需求:搜索框输入防抖,仅在实际停顿后再请求。

手写(要管定时器、清理、依赖)

function SearchBox() {
    const [keyword, setKeyword] = useState('');
    const [debouncedKeyword, setDebouncedKeyword] = useState('');

    useEffect(() => {
        const timer = setTimeout(() => setDebouncedKeyword(keyword), 300);
        return () => clearTimeout(timer);
    }, [keyword]);

    useEffect(() => {
        if (!debouncedKeyword) return;
        fetch(`/api/search?q=${debouncedKeyword}`).then(/* ... */);
    }, [debouncedKeyword]);

    return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}

你要自己保证:防抖时间、清理、以及「防抖后的值」和「请求」的依赖关系正确。

用 ahooks 的 useDebounce(减负)

import { useDebounce } from 'ahooks';

function SearchBox() {
    const [keyword, setKeyword] = useState('');
    const debouncedKeyword = useDebounce(keyword, { wait: 300 });

    useEffect(() => {
        if (!debouncedKeyword) return;
        fetch(`/api/search?q=${debouncedKeyword}`).then(/* ... */);
    }, [debouncedKeyword]);

    return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}

减负体现:防抖逻辑交给 useDebounce,你只关心「用防抖后的值做什么」;少写定时器、少操心清理,心智负担更小。


五、React 如何用 Hooks 减轻心智负担(小结三点)

  1. 按功能组织,而非按生命周期
    同一块逻辑(如「窗口尺寸」「请求用户」)收拢在一个 Hook 里,相关代码在一起,读起来是「这个组件用了哪些能力」,而不是「mount 里干了啥、update 里又干了啥」。

  2. 无 this,闭包清晰
    函数组件 + Hooks 没有 this,state 和更新函数都来自 useState 等 API,依赖关系写在 Hook 的依赖数组里,减少「this 指向错了」「忘了 bind」这类问题。

  3. 复用即「调用 Hook」
    复用带状态的逻辑不再依赖 HOC 或 render props 的层层包装,直接「调用自定义 Hook」即可,组件树扁平、数据来源一目了然。

在此基础上,用好现成的 Hooks 库(如 ahooks)可以进一步减少「自己管请求、防抖、节流、缓存」的心智负担,把精力放在业务 UI 和交互上。


六、推荐 ahooks:为业务而生的 Hooks 库

ahooks 是阿里开源的 React Hooks 库,目标是做 Hooks 领域的「lodash」——稳定、可长期依赖。它用 TypeScript 编写,提供完整类型,且针对闭包、SSR 等做了处理,适合在真实项目里直接使用。

安装

npm install ahooks
# 或 pnpm add ahooks / yarn add ahooks

常用 Hooks 一览

场景 Hook 作用简述
异步请求 useRequest 自动/手动请求、loading、重试、轮询、缓存
防抖 / 节流 useDebounce / useThrottle 值或函数的防抖/节流
状态与存储 useLocalStorageState 持久化到 localStorage
DOM / 尺寸 useSizeuseScroll 元素尺寸、滚动位置
生命周期相关 useUnmountuseUpdateEffect 仅卸载时执行、仅更新时执行

与本文 demo 的对应关系

  • Demo 2 用了 useRequest,可直接替换手写的 useEffect + fetch,并享受重试、轮询、缓存等能力。
  • Demo 3 用了 useDebounce,把「防抖后的值」从状态和定时器里抽离出来,代码更短、更稳。

更多 API 和用法见官网:ahooks.js.org/zh-CN


总结

  • 高阶函数(HOC/render props) 能复用逻辑,但带来嵌套、this、生命周期分散等心智负担。
  • Hooks 通过「按功能组织、无 this、复用即调 Hook」减轻负担;用 自定义 Hook 替代 HOC,组件树更扁平、数据流更清晰。
  • 文中用 窗口尺寸、请求数据、防抖输入 三个 demo 对比手写/HOC 与 Hook/ahooks 的写法,直观看到 Hooks 的优势。
  • ahooks 提供 useRequestuseDebounce 等常用能力,建议在项目中直接使用,进一步减少重复逻辑与心智负担。

若对你有用,欢迎点赞、收藏;有更好的 Hooks 实践或 ahooks 用法也欢迎在评论区分享。

前端老哥的救命稻草:用 Obsidian 搞定 Claude Code 的「金鱼记忆」

作者 jerrywus
2026年3月2日 17:51

写在前面

前端开发中常见这些问题:

  • 每次写代码都要翻一遍同样的规范
  • 踩过的坑,过段时间又忘了
  • 项目规范写在文档里,但开发时根本想不起来
  • 想沉淀经验,但不知道从哪下手
  • Claude Code 有时候不按规范写(上下文丢失)

这篇文章讲讲我们怎么用 Obsidian + Claude Code 来解决这些问题。

整体方案

三层结构:

Memory 文件(200行左右)→ Smart Context Skill → Obsidian docs/

工作流程很简单:

  1. 你给 Claude Code 一个编程任务
  2. Smart Context 自动触发
  3. 自动查 Obsidian 里的相关规范和踩坑记录
  4. 带着上下文开始写代码

步骤一:创建 Obsidian 文档结构

在项目根目录建 docs/ 文件夹:

docs/
├── 00-索引.md
├── 01-快速开始.md
├── 02-开发规范/
│   ├── index.md
│   ├── API规范.md
│   ├── 组件使用.md
│   ├── 命名约定.md
│   └── 页面开发.md
├── 03-架构设计/
│   ├── index.md
│   ├── 目录结构.md
│   └── 分包策略.md
├── 04-开发笔记/
│   ├── index.md
│   └── 踩坑记录.md
└── 05-Claude相关/
    ├── index.md
    └── 规则文件说明.md

示例:踩坑记录

docs/04-开发笔记/踩坑记录.md

# 踩坑记录

## Taro 相关

### scroll-view 下拉加载不触发
**问题**: @scrolltolower 事件不触发
**原因**: scroll-view 高度未设置
**解决**: 设置 scroll-y 和 height: 100vh

### 内联 SVG 不支持
**问题**: svg 标签不渲染
**解决**: 使用图片 URL 或 IconFont

示例:API 规范

docs/02-开发规范/API规范.md

# API 开发规范

## 函数命名
- query: 查询/获取
- add: 新增
- edit: 编辑
- delete: 删除
- toggle: 切换状态
- do: 执行操作

## 标准模式
1. try/catch 包裹
2. 检查 code === EResponseCode.Succeed
3. 从 context 提取数据
4. catch 中使用 getHttpErrorMessage

步骤二:Memory 文件

这个是claudecode自带的,/memory去开启即可

文件位置:

~/.claude/projects/-项目名-/memory/MEMORY.md

内容首次微调到精简到 50 行以内 (因为后续cc会自动往里面加记忆):

# Project Memory

## 知识库架构

> Memory(200行核心)→ Obsidian docs/(完整知识)

| 层级 | 存储 | 用途 |
|------|------|------|
| Memory | 核心规范摘要 | 始终加载 |
| Obsidian | 完整文档/踩坑记录 | 检索使用 |

## 核心规范

- **页面**: SafeLayout 根容器,列表 graybg/详情 whitebg
- **API**: query/add/edit/delete/toggle/do + try/catch
- **组件**: 优先 src/components/,禁 SVG 用 IconFont

## 常见避坑

1. scroll-view: 设 scroll-y + height
2. 小程序禁 SVG: 用图片 URL
3. NutUI 样式: 查 auto-import
4. ref template 不需 .value

## Obsidian 检索

```bash
obsidian search query=页面开发 # 搜索
grep -r "xxx" docs/ # 失败时才用 Grep
知识 文件
踩坑 docs/04-开发笔记/踩坑记录.md
API docs/02-开发规范/API规范.md
页面 docs/02-开发规范/页面开发.md

触发条件

编程任务自动检索:新增/修复/重构/询问"怎么做"/业务模块

## 步骤三:创建 Smart Context Skill

在项目 `.claude/skills/` 目录下创建:

.claude/skills/smart-context/
└── skill.md

内容:

---
name: smart-context
description: |
  智能上下文增强技能。自动检索本项目 Obsidian 知识库(docs/)中的项目规范、踩坑记录。
  触发条件:(1) 实现新功能/创建页面/添加API (2) 修复bug/解决报错 (3) 重构代码
  (4) 询问"怎么做" (5) 提到业务模块(提货/结算/销售/会员等)。
  优先使用 obsidian-cli 搜索 Obsidian 文档,失败才使用 Grep。
---

# Smart Context - 智能上下文增强

## 核心原则

1. 以 Obsidian 为知识库,obsidian-cli 为检索工具
2. 当用户说"把这个加入知识库"时,优先使用 obsidian-cli 增加

✅ obsidian search query=文档关键词 ❌ grep -r "scroll-view" docs/


## 工作流程

### 第一步:分析任务意图
- 任务类型:新增/修改/修复/查询
- 业务模块:提货/结算/销售/会员
- 技术领域:API/页面/组件

### 第二步:检索 Obsidian
```bash
obsidian search query="{关键词}"

第三步:按需读取

obsidian read path=docs/04-开发笔记/踩坑记录.md

第四步:注入上下文执行

检索关键词

任务 搜索词
创建页面 页面开发、SafeLayout、列表页
添加 API API规范、query、try catch
修复报错 踩坑、{报错关键词}
提货相关 提货、pickup

示例

用户输入:帮我创建一个退款订单列表页面

自动执行:

  1. 分析:创建页面,销售/退款
  2. 搜索:obsidian search query=页面开发
  3. 搜索:obsidian search query=列表页
  4. 读取踩坑记录
  5. 注入上下文,开始实现

注意事项

  • 必须使用 obsidian-cli,禁止 Grep 搜索 docs/
  • 按需读取,不要整个文件加载
  • 实现前先查踩坑记录,避免重复踩坑

编码实测

配置完成后,实际效果长这样:

左边 Claude Code 正在工作,右边 Obsidian 里的搜索结果同步显示。它自动检索到了相关规范,比如页面开发、SafeLayout 这些关键信息。

再看另一个角度:

左边继续从 Obsidian 拉取踩坑记录,右边代码已经写上了。Smart Context 把规范和避坑信息注入上下文,Claude 直接沿着正确方向写,不需要你中途打断去纠正。


步骤四:验证和使用

验证配置

重启 Claude Code,然后测试:

帮我创建一个订单列表页面

观察 Claude 的行为:

  1. 自动触发 Smart Context
  2. 使用 obsidian search 搜索相关文档
  3. 读取关键片段
  4. 注入上下文后开始实现

添加新知识

遇到新踩坑时,直接告诉 Claude:

把这个加入知识库:Taro 项目上传图片时,如果使用本地路径不显示,
需要使用 require() 或者用 COS 托管的图片 URL

Claude 会自动:

  1. 使用 obsidian-cli 找到踩坑记录文件
  2. 追加新的踩坑内容
  3. 可选的,同步更新 Memory 文件

常见问题

Q0: 如何启用obsidian-cli

在obsidian软件设置->关于-> 打开"允许命令行和obsidian交互“, 然后重启cc会话即可。

同时安装一下mcp服务:

mcp-obsidian.org/install/

再安装obsidian skills

请打开:obsidian skills

Q1:为什么要用 obsidian-cli 而不是 Grep?

  • obsidian-cli 是 Obsidian MCP 工具,专门用于搜索和操作 Obsidian 文档,速度极快
  • Grep 搜索会破坏 Obsidian 的双向链接和知识图谱
  • obsidian-cli 支持更智能的搜索

Q2:Memory 文件太长怎么办?

  • 减少memory篇幅,只放核心规范(约 50-200 行)
  • 详细内容通过双向链接指向 Obsidian 文档
  • 用表格和列表,减少段落

Q3:Obsidian 需要手动打开吗?

不需要。Claude Code 通过 obsidian-cli MCP 工具直接操作,Obsidian 可以关闭,只是一个存储软件,实际cc调用效果如下图:

比如我让他修改文档


总结

配置完成后,你得到一个自动化的知识增强系统:

功能 实现方式
记忆增强 Obsidian 持久存储 + Memory 始终加载
规范约束 Smart Context 自动检索
避坑提醒 踩坑记录 + 自动查询
知识更新 自然语言告诉 Claude "加入知识库"

核心思路:让 Claude Code 在每次编程时自动检索相关规范,而不是靠人工记忆。

关于obsidian更多用法,请关注后续写新的文章~

游戏官网前端工具库:海内外案例解析

2026年3月2日 17:43

在游戏行业竞争日趋激烈的当下,游戏官网早已不再是单纯的信息展示页,而是承载品牌叙事、玩家沉浸体验、内容传播的核心载体。本篇文章,基于海内外游戏官网案例(GTA VI、崩:星穹铁道、Elden Ring 等等),从 动画交互实用工具性能优化UI 组件 等多个维度,对各类工具进行梳理和分析,希望能为游戏官网开发者提供参考思路。

一、动画与交互

动画与交互是游戏官网吸引用户的核心要素,涵盖滚动控制、动态效果、手势响应等工具。

1.1 滚动与视差

通过平滑滚动与多层背景差速移动,营造深度感和电影般的叙事节奏。

💠 Lenis

Lenis 是一款 轻量级(4.7kB)的平滑滚动库。保留滚动条、不破坏原生事件,却能给出 惯性阻尼自定义缓动。可以与主流动画库如 GSAP 集成,实现基于滚动进度的动画。非常适合用于追求流畅体验的网站。

地址:github.com/darkroomeng…

案例:劍與遠征:啟程

1.gif

ouseMultiplier: .7:鼠标滚动倍率(0.7 倍原生速度),较慢的滚动速度能让玩家更从容地浏览角色立绘和剧情介绍。smooth: !0 开启平滑滚动,配合 GSAP 控制的渐显动画,增强视觉连贯性。将 UI状态变化(导航固定、右侧悬浮栏显示)放在滚动事件监听器里。

const lenis = new Lenis({
  mouseMultiplier: 0.7,
  smooth: true,
  smoothTouch: false
})

lenis.on('scroll', (e) => {
  document.querySelector('header').classList.toggle('Fixed', e.scroll > 200)
  document.querySelector('.back-top').classList.toggle('show', e.scroll > window.innerHeight / 2)
  if (window.ScrollTrigger) ScrollTrigger.update()
})

function raf(time) {
  lenis.raf(time)
  requestAnimationFrame(raf)
}
requestAnimationFrame(raf)

💠 fullPage

fullPage.js 专门用于快速创建全屏滚动网站(也称为单页滚动网站)。它将浏览器视口分割成多个全屏大小的部分,并通过平滑的垂直或水平滚动在它们之间进行导航。能提供一种沉浸式、滚动翻阅般的浏览体验,适合需要以 “叙事节奏” 引导用户探索的场景。

地址:alvarotrigo.com/fullPage/

案例:最终幻想14

2.gif

平衡多端体验,responsiveWidth: 750 手机/平板直接原生滑动,优先保证浏览流畅性。afterLoad 事件自动播放视频 & 音轨,onLeave 事件暂停节省流量和优化性能。scrollOverflow 对于内容超高的区域允许单屏内部再滚动, 解决长内容的展示问题。

new fullpage('#fullpages', {
  scrollOverflowReset: true,
  scrollOverflow: true,
  scrollBar: false,
  ...
  resize: true,
  responsiveWidth: 750,
  afterLoad: this.afterLoad,
  onLeave: this.onLeave,
});

💠 Locomotive Scroll

相较于 Lenis 的 物理仿真 或 fullPage 的 全屏分段,Locomotive Scroll 的核心优势在于对 微视差 的精准控制,通过精确监测滚动位置并驱动元素应用视差、平滑过渡等效果,配合 GPU 硬件加速确保流畅运行,尤其适合用于追求强视觉冲击力的官网场景。

地址:locomotivemtl.github.io/locomotive-…

案例:Wizardry Variants Daphne

3.gif

协同使用 Locomotive Scroll(负责平滑滚动)和 GSAP ScrollTrigger(负责基于滚动的动画触发)来创建复杂的交互效果。smartphone 和 tablet 的配置确保在各种移动设备上都能保持平滑滚动体验。lerp: .1 让滚动带有轻微阻尼感,配合魔法场景强化世界观的沉浸感,touchMultiplier: 2 则优化移动端体验,触摸滚动灵敏度倍增,让手机玩家滑动时能快速切换场景。

gsap.registerPlugin(ScrollTrigger);
locomotiveScroll = new LocomotiveScroll({
  el: document.querySelector('.js-root'), 
  smooth: true,                          
  smartphone: { smooth: true },        
  table: { smooth: true },            
  touchMultiplier: 2,                
  lerp: 0.1                           
});

1.2 过渡与动效

为页面元素的状态变化添加流畅的过渡效果,优化官网内容切换的节奏感与视觉连贯性。

💠 Animate.css

Animate.css 是一款轻量级纯 CSS 动画库,提供了多达 60 多种 预设的动画效果(如淡入淡出、滑动、弹跳、旋转等),覆盖页面元素加载、交互反馈、场景过渡等需求。只需为 HTML 元素添加相应的 CSS 类名(例如 animate__animated 基础类和 animate__fadeIn 淡入)即可快速添加动画。

地址:animate.style/

案例:重返未来:1999

5.gif

世界板块 视觉内容(游戏图片)animate__fadeInLeft 从左侧淡入,右侧文字描述 animate__fadeInUp 随后浮现。影音板块 视频/图集/音乐 animate__fadeInUp 统一从下方淡入。通过 animate__delay-* 实现 阶梯式延迟(0s, 1s, 2s),形成依次入场的效果。

<!-- 世界 -->
<div class="swiper-slide pc backstory" data-mouse="small" id="slide4">
    <div class="backstory-left animate__animated animate__fadeInLeft" data-mouse="small">
        <div class="swiper mySwiper backstory-border" id="pcbackstory" data-mouse="small">
            <!-- 视觉内容(游戏图片) -->
        </div>
    </div>
    <div class="backstory-right" data-mouse="small">
        <div class="backstory-right-str animate__animated animate__fadeInUp" data-mouse="small" id="backstoryStr">
            1999年最后一天,“暴雨”降临世界:<br>
            地面无故溢起积水,你的指尖碰到飞升的雨滴—— 一场“暴雨”在向天空倾泻。<br>
            行人和墙壁在雨中剥落溶解,世界似乎来到一个崭新的旧时代。<br>
            而除了你之外的所有人,都在“暴雨”侵蚀后不知所踪。<br>
            1999年的秘密,藏在层层雨幕的背后,藏在1999年最后一天。
        </div>
    </div>
</div>

<!-- 影音 -->
<div class="swiper-slide pc gallery" data-mouse="small" id="slide5">
    <div class="gallery-top" data-mouse="small">
        <div class="gallery-top-1 animate__animated animate__fadeInUp" data-mouse="small" onclick="openVideomask()">
            <!--游戏视频-->
        </div>
        <div class="gallery-top-2 animate__animated animate__fadeInUp animate__delay-1.5s" data-mouse="small"
            onclick="openPapermask()"> 
            <!--游戏图集-->
        </div>
        <div class="gallery-top-3 animate__animated animate__fadeInUp animate__delay-2s" data-mouse="small"
            onclick="openMusicmask()"> 
            <!--游戏音乐-->
        </div>
    </div>
</div>

💠 AOS

AOS(Animate On Scroll)是一款专注于 “滚动触发动画” 的轻量级 JavaScript 库,核心功能是监测页面元素滚动至视口范围时,自动触发淡入、缩放、位移、旋转等预定义动效。既以轻量化特性(核心体积仅 15KB)避免性能损耗,又强化了内容浏览的叙事层次感。

地址:michalsnik.github.io/aos/

案例:Counter-Strike 2

6.gif

只需通过简单的 HTML 属性进行配置,即可快速实现动画。例如作为页面最顶部的核心宣传语,长时长(data-aos-duration="2500")的淡入动画(data-aos="fade-up")让文字缓慢浮现,比其他元素稍晚出现(data-aos-delay="600"),创造一种错落有致的入场节奏。

<div class="aos-init aos-animate" data-aos="fade-up" data-aos-delay="600" data-aos-duration="2500">
    “反恐精英历史上最大的技术飞跃。”
</div>
<div class="aos-init aos-animate" data-aos="fade-in" data-aos-delay="500" data-aos-duration="1000">
了解更多
</div>
<div class="aos-init aos-animate" data-aos="fade-right" data-aos-delay="100" data-aos-duration="1500">
反恐精英 预告片
</div>

1.3 动画引擎

当涉及复杂动画时,专业引擎可实现复杂的时间线动画、物理动效与骨骼动画。

💠 Lottie

Lottie.js 是 Airbnb 开源的一个轻量级动画渲染库,核心功能是将 After Effects 导出的 Lottie 格式(JSON 文件)动画在网页端直接渲染,能流畅实现骨骼动画、粒子特效、路径动画等复杂效果,让设计师的创意能还原为前端可交互的沉浸式动画。

地址:github.com/LottieFiles…

案例:逆水寒

4.gif

将设计师在 AE 中制作的图标动画还原到网页,使用 renderer: "svg" SVG 渲染可保证在任何屏幕尺寸下不失真。鼠标悬停触发动画播放,当鼠标移开时,动画立即停止并跳回第一帧,这确保了下次悬停时动画总是从开头播放。

createLottieAnim = function(e) {
  var n = window.lottie.loadAnimation((0, o.default)({
    renderer: "svg", 
    loop: true, 
    autoplay: true
  }, e, {
    path: "https://n.res.netease.com/pc/zt/20210308165742/" + e.path
  }));

  return "hover" === e.event && $(e.hoverElem || e.container).hover(
    function() { n.play() }, 
    function() { n.stop() }  
  ),
  n;
}

💠 GSAP

GSAP(GreenSock Animation Platform)是一款功能强大、性能卓越的专业动画引擎,它能够高效地创建从简单过渡到复杂序列的各类动画,支持驱动 DOM 元素、SVG、Canvas、3D 模型(如 Three.js)、骨骼动画(如 Spine)等多类型载体的动画。

地址:gsap.com/

案例:Honkai: Star Rail – May this journey lead us starward

7.gif

GSAP 在案例中被用来驱动 Three.js 3D 对象 getObjectByName("s_line") 的属性,创建出流畅和富有质感的交互效果。不仅可以实现常规的缩放 scale、旋转 rotation 动画,还可以变化着色器的 uniforms 变量,实现亮度增加的材质动画。

this.focusOver = function(e) {
    var t = e.getObjectByName("s_line");
    gsap.to(t.scale, .8, {
        x: .9 * t.userData.initScl.x,
        y: .9 * t.userData.initScl.y,
        ease: o.Back.easeInOut,
        overwrite: 1
    }),
    gsap.fromTo(t.rotation, .8, {
        z: t.userData.initRot.z
    }, {
        z: t.userData.initRot.z + 1,
        ease: o.Back.easeOut,
        overwrite: 1
    }),
    gsap.to(t.material.uniforms.brightness, .2, {
        value: .2,
        overwrite: 1
    })
}

💠 Three.js

Three.js 是基于 WebGL 的 Web 3D 渲染引擎,提供了简洁易用的 API,使开发者无需掌握深厚的图形学知识,就能在网页浏览器中高效创建和展示交互式的 3D 场景、动画和模型。内置了灯光、阴影、材质、几何体、相机控制等丰富的 3D 图形功能,并支持导入多种格式的 3D 模型。

地址:threejs.org/

案例:第五人格

10.gif

案例展示了经典和实用的 3D 角色展示方案,添加环境光 AmbientLight 提供基础亮度,平行光 DirectionalLight 模拟主光源。加载GLTF格式的3D模型,并通过 AnimationMixerclipAction 播放模型自带的动画,同时支持玩家通过拖拽来旋转模型。

// 环境光
var i = new THREE.AmbientLight(16777215, 1);
// 平行光
var n = new THREE.DirectionalLight(16777215, 1);

// 初始化GLTF模型加载器(游戏常用3D格式,支持模型+动画)
var c = new THREE.GLTFLoader; 
var h = e; // e为模型文件路径(如“survivor_doctor.glb”,角色的3D模型)

c.load(h, function(e) { 
  u = e.scene; 
  // 初始化动画混合器(控制角色动画播放)
  var o = e.animations; 
  m = new THREE.AnimationMixer(u);
  var i = m.clipAction(o[0]); 
  i.play();
  s.add(u); 
})

// 鼠标移动:计算偏移,更新模型旋转
window.addEventListener("mousemove", function(e) {
  v.ex = e.pageX; 
  var o = v.ex - v.sx; 
  u.rotation.y = v.rt + .01 * o; // 更新模型旋转角(0.01为旋转速度,避免过快)
});

💠 PixiJS

PIXI.js 是一款开源、高性能的 2D 渲染引擎,核心优势在于基于 WebGL 硬件加速的高效绘制能力,能以极低的性能损耗渲染大量 2D 元素,同时兼容 Canvas 作为降级方案。支持精灵 Sheet 优化资源加载、骨骼动画驱动角色动作、鼠标 / 触摸交互检测(如点击、拖拽、碰撞)等功能。

地址:pixijs.com/

案例:Crystal of Atlan

案例展示了基于 PixiJS 实现游戏角色的展示,利用 Assets 系统统一预加载、缓存角色资源,提升加载速度。静态角色用轻量 Sprite 节省性能,动态角色用 Spine 骨骼动画增强表现力。并通过 getLocalBounds() 获取骨骼动画的边界框,结合自定义的偏移量来精确调整位置。

11.gif

// 注册角色资源到Pixi的资源管理器
Ni(IK).call(IK, (function(t) {
  t.characterImgUrl && e.Assets.add(t.pinyin, t.characterImgUrl),
  t.spineUrl && e.Assets.add(t.pinyin, t.spineUrl)
}));

// 后台预加载所有注册的角色资源
window.PIXI.Assets.backgroundLoad(
  Vr(IK).call(IK, (function(e) { return e.pinyin })) 
);

Ni(IK).call(IK, (function(t, n) {
  e.Assets.load(t.pinyin).then((function(r) {
    ...
    // 静态角色图片:创建Pixi精灵(Sprite)
    if (t.characterImgUrl) {
      CK[n] = new e.Sprite(r); 
    } 
    // 动态骨骼动画:创建Spine动画对象
    else {
      var a = new e.spine.Spine(r.spineData); 
      a.skeleton.setToSetupPose(); 
      a.update(0); 
      ...
      // 计算骨骼动画的本地边界(用于定位调整)
      var s, u, l = a.getLocalBounds();
      
      // 调整骨骼动画位置(基于边界计算,确保角色锚点正确)
      a.position.set(
        -l.x + (t.skelOption.characteX || 0), // X轴偏移(可自定义微调)
        -l.y + (t.skelOption.characterY || 0)  // Y轴偏移
      );
      ...
    }
  }));
}));

1.4 轮播与滑动

以可交互的滑动组件高效展示预告视频、角色立绘与新闻资讯等内容。

💠 Swiper

Swiper 是一款滑动交互组件库,它提供丰富的配置项(如自动播放、自定义分页器、过渡动画时长),内置淡入淡出、滑动、 cube 3D 等过渡效果,使开发者能够轻松构建响应式的轮播图、画廊、内容滑块及选项卡切换等交互组件,同时兼容框架如 React、Vue 等。

地址:swiperjs.com/

案例:哈利波特:魔法觉醒

8.gif

案例通过 coverflow 3D 效果,结合 centeredSlides(当前卡片居中放大)和 slidesPerView(自动适配数量),打造了一个3D立体翻转的轮播图。其中 rotate 旋转角度创造出卡牌翻转的视觉效果;stretch 拉伸强度影响卡牌之间的间距和变形;depth 深度控制前后堆叠的层次感。

this.featureSwiper = new Swiper('.feature_container', {
    effect: 'coverflow',
    centeredSlides: true,
    slidesPerView: 'auto',
    coverflow: {
        rotate: 50,
        depth: 100,
        stretch: 120,
        slideShadows: false,
    },
})

💠 Flickity

Flickity 专注于创建 流畅的触摸滑动组件(如轮播图、内容滑块),主打模拟真实物理惯性的滑动体验,核心特色是支持 非固定帧自由拖拽(freeScroll 模式)—— 用户可随意滑动浏览内容,无需像传统轮播那样强制切换完整幻灯片,配合自然的惯性衰减动效,还原 “随手翻阅卡片” 的真实触感。

地址:flickity.metafizzy.co/

案例:Baldur's Gate 3

9.gif

案例使用 Flickity 创建了一个用于展示奖项荣誉的自动轮播组件,启用 draggable 意味着在移动设备上用户可以自然地进行触控滑动,而在桌面端也可能支持鼠标拖拽。结合 wrapAround 实现的无限循环,无限循环让玩家可反复浏览,也避免 “滑到尽头后无法继续” 的生硬体验。

const slider = new window.Flickity(document.getElementById("awards"),{
adaptiveHeight: false,
    cellAlign: "left",
    wrapAround: true,
    draggable: true,
    autoPlay: true
})

二、工具类集成

工具库能快速为官网添加复杂功能,降低开发成本,覆盖社媒、支付、安全等全场景需求。

2.1 社交与分享

社交平台组件方便玩家分享内容并展示社区动态,扩大传播范围。

💠 Facebook Widgets

Facebook Widgets 公共主页插件是由 Facebook 官方提供 的工具,允许将 Facebook 公共主页直接嵌入到网站上。用户无需离开当前网站即可查看主页的封面、帖子流、活动信息,并能直接进行 点赞、关注、分享 等互动操作,有效帮助提升粉丝数量和内容曝光度,加强与社交媒体的联动。

地址:developers.facebook.com/docs/plugin…

案例:浮生憶玲瓏

13.gif

案例的核心配置如下:

  • data-tabs="timeline":设置显示 “时间线”(主页动态流)
  • data-adapt-container-width:在手机等窄屏设备上自动缩小宽度(保持比例),避免插件溢出容器导致的布局错乱,确保移动端用户也能正常查看。
  • data-small-header="false":显示完整头部(含主页名称 “浮生憶玲瓏”、粉丝数)。
  • data-hide-cover="false":展示 Facebook 主页的封面图(通常是游戏宣传图、角色插画)。
  • data-show-facepile="true":显示互动粉丝的头像(如点赞、评论过的玩家)。
<div class="facebook_box">
    <div class="facebook">
        <div class="fb-page" 
        data-href="https://www.facebook.com/fsyll.tw" 
        data-tabs="timeline" 
        data-width="500" 
        data-height="270" 
        data-small-header="false" 
        data-adapt-container-width="true" 
        data-hide-cover="false" 
        data-show-facepile="true"
        >
        <blockquote cite="https://www.facebook.com/fsyll.tw" class="fb-xfbml-parse-ignore">
        <a href="https://www.facebook.com/fsyll.tw">浮生憶玲瓏</a>
       </blockquote>
    </div>
    </div>
</div>

2.2 媒体播放

提供跨浏览器的视频播放解决方案,展示游戏预告片、实机演示和直播流等。

💠 YouTube 播放器

以下是嵌入 YouTube 视频的两种主要方式:

<iframe> 嵌入

直接 <iframe> 嵌入是 最简单、最快捷 的方法,只需从 YouTube 分享界面复制现成的 <iframe> 代码并粘贴到 HTML 中即可。这种方法无需编写 JavaScript 代码,适合需要快速、简单地在网页上静态展示视频的场景,但交互控制有限,仅能满足基础播放需求。

地址:www.youtube.com

案例:ELDEN RING NIGHTREIGN

15.gif

案例中的核心参数如下:

  • autoplay=1&mute=1&loop=1&playlist=同一ID:自动播放且静音,现代浏览器默认禁止有声自动播放。loop 配合 playlist 指定同一视频 ID 确保视频持续循环。
  • rel=0:隐藏相关视频推荐,避免用户被其他视频分流。
  • hd=1:优先加载 720p 或更高清 画质,确保画面质感。
  • loading="lazy":首屏懒加载,避免因视频资源过大导致页面卡顿。
  • allow="accelerometer; autoplay; ...; picture-in-picture":声明允许的浏览器功能,支持移动端陀螺仪(增强横屏体验)、自动播放、画中画等,适配多设备交互。
<iframe id="ytplayer" frameBorder="0" allowfullscreen="" loading="lazy" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
title="ELDEN RING NIGHTREIGN" width="100%" height="100%" 
src="https://www.youtube.com/embed/UABQQ5TyGNU?autoplay=1&amp;loop=1&amp;playlist=UABQQ5TyGNU&amp;mute=1&amp;hd=1&amp;controls=0&amp;rel=0&amp;fs=0&amp;enablejsapi=1&amp;origin=https%3A%2F%2Fbandainamcoent.asia&amp;widgetid=1">
</iframe>

Player API

加载 API 脚本并通过 JavaScript 控制视频,可以实现复杂的交互,例如控制视频的播放、暂停、跳转,监听播放器的状态变化(如开始播放、暂停、缓冲等),以及动态加载播放列表或其他视频。适合需要高度自定义交互和复杂功能的场景。灵活性远高于直接 iframe 嵌入,但需一定开发成本。

地址:developers.google.com/youtube/ifr…

案例:Seven Knights Idle Adventure

16.gif

案例封装了 YouTube 视频播放器组件,onYouTubeIframeAPIReady 回调注册播放状态常量。使用计算属性来定义播放器的各项参数(尺寸、视频ID、按钮列表等),使得播放器组件可复用。并在 beforeDestroy 中调用 player.destroy() 销毁播放器实例防止内存泄漏。

window.onYouTubeIframeAPIReady = function() {
  o.YT = YT;
  var t = YT.PlayerState; 
  
  o.events[t.ENDED] = "ended",
  o.events[t.PLAYING] = "playing",
  o.events[t.PAUSED] = "paused",
  o.events[t.BUFFERING] = "buffering",
  o.events[t.CUED] = "cued";
  
  o.Vue.nextTick((function() {
    o.run()
  }));
};
...
computed: {
  youtube: function() { return this.item.args.youtube || { id: "" } }, 
  youtubeId: function() { return this.youtube.id },
  playerId: function() { return "".concat(this.item.id, "-player-").concat((new Date).getTime()) }, 
  width: function() { 
    return this.youtube.width ? "".concat(parseInt(this.youtube.width), "px") : "pc" === this.device ? "74vw" : "700px"
  },
  height: function() { 
    return this.youtube.height ? "".concat(parseInt(this.youtube.height), "px") : "pc" === this.device ? "".concat(41.625, "vw") : "393px"
  },
  vars: function() { return d({ rel: 0, wmode: "opaque" }, this.youtube.vars) } 
}
...
beforeDestroy: function() {
  "function" == typeof this.player.destroy && this.player.destroy()
}

💠 HLS.js

HLS.js 是一款开源的流媒体播放库,能让不支持 HTTP Live Streaming (HLS) 协议的现代浏览器,将视频流(如 MPEG-TS 片段)转换为浏览器可播放的格式(如 MP4),从而实现在网页中原生、流畅地播放 HLS 直播或点播视频,并支持 自适应码率 (ABR) 等关键功能以提升观看体验。

地址:github.com/video-dev/h…

案例:薩爾達傳說 王國之淚

image.png

案例自动播放、循环播放结合 hls.js 的分片加载能力,让视频在不同网络环境下都能流畅播放(如弱网时自动切换低清晰度分片)。优先使用 hls.js 播放 hls.loadSource(针对 Chrome、Firefox 等),否则降级使用 Safari 等浏览器的原生 HLS 支持,确保了几乎所有现代浏览器都能正常播放视频。

if (video.classList.contains('is_hls_ss')) {
  // 获取HLS源(.m3u8路径)
  var src = video.querySelector('source.active').dataset.src;
  // 初始化hls.js实例
  var hls = new (hls_default())();
  
  // 方案1:浏览器支持hls.js(如Chrome、Firefox)
  if (hls_default().isSupported()) {
    hls.loadSource(src); // 加载HLS源(解析.m3u8索引,获取.ts分片)
    hls.attachMedia(video); // 将HLS流关联到video元素
  } 
  // 方案2:浏览器原生支持HLS(如Safari)
  else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = src; // 直接设置src为.m3u8,利用原生支持
    video.load();
  }
  
  // 监听视频加载完成事件(确保播放准备就绪)
  video.addEventListener('loadedmetadata', resolve);
  video.addEventListener('canplay', resolve);
}

💠 Video.js

Video.js 用于构建功能丰富、兼容性极强的网页视频播放器。不仅支持播放 MP4、WebM 等传统视频格式,还兼容 HLS、DASH 等现代自适应流媒体协议,确保了视频在不同浏览器和设备上的一致性与流畅体验。广泛应用于需要稳定、灵活视频播放解决方案的网站。

地址:videojs.org/

案例:Splatoon™ 3 for Nintendo Switch™

17.gif

案例实现了基于 Cloudinary 流媒体服务和 Video.js 的视频播放器。["hls/h265", "hls/h264"] 优先使用更高效的 H.265 编码,不支持则自动降级至兼容性更广的 H.264。accent: "#E60012" 将控制栏、进度条等元素的主题色设置为品牌色。volumechange 事件 + Cookie 存储监听音量变化,确保用户下次访问时保持之前的音量设置。

this.videoPlayerOptions = function() {
  ...
  return {
    ...
    source: {
      ...
      sourceTypes: e.startsWith("Legacy Videos/") ? ["hls/h264"] : ["hls/h265", "hls/h264"],
      sourceTransformation: {
        "hls/h264": [{ streaming_profile: "full_hd" }],
        "hls/h265": [{ streaming_profile: "h265_full_hd" }]
      }
    },
    ...
    colors: { accent: "#E60012", text: "#FFF" }, 
    ...
  };
};
...
this.volumeChange = function() {
  nclood.Cookie.set("nintendoVideoVolume", this.player.volume(), {
    maxAge: 365 * 24 * 60 * 60, 
    domain: n, path: "/"
  });
};

2.3 音频

控制角色语音、背景音效的播放与 3D 音效效果,增强官网的沉浸感和氛围感。

💠 Howler.js

howler.js 是一款轻量且功能强大的 JavaScript 音频库,它通过封装 Web Audio APIHTML5 Audio,为现代 Web 应用提供了简洁统一的 API。具备音频精灵、空间音效、音量控制、自动缓存、淡入淡出等高级功能,是网站处理音效与背景音乐的理想选择。

地址:github.com/goldfire/ho…

案例:Honkai: Star Rail

18.gif

案例基于 howler.js 构建了一个音频管理器,背景音乐 loop: true + preload: true,确保页面加载后无延迟循环播放。fade(0, initVolume, initFade) 播放时淡入,fade(volume, 0, 300) 暂停/停止时淡出。rate 播放速率默认1,支持变速播放,如特殊音效加速。

sounds[n] = new Howl({
  src: r, 
  volume: o, // 初始音量
  html5: l, 
  loop: f, 
  preload: d, 
  autoplay: p,
  rate: g // 播放速率(默认1,支持变速播放,如特殊音效加速)
}),
sounds[n].initVolume = o, // 存储初始音量(用于静音后恢复)
sounds[n].initFade = b, // 存储淡入淡出时长(统一音效过渡效果)
...
{
  key: "playSound",
  value: function(e) {
    var t = this, n = this.sounds[e];
    if (n) {
      var r = function() {
        !t.muted && n._initFade && n.fade(0, n._initVolume, n._initFade), // 淡入效果
        n.play()
      };
      "loaded" !== n.state() ? (n.once("load", r), n.load()) : r() // 未加载则先加载再播放
    } else console.warn("no sound: " + e)
  }
},
{
  key: "pauseSound",
  value: function(e, t) {
    var n = this.sounds[e];
    n && (t ? (n.fade(n._volume, 0, 300), n.once("fade", (function() {
      n.pause() // 淡出后暂停
    }))) : n.pause())
  }
}

💠 SoundManager

SoundManager 是一款老牌开源的音频管理库,提供了可靠且功能丰富的音频播放能力。简化了音频资源的加载、播放控制(如播放、暂停、音量调节、循环播放)和事件监听,并支持音频的淡入淡出等高级效果,极大地简化了在网页中集成音效、背景音乐等功能的开发流程。

地址:schillmania.com/projects/so…

案例:Genesis Augmented

案例构建了一套相当完善且用户体验良好的音频管理系统。使用 soundManager.createSound 创建音频对象实例。whileplaying 在播放中同步进度条,用户拖拽进度条时调用 setPosition 跳转播放位置。监听 blur/focus 事件实现页面切换时的音频淡入淡出。

this.api = soundManager.createSound({ 
  volume: 100,
  whileplaying: function() { this.step() }, // 播放中更新进度
  onplay: function() { t.addClass("nk-audio-plain-playing") }, 
  onpause: function() { t.removeClass("nk-audio-plain-playing") }, 
  onfinish: function() { this.seek(0); this.step(); ... } 
});
e.prototype = {
  // 进度更新:同步进度条与时间显示
  step: function() {
    var t = this.api.position || 0;
    this.progress = t / this.api.duration;
    this.$timer.html(this.formatTime(Math.round(t))); 
    this.$progress.css("width", "".concat(100 * this.progress || 0, "%"));
  },
  // 拖拽进度条跳转播放位置
  seek: function(t) {
    this.api.setPosition(this.api.duration * t); // t为0-1的比例值
  }
};
// 页面离开(blur)时淡出暂停,返回(focus)时淡入恢复
k.$wnd.on("blur focus", function(n) {
  setTimeout(function() {
    if ("blur" === n.type) {
      !a.paused && a.playState && (i = !0,
      t = a.volume,
      e = 1e3 / Math.abs(+t),
      clearInterval(l),
      l = setInterval(function() { // 淡出到0音量后暂停
        t = 0 < t ? t - 1 : t + 1,
        a.setVolume(t),
        0 === t && (clearInterval(l), a.pause())
      }, e))
    } else i && (i = !1, v()); // 恢复时淡入
  }, 0)
}));

2.4 评论系统

集成第三方服务,快速为官网添加用户评论、互动功能,构建社区氛围。

💠 Disqus

Disqus 是一款广泛应用的第三方嵌入式评论系统,为网站提供便捷的用户互动解决方案。它支持用户通过社交媒体账号(如 Google、Facebook)或 Disqus 账号登录,实现评论、回复、点赞、分享等功能,同时提供跨平台评论同步,提升用户体验的连贯性。

地址:disqus.com/

案例:Stardew Valley

image 1.png

案例展示了如何将 Disqus 评论系统集成到网站中,把 ID、URL、标题等注入到 JavaScript 变量中,Disqus 脚本获取并显示该页面对应的评论。language 配置确保不同地区玩家看到对应语言的评论区界面;sso 单点登录减少玩家评论门槛(无需单独注册 Disqus 账号),提升参与度。

<script type='text/javascript'>
/* <![CDATA[ */
var embedVars = {"disqusConfig":{"integration":"wordpress 3.0.17"},"disqusIdentifier":"1926 https:\/\/www.stardewvalley.net\/?p=1926","disqusShortname":"stardewvalley","disqusTitle":"Stardew Valley 1.5.5 Released on PC","disqusUrl":"https:\/\/www.stardewvalley.net\/stardew-valley-1-5-5-released-on-pc\/","postId":"1926"};
/* ]]> */
</script>
var disqus_config = function () {
  var dsqConfig = embedVars.disqusConfig;
  this.page.integration = dsqConfig.integration; // 声明集成环境为WordPress(Disqus适配其数据交互)
  this.page.remote_auth_s3 = dsqConfig.remote_auth_s3; // 远程认证参数(支持官网用户体系与Disqus联动,如自动登录)
  this.page.api_key = dsqConfig.api_key; // API密钥(用于Disqus高级功能,如数据统计)
  this.sso = dsqConfig.sso; // 单点登录配置(玩家可用官网账号直接登录评论区,无需重复注册)
  this.language = dsqConfig.language; // 评论区语言(默认跟随官网,适配全球玩家)

  if (disqus_config_custom) disqus_config_custom.call(this); // 允许自定义扩展配置(如添加评论过滤规则)
};

(function() {
  var dsq = document.createElement('script');
  dsq.type = 'text/javascript';
  dsq.async = true; // 异步加载(不阻塞官网页面渲染,保证玩家浏览更新内容时不卡顿)
  dsq.src = 'https://' + disqus_shortname + '.disqus.com/embed.js'; // 加载Disqus核心脚本(对应星露谷的专属评论脚本)
  // 将脚本插入页面(head或body,确保能正常执行)
  (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();

2.5 客服与反馈

💠 Zendesk

Zendesk 核心产品是一个 全渠道的智能客户服务与互动平台。通过整合网页、邮件、社交媒体、即时聊天、电话等多种沟通渠道,并利用自动化工单系统AI功能(如Answer Bot智能客服和数据分析)以及知识库工具,帮助高效地管理客户查询、优化支持流程并提升客户满意度。

地址:developer.zendesk.com/api-referen…

案例:Splinterlands

24.gif

案例实现了 Zendesk客服组件加载方案。小屏设备不加载插件,大屏动态加载脚本,script.id 确保能正确识别并初始化。通过专属 key 关联游戏定制化客服规则,确保玩家咨询(如链游资产异常、战斗 BUG)能精准分流到对应客服团队,提升问题解决效率。

if (!(window.innerWidth <= 800) && !(window.innerHeight <= 600)) {
var script = document.createElement('script');
script.setAttribute('id', 'ze-snippet');
script.setAttribute('src', 'https://static.zdassets.com/ekr/snippet.js?key=...');
document.head.appendChild(script);
}

三、性能优化与兼容性

确保官网在各种设备与网络环境下都能快速、稳定运行。

3.1 懒加载

延迟加载非关键资源(如图片、视频),提升首屏加载速度。

💠 LazySizes

LazySizes 是一款 图片延迟加载库,它通过智能检测元素是否进入浏览器视口来动态加载图片和 iframe,能显著提升页面加载速度、节省带宽,并因其不会向搜索引擎隐藏内容而保持 SEO 友好性;该库原生支持响应式图像,可自动计算适配屏幕尺寸的图片。

地址:github.com/aFarkas/laz…

案例:Wizardry Variants Daphne

12.gif

案例的只加载 1x1 像素的占位符 data:image/gif;base64,R0l...AA7,使首屏内容可以极速呈现。当滚动即将看到图片时才加载资源,并预设宽高比 data-aspectratio 避免布局抖动。picture 标签优先加载 data-srcset 中的 WebP 图片,否则降级加载 img 标签的 PNG 图片。

<picture>
    <!-- WebP格式图片(优先加载,体积更小) -->
    <source 
        srcset="data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7" 
        data-srcset="/path/to/button.webp" 
        type="image/webp">
    
    <!-- PNG格式图片(降级方案,兼容不支持WebP的浏览器) -->
    <img 
    class="lazyload" 
    src="data:image/gif;base64,R0lGODlhAQABAGAAACH5BAEKAP8ALAAAAAABAAEAAAgEAP8FBAA7" 
    data-src="/path/to/button.png" alt="...">
</picture>

3.2 兼容性适配

解决旧浏览器对 HTML5/CSS3 的支持问题,适配低配置设备。

💠 Modernizr

Modernizr 的核心功能是检测用户浏览器对 HTML5 和 CSS3 各项特性的支持情况。它通过运行一系列快速的测试来判断浏览器是否支持各项特性,使开发者能够基于浏览器实际能力而非浏览器品牌和版本来编写 CSS 规则和 JavaScript 逻辑,从而更优雅地实现渐进增强优雅降级

地址:modernizr.com/

案例:Wizardry Variants Daphne

image 2.png

案例中 Modernizr 检测浏览器是否支持 WebP 格式,在 <html> 标签上添加 .webp 类(支持)或 .no-webp 类(不支持)。混合宏利用这两个类名,为不同浏览器提供对应的图片格式 —— 支持 WebP 的用体积更小的 WebP,不支持的用兼容性更好的 PNG。

@function replace($url) {
  $substr: '.png';
  $newsubstr: '.webp';
  $pos : str-index($url, $substr);
  $strlen : str-length($substr);
  $start : str-slice($url, 0, $pos - 1);
  $end : str-slice($url, $pos + $strlen);
  $url : $start + $newsubstr + $end;
  @return $url;
}
@mixin webp($url) {
  .no-webp & {
    background-image: url($url);
  }
  .webp & {
    background-image: url(replace($url));
  }
}

还有 更多特性检测,例如 Battery APICSS position: stickydetails Element 等等。

image 3.png

💠 HTML5 Shiv

💡 可忽略,是前端老兵的兼容代表方案。随着现代浏览器普及,项目已基本不需要。

HTML5 Shiv 是早期实现跨浏览器兼容性、推动开发者无顾虑采用 HTML5 新标准的重要工具之一。核心作用是让旧版 Internet Explorer 浏览器(特指 IE6-IE8) 能够识别并正确渲染 HTML5 新增的语义化标签(如 <article><section><nav><header> 和 <footer> 等)。

地址:github.com/aFarkas/htm…

案例:Wizardry Variants Daphne

<!--[if lt IE 9]>
    <script src="https://nie.res.netease.com/comm/html5/html5shiv.js "></script>
<![endif]-->

案例通过 HTML5 Shiv 为旧版浏览器提供一个 Polyfill,确保基础的内容和功能仍然可用,实现了优雅降级。同时,只有IE6、IE7、IE8会加载并执行这个脚本,而现代浏览器则会完全忽略这段代码,避免了不必要的资源消耗。

3.3 隐私合规

管理 Cookie 偏好设置、隐私政策生成,满足 GDPR 等全球合规要求。

💠 Cookiebot

Cookiebot 是一款即插即用式 Cookie 同意管理平台(CMP),其主要功能是通过在网站上嵌入可定制的支持 46 种语言的同意横幅、自动扫描并分类Cookie及追踪技术,并在获得用户明确同意前阻止这些技术的执行,来帮助网站所有者遵守 GDPRCCPA 等全球数据隐私法规。

地址:www.cookiebot.com/

案例:Home of the Cyberpunk 2077 universe

19.gif

案例实现了一个定制化的 Cookie同意管理解决方案,不仅完成了基本的合规要求,还实现了模态框集成和品牌化定制。根据 lang 设置 data-culture 属性智能识别用户语言,监听 CookiebotOnDialogDisplay 事件替换图片为自定义品牌图标。

var s = new n.modal({
  cssClass: ["cookie-declaration-modal"], // 自定义样式类(赛博朋克风格适配)
  onOpen: function() {
    ...
    s.close(), // 临时关闭,等待Cookiebot内容加载
    // Cookiebot声明加载完成后,检查内容是否溢出(确保显示完整)
    window.CookiebotCallback_OnDialogLoad = function() { s.checkOverflow() }
  },
  onClose: function() {
    delete window.CookiebotCallback_OnDialogLoad // 清除回调,避免内存泄漏
  }
});

// 点击“Cookie声明”链接时触发
i.addEventListener("click", (function(t) {
  t.preventDefault(); 
  var e = document.documentElement.lang; // 获取页面当前语言(如pt-br、zh-cn)
  var o = document.querySelector(".cookie-declaration-modal .tingle-modal-box__content");
  // 动态创建Cookiebot声明脚本
  var n = document.createElement("script");
  n.id = "CookieDeclaration",
  n.async = !0,
  // 根据页面语言设置Cookie声明的显示语言(多语言适配)
  n.setAttribute("data-culture", 
    "pt-br" === e || "pt-BR" === e ? "pt" : 
    "zh-cn" === e ? "zh" :
    "zh-tw" === e ? "zu" : 
    e 
  ),
  // 加载Cookiebot的Cookie声明脚本(关联官网的Cookiebot账户ID:acc3ad63-...)
  n.src = "https://consent.cookiebot.com/acc3ad63-2aea-464b-beeb-bd0b8a85bc05/cd.js",
  o.appendChild(n), 
  s.open() 
}));

// 监听Cookiebot同意弹窗显示事件
window.addEventListener('CookiebotOnDialogDisplay', function (e) {
  // 替换Cookiebot默认的“Powered by”图标为游戏自定义图标
  var el = document.getElementById('CybotCookiebotDialogPoweredbyImage');
  if (el) el.src = 'https://cyberpunk-static.qtlglb.com/build/images/cookies-icon-03723b68.png';
}, false);

💠 OneTrust

OneTrust 是一个企业级的隐私合规与数据治理平台,通过 Cookie 自动扫描、动态国家/州特定同意横幅、偏好中心及基于 Cookiepedia 数据库的预分类技术,帮助组织遵守 GDPR、CCPA 等全球隐私法规。可深度定制同意弹窗样式与交互流程,通过自动化工具简化合规流程、降低运营成本。

地址:www.onetrust.com/

案例:ELDEN RING NIGHTREIGN

20.gif

案例采用 React SSR + CSP 安全策略 + 动态脚本注入的方式,cdn-apac 亚太区CDN,加速亚洲用户访问。data-document-language 自动读取 HTML lang 属性切换语言。OptanonWrapper OneTrust 要求的全局函数钩子,用于在同意状态变更时执行自定义逻辑。

<meta http-equiv="Content-Security-Policy" content="script-src * data: https://cdn-apac.onetrust.com/scripttemplates/otSDKStub.js 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com 'unsafe-inline' 'unsafe-eval';"/>

// 加载OneTrust核心SDK脚本
h.jsx)(F.default, {
  src: "https://cdn-apac.onetrust.com/scripttemplates/otSDKStub.js", // 亚太区CDN,加速亚洲用户访问
  "data-document-language": "true", // 自动检测页面语言,适配多语言同意弹窗
  type: "text/javascript",
  charSet: "UTF-8",
  "data-domain-script": "e44e50cd-1032-4426-a8a0-0304ff8a035b-test" // 关联官网的OneTrust配置ID(测试环境)
}),

// 定义OneTrust回调函数(用户同意操作后触发)
h.jsx)(F.default, {
  id: "optanon-wrapper",
  dangerouslySetInnerHTML: {
    __html: "function OptanonWrapper() { }" // 默认空实现,可扩展为同意后启用分析/广告脚本
  }
})

💠 Osano

Osano 通过SaaS模式为企业提供一套完整的隐私合规解决方案,帮助企业自动化遵守全球隐私法规(如GDPR、CCR)。其核心平台集成了同意管理(Cookie横幅)、数据主体权利请求(DSAR)处理、供应商风险监控、数据映射和隐私评估等功能,特色是能够通过单行代码快速部署。

地址:www.osano.com/

案例:VALORANT

21.gif

案例基于 Osano 结合 Google Consent Mode,gtag('consent','default',{ 'ad_storage':'denied', ... }) 获取用户同意前默认拒绝所有广告、分析类的数据存储。点击自定义按钮时,通过window.Osano.cm.showDrawer()打开 Osano 的偏好设置面板,让用户可随时修改同意选项。Cookie 中携带 geo=SGlang=zh-CN,Osano 自动匹配当地法律要求(如 PDPA)。

{/* 加载Osano核心脚本,绑定官网专属合规政策 */}
<script
  id="osano-script"
  src={`https://cmp.osano.com/16BZ95S4qp9Kl2gUA/${page.osanoPolicyId}/osano.js`}
/>

{/* Google Consent脚本:默认禁用所有数据存储,等待用户同意 */}
gtag('consent','default',{
  'ad_storage':'denied',
  'analytics_storage':'denied',
  'ad_user_data':'denied',
  'ad_personalization':'denied',
  'wait_for_update': 500
});
gtag("set", "ads_data_redaction", true);

document.cookie = "osano_consentmanager_uuid=...; geo=SG; lang=zh-CN";

3.4 验证码

通过人机验证等手段,保护官网表单和业务接口免受机器人和恶意程序的攻击。

💠 GeeTest

GeeTest是一家来自中国的 交互安全服务提供商,其核心产品是 基于人工智能与行为式验证技术的智能验证码系统。能有效防御垃圾注册、撞库登录、恶意刷票等自动化攻击。与传统依赖文字扭曲识别的验证码不同,GeeTest 提供了如滑动拼图、图标点选等多种更具用户体验的验证形式。

地址:www.geetest.com/

案例:鸣潮

22.gif

案例集成 GeeTest 4.x 版本人机验证,language 多语言适配验证界面,riskType: "slide" 指定验证类型为 滑动验证,过 onReady/onSuccess/onError 等事件,同步控制加载弹窗的显示 / 隐藏,让用户清晰感知验证流程状态(如 “加载中→验证界面→验证完成”)。

function geetest(D, S) {
  return Ne(this, null, function*() {
    return yield new Promise( (E, x) => {
      // 语言映射:将前端语言标识转为GeeTest支持的格式
      const U = { "zh-Hans": "zho", "zh-Hant": "zho-tw", ja: "jpn", en: "eng" };
      const Y = { lot_number: "", captcha_output: "", pass_token: "", gen_time: "" };

      if (typeof initGeetest4 != "function") { S(Y); E(Y); return }

      Modal.showLoading(), // 显示加载弹窗,提升用户感知
      initGeetest4({
        captchaId: commonIds.captchaId, 
        language: (V = U[D]) != null ? V : "zho", // 适配验证界面语言
        riskType: "slide", // 指定验证类型为“滑动验证”
        product: "bind" 
      }, function(R) {
        // 验证组件加载完成:隐藏加载弹窗,显示验证界面
        R.onReady(function() { Modal.hideLoading(); R.showCaptcha() });
        // 验证成功:获取验证参数并返回(供登录接口使用)
        R.onSuccess(function() { S(R.getValidate()); E(R.getValidate()) });
        // 验证错误/失败/关闭:清理状态,记录日志
        R.onError(function(Z) { Modal.hideLoading(); x(Z); log("onError", Z) });
        R.onFail(function(Z) { Modal.hideLoading(); log("onFail", Z) });
        R.onClose(function() { Modal.hideLoading() });
      })
    })
  })
}

💠 reCAPTCHA

reCAPTCHA 是谷歌提供的验证码服务,旨在通过各种验证方式(如图像识别、滑块验证、点击验证等)区分人类用户和自动化机器人,广泛应用于网站登录、注册、评论等场景,以防止恶意攻击和滥用。同时支持多种语言和自定义样式,帮助网站开发者轻松集成并保护网站安全。

地址:developers.google.com/recaptcha

案例:Cyberpunk: Edgerunners

23.gif

案例用于保护订阅的表单,防止机器人自动提交。data-size="invisible" 使用隐形验证 —— 后台静默分析用户行为,仅当判定为高风险时才弹出验证,极大减少对用户的干扰。data-callback 指定验证成功后的回调函数,grecaptcha.execute() 执行验证。

<!-- 启用“隐形验证”模式 -->
<div id='recaptcha' class="g-recaptcha" 
     data-sitekey="6Lfta6oUAAAAAE5W9wJ12TZ9WBz7gAEANTt3UmoN"  
     data-callback="submitNewsletterForm" 
     data-size="invisible">  
</div>
n.on("submit", (function(t) {
  t.preventDefault(), // 拦截表单默认提交
  !c.prop("disabled") && (s ? grecaptcha.execute() : f(new FormData(n[0])))
}))

window.submitNewsletterForm = function(t) {
  f(new FormData(n[0])) // 验证成功后,提交表单
}

四、UI 组件与样式

构建用户界面的视觉语言、基础构件和设计规范,塑造游戏官网品牌视觉风格。

4.1 UI 框架/库

提供一套预置的样式类和组件,助力开发者快速构建符合游戏风格的界面。

💠 Tailwind CSS

Tailwind CSS 是一款以实用优先(Utility-First) 为核心的开源 CSS 框架,它提供海量原子化 CSS 工具类(如mt-4flexbg-blue-500),开发者可直接在 HTML 标签中组合这些工具类快速构建自定义界面,无需编写大量冗余的自定义 CSS。支持高度可定制的主题系统(自定义颜色、字体、间距),能 显著提升开发效率 并 确保设计一致性

地址:tailwindcss.com/

案例:FINAL FANTASY XVI | SQUARE ENIX

25.gif

案例使用 Tailwind CSS 构建的响应式导航栏组件。全程使用 px-4, mx-5, mb-[1px] 等原子类,消除 magic number。利用 lg/md/2xl 断点,兼顾 PC、平板、手机玩家的导航体验。data-[open=true]:-rotate-180 通过 数据属性状态类,实现下拉菜单展开时箭头旋转 180° 的交互。

<nav class="sticky top-[var(--header-bar-pos)] z-50 flex h-20 w-full items-center justify-center bg-gradient-to-b from-black/80 to-transparent font-bold text-white">
<div class="flex w-full max-w-screen-2xl items-center justify-between px-4 2xl:px-0">
<ul class="mx-5 hidden flex-1 flex-row lg:flex">
<li class="mx-5 cursor-pointer"></li>
...
<li class="mx-5 cursor-pointer">
<div class="relative">
<button type="button" data-open="false" class="flex items-center gap-2 hover:text-hampton data-[open=true]:text-hampton">DLC
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" data-open="false" class="mb-[2px] duration-200 ease-in-out data-[open=true]:-rotate-180">
<path d="M4 6H11L7.5 10.5L4 6Z" fill="currentColor"></path>
</svg>
</button>
<ul data-open="false" class="absolute left-0 mt-1 w-full md:w-auto data-[open=false]:md:hidden">
<li class="mb-[1px] w-full whitespace-nowrap bg-black/60 p-2 text-base leading-none"><a class="hover:text-hampton" href="https://uk.finalfantasyxvi.com/dlc">Expansion Pass</a></li>
<li class="mb-[1px] w-full whitespace-nowrap bg-black/60 p-2 text-base leading-none"><a class="hover:text-hampton" href="https://uk.finalfantasyxvi.com/dlc/echoes-of-the-fallen">DLC: Echoes of the Fallen</a></li>
<li class="mb-[1px] w-full whitespace-nowrap bg-black/60 p-2 text-base leading-none"><a class="hover:text-hampton" href="https://uk.finalfantasyxvi.com/dlc/the-rising-tide">DLC: The Rising Tide</a></li>
</ul>
</div>
</li>
</ul>
<button type="button" class="block lg:hidden">Menu</button>
</div>
</nav>

💠 Bootstrap

Bootstrap 是由 Twitter 公司设计师开发的一款开源前端框架。它提供了一套丰富的预定义样式、组件(如导航栏、按钮、表单)和 JavaScript 插件,并以其灵活的栅格系统为核心,能够自动适配不同尺寸的屏幕。由于其简洁易用、文档完善且具有高度可定制性,被广泛应用于各类网站开发中。

地址:getbootstrap.com/

案例:Genesis Augmented | Official Website

29.gif

案例基于 Bootstrap 实现 响应式核心布局 开发。container 固定宽度容器,自动居中适配不同屏幕,row no-gutters 行容器,移除列之间的默认间距,让分栏更紧凑,是 Bootstrap 栅格的标准组合。col-md-5 是 Bootstrap 断点类(md 对应 768px),平板 / 桌面 两端分栏;手机 两列自动垂直堆叠,避免窄屏挤兑。

<div class="container">
        <div class="row no-gutters" style="justify-content: space-between; text-align: center;">
            <div class="col-md-5 image-margin-top">
                <img id="promo-title" draggable="false" ondragstart="return false;" oncontextmenu="return false;" loading="lazy" src="../crypto/assets/logo_light_sm.webp">
                <h3>Talk to <span class="avatar-select-name">Emma</span></h3>
                <p>- Ask me anything -</p>
                <div id="hero-loverboy" class="loverboy">
                    <textarea class="loverboy-output" readonly="" placeholder="Use the input box below to ask questions."></textarea>
                    <input id="loverboy-input" type="text" placeholder="Who is XMEG">
                </div>
                <a id="loverboy-transmit"><button class="nk-btn nk-btn-blue nk-btn-lg">Send Message</button></a>
            </div>
            <div id="avatar-profile-container" class="col-md-5 image-margin-top">
                <!-- ipad + desktop style = object-fit: contain; border-radius: 0; max-height: 600px; -->
                <img draggable="false" ondragstart="return false;" oncontextmenu="return false;" loading="lazy" id="avatar-profile-img" alt="Futurstic space elf girl from the Genesis Augmented Reality Trading Card Game" src="../img/emma.webp">
            </div>
        </div>
    </div>

💠 Radix UI

Radix UI是一款面向 React/Vue 生态的 无样式、无障碍优先的开源UI组件库,核心提供对话框、下拉菜单、滑块等基础交互组件,严格遵循 WAI-ARIA 规范,内置键盘导航、焦点管理与屏幕阅读器适配等无障碍能力。是平衡无障碍合规、交互稳定性与视觉个性化的现代前端解决方案。

地址:www.radix-ui.com/

案例:Grand Theft Auto VI - Rockstar Games

28.gif

案例使用了 Radix UI 的 Dialog 组件来构建多个 可访问、交互稳健的弹窗系统Dialog.Content 会自动通过 aria-labelledbyaria-describedby 属性与 Dialog.TitleDialog.Description 关联,这对于屏幕阅读器用户理解弹窗内容至关重要。支持 TAB 切换图片预览,Enter 打开弹窗 和 ESC 键关闭弹窗,这是用户预期的标准行为。

image 4.png

4.2 字体 & 图标

选用符合游戏风格的字体和图标库,是定义产品调性和确保界面清晰易用的基础。

💠 Google Fonts API

Google Fonts API 是一项免费的 Web 字体服务。该 API 支持指定多种字体、样式、粗细,并提供 font-display 控制字体加载行为、subset 参数下载特定语言子集、text 参数实现按需加载字体子集等优化功能,同时兼容国际字符和 UTF-8 编码,适用于各类网站和应用的字体需求。

地址:developers.google.cn/fonts/docs/…

案例:Seven Knights Idle Adventure - Netmarble

26.gif

案例 多语言动态字体加载 实现,采用 Nuxt.js SSR 架构,结合 Google Fonts API,为不同语言环境提供最优字体方案。Nuxt.js head() + 条件式字体加载,display=auto Google Fonts 自动选择最佳 font-display 策略。日语版本额外加载 RocknRoll One 字体,用于游戏标题、活动文案等视觉重点区域,通过个性化字体强化游戏的日系风格。

r = {
  en: "Palanquin+Dark:wght@700",    // 英文:粗体标题字体
  ja: "Noto+Sans+JP:wght@300;500",  // 日语:常规/中等字重,适配日文排版
  sc: "Noto+Sans+SC:wght@400;700;900", // 简体中文:常规/粗体/黑体,覆盖正文+标题
  tc: "Noto+Sans+TC:wght@400;700;900", // 繁体中文:适配繁体字形
  th: "Prompt:wght@400;700"         // 泰语:适配泰文字形
};

head() {
  return {
    link: [
      // 1. 基础 Noto Sans(全局)
      // 2. 条件:非 KO/SC/TC/JA/TH 时加载英文补充字体
      // 3. 条件:SC/TC/JA/TH 时加载对应语言字体
      // 4. 条件:KO/EN/SC/TC/JA 时加载 Netmarble 品牌字体
      // 5. 条件:JA 时额外加载 RocknRoll One(标题用)
      // 6. SEO: canonical + hreflang 多语言标签
      {
          hid: "google-webfont",
          rel: "stylesheet",
          href: "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=auto"
      }, ["ko", "sc", "tc", "ja", "th"].includes(this.getLang) ? {} : {
          hid: "google-webfont-lang",
          rel: "stylesheet",
          href: "https://fonts.googleapis.com/css2?family=".concat(r.en, "&display=auto")
      }, ["sc", "tc", "ja", "th"].includes(this.getLang) ? {
          hid: "google-webfont-lang",
          rel: "stylesheet",
          href: "https://fonts.googleapis.com/css2?family=".concat(r[this.getLang], "&display=auto")
      } : {}, ["ko", "en", "sc", "tc", "ja"].includes(this.getLang) ? {
          hid: "google-webfont-lang",
          rel: "stylesheet",
          href: "https://sgimage.netmarble.com/font/v2/font.css"
      } : {}, ["ja"].includes(this.getLang) ? {
          hid: "google-webfont-lang",
          rel: "stylesheet",
          href: "https://fonts.googleapis.com/css2?family=RocknRoll+One&display=auto"
      } : {}, {
          hid: "canonical",
          rel: "canonical",
          href: "".concat(this.getDomain).concat(this.$route.fullPath)
      }, {
          hid: "alternate-x",
          rel: "alternate",
          href: "".concat(this.getDomain).concat(e),
          hreflang: "x-default"
      }
    ]
  }
}

💠 Adobe Fonts(Typekit)

Adobe Fonts(前身为 Typekit)是 Adobe Creative Cloud 旗下的专业字体服务,提供超过 20,000 种高质量字体,支持通过简单的 CSS 集成或桌面同步,在网页设计和创意项目中合法、无缝地使用字体,所有字体均已预授权并自动处理 Web 字体托管、优化和跨浏览器兼容性问题。

地址:helpx.adobe.com/cn/fonts/us…

案例:Roberts Space Industries

image 5.png

案例使用了 Adobe Fonts 服务来加载 Univia Pro 字体家族,这是一个完整的字体包,包含多个字重(覆盖100/400/500/600/700)和样式变体(normal/italic)。使用 @import 动态加载字体,每个变体都提供 WOFF2、WOFF 和 OpenType 格式。

@import url("https://p.typekit.net/p.css?s=1&k=dhw3beb&ht=tk&f=28764.28765.28767.28771.28772.28774.28775.28778.28779&a=86281447&app=typekit&e=css"); 
@font-face {
    font-family: "univia-pro";
    src: url("https://use.typekit.net/af/fbc5c1/00000000000000003b9add6d/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i6&v=3") format("woff2"),url("https://use.typekit.net/af/fbc5c1/00000000000000003b9add6d/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i6&v=3") format("woff"),url("https://use.typekit.net/af/fbc5c1/00000000000000003b9add6d/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=i6&v=3") format("opentype");
    font-display: auto;
    font-style: italic;
    font-weight: 600;
    font-stretch: normal;
}
/* 后续多个@font-face声明:覆盖100/400/500/600/700字重 + normal/italic样式 */

💠 Twitter Emoji(Twemoji)

Twitter Emoji(Twemoji)是 Twitter 开源的一套 Emoji 图标库,它通过将 Unicode 标准中的 Emoji 字符转换为统一风格的图片(如 SVG 或 PNG),解决了不同操作系统和设备上 Emoji 显示效果不一致的问题,确保在所有平台上呈现 完全相同的视觉效果

地址:github.com/twitter/twe…

案例:Stardew Valley

image 6.png

案例

💠 Font Awesome

Font Awesome 是一款开源、可免费商用 的图标字体库和 CSS 框架,提供数千个可缩放的矢量图标,通过简单的 CSS 类名即可快速集成到网站中,支持多种风格和格式,并可通过 kits 和 API 实现按需加载、自定义图标集和动态图标,是前端开发中提升 UI 效率和一致性的标准解决方案。

地址:developers.google.cn/fonts/docs/…

案例:Hogwarts Legacy - Principal

27.gif

案例集成了 Font Awesome 图标库,实现了 社交媒体导航栏。使用 Vue 专用组件 <font-awesome-icon>,通过 fab 前缀指定品牌图标库,确保图标渲染高效精准。fixed-width 属性强制所有图标保持相同宽度,保证导航栏视觉对齐和美观度。

<ul>
<li>
<a class="nav-link dc"
           href="https://discord.gg/HogwartsLegacy"
           title="Discord"
           target="_blank"
           rel="noopener"
           aria-label="Visit Discord.com"
           data-toggle="tooltip" data-placement="bottom">
            <font-awesome-icon :icon="['fab', 'discord']" fixed-width></font-awesome-icon>
</a>
</li>
...
</ul>

总结

没有“万能”的工具,只有“最适合”的方案。技术选型,是在 视觉表现用户体验开发效率性能指标 之间的平衡。选择社区活跃、文档完备的开源工具或成熟的商业服务,能为项目的长期维护和团队协作降低大量成本。希望本篇文章能为游戏官网开发者提供灵感和参考。

干掉 Virtual DOM?尤雨溪开始"强推" Vapor Mode?

作者 前端Hardy
2026年3月2日 17:43

前端这两年有一个明显趋势:

用编译优化彻底消灭运行时开销。

从 Rust 重写工具链,到服务端组件,底层正在被全面 "静态化"

而这一次,轮到了 Vue。

一个正在快速演进的技术 ——Vapor Mode,正在尝试用 Vue 3.5 重构整套 Vue 渲染体系。

它不仅仅是"再快一点",而是想把 Vue 的响应式系统、组件渲染、模板编译、更新机制全部重写为编译时优化

Vapor Mode 到底是什么?

Vapor Mode = 用编译时优化重写 Vue 渲染器。

它是一个全新的渲染模式(非默认),覆盖:

  • 无 Virtual DOM 渲染(细粒度响应式绑定)
  • 编译时依赖追踪(自动依赖收集)
  • 零运行时开销(无 diff 算法)
  • 原生 DOM 操作(直接更新,无代理)
  • 完整生态兼容(Vue Router、Pinia 无缝支持)

你没看错——它不是一个渐进升级,而是一整套"Vue 渲染器重构计划"。

它和普通模式是什么关系?

很多人第一反应:

那它是不是要干掉 Virtual DOM?

答案:不是同一个层级

  • 普通 Vue 模式 = Virtual DOM + 响应式运行时
  • Vapor Mode = 细粒度响应式 + 编译时优化

更准确理解:

  • 普通模式:data 变化 → 触发 setter → 通知依赖 → Virtual DOM diff → 更新真实 DOM
  • Vapor Mode:data 变化 → 直接触发关联 DOM 节点更新

这更像是:

给 Vue 换一颗"零开销引擎"。

快速上手体验

传统模式:Virtual DOM(经典但昂贵)

你以前写 Vue 组件,大概是这样的:

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

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

const title = ref('Hello Vue');
const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

运行时发生了什么?

// 简化后的执行流程
1. 组件初始化:创建 Proxy(title, count)
2. 响应式收集:渲染时追踪依赖(title → h1, count → p)
3. 状态更新:count.value++ 触发 setter
4. 依赖通知:通知所有订阅 count 的组件
5. Virtual DOM diff:对比新旧 VNode6. DOM 更新:真实 DOM 仅更新 p 文本

痛点分析:

  • 每次更新都要运行 Virtual DOM diff(即使只改一个数字)
  • Proxy 开销(内存 + CPU)
  • 响应式系统运行时收集依赖
  • 大型应用下 diff 成本显著

Vapor Mode 方式:零运行时开销

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

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

const title = ref('Hello Vue');
const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

唯一的区别:

<script setup> 中添加 vapor 指令

编译后生成什么?

// 简化后的编译输出(伪代码)
export function render(_ctx) {
  // 1. 直接 DOM 引用(无 VNode)
  const h1 = document.querySelector('h1');
  const p = document.querySelector('p');
  const button = document.querySelector('button');

  // 2. 细粒度绑定(无 Proxy)
  _ctx.title = reactiveValue('Hello Vue', (val) => {
    h1.textContent = val; // 直接更新 DOM
  });

  _ctx.count = reactiveValue(0, (val) => {
    p.textContent = val; // 直接更新 DOM
  });

  button.onclick = () => {
    _ctx.count.value++; // 直接触发更新
  };
}

核心差异:

  • 无 Virtual DOM diff
  • 无 Proxy 开销
  • 编译时依赖追踪
  • 直接 DOM 操作
  • 零运行时响应式系统

架构设计:为什么不只是"更快一点"?

传统 Vue 渲染器(3.x)

  Template Compiler
         ↓
  Render Functions
         ↓
  Virtual DOM Tree
         ↓
  Reconciliation (diff)
         ↓
  Real DOM Updates

特点:

  • 运行时依赖收集(Proxy + Effect)
  • Virtual DOM diff 算法(O(n) 复杂度)
  • 组件级更新(粒度较粗)

Vapor Mode 渲染器

Template Compiler (Vapor)
↓
Dependency Analysis
↓
Fine-grained Binding
↓
Direct DOM Updates

特点:

  • 编译时依赖分析
  • 细粒度绑定(表达式级别)
  • 原生 DOM 操作(无 diff)

对比总结

维度 普通 Vue 模式 Vapor Mode
渲染机制 Virtual DOM diff 直接 DOM 操作
依赖追踪 运行时 Proxy 编译时静态分析
更新粒度 组件级 表达式级
运行时开销 高(diff + Proxy) 极低(仅执行更新逻辑)
编译时优化 有限 极致

性能对比:不是优化,是碾压

官方基准测试(10,000 个简单组件):

场景 普通 Vue 3.4 Vapor Mode 提升
初始渲染 125ms 32ms 3.9×
单个属性更新 8ms 0.8ms 10×
10% 组件更新 45ms 3ms 15×
50% 组件更新 220ms 12ms 18.3×
列表重排序 180ms 5ms 36×

为什么这么快?

1. 无 Virtual DOM diff

// 普通 Vue:每次更新都要 diff
function update() {
  const oldVNode = currentVNode;
  const newVNode = render(); // 重新生成 VNode 树
  const patches = diff(oldVNode, newVNode); // O(n) diff
  applyPatches(patches); // 应用补丁
}

// Vapor Mode:直接更新
function update() {
  textContent.value = newValue; // 直接修改 DOM 文本
}

2. 编译时依赖追踪

<template>
  <div>{{ count }}</div>
</template>
// 普通 Vue:运行时收集
const count = ref(0);
effect(() => {
  div.textContent = count.value; // 运行时追踪依赖
});

// Vapor Mode:编译时已知
const count = reactiveValue(0, (val) => {
  div.textContent = val; // 编译时生成更新逻辑
});

3. 细粒度更新

<template>
  <div>
    <p>Name: {{ user.name }}</p>
    <p>Age: {{ user.age }}</p>
    <p>Email: {{ user.email }}</p>
  </div>
</template>
// 普通 Vue:user 变化 → 整个组件重新渲染
watch(() => user.value, () => {
  render(); // 重渲染整个组件
});

// Vapor Mode:user.name 变化 → 只更新第一个 <p>
user.name.onUpdate((val) => {
  p1.textContent = val; // 只更新对应 DOM
});

user.age.onUpdate((val) => {
  p2.textContent = val;
});

user.email.onUpdate((val) => {
  p3.textContent = val;
});

更疯狂的是:完整生态兼容

Vapor Mode 不是重写所有 Vue,而是无缝集成:

1. Vue Router 兼容

<!-- app.vue -->
<script setup vapor>
import { RouterView } from 'vue-router';
</script>

<template>
  <RouterView /> <!-- Vapor 组件可以渲染普通组件 -->
</template>

2. Pinia 兼容

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  }
});
<!-- components/Counter.vue -->
<script setup vapor>
import { useCounterStore } from '@/stores/counter';

const store = useCounterStore();
</script>

<template>
  <p>{{ store.count }}</p>
  <button @click="store.increment">+1</button>
</template>

3. 渐进式采用

<!-- 混合使用:Vapor 组件 + 普通组件 -->
<script setup vapor>
import OrdinaryComponent from './OrdinaryComponent.vue';
</script>

<template>
  <OrdinaryComponent /> <!-- 普通组件在 Vapor 组件中正常工作 -->
</template>

现在能生产使用吗?

部分可用。

Vapor Mode 当前状态:

  • 核心功能稳定(Vue 3.5+)
  • 完整生态兼容
  • TypeScript 支持
  • 部分指令仍在完善(v-for、v-if 复杂场景)
  • 调试工具仍在改进

建议采用场景:

  • 性能敏感型应用(高频更新列表)
  • 移动端应用(低性能设备)
  • 数据可视化(实时图表)
  • 简单 CRUD 应用(收益不明显)

总结一句话

如果说:

  • Vue 2 用 Virtual DOM 解决了 "跨浏览器兼容性"
  • Vue 3 用 Composition API 解决了 "代码复用性"

那 Vapor Mode 正在解决 "极致性能"

它可能不会明天取代所有 Vue 模式(渐进升级策略),

但它已经说明了一件事:

Vue 的未来,不止是框架升级,而是渲染器升级。

如果你是:

  • Vue 深度使用者
  • 性能优化爱好者
  • 编译原理探索者
  • 或对前端性能有极致追求

这个技术值得关注。

官方资源:

扩展阅读:

  • SolidJS 的细粒度响应式系统
  • Svelte 的编译时优化
  • Qwik 的 Resumability 架构

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

04 | 别再写几十个参数的构造函数了——建造者模式

2026年3月2日 17:14

不知道你有没有接手过那种“祖传代码”,里面有一个极其庞大的类,初始化的时候需要传十几个参数。每次调用它,你都得小心翼翼地数逗号:new User('张三', null, true, 18, null, 'admin', ...)

一旦中间少传了一个 null,或者把第 5 个参数和第 6 个参数搞反了,整个程序直接报错,查都查不出来。 我以前写这种代码的时候,自己都觉得心虚,生怕哪天把自己给坑了。

今天咱们聊的这个建造者模式(Builder Pattern),就是专门为了解决这种“参数地狱”而生的。

为什么我们总是被“参数列表”搞得晕头转向?

说白了,这种长参数列表的问题,在于我们试图一口吃成个胖子。 我们想在实例化的一瞬间,把所有属性都塞进去。

但这违背了人类的认知习惯。 想象一下你去赛百味(Subway)买三明治。 你不会一进门就冲着店员喊一串代码:“我要全麦面包加火腿加生菜去洋葱加蛋黄酱烤热带走!” 店员肯定懵圈。

正确的流程是分步骤: 先选面包,再选肉,然后选配菜,最后选酱料。 每一步都是独立的,你可以选,也可以不选。

建造者模式的底层逻辑就是:把一个复杂对象的“构建过程”和它的“部件”分离。 不再是一次性 new 出来,而是通过一个专门的“建造者”,一步一步地把对象组装起来。

怎么把代码写得像“点菜”一样优雅?

在 JavaScript 里,我们可以利用链式调用(Chaining),把这个模式实现得非常漂亮。

假设我们要创建一个复杂的 Request 对象,用来发网络请求。

如果不适用模式,代码是这样的:

// 参数太多,根本记不住哪个位置是干啥的
// 第三个参数是 timeout 还是 headers?完全靠猜
const req = new HttpRequest('https://api.com', 'POST', null, 5000, { 'Content-Type': 'json' });

现在,我们用建造者模式改造一下:

class RequestBuilder {
  constructor(url) {
    this.url = url;
    this.method = 'GET'; // 默认值
    this.headers = {};
    this.body = null;
  }

  setMethod(method) {
    this.method = method;
    return this; // 关键:返回 this,实现链式调用
  }

  setHeader(key, value) {
    this.headers[key] = value;
    return this;
  }

  setBody(data) {
    this.body = JSON.stringify(data);
    return this;
  }

  // 最后一步:产出真正的对象
  build() {
    // 这里还可以加校验逻辑,比如:如果是 POST,必须有 body
    if (this.method === 'POST' && !this.body) {
      throw new Error('POST 请求必须有 Body');
    }
    return {
      url: this.url,
      method: this.method,
      headers: this.headers,
      body: this.body
    };
  }
}

// 使用起来就像写文章一样流畅
const request = new RequestBuilder('https://api.com')
  .setMethod('POST')
  .setHeader('Authorization', 'Bearer xxx')
  .setBody({ name: '小美' })
  .build();

两种写法的直观对比

容易出问题的写法: new Class(a, b, c, d, e...) 后果:代码可读性极差,维护者必须对着文档数参数位置。如果中间要插入一个新参数,所有调用方都得改。

更稳健的建造者写法: .setA().setB().build() 后果:代码本身就是文档,读起来像英语句子。参数顺序无所谓,不想传的参数直接跳过,用默认值即可。

给你的 3 条行动建议

  1. 参数超过 4 个就该警惕了:如果你的构造函数参数超过 4 个,或者有好几个参数是可选的(经常传 null),别犹豫,马上换成建造者模式,或者至少用“配置对象”传参。

  2. 把校验逻辑放在 build 里:这是建造者模式最大的隐藏红利。你可以在 build() 方法里统一检查“A 属性存在时 B 属性是否也存在”,保证产出的对象永远是合法的。

  3. JS 的“配置对象”其实是简化版:在 JS 里,我们经常直接传一个对象 { url: '...', method: '...' }。这其实是建造者模式的一种“变体”。但如果你需要复杂的构建逻辑(比如根据 A 参数自动计算 B 参数),标准的 Builder 类还是更清晰。

我以前总觉得多写一个 Builder 类是增加代码量。 后来在一次重构中,我把一个 12 个参数的初始化函数改成了 Builder,那天下午我看着那段清晰的代码,心里那个舒坦。

代码是写给人看的,顺便给机器运行。 让调用者用得舒服,是你作为 API 设计者的温柔。

VersionCheck.js - 让前端版本更新变得简单优雅

2026年3月2日 17:12

VersionCheck.js - 让前端版本更新变得简单优雅

在现代Web应用开发中,如何优雅地处理前端版本更新一直是一个重要但容易被忽视的问题。今天我要向大家推荐一款极简但功能强大的前端版本检测工具 —— VersionCheck.js

🎯 解决什么问题?

相信很多开发者都遇到过这样的困扰:

  • 用户访问网站时加载的是旧版本缓存
  • 新功能上线后用户看不到更新内容
  • 手动刷新页面影响用户体验
  • 缺乏有效的版本检测机制

VersionCheck.js 正是为解决这些问题而生!

✨ 核心特性一览

🔄 智能双模式检测

  • ETag模式(默认):通过HTTP响应头自动检测
  • 版本文件模式:通过JSON文件版本字段精确控制

⚡ 自动化轮询

  • 默认每10分钟自动检测一次
  • 页面隐藏时自动暂停,节省资源
  • 支持自定义检测频率

🎨 灵活的交互方式

  • 内置原生confirm弹窗
  • 支持自定义提示文案
  • 可配置更新回调函数

🛡️ 健壮的容错机制

  • localStorage自动降级到内存存储
  • 完善的错误处理和日志记录
  • 多环境兼容(UMD模块规范)

📊 为什么选择VersionCheck.js?

特性 VersionCheck.js 其他方案
配置复杂度 极简配置 需要复杂配置
检测准确性 双模式保障 单一模式
用户体验 无感知检测 影响用户体验
兼容性 多环境支持 环境限制多
维护成本 零依赖 需要持续维护

🌟 项目亮点

  • 零学习成本:API设计简洁直观
  • 高性能:智能暂停机制节省资源
  • 高可靠性:完善的错误处理机制
  • 高扩展性:丰富的配置选项
  • 开源免费:MIT许可证,可商用

📦 安装方式

1. 通过 <script> 标签引入(浏览器环境)

<!-- 生产环境 -->
<script src="dist/index.js"></script>

<!-- 或使用 CDN -->
<script src="https://cdn.jsdelivr.net/npm/version-check-js@latest/dist/index.js"></script>

2. 通过 NPM 安装(Node.js 环境)

# 安装最新版本
npm install version-check-js

# 或使用 yarn
yarn add version-check-js

然后在代码中导入:

// CommonJS 方式
const VersionCheck = require('version-check-js');

// ES6 模块方式
import VersionCheck from 'version-check-js';

3. 通过 unpkg CDN 引入

<script src="https://unpkg.com/version-check-js@latest/dist/index.js"></script>

🚀 快速开始

基础用法

// 实例化 VersionCheck
const versionCheck = new VersionCheck({
  url: '/version.json', // 指定版本文件路径(或默认使用 '/' 进入 ETag 模式)
  interval: 60 * 1000, // 设置检测间隔为 1 分钟
  message: '发现新版本,是否立即刷新?', // 自定义提示文案
});

// 启动自动检测
versionCheck.start();

// 停止自动检测
// versionCheck.stop();

// 销毁实例
// versionCheck.destroy();

// 手动触发一次检测
versionCheck.check().then(hasUpdate => {
  console.log('是否有更新:', hasUpdate);
});

⚙️ 配置项详解

参数名 类型 默认值 描述
url string '/' 检测地址:
- 默认 '/':启用 ETag 模式
- 文件路径(如 /version.json):启用版本文件模式
interval number 10 * 60 * 1000(10 分钟) 轮询检测间隔时间(毫秒),建议不小于 30 秒
message string '检测到新版本,是否立即刷新?' 更新提示文案,仅在未设置 onUpdate 时生效
onUpdate Function null 自定义更新回调函数(优先级高于默认 confirm 弹窗)
onError Function (err) => console.error('版本检测失败:', err) 错误回调函数,接收错误对象作为参数
onLog Function null 操作日志回调函数,用于记录正常操作信息
storage Object null 自定义存储配置(需提供 getsetremove 方法),默认使用 localStorage

配置项最佳实践

const versionCheck = new VersionCheck({
  // 基础配置
  url: '/api/version', // 推荐使用具体的 API 接口
  interval: 5 * 60 * 1000, // 5分钟检测一次(生产环境推荐)

  // 用户体验优化
  message: '发现新版本可用,是否立即更新?',

  // 自定义回调
  onUpdate: () => {
    // 自定义更新逻辑
    console.log('执行更新...');
    // 可以在这里添加动画、提示等
    window.location.reload();
  },

  // 错误处理
  onError: error => {
    // 生产环境可以发送错误日志到监控系统
    console.error('版本检测异常:', error);
  },

  // 操作日志
  onLog: message => {
    // 记录操作日志,便于调试
    console.log('VersionCheck:', message);
  },
});

🔧 完整 API 文档

实例方法

start()

启动自动轮询检测。

versionCheck.start(); // 返回 undefined
stop([isInternal])

停止自动轮询检测。

versionCheck.stop(); // 外部调用,会触发 onLog
versionCheck.stop(true); // 内部调用,不会触发 onLog
check()

手动触发一次检测,返回 Promise。

try {
  const hasUpdate = await versionCheck.check();
  if (hasUpdate) {
    console.log('检测到新版本!');
  }
} catch (error) {
  console.error('检测失败:', error);
}
reload()

强制刷新页面,自动处理 URL 参数去重。

versionCheck.reload(); // 会添加时间戳参数避免缓存
destroy()

销毁实例,清理所有资源(定时器、事件监听器、存储引用等)。

versionCheck.destroy(); // 实例销毁后不可再次使用

静态属性

checkMode

获取当前检测模式。

console.log(versionCheck.checkMode); // 'etag' 或 'file'
isRunning

获取当前检测状态。

console.log(versionCheck.isRunning); // boolean

📝 使用场景和示例

配合 Axios 拦截器使用

// axios 配置
import axios from 'axios';
import VersionCheck from 'version-check-js';

const versionCheck = new VersionCheck({
  url: '/api/version',
  interval: 300000,
});

// 请求拦截器中进行版本检测
axios.interceptors.request.use(
  async config => {
    if (process.env.NODE_ENV === 'production') {
      try {
        await versionCheck.check().then(flag => {
          console.log(flag);
        });
      } catch (error) {
        console.warn('版本检测失败:', error);
      }
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  },
);

versionCheck.start();

🛠️ 高级配置和最佳实践

存储策略配置

// 自定义存储适配器
const customStorage = {
  get: function (key) {
    try {
      return localStorage.getItem(key);
    } catch (e) {
      // 降级到 cookie
      return this._getFromCookie(key);
    }
  },

  set: function (key, value) {
    try {
      localStorage.setItem(key, value);
      return true;
    } catch (e) {
      // 降级到 cookie
      return this._setToCookie(key, value);
    }
  },
  // Cookie 操作方法
  _getFromCookie: function (key) {
    /* ... */
  },
  _setToCookie: function (key, value) {
    /* ... */
  },
};

const versionCheck = new VersionCheck({
  storage: customStorage,
});

错误监控集成

const versionCheck = new VersionCheck({
  onError: error => {
    // 发送到自定义监控接口
    fetch('/api/error-report', {
      method: 'POST',
      body: JSON.stringify({
        error: error.message,
        stack: error.stack,
        timestamp: Date.now(),
      }),
    });
  },

  onLog: message => {
    // 记录操作日志
  },
});

性能优化建议

// 生产环境配置
const prodConfig = {
  url: '/api/version',
  interval: 5 * 60 * 1000, // 5分钟(避免过于频繁)
  onError: error => {
    // 生产环境静默处理,避免影响用户体验
    console.debug('Version check error:', error.message);
  },
};

// 开发环境配置
const devConfig = {
  url: '/api/version',
  interval: 30 * 1000, // 30秒(便于调试)
  onLog: message => {
    // 开发环境详细日志
    console.log('🔧 VersionCheck:', message);
  },
};

const versionCheck = new VersionCheck(process.env.NODE_ENV === 'production' ? prodConfig : devConfig);

🔗 相关链接

📄 许可证

MIT License

💬 结语

VersionCheck.js以其极简的配置强大的功能优雅的设计,成为了前端版本检测领域的优秀解决方案。无论你是个人开发者还是团队项目,都能从中受益。

立即试试VersionCheck.js,让你的应用版本更新变得更加智能和优雅!


如果你觉得这个工具不错,欢迎Star我们的GitHub项目,让更多开发者受益!

How to Install TeamViewer on Ubuntu 24.04

TeamViewer is a cross-platform remote access and support solution that allows you to connect to and control remote computers over the internet. It supports remote control, file transfer, desktop sharing, and online meetings across Windows, macOS, and Linux.

This guide explains how to install TeamViewer on Ubuntu 24.04.

Quick Reference

Task Command
Download package wget https://download.teamviewer.com/download/linux/teamviewer_amd64.deb
Install sudo apt install ./teamviewer_amd64.deb
Launch teamviewer
Update sudo apt update && sudo apt upgrade teamviewer
Remove sudo apt remove teamviewer

Prerequisites

You need to be logged in as root or as a user with sudo privileges .

Installing TeamViewer on Ubuntu 24.04

TeamViewer is proprietary software and is not available in the standard Ubuntu repositories. You will download and install the official .deb package directly from TeamViewer.

Open your terminal and download the latest TeamViewer .deb package using wget :

Terminal
wget https://download.teamviewer.com/download/linux/teamviewer_amd64.deb

Once the download is complete, update the package index and install the package:

Terminal
sudo apt update
sudo apt install ./teamviewer_amd64.deb

When prompted Do you want to continue? [Y/n], type Y to confirm.

Info
TeamViewer depends on Qt libraries. The installation pulls in several Qt packages automatically. During the process, the official TeamViewer APT repository is also added to your system, which keeps TeamViewer up to date through the standard package manager.

TeamViewer is now installed on your Ubuntu 24.04 system.

Starting TeamViewer

You can launch TeamViewer from the command line:

Terminal
teamviewer

Alternatively, open the Activities menu, search for “TeamViewer”, and click its icon.

When TeamViewer starts for the first time, it will prompt you to accept the license agreement. Click “Accept License Agreement” to continue.

After accepting, the main TeamViewer window opens and displays your ID and Password. Share these with anyone who needs to connect to your machine, or enter a remote computer’s ID to initiate a connection yourself.

TeamViewer main window showing ID and password on Ubuntu 24.04

Updating TeamViewer

During installation, the official TeamViewer repository is added to your system. You can verify it with the cat command :

Terminal
cat /etc/apt/sources.list.d/teamviewer.list
output
deb https://linux.teamviewer.com/deb stable main

Because the repository is configured, you can update TeamViewer the same way you update any other package:

Terminal
sudo apt update
sudo apt upgrade teamviewer

You can also update through the Software Updater application in the GNOME desktop.

Uninstalling TeamViewer

To remove TeamViewer while keeping its configuration files:

Terminal
sudo apt remove teamviewer

To remove TeamViewer along with all its configuration files:

Terminal
sudo apt purge teamviewer

To also remove the TeamViewer APT repository so it no longer appears in your package sources:

Terminal
sudo rm /etc/apt/sources.list.d/teamviewer.list
sudo apt update

Troubleshooting

teamviewer: command not found after installation Close and reopen your terminal, or log out and back in so the new binary is picked up by your shell’s PATH. If the issue persists, verify the installation with which teamviewer or dpkg -l teamviewer.

Qt library errors during installation Run sudo apt install -f to resolve broken or missing dependencies, then retry the installation.

dpkg error when installing the .deb file Run sudo dpkg --configure -a to fix any interrupted package operations, then run sudo apt install ./teamviewer_amd64.deb again.

TeamViewer shows “Not ready. Please check your connection.” This usually means the TeamViewer daemon is not running or cannot reach the internet. Try restarting the service:

Terminal
sudo systemctl restart teamviewerd

Verify the service is active:

Terminal
sudo systemctl status teamviewerd

Wrong architecture — running an ARM system The teamviewer_amd64.deb package is for 64-bit x86 systems. For ARM-based systems such as a Raspberry Pi running Ubuntu, download the ARM package from the TeamViewer Linux downloads page .

FAQ

Is TeamViewer free on Linux? TeamViewer is free for personal and non-commercial use. Commercial use requires a paid license. See the TeamViewer pricing page for details.

Does the same installation method work on Ubuntu 22.04 and Debian? Yes. The same .deb package and apt install steps work on Ubuntu 22.04 and other Debian-based distributions. For Ubuntu 22.04 specific instructions, see How to Install TeamViewer on Ubuntu 22.04 .

How do I connect to a remote computer? Open TeamViewer, enter the remote computer’s ID in the “Partner ID” field, and click “Connect”. Enter the password when prompted. The remote session starts immediately if the correct credentials are provided.

How do I allow unattended access? In the TeamViewer main window, go to Extras → Options → Security and set a personal password. This allows connections to your machine without requiring someone to be present to share the one-time password.

Conclusion

TeamViewer can be installed on Ubuntu 24.04 in two commands — download the .deb package and install it with apt. The TeamViewer repository added during installation keeps the application up to date through the standard Ubuntu update process.

If you have any questions, feel free to leave a comment below.

Flutter 实现 AI 聊天页面 —— 记一次 Markdown 数学公式显示的踩坑之旅

作者 明君87997
2026年3月2日 16:58

Flutter 实现 AI 聊天页面 —— 记一次 Markdown 数学公式显示的踩坑之旅

最近在做一个 Flutter 智能体聊天组件库,AI 返回的内容里如果经常夹着数学公式,结果页面上全是 $E=mc^2$ 这种原始字符串,如果是比较复杂的数学公式完全看不懂写的是啥, 完全渲染不出来正确的公式格式。折腾了一天,终于找到了一套比较完整的解决方案,记录一下。


先说说背景

项目是一个 Flutter 插件库,对外暴露两个组件:

  • AiChatPage:独立聊天页面,直接 push 进去就能用
  • AiChatWidget:可嵌入任意页面的聊天 Widget

AI 的回复走流式输出(SSE),内容是 Markdown 格式。用 flutter_markdown 渲染普通内容没问题,但一碰到公式就原样显示了,因为标准 Markdown 压根不认识 $...$ 这个语法。


问题在哪

flutter_markdown 底层依赖 markdown 包做解析。整个渲染流程分两步:

  1. 解析:把 Markdown 文本解析成 AST 节点树
  2. 渲染:遍历 AST,把每个节点转成 Flutter Widget

公式渲染挂的点在第一步——markdown 包不认识 $...$,直接把它当普通文本处理了,后面的渲染器根本没有机会介入。

所以光加一个渲染器是不够的,解析层和渲染层都要动


解法:在解析层注入自定义语法

markdown 包提供了 InlineSyntax 接口,可以用正则表达式匹配任意行内语法,命中后生成自定义 AST 节点。

我写了两个解析器:

/// 行内公式:$...$
class InlineMathSyntax extends md.InlineSyntax {
  InlineMathSyntax() : super(r'\$([^\$]+)\$');

  @override
  bool onMatch(md.InlineParser parser, Match match) {
    final element = md.Element.text('math', match[1]!);
    element.attributes['inline'] = 'true';
    parser.addNode(element);
    return true;
  }
}

/// 块级公式:$$...$$
class BlockMathSyntax extends md.InlineSyntax {
  BlockMathSyntax() : super(r'\$\$([^\$]+)\$\$');

  @override
  bool onMatch(md.InlineParser parser, Match match) {
    final element = md.Element.text('math', match[1]!);
    element.attributes['inline'] = 'false';
    parser.addNode(element);
    return true;
  }
}

这里有个小细节:用 element.attributes['inline'] 把"行内/块级"信息带到后续渲染阶段,不然渲染器不知道该怎么处理。

然后把这两个解析器注册进去:

extensionSet: md.ExtensionSet(
  md.ExtensionSet.gitHubFlavored.blockSyntaxes,
  <md.InlineSyntax>[
    ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes,
    InlineMathSyntax(),
    BlockMathSyntax(),
  ],
),

解法:在渲染层用 flutter_math_fork 渲染

解析层把公式内容提取成 math 标签节点了,接下来写一个 MarkdownElementBuilder 来消费它:

class MathElementBuilder extends MarkdownElementBuilder {
  final ChatTheme theme;

  @override
  Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
    final mathContent = element.textContent;
    final isInline = element.attributes['inline'] == 'true';
    return _buildMathWidget(mathContent, isInline);
  }

  Widget _buildMathWidget(String mathContent, bool isInline) {
    try {
      if (isInline) {
        return Math.tex(
          mathContent,
          mathStyle: MathStyle.text,
          options: MathOptions(fontSize: theme.fontSize, color: theme.aiTextColor),
        );
      } else {
        // 块级公式加水平滚动,防止长公式撑出屏幕
        return SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Math.tex(
            mathContent,
            mathStyle: MathStyle.display,
            options: MathOptions(fontSize: theme.fontSize + 2, color: theme.aiTextColor),
          ),
        );
      }
    } catch (e) {
      // 渲染失败就降级,把原始文本显示出来,总比白屏强
      return Text(
        isInline ? '\$$mathContent\$' : '\$\$$mathContent\$\$',
        style: TextStyle(color: Colors.red, fontFamily: 'monospace'),
      );
    }
  }
}

最后把渲染器挂上:

builders: {
  'code': OptimizedCodeElementBuilder(theme: theme),
  'math': MathElementBuilder(theme: theme),
},

又踩了一个坑:表格里的行内公式会溢出

以为公式能显示就搞定了,结果发现公式出现在表格单元格里时,因为父容器宽度受限,长公式直接溢出报 overflow 错误。

最开始想直接给所有行内公式套一个 SingleChildScrollView,但这样又会影响在普通段落里的布局。

后来用 LayoutBuilder 判断了一下:

class _InlineMathWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final mathWidget = Math.tex(mathContent, mathStyle: MathStyle.text, ...);

    return LayoutBuilder(
      builder: (context, constraints) {
        // 父容器宽度有限(比如表格单元格)才加滚动
        if (constraints.maxWidth.isFinite && constraints.maxWidth < double.infinity) {
          return SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            physics: const ClampingScrollPhysics(),
            child: mathWidget,
          );
        }
        // 普通段落里直接渲染就好
        return mathWidget;
      },
    );
  }
}

这样既解决了溢出,又不影响正常场景的性能。


流式输出导致的性能问题

AI 是流式回复的,每来一个 chunk 就更新一次消息内容,MarkdownRenderer 会跟着重建。如果每次都重新渲染一遍所有公式,当内容稍微长一点,肉眼就能看到卡顿。

我加了一个基于 LinkedHashMap 实现的 LRU 缓存,把渲染好的公式 Widget 缓存起来:

class LRUCache<K, V> {
  final int capacity;
  final LinkedHashMap<K, V> _cache = LinkedHashMap();

  V? operator [](K key) {
    if (!_cache.containsKey(key)) return null;
    final value = _cache.remove(key)!;
    _cache[key] = value; // 移到末尾(最近使用)
    return value;
  }

  void operator []=(K key, V value) {
    if (_cache.containsKey(key)) _cache.remove(key);
    else if (_cache.length >= capacity) _cache.remove(_cache.keys.first);
    _cache[key] = value;
  }
}

缓存 key 用 公式内容 hashCode + 是否行内 + 主题 hashCode 组合,数学公式缓存上限设 100 个,代码块 50 个,基本够用了。

另外每个公式都包了一层 RepaintBoundary,流式更新时只重绘变化部分,不会连带整个消息列表重绘。


最终用法

依赖这几个包:

dependencies:
  flutter_markdown: ^0.7.x
  markdown: ^7.x
  flutter_math_fork: ^0.7.x
  flutter_highlight: ^0.7.x

使用的时候配置好 ChatConfig 和主题,传入控制器就行:

final controller = ChatStreamController(
  config: ChatConfig(
    apiProviders: {'default': myApiService},
    enableMarkdown: true,
  ),
);

// 嵌入页面
AiChatWidget(
  controller: controller,
  theme: ChatTheme.light(),
)

// 或者独立页面
Navigator.push(context, MaterialPageRoute(
  builder: (_) => AiChatPage(
    controller: controller,
    theme: ChatTheme.light(),
    title: 'AI 助手',
  ),
));

总结

整个问题其实不复杂,捋清楚之后就是两步:

  1. 解析层:继承 InlineSyntax,用正则把 $...$$$...$$ 识别出来,转成自定义 AST 节点
  2. 渲染层:继承 MarkdownElementBuilder,用 flutter_math_fork 把节点渲染成 Widget

额外需要注意的是:

  • 块级公式要加横向滚动,防止溢出
  • 行内公式在有宽度限制的容器里也要加滚动(用 LayoutBuilder 判断)
  • 流式场景下务必加缓存,不然会卡
  • 公式解析失败要有降级处理,不能白屏

flutter_markdown 的扩展机制其实相当灵活,这套解析器 + 渲染器的模式可以推广到任何自定义 Markdown 元素,不只是数学公式。

整理「祖传」代码,就是在开发脚手架?

作者 codingWhat
2026年3月2日 16:26

前言

  • 每次起新项目,都要复制一整套「祖传」配置、改包名、删示例,稍不留神就漏掉埋点或兼容文件?
  • 团队里 Vue 和 React 混用,项目结构五花八门,新人上手全靠口口相传?
  • 你和我说这就是在开发脚手架?No,No,No,你这是在扒拉项目结构。

脚手架不是框架,而是用命令行把「创建项目、统一规范」自动化的工具。我们天天在用的 npmvue createcreate-react-app,背后都是同一套思路:用 Node.js 写一个 CLI,把最佳实践固化成一条命令。


一、脚手架是什么?一条命令里藏着四样东西

回想一下你敲的每一条脚手架命令,无非四部分:

vue create vue-test-app --force -r https://registry.npmmirror.com
部分 示例 说明
主命令 vue 对应一个可执行文件
子命令 create 具体做什么事
参数 vue-test-app 子命令的输入
选项 --force-r <url> 开关或带值的配置

所以脚手架就是一个「主命令 + 子命令 + 参数 + 选项」的命令行客户端

执行时发生了什么? 终端先根据主命令在 PATH 里找到可执行文件(例如全局的 vue.js),再用 Node 执行它(因为文件头有 #!/usr/bin/env node),脚本里解析子命令和选项后执行对应的逻辑,结束退出。
一句话:脚手架本质是操作系统的客户端,只不过这个「客户端」是一段用 Node 跑的 JS,通过命令行和你交互罢了。


二、为什么值得花费成本自己做一套?

vue-cli、create-react-app 解决的是「从零搭一个标准项目」。但日常团队中会沉淀出一堆自家的东西,比如H5 兼容、接口封装、埋点、公共组件、登录/权限等,甚至整块业务都会被复用。每次起项目都从零复制,既费时又容易出错。

依我看,自己做项目创建脚手架,最起码能带来三件收益:模板沉淀(把「我们团队该怎么起项目」固化成可选模板)、标准化(类型、名称、框架通过交互选择,减少人为差异)、可复用(新人一条命令就和团队站在同一起跑线)。


三、原理:三个问题搞懂脚手架执行的过程

回答三个问题,原理就通了:

  1. 为什么装的是 @vue/cli,敲的却是 vue
    package.json 里有个 bin 字段,例如 "bin": { "vue": "bin/vue.js" }。全局安装时,npm 会在可执行路径下创建一个叫 vue软链接,指向这个 js 文件,所以命令名可以和包名不一样。

  2. 全局安装时到底干了啥?
    把包下到全局 node_modules,再按 bin 配置在系统 PATH 能搜到的地方建好软链接,这样你在任意目录敲 vue 都能找到对应脚本。

  3. 为什么一个 .js 文件能直接当命令执行?
    因为第一行写了 shebang#!/usr/bin/env node。系统看到 #! 就知道要用后面的解释器来跑这个文件,于是用当前环境的 node 去执行。用 env node 而不是写死 /usr/bin/node,换机器、换环境也能用。


四、给我一首歌的时间,从 0 跑通一个最小 的CLI

四步:建项目、写入口、配 bin、本地 link。跑通后你就有了一条「真」命令,再往上加 init、install 只是扩展。

1. 初始化项目

mkdir my-cli && cd my-cli
npm init -y

2. 写入口并加上 shebang

创建 bin/cli.js。第一行 #!/usr/bin/env nodeshebang(希棒):以 #! 开头,告诉系统「用谁」来执行这个文件。这里用当前环境的 node,所以终端里直接敲 my-cli 就会用 Node 跑这段脚本,不用再写 node bin/cli.js

#!/usr/bin/env node

import { program } from 'commander';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(
  readFileSync(join(__dirname, '../package.json'), 'utf-8')
);

program
  .name('my-cli')
  .description('最小 CLI 示例')
  .version(pkg.version);

program
  .command('hello [name]')
  .description('打个招呼')
  .action((name) => {
    console.log('Hello,', name || 'World');
  });

program.parse();

3. 配置 package.json

补上 bintype: "module"(推荐用 ESM,大势所趋):

{
  "name": "my-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-cli": "bin/cli.js"
  },
  "dependencies": {
    "commander": "^11.0.0"
  }
}

4. 本地调试

npm install
npm link

在任意目录执行 my-cli --versionmy-cli hello 张三,能输出版本、能打招呼,就说明最小 CLI 已经跑通。后面要支持 init、install 等多条命令,无非是把脚手架拆成多包、抽象出命令基类,在入口里按「一条命令一个子类」挂上去,是不是很简单?


五、来看一个真实项目

拿我们组脚手架为例,采用 commander框架,实现了组内自定义Vue/React模板框架的生成功能。

目录

your-cli/
├── package.json
├── packages/
│   ├── cli/              # 入口、createCLI、注册命令
│   ├── command/          # 命令基类
│   ├── utils/            # 日志、inquirer、npm、Git
│   ├── init/             # 命令 init:模板 → 下载 → 安装
│   └── install/          # 命令 install:搜索 → 选 tag → clone → 装依赖 → 运行

Command 基类说明:项目中,子命令不直接调 Commander,而是继承基类,从而实现 commanddescriptionoptionsaction,基类在构造函数里统一完成了「注册命令 + 绑定 action」。这样新增命令 = 新子类 + 入口挂一行。核心逻辑如下:

// packages/command/lib/index.js(思路示例,已脱敏)
class Command {
  constructor(program) {
    if (!program) throw new Error('command instance must not be null!');
    this.program = program;
    const cmd = this.program.command(this.command);
    cmd.description(this.description);
    if (this.options?.length > 0) {
      this.options.forEach(opt => cmd.option(...opt));
    }
    cmd.action((...params) => this.action(...params));
  }

  get command() {
    throw new Error('command must be implemented');
  }
  get description() {
    throw new Error('description must be implemented');
  }
  get options() {
    return [];
  }
  async action() {
    throw new Error('action must be implemented');
  }
}
export default Command;

入口里只需:通过createCLI() 得到 program
createInitCommand(program)createInstallCommand(program)
最后 program.parse(process.argv)
通常createCLI() 里需要包含 name、version、--debug、Node 版本检查、未知命令提示等功能。


六、来看看init 命令干了啥

init 只做一件事:从模板创建项目。在项目里,InitCommand 的 action 拆成三步,对应三个文件:

步骤 做的事 对应模块
1 选择模板,生成安装信息 createTemplate.js
2 下载模板到缓存目录 downloadTemplate.js
3 拷贝到项目目录并渲染 installTemplate.js

第一步:createTemplate
入参是项目名 name 和命令行 opts--type--template--force)。
没传 type/template 就通过交互收集。
选定后用 getLatestVersion(template.npmName) 从 npm 拉最新版本,并返回 { type, name, template, targetPath },其中targetPath 即缓存目录(如 ~/.your-cli/addTemplate)。

第二步:downloadTemplate
在缓存目录下执行 npm install ${npmName}@${version},把模板包装进 node_modules。示例:

// 思路示例,已脱敏
import { execa } from 'execa';
import ora from 'ora';

async function downloadAddTemplate(targetPath, template) {
  const { npmName, version } = template;
  await execa('npm', ['install', `${npmName}@${version}`], { cwd: targetPath });
}

export default async function downloadTemplate(selectedTemplate) {
  const { targetPath, template } = selectedTemplate;
  ensureDirSync(targetPath);
  const spinner = ora('正在下载模板...').start();
  try {
    await downloadAddTemplate(targetPath, template);
    spinner.stop();
    log.success('下载模板成功');
  } catch (e) {
    spinner.stop();
    printErrorLog(e);
  }
}

第三步:installTemplate
目标目录是当前目录下的 name 文件夹;
从缓存的 node_modules/<npmName>/template 拷贝到目标目录,之后用 ejs 注入 name 后写回。
代码示例如下:

// 思路示例,已脱敏
import fse from 'fs-extra';
import { pathExistsSync } from 'path-exists';
import ejs from 'ejs';
import glob from 'glob';

export default async function installTemplate(selectedTemplate, opts) {
  const { force = false } = opts;
  const { targetPath, name, template } = selectedTemplate;
  const rootDir = process.cwd();
  const installDir = path.resolve(rootDir, name);

  if (pathExistsSync(installDir)) {
    if (!force) {
      log.error(`当前目录下已存在 ${installDir}`);
      return;
    }
    fse.removeSync(installDir);
  }
  fse.ensureDirSync(installDir);

  const originFile = path.resolve(targetPath, 'node_modules', template.npmName, 'template');
  const fileList = fse.readdirSync(originFile);
  fileList.forEach((file) => {
    fse.copySync(path.join(originFile, file), path.join(installDir, file));
  });

  const ejsData = { name, ...customData };
  glob('**', { cwd: installDir, nodir: true, ignore: template.ignore }, (err, files) => {
    files.forEach((file) => {
      const filePath = path.join(installDir, file);
      ejs.renderFile(filePath, ejsData, (err, result) => {
        if (!err) fse.writeFileSync(filePath, result);
      });
    });
  });
}

最终,InitCommand 的 action 就是三步串联:

// 思路示例,已脱敏
async action([name, opts]) {
  const selectedTemplate = await createTemplate(name, opts);
  await downloadTemplate(selectedTemplate);
  await installTemplate(selectedTemplate, opts);
}

ps:为何用 npm 管理模板? 不占服务器、自带版本、用 registry API 查 dist-tags.latest 即可拿到最新版的包。我们内部使用了自己部署的Verdaccio,也推荐给大家!
模板包约定:模板统一放在 template/下,支持多框架(React/Vue)。


七、React/Vue 模板来源有哪些?

「从模板创建项目」时,React/Vue 通常有两种来源,可以同时提供:

来源 命令 场景
npm 模板 init 团队标准化:把 React/Vue 模板打成 npm 包,init 时选模板一步到位
Git 仓库 install 选取目标仓库和 tag 后 clone、装依赖、可选运行

init 做「内部」创建,install 做「任意仓库」拉取,两条能力互补。


八、总结

  • 架构commander框架。
  • 项目模板:npm 托管 + registry API 查版本。
  • 交互:Inquirer进行选择与输入,validate 做必填;ora 做 loading,log做成功/失败统一提示日志 ,debug模式采用log.verbose等。

【大白话前端 02】网页从解析到绘制的全流程

2026年3月2日 16:14

上一章我们讲到,浏览器在网络层面一路跋山涉水,最终把被切碎的数据包拼装成了一份纯文本文件。拿到文件后,便立刻交给了浏览器内部的渲染机器。

这台机器本质上就是一个精密的代码代工厂。纯文本的 HTML、CSS 和 JS 是无法直接变成眼前绚丽的页面,机器必须执行一套严密的 5 步流水线工序,把纯文本拼装成最终的交互画面。

graph TD
    A[HTML 代码] --> B(解析生成 DOM 树)
    C[CSS 代码] --> D(解析生成 CSSOM 树)
    B --> E{合成 Render Tree 渲染树}
    D --> E
    E --> F(Layout 布局计算)
    F --> G(Paint 绘制出图)

第一步:解析图纸,搭起车架子 (DOM 树)

浏览器无法直接阅读 HTML 里的尖括号文本。它做的第一件事,就是把这些嵌套的文本标签,转译成机器能懂的层级结构DOM 树。这棵树就是整个页面的车架子,规定了哪里是车门(<div>),哪里是方向盘(<button>)。

第二步:制备车漆配置单 (CSSOM 树)

骨架搭完后,浏览器紧接着解析 CSS 文本。不管样式写在哪,都会被统一合并梳理成另一棵树CSSOM。它记录了这扇门用什么颜色喷漆,那个轮毂多大尺寸。

致命阻塞:当流水线遇到 JavaScript

核心定律: 由于浏览器里的 JS 是单线程的,机器无法做到一边解析 HTML,一边去执行 JS。

所以一旦遇到了 <script> 标签,整条流水线会立刻强制停止。机器必须先去下载并执行完这段 JS 代码,然后才能继续解析HTML。这会导致网页首屏白屏卡住。

为了不让 JS 阻塞页面的渲染显示,我们有 3 种解法:

解法 1:传统写法 把所有 <script> 标签扔到 HTML 的最底部(也就是 </body> 前面)。这样机器会一口气先把页面画完,最后再去下 JS。

解法 2 和 解法 3:让机器一心二用 如果非把 JS 写在头部(<head>),可以给它加上 asyncdefer 属性。它们都能让机器开启后台并行下载,但之后的行为完全不同:

写法属性 下载时机 (谁在下?) 执行时机 (何时跑?) 适用场景 结论
啥都不加 阻塞流水线,必须下完 马上执行,流水线继续停工 旧时代的默认写法 极慢,已被淘汰
加 async 在后台并行下载 下载完马上霸道插队执行,流水线被迫停工 互相独立、不需要按顺序跑的脚本(如:百度统计代码) 谁先下完谁执行,适合独立模块
加 defer 在后台并行下载 必须等所有 HTML 全解析完,再按顺序排队执行 需要操作 DOM、或有关联顺序的核心业务代码 无脑首选,最稳健防白屏

正确做法: 绝大多数情况下,直接无脑写 <script src='xxx.js' defer></script>。这相当于告诉机器:你先去后台排队下吧,继续画你的 HTML 图纸,等你看完全部图纸了最后再来行这些 JS。

第三步:合成渲染树 (组装核心部件)

DOM 树只讲结构,CSSOM 树只讲样式。到了第三步,流水线把这两份图纸合并,生成真正用于显示的渲染树 (Render Tree)。这就好比按图纸把车架子和车漆拼成了待喷漆的白模车。

常见错误: 渲染树里只保留需要显示的节点。如果一个元素加了 display: none,或者它本身就是 <head> 这种不可见标签,这一步会被直接丢弃,根本不进后续流程。

第四步:布局 Layout (计算尺寸与位置)

进入布局阶段,也叫重排 (Reflow)。在这个阶段,机器精确计算每个零件在屏幕上的确切坐标(距离顶部、左边多少)和尺寸(宽、高)。相当于实地测绘占地面积。

第五步:绘制 Paint (上色与纹理)

最终坐标算清后,最后一步就是调用绘图 API,把真实像素画在屏幕上。这步也叫重绘 (Repaint)。至此,你在屏幕上终于看到了网页。

重排与重绘的性能损失

理解了流水线,你在写代码时就要尽量规避让机器大范围返工的操作。

概念 做了什么骚操作 机器被你逼着怎么返工? 性能开销
重排 (Reflow) 动了位置(如修改 width、margin) 布局 + 绘制这两步推翻重走,重新测绘周边所有元素位置。 极大(极易卡顿掉帧)
重绘 (Repaint) 只是换色(如修改 color、background) 尺寸和占地没变,只需把绘制这一步重走一遍。 较小

避坑指南:做“动画/动态位移”时,用 transform 代替 margin

1. 底层逻辑(为什么加了 transform 动画就不卡了?):

  • 如果你用 margin 做动画: 等于你告诉主流水线:“我要把这个小螺丝往右挪 100 像素”。流水线的刻板反应是:“收到,但螺丝位置变了,旁边的挡板可能要跟着动,车架子的重心可能变了——不行,我得把周边所有相连元素的受力面积和坐标全重新计算一遍(触发大规模重排),然后把它们全员重新上色(触发重绘)。” 这就叫牵一发而动全身。
  • 如果你用 transform 做动画: 浏览器一旦看到 transform 这个词,立刻懂了。它会在之前第 5 步(绘制 Paint)结束后,把这个加了位移的元素单独剪下来,变成一张独立的透明贴纸(前端术语叫:提升为独立图层 Layer)。 发生位移动画时,不仅不动兄弟元素的布局,它自己连颜色都不用重涂。这张画好的贴图直接过继给显卡(GPU),显卡不需要算尺寸,只需要像推玻璃一样,把这张完整的贴纸“滑”到新位置。这最后凌驾于流水线之上滑动图层拼装的工序,叫作合成(Composite)。

2. 终极一问:能无脑全用 transform 替代 margin 布局吗? 绝对不行! 正因为 transform 变成了悬浮的贴纸,它在视觉上飞走后,原地的物理坑位还在,它根本碰不到、也挤不开周围的兄弟元素

  • 结论:搭静态页面基础架构(抢地盘)时,就得砸重排的开销,老老实实用 margin / padding。而在做轮播图、弹窗飞入、元素悬浮变大等频繁动来动去的交互动画时,死磕 transform

了解了浏览器的渲染流水线,下一个要面对的现实是:不同浏览器(如 Chrome、Safari)的流水线标准并不完全一样。如果按个人直觉随意写代码,很容易出现不同设备上样式错位,或是触发刚才讲过的性能卡顿。 为此,整个前端行业制定了一套统一的“图纸底线规范”。

下一章请看:【大白话前端 03】Web 标准与最佳实践,我们将直接拆解这套规范,看看实战中到底该用什么样的标准来编写代码。

微信小程序实现随机撒花效果

作者 米饭同学i
2026年3月2日 16:10

微信小程序实现随机撒花效果的完整指南

效果展示

撒花效果是一种常见的页面装饰动画,特别是在活动页面、节日庆祝等场景中非常受欢迎。本文将详细介绍如何在微信小程序中实现一个随机撒花效果,包括随机位置、随机图片、随机动画时间等特性。

实现原理

实现撒花效果主要分为三个部分:

  1. 数据生成:通过 JavaScript 生成随机的撒花数据
  2. 页面渲染:使用 WXML 循环渲染撒花元素
  3. 动画效果:通过 CSS 定义撒花的下落和旋转动画

核心代码实现

1. 数据生成(JavaScript)

首先,我们需要创建一个函数来生成随机的撒花数据:

// 随机生成撒花
generateFlowerEffectArray(numFlowers){
  const images = Array.from({ length: 12 }, (_, i) => i + 1); // 生成1到12的图片编号数组  
  const maxTop = 100; // 撒花效果在视窗内的顶部范围
  const maxLeft = 750; // 撒花效果在视窗内的左侧范围
  const minTime = 2000; // 最小显示时间(毫秒)
  const maxTime = 7000; // 最大显示时间(毫秒)

  let flowerArray = [];  
  for (let i = 0; i < numFlowers; i++) {  
      const image = images[Math.floor(Math.random() * images.length)]; // 随机选择图片编号  
      const top = Math.random() * maxTop; // 随机顶部位置  
      const left = Math.random() * maxLeft; // 随机左侧位置  
      const time = (minTime + Math.random() * (maxTime - minTime))/1000; // 随机显示时间  

      flowerArray.push({  
          image,  
          top: -Math.round(top), // 负数表示从屏幕顶部外开始下落
          left: Math.round(left), // 四舍五入取整
          time: `${time}s`
      });  
  }  
  this.setData({
    ribbon: flowerArray
  })
}

2. 页面渲染(WXML)

在 WXML 文件中,我们使用 wx:for 循环渲染撒花元素:

<!-- 撒花效果 -->
<view>
  <block wx:for="{{ribbon}}" wx:key="{{index}}">
    <view class="ribbon" style="width:32rpx;height:32rpx;top:{{item.top}}rpx;left:{{item.left}}rpx;animation-duration:{{item.time}};animation-delay:{{index<80 ? '0s':'1.5s'}}">
            <image src="/path/to/ribbon{{item.image}}.png" style="width:32rpx;height:32rpx;animation-name: {{index%2==0 ? 'clockwiseSpin':'counterclockwiseSpinAndFlip'}};animation-duration:{{item.time}};animation-delay:{{index<80 ? '0s':'1.5s'}}"/>
    </view>
  </block>
</view>

3. 动画效果(CSS/SCSS)

在 SCSS 文件中,我们定义撒花的动画效果:

/* 撒花容器样式 */
.ribbon {
  position: absolute;
  animation-name: fade, drop;
  animation-delay: 0s, 0s;
  animation-iteration-count: infinite, infinite;
  animation-direction: normal, normal;
  animation-timing-function: linear, ease-in;

  image {
    animation-iteration-count: infinite;
    animation-direction: alternate;
    animation-timing-function: ease-in-out;
    transform-origin: 50% -100%;
  }
}

/* 淡入淡出动画 */
@keyframes fade {
  0% {
    opacity: 1;
  }

  95% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* 下落动画 */
@keyframes drop {
  0% {
    -webkit-transform: translate(0px, -50px);
  }

  100% {
    -webkit-transform: translate(0px, 1344rpx);
  }
}

/* 顺时针旋转动画 */
@keyframes clockwiseSpin {
  0% {
    -webkit-transform: rotate(-80deg);
  }

  100% {
    -webkit-transform: rotate(80deg);
  }
}

/* 逆时针旋转并翻转动画 */
@keyframes counterclockwiseSpinAndFlip {
  0% {
    -webkit-transform: scale(-1, 1) rotate(50deg);
  }

  100% {
    -webkit-transform: scale(-1, 1) rotate(-50deg);
  }
}

完整实现步骤

1. 初始化撒花效果

在页面加载时调用 generateFlowerEffectArray 函数,生成指定数量的撒花:

onLoad(options) {
  // 其他初始化代码...
  this.generateFlowerEffectArray(50); // 生成50个撒花元素
  // 其他初始化代码...
}

2. 图片资源准备

准备 12 张不同的撒花图片,命名为 ribbon1.pngribbon12.png,并放置在合适的目录中。

3. 样式调整

根据实际页面布局,调整以下参数:

  • maxTop:撒花开始下落的顶部范围
  • maxLeft:撒花的水平分布范围
  • minTimemaxTime:撒花下落的时间范围
  • animation-duration:动画持续时间
  • animation-delay:动画延迟时间

实现效果解析

  1. 随机性:通过 Math.random() 实现撒花的随机位置、随机图片和随机下落时间
  2. 层次感:通过 animation-delay 实现分批下落,增强视觉层次感
  3. 动态效果:结合 drop 下落动画和 clockwiseSpin/counterclockwiseSpinAndFlip 旋转动画,使撒花效果更加生动
  4. 性能优化:通过合理控制撒花数量,避免过多元素导致性能问题

代码优化建议

  1. 图片资源优化

    • 压缩撒花图片,减少加载时间
    • 考虑使用精灵图(Sprite)减少请求次数
  2. 动画性能优化

    • 使用 transformopacity 进行动画,避免重排
    • 考虑使用 will-change 属性提示浏览器优化动画
  3. 可配置性增强

    • 将撒花参数(数量、范围、速度等)提取为配置项
    • 支持不同场景的撒花效果定制

完整示例代码

JavaScript 部分

Page({
  data: {
    ribbon: []
  },

  onLoad() {
    this.generateFlowerEffectArray(50);
  },

  // 随机生成撒花
  generateFlowerEffectArray(numFlowers){
    const images = Array.from({ length: 12 }, (_, i) => i + 1);
    const maxTop = 100;
    const maxLeft = 750;
    const minTime = 2000;
    const maxTime = 7000;

    let flowerArray = [];
    for (let i = 0; i < numFlowers; i++) {
      const image = images[Math.floor(Math.random() * images.length)];
      const top = Math.random() * maxTop;
      const left = Math.random() * maxLeft;
      const time = (minTime + Math.random() * (maxTime - minTime))/1000;

      flowerArray.push({
        image,
        top: -Math.round(top),
        left: Math.round(left),
        time: `${time}s`
      });
    }
    this.setData({
      ribbon: flowerArray
    })
  }
})

WXML 部分

<view class="container">
  <!-- 撒花效果 -->
  <view>
    <block wx:for="{{ribbon}}" wx:key="{{index}}">
      <view class="ribbon" style="width:32rpx;height:32rpx;top:{{item.top}}rpx;left:{{item.left}}rpx;animation-duration:{{item.time}};animation-delay:{{index<80 ? '0s':'1.5s'}}">
              <image src="/path/to/ribbon{{item.image}}.png" style="width:32rpx;height:32rpx;animation-name: {{index%2==0 ? 'clockwiseSpin':'counterclockwiseSpinAndFlip'}};animation-duration:{{item.time}};animation-delay:{{index<80 ? '0s':'1.5s'}}"/>
      </view>
    </block>
  </view>
</view>

CSS 部分

.container {
  position: relative;
  overflow: hidden;
  height: 100vh;
}

.ribbon {
  position: absolute;
  animation-name: fade, drop;
  animation-delay: 0s, 0s;
  animation-iteration-count: infinite, infinite;
  animation-direction: normal, normal;
  animation-timing-function: linear, ease-in;
}

.ribbon image {
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-timing-function: ease-in-out;
  transform-origin: 50% -100%;
}

@keyframes fade {
  0% { opacity: 1; }
  95% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes drop {
  0% { transform: translate(0px, -50px); }
  100% { transform: translate(0px, 1344rpx); }
}

@keyframes clockwiseSpin {
  0% { transform: rotate(-80deg); }
  100% { transform: rotate(80deg); }
}

@keyframes counterclockwiseSpinAndFlip {
  0% { transform: scale(-1, 1) rotate(50deg); }
  100% { transform: scale(-1, 1) rotate(-50deg); }
}

总结

通过以上实现,我们可以在微信小程序中创建一个视觉效果丰富的随机撒花动画。这种效果不仅可以增强页面的视觉吸引力,还能为用户带来愉悦的交互体验。

实现过程中,我们通过 JavaScript 生成随机数据,WXML 循环渲染元素,CSS 定义动画效果,三部分紧密配合,共同构成了一个完整的撒花效果。

你可以根据实际需求调整参数,创造出不同风格的撒花效果,为你的小程序增添更多活力和趣味性。

❌
❌