普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月6日首页

全球最大云厂商,将「最牛马」的工作交给了 AI Agent

2025年12月6日 18:24

全球最大的云厂商,正在重构其在 Agent 时代的云计算核心。

美国东部时间 2025 年 12 月 2 日上午(北京时间 12 月 3 日凌晨),在拉斯维加斯举行的 re:Invent 大会上,亚马逊云科技宣布了一系列围绕 AI Agent 的重大更新:一方面是面向客户的 Agent 应用和平台工具,如 Amazon Connect、Kiro 等;另一方面是面向未来的底层基础设施,包括新一代 Trn4 AI 芯片以及 Trn3 超级服务器等。

今年以来,AI Agent 的爆发正在深刻影响企业工作流和生产力模式。亚马逊云科技的这些新产品和发布,似乎在回答两个关键的行业问题:如何确保 Agent 安全合规地「用好」,以及如何让 Agent 以经济可行的方式「跑起来」。正如亚马逊云科技 CEO Matt Garman 在主题演讲中所强调的,最终目标是实现「将 Agent 投入工作」(Put Agent into Work)。

如何让 Agent 可用丨来自:2025 re:Invent

有意思的是,在长达 2 个小时的主题演讲中,Matt Garman 用了 1 小时 50 分钟的时间介绍 AI 基础设施和全新的 Agent 产品,而只用了 10 分钟的时间提及传统云产品升级(如实例、存储、数据库等)。11:1 的时间分配对比,也印证了 AI Agent 及其背后的基础设施,已成为当下云厂商们最重要的战略核心。

Agent 应用与治理:从模型到可控的「数字员工」

AI Agent 的价值不再是传统的「聊天」,而在于「行动」。今年 re:Invent 期间,AWS 发布了一系列围绕 Agent 构建的产品和应用。

一方面,在企业服务领域,AWS 将 Agent 融入到各类传统的平台和工具之中,对后者实现技术升级从而提高生产效率。今年,针对企业数字化经营中最耗时、最复杂的三个领域:代码运维、应用现代化和客户服务,AWS 推出了一系列 Agent 产品。

三个不同场景的 Agent丨来自:2025 re:Invent

首先是在代码和运维领域,AWS 发布了多个 Agent 产品。其中 Kiro Autonomous Agent 值得关注。以前的 AI 编程工具虽然能写代码,但往往把开发者变成了忙于搬运上下文和协调工具的「助理」。而 Autonomous Agent 解决的就是这个问题,它可以被视为团队中一位 24 小时待命、过目不忘的「影子开发者」。

开发者只需专注于核心难题,把修 Bug、跨库变更等繁琐任务直接丢给它。它不仅能在后台自主规划并执行,还能像真人一样记住跨会话的上下文,通过连接 Jira 和 Slack 深度学习团队的业务逻辑与协作规范。随着每一次代码审查,它会越来越懂你们的产品和标准,真正成为团队共享的「超级大脑」,让开发者只做决策,不再打杂。

还有 DevOps Agent。从国内的「双 11」到海外的「黑五」,大型数字营销活动背后,技术运营人员熬夜加班已经成为常态。当下的模式是系统监测预警,技术人员需要随时待命处理每一个问题。而 AWS DevOps Agent 像是一个永不眠的运维员工,可以 24 小时调查事故和识别运营问题,从而大幅减少需要提报告给运营人员的警报数量,让后者可以更轻松更高效地处理那些真正有价值的问题,而非一直被一些没什么意义的警报轰炸。

「炸掉」技术债务丨来自:2025 re:Invent

同时,Agent 还在重塑企业的核心资产——传统应用。今天很多企业面临沉重的「技术债务」,很多传统应用的工作负载都运行在大型机、VMware 等传统服务器上,云迁移虽是趋势,但也是巨大的负担。而 Amazon Transform Agent 就像一位专业的「全能代码重构工程师」,能够对任何代码、API、语言、甚至企业自有的编程语言或框架进行定制化转换。这种能力将传统应用现代化的速度提升至 5 倍,减少 80% 的时间和成本。

在客服领域,Amazon Connect 的新 Agent 也帮助这个产品实现了能力飞跃。此次 Amazon Connect 一口气发布了四项更新,包括用更先进的语言模型提供更加自然、类人的对话体验;同时让 AI 掌握工具从而完成整理材料、执行常规流程等工作,让其与真人员工更好地协作;以及基于历史行为和点击流等构建客户画像,让 AI 可以提供更加个性化的推荐。

另外,随着企业部署越来越多 AI Agent 参与客户交互,理解其决策过程对保障服务质量与合规至关重要。Amazon Connect 还新增 AI Agent 可观测性功能,为企业提供高度透明度——清晰呈现 AI 的理解内容、使用的工具以及决策过程。这种可见性帮助企业优化性能、确保合规,并增强对 AI 交互体验的信心。

除了在已有的服务场景中引入 Agent 能力,另一方面,AWS 也对构建 Agent 的平台工具进行了升级。其中最核心的是加强了 Agent 的治理与评估,让客户可以为 Agent 行为设置「红线」。

让 Agent 可评估和被约束丨来自:2025 re:Invent

随着 Agent 获得执行企业操作的权限,可控性和可信赖性成为其规模化落地的首要前提。AWS 本次发布的重点在于:对 Agent 的约束,必须从传统的「内容安全」转向更高级的「行为治理」

AWS 推出的 Policy in AgentCore 功能,正是针对 Agent 行为治理的新工具。它允许用户使用自然语言来设定 Agent 的行为边界。这与传统安全护栏(Guardrails)仅过滤语言内容不同,Policy 可以简单地创建和管理 Agent 运行策略,并在 Agent 工作流中进行实时检查,确保 Agent 的操作始终在预设的权限范围内。

此外,为了确保 Agent 在实际工作中的表现,AWS 还推出了 AgentCore Evaluations。这项服务允许基于真实世界的行为对 Agent 的工作质量进行持续检查和评估,为企业规模化部署 Agent 提供了可靠的性能衡量标准。

通过这一系列治理工具,AWS 试图告诉企业:AI Agent 是可以信任的。只要设置了明确的「红线」,平台就能保证 Agent 的行为始终在安全边界之内。

基础设施升级:构建 AI 应用生态的算力和模型「基石」

如果说 Agent 是台前的「数字员工」,那么支撑它们日夜运转的基础设施就是幕后的「超级工厂」。

今年 AWS 在基础设施层面的动作,似乎在向行业传达一个信号:要让 Agent 真正普及,不仅要让它变得聪明,更要让企业「用得起」且「不论用什么模型都能跑得好」。

首先是算力层面。Agent 时代对算力的消耗模式发生了根本改变。过去,企业关注的是「训练」一个大模型需要多少张卡;而在 Agent 时代,成千上万个 Agent 需要 24 小时在线,进行持续的推理、规划和工具调用。推理成本如果居高不下,Agent 就无法大规模落地。

Trn3 UltraServer丨来自:2025 re:invent

Matt Garman 在会上宣布,AWS 正式推出了由第四代 AI 芯片驱动的 Amazon EC2 Trn3 UltraServer。作为 AWS 首款采用 3nm 工艺制造的 AI 芯片,Trainium3 实际上是在构建一种比通用 GPU 更具性价比的算力替代方案。每颗芯片提供 2.52 PFLOPs 的 FP8 计算能力,配合 144 GB 内存(比上一代提升 1.5 倍),它完美契合了 Agent 应用中长上下文架构和多模态推理的需求。

Trn3 UltraServer 服务器最多可以集成 144 颗 Trn3 芯片,并可以通过 EC2 UltraClusters 扩展至数十万颗。而对于企业最关心的成本问题,Trn3 在 Bedrock 上的表现是:相比 Trn2,性能提升 3 倍,每 MW 能耗的输出 Tokens 数提升超过 5 倍。对于目前 AI 算力昂贵的挑战,Trn3 的推出可以起到明显的降低作用。

而在模型层面,AWS 再次证明了自己是「最开放的 AI 平台」。通过 Amazon Bedrock,AWS 打出了一套「自研强模型 + 全球全明星模型托管」的组合拳。

自研模型方面,AWS 正式发布了下一代 Amazon Nova 2 模型家族。其中包括了针对 Agent 语音交互优化的 Nova 2 Sonic——这是一款新一代的 speech-to-speech 模型,它不仅具备行业领先的对话质量和更低延迟,还能实现实时、人类般的语音对话。

此外,AWS 还推出了 Amazon Nova Forge,首次引入了「开放式训练模型」理念。它解决了企业「既想深度定制又怕灾难性遗忘」的痛点。与今天大多专有模型依赖后训练的精调或者接入专用数据库不同,Forge 允许开发者访问 Nova 训练检查点,并在训练的每一个阶段将自有数据与 Amazon 精选的数据集深度融合,从而训练出既理解业务又保留大模型智能水平的专属模型。

Bedrock 的模型合作伙伴丨来自:2025 re:invent

另外值得关注的是,今年 AWS 展现出对中国本土 AI 模型前所未有的拥抱。在 Bedrock 新增的 18 个完全托管模型名单中,三家中国公司的四个模型名列其中:

  • 月之暗面的 Kimi K2 思考模型:具备深度推理能力,能在使用工具的同时进行思考。
  • MiniMax AI 的 MiniMax M2 模型:适合 Coding 和自动化场景,擅长多文件编辑和长工具调用链,被视为开发者 Agent 的强力大脑。
  • 阿里巴巴的 Qwen 模型:其中的 Qwen3-VL 能将屏幕截图直接转换为可运行的代码,并自动执行界面点击操作,是自动化 UI 操作的神器。

过去,全球云厂商往往更倾向于绑定少数几家欧美头部模型厂商。而 AWS 此次将 Kimi、MiniMax、Qwen 等中国顶尖模型纳入核心库,不仅是因为这些模型在性能上已经具备了全球竞争力,更显示了 AWS「互联互通」的生态格局。

对于企业而言,这意味着选择权的极大丰富。无论是需要 Mistral Large 3 这样的长文档处理专家,还是需要中国本土的优秀模型,AWS Bedrock 正在变成一个打破地域和技术栈隔阂的「万能转换插座」,让算力和模型真正成为像水电一样的资源。

当喧嚣退去,AWS 正在为 AI Agent 制定「基本法」

乍看之下,今年的 re:Invent 似乎显得有些「波澜不惊」。这里没有令人瞠目结舌的参数大跃进,也没有颠覆认知的「黑科技」突袭。

Trn3 的性能提升固然强悍,但基本也在预期之内;更有性价比的 Nova 2 和首次推出的语音模型,虽然让人眼前一亮,但在如今 SOTA 模型遍地跑、参数竞赛白热化的行业背景下,似乎也算不上「核弹级」的重磅发布。即便是最受关注的 AI 编程工具 Kiro 和 Agent 开发平台 AgentCore,也多是基于既有产品的更新——这些关于安全性、可观测性或辅助功能的修修补补,难免让人产生「没什么大动作」的错觉。

然而,这种「平淡」或许正是 AWS 最厉害的地方。当我们将视线从单个产品移开,投向整个产业,会发现 AWS 其实在定义下一代基础设施的路上,迈出了极为关键的一步。

AWS 做对的第一件事,是率先打破了 Agent 的「空谈」阶段。在大多数平台还在比拼 Agent 框架的灵活性、推理速度时,AWS 敏锐地意识到:企业需要的不是一个能聊天的机器人,而是一个能干活的员工。于是我们看到,Transform Agent 被用来解决棘手的技术债务,DevOps Agent 被用来处理繁琐的运维报警。

AWS 不再只是提供一个简单的 LLM 接口,而是将行业 Know-how(如 19 年的运维经验、代码迁移经验)封装进 Agent,将其打造成了真正能解决具体业务痛点的「成品工具」。这种「将能力封装为产品」的思路,标志着 Agent 从技术玩具正式迈向了商业实战。

更深层的变革在于 Agent 治理。Agent 的运行范式与过去的 Chat 类应用和传统的云计算业务有着本质区别。传统的云关注「资源」,Chat 应用关注「内容」,而 Agent 关注的是「行动」。将一个拥有自主决策权的 Agent 放入企业的核心业务流,其风险不亚于招聘一名不受控的员工。难点不在于如何让 Agent 跑起来,而在于如何让它不乱跑

Policy in AgentCore丨来自:2025 re:invent

AWS 在本次大会上展示的 Policy 功能,实际上是在尝试重新定义一套 Agent 时代的治理范式。这种用自然语言设定边界的方式,不再是死板的代码约束,而更像是给数字员工颁布一套「法律」。它让管理者可以用人类的逻辑(如「退款金额不得超过 1000 元」)来约束 AI 的行为。这种治理模式的建立,比单一模型的性能提升更具战略意义——因为只有解决了「可控性」和「合规性」这两个拦路虎,企业才敢真正让 Agent 接入核心业务。

最终,当我们重新审视这次 re:Invent,会发现它的意义不在于某个单品的参数碾压,而在于生态位的抢先占领。当大多数玩家还沉浸在解决架构优化和算力堆叠的「基建期」时,AWS 已经通过一系列真实的落地案例和完善的治理技术栈,开始为行业「打样」——它展示了一个 Agent 在真实企业环境中,应该如何被构建、如何被管理、以及如何产生价值。

这或许不是一场充满噱头的发布会,但对于渴望用 AI 提效的实体产业而言,AWS 正在构建的那套让 Agent「可用、可控、可信」 的基础设施,可能是通往未来的真正门票。

在拉斯维加斯,我看到了体育的未来

2025年12月6日 18:18

今年在拉斯维加斯举行的「云计算春晚」——re:Invent,新增了一个非常特殊的板块:体育论坛(Sports Forum)。

如果你是 re:Invent 的常客,大概会对其典型的「硬核技术风」印象深刻:在威尼斯人酒店望不到头的长廊里,数百个会议室密集输出着关于架构、代码和 AI 的硬核干货;展区里则密密麻麻排列着大大小小的展位,一张桌子、一台演示 demo 的笔记本,往往就代表着一个复杂的 ToB 产品。

这里的空气,充斥着「计算存储」、「云原生」、「Agentic AI」等术语。走廊里随处可见行色匆匆的开发者,或是盘腿坐在地毯上敲代码的极客,抑或是坐在简易塑料桌椅上低声洽谈百万级合作的行业伙伴。

Sports Forum 丨来自:2025 re:Invent

但当你推开 Sports Forum 的大门时,画风发生了一百八十度的大转弯。我甚至一度怀疑自己是不是走错了片场。这里不再是严肃的技术讨论场,而是一个充满活力的「主题乐园」。眼前不再是枯燥的代码屏幕和架构图,而是投篮机、正规尺寸的半场篮球场、乒乓球台,以及轰鸣声不断的 F1 模拟器和激战正酣的电竞舞台。

但如果你认为这仅仅是个活跃气氛的「游乐场」,那就被骗了。事实上,这可能是整个 re:Invent 技术含量最高的区域之一。揭开这些娱乐设施的幕布,背后全是硬核的算力和算法。亚马逊云科技正在用云和 AI,在体育行业里掀起新一轮技术革命。

Sports Forum 里的 VR 观赛体验区丨来自:2025 re:Invent

展馆中央的 NBA VR 体验区格外引人注目。戴上头显后,你不再是个被固定在座位上观看 360 度全景的观众,而是能以裁判甚至球员的视角,自由观看比赛名场面。更令人惊喜的是,系统还能实时展示投篮难度、防守统计等高端数据分析。

我瞬间意识到:这不仅是显示技术的进步,更是 AI 技术的深度应用。

NBA 的数据革命:从「统计结果」到「理解过程」

NBA 名宿、怒吼天尊拉希德·华莱士有一个标志性台词:「篮球不会说谎。」

这个名言广泛流传于 NBA 文化圈,并被勒布朗·詹姆斯等明星球员在比赛中多次引用的背后,其实道出的是职业球员的无奈:数据很多时候并不能反映真实的比赛过程。而这一现象,正在随着科技的发展迎来改变。

2025 年 10 月 2 日,当 NBA 宣布与亚马逊云科技达成战略合作的那一刻,篮球这项拥有百年历史的运动,悄然迎来改变。

对于资深球迷而言,我们习惯了用数据去评价球员:得分、篮板、助攻,进阶一点的看 PER 值、正负值。但坦白讲,这些传统的高阶数据依然停留在「统计学」的范畴——它们记录的是结果,而不是真正体现出比赛过程。

这就导致了「数据刷子」的存在,也导致了许多隐形价值被忽略:库里无球跑动时对防线的巨大牵制力,在统计表里是 0;一位防守悍将对持球人的窒息逼抢,只要没产生抢断或盖帽,在数据栏里也是空白。而即使效率值和正负值这种高阶数据,实际上也很难完美体现出每一个不同个性和特点的球员在场上的真正作用。

而亚马逊云科技带来的技术解法,是让机器真正「看懂」比赛。

通过计算机视觉和机器学习技术,现在的系统不再只是记录「球进没进」,而是以每秒 60 次的高频率,实时捕捉并分析球员身上 29 个骨骼点的移动轨迹。

这标志着体育数据从「结构化统计」迈向了「多模态理解」。基于此,NBA 在 2025-26 赛季能够推出三项全新的高阶数据:

投篮难度指数丨来自:亚马逊云科技

第一,防守数据统计(Defensive Box Score)。防守一直是篮球场上的「玄学」。过去我们评价追梦格林防守好,全凭印象流。现在,AI 算法能实时识别每一秒钟「场上谁在防谁」,并计算防守施压频率、协防质量等。这意味着,防守端的贡献第一次有了客观的数据标尺。

第二,投篮难度指数(Shot Difficulty)。不是所有的两分球都生而平等。空位吃饼的 50% 命中率,和在双人包夹下后仰跳投的 45% 命中率,含金量截然不同。新系统通过分析投篮时的身体平衡、防守干扰距离等因素,计算出每一次出手的「难度分」。它能有效区分「体系球员」和「巨星硬解」,还原球星的真实价值。

第三,引力指标(Gravity)。这可能是最令战术分析师兴奋的指标。它通过复杂的三角函数运算,量化一名无球球员吸引了多少防守注意力,以及为队友拉扯出了多大的空间。库里那种「虽然没拿球,但整个防线都因我而动」的影响力,终于变成了可视化的数据。

除了赛场上数据统计规则的重塑,场下的训练和观赛体验也在被改写。

数字投篮实验室丨来自:2025 re:Invent

在 Sports Forum 现场,NBA 多伦多猛龙队展示了他们的「数字投篮实验室」。利用先进的摄像机网络和 AI,系统能实时捕捉每一次投篮的详细生物力学数据,即时分析姿势、轨迹和发力机制。这相当于给每位球员配备了一个拥有「火眼金睛」的 AI 助教,能精确指出哪怕 1 度的姿态偏差。

亚马逊云科技的体育科技:当 AI 介入毫秒级的竞赛

不仅是 NBA,如果我们把视野拉宽,会发现亚马逊云科技构建的这套技术栈,正在重塑整个职业体育的「竞技」与「体验」。

首先是在残酷的职业赛场,比如毫秒必争的 F1、NFL 等比赛,AI 正在成为球队的新助教。

模拟 F1 车队进站换胎丨来自:2025 re:Invent

以 F1 法拉利车队为例,进站换胎是 0.1 秒级的战争。法拉利利用亚马逊云科技 SageMaker 开发了一套进站分析系统,将完成单次进站分析的时间从数小时压缩到了 60-90 秒。系统通过 AI 视觉识别,能自动分析换胎工的每一个动作细节,帮助车队在每一场比赛中寻找那微小的效率提升空间。同时,在车辆设计上,亚马逊云科技的高性能计算如同「数字风洞」,通过千万次的流体力学模拟替代昂贵的物理测试,让赛车设计的迭代速度提升了 70%。

而在对抗激烈的 NFL(职业橄榄球大联盟),亚马逊云科技协助创建了「数字运动员」(Digital Athlete())平台。这实际上是在云端构建了球员的「数字双胞胎」。系统运行了数百万次比赛场景模拟,涵盖了相当于 10000 个赛季的数据,以此来预测受伤风险。NFL 最近修改的开球规则,正是基于这些模拟数据,在保护球员安全与保证比赛观赏性之间找到了最优解。

而对于屏幕前的观众,AI 正在将「看热闹」升级为「看门道」。

今年 re:Invent 期间,亚马逊云科技 CEO Matt Garman 发布了新一代自研的 Amazon Nova2() 的系列模型,不仅有高性价比的推理模型 Lite、处理复杂推理的 Pro, 语音模型 Sonic, 这次还推出了业界首个真正统一的多模态模型 Omni。

而在过去一年里,Nova 模型正在悄然改变着体育行业的内容生态。

AI 辅助生成的德甲短内容丨来自:2025 re:Invent

比如德甲联赛,就在尝试利用亚马逊云科技的技术能力,成为最受球迷欢迎的足球联赛。其负责人在 Sports Forum 上分享了德甲联赛如何利用 Nova 改造了其内容生产的工作流,包括帮助编辑节省时间的「自动化战报」、「德甲故事」,翻译和转录来实现视频本地化,在保持比赛原声和氛围的同时,自动完成多语言转换, 以及满足球迷查阅和聊天需求的「AI 球迷助手」。

现代体育赛事本就是一个多维度信息的融合:从实况解说的语音,到精彩瞬间的画面,从战术数据的分析,到球员表情的特写,每一个环节都在传递着比赛的张力与故事。Nova 的多模态处理能力恰恰可以满足这种复杂场景的需求,精准处理这些交织在一起的文本、图像、视频和音频信息,为球迷带来更丰富的观赛体验。

还有更早推出的「比赛事实」(Match Facts)。AI 实时计算「预期进球概率」(xGoals),让观众直观地知道,这个球没进究竟是运气太差,还是射术不精。更有趣的是「技能角色卡」功能,AI 能自动分析出谁是「终结者」,谁是「策动者」,让伪球迷也能瞬间秒懂场上球员的战术定位。

通过这些措施,德甲编辑可以在人手不变的情况下,几倍增加生成内容,不论是海外球迷、新球迷还是硬核球迷,都能有更好的观赛体验。

而伴随技术进步,生成式 AI 也在改变我们观看比赛的互动逻辑。比如开头提到的 VR 观赛,就用到了 NBA 最新的 "战术探索"(Play Finder())功能,允许球迷用自然语言搜索视频。你不用再输入复杂的关键词,只需说一句「帮我找东契奇所有的后撤步三分」,AI 不仅能理解语义,还能结合对球员骨骼移动轨迹的分析,从海量历史视频库中精准匹配出相关片段。

AI 改变竞技体育丨来自:2025 re:Invent

结语

走出 Sports Forum,我不禁思考:为什么亚马逊、微软、谷歌,以及国内的阿里云、腾讯云等科技巨头,都要在体育领域卷得这么厉害?

仅仅是为了卖云服务给体育联盟吗?我想这只是商业的一面。

从技术演进的角度看,体育正在成为 AI 的终极试炼场

历史上,F1 赛车一直是汽车工业的试验场,如今的民用车技术许多都源自赛道;NBA 和世界杯则是鞋服科技的试验田。而现在,体育场景拥有最极端的要求:毫秒级的低延迟、物理世界的极端复杂性、以及难以预测的球员动作。

如果亚马逊云科技的 AI 能力,能在 NBA 总决赛中提供毫秒级的投篮概率预测,能在 F1 赛车 300 公里时速下完成实时推理,能在 NFL 的肌肉丛林中准确预测人体风险……经历过"魔鬼级"应用场景下一系列的"抗压测试", 那么,证明这套技术在物理世界中具有了极强的鲁棒性。

这种溢出效应的价值前景非常可观。今天我们在 Sports Forum 里看到的、用来保护 NFL 球员膝盖的算法,明天可能就会应用在老人的康复医疗中;今天用来分析 F1 赛车流体力学的算力,明天可能就会用于设计更高效的新能源汽车。

我们在 re:Invent 现场看到的,不仅仅是更精彩的比赛,更是 AI 技术通过体育这一载体,向物理世界和人体奥秘深度渗透的预演。

当科技的终极命题遇上人类最纯粹的竞技热情,一个由数据驱动、AI 赋能的新纪元,正在加速到来。

Event Loop 教你高效 “划水”:JS 单线程的“摸鱼”指南

2025年12月6日 18:05

前言

各位前端打工人,有没有过这种经历:明明写了 setTimeout(() => console.log('摸鱼')),结果同步代码还没跑完,摸鱼计划就被打断?其实 JS 单线程就像一个只能专注干一件事的打工人,而 Event Loop 就是它的 “高效摸鱼手册”—— 既能按时完成核心工作,又能把耗时任务 “挂起摸鱼”,今天咱们就一起好好聊聊这份手册!

一、先搞懂:JS 打工人为啥不能 “硬卷”?(进程线程的底层逻辑)

要想摸鱼,得先知道 “工作台” 的规矩:

  • 进程:好比公司的独立部门 —— 比如浏览器开个新标签页,就是开了个新部门,每个部门都有自己的办公资源(电脑、文件)。

  • 线程:部门里真正干活的打工人 —— 浏览器部门里就有三个核心员工:

    1. 渲染线程(负责画页面,比如给按钮上色、排版文字);
    2. JS 引擎线程(咱们的主角,负责跑代码);
    3. HTTP 请求线程(负责发接口,比如向服务器要数据)。

但这里有个 “办公室规定”:JS 引擎线程和渲染线程是 “互斥同事” ——JS 能修改 DOM(比如把按钮改成红色),要是它俩同时干活,页面就会出现 “排版错乱”(比如按钮画到一半被改成红色),所以必须 “你歇我干”。

更关键的是:JS 引擎线程是个 “独生子” (V8 引擎默认只开一个线程)。这就意味着:如果 JS 遇到一个耗时 10 秒的计算任务(比如统计 100 万条数据),它就会一直死磕这个任务,导致渲染线程没法干活,页面直接卡成 “PPT”—— 这就是 “硬卷” 的下场!

所以 JS 打工人的生存法则是:能摸鱼就不硬卷,耗时任务先 “挂起”,等核心工作做完再处理—— 这就是 “异步摸鱼” 的核心逻辑。

二、Event Loop:摸鱼任务的 “优先级排序”

JS 里的 “摸鱼任务”(异步任务) 分两类,就像公司里的 “紧急任务”“常规任务”,得按顺序处理,不能乱摸鱼:

  • 微任务:紧急摸鱼任务(优先级高)—— 比如 Promise.then()async/await 后续代码、process.nextTick()(Node 环境),相当于 “老板临时交代的小任务,必须在下班前做完”;
  • 宏任务:常规摸鱼任务(优先级低)—— 比如 setTimeoutsetInterval、ajax 请求、I/O 操作、UI 渲染,相当于 “下周要交的报告,先放一放”;
  • 还有个特殊角色:同步任务—— 核心工作(比如写代码、算结果),必须优先做完,相当于 “当天要交的核心 KPI”。

Event Loop 就是这套摸鱼规则的 “监督者”,它的工作流程就像打工人的一天,记好这 4,摸鱼不翻车:

  1. 先清核心 KPI:先把当天的同步任务 (核心工作) 全部做完,遇到异步任务 (摸鱼任务),就按类型扔进 “微任务队列” (紧急摸鱼) 和 “宏任务队列” (常规摸鱼)
  2. 再处理紧急摸鱼:核心 KPI 做完后,把 “微任务队列” 里的所有任务一次性清完(比如老板临时交代的 3 个小任务,必须连续做完,不能中途打断);
  3. 中场休息(渲染页面) :紧急摸鱼任务处理完,浏览器会进行 “页面渲染”(比如更新 DOM、刷新页面),相当于打工人喝杯咖啡歇一歇;
  4. 开启下一轮摸鱼:从 “宏任务队列” 里拿一个任务执行,然后重复 1-3 步,直到所有任务做完。

三、实战摸鱼:用代码例子验证规则

光说不练假把式,咱们用真实代码模拟 JS 打工人的 “摸鱼一天”,看看 Event Loop 是怎么安排任务的!

例子 1:setTimeout为啥 “跑不赢” 同步代码?

先看这串经典代码:

let a = 1;
setTimeout(() => {
    a = 2
}, 1000)
console.log(a);

分析摸鱼过程

  • 同步代码(属于宏任务)先跑:let a=1 → 执行console.log(a),此时a还是 1;
  • setTimeout是宏任务,被扔进 “宏任务队列” 排队;
  • 同步跑完后,微任务队列为空,直接执行下一个宏任务(也就是 1 秒后的a=2)。

所以结果是:先输出 1,1 秒后a才变成 2

image.png

例子 2:Promise.then的 “VIP 特权”

我们看一道经典面试题:

console.log(1);
new Promise((resolve) => {
    console.log(2);
    resolve();
})
.then(() => {
    console.log(3);
    setTimeout(() => {
        console.log(4);
    }, 0)
})
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(6);
    }, 0)
}, 0)
console.log(7);

是不是已经头皮发麻了?根本不清楚打印顺序是啥,但是这道面试题我们必须拿下!

摸鱼步骤拆解

  1. 常规摸鱼(宏任务)开跑

    • 先执行console.log(1) → 输出1
    • 遇到new PromisePromise 构造函数里的代码是同步的,执行console.log(2) → 输出2,然后resolve()
    • then是微任务,扔进 “微任务队列”;
    • 遇到外层setTimeout:宏任务,扔进 “宏任务队列”;
    • 最后执行console.log(7) → 输出7
  2. 紧急摸鱼(微任务)接棒

    • 微任务队列里只有then的回调,执行它:console.log(3) → 输出3
    • 回调里的setTimeout(4)是宏任务,扔进 “宏任务队列”。
  3. 宏任务队列开跑(下一轮摸鱼)

    • 先拿第一个宏任务(外层setTimeout):执行console.log(5) → 输出5
    • 里面的setTimeout(6)扔进宏任务队列;
    • 再拿下一个宏任务(then里的setTimeout(4)):执行console.log(4) → 输出4
    • 最后拿setTimeout(6):执行console.log(6) → 输出6

最终输出顺序1 → 2 → 7 → 3 → 5 → 4 → 6

image.png

上图更清晰:

image.png

例子 3:async/await 是 “优雅摸鱼” 的语法糖

async/await 本质是 Promise 的语法糖,相当于给摸鱼任务加了 “自动排队” 功能,先搞懂它的用法

console.log('script start');
async function async1() {
    await async2()
    console.log('async1 end');
}
async function async2() {
    console.log('async2 end');
}
async1();

关键规则

  • async函数本身相当于 “返回 Promise 的函数”;
  • await fn()的本质是:await后面的代码,塞进了fn()返回的 Promise 的then里(也就是微任务队列)

拿这段代码分析:

  1. 同步执行console.log('script start') → 输出;

  2. 执行async1()

    • 进入async1,遇到await async2() → 先执行async2()(同步),输出async2 end
    • await把后续的console.log('async1 end')扔进微任务队列
  3. 继续执行同步代码

image.png

OK既然知道了原理我们就实战摸鱼

// 模拟耗时任务:向服务器要数据(宏任务)
function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('常规摸鱼:发接口请求(耗时 1 秒)');
            resolve('接口返回数据:用户列表');
        }, 1000);
    });
}
// 核心工作函数(async 标记为异步函数)
async function work() {
    console.log('核心工作:开始处理用户数据');
    // await 相当于“等待摸鱼任务完成,再继续核心工作”
    const data = await fetchData();
    // 这行代码会被扔进微任务队列,相当于“紧急摸鱼后的收尾工作”
    console.log(`核心工作:使用${data}完成报表`);
}
// 执行核心工作
work();
// 其他同步任务
console.log('核心工作:处理其他紧急事务');

摸鱼流程拆解:

  1. 执行同步任务:

    • 调用 work() 函数,打印 核心工作:开始处理用户数据
    • 遇到 await fetchData(),先执行 fetchData(),里面的 setTimeout 被扔进 “宏任务队列”(常规摸鱼);
    • await 会暂停 work 函数,跳出去执行其他同步任务,打印 核心工作:处理其他紧急事务 → 同步任务完成。
  2. 微任务队列为空,直接进入中场休息。

  3. 处理宏任务队列(常规摸鱼):

    • 1 秒后,执行 setTimeout 回调,打印 常规摸鱼:发接口请求(耗时 1 秒)Promise resolve 后,await 后面的代码被扔进 “微任务队列”。
  4. 再次处理微任务队列:

    • 执行 console.log(核心工作:使用 ${data} 完成报表) → 核心工作收尾。

image.png

这里的关键是:await 后面的代码会被自动塞进微任务队列,相当于 “摸鱼结束后,优先处理收尾工作”,不用手动写 then 回调,摸鱼更优雅!

大家可以复制代码去运行一下,时间延迟照片体现不出来~~

四、摸鱼避坑:这些误区千万别踩

  1. 误区 1:setTimeout 延迟时间是 “准确时间”

错! setTimeout(() => {}, 1000) 不是 “1 秒后立即执行”,而是 “1 秒后把任务扔进宏任务队列”,得等同步任务和微任务全部完成后才会执行。如果前面的任务耗时 2 秒,那摸鱼就得等 2 秒后才开始。

  1. 误区 2:Promise 构造函数里的代码是异步的

错! new Promise((resolve) => { 同步代码 }) 里的代码是同步执行的,只有 thencatch 回调才是微任务(异步)。比如下面的代码,会先打印 同步代码,再打印 微任务

new Promise((resolve) => {
    console.log('同步代码');
    resolve();
})
.then(() => {
    console.log('微任务')
});

image.png 3. 误区 3:async 函数返回值是 “原始数据”

错! async 函数默认返回一个 Promise 对象,哪怕你写 async function fn() { return 1; },调用 fn() 得到的也是 Promise { 1 },需要用 await 或 then 才能拿到值。

五、总结:Event Loop 摸鱼口诀(记熟直接用)

同步任务先干完,微任务队列清干净;

渲染页面歇一歇,宏任务来轮着干;

await 后藏微任务,Promise 构造是同步;

Event Loop 掌节奏,摸鱼工作两不误!

结语

其实 JS 单线程的 “摸鱼哲学”,本质是 “优先级管理”—— 核心工作优先做,耗时任务排队做,既不耽误事,又不浪费时间。掌握了 Event Loop,你不仅能看懂 JS 异步代码的执行顺序,还能写出更高效的代码,就像打工人掌握了摸鱼技巧,工作效率翻倍,摸鱼也不心慌!

Vue 3 Keep-Alive 深度实践:从原理到最佳实践

作者 cindershade
2025年12月6日 17:47

Vue 3 Keep-Alive 深度实践:从原理到最佳实践

前言

初入职场,我被安排用 Vue3 制作公司官网,有 5-6 个静态页面。开发完成后,领导在测试时提出一个问题:“为什么页面滑动后再切换到其它页面,返回时没有回到顶部?”调试后发现,是因为使用了 <keep-alive> 组件缓存页面导致的。这引发了我对 Vue 3 Keep-Alive 的浓厚兴趣。Keep-Alive 能帮助我们在页面间切换时保留组件的状态,使用户体验更加流畅。特别是在带有筛选和滚动列表的页面中,使用 Keep-Alive 可以在返回时保留用户之前的筛选条件和滚动位置,无需重新加载或初始化。

在本文中,我将结合实例,从基础到深入地解析 Vue 3 中的 Keep-Alive 组件原理、常见问题及最佳实践,帮助大家全面掌握这一功能。


一、了解 Keep-Alive:什么是组件缓存?

1.1 Keep-Alive 的本质

<keep-alive> 是 Vue 的内置组件,用于缓存组件实例,避免在切换时重复创建和销毁组件实例。换言之,当组件被包裹在 <keep-alive> 中离开视图时,它不会被销毁,而是进入缓存;再次访问时,该组件实例会被重新激活,状态依然保留。

示例场景:用户从列表页进入详情页后再返回列表页。

没有 Keep-Alive 的情况

  • 用户操作:首页 → 探索页 → 文章详情 → 探索页

  • 组件生命周期:

    • 首页:创建 → 挂载 → 销毁
    • 探索页:创建 → 挂载 → 销毁 → 重新创建 → 重新挂载
    • 文章详情:创建 → 挂载 → 销毁
    • 探索页(再次):重新创建 → 重新挂载(状态丢失)

有 Keep-Alive 的情况

  • 用户操作:首页 → 探索页 → 文章详情 → 探索页

  • 组件生命周期:

    • 首页:创建 → 挂载 → 停用(缓存)
    • 探索页:创建 → 挂载 → 停用(缓存)
    • 文章详情:创建 → 挂载 → 销毁
    • 探索页(再次):激活(从缓存恢复,状态保持)

使用 <keep-alive> 包裹的组件,在离开时不会销毁,而是进入「停用(deactivated)」状态;再次访问时触发「激活(activated)」状态,原先所有的响应式数据都仍然保留。这意味着,探索页中的筛选条件和滚动位置都还能保留在页面返回时显示,提高了用户体验。

1.2 Keep-Alive 的工作原理

Keep-Alive 通过以下机制来实现组件缓存:

  • 缓存机制:当组件从视图中被移除时,如果包裹在 <keep-alive> 中,组件实例不会被销毁,而是存放在内存中。下次访问该组件时,直接复用之前缓存的实例。
  • 生命周期钩子:被缓存组件在进入和离开时,会触发两个特殊的钩子 —— onActivated / onDeactivatedactivated / deactivated。可以在这些钩子中执行恢复或清理操作,例如刷新数据或保存状态。
  • 组件匹配<keep-alive> 默认会缓存所有包裹其中的组件实例。但如果需要精确控制,就会用到 includeexclude 属性,匹配组件的 name 选项来决定是否缓存。注意,这里的匹配依赖于组件的 name 属性,与路由配置无关。

1.3 核心属性

  • include:字符串、正则或数组,只有 name 匹配的组件才会被缓存。
  • exclude:字符串、正则或数组,name 匹配的组件将不会被缓存。
  • max:数字,指定最多缓存多少个组件实例,超过限制时会删除最近最少使用的实例。

注意:include/exclude 匹配的是组件的 name 选项。在 Vue 3.2.34 及以后,如果使用了 <script setup>,组件会自动根据文件名推断出 name,无需手动声明。


二、使用 Keep-Alive:基础到进阶

2.1 基础使用

最简单的使用方式是将动态组件放在 <keep-alive> 里面:

<template>
  <keep-alive>
    <component :is="currentComponent" />
  </keep-alive>
</template>

这样每次切换 currentComponent 时,之前的组件实例会被缓存,状态不会丢失。

2.2 在 Vue Router 中使用

在 Vue Router 配置中,为了让路由页面支持缓存,需要将 <keep-alive> 放在 <router-view> 的插槽中:

<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

这样 <keep-alive> 缓存的是路由对应的组件,而非 <router-view> 自身。不要包裹整个 <router-view>,而是通过插槽嵌套其渲染的组件。

2.3 使用 include 精确控制

如果只想缓存特定组件,可利用 include 属性:

<template>
  <router-view v-slot="{ Component }">
    <keep-alive include="Home,Explore">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

include 中的名称必须与组件的 name 完全一致,否则不起作用。

2.4 滑动位置缓存示例

以“探索”列表页为例:用户在该页设置筛选条件并滚动列表后,跳转到文章详情页,再返回“探索”页。如果没有使用 Keep-Alive,列表页组件会被重新创建,筛选条件和滚动位置会重置。

使用 <keep-alive> 缓存“探索”页后,返回时组件从缓存中激活,之前的 ref 值和 DOM 滚动位置依然保留。这保证了用户回到列表页时,能够看到原先浏览到的内容和筛选状态。

可以在组件中配合路由导航守卫保存和恢复滚动条位置:

  • onBeforeRouteLeave 钩子中记录 scrollTop
  • onActivated 钩子中恢复滚动条位置。

三、使用中的问题:Name 匹配的陷阱

3.1 问题场景

我们经常希望缓存某些页面状态,同时让某些页面不被缓存,例如:

  • “探索”列表页:需要缓存。
  • 登录/注册页:不需要缓存。
  • 文章详情页:通常不缓存。

3.2 第一次尝试:手动定义 Name

<script setup>
defineOptions({ name: 'Explore' })
</script>

然后在主组件中使用 include 指定名称:

<router-view v-slot="{ Component }">
  <keep-alive include="Home,Explore,UserCenter">
    <component :is="Component" />
  </keep-alive>
</router-view>

理论上只缓存 HomeExploreUserCenter

3.3 问题出现:为什么 Include 不生效?

  • 组件名称不匹配:include/exclude 匹配的是组件自身的 name 属性,而非路由配置中的 name
  • 自动生成的 Name:Vue 3.2.34+ 使用 <script setup> 会自动根据文件路径生成组件名,手动写的 name 可能与自动生成冲突。
  • 路由包装机制:Vue Router 渲染组件时可能进行包装,导致组件实际名称与原始组件不同。

依赖组件名匹配容易出错,需要更灵活的方法。


四、解决方式:深入理解底层逻辑

4.1 理解组件 Name 的生成机制

Vue 3.2.34+ 使用 <script setup> 的单文件组件会自动根据文件名推断组件的 name

  • src/pages/Explore/index.vue → 组件名 Explore
  • src/pages/User/Profile.vue → 组件名 Profile

无需手动定义 name,避免与自动推断冲突。

4.2 问题根源分析

  • 自动 Name 与路由名不一致。
  • Router 的组件包装可能导致 <keep-alive> 无法捕获组件原始 name。

4.3 解决方案:路由 Meta 控制缓存

  1. 移除手动定义的 Name
<script setup lang="js">
// Vue 会自动根据路径生成 name
</script>
  1. 在路由配置中设置 Meta
const routes = [
  {
    path: '/explore',
    name: 'Explore',
    component: () => import('@/pages/Explore/index.vue'),
    meta: { title: '探索', keepAlive: true }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/pages/Auth/index.vue'),
    meta: { title: '登录', keepAlive: false }
  },
  {
    path: '/article/:id',
    name: 'ArticleDetail',
    component: () => import('@/pages/ArticleDetail/index.vue'),
    meta: { title: '文章详情', keepAlive: false }
  }
]
  1. 在 App.vue 中根据 Meta 控制
<script setup lang="js">
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const shouldCache = computed(() => route.meta?.keepAlive !== false)
</script>

<template>
  <router-view v-slot="{ Component }">
    <keep-alive v-if="shouldCache">
      <component :is="Component" />
    </keep-alive>
    <component v-else :is="Component" />
  </router-view>
</template>

默认缓存所有页面,只有 meta.keepAlive 明确为 false 时才不缓存。

4.4 方案优势

  • 灵活性强:缓存策略直接写在路由配置中。
  • 可维护性好:缓存策略集中管理。
  • 避免匹配失败:不依赖手动 name。
  • 默认友好:设置默认缓存,仅对不需要缓存页面标记即可。

五、最佳实践总结

5.1 缓存策略建议

页面类型 是否缓存 缓存原因
首页(静态) ❌ 不缓存 内容简单,一般无需缓存
列表/浏览页 ✅ 缓存 保持筛选条件、分页状态、滚动位置等
详情页 ❌ 不缓存 每次展示不同内容,应重新加载
表单页 ❌ 不缓存 避免表单数据残留
登录/注册页 ❌ 不缓存 用户身份相关,每次重新初始化
个人中心/控制台 ✅ 缓存 保留子页面状态,提升体验

5.2 代码规范

  • 不要手动定义 Name,在 Vue 3.2.34+ 中自动推断。
<script setup>
// Vue 会自动推断 name
</script>
  • 使用路由 Meta 控制缓存。
  • 统一在 App.vue 中处理缓存逻辑。

5.3 生命周期钩子的使用

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  console.log('组件被激活(从缓存恢复)')
})

onDeactivated(() => {
  console.log('组件被停用(进入缓存)')
})
</script>

5.4 性能考虑

  • 内存占用:不要无限制缓存过多页面,可使用 max 限制。
  • 数据刷新:在 onActivated 中进行必要更新。
  • 缓存清理:登出或不常用页面可手动清除缓存。
  • 动画与过渡:确保 <keep-alive><transition> 嵌套顺序正确。

六、总结

6.1 关键要点

  • <keep-alive> 缓存组件实例,通过停用保留状态。
  • include/exclude 功能依赖组件 name
  • 推荐使用路由 meta.keepAlive 控制缓存。
  • 缓存组件支持 onActivated / onDeactivated 钩子。
  • 默认缓存大部分页面,只对需刷新页面明确禁用。

6.2 技术演进

手动定义 Name → 自动 Name → Meta 控制

  • 冗长易错 → 简化代码 → 灵活可靠

6.3 最终方案

  • 利用自动生成的组件名取消手动命名。
  • 通过路由 meta.keepAlive 控制缓存。
  • 在根组件统一处理缓存逻辑。
  • 默认缓存,明确例外。

这样既保持了代码简洁,又实现了灵活可控的缓存策略,确保用户在页面切换时能获得更好的体验。


参考资料

  • Vue 3 Keep-Alive 官方文档
  • Vue Router 官方文档
  • Vue 3.2.34 更新日志

【基础】Unity着色器网格和计算对象介绍

作者 SmalBox
2025年12月6日 17:47

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

Mesh网格定义与核心概念

顶点(Vertex)的本质与特性

顶点是构成3D模型的基本几何单元,每个顶点在三维空间中具有明确的坐标位置(x,y,z)。在Unity中,顶点不仅包含位置信息,还承载着模型渲染所需的多维数据:

  • 法线(Normal):垂直于表面的单位向量,决定光照计算的反射方向。平滑着色时,法线通过相邻面计算;硬边着色则直接使用面法线。
  • UV坐标:二维纹理映射坐标,将2D纹理精准贴合到3D表面。UV值范围通常为0-1,超出部分通过纹理环绕模式处理。
  • 顶点颜色:支持RGBA通道的颜色数据,常用于实现渐变纹理或动态光照效果。

程序化顶点生成

通过Shader Graph的Position节点和数学运算,可动态生成顶点位置。例如,创建波浪效果:

// 伪代码示例:顶点位置偏移

float4 position = TransformPosition(float4(input.position.x, sin(input.position.x * 10) * 0.1, input.position.z, 1));

此代码通过正弦函数沿X轴生成周期性波动,实现水面扭曲效果。

面(Face)的构成与渲染优化

三角形面片的优势

三角形作为3D建模的最小单位,具有以下核心特性:

  • 平面性:三个顶点必然共面,简化碰撞检测和光照计算。
  • 固定朝向:通过顶点顺序(顺时针/逆时针)定义正面/背面,支持背面剔除提升渲染效率。
  • 计算高效:三角形仅需3个顶点和3条边,比多边形更适合GPU并行处理。

多边形的实现原理

虽然多边形面片(如四边形)在建模中更直观,但渲染时会被分解为三角形。例如,Unity的网格渲染器会自动将四边形拆分为两个三角形,确保硬件兼容性。

URP Shader Graph中的网格数据处理

顶点属性节点详解

在Shader Graph中,通过以下节点访问顶点数据:

  • Position:获取模型空间或世界空间坐标。
  • Normal:读取法线向量,用于光照计算。
  • UV:访问纹理坐标,支持多通道UV(如UV1、UV2)。
  • Color:读取顶点颜色,支持与纹理混合。

示例:动态法线修改

创建凹凸效果时,可通过修改法线改变光照表现:

// 伪代码示例:法线扰动

float3 normal = normalize(input.normal + float3(0, sin(input.position.x * 10) * 0.1, 0));

此代码沿Y轴添加正弦波动,模拟表面起伏。

纹理映射与UV坐标实践

UV坐标的工作原理

UV坐标通过将3D表面展开为2D平面实现纹理映射。例如,立方体需6组UV坐标,而球体通常使用球形投影或立方体映射。

多通道UV应用

复杂模型可能使用多组UV坐标:

  • UV1:主纹理通道。
  • UV2:辅助纹理(如法线贴图)。
  • UV3:顶点动画或动态遮罩。

在Shader Graph中,通过UV节点选择通道,结合Sample Texture 2D实现多纹理混合。

顶点颜色与动态效果

顶点颜色的应用场景

  • 渐变纹理:通过顶点颜色控制材质过渡。
  • 动态光照:结合顶点颜色实现局部光照变化。
  • 调试工具:可视化法线或UV坐标。

示例:顶点颜色驱动透明度

创建渐隐效果时,可通过顶点颜色控制透明度:

// 伪代码示例:颜色驱动透明度

float4 color = input.color * float4(1, 1, 1, smoothstep(0.5, 0.8, input.color.a));

此代码根据顶点Alpha值平滑调整透明度,实现边缘渐隐。

URP Shader Graph的优化技巧

性能优化策略

  • 减少动态计算:将顶点属性计算移至顶点着色器。
  • 合并属性:通过Attributes节点打包数据,减少采样次数。
  • 使用LOD:根据距离简化网格复杂度。

移动端适配

  • 简化着色器:避免复杂数学运算。
  • 压缩纹理:使用ASTC或ETC2格式。
  • 动态批处理:启用URP的自动批处理功能。

进阶应用:程序化网格生成

动态网格创建

通过Create Mesh节点和Set Mesh节点,可在运行时生成网格:

// 伪代码示例:生成平面网格

Mesh mesh = new Mesh(); 
mesh.vertices = new Vector3[] {
          Vector3.zero,
          Vector3.right,
          Vector3.up,
          Vector3.right + Vector3.up
          };
mesh.triangles = new int[] { 0, 1, 2, 0, 2, 3 };

此代码创建了一个包含两个三角形的平面。

实例化渲染

使用Instancing节点和Set Mesh节点,可高效渲染大量相同网格:

// 伪代码示例:实例化渲染` 

MaterialPropertyBlock props = new MaterialPropertyBlock();
props.SetVector("_Color", Color.red);
Renderer renderer = GetComponent<Renderer>();
renderer.SetPropertyBlock(props); 
renderer.SetMaterial(material, 0);

此代码为所有实例设置统一颜色,减少Draw Calls。

常见问题与解决方案

法线错误

  • 现象:模型出现光照异常。
  • 解决:检查法线方向,使用Normalize节点修正。

UV拉伸

  • 现象:纹理在模型表面扭曲。
  • 解决:优化UV展开,或使用Tiling And Offset节点调整。

性能瓶颈

  • 现象:帧率下降。
  • 解决:简化着色器,减少动态计算,启用批处理。

总结与最佳实践

URP Shader Graph通过可视化节点系统,大幅降低了着色器开发门槛。掌握网格数据处理的核心要点:

  • 顶点属性:灵活运用位置、法线、UV和颜色。
  • 三角形优势:利用其平面性和计算效率优化渲染。
  • 程序化生成:通过动态创建实现复杂效果。
  • 性能优化:减少计算,合并数据,适配移动端。

结合URP的渲染管线特性和Shader Graph的节点化设计,开发者可快速实现从简单材质到复杂视觉效果的全方位创作。


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

Vue3 + Keep-Alive:实习中遇到的 window 滚动问题与实践

作者 cindershade
2025年12月6日 17:47

Vue3 + Keep-Alive:实习中遇到的 window 滚动问题与实践

前景:实习项目中的困扰

在实习期间,我参与了公司项目的前端开发,页面主要包括首页(Home)和探索页(Explore)。在项目中,这两个页面都使用 window 作为滚动容器。测试时发现一个问题:

首页和探索页都使用 window 作为滚动容器
↓
它们共享同一个 window.scrollY(全局变量)
↓
用户在探索页滚动到 500px
↓
window.scrollY = 500(全局状态)
↓
切换到首页(首页组件被缓存,状态保留)
↓
但 window.scrollY 仍然是 500(全局共享)
↓
首页显示时,看起来也在 500px 的位置 ❌

这个问题的原因在于:

  • <keep-alive> 只缓存组件实例和 DOM,不管理滚动状态。
  • window.scrollY 是全局浏览器状态,不会随组件缓存自动恢复。
  • 结果就是组件被缓存后,滚动位置被错误共享,导致用户体验不佳。

我的思路:滚动位置管理工具

为了在自己的项目中解决类似问题,我考虑了手动管理滚动位置的方案:

/**
 * 滚动位置管理工具
 * 用于在 keep-alive 缓存页面时,为每个路由独立保存和恢复滚动位置
 */
const scrollPositions = new Map()

export function saveScrollPosition(routePath) {
  const y = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop
  scrollPositions.set(routePath, y)
}

export function restoreScrollPosition(routePath, defaultY = 0) {
  const saved = scrollPositions.get(routePath) ?? defaultY
  requestAnimationFrame(() => {
    window.scrollTo(0, saved)
    document.documentElement.scrollTop = saved
    document.body.scrollTop = saved
  })
}

在组件中配合 Vue 生命周期钩子使用:

import { onActivated, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import { saveScrollPosition, restoreScrollPosition } from './scrollManager'

export default {
  setup() {
    const route = useRoute()

    // 组件激活时恢复滚动
    onActivated(() => {
      restoreScrollPosition(route.path, 0)
    })

    // 组件离开前保存滚动
    onBeforeUnmount(() => {
      saveScrollPosition(route.path)
    })
  }
}

公司项目的简化处理

在公司项目中,由于页面结构简单,不需要为每个路由保存独立滚动位置,因此我采用了统一重置滚动到顶部的方式:

// 路由切换后重置滚动位置
router.afterEach((to, from) => {
  if (to.path !== from.path) {
    setTimeout(() => {
      window.scrollTo(0, 0)
      document.documentElement.scrollTop = 0
      document.body.scrollTop = 0
    }, 0)
  }
})

这样可以保证:

  • 切换页面时始终从顶部开始。
  • 简单易维护,符合公司项目需求。
  • 避免了 Keep-Alive 缓存滚动穿透的问题。

总结

  1. <keep-alive> 缓存组件实例,但不管理 window 滚动状态,导致全局滚动共享问题。
  2. 自己项目中,可以通过滚动位置管理工具为每个路由独立保存和恢复滚动。
  3. 公司项目中,为简化处理,只需在路由切换后重置滚动到顶部即可。
  4. 总体经验:滚动管理要根据项目复杂度和需求选择方案,既保证用户体验,又保证可维护性。

过去三年跑输业绩比较基准10%且利润率为负的基金经理降薪30%

2025年12月6日 17:34
基金经理绩效薪酬与基金业绩强挂钩。根据《基金管理公司绩效考核管理指引》征求意见稿,对基金经理薪酬进行了调整,过去三年产品业绩低于业绩比较基准超过十个百分点且基金利润率为负的,其绩效薪酬应当较上一年明显下降,降幅不得少于30%;低于业绩比较基准超过十个百分点但基金利润率为正的,其绩效薪酬应当下降;低于业绩比较基准不足十个百分点且基金利润率为负的,其绩效薪酬不得提高;显著超过业绩比较基准且基金利润率为正的,其绩效薪酬可以合理适度提高。(财联社)

ElementUI组件出现大量重复样式

作者 魂祈梦
2025年12月6日 17:25

情况

image.png

点进去,是一个style标签,里面有六万多行样式 进去使用正则查找,发现有11处一模一样的样式

^.el-textarea__inner \{

image.png

过程

经过简单排查,发现问题在于element-variables.scss这个文件中,我框选的这一条代码。
image.png

但是把它注释掉,样式就没了,因为项目引入样式的方式是scss。
于是乎去查看官方文档,确实没啥问题。

image.png

于是我起了一个新的vue2+element-ui+scss项目,用同样的方式引入。
结果发现,是一样的,也有重复的样式说明这是Element的问题。

image.png

原因

element官方的scss文件中重复定义了样式 比如我引入以下样式 image.png 可以发现有两个重复样式

image.png

解决方法

Element早已停更,假如你不是迫不得已,应该停止使用这个UI库。
以下的所有方法都并不是一种优雅的解决方式,但是他们可以解决当前的问题。
解决方法来自github,但是位于以下文章的引用让我发现这个问题。
[vue.js - ElementUI重复引入样式问题 - 学习前端历程 - SegmentFault 思否] (segmentfault.com/a/119000002…)
令人遗憾的是,这篇文章里的方法根本不起作用。

postcss的cssnano(推荐)

github.com/ElemeFE/ele…
你只需要创建postcss.config.js文件,添加cssnano: {}即可去掉重复的样式。

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {},
    cssnano: {}
  },
};

fast-sass-loader(不推荐)

更换依赖为项目引入了额外的复杂性,所以这并不是推荐的方法

核心在于chainWebpack的配置,代码来自如下链接。
github.com/yibn2008/fa…
忽略下面的注释,这是我之前做的尝试。

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: (config) => {
    config.module.rules.delete('scss')

    let scssRule = config.module.rule('scss')
      .test(/\.scss$/);

    [
      { name: 'vue-style-loader' },
      { name: 'css-loader' },
      { name: 'postcss-loader' },
      { name: 'fast-sass-loader' }
    ].forEach((load) => {
      scssRule
        .use(load.name)
        .loader(load.loader || load.name)
        .options(load.options || {})
    })
  },
  // configureWebpack: {
  //   module: {
  //     rules: [
  //       {
  //         test: /\.(scss|sass)$/,
  //         use: [
  //           'css-loader',
  //           {
  //             loader: 'fast-sass-loader',
  //             options: {
  //               // includePaths: [... ]
  //             }
  //           }
  //         ]
  //       },
  //       // other loaders ...
  //     ]
  //   }
  // }
})

fast-sass-loader解决了这个问题,但是官方并没有给出vue-cli中的合理使用方式。
我找了很久如何在vue中使用这个东西。
当我直接修改vue中的webpack配置,卸载了sass-loader,完全没有作用。
包括github issue中有部分人也尝试使用这个工具,他们的配置也失败了,说明这不是个例。
image.png

编译出css避开问题(不推荐)

假如我要新加一个scss变量呢?
不推荐这种削足适履的方式

我没有尝试这种方式,但这种方式在原理上是可行的,因为他完全避开了问题,当使用css文件时,就不会编译,自然也就不会引发重复样式的问题。

github.com/ElemeFE/ele…
github.com/ElemeFE/ele…

总结

如果可以,我真不想用vue2和element。

广东白云机场T3航站楼投运满月,日均执行航班达158个

2025年12月6日 17:11
今年10月30日,大湾区重大工程——广州白云国际机场T3航站楼及第五跑道正式投运,白云机场成为中国民航首个拥有五条商用跑道的机场。如今,T3航站楼运行已超一个月。T3航站楼投运后,已有5家航司的国内业务从T1航站楼转场至这里,日均执行航班达158个,日均旅客吞吐量达2.6万人次左右。(央视新闻)

#1 onLongPressGesture

作者 Neo_Arsaka
2025年12月6日 17:00

功能

为任意 View 添加长按手势识别。当用户持续按压且达到指定时长、同时手指偏移不超过阈值时,视为一次有效长按;可实时获取按压状态以驱动过渡动画。

参数说明

  • minimumDuration:触发所需最短按压时间(秒)。
  • maximumDistance:手指允许的最大偏移,单位为点;超限即判定为取消。
  • onPressingChanged:按压状态变化回调;true 表示按下,false 表示抬起或滑出。
  • action:满足时长与偏移条件后执行的一次性回调。

代码示例

struct LongPressGestureBootcamp: View {
    
    @State var isComplete: Bool = false
    @State var isSuccess: Bool = false
    var body: some View {
        
        VStack {
            Rectangle()
                .fill(isSuccess ? .green : .blue)
                .frame(maxWidth: isComplete ? .infinity : 0)
                .frame(height: 56)
                .frame(maxWidth: .infinity, alignment: .leading)
                .background(.gray)
            
            HStack {
                Text("CLICK HERE")
                    .foregroundStyle(.white)
                    .padding()
                    .background(.black)
                    .cornerRadius(8)
                    .onLongPressGesture(
                        minimumDuration: 1.0,
                        maximumDistance: 56) { (isPressing) in
                            // start of press -> min duration
                            if isPressing {
                                withAnimation(.easeInOut(duration: 1.0)) {
                                    isComplete = true
                                }
                            }
                            else {
                                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                    if !isSuccess {
                                        withAnimation(.easeInOut) {
                                            isComplete = false
                                        }
                                    }
                                }
                            }
                        } perform: {
                            // at the min duration
                            withAnimation(.easeInOut) {
                                isSuccess = true
                            }
                        }
                
                Text("RESET")
                    .foregroundStyle(.white)
                    .padding()
                    .background(.black)
                    .cornerRadius(8)
                    .onTapGesture {
                        isComplete = false;
                        isSuccess = false;
                    }
            }
        }
        
        
//        Text(isComplete ? "COMPLETED" : "NOT COMPLETE")
//            .padding()
//            .background(isComplete ? .green : .gray)
//            .cornerRadius(8)
////            .onTapGesture {
////                withAnimation {
////                    isComplete.toggle()
////                }
////            }
//            .onLongPressGesture(minimumDuration: 1.0, maximumDistance: 50, perform: {
//                isComplete.toggle()
//            })
    }
}

注意事项

  1. 若同时附加 .onTapGesture,长按结束后可能额外触发一次点按,应通过状态标志互斥。
  2. onPressingChanged 中更新界面时,请使用 withAnimation 保证过渡流畅。
  3. 耗时操作请置于 action 的异步闭包内,避免阻塞主线程。

阿根廷将发行美元国债以偿还到期债务

2025年12月6日 16:52
阿根廷经济部长路易斯·卡普托5日确认该国将发行美元国债,标志着阿根廷重返国际债务市场。卡普托表示,阿根廷政府将于10日发行年名义利率为6.5%的债券,到期日为2029年11月,筹集的美元将部分用于偿还将于明年1月到期的债务。这是2018年1月以来阿根廷首次在国际债务市场上寻求外汇以偿还到期债务。卡普托强调,本次发行的国债不是新增债务,而是用来偿还旧债务。(新华社)

《Flutter全栈开发实战指南:从零到高级》- 23 -混合开发与WebView

2025年12月6日 16:51

混合开发为何如此重要?

在实际项目中,我们常面临这样的困境:业务需要快速迭代,但原生发版周期长;H5页面体验不佳,但开发速度快。混合开发正是解决这一矛盾的最佳平衡点

graph TD
    A[业务需求] --> B{开发方案选择}
    B --> C[原生开发]
    B --> D[Web开发]
    B --> E[混合开发]
    
    C --> F[优势: 性能最佳]
    C --> G[劣势: 迭代慢, 双端开发]
    
    D --> H[优势: 跨平台, 热更新]
    D --> I[劣势: 体验差, 能力受限]
    
    E --> J[融合两者优势]
    E --> K[平衡性能与效率]
    
    J --> L[原生体验 + Web灵活性]
    K --> M[快速迭代 + 一致体验]

一:WebView核心原理

1.1 WebView的本质是什么?

很多人以为WebView只是一个内置浏览器,其实远不止如此。WebView实际上是一个微型浏览器,它包含了HTML解析器、CSS渲染器、JavaScript引擎等完整组件。

graph TB
    subgraph "WebView内部架构"
        A[WebView容器] --> B[渲染引擎]
        A --> C[JavaScript引擎]
        A --> D[网络模块]
        
        B --> E[HTML解析器]
        B --> F[CSS渲染器]
        B --> G[布局引擎]
        
        C --> H[V8/JSCore引擎]
        D --> I[网络请求处理]
    end
    
    subgraph "Flutter侧"
        J[Dart VM] --> K[Flutter Engine]
        K --> L[Skia渲染]
    end
    
    A -.-> K
    C -.-> J

WebView和Flutter运行在不同的隔离环境中:

  • Flutter:运行在Dart VM,使用Skia渲染
  • WebView:运行在浏览器引擎中,有自己的渲染管线

1.2 Flutter中的WebView实现原理

Flutter的WebView并不是自己实现的浏览器引擎,而是对原生WebView的桥接封装

Untitled.png

Platform Channels工作原理

// Flutter调用原生方法的流程
1. Dart代码调用WebViewController的方法
2. 通过MethodChannel将二进制消息发送到原生端
3. 原生端调用对应的WebView API
4. 结果通过MethodChannel返回Dart

二:封装WebView

2.1 基础封装

先看一个在实际项目中使用的WebView封装,这个版本已经处理了大部分常见问题:

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class TestWebView extends StatefulWidget {
  final String url;
  final Map<String, String>? headers;
  
  const TestWebView({
    Key? key,
    required this.url,
    this.headers,
  }) : super(key: key);
  
  @override
  _TestWebViewState createState() => _TestWebViewState();
}

class _TestWebViewState extends State<TestWebView> {
  // 控制器
  late WebViewController _controller;
  
  // 状态管理
  double _progress = 0.0;
  bool _isLoading = true;
  bool _hasError = false;
  String? _pageTitle;
  
  @override
  void initState() {
    super.initState();
    _initWebView();
  }
  
  void _initWebView() {
    _controller = WebViewController()
      // 1. 基础配置
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.transparent)
      
      // 2. 注册JavaScript通信通道
      ..addJavaScriptChannel(
        'FlutterBridge',
        onMessageReceived: (message) {
          _handleJavaScriptMessage(message.message);
        },
      )
      
      // 3. 导航
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              _progress = 0.0;
              _isLoading = true;
              _hasError = false;
            });
          },
          
          onProgress: (progress) {
            setState(() => _progress = progress / 100.0);
          },
          
          onPageFinished: (url) async {
            // 获取页面标题
            final title = await _controller.getTitle();
            setState(() {
              _pageTitle = title;
              _isLoading = false;
            });
            
            // 注入自定义脚本
            await _injectCustomScripts();
          },
          
          // URL拦截
          onNavigationRequest: (request) {
            return _handleNavigation(request);
          },
        ),
      )
      
      // 4. 加载页面
      ..loadRequest(
        Uri.parse(widget.url),
        headers: widget.headers ?? {},
      );
  }
  
  // 处理JS消息
  void _handleJavaScriptMessage(String message) {
    try {
      final data = jsonDecode(message);
      final type = data['type'];
      final payload = data['data'];
      
      switch (type) {
        case 'userAction':
          _handleUserAction(payload);
          break;
        case 'getUserInfo':
          _sendUserInfoToWeb();
          break;
      }
    } catch (e) {
      print('JS消息解析失败: $e');
    }
  }
  
  // URL导航处理逻辑
  NavigationDecision _handleNavigation(NavigationRequest request) {
    final url = request.url;
    
    // 白名单
    if (!_isUrlInWhitelist(url)) {
      return NavigationDecision.prevent;
    }
    
    // 链接处理
    if (url.startsWith('myapp://')) {
      _handleDeepLink(url);
      return NavigationDecision.prevent;
    }
    
    return NavigationDecision.navigate;
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(),
      body: Stack(
        children: [
          // WebView主体
          WebViewWidget(controller: _controller),
          
          // 进度条
          if (_isLoading && _progress < 1.0)
            LinearProgressIndicator(
              value: _progress,
              backgroundColor: Colors.grey[200],
            ),
          
          // 错误状态
          if (_hasError)
            _buildErrorWidget(),
        ],
      ),
      // 底部导航栏
      bottomNavigationBar: _buildBottomBar(),
    );
  }
}

2.2 核心功能点

2.2.1 JavaScript通信原理

JavaScript与Flutter的通信是通过桥接实现的:

sequenceDiagram
    participant W as WebView(JS环境)
    participant B as JavaScriptChannel
    participant F as Flutter(Dart环境)
    participant H as 消息处理器
    
    W->>B: window.FlutterBridge.postMessage(JSON)
    B->>F: 通过Platform Channel传递消息
    F->>H: 解析并处理消息
    H->>F: 返回处理结果
    F->>W: _controller.runJavaScript()

核心技术点

  • 消息序列化:所有数据必须转为JSON串
  • 异步处理:异步通信并处理回调
  • 错误处理:JS或Flutter都可能出错,需要有错误处理

2.2.2 性能优化

class WebViewOptimizer {
  // 1. 缓存
  static void setupCache(WebViewController controller) async {
    await controller.runJavaScript('''
      // 启用Service Worker缓存
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/sw.js');
      }
      
      // 本地存储
      if (window.localStorage) {
        localStorage.setItem('lastVisit', new Date().toISOString());
      }
    ''');
  }
  
  // 2. 内存管理
  static void manageMemory(WebViewController controller) {
    // 清理缓存
    Timer.periodic(Duration(minutes: 5), (_) {
      controller.clearCache();
    });
  }
  
  // 3. 预加载
  static Future<void> preloadWebView({
    required String url,
    required BuildContext context,
  }) async {
    // 提前初始化WebView但不显示
    final controller = WebViewController();
    await controller.loadRequest(Uri.parse(url));
    
    // 保存到全局缓存
    WebViewCache.instance.cache(url, controller);
  }
}

三:混合应用设计

3.1 分层架构

一个良好的混合架构应该分为四个层次:

graph TB
    subgraph "1.表现层 Presentation Layer"
        A1[Flutter原生页面]
        A2[WebView容器]
        A3[混合页面]
    end
    
    subgraph "2.桥接层 Bridge Layer"
        B1[JavaScript Bridge]
        B2[消息路由器]
        B3[协议编解码器]
    end
    
    subgraph "3.业务层 Business Layer"
        C1[用户服务]
        C2[支付服务]
        C3[数据服务]
    end
    
    subgraph "4.基础设施层 Infrastructure"
        D1[WebView池]
        D2[缓存管理器]
        D3[网络层]
        D4[安全模块]
    end
    
    A1 --> B1
    A2 --> B1
    A3 --> B1
    
    B1 --> B2
    B2 --> C1
    B2 --> C2
    B2 --> C3
    
    C1 --> D4
    C2 --> D3
    C3 --> D2
    
    A2 --> D1

3.2 路由管理

混合应用最复杂的是路由管理。我们需要决定什么时候用原生页面,什么时候用WebView。

class HybridRouter {
  static final Map<String, RouteConfig> _routeTable = {
    '/home': RouteConfig(
      type: RouteType.native,
      path: '/home',
      webUrl: null,
    ),
    '/product/:id': RouteConfig(
      type: RouteType.hybrid,
      path: '/product/:id',
      webUrl: 'https://api.xxxx.com/product/{id}',
      nativeFallback: '/productDetail',
    ),
    '/promotion/:code': RouteConfig(
      type: RouteType.web,
      path: '/promotion/:code',
      webUrl: 'https://promo.xxxx.com/{code}',
    ),
  };
  
  // 路由
  static Future<void> navigateTo({
    required BuildContext context,
    required String path,
    Map<String, dynamic>? params,
  }) async {
    final config = _findRouteConfig(path);
    
    if (config == null) {
      // WebView
      await _openWebView(context, path, params);
      return;
    }
    
    final useWeb = await _shouldUseWebVersion(config);
    
    if (useWeb) {
      await _openWebView(context, config.webUrl!, params);
    } else {
      await _openNativePage(context, config.nativeFallback!, params);
    }
  }
  
  // 条件
  static Future<bool> _shouldUseWebVersion(RouteConfig config) async {
    // 1. 检查网络状况
    final connectivity = await Connectivity().checkConnectivity();
    if (connectivity == ConnectivityResult.none) {
      return false; // 离线时用原生
    }
    
    // 2. 检查用户偏好
    final prefs = await SharedPreferences.getInstance();
    final preferNative = prefs.getBool('prefer_native') ?? false;
    
    // 3. 检查页面类型
    switch (config.type) {
      case RouteType.native:
        return false;
      case RouteType.web:
        return true;
      case RouteType.hybrid:
        return await _businessDecision(config);
    }
  }
}

3.3 状态管理

混合应用的状态管理比纯原生应用更复杂,因为状态可能在三个地方:

状态存储位置:
1. Flutter/Dart状态
2. WebView/JavaScript状态  
3. 原生平台状态(iOS/Android)

状态管理.png

实现方案:

class HybridStateManager {
  // 状态存储
  final Map<String, dynamic> _globalState = {};
  
  // 状态同步方法
  Future<void> syncStateToWeb(WebViewController controller) async {
    final stateJson = jsonEncode(_globalState);
    await controller.runJavaScript('''
      // 更新Web端状态
      window.appState = $stateJson;
      
      // 触发状态更新事件
      window.dispatchEvent(new CustomEvent('appStateChanged', {
        detail: $stateJson
      }));
    ''');
  }
  
  // 从Web接收状态更新
  void handleStateFromWeb(Map<String, dynamic> newState) {
    _globalState.addAll(newState);
    
    // 通知Flutter组件
    _stateNotifier.value = {..._globalState};
    
    // 持久化
    _persistState();
  }
}

四:通信协议

4.1 消息协议

良好的通信从定义协议开始,实际项目中使用的协议规范,如下:

// 定义消息协议
class BridgeMessage {
  final String id;           // 消息ID
  final String type;         // 消息类型
  final String method;       // 方法名
  final dynamic data;        // 消息数据
  final int timestamp;       // 时间戳
  final String? callbackId;  // 回调ID
  
  // 消息类型
  static const String TYPE_REQUEST = 'request';
  static const String TYPE_RESPONSE = 'response';
  static const String TYPE_EVENT = 'event';
  
  // 常用方法
  static const String METHOD_GET_USER_INFO = 'getUserInfo';
  static const String METHOD_PAYMENT = 'startPayment';
  static const String METHOD_SHARE = 'shareContent';
  
  // 序列化
  String toJson() {
    return jsonEncode({
      'id': id,
      'type': type,
      'method': method,
      'data': data,
      'timestamp': timestamp,
      'callbackId': callbackId,
    });
  }
  
  // 反序列化
  static BridgeMessage fromJson(String jsonStr) {
    final map = jsonDecode(jsonStr);
    return BridgeMessage(
      id: map['id'],
      type: map['type'],
      method: map['method'],
      data: map['data'],
      timestamp: map['timestamp'],
      callbackId: map['callbackId'],
    );
  }
}

4.2 通信流程

sequenceDiagram
    participant H as H5页面(JS)
    participant B as JavaScript Bridge
    participant D as Dart消息分发器
    participant S as 业务服务
    participant N as 原生功能
    
    H->>B: 发送请求<br/>BridgeMessage
    Note over H,B: 1. 用户点击购买按钮
    
    B->>D: 通过Channel传递
    Note over B,D: 2. 平台通道传输
    
    D->>D: 解析验证消息
    Note over D: 3. 安全检查与验证
    
    alt 需要原生功能
        D->>N: 调用原生模块
        N->>D: 返回结果
    else 需要业务服务
        D->>S: 调用业务服务
        S->>D: 返回业务数据
    end
    
    D->>B: 构造响应消息
    B->>H: 返回结果
    Note over B,H: 6. 更新H5页面状态

4.3 错误处理

class BridgeErrorHandler {
  // 定义错误码
  static const Map<int, String> errorCodes = {
    1001: '网络连接失败',
    1002: '用户未登录',
    1003: '参数验证失败',
    1004: '权限不足',
    1005: '服务端错误',
  };
  
  // 统一错误处理
  static BridgeMessage handleError(
    dynamic error, 
    String messageId,
    String method,
  ) {
    int code = 1005; // 默认错误码
    String message = '未知错误';
    
    if (error is PlatformException) {
      code = int.parse(error.code);
      message = error.message ?? '平台异常';
    } else if (error is HttpException) {
      code = 1001;
      message = '网络请求失败';
    }
    
    return BridgeMessage(
      id: messageId,
      type: BridgeMessage.TYPE_RESPONSE,
      method: method,
      data: {
        'success': false,
        'error': {
          'code': code,
          'message': errorCodes[code] ?? message,
          'detail': error.toString(),
        },
      },
      timestamp: DateTime.now().millisecondsSinceEpoch,
    );
  }
}

五:性能优化

5.1 WebView启动优化

WebView首次启动慢是常见问题。我们可以通过预加载和复用来优化:

// 实现WebView池
class WebViewPool {
  static final Map<String, WebViewController> _pool = {};
  static final Map<String, DateTime> _lastUsed = {};
  
  // 获取WebView
  static Future<WebViewController> getWebView({
    required String key,
    required Future<WebViewController> Function() builder,
  }) async {
    // 1. 检查池中是否有可复用的
    if (_pool.containsKey(key)) {
      _lastUsed[key] = DateTime.now();
      return _pool[key]!;
    }
    
    // 2. 创建新的WebView
    final controller = await builder();
    _pool[key] = controller;
    _lastUsed[key] = DateTime.now();
    
    // 3. 清理过期缓存
    _cleanup();
    
    return controller;
  }
  
  // 预加载
  static Future<void> preload(List<String> urls) async {
    for (final url in urls) {
      final controller = WebViewController();
      await controller.loadRequest(Uri.parse(url));
      _pool[url] = controller;
    }
  }
}

5.2 内存管理

WebView是内存消耗大户,需要精细管理:

class WebViewMemoryManager {
  // 处理内存压力
  static void setupMemoryPressureHandler() {
    SystemChannels.lifecycle.setMessageHandler((msg) async {
      if (msg == AppLifecycleState.paused.toString()) {
        // App进入后台,释放WebView内存
        await _releaseWebViewMemory();
      } else if (msg == AppLifecycleState.resumed.toString()) {
        // App回到前台,恢复必要状态
        await _restoreWebViewState();
      }
      return null;
    });
  }
  
  static Future<void> _releaseWebViewMemory() async {
    // 1. 清除缓存
    for (final controller in WebViewPool._pool.values) {
      await controller.clearCache();
    }
    
    // 2. 卸载不活动的WebView
    final now = DateTime.now();
    WebViewPool._pool.entries
      .where((entry) {
        final lastUsed = WebViewPool._lastUsed[entry.key];
        return lastUsed != null && 
               now.difference(lastUsed) > Duration(minutes: 10);
      })
      .forEach((entry) {
        WebViewPool._pool.remove(entry.key);
        WebViewPool._lastUsed.remove(entry.key);
      });
  }
}

5.3 渲染性能优化

class WebViewPerformance {
  // 启用硬件加速
  static void enableHardwareAcceleration(WebViewController controller) {
    controller.runJavaScript('''
      // 启用CSS硬件加速
      const style = document.createElement('style');
      style.textContent = \`
        .animate-element {
          transform: translateZ(0);
          will-change: transform;
        }
        .fixed-element {
          position: fixed;
          backface-visibility: hidden;
        }
      \`;
      document.head.appendChild(style);
    ''');
  }
  
  // 监控性能指标
  static void setupPerformanceMonitor(WebViewController controller) {
    controller.runJavaScript('''
      // 使用Performance API监控
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach(entry => {
          if (entry.duration > 100) { 
            console.warn('长任务:', entry.name, entry.duration);
            
            // 发送到Flutter监控
            if (window.FlutterBridge) {
              window.FlutterBridge.postMessage(JSON.stringify({
                type: 'performance',
                data: {
                  metric: 'long_task',
                  name: entry.name,
                  duration: entry.duration,
                  timestamp: Date.now()
                }
              }));
            }
          }
        });
      });
      
      observer.observe({entryTypes: ['longtask']});
    ''');
  }
}

六:安全防护

6.1 多层安全防护

graph TD
    subgraph "安全防护体系"
        A[输入层防护] --> B[通信层防护]
        B --> C[执行层防护]
        C --> D[数据层防护]
    end
    
    subgraph "具体措施"
        A1[URL白名单验证]
        A2[输入参数过滤]
        
        B1[HTTPS强制]
        B2[消息签名]
        B3[防重放攻击]
        
        C1[JS沙盒隔离]
        C2[权限最小化]
        
        D1[数据加密]
        D2[本地存储安全]
    end
    
    A1 --> A
    A2 --> A
    B1 --> B
    B2 --> B
    B3 --> B
    C1 --> C
    C2 --> C
    D1 --> D
    D2 --> D

6.2 具体实现

class WebViewSecurity {
  // 验证URL白名单
  static final List<RegExp> _urlWhitelist = [
    RegExp(r'^https://api\.xxxx\.com/'),
    RegExp(r'^https://cdn\.xxxx\.com/'),
    RegExp(r'^https://sso\.xxxx\.com/'),
  ];
  
  static bool isUrlAllowed(String url) {
    return _urlWhitelist.any((pattern) => pattern.hasMatch(url));
  }
  
  // 验证消息签名
  static bool verifyMessageSignature(
    Map<String, dynamic> message,
    String signature,
  ) {
    // 1. 检查时间戳
    final timestamp = message['timestamp'];
    final now = DateTime.now().millisecondsSinceEpoch;
    if ((now - timestamp).abs() > 300000) { // 5分钟有效期
      return false;
    }
    
    // 2. 验证签名
    final secretKey = 'your_secret_key_here';
    final dataToSign = '${message['id']}:${timestamp}:$secretKey';
    final expectedSig = sha256.convert(utf8.encode(dataToSign)).toString();
    
    return expectedSig == signature;
  }
  
  // 防XSS注入
  static String sanitizeInput(String input) {
    // 移除危险标签和属性
    return input
        .replaceAll(RegExp(r'<script[^>]*>.*?</script>', caseSensitive: false), '')
        .replaceAll(RegExp(r'on\w+="[^"]*"', caseSensitive: false), '')
        .replaceAll(RegExp(r'javascript:', caseSensitive: false), '')
        .replaceAll(RegExp(r'data:', caseSensitive: false), '');
  }
}

6.3 Content Security Policy

Future<void> setupContentSecurityPolicy(WebViewController controller) async {
  await controller.runJavaScript('''
    // 添加CSP Meta标签
    const cspMeta = document.createElement('meta');
    cspMeta.httpEquiv = 'Content-Security-Policy';
    cspMeta.content = \`
      default-src 'self' https://api.xxxx.com;
      script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.xxxx.com;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https:;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.xxxx.com wss://ws.xxxx.com;
      frame-ancestors 'self';
      form-action 'self' https://sso.xxxx.com;
    \`;
    
    document.head.appendChild(cspMeta);
    
    // 禁用危险API
    Object.defineProperty(window, 'eval', {
      value: function() {
        console.warn('eval() is disabled for security reasons');
        return null;
      }
    });
    
    // 监控可疑行为
    const originalPostMessage = window.postMessage;
    window.postMessage = function(message, targetOrigin) {
      if (!targetOrigin || targetOrigin === '*') {
        console.warn('postMessage without targetOrigin is restricted');
        return;
      }
      return originalPostMessage.call(this, message, targetOrigin);
    };
  ''');
}

七:调试

7.1 集成调试工具

class WebViewDebugger {
  // 启用远程调试
  static void enableRemoteDebugging(WebViewController controller) {
    // Android: Chrome DevTools
    // iOS: Safari Web Inspector
    
    controller.runJavaScript('''
      console.log = function(...args) {
        // 重定向console到Flutter
        const message = args.map(arg => 
          typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
        ).join(' ');
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'console',
            level: 'log',
            message: message,
            timestamp: Date.now()
          }));
        }
        
        // 保留原始console功能
        originalConsoleLog.apply(console, args);
      };
      
      const originalConsoleLog = console.log;
    ''');
  }
}

7.2 监控错误与上报

class WebViewErrorMonitor {
  static final List<WebViewError> _errors = [];
  
  static void setupErrorMonitoring(WebViewController controller) {
    // 监控JS错误
    controller.runJavaScript('''
      window.addEventListener('error', function(event) {
        const errorData = {
          message: event.message,
          filename: event.filename,
          lineno: event.lineno,
          colno: event.colno,
          error: event.error?.toString(),
          stack: event.error?.stack,
          timestamp: Date.now(),
          url: window.location.href
        };
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'error',
            data: errorData
          }));
        }
      }, true);
      
      // 监控未处理的Promise拒绝
      window.addEventListener('unhandledrejection', function(event) {
        const errorData = {
          type: 'promise_rejection',
          reason: event.reason?.toString(),
          timestamp: Date.now()
        };
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'error',
            data: errorData
          }));
        }
      });
    ''');
  }
  
  // 上报error到服务端
  static Future<void> reportErrors() async {
    if (_errors.isEmpty) return;
    
    try {
      await http.post(
        Uri.parse('https://api.xxxx.com/error-report'),
        body: jsonEncode({
          'appVersion': '1.0.0',
          'platform': Platform.operatingSystem,
          'errors': _errors,
        }),
        headers: {'Content-Type': 'application/json'},
      );
      
      _errors.clear();
    } catch (e) {
      print('错误上报失败: $e');
    }
  }
}

八:以电商混合应用为例

8.1 项目架构

下面我们通过一个电商App的案例,把前面所有知识点串联起来:

lib/
├── main.dart
├── core/
│   ├── hybrid/           # 混合开发
│   │   ├── manager.dart     # 混合管理器
│   │   ├── bridge.dart      # 桥接文件
│   │   ├── router.dart      # 混合路由
│   │   └── security.dart    # 安全模块
│   └── di/               # 依赖注入
├── modules/
│   ├── product/          # 商品模块
│   │   ├── list_page.dart   # 原生列表
│   │   └── detail_page.dart # WebView详情
│   ├── cart/             # 购物车模块
│   └── order/            # 订单模块
└── shared/
    ├── widgets/          # 共享组件
    ├── utils/            # 工具类
    └── constants/        # 常量定义

8.2 实现商品详情页

class ProductDetailPage extends StatefulWidget {
  final String productId;
  
  const ProductDetailPage({Key? key, required this.productId}) 
      : super(key: key);
  
  @override
  _ProductDetailPageState createState() => _ProductDetailPageState();
}

class _ProductDetailPageState extends State<ProductDetailPage> {
  late WebViewController _controller;
  final ProductService _productService = ProductService();
  
  @override
  void initState() {
    super.initState();
    _initWebView();
    _prefetchProductData();
  }
  
  void _initWebView() {
    // 从WebView池获取或创建
    _controller = WebViewPool.getWebView(
      key: 'product_${widget.productId}',
      builder: () => _createWebViewController(),
    );
  }
  
  Future<WebViewController> _createWebViewController() async {
    final controller = WebViewController();
    
    // 获取用户信息和商品数据
    final userInfo = await UserService().getCurrentUser();
    final productData = await _productService.getProduct(widget.productId);
    
    // 含参URL
    final url = _buildProductUrl(productData, userInfo);
    
    await controller.loadRequest(Uri.parse(url));
    
    return controller;
  }
  
  String _buildProductUrl(Product product, User? user) {
    final params = {
      'product_id': product.id,
      'product_name': Uri.encodeComponent(product.name),
      'price': product.price.toString(),
      'user_id': user?.id ?? '',
      'user_token': user?.token ?? '',
      'platform': Platform.operatingSystem,
      'app_version': '1.0.0',
      'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
    };
    
    // 添加签名
    final signature = _generateSignature(params);
    params['sign'] = signature;
    
    final uri = Uri.parse('https://m.xxxx.com/product/detail')
        .replace(queryParameters: params);
    
    return uri.toString();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('商品详情'),
        actions: _buildAppBarActions(),
      ),
      body: Column(
        children: [
          // 顶部:商品简介
          _buildProductSummary(),
          
          // WebView详情部分
          Expanded(
            child: WebViewWidget(controller: _controller),
          ),
          
          // 底部:原生操作栏
          _buildBottomActionBar(),
        ],
      ),
    );
  }
  
  Widget _buildProductSummary() {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '商品名称',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 8),
          Row(
            children: [
              Text(
                '¥ 299.00',
                style: TextStyle(
                  fontSize: 24,
                  color: Colors.red,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(width: 8),
              Text(
                '¥ 399.00',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey,
                  decoration: TextDecoration.lineThrough,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  Widget _buildBottomActionBar() {
    return Container(
      height: 60,
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(top: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Row(
        children: [
          // 客服
          Expanded(
            child: TextButton.icon(
              onPressed: _contactCustomerService,
              icon: Icon(Icons.chat),
              label: Text('客服'),
            ),
          ),
          
          // 加入购物车
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _addToCart,
              icon: Icon(Icons.shopping_cart),
              label: Text('加入购物车'),
              style: ElevatedButton.styleFrom(
                primary: Colors.orange,
              ),
            ),
          ),
          
          // 立即购买
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _buyNow,
              icon: Icon(Icons.shopping_bag),
              label: Text('立即购买'),
              style: ElevatedButton.styleFrom(
                primary: Colors.red,
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  Future<void> _addToCart() async {
    // 通过桥接通知H5页面
    await _controller.runJavaScript('''
      if (window.addToCart) {
        window.addToCart();
      } else {
        // 调用Flutter原生方法
        window.FlutterBridge.postMessage(JSON.stringify({
          type: 'action',
          method: 'addToCart',
          data: {productId: '${widget.productId}'}
        }));
      }
    ''');
  }
}

8.3 适配H5页面

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>商品详情</title>
    <script>
        // Flutter桥接适配
        class FlutterAdapter {
            constructor() {
                this.callbacks = new Map();
                this.messageId = 0;
                this.setupBridge();
            }
            
            setupBridge() {
                // 注册Flutter调用方法
                window.addToCart = () => this.addToCart();
                window.buyNow = () => this.buyNow();
                window.getUserInfo = () => this.getUserInfo();
                
                // 初始化消息监听
                if (window.FlutterBridge) {
                    console.log('Flutter桥接已OK');
                }
            }
            
            // 添加购物车
            async addToCart() {
                const productId = this.getQueryParam('product_id');
                
                try {
                    // 通过桥接调用Flutter
                    const result = await this.callFlutter('addToCart', {
                        productId: productId,
                        quantity: 1
                    });
                    
                    if (result.success) {
                        this.showToast('添加成功');
                    } else {
                        this.showToast('添加失败: ' + result.message);
                    }
                } catch (error) {
                    console.error('添加购物车失败:', error);
                    this.showToast('网络异常,请重试');
                }
            }
            
            // 调用Flutter方法
            callFlutter(method, data) {
                return new Promise((resolve, reject) => {
                    const messageId = ++this.messageId;
                    
                    this.callbacks.set(messageId, { resolve, reject });
                    
                    // 设置超时
                    setTimeout(() => {
                        if (this.callbacks.has(messageId)) {
                            this.callbacks.delete(messageId);
                            reject(new Error('请求超时'));
                        }
                    }, 10000);
                    
                    // 发送消息
                    window.FlutterBridge.postMessage(JSON.stringify({
                        id: messageId.toString(),
                        type: 'request',
                        method: method,
                        data: data,
                        timestamp: Date.now()
                    }));
                });
            }
            
            // 接收Flutter消息
            onFlutterMessage(message) {
                try {
                    const data = JSON.parse(message);
                    
                    if (data.type === 'response' && data.id) {
                        const callback = this.callbacks.get(parseInt(data.id));
                        if (callback) {
                            this.callbacks.delete(parseInt(data.id));
                            
                            if (data.data.success) {
                                callback.resolve(data.data);
                            } else {
                                callback.reject(new Error(data.data.message));
                            }
                        }
                    } else if (data.type === 'event') {
                        // 处理Flutter发来的事件
                        this.handleEvent(data);
                    }
                } catch (error) {
                    console.error('处理Flutter消息失败:', error);
                }
            }
            
            getQueryParam(name) {
                const urlParams = new URLSearchParams(window.location.search);
                return urlParams.get(name);
            }
            
            showToast(message) {
                // 显示提示
                const toast = document.createElement('div');
                toast.textContent = message;
                toast.style.cssText = `
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(0,0,0,0.8);
                    color: white;
                    padding: 12px 24px;
                    border-radius: 8px;
                    z-index: 1000;
                `;
                document.body.appendChild(toast);
                
                setTimeout(() => {
                    document.body.removeChild(toast);
                }, 2000);
            }
        }
        
        // 页面初始化
        document.addEventListener('DOMContentLoaded', function() {
            const adapter = new FlutterAdapter();
            
            // 检测运行环境
            const isInApp = navigator.userAgent.includes('FlutterWebView');
            
            if (isInApp) {
                // App内特有逻辑
                document.body.classList.add('in-app');
                
                // 适配安全区域
                document.documentElement.style.setProperty(
                    '--safe-area-top', 
                    'env(safe-area-inset-top, 0px)'
                );
                document.documentElement.style.setProperty(
                    '--safe-area-bottom', 
                    'env(safe-area-inset-bottom, 0px)'
                );
                
                // 隐藏H5导航
                const h5Nav = document.querySelector('.h5-navigation');
                if (h5Nav) h5Nav.style.display = 'none';
            }
            
            // 加载商品数据
            loadProductData();
        });
        
        async function loadProductData() {
            const productId = new URLSearchParams(window.location.search)
                .get('product_id');
            
            if (!productId) return;
            
            try {
                const response = await fetch(
                    `https://api.xxxx.com/products/${productId}`
                );
                const product = await response.json();
                
                renderProduct(product);
            } catch (error) {
                console.error('加载商品失败:', error);
                showError('加载失败,请重试');
            }
        }
        
        function renderProduct(product) {
            // 渲染商品信息
            document.getElementById('product-title').textContent = product.name;
            document.getElementById('product-price').textContent = 
                ${product.price}`;
            document.getElementById('product-desc').innerHTML = 
                product.description;
            
            // 渲染图片
            const gallery = document.getElementById('product-gallery');
            product.images.forEach(img => {
                const imgEl = document.createElement('img');
                imgEl.src = img.url;
                imgEl.alt = product.name;
                gallery.appendChild(imgEl);
            });
        }
    </script>
</head>
<body>
    <div class="product-container">
        <h1 id="product-title"></h1>
        <div class="price" id="product-price"></div>
        <div class="gallery" id="product-gallery"></div>
        <div class="description" id="product-desc"></div>
    </div>
</body>
</html>

总结

至此Flutter混合开发与WebView相关知识点就全部介绍完了,牢记一下核心原则:

  • 优先性能:WebView预加载、内存管理、缓存
  • 安全第一:输入验证、通信加密、权限控制

避坑指南:

常见问题及解决方案:

  1. WebView白屏

  • 原因:内存不足或初始化问题

  • 解决:实现WebView复用,添加重试机制

  1. 通信延迟高

  • 原因:频繁小消息通信

  • 解决:批量处理,二进制协议

  1. 内存泄漏

  • 原因:未正确释放WebView

  • 解决:使用WeakReference,管理生命周期

  1. 跨平台差异

  • 原因:iOS/Android WebView实现不同
  • 解决:平台适配层,功能降级

结语

混合开发不是简单的炫技,而是在原生与web之间的性能、安全、体验、效率等方面寻找一个最佳的平衡点。技术只是手段,用户体验才是目的。

如果觉得本文对你有帮助,别忘了一键三连~~~,有任何问题或想法,欢迎在评论区交流讨论! 转载请注明出处!!!

Swift 疑难杂想

作者 Neo_Arsaka
2025年12月6日 16:50

@State, @StateObject, @Published

@State

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("点了 (count) 次")   // 2. 读值
            Button("+1") {
                count += 1           // 3. 改值 → 自动刷新界面
            }
        }
        .font(.largeTitle)
    }
}

@State 是 SwiftUI 里最常用的属性包装器之一。

注意事项

  • 只能用于 当前 View 内部 的私有可变状态。
  • @State 的值改变时,SwiftUI 会 自动重新计算 body,把最新数据画到屏幕上。

@StateObject

import SwiftUI
import Combine

// 1. 先写一个可观察的模型
class TimerModel: ObservableObject {
    @Published var seconds = 0        // 2. 发布变化
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { _ in
                self.seconds += 1
            }
    }
}

// 3. 视图里“创建并持有”这个模型
struct TimerView: View {
    @StateObject private var model = TimerModel()   // ← 关键:@StateObject
    
    var body: some View {
        Text("(model.seconds) 秒")
            .font(.largeTitle)
    }
}

@StateObject 也是属性包装器,专门用来 创建并持有 一个 ObservableObject 实例。

注意事项

  • 对象里的 @Published 属性一旦变化,所有用到它的视图自动刷新
  • 只有第一次初始化时才会真正创建;后面 SwiftUI 重绘视图时不会反复 new 出新对象。

@Published

@Published 不是给 View 用的属性包装器,而是 写在 ObservableObject 里的“广播器”只要这个属性值一变,立刻通知所有正在监听它的视图

注意事项

  • 只能用在 ObservableObject 协议 的类里。
  • 标记为 @Published 的属性,SwiftUI 会自动生成 objectWillChange 发布事件。
  • 视图那一端用 @StateObject@ObservedObject 拿到这个对象后,就能 实时响应 这些变化。

Q&A

Q:@State 只能用在集成了 View 的struct或者class 中?

A:@State 只能出现在 SwiftUI 的 View 结构体(或极少数自己实现 View 协议的类)里,而且必须是 当前视图私有的可变状态

Q:@state只能修饰一些基础类型,无法修饰复杂的引用类型等

A:@State 完全可以修饰“复杂”的引用类型(class、嵌套 struct、数组、字典、自定义模型……),只要它满足两点:

  • 类型遵守 Equatable(SwiftUI 靠它 diff,官方文档推荐,不遵守也能跑,但可能多刷)。
  • 你在 View 里 把整个实例换掉(值语义)——SwiftUI 只关心“旧值 vs 新值”有没有变化,不关心内部字段。

Q:就是说比如我定义了一个 @State var person = Person(name: "Xiaomi") ,然后当我更换引用对象比如,person = Person(name:"Sam")也会触发界面的刷新?

A:没错,会刷新——前提是你把“整颗对象”换掉

Q:那么如果我想让类的成员可视化到界面上,并实时显示最新的值,那么我就得遵守 ObservableObject 协议 ,并且使用 @Published 修饰对应的成员?

A:是的,想让“类的某个成员”一变化就自动刷新界面,必须同时满足:

  • 类遵守 ObservableObject 协议;
  • 那个成员用 @Published 修饰;
  • 视图通过 @StateObject(或 @ObservedObject)持有这个类实例。

避坑小贴士

  1. 不要init 里直接修改 @State / @StateObject 包装的值,SwiftUI 会忽略这次写入。
  2. 不要@StateObject 用在“不是真正拥有者”的视图里,否则可能出现“同一模型被重复创建”或“野指针”问题;子视图只观察时用 @ObservedObject
  3. 不要ObservableObject 里手动调用 objectWillChange.send(),除非你有特殊时机需求;@Published 已经帮你做了。
  4. 不要把值类型(struct)也加上 @Published,它照样工作,但会失去“共享引用”的意义,容易陷入“改了副本、界面没反应”的错觉。

界面背景色

Q&A

Q:最新的swift ui语法中,如何设置界面的背景色,并且忽略安全边界? Color.blue.ignoreSageAre() 这么写是不是不是最佳开发实践了?

A: 你写的 ignoreSageAre() 拼写错了,应该是
.ignoresSafeArea()(iOS 14+ 的名字,以前叫 .edgesIgnoringSafeArea(.all))。

另外,直接把 Color.blue 当根视图 虽然能跑,但属于“偷懒写法”——
一旦后面要加别的控件,就得再包 ZStack,不如一开始就写成“背景层 + 内容层”分离的形式,可读性、可维护性都更好。

推荐写法(iOS 14+ 官方风格):

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue                // 1. 背景层
                .ignoresSafeArea()    // 2. 忽略安全边界
            
            Text("Hello, SwiftUI")    // 3. 内容层
                .foregroundColor(.white)
        }
    }
}

Q:我记得还可以直接 ZStack{..}.background(.blue).ignore啥的,是不是也可以?

A:可以,但顺序得换一下:
background 只是给 ZStack 刷一层颜色,本身不会自动撑满屏幕,所以要把 .ignoresSafeArea() 贴在真正“能撑满”的那个视图(也就是 Color.blue)上,而不是贴在 ZStack 后面。

ZStack {
    Text("Hello, SwiftUI").foregroundColor(.white)
}
.background(.blue.ignoresSafeArea())   // iOS 15+ 语法糖

Q:和Color.blue.ignoresSafeArea() 这种方式比,那种方式更好一些?

A:两种写法在最终视觉效果上几乎一样:整屏蓝底、无视安全区。差异主要在可读性、扩展性、习惯上,而不是性能。

Color.blue.ignoresSafeArea() 是最佳实践。

两种写法在最终视觉效果上几乎一样:整屏蓝底、无视安全区。
差异主要在可读性、扩展性、习惯上,而不是性能。

  • 快速原型、单页极简界面:用 .background(Color.blue.ignoresSafeArea()) 完全没问题。
  • 正式项目、多人协作、后续会迭代:优先 ZStack 里单独放 Color,扩展、阅读都更舒服。

字体颜色

Q&A

Q:最新的swift ui 中 .fontColor 好像已经不推荐了?

A:在 SwiftUI 里并没有 .fontColor 这个修饰器,从 iOS 13 开始官方就统一用
.foregroundColor(_:) 或 iOS 15 起更通用的 .foregroundStyle(_:) 来给 Text、Image、Shape 等设置“前景色”。

Text("Hello")
    .foregroundColor(.red)      // iOS 13+ 专用颜色
// 或者
Text("Hello")
    .foregroundStyle(.red)      // iOS 15+ 接受任何 ShapeStyle(纯色、渐变、材质)

圆角

Q&A

Q:Text("CLICK HERE") .foregroundStyle(.white) .padding() .background(.black).cornerRadius(8) 这么设置圆角好像也不推荐了在新的swiftui中

A:cornerRadius(8) 并没有被废弃,代码现在依旧能编译、能跑;
但在 iOS 17 的 Apple Sample 和 WWDC 视频里,官方更推荐用 .clipShape(.rect(cornerRadius: 8)) (或者 .clipShape(RoundedRectangle(cornerRadius: 8)))来裁切,语义更清晰——“先画完再裁”,而不是“把边框圆角当成修饰器”。

// 1. 老写法(依旧可用)
Text("CLICK HERE")
    .foregroundStyle(.white)
    .padding()
    .background(.black)
    .cornerRadius(8)

// 2. iOS 17 推荐写法
Text("CLICK HERE")
    .foregroundStyle(.white)
    .padding()
    .background(.black)
    .clipShape(.rect(cornerRadius: 8))   // ← 语义:把整体裁成圆角矩形

豆包手机助手:无法直接查询银行卡余额,需用户授权及手动确认

2025年12月6日 16:50
36氪获悉,12月6日,豆包手机助手发布声明称,网传“豆包手机助手可以直接查询银行卡余额,并跳过认证读取用户信息”为谣言。豆包手机助手表示,其仅在用户的要求与授权下帮助用户查询银行卡余额,其中涉及敏感操作,需用户手动确认。此外,豆包手机助手在用户授权操作的过程中,并不存在系统权限可以直接跳过密码、人脸识别等类似认证环节,查询过程中仍需用户手动完成符合银行App要求的真人检测、密码输入等。

倒车灯故障,福特在美召回1195辆林肯领航员

2025年12月6日 16:50
12月6日,据美国国家公路交通安全管理局(NHTSA)披露,由于后灯条的外透镜或焊缝可能存在裂纹,导致水分积聚,进而引发倒车灯失效或闪烁故障,福特汽车公司正在召回部分2025款林肯领航员车型,共计1195辆。(界面)

吴清:中国证券业协会的新一届会长将由行业代表担任

2025年12月6日 16:39
12月6日,在中国证券业协会第八次会员大会上,证监会主席吴清围绕证券行业高质量发展发表致辞。吴清指出,中国证券业协会的新一届会长将由行业代表担任,这对协会制度提出新的要求,无论是专职还是兼职,都要和舟共济,不断增强全体会员的认同感、向心力和参与度,切实提升共建、共治、共享质效。(证券时报)

微软股东批准CEO纳德拉9650万美元薪酬包

2025年12月6日 16:37
微软当地时间12月5日投票通过了所有公司支持的提案,包括选举12名董事及2026年股票计划。据报道,首席执行官萨蒂亚·纳德拉的薪酬从去年的7910万美元提升至9650万美元,沃尔玛首席财务官约翰·戴维·雷尼出任微软董事会的提名也获得批准。(界面)
❌
❌