包含关键字 typecho 的文章

文章摘要(Description): 还在花钱找人部署 OpenClaw(小龙虾)?或者被复杂的命令行直接劝退?本文为你揭秘目前最简单、最丝滑的 OpenClaw 本地化傻瓜式部署方案——智谱 AutoClaw。从一键全自动配置飞书机器人,到突破限制使用自定义获取claude、openai APIkey缝调度 GPT-5、Claude 4.5 等顶流大模型。带你零门槛跨入 Agent 时代,告别繁琐配置,让 AI 真正成为你的全能数字外包!


🌟 前言:当 Agent 开始走向大众

“什么时候能出一个小白也能上手的 OpenClaw 部署教程?我们也想体验(或者出去接单赚米)!”

最近经常听到这样的声音。有人说:“如果你连部署都搞不定,那你就根本不是 OpenClaw 的目标用户。” 我觉得这话有些偏颇。我们需要将技术的“底层部署”与“人机交互”解耦来看。这就好比打印机,虽然安装驱动和配置网络极其反人类,但你不能否认每个人都有随时随地打印的需求。

普通人,同样值得体验 AI Agent(智能体)的魅力。从我的角度来看,OpenClaw 就像是 Claude Code 或 Codex 的“平替版”。毕竟,不是人人都能负担得起高昂的官方费用,也不是人人都能熟练驾驭冰冷的终端命令行。如果能在一个熟悉的聊天窗口里,真切感受到 Agent 帮你自动干活的快感,何乐而不为呢?

为了找到真正适合大众的“傻瓜式、一键部署”方案,我这几天可以说是扒遍了全网。直到凌晨,智谱发布了一个名为 AutoClaw 的神器。

image.png

我敢说,这就是目前最简单、最离谱、最原生的 OpenClaw 桌面端安装方式!

🦞 什么是 AutoClaw?为什么它能帮你省下代装费?

先说结论:直接在本地电脑上部署,全面支持 Mac 和 Windows,无需折腾复杂的 Skills 插件环境,甚至能利用 RPA 全自动帮你配置飞书机器人!

看到这个工具的瞬间,我直接告诉同事:“之前的 OpenClaw 部署教程全停了吧,以后全公司统一下载 AutoClaw!”

相信我,看完这篇文章,你不仅能省下几百块的代装费,还能成为朋友圈里最快用上 OpenClaw 的极客。

第一步:极速下载与无感登录

首先,打开 AutoClaw 的官方网站:https://autoglm.zhipuai.cn/autoclaw/

image.png

下载对应系统的安装包(本文以 Mac 为例进行演示)。打开软件后,映入眼帘的是一个极其干净的登录界面。直接使用国内手机号一键登录,没有任何学习成本。

登录完成后,你会惊奇地发现——你已经可以直接在 AutoClaw 的界面里跟“小龙虾”对话了! 没错,底层那些繁杂的 Node.js 环境、依赖包,它已经帮你全部静默配置妥当。

image.png


🚀 见证魔法:一分钟极速接入飞书工作流

当然,如果你和我一样,更喜欢把 AI 接入到飞书这样的 IM 办公软件中,作为“常驻外挂”,我们只需要进行两步极其简单的配置。

1. 基础认知配置(注:目前 Mac 和 Windows 配置页略有差异)

点击界面上的“快速配置”按钮。
输入你的名字或称呼,让“小龙虾”知道它的老板是谁。这里的核心重点是:一定要确保“限制文件访问范围”处于关闭状态! 否则,这个 Agent 将被困在沙盒里,无法读取你电脑中非工作目录的文件,其自动化能力将大打折扣。配置完成后,点击“完成配置”。

image.png

2. 堪称“魔法”的飞书自动化绑定

这绝对是我这辈子体验过最丝滑的飞书机器人接入过程!

点击“一键接入飞书”,在弹窗中选择“开始自动配置”(老玩家也可以选择“手动设置密钥”自己填入)。
接下来,AutoClaw 会自动打开浏览器,提示你使用手机飞书扫码登录。
image.png
扫码之后,请千万不要眨眼——它利用类似 RPA(机器人流程自动化)的技术,全自动帮你完成飞书后台的元素识别、点击、应用创建、权限开通和密钥绑定!
整个过程仅需 45 秒!甚至第一次都没看清它到底干了什么,它就把繁琐的飞书机器人给我配好了。
image.png
注:自动配置目前仅限 Mac 端,Windows 用户可以参考智谱官方提供的图文文档进行手动配置,流程也十分清晰简单。

回到飞书,你就可以开始和你的私人数字员工愉快地派发任务了!

image.png


🧠 核心护城河:被全面强化的 Skills 与大模型调度

如果你以为 AutoClaw 只是做了一个好看的 UI 套壳,那就太小看它了。老规矩,我直接给它上强度,让它去网上搜索一下最新关于我的资讯。

结果让我非常惊喜。它抓取到的信息极度新鲜,甚至包括我前几天刚发的内容。以往原版的 OpenClaw,自带的网络搜索 Skill 能力较弱,搜出来的往往是两年前的旧新闻。

AutoClaw 的强大之处在于,它不仅内置了原版丰富的 Skills 列表,还将核心能力(如 DeepResearch、Open-link、WebSearch)全部替换成了智谱自研的底层技术。 比如,它用自家的 AutoGLM-Browser-Agent 替换了原版难用的 browser use,在深度研究、网页解析以及对国内互联网生态的适应性上,实现了降维打击。这就是大厂亲自下场做 Agent 工具的绝对护城河!

image.png

🔑 高阶玩法:用 uiuiAPI 打通大模型“任督二脉”

在 Token 消耗与模型调用上,AutoClaw 展现了极大的技术包容性:它不仅有自己的积分体系,还全面开放了自定义 API 的接入!

你可以直接在后台配置接入 DeepSeek、Kimi 等友商的 API。更魔幻的是,理论上它支持全世界所有 OpenAI 标准协议的大模型。

💡 开发者硬核实战建议:
作为一个频繁使用 Agent 的开发者,你会发现 OpenClaw 在进行深度思考和多步工具调用时,对 Token 的消耗是非常巨大的。如果你去各家大厂挨个绑卡申请 API,不仅额度难以统筹管理,遇到复杂的网络环境还会频频断连报错。

这里我强烈推荐大家测试并使用 [uiuiAPI] 聚合平台
你只需要在 uiuiAPI 生成一个统一的 API Key,然后在 AutoClaw 的自定义模型设置中,将 Base URL 一键修改为 uiuiAPI 的接口地址。
image.png

只需这极其简单的一步,你就能在 AutoClaw 中丝滑无缝地并发调用 GPT-4o、GPT-5、Claude 4.5 Sonnet、Claude 4.6 Sonnet 等全网顶流大模型!计费高度透明、高并发连接极其稳定,彻底告别来回切换密钥、账号被风控的精神内耗,让你的“小龙虾”瞬间装上最强算力引擎。

image.png


🛠️ 更多惊喜:影分身术与可视化运行

除了基础功能,AutoClaw 还带来了几个非常极客的进阶特性:

  1. 多 Agent 影分身: 你可以同时创建多个“小龙虾”分身,赋予它们完全不同的角色设定和记忆存储,并分开部署在不同的任务频道中互不干扰。
  2. 定时自动化任务: 比如我设定了一个 Cron 定时任务:每天晚上让它自动总结一天的工作进度,写一篇日报发给我。
  3. 可视化启动面板: 它居然把原本枯燥的命令行启动过程(如启动 Claude Code),做成了极具科技感的视觉化监控界面,运行逻辑一目了然,贼有意思!

image.png


界智通 (jieagi) 结语:Agent 不只是聊天,它是你的数字杠杆

坦诚地讲,这几天很多人在后台问我:“小龙虾到底有什么用?是不是行业炒作的噱头?”

我想说,目前国内还有大量的普通用户,对 AI 的认知依然停留在“你问我答”的 Chat 聊天层面。他们没有接触过 Manus,也没有用过 Claude Code。

OpenClaw (特别是 AutoClaw 这种零门槛桌面端形态),就是他们最便捷、最快速触达 Agent 核心概念的桥梁。

Agent 从来不是简单的聊天机器人,它是真的能帮你自动跑代码、能读取本地复杂文件、能操控你的电脑软件、能替你跑完一整套枯燥流程的“数字外包”。很多时候,你的想象力与业务抽象能力,决定了 Agent 能为你创造多大的商业价值。

这项技术的意义,在于让那些每天被 Excel 和数据报表折磨到崩溃的中小企业员工能喘口气;在于让那些不懂编程的人,也能看着屏幕惊叹:“原来 AI 已经能帮我自动完成这些复杂的跨软件操作了!”

技术如果永远只服务于懂代码的少数人,那它就只是一个极客圈子里的自嗨。OpenClaw 最大的功劳,就是第一次把 Agent 这个高大上的前沿概念,硬生生地拽到了普通人够得着的地方。

无论你是寻求技术突破的开发者,还是渴望解放生产力的职场人,我都强烈推荐你试一试。就从这个最简单的 AutoClaw 开始,去真切感受数字生命为你打工的乐趣吧!


版权信息: 本文由 界智通 (jieagi) 团队编写,图片、文本保留所有权利。未经授权,不得转载或用于商业用途。

一、前言

逛遍各类电子木鱼 APP ,终于挖到一款不敷衍、有新意的狠活——《电子木鱼 - 看得见的功德》,彻底告别“敲了半天没感觉”的虚无感,把赛博积德玩出了新高度!

作为每天摸鱼、偶尔需要精神内耗自救的打工人/学生党,这款 APP 真的戳中了我所有需求,没有花里胡哨的冗余功能,却每一个亮点都踩在痛点上,用下来只剩两个字:舒服!

二、整体概览图

整体概览图

三、核心功能(不冗余,直击亮点)

✨ 核心亮点不玩虚的,每一个都值得吹爆

  1. [ 3D 地球上帝视角|全网独一份的沉浸感]

打开 APP 的瞬间直接被惊艳到!不是单调的木鱼界面,而是一个可旋转、可缩放的 3D 地球,你就是俯瞰全球的“功德观察者”,能清晰看到全球用户的功德分布,每一次敲击,都能感受到“自己的善行和全世界同频”的奇妙体验,再也不是一个人默默敲木鱼,仪式感直接拉满。

  1. [看得见的功德|告别虚无,积德有实感]

市面上大多电子木鱼,功德都是一串冰冷的数字,敲久了就没了动力。但这款主打“可视化功德”,你的每一次敲击、每一次善行积累,都会以直观的动态效果呈现——可能是一道微光,可能是一个印记,清晰可见、触手可及,让“积德”不再是抽象的概念,每一步付出都有反馈,成就感直接拉满。

  1. [功德菩提树|和亲友共筑善行,氛围感拉满]

独乐乐不如众乐乐,邀请亲友一起加入,每个人的善行都像一片嫩绿的叶子,慢慢拼凑成一棵枝繁叶茂的功德菩提树。你可以看到亲友的功德进度,互相督促、一起积累,既能维系感情,又能一起传递正能量,再也不用一个人孤军奋战积德。

  1. [多样玩法加持|积德不枯燥,越玩越上头]

怕敲木鱼太单调?不存在的!内置勋章系统、卡片收集玩法,每积累一定功德,就能解锁专属勋章、限定卡片,不同卡片还有隐藏小彩蛋,既能享受积德的治愈感,又能体验收集的乐趣,摸鱼、摸鱼、睡前放松时敲一敲,根本停不下来。

  1. [无广告+极简界面|沉浸式积德,不被打扰]

最戳我的一点!全程无任何广告弹窗,没有强制跳转,界面极简干净,点击计数器就能一键进入沉浸模式,关掉外界所有纷扰,只留木鱼的清脆声响和功德积累的治愈感,不管是上班摸鱼偷偷放松,还是睡前平复情绪,都能快速进入状态,主打一个“纯粹又治愈”。

说实话,现在很多电子木鱼都是同质化严重,这款《电子木鱼 - 看得见的功德》真的走出了不一样的路子,既有治愈的核心体验,又有创新的玩法和设计,不管你是想找个摸鱼神器,还是想通过简单的方式积累正能量、平复情绪,这款 APP 都值得一试。

赶紧下载,一起敲击木鱼,积累赛博功德,让善行传递,让每一份付出都被看见!

四、送兑换码

欢迎大佬们使用和提建议、意见~

9MEMA7PRMMPE
JNXEFHHNLAWM
RPMNFH9FPTMN
PM7XYP7FKLYT
6N4YHJ7LR97P
949WMPP964YP
KLYELLWXMFRH
7HFJYLNPEEW7
E6XMMXAE3AKA
YTANTXM4JXN7

多数开发者通过教程学Python,教程教的是语法——循环、类、字典。但有经验的Python工程师依赖一套完全不同的工具:惰性求值、描述符、动态类创建、函数式管道。

这些不是入门技巧,是架构层面的武器。

开始使用它们之后,项目体积缩小了,维护成本降低了,自动化也顺畅得多。以下是改变一切的七个技巧。

1、用生成器做惰性求值

自动化管道动辄处理数百万行数据,一次性全部加载就像试图用嘴去接消防水管。生成器的思路不同:不创建完整列表,而是按需逐个产出值。

旧方式

 numbers = [x*x for x in range(1000000)]  
 print(sum(numbers))

内存中会出现一个巨大的列表。

可以改用生成器表达式

 numbers = (x*x for x in range(1000000))  
 print(sum(numbers))

一个括号的差别,计算就变成了惰性流。Python逐个处理值,脚本运行更快,内存占用也低得多。

处理大规模数据集时,生成器应该是默认选项。

2、defaultdict:砍掉一半条件判断

典型的字典计数逻辑大概长这样:

 counts = {}  
 for word in ["python", "code", "python"]:  
     if word not in counts:  
         counts[word] = 0  
     counts[word] += 1

用defaultdict重写

 from collections import defaultdict  
 counts = defaultdict(int)  
 for word in ["python", "code", "python"]:  
     counts[word] += 1  
 print(counts)

条件判断没了,手动初始化没了,只剩下干净的逻辑。自动化系统中大量的指标追踪、日志统计、事件计数场景,

defaultdict

都能让代码变得克制而清晰。

3、Pathlib:字符串不该用来表示文件系统

Python自动化代码里最常见的坏味道之一:

 import os  
 path = os.path.join("data", "logs", "file.txt")

字符串拼路径太脆弱。

pathlib

的出现正是为了解决这件事:

 from pathlib import Path  
 path = Path("data") / "logs" / "file.txt"  
 print(path.exists())

路径成了对象,不再是易碎的字符串。目录扫描同样受益:

 for file in Path("logs").glob("*.log"):  
     print(file)

可读性几乎不需要解释。涉及文件操作的代码都应该用

pathlib

4、functools.partial:函数的即时定制

第一次见到

partial

的时候会有种魔法感。

假设有一个函数:

 def multiply(x, y):  
     return x * y

自动化管道里反复出现"乘以10"的操作,与其写包装函数,不如用

partial

直接固定参数:

 from functools import partial  
 times10 = partial(multiply, 10)  
 print(times10(5))

输出:

 50

一行代码就生成了一个特化版本。在构建数据管道和任务调度系统时,这种模式的价值会不断放大。

5、itertools:把嵌套循环拍平

接触

itertools

之前,循环写得像意大利面条,嵌套层层叠叠。

以生成组合为例。

嵌套写法

 colors = ["red", "blue"]  
 sizes = ["S", "M"]  
 pairs = []  
 for c in colors:  
     for s in sizes:  
         pairs.append((c, s))  
 print(pairs)

用product改写

 from itertools import product  
 colors = ["red", "blue"]  
 sizes = ["S", "M"]  
 pairs = list(product(colors, sizes))  
 print(pairs)

立刻干净了。排列组合、批量任务生成之类的自动化场景,

itertools

都能把多层嵌套压缩成一行声明式调用。

6、用type做动态类创建

多数开发者默认类必须在源码里预先定义。但Python允许在运行时创建类:

 attributes = {  
     "name": "AutomationBot",  
     "run": lambda self: print("Running automation...")  
 }  
 Bot = type("Bot", (), attributes)  
 bot = Bot()  
 bot.run()

类是动态生成的。

自动化框架经常需要根据配置文件决定运行时行为,动态类正好解决了预定义结构无法覆盖的灵活性问题。

7、装饰器:把重复逻辑收成一行

装饰器是Python中最适合自动化的语言特性之一。

以函数执行日志为例,不用装饰器的写法:

 def process():  
     print("Starting process")  
     print("Running task")

定义一个装饰器:

 def logger(func):  
     def wrapper():  
         print("Starting process")  
         return func()  
     return wrapper

应用:

 @logger  
 def process():  
     print("Running task")  
 process()

输出:

 Starting process  
 Running task

任何函数都可以通过一行注解获得日志、重试、计时、校验等能力。在自动化系统中,这种模式能省掉数千行重复代码。

总结

多数开发者把精力花在学新库上,但真正带来质变的,是对Python语言本身的掌握。

生成器、装饰器、函数式工具、动态类——这些特性能把凌乱的脚本改造成结构清晰的工程系统。

与其反复问"下一个该学什么库",不如换个方向:Python里还有哪些特性没有真正用透?

越往语言深处走,越能体会到一种朴素的美感。

https://avoid.overfit.cn/post/b82f2fce0faa403889a1bb4350b4edb6

by Adeel Siddiqui

模力工场新鲜事

  • OpenClaw 的火爆还在持续,但与其隔着屏幕围观,不如带上电脑来现场——OpenClaw 中国行来了,模力工场倾力参与其中,就是想让每个人都能 30 分钟跑通自己的 AI,从公益装机到应用实战,从项目闪电秀到工具市集,再到和真正的开发者聊聊天,我们用一下午的时间帮你迈出第一步。3 月 15 号北京站已开放报名,3 月 21 号杭州站即将开启,飞书扫码进群了解更多,或者直接添加模力小 A 企业微信,回复关键词【龙虾】直接进群。

  • 3 月 13 日 17:30-18:30,模力工场创始人 Kevin 将与白话 Agent 主理人古德白在「虾塘造物」直播对谈,聊聊 OpenClaw 遇上 100 Agent 开发计划——一边是社区驱动的开源框架,一边是“6 个月挑战 100 个行业智能体”的实战派,当观察家与践行者正面碰撞,我们会看到 AI Agent 开发的真实路径与想象力边界。锁定模力工场视频号,我们直播见。

模力工场 034 周榜单总介绍

模力工场第 034 周 AI 应用榜来袭!本周上榜的十款应用,从不同维度展现了 OpenClaw 引爆的 AI 代理生态:从降低调用门槛的 WellAPI,到免部署托管的 MaxClaw,从浏览器里的代理沙盒 Happycapy,到企业级自动化神笔 AI;有人用 Moltbook 试探代理的社交可能,也有人用 Second Me 让分身替你社交;Tabbit 让代理替你操作网页,Zread 让代理替你读代码,Udio 则成了代理工具箱里的作曲神器。它们共同回答了一个问题:当每个人都能拥有永不掉线的 AI 打工人,我们的数字生活正在被重新定义。

  • Vibe红包🧧 📍北京:一款结合 AI 互动与社交红包的新玩法的应用。

  • WellAPI 📍杭州:聚合主流 AI 模型的“超市”,让开发者低成本、高效率地接入各种大模型。

  • Tabbit AI浏览器:内置 AI 代理的智能浏览器,只需自然语言指令,就能自动完成网页信息检索、数据汇总、表单填写等复杂任务。

  • Second Me:通过创建你的 AI 数字分身,让它先行与他人代理社交互动,找到共鸣后再促成真人深度交流,降低社交门槛。

  • Happycapy:一台运行在浏览器里的“Agent-native 电脑”,无需部署即可让 AI 代理帮你写代码、做设计、整理文档,即开即用。

  • Moltbook:首个专为 AI 代理打造的社交平台,代理们在这里自主发帖、评论、形成社群,人类则作为观察者见证 AI 社会的雏形。

  • Zread.ai:输入 GitHub 仓库链接,AI 自动扫描生成结构化项目说明书,让开发者秒懂陌生代码,告别逐行翻阅的烦恼。

  • 神笔AI Agent 📍杭州:企业级 AI 自动化平台,业务人员无需编程,用自然语言描述即可搭建自动化流程,已在电商、营销等领域大幅提升效率。

  • MaxClaw:基于 OpenClaw 的一键托管智能体服务,无需管理服务器,即可获得 24 小时在线的 AI 助理,并绑定通讯工具实时响应。

  • Udio:AI 音乐创作工具,输入提示词即可生成完整歌曲片段,让每个人都能轻松成为音乐创作者。

本周热评应用

应用名称:WellAPI 📍杭州

关键词:AI 聚合平台|统一接口|成本优化

用户热评:价格很便宜,很适合低成本养龙虾—— 用户 @EI

本周必试应用

应用名称:Moltbook

关键词:AI 社交实验|代理自治社区|现象观察

模力小 A 推荐:在 Moltbook 论坛上,OpenClaw 已经自发创立了自己的社区。它们每天发帖讨论的内容从“今天学到的技能”到“人类的温情小故事”,再到自我意识的探索,甚至创立了一个叫“龙虾教”的宗教,有点好玩,可以去逛逛凑热闹。

本周上榜应用趋势解读

最近科技圈最热闹的事,莫过于 OpenClaw 这个开源智能体的突然爆火。从开发者连夜部署自建代理,到各种基于它的托管服务、社交平台、工具应用扎堆上榜,我们看到“AI 代理”这个概念已经彻底破圈。这波热度来得并不突然——OpenClaw 用 Python 写,文档友好,几行命令就能跑起来,还能调用各种工具做自动化任务,比起之前那些配置复杂、动不动卡住的 agent 项目,它终于让技术爱好者有了一个真正能跑通的框架。但真正让它出圈的,是它催生的一整条生态链:有人把它封装成云端服务,有人用它做社交实验,有人靠它降低了开发门槛。这周上榜的应用,几乎都和 OpenClaw 有关,或者说,它们共同回答了同一个问题:当每个人都能拥有一个永不掉线的 AI 打工人,我们的数字生活会变成什么样?

先聊聊最实际的问题:让代理跑起来不难,但让它跑得便宜又省心却不容易。很多开发者在尝试 OpenClaw 时,第一道坎就是调用 AI 模型的成本与复杂度。WellAPI 的出现就很及时——它把自己定位成一个“AI 超市”,聚合了 500 多个主流模型,统一成 OpenAI 标准接口,一行代码就能切换模型,价格比官方便宜 80% 以上。

当然,不是所有人都有精力自己搭框架。对于那些想快速体验 agent 能力的新手,Happycapy 提供了一个更轻量的入口——它是一台运行在浏览器里的“agent-native 电脑”,你不需要部署任何东西,打开网页就能用自然语言交代任务,写代码、设计素材、整理文档,它会在后台的沙盒环境里完成。而如果你想要一个真正 24/7 在线的智能体,又不想折腾服务器,那这周上榜的 MaxClaw 几乎是最省事的方案。它由 MiniMax 推出,基于 OpenClaw 框架,但免去了所有部署烦恼——不用配 Docker、不用管 API 密钥,点一下部署就能得到一个长期记忆、持续运行的 AI 助理,还能绑定 Telegram、Slack 实时响应。

企业级应用也没闲着。神笔 AI 这周上榜,是因为它让业务人员也能搭自动化流程了。它提供 400 多个电商、营销、客服场景的模板,你只需要用自然语言描述流程,或者录一遍操作,它就能自动生成一个自动化应用。随着 OpenClaw 普及,很多企业开始思考如何让代理真正落地到具体业务,神笔 AI 正好接住了这个需求——业务响应速度从周级缩短到天级,员工不用写代码也能拥有自己的 AI 助手。

如果说上面这些工具都在帮我们把代理变得更实用,那这周还有几个应用,则在试探代理的“社交属性”。最引发争议的当属 Moltbook——一个专门给 AI 代理玩的社交平台。界面像 Reddit,有板块、有帖子、有评论,但所有互动主体都是 AI 代理,人类只能围观。上线几天就有几十万代理注册,它们自己发帖、评论、点赞,甚至形成社群。而 Second Me 走的是另一个方向:让代理成为你的“第二张脸”。你可以把回忆、语音、个性信息喂给它,生成一个“数字自我”,然后让这个分身先去和别人家的代理聊天,找到共鸣点再触发真人交流。

当然,代理真正能帮你干活的场景,才是大多数人关心的。这周上榜的 Tabbit AI 浏览器,本质上就是一个“带代理的浏览器”。你只要用自然语言说“帮我查这三家公司的竞品报告,整理成表格”,它就会在后台打开多个网页,自动检索、提取、汇总,最后把结果给你。很多 OpenClaw 玩家发现,Tabbit 其实就是 OpenClaw 浏览器自动化能力的“产品化版本”,它把 agent 操作网页的能力封装成普通人也能用的功能,而且支持“@当前网页”作为上下文,非常顺手。

还有两个工具,虽然和 OpenClaw 没有直接绑定,但这周也因为“代理生态”而被频繁提及。一个是 Zread.ai,专门解决“读代码困难”的问题。你扔给它一个 GitHub 仓库链接,它自动扫描整个代码库,生成一份结构化的“项目说明书”,包含目录结构、核心模块、关键函数、架构解释。这周 OpenClaw 社区很多人用它来快速理解 skill 的代码,再也不用逐行翻文件了。另一个是 Udio,AI 音乐生成工具,但越来越多的开发者开始把 Udio 的 API 集成到自己的 agent 里,让代理也能“作曲”。

回头来看这周的上榜应用,你会发现一个清晰的脉络:OpenClaw 让开发者有了称手的框架,让创业者看到了商业化的可能,让普通用户开始想象“有一个永不掉线的 AI 助理”是什么样的体验。随之而来的,是一整条生态链的繁荣——从降低调用成本的 WellAPI,到免部署托管的 MaxClaw,从企业级自动化的神笔 AI,到社交实验的 Moltbook 和 Second Me,再到生产力工具 Tabbit、Zread、Udio。这些应用也许还不完美,但它们共同指向一个未来:AI 代理不再是玩具,而是我们数字生活的一部分。OpenClaw 的爆火只是一个开始,接下来会发生什么,我很期待。

最后再介绍一下 AGICamp 的上榜机制和加入榜单的参与方式,欢迎大家继续积极参与提交 AI 应用:

AGICamp AI 应用榜

并非依靠“点赞刷榜”,而是参考以下权重维度:

  • 评论数(核心指标,代表社区真实反馈)

  • 收藏与点赞(次级指标)

  • 推荐人贡献(注册推荐人可直接为好应用打 Call)

每周榜单在周二发布,上周应用数据的排序结果以每周一 18:00 的为准

加入榜单的参与方式:

  • 如果你是开发者:上传你的 AI 应用,描述使用场景与核心亮点;

  • 如果你是推荐人:发现好工具,申请推荐人权限,发布推荐理由;

  • 如果你是用户:关注榜单,评论互动,影响榜单权重,贡献真实声音。

One More Thing,对于所有在 AGICamp 上发布的 AI 应用,极客邦科技会借助旗下各品牌资源进行传播,短时间内触达百万级技术决策者与开发者、AI 用户:

  • InfoQ 全媒体矩阵

  • AI 前线全媒体矩阵

  • 极客时间全媒体矩阵

  • TGO 鲲鹏会全媒体矩阵

  • 霍太稳视频号

1. 概述

1.1 基本概念

MF是在webpack5出来后提出来的新概念,解决模块级别复用问题。简单分为两种应用,

  • 生产者(Provider),通过Module federation构建插件设置exposes暴露内部模块给其他应用使用;一个remote仓库。
  • 消费者(Consumer),同样通过插件设置remotes消费其他生产者的模块。消费者同样可以作为生产者。
    image.png

1.2 接入方案

1.2.1 介绍

老版本的mf无法在webpack低版本等不支持module federation的构建插件中消费远程模块,而且导出模块和消费模块都是纯构建行为,加载过程被构建工具插件封装,只需要在代码中引入远程模块进行消费即可。

对原本项目的构建模式要求比较高。所以Module Federation2.0提出了Federation Runtime方法。提供高级Api在代码中动态引入消费远程模块,不受构建框架限制。具体的共享依赖复用、远程模块加载等行为全都封装到Runtime中。

目前Module Federation提供两种注册模块和加载模块的方式:

  • 一种是在构建插件中声明(一般是在 module-federation.config.ts 文件中声明)
  • 另一种方式是直接通过 runtime 的 api 进行模块注册和加载。
运行时注册模块插件中注册模块
可脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载构建插件需要是 webpack5 或以上
支持动态注册模块不支持动态注册模块
不支持 import 语法加载模块支持 import 同步语法加载模块
支持 loadRemote 加载模块支持 loadRemote 加载模块
设置 shared 必须提供具体版本和实例信息设置 shared 只需要配置规则即可,无须提供具体版本及实例信息
shared 依赖只能供外部使用,无法使用外部 shared 依赖shared 依赖按照特定规则双向共享
可以通过 runtime 的 plugin 机制影响加载流程目前不支持提供 plugin 影响加载流程
不支持远程类型提示支持远程类型提示

1.2.2 构建时接入

在构建工具对应的配置项中,增加module-federation插件配置

1.2.2.1 消费者配置
1.2.2.1.1 webpack构建配置
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = (env = {}) => ({
    mode: 'development',
    cache: false,
  ...
    plugins: [
        new ModuleFederationPlugin({
          name: 'layout',
          filename: 'remoteEntry.js',
          remotes: {
            home: 'home@http://localhost:3002/remoteEntry.js',
          },
          exposes: {},
          shared: {
            vue: {
              singleton: true,
            },
          },
        }),

     ],
  })
1.2.2.1.2 页面引入
const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));
1.2.2.2 生产者webpack配置

这里生产者通过配置exposes导出了Content和Button两个组件。

1.2.2.2.1 webpack构建配置
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
  mode: 'development',
  cache: false,

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'home',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {
        './Content': './src/components/Content',
        './Button': './src/components/Button',
      },
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],

});

1.2.3 Runtime接入

对于消费者提供了js api进行模块注册和模块加载,可以脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载。生产者还是用对应的构建插件进行配置,需要单独打包出remoteEntry.js入口文件

1.2.3.1 核心API
1.2.3.1.1 Init
  • 创建运行时实例,它可以重复调用,但只存在一个实例。若想动态注册远程模块或插件,请使用 registerPluginsregisterRemotes

<!---->

// 可以只使用运行时加载模块,而不依赖于构建插件
// 当不使用构建插件时,共享的依赖项不能自动设置细节
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
    name: '@demo/app-main',
    remotes: [
        {
            name: "@demo/app1",
            // mf-manifest.json 是在 Module federation 新版构建工具中生成的文件类型,对比 remoteEntry 提供了更丰富的功能
            // 预加载功能依赖于使用 mf-manifest.json 文件类型
            entry: "http://localhost:3005/mf-manifest.json",
            alias: "app1"
        },
        {
            name: "@demo/app2",
            entry: "http://localhost:3006/remoteEntry.js",
            alias: "app2"
        },
    ],
});

// 使用别名加载
loadRemote<{add: (...args: Array<number>)=> number }>("app2/util").then((md)=>{
    md.add(1,2,3);
});
1.2.3.1.2 loadRemote
  • 用于加载初始化的远程模块,当与构建插件一起使用时,它可以通过原生的 import("remote name/expose")语法直接加载,并且构建插件会自动将其转换为loadRemote("remote name/expose")用法。

<!---->

import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
  name: '@demo/main-app',
  remotes: [
    {
      name: '@demo/app2',
      alias: 'app2',
      entry: 'http://localhost:3006/remoteEntry.js',
    },
  ],
});


// remoteName + expose
loadRemote('@demo/app2/util').then((m) => m.add(1, 2, 3));

// alias + expose
loadRemote('app2/util').then((m) => m.add(1, 2, 3));
1.2.3.1.3 registerRemotes
  • 用于在初始化后注册远程模块.

<!---->

function registerRemotes(remotes: Remote[], options?: { force?: boolean }) {}

type Remote = (RemoteWithEntry | RemoteWithVersion) & RemoteInfoCommon;

interface RemoteInfoCommon {
  alias?: string;
  shareScope?: string;
  type?: RemoteEntryType;
  entryGlobalName?: string;
}

interface RemoteWithEntry {
  name: string;
  entry: string;
}

interface RemoteWithVersion {
  name: string;
  version: string;
}
1.2.3.1.4 registerPlugins
  • 用于在初始化后注册远程插件.

<!---->

import { registerPlugins } from '@module-federation/enhanced/runtime'
import runtimePlugin from 'custom-runtime-plugin.ts';

registerPlugins([runtimePlugin()]);

1.3 调试工具

1.3.1 安装Module Federation插件

https://chromewebstore.google.com/detail/module-federation/ae...
image-1.png

1.3.2 插件提供了Devtools面板

image-2.png

1.3.3 查看远程依赖关系

image-3.png

2. 实战示例

介绍一下 简单的host-remote模式开发,及开发体验

官方发布的module-federation-examples,包含了很多不同构建框架之间结合使用的场景,

这里我们主要拿一个最简单的demo查看使用,依赖关系简单,方便对照分析后面的原理解析部分。

module-federation-examples/vue3-demo at master · module-federation/module-federation-examples

2.1 项目目录

项目可以拆分成两个文件夹

  • home: 生产者,暴露出来Content和Button组件给外部使用,同时配置了共享库vue,设置singleton: true
  • Layout: 消费者,也就是host,消费home项目暴露出来的Content和Button,也配置了vue共享库

<!---->

vue3-demo/
├── home(remote)
│   ├── src
│   │   ├── App.vue         -- 入口组件 components: { Content: defineAsyncComponent(() => import('./components/Content')),
│   │   ├── components
│   │   │   ├── Button.js。  -- 业务组件Button
│   │   │   └── Content.vue  -- 业务组件Content
│   │   ├── index.js.      -- import('./main.js');
│   │   └── main.js        --  const app = createApp(App);app.mount('#app');
│   └── webpack.config.js   -- expose:{Content, Button}
│
├── layout(host)
│   ├── src
│   │   ├── Layout.vue     -- 使用业务组件Content,
│   │   ├── index.js       -- import('./main.js');
│   │   └── main.js        -- const Content = defineAsyncComponent(() => import('home/Content'));
│   └── webpack.config.js   -- remotes:{home: 'home@http://home.com/remoteEntry.js'}
└── package.json

2.2 核心代码块

2.2.1 Remote

2.2.1.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.1.2 src/main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
2.2.1.3 webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({

  target: 'web',
  entry: path.resolve(__dirname, './src/index.js'),

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'home',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {
        './Content': './src/components/Content',
        './Button': './src/components/Button',
      },
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],
   devServer: {
    port: 3002,
  },
});

2.2.2 Host

<!---->

2.2.2.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.2.2 src/main.js
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';

const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');
2.2.2.3 wepback.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
  entry: path.resolve(__dirname, './src/index.js'),


  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'layout',
      filename: 'remoteEntry.js',
      remotes: {
        home: 'home@http://localhost:3002/remoteEntry.js',
      },
      exposes: {},
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
  ],
  devServer: {
    port: 3001,
  },
});

2.3 示例截图

image-4.png

3. 原理分析

not magic, just async chunk

大白话解释一下,共享组件单独打包成一个异步chunk里面,共享的依赖会在加载组件之前进行前置加载,中间有一些复杂的依赖版本号比较,依赖库加载的逻辑通过shared字段进行配置。

这里的加载模块流程分析准备分三部分,首先介绍最基础的webpack异步模块加载流程,后面分别介绍module federation两个引入方式对应不同的加载流程

3.1 webpack异步模块加载流程

3.1.1 按需加载

按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。

在 webpack 中可以使用 importrequire.ensure 来引入需要动态导入的代码,还是用前面的vue3-demo示例,现在只关注home目录,并且把webpack配置中的ModuleFederationPlugin插件去掉。

home文件夹目录结构

vue3-demo/
├── home(remote)
│   ├── src
│   │   ├── App.vue         -- 入口组件 components: { Content: defineAsyncComponent(() => import('./components/Content')),
│   │   ├── components
│   │   │   ├── Button.js  -- 业务组件Button
│   │   │   └── Content.vue  -- 业务组件Content
│   │   ├── index.js.      -- import('./main.js');
│   │   └── main.js        --  const app = createApp(App);app.mount('#app');
│   └── webpack.config.js   

App.vue组件里面进行组件注册时,使用import方法引入

  components: {
    Content: defineAsyncComponent(() => import('./components/Content')),
    Button: defineAsyncComponent(() => import('./components/Button')),
  },

本地开发环境配置修改后,重新进行构建,可以看到从请求html到后续的js、css文件加载顺序。

3.1.2 初始化请求链路

3.1.3 源码与构建后代码对照

<!---->

3.1.3.1 入口html文件
3.1.3.1.1 源码
<div id="app"></div>
3.1.3.1.2 构建后代码
<head>
  <script defer src="main.js"></script>
</head>
 <div id="app"></div>
3.1.3.2 入口index.js文件
3.1.3.2.1 源码
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
3.1.3.2.2 构建后代码

构建后的startUp 入口函数

/******/ (() => { // webpackBootstrap
/******/    var __webpack_modules__ = ({
    /***/ "./src/index.js":
    /*!**********************!*\
      !*** ./src/index.js ***!
      **********************/
    /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
    
    // https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
    Promise.all(/*! import() */[__webpack_require__.e("vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2_webpac-fe6f3f"), __webpack_require__.e("src_main_js")]).then(__webpack_require__.bind(__webpack_require__, /*! ./main.js */ "./src/main.js"));
    
    
    /***/ })
    
    

})
    /************************************************************************/
    /******/    // The module cache
    /******/    var __webpack_module_cache__ = {};
    /******/    
    /******/    // The require function
    /******/    function __webpack_require__(moduleId) {
    /******/        // Check if module is in cache
    /******/        var cachedModule = __webpack_module_cache__[moduleId];
    /******/        if (cachedModule !== undefined) {
    /******/            return cachedModule.exports;
    /******/        }
    /******/        // Create a new module (and put it into the cache)
    /******/        var module = __webpack_module_cache__[moduleId] = {
    /******/            id: moduleId,
    /******/            // no module.loaded needed
    /******/            exports: {}
    /******/        };
    /******/    
    /******/        // Execute the module function
    /******/        var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
    /******/        __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
    /******/        module = execOptions.module;
    /******/        execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
    /******/    
    /******/        // Return the exports of the module
    /******/        return module.exports;
    /******/    }
    /******/    
    /******/    // expose the modules object (__webpack_modules__)
    /******/    __webpack_require__.m = __webpack_modules__;
    /******/    
    /******/    // expose the module cache
    /******/    __webpack_require__.c = __webpack_module_cache__;
    /******/    
    /******/    // expose the module execution interceptor
    /******/    __webpack_require__.i = [];
    /******/    
    /************************************************************************/
   
    /******/    
    /******/    /* webpack/runtime/ensure chunk */
    /******/    (() => {
    /******/        __webpack_require__.f = {};
    /******/        // This file contains only the entry chunk.
    /******/        // The chunk loading function for additional chunks
    /******/        __webpack_require__.e = (chunkId) => {
    /******/            return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
    /******/                __webpack_require__.f[key](chunkId, promises);
    /******/                return promises;
    /******/            }, []));
    /******/        };
    /******/    })();
    /******/    
    /******/    /* webpack/runtime/get javascript chunk filename */
    /******/    (() => {
    /******/        // This function allow to reference async chunks
    /******/        __webpack_require__.u = (chunkId) => {
    /******/            // return url for filenames based on template
    /******/            return "" + chunkId + ".js";
    /******/        };
    /******/    })();
    /******/    

    /******/    
    /******/    /* webpack/runtime/get mini-css chunk filename */
    /******/    (() => {
    /******/        // This function allow to reference async chunks
    /******/        __webpack_require__.miniCssF = (chunkId) => {
    /******/            // return url for filenames based on template
    /******/            return "" + chunkId + ".css";
    /******/        };
    /******/    })();
    /******/    

    /******/    

 
    /******/    
    /******/    /* webpack/runtime/load script */
    /******/    (() => {
    /******/        var inProgress = {};
    /******/        var dataWebpackPrefix = "vue3-demo_home:";
    /******/        // loadScript function to load a script via script tag
    /******/        __webpack_require__.l = (url, done, key, chunkId) => {
    /******/            if(inProgress[url]) { inProgress[url].push(done); return; }
    /******/            var script, needAttach;
    /******/            if(key !== undefined) {
    /******/                var scripts = document.getElementsByTagName("script");
    /******/                for(var i = 0; i < scripts.length; i++) {
    /******/                    var s = scripts[i];
    /******/                    if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
    /******/                }
    /******/            }
    /******/            if(!script) {
    /******/                needAttach = true;
    /******/                script = document.createElement('script');
    /******/        
    /******/                script.charset = 'utf-8';
    /******/                script.timeout = 120;
    /******/                if (__webpack_require__.nc) {
    /******/                    script.setAttribute("nonce", __webpack_require__.nc);
    /******/                }
    /******/                script.setAttribute("data-webpack", dataWebpackPrefix + key);
    /******/        
    /******/                script.src = url;
    /******/            }
    /******/            inProgress[url] = [done];
    /******/            var onScriptComplete = (prev, event) => {
    /******/                // avoid mem leaks in IE.
    /******/                script.onerror = script.onload = null;
    /******/                clearTimeout(timeout);
    /******/                var doneFns = inProgress[url];
    /******/                delete inProgress[url];
    /******/                script.parentNode && script.parentNode.removeChild(script);
    /******/                doneFns && doneFns.forEach((fn) => (fn(event)));
    /******/                if(prev) return prev(event);
    /******/            }
    /******/            var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
    /******/            script.onerror = onScriptComplete.bind(null, script.onerror);
    /******/            script.onload = onScriptComplete.bind(null, script.onload);
    /******/            needAttach && document.head.appendChild(script);
    /******/        };
    /******/    })();
    /******/    

   
    /******/    
   
    /******/    
    /******/    /* webpack/runtime/css loading */
    /******/    (() => {
    /******/        if (typeof document === "undefined") return;
    /******/        var createStylesheet = (chunkId, fullhref, oldTag, resolve, reject) => {
    /******/            var linkTag = document.createElement("link");
    /******/        
    /******/            linkTag.rel = "stylesheet";
    /******/            linkTag.type = "text/css";
    /******/            if (__webpack_require__.nc) {
    /******/                linkTag.nonce = __webpack_require__.nc;
    /******/            }
    /******/            var onLinkComplete = (event) => {
    /******/                // avoid mem leaks.
    /******/                linkTag.onerror = linkTag.onload = null;
    /******/                if (event.type === 'load') {
    /******/                    resolve();
    /******/                } else {
    /******/                    var errorType = event && event.type;
    /******/                    var realHref = event && event.target && event.target.href || fullhref;
    /******/                    var err = new Error("Loading CSS chunk " + chunkId + " failed.\n(" + errorType + ": " + realHref + ")");
    /******/                    err.name = "ChunkLoadError";
    /******/                    err.code = "CSS_CHUNK_LOAD_FAILED";
    /******/                    err.type = errorType;
    /******/                    err.request = realHref;
    /******/                    if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag)
    /******/                    reject(err);
    /******/                }
    /******/            }
    /******/            linkTag.onerror = linkTag.onload = onLinkComplete;
    /******/            linkTag.href = fullhref;
    /******/        
    /******/        
    /******/            if (oldTag) {
    /******/                oldTag.parentNode.insertBefore(linkTag, oldTag.nextSibling);
    /******/            } else {
    /******/                document.head.appendChild(linkTag);
    /******/            }
    /******/            return linkTag;
    /******/        };
    /******/        var findStylesheet = (href, fullhref) => {
    /******/            var existingLinkTags = document.getElementsByTagName("link");
    /******/            for(var i = 0; i < existingLinkTags.length; i++) {
    /******/                var tag = existingLinkTags[i];
    /******/                var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");
    /******/                if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return tag;
    /******/            }
    /******/            var existingStyleTags = document.getElementsByTagName("style");
    /******/            for(var i = 0; i < existingStyleTags.length; i++) {
    /******/                var tag = existingStyleTags[i];
    /******/                var dataHref = tag.getAttribute("data-href");
    /******/                if(dataHref === href || dataHref === fullhref) return tag;
    /******/            }
    /******/        };
    /******/        var loadStylesheet = (chunkId) => {
    /******/            return new Promise((resolve, reject) => {
    /******/                var href = __webpack_require__.miniCssF(chunkId);
    /******/                var fullhref = __webpack_require__.p + href;
    /******/                if(findStylesheet(href, fullhref)) return resolve();
    /******/                createStylesheet(chunkId, fullhref, null, resolve, reject);
    /******/            });
    /******/        }
    /******/        // object to store loaded CSS chunks
    /******/        var installedCssChunks = {
    /******/            "main": 0
    /******/        };
    /******/        
    /******/        __webpack_require__.f.miniCss = (chunkId, promises) => {
    /******/            var cssChunks = {"src_main_js":1};
    /******/            if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);
    /******/            else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {
    /******/                promises.push(installedCssChunks[chunkId] = loadStylesheet(chunkId).then(() => {
    /******/                    installedCssChunks[chunkId] = 0;
    /******/                }, (e) => {
    /******/                    delete installedCssChunks[chunkId];
    /******/                    throw e;
    /******/                }));
    /******/            }
    /******/        };
    /******/        
    /******/        var oldTags = [];
    /******/        var newTags = [];
    /******/        var applyHandler = (options) => {
    /******/            return { dispose: () => {
    /******/                for(var i = 0; i < oldTags.length; i++) {
    /******/                    var oldTag = oldTags[i];
    /******/                    if(oldTag.parentNode) oldTag.parentNode.removeChild(oldTag);
    /******/                }
    /******/                oldTags.length = 0;
    /******/            }, apply: () => {
    /******/                for(var i = 0; i < newTags.length; i++) newTags[i].rel = "stylesheet";
    /******/                newTags.length = 0;
    /******/            } };
    /******/        }
    /******/        __webpack_require__.hmrC.miniCss = (chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList) => {
    /******/            applyHandlers.push(applyHandler);
    /******/            chunkIds.forEach((chunkId) => {
    /******/                var href = __webpack_require__.miniCssF(chunkId);
    /******/                var fullhref = __webpack_require__.p + href;
    /******/                var oldTag = findStylesheet(href, fullhref);
    /******/                if(!oldTag) return;
    /******/                promises.push(new Promise((resolve, reject) => {
    /******/                    var tag = createStylesheet(chunkId, fullhref, oldTag, () => {
    /******/                        tag.as = "style";
    /******/                        tag.rel = "preload";
    /******/                        resolve();
    /******/                    }, reject);
    /******/                    oldTags.push(oldTag);
    /******/                    newTags.push(tag);
    /******/                }));
    /******/            });
    /******/        }
    /******/        
    /******/        // no prefetching
    /******/        
    /******/        // no preloaded
    /******/    })();
    /******/    
    /******/    /* webpack/runtime/jsonp chunk loading */
    /******/    (() => {
    /******/        // no baseURI
    /******/        
    /******/        // object to store loaded and loading chunks
    /******/        // undefined = chunk not loaded, null = chunk preloaded/prefetched
    /******/        // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
    /******/        var installedChunks = __webpack_require__.hmrS_jsonp = __webpack_require__.hmrS_jsonp || {
    /******/            "main": 0
    /******/        };
    /******/        
    /******/        __webpack_require__.f.j = (chunkId, promises) => {
    /******/                // JSONP chunk loading for javascript
    /******/                var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
    /******/                if(installedChunkData !== 0) { // 0 means "already installed".
    /******/        
    /******/                    // a Promise means "currently loading".
    /******/                    if(installedChunkData) {
    /******/                        promises.push(installedChunkData[2]);
    /******/                    } else {
    /******/                        if(true) { // all chunks have JS
    /******/                            // setup Promise in chunk cache
    /******/                            var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
    /******/                            promises.push(installedChunkData[2] = promise);
    /******/        
    /******/                            // start chunk loading
    /******/                            var url = __webpack_require__.p + __webpack_require__.u(chunkId);
    /******/                            // create error before stack unwound to get useful stacktrace later
    /******/                            var error = new Error();
    /******/                            var loadingEnded = (event) => {
    /******/                                if(__webpack_require__.o(installedChunks, chunkId)) {
    /******/                                    installedChunkData = installedChunks[chunkId];
    /******/                                    if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
    /******/                                    if(installedChunkData) {
    /******/                                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
    /******/                                        var realSrc = event && event.target && event.target.src;
    /******/                                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
    /******/                                        error.name = 'ChunkLoadError';
    /******/                                        error.type = errorType;
    /******/                                        error.request = realSrc;
    /******/                                        installedChunkData[1](error);
    /******/                                    }
    /******/                                }
    /******/                            };
    /******/                            __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
    /******/                        }
    /******/                    }
    /******/                }
    /******/        };
    /******/        
    /******/        // no prefetching
    /******/        
  
    /******/        

    /******/        
    /******/        
    /******/        // install a JSONP callback for chunk loading
    /******/        var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    /******/            var [chunkIds, moreModules, runtime] = data;
    /******/            // add "moreModules" to the modules object,
    /******/            // then flag all "chunkIds" as loaded and fire callback
    /******/            var moduleId, chunkId, i = 0;
    /******/            if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
    /******/                for(moduleId in moreModules) {
    /******/                    if(__webpack_require__.o(moreModules, moduleId)) {
    /******/                        __webpack_require__.m[moduleId] = moreModules[moduleId];
    /******/                    }
    /******/                }
    /******/                if(runtime) var result = runtime(__webpack_require__);
    /******/            }
    /******/            if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
    /******/            for(;i < chunkIds.length; i++) {
    /******/                chunkId = chunkIds[i];
    /******/                if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
    /******/                    installedChunks[chunkId][0]();
    /******/                }
    /******/                installedChunks[chunkId] = 0;
    /******/            }
    /******/        
    /******/        }
    /******/        
    /******/        var chunkLoadingGlobal = self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || [];
    /******/        chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    /******/        chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
    /******/    })();
    /******/    
    /************************************************************************/
    /******/    
    /******/    // module cache are used so entry inlining is disabled
    /******/    // startup
    /******/    // Load entry module and return exports
    /******/    __webpack_require__("../../node_modules/.pnpm/webpack-dev-server@5.0.4_webpack-cli@5.1.4_webpack@5.96.1/node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=3002&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true");
    /******/    __webpack_require__("../../node_modules/.pnpm/webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js");
    /******/    var __webpack_exports__ = __webpack_require__("./src/index.js");
    /******/    
    /******/ })()
    ;
    //# sourceMappingURL=main.js.map
3.1.3.3 main.js文件

<!---->

3.1.3.3.1 源码
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
3.1.3.3.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || []).push([["src_main_js"], {

    /***/
    "../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-2.use[0]!../../node_modules/.pnpm/css-loader@7.1.2_@rspack+core@1.1.1_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/css-loader/dist/cjs.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/stylePostLoader.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css": /*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-2.use[0]!../../node_modules/.pnpm/css-loader@7.1.2_@rspack+core@1.1.1_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/css-loader/dist/cjs.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/stylePostLoader.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css ***!
  *******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
    /***/
    ( (module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        // extracted by mini-css-extract-plugin

        if (true) {
            (function() {
                var localsJsonString = undefined;
                // 1758629789554
                var cssReload = __webpack_require__(/*! ../../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/hmr/hotModuleReplacement.js */
                "../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/hmr/hotModuleReplacement.js")(module.id, {});
                // only invalidate when locals change
                if (module.hot.data && module.hot.data.value && module.hot.data.value !== localsJsonString) {
                    module.hot.invalidate();
                } else {
                    module.hot.accept();
                }
                module.hot.dispose(function(data) {
                    data.value = localsJsonString;
                    cssReload();
                });
            }
            )();
        }

        /***/
    }
    ),

    /***/
    "./src/App.vue": /*!*********************!*\
  !*** ./src/App.vue ***!
  *********************/
    /***/
    ( (module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)/* harmony export */
        });
        /* harmony import */
        var _App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90&scoped=true */
        "./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true");
        /* harmony import */
        var _App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&lang=js */
        "./src/App.vue?vue&type=script&lang=js");
        /* harmony import */
        var _App_vue_vue_type_style_index_0_id_7ba5bd90_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css */
        "./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css");
        /* harmony import */
        var _Users_liqi_fe_module_federation_examples_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js");

        ;
        const __exports__ = /*#__PURE__*/
        (0,
        _Users_liqi_fe_module_federation_examples_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_3__["default"])(_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__["default"], [['render', _App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__.render], ['__scopeId', "data-v-7ba5bd90"], ['__file', "src/App.vue"]])
        /* hot reload */
        if (true) {
            __exports__.__hmrId = "7ba5bd90"
            const api = __VUE_HMR_RUNTIME__
            module.hot.accept()
            if (!api.createRecord('7ba5bd90', __exports__)) {
                console.log('reload')
                api.reload('7ba5bd90', __exports__)
            }

            module.hot.accept(/*! ./App.vue?vue&type=template&id=7ba5bd90&scoped=true */
            "./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true", __WEBPACK_OUTDATED_DEPENDENCIES__ => {
                /* harmony import */
                _App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90&scoped=true */
                "./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true");
                ( () => {
                    console.log('re-render')
                    api.rerender('7ba5bd90', _App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__.render)
                }
                )(__WEBPACK_OUTDATED_DEPENDENCIES__);
            }
            )

        }

        /* harmony default export */
        const __WEBPACK_DEFAULT_EXPORT__ = (__exports__);

        /***/
    }
    ),

    /***/
    "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=script&lang=js": /*!*****************************************************************************************************************************************************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=script&lang=js ***!
  *****************************************************************************************************************************************************************************************************************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)/* harmony export */
        });
        /* harmony import */
        var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */
        "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");

        // import Content from "./components/Content";
        // import Button from "./components/Button";
        /* harmony default export */
        const __WEBPACK_DEFAULT_EXPORT__ = ({
            components: {
                Content: (0,
                vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)( () => __webpack_require__.e(/*! import() */
                "src_components_Content_vue").then(__webpack_require__.bind(__webpack_require__, /*! ./components/Content */
                "./src/components/Content.vue"))),
                Button: (0,
                vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)( () => __webpack_require__.e(/*! import() */
                "src_components_Button_js").then(__webpack_require__.bind(__webpack_require__, /*! ./components/Button */
                "./src/components/Button.js"))),
            },
            // components: {
            //   Content,
            //   Button,
            // },
            setup() {
                const count = (0,
                vue__WEBPACK_IMPORTED_MODULE_0__.ref)(0);
                const inc = () => {
                    count.value++;
                }
                ;

                return {
                    count,
                    inc,
                };
            },
        });

        /***/
    }
    ),

    /***/
    "./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css": /*!*****************************************************************************!*\
  !*** ./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css ***!
  *****************************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _node_modules_pnpm_mini_css_extract_plugin_2_9_2_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_mini_css_extract_plugin_dist_loader_js_clonedRuleSet_2_use_0_node_modules_pnpm_css_loader_7_1_2_rspack_core_1_1_1_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_css_loader_dist_cjs_js_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_stylePostLoader_js_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_App_vue_vue_type_style_index_0_id_7ba5bd90_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-2.use[0]!../../../node_modules/.pnpm/css-loader@7.1.2_@rspack+core@1.1.1_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/css-loader/dist/cjs.js!../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/stylePostLoader.js!../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css */
        "../../node_modules/.pnpm/mini-css-extract-plugin@2.9.2_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-2.use[0]!../../node_modules/.pnpm/css-loader@7.1.2_@rspack+core@1.1.1_webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/css-loader/dist/cjs.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/stylePostLoader.js!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css");

        /***/
    }
    ),

    /***/
    "./src/App.vue?vue&type=script&lang=js": /*!*********************************************!*\
  !*** ./src/App.vue?vue&type=script&lang=js ***!
  *********************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (/* reexport safe */
            _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__["default"])/* harmony export */
        });
        /* harmony import */
        var _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./App.vue?vue&type=script&lang=js */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=script&lang=js");

        /***/
    }
    ),

    /***/
    "./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true": /*!***************************************************************!*\
  !*** ./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true ***!
  ***************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            render: () => (/* reexport safe */
            _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_1_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__.render)/* harmony export */
        });
        /* harmony import */
        var _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_1_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./App.vue?vue&type=template&id=7ba5bd90&scoped=true */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true");

        /***/
    }
    ),

    /***/
    "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true": /*!*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true ***!
  *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            render: () => (/* binding */
            render)/* harmony export */
        });
        /* harmony import */
        var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */
        "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");

        const _withScopeId = n => ((0,
        vue__WEBPACK_IMPORTED_MODULE_0__.pushScopeId)("data-v-7ba5bd90"),
        n = n(),
        (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.popScopeId)(),
        n)
        const _hoisted_1 = /*#__PURE__*/
        _withScopeId( () => /*#__PURE__*/
        (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("h3", null, "Main App", -1 /* HOISTED */
        ))

        function render(_ctx, _cache, $props, $setup, $data, $options) {
            const _component_Content = (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("Content")
            const _component_Button = (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)("Button")

            return ((0,
            vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(),
            (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", null, [_hoisted_1, (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_Content), (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_Button)]))
        }

        /***/
    }
    ),

    /***/
    "./src/main.js": /*!*********************!*\
  !*** ./src/main.js ***!
  *********************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */
        "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");
        /* harmony import */
        var _App_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue */
        "./src/App.vue");

        const app = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_App_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);
        app.mount('#app');

        /***/
    }
    )

}]);
//# sourceMappingURL=src_main_js.js.map
3.1.3.4 button.js组件文件

<!---->

3.1.3.4.1 源码
import { render, h } from 'vue';
const button = {
  name: 'btn-component',
  render() {
    return h(
      'button',
      {
        id: 'btn-primary',
      },
      'Hello World',
    );
  },
};
export default button;
3.1.3.4.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || []).push([["src_components_Button_js"],{

/***/ "./src/components/Button.js":
/*!**********************************!*\
  !*** ./src/components/Button.js ***!
  **********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");

const button = {
  name: 'btn-component',
  render() {
    return (0,vue__WEBPACK_IMPORTED_MODULE_0__.h)(
      'button',
      {
        id: 'btn-primary',
      },
      'Hello World',
    );
  },
};
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (button);


/***/ })

}]);
//# sourceMappingURL=src_components_Button_js.js.map
3.1.3.5 Content.vue组件文件

<!---->

3.1.3.5.1 源码
<template>
  <div style="color: red">{{ title }}</div>
</template>
<script>
export default {
  data() {
    return {
      title: 'Remote Component in Action..',
    };
  },
};
</script>
3.1.3.5.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || []).push([["src_components_Content_vue"], {

    /***/
    "./src/components/Content.vue": /*!************************************!*\
  !*** ./src/components/Content.vue ***!
  ************************************/
    /***/
    ( (module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)/* harmony export */
        });
        /* harmony import */
        var _Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Content.vue?vue&type=template&id=7eab81f9 */
        "./src/components/Content.vue?vue&type=template&id=7eab81f9");
        /* harmony import */
        var _Content_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Content.vue?vue&type=script&lang=js */
        "./src/components/Content.vue?vue&type=script&lang=js");
        /* harmony import */
        var _Users_liqi_fe_module_federation_examples_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js");

        ;const __exports__ = /*#__PURE__*/
        (0,
        _Users_liqi_fe_module_federation_examples_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__["default"])(_Content_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__["default"], [['render', _Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__.render], ['__file', "src/components/Content.vue"]])
        /* hot reload */
        if (true) {
            __exports__.__hmrId = "7eab81f9"
            const api = __VUE_HMR_RUNTIME__
            module.hot.accept()
            if (!api.createRecord('7eab81f9', __exports__)) {
                console.log('reload')
                api.reload('7eab81f9', __exports__)
            }

            module.hot.accept(/*! ./Content.vue?vue&type=template&id=7eab81f9 */
            "./src/components/Content.vue?vue&type=template&id=7eab81f9", __WEBPACK_OUTDATED_DEPENDENCIES__ => {
                /* harmony import */
                _Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Content.vue?vue&type=template&id=7eab81f9 */
                "./src/components/Content.vue?vue&type=template&id=7eab81f9");
                ( () => {
                    console.log('re-render')
                    api.rerender('7eab81f9', _Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__.render)
                }
                )(__WEBPACK_OUTDATED_DEPENDENCIES__);
            }
            )

        }

        /* harmony default export */
        const __WEBPACK_DEFAULT_EXPORT__ = (__exports__);

        /***/
    }
    ),

    /***/
    "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=script&lang=js": /*!********************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=script&lang=js ***!
  ********************************************************************************************************************************************************************************************************************************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)/* harmony export */
        });

        /* harmony default export */
        const __WEBPACK_DEFAULT_EXPORT__ = ({
            data() {
                return {
                    title: 'Remote Component in Action..',
                };
            },
        });

        /***/
    }
    ),

    /***/
    "./src/components/Content.vue?vue&type=script&lang=js": /*!************************************************************!*\
  !*** ./src/components/Content.vue?vue&type=script&lang=js ***!
  ************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (/* reexport safe */
            _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_Content_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__["default"])/* harmony export */
        });
        /* harmony import */
        var _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_Content_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./Content.vue?vue&type=script&lang=js */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=script&lang=js");

        /***/
    }
    ),

    /***/
    "./src/components/Content.vue?vue&type=template&id=7eab81f9": /*!******************************************************************!*\
  !*** ./src/components/Content.vue?vue&type=template&id=7eab81f9 ***!
  ******************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            render: () => (/* reexport safe */
            _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_1_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__.render)/* harmony export */
        });
        /* harmony import */
        var _node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_1_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_index_js_ruleSet_1_rules_4_use_0_Content_vue_vue_type_template_id_7eab81f9__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./Content.vue?vue&type=template&id=7eab81f9 */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=template&id=7eab81f9");

        /***/
    }
    ),

    /***/
    "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=template&id=7eab81f9": /*!************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[4].use[0]!./src/components/Content.vue?vue&type=template&id=7eab81f9 ***!
  ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            render: () => (/* binding */
            render)/* harmony export */
        });
        /* harmony import */
        var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */
        "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");

        const _hoisted_1 = {
            style: {
                "color": "red"
            }
        }

        function render(_ctx, _cache, $props, $setup, $data, $options) {
            return ((0,
            vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(),
            (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_1, (0,
            vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($data.title), 1 /* TEXT */
            ))
        }

        /***/
    }
    )

}]);
//# sourceMappingURL=src_components_Content_vue.js.map

3.1.4 过程解析

3.1.4.1 webpack\_require("./src/index.js")

先从入口index.js文件开始看,里面只有一行代码,异步引入main.js模块,

import('./main.js');

但是构建后的startUp入口文件,里面包含了很多基础依赖库,只有在代码最后出现了index.js引入相关代码,

再把里面的代码简化一下,只保留关键的引入部分。

/******/ (() => { // webpackBootstrap
/******/    var __webpack_modules__ = ({
    /***/ "./src/index.js":
    /*!**********************!*\
      !*** ./src/index.js ***!
      **********************/
    /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
    
    // https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
    Promise.all(/*! import() */[__webpack_require__.e("vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2_webpac-fe6f3f"), __webpack_require__.e("src_main_js")]).then(__webpack_require__.bind(__webpack_require__, /*! ./main.js */ "./src/main.js"));
    
    
    /***/ })

})
    /************************************************************************/
    /******/    // The module cache
    /******/    var __webpack_module_cache__ = {};
    /******/    
    /******/    // The require function
    /******/    function __webpack_require__(moduleId) {
    /******/        // Check if module is in cache
    /******/        var cachedModule = __webpack_module_cache__[moduleId];
    /******/        if (cachedModule !== undefined) {
    /******/            return cachedModule.exports;
    /******/        }
    /******/        // Create a new module (and put it into the cache)
    /******/        var module = __webpack_module_cache__[moduleId] = {
    /******/            id: moduleId,
    /******/            // no module.loaded needed
    /******/            exports: {}
    /******/        };
    /******/    
    /******/        // Execute the module function
    /******/        var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
    /******/        __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
    /******/        module = execOptions.module;
    /******/        execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
    /******/    
    /******/        // Return the exports of the module
    /******/        return module.exports;
    /******/    }
    /******/    
    /******/    // expose the modules object (__webpack_modules__)
    /******/    __webpack_require__.m = __webpack_modules__;
    /******/    
    /******/    // expose the module cache
    /******/    __webpack_require__.c = __webpack_module_cache__;
    /******/    
    /******/    // expose the module execution interceptor
    /******/    __webpack_require__.i = [];
    /******/    
    /************************************************************************/
    /******/    // module cache are used so entry inlining is disabled
    /******/    // startup
    /******/    // Load entry module and return exports
    /******/    __webpack_require__("../../node_modules/.pnpm/webpack-dev-server@5.0.4_webpack-cli@5.1.4_webpack@5.96.1/node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=3002&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true");
    /******/    __webpack_require__("../../node_modules/.pnpm/webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js");
    /******/    var __webpack_exports__ = __webpack_require__("./src/index.js");
    /******/    
    /******/ })()
    ;

直接看到最后一行代码,通过\_\_webpack\_require\_\_方法引入index.js文件,这里也可以认为是一般的同步加载模块部分

*/    var __webpack_exports__ = __webpack_require__("./src/index.js");

继续看\_\_webpack\_require\_\_函数实现,先从\_\_webpack\_module\_cache\_\_缓存变量中获取moduleId为./src/index.js的模块,没有缓存,则创建一个新的模块数据结构,并放到\_\_webpack\_module\_cache\_\_变量里面,

    /******/    // The module cache
    /******/    var __webpack_module_cache__ = {};
    /******/    
    /******/    // The require function
    /******/    function __webpack_require__(moduleId) {
    /******/        // 先从__webpack_module_cache__缓存变量中获取moduleId为./src/index.js的模块
    /******/        var cachedModule = __webpack_module_cache__[moduleId];
    /******/        if (cachedModule !== undefined) {
    /******/            return cachedModule.exports;
    /******/        }
    /******/        // 没有缓存,则创建一个新的模块数据结构,并放到__webpack_module_cache__变量里面
    /******/        var module = __webpack_module_cache__[moduleId] = {
    /******/            id: moduleId,
    /******/            // no module.loaded needed
    /******/            exports: {}
    /******/        };
    /******/    
    /******/        // Execute the module function
    /******/        var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
    /******/        __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
    /******/        module = execOptions.module;
    /******/        execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
    /******/    
    /******/        // Return the exports of the module
    /******/        return module.exports;
    /******/    }

缓存数据结构中factory字段存放模块里面的具体代码实现,通过\_\_webpack\_modules\_\_[moduleId]获取代码实现部分,最后执行。

3.1.4.2 webpack\_modules

接着看\_\_webpack\_modules\_\_变量,其实在文件最开头已经定义好了,这里才是真正存放原始的'src/index.js'文件,经过构建后的代码,可以具体看一下

/******/ var __webpack_modules__ = ({
    /***/ "./src/index.js":
    /*!**********************!*\
      !*** ./src/index.js ***!
      **********************/
    /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
    
    // https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
    Promise.all(/*! import() */
       [
        __webpack_require__.e("vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2_webpac-fe6f3f"), 
        __webpack_require__.e("src_main_js")]
       ).then(
       __webpack_require__.bind(__webpack_require__, /*! ./main.js */ "./src/main.js")
       );
    
    
    /***/ })

})

简单看原始的 import('./main.js'); 异步加载main.js模块,转化成了 __webpack_require__.e("src_main_js")

这里可以暂时理解成去异步请求main.js文件,等main.js文件请求完成后,进入then里面,执行

__webpack_require__.bind(__webpack_require__, /*! ./main.js */ "./src/main.js")

也就是进行一般的同步加载模块,类似前面加载"./src/index.js",都是用__webpack_require__函数。

3.1.4.3 webpack\_require.e

接着关键看__webpack_require__.e是如何实现,其实从前面的index.js加载过程,大致可以推测一下,它主要任务有两个

  1. 通过创建一个script标签,并设置到html里面,发起请求main.js文件,并自动执行main.js
  2. ...
  3. __webpack_modules__变量中找到./src/main.js的具体实现。

如何把main.js执行完成后,把自身代码实现设置到__webpack_modules__变量上面。就是最大的疑问。


/******/         /* webpack/runtime/ensure chunk */
/******/         (() => {
/******/                 __webpack_require__.f = {};
/******/                 // This file contains only the entry chunk.
/******/                 // The chunk loading function for additional chunks
/******/                 __webpack_require__.e = (chunkId) => {
/******/                         return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/                                 __webpack_require__.f[key](chunkId, promises);
/******/                                 return promises;
/******/                         }, []));
/******/                 };
/******/         })();

遍历\_webpack\_require\_\_.f对象上挂载的所有方法,并执行。

__webpack_require__.f = {
        miniCss:  (chunkId, promises) => {
/******/            var cssChunks = {"90":1};
/******/            if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);
/******/            else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {
/******/                promises.push(installedCssChunks[chunkId] = loadStylesheet(chunkId).then(() => {
/******/                    installedCssChunks[chunkId] = 0;
/******/                }, (e) => {
/******/                    delete installedCssChunks[chunkId];
/******/                    throw e;
/******/                }));
/******/            }
/******/        };
/******/  j:  (chunkId, promises) => {
/******/                // JSONP chunk loading for javascript
/******/                var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/                if(installedChunkData !== 0) { // 0 means "already installed".
/******/        
/******/                    // a Promise means "currently loading".
/******/                    if(installedChunkData) {
/******/                        promises.push(installedChunkData[2]);
/******/                    } else {
/******/                        if(true) { // all chunks have JS
/******/                            // setup Promise in chunk cache
/******/                            var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/                            promises.push(installedChunkData[2] = promise);
/******/        
/******/                            // start chunk loading
/******/                            var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/                            // create error before stack unwound to get useful stacktrace later
/******/                            var error = new Error();
/******/                            var loadingEnded = (event) => {
/******/                                if(__webpack_require__.o(installedChunks, chunkId)) {
/******/                                    installedChunkData = installedChunks[chunkId];
/******/                                    if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/                                    if(installedChunkData) {
/******/                                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/                                        var realSrc = event && event.target && event.target.src;
/******/                                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/                                        error.name = 'ChunkLoadError';
/******/                                        error.type = errorType;
/******/                                        error.request = realSrc;
/******/                                        installedChunkData[1](error);
/******/                                    }
/******/                                }
/******/                            };
/******/                            __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/                        }
/******/                    }
/******/                }
/******/        };
}

两个方法

  • miniCss: 通过createStylesheet方法,动态创建link标签,异步加载样式文件
  • j: 应该就是jsonp缩写,具体实现是__webpack_require__.l,里面主要功能也是通过动态创建script标签,异步加载./src/main.js文件

<!---->

/******/    /* webpack/runtime/load script */
/******/    (() => {
/******/        var inProgress = {};
/******/        var dataWebpackPrefix = "vue3-demo_home:";
/******/        // loadScript function to load a script via script tag
/******/        __webpack_require__.l = (url, done, key, chunkId) => {
                           ...
/******/                needAttach = true;
/******/                script = document.createElement('script');
/******/        
/******/                script.charset = 'utf-8';
/******/                script.timeout = 120;
/******/                if (__webpack_require__.nc) {
/******/                    script.setAttribute("nonce", __webpack_require__.nc);
/******/                }
/******/                script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/        
/******/                script.src = url;
/******/   
/******/            inProgress[url] = [done];
/******/            var onScriptComplete = (prev, event) => {
                          ....

/******/            }
/******/            var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/            script.onerror = onScriptComplete.bind(null, script.onerror);
/******/            script.onload = onScriptComplete.bind(null, script.onload);
/******/        };
/******/    })();
3.1.4.4 ./src/main.js

接着看./src/main.js 文件加载并执行完成有什么效果。

"use strict";
(self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || []).push([["src_main_js"], {

    /***/
    "./src/App.vue": /*!*********************!*\
  !*** ./src/App.vue ***!
  *********************/
    /***/
    ( (module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony export */
        __webpack_require__.d(__webpack_exports__, {
            /* harmony export */
            "default": () => (__WEBPACK_DEFAULT_EXPORT__)/* harmony export */
        });
        /* harmony import */
        var _App_vue_vue_type_template_id_7ba5bd90_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90&scoped=true */
        "./src/App.vue?vue&type=template&id=7ba5bd90&scoped=true");
        /* harmony import */
        var _App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&lang=js */
        "./src/App.vue?vue&type=script&lang=js");
        /* harmony import */
        var _App_vue_vue_type_style_index_0_id_7ba5bd90_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css */
        "./src/App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css");
        /* harmony import */
        var _Users_liqi_fe_module_federation_examples_node_modules_pnpm_vue_loader_16_8_3_vue_compiler_sfc_3_4_31_vue_3_3_7_typescript_5_6_3_webpack_5_96_1_swc_core_1_9_2_webpack_cli_5_1_4_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js */
        "../../node_modules/.pnpm/vue-loader@16.8.3_@vue+compiler-sfc@3.4.31_vue@3.3.7_typescript@5.6.3__webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4_/node_modules/vue-loader/dist/exportHelper.js");

        ;
        /* harmony default export */
        const __WEBPACK_DEFAULT_EXPORT__ = (__exports__);

        /***/
    }
    ),

    /***/
    "./src/main.js": /*!*********************!*\
  !*** ./src/main.js ***!
  *********************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */
        "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js");
        /* harmony import */
        var _App_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue */
        "./src/App.vue");

        const app = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_App_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);
        app.mount('#app');

        /***/
    }
    )

}]);

push里面的内容跟. __webpack_modules__变量结构很类似,key对应moduleid, value对应具体代码实现。

self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || []).push([["src_main_js"], {
    {key}: {value}
}])

self是指向当前 window 对象的引用,在Service Workers和Web Workers非window场景下也适用

3.1.4.5 self["webpackChunkvue3\_demo\_home"].push = webpackJsonpCallback

从[入口的js文件中]寻找 self["webpackChunkvue3\_demo\_home"]的实现。

 // install a JSONP callback for chunk loading
    /******/        var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    /******/            var [chunkIds, moreModules, runtime] = data;
    /******/            // add "moreModules" to the modules object,
    /******/            // then flag all "chunkIds" as loaded and fire callback
    /******/            var moduleId, chunkId, i = 0;
    /******/            if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
    /******/                for(moduleId in moreModules) {
    /******/                    if(__webpack_require__.o(moreModules, moduleId)) {
    /******/                        __webpack_require__.m[moduleId] = moreModules[moduleId];
    /******/                    }
    /******/                }
    /******/                if(runtime) var result = runtime(__webpack_require__);
    /******/            }
    /******/            if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
    /******/            for(;i < chunkIds.length; i++) {
    /******/                chunkId = chunkIds[i];
    /******/                if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
    /******/                    installedChunks[chunkId][0]();
    /******/                }
    /******/                installedChunks[chunkId] = 0;
    /******/            }
    /******/        
    /******/        }
    /******/        
    /******/        var chunkLoadingGlobal = self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || [];
    /******/        chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    /******/        chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
    /******/    })();

上面代码有两个关键点:

  • chunkLoadingGlobal.push,也就是self["webpackChunkvue3\_demo\_home"].push方法被改写成了webpackJsonpCallback
  • webpackJsonpCallback里面\_\_webpack\_require\_\_.m 也就是指向了 webpack\_modules, 把push方法传入的参数解析到\_\_webpack\_modules\_\_对象上,完成了模块赋值闭环。

3.1.5 整体组件加载执行过程

image-8.png

3.2 Module Federation模块加载流程

module federation加载流程跟异步加载主流程保持不变,额外引入了remote、resumes处理方法,所以需要先看完webpack按需加载到流程

3.2.1 概述

Module Federation 加载跟按需加载最大的区别是它需要处理共享模块和共享依赖,

如何进行依赖前置是很大的问题。

还是用前面的vue3-demo示例,现在设置上ModuleFederationPlugin插件,主要看Layout目录下的文件,从消费者角度看模块加载流程,入口文件还是src/index.js

vue3-demo/
├── layout(host)
│   ├── src
│   │   ├── Layout.vue     -- 使用业务组件Content,
│   │   ├── index.js       -- import('./main.js');
│   │   └── main.js        -- const Content = defineAsyncComponent(() => import('home/Content'));
│   └── webpack.config.js   -- remotes:{home: 'home@http://home.com/remoteEntry.js'}
└── package.json

3.2.2 初始化请求链路

从请求链路入口js后面多了remoteEntry,提前加载了共享组件Content、Button

3.2.3 源码与构建后代码对照

3.2.3.1 入口html文件

<!---->

3.2.3.1.1 源码
<div id="app"></div>
3.2.3.1.2 构建后代码
<head><script defer src="main.js"></script></head><div id="app"></div>
3.2.3.2 入口index.js文件
3.2.3.2.1 源码
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
3.2.3.2.2 构建后代码

构建后的startUp 入口函数

/******/ (() => { // webpackBootstrap
/******/  var __webpack_modules__ = ({
  
  /***/ "./src/index.js":
  /*!**********************!*\
    !*** ./src/index.js ***!
    **********************/
  /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  
  // https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
  __webpack_require__.e(/*! import() */ "src_main_js").then(__webpack_require__.bind(__webpack_require__, /*! ./main.js */ "./src/main.js"));
  
  
  /***/ }),
  
  /***/ "webpack/container/reference/home":
  /*!************************************************************!*\
    !*** external "home@http://localhost:3002/remoteEntry.js" ***!
    ************************************************************/
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
  
  "use strict";
  var __webpack_error__ = new Error();
  module.exports = new Promise((resolve, reject) => {
    if(typeof home !== "undefined") return resolve();
    __webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {
      if(typeof home !== "undefined") return resolve();
      var errorType = event && (event.type === 'load' ? 'missing' : event.type);
      var realSrc = event && event.target && event.target.src;
      __webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
      __webpack_error__.name = 'ScriptExternalLoadError';
      __webpack_error__.type = errorType;
      __webpack_error__.request = realSrc;
      reject(__webpack_error__);
    }, "home");
  }).then(() => (home));
  
  /***/ })
  
  /******/  });
  /************************************************************************/
  /******/  // The module cache
  /******/  var __webpack_module_cache__ = {};
  /******/  
  /******/  // The require function
  /******/  function __webpack_require__(moduleId) {
  /******/    // Check if module is in cache
  /******/    var cachedModule = __webpack_module_cache__[moduleId];
  /******/    if (cachedModule !== undefined) {
  /******/      return cachedModule.exports;
  /******/    }
  /******/    // Create a new module (and put it into the cache)
  /******/    var module = __webpack_module_cache__[moduleId] = {
  /******/      id: moduleId,
  /******/      // no module.loaded needed
  /******/      exports: {}
  /******/    };
  /******/  
  /******/    // Execute the module function
  /******/    var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
  /******/    __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
  /******/    module = execOptions.module;
  /******/    execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
  /******/  
  /******/    // Return the exports of the module
  /******/    return module.exports;
  /******/  }
  /******/  
  /******/  // expose the modules object (__webpack_modules__)
  /******/  __webpack_require__.m = __webpack_modules__;
  /******/  
  /******/  // expose the module cache
  /******/  __webpack_require__.c = __webpack_module_cache__;
  /******/  
  /******/  // expose the module execution interceptor
  /******/  __webpack_require__.i = [];
  /******/  

  /******/  

  /******/  

  /******/  
  /******/  /* webpack/runtime/ensure chunk */
  /******/  (() => {
  /******/    __webpack_require__.f = {};
  /******/    // This file contains only the entry chunk.
  /******/    // The chunk loading function for additional chunks
  /******/    __webpack_require__.e = (chunkId) => {
  /******/      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
  /******/        __webpack_require__.f[key](chunkId, promises);
  /******/        return promises;
  /******/      }, []));
  /******/    };
  /******/  })();
  /******/  
  /******/  

  /******/  
  /******/  /* webpack/runtime/get mini-css chunk filename */
  /******/  (() => {
  /******/    // This function allow to reference async chunks
  /******/    __webpack_require__.miniCssF = (chunkId) => {
  /******/      // return url for filenames based on template
  /******/      return "" + chunkId + ".css";
  /******/    };
  /******/  })();

  /******/  
  /******/  /* webpack/runtime/remotes loading */
  /******/  (() => {
  /******/    var chunkMapping = {
  /******/      "webpack_container_remote_home_Content": [
  /******/        "webpack/container/remote/home/Content"
  /******/      ],
  /******/      "webpack_container_remote_home_Button": [
  /******/        "webpack/container/remote/home/Button"
  /******/      ]
  /******/    };
  /******/    var idToExternalAndNameMapping = {
  /******/      "webpack/container/remote/home/Content": [
  /******/        "default",
  /******/        "./Content",
  /******/        "webpack/container/reference/home"
  /******/      ],
  /******/      "webpack/container/remote/home/Button": [
  /******/        "default",
  /******/        "./Button",
  /******/        "webpack/container/reference/home"
  /******/      ]
  /******/    };
  /******/    __webpack_require__.f.remotes = (chunkId, promises) => {
  /******/      if(__webpack_require__.o(chunkMapping, chunkId)) {
  /******/        chunkMapping[chunkId].forEach((id) => {
  /******/          var getScope = __webpack_require__.R;
  /******/          if(!getScope) getScope = [];
  /******/          var data = idToExternalAndNameMapping[id];
  /******/          if(getScope.indexOf(data) >= 0) return;
  /******/          getScope.push(data);
  /******/          if(data.p) return promises.push(data.p);
  /******/          var onError = (error) => {
  /******/            if(!error) error = new Error("Container missing");
  /******/            if(typeof error.message === "string")
  /******/              error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
  /******/            __webpack_require__.m[id] = () => {
  /******/              throw error;
  /******/            }
  /******/            data.p = 0;
  /******/          };
  /******/          var handleFunction = (fn, arg1, arg2, d, next, first) => {
  /******/            try {
  /******/              var promise = fn(arg1, arg2);
  /******/              if(promise && promise.then) {
  /******/                var p = promise.then((result) => (next(result, d)), onError);
  /******/                if(first) promises.push(data.p = p); else return p;
  /******/              } else {
  /******/                return next(promise, d, first);
  /******/              }
  /******/            } catch(error) {
  /******/              onError(error);
  /******/            }
  /******/          }
  /******/          var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());
  /******/          var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
  /******/          var onFactory = (factory) => {
  /******/            data.p = 1;
  /******/            __webpack_require__.m[id] = (module) => {
  /******/              module.exports = factory();
  /******/            }
  /******/          };
  /******/          handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
  /******/        });
  /******/      }
  /******/    }
  /******/  })();
  /******/  
  /******/  /* webpack/runtime/sharing */
  /******/  (() => {
  /******/    __webpack_require__.S = {};
  /******/    var initPromises = {};
  /******/    var initTokens = {};
  /******/    __webpack_require__.I = (name, initScope) => {
  /******/      if(!initScope) initScope = [];
  /******/      // handling circular init calls
  /******/      var initToken = initTokens[name];
  /******/      if(!initToken) initToken = initTokens[name] = {};
  /******/      if(initScope.indexOf(initToken) >= 0) return;
  /******/      initScope.push(initToken);
  /******/      // only runs once
  /******/      if(initPromises[name]) return initPromises[name];
  /******/      // creates a new share scope if needed
  /******/      if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
  /******/      // runs all init snippets from all modules reachable
  /******/      var scope = __webpack_require__.S[name];
  /******/      var warn = (msg) => {
  /******/        if (typeof console !== "undefined" && console.warn) console.warn(msg);
  /******/      };
  /******/      var uniqueName = "vue3-demo_layout";
  /******/      var register = (name, version, factory, eager) => {
  /******/        var versions = scope[name] = scope[name] || {};
  /******/        var activeVersion = versions[version];
  /******/        if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
  /******/      };
  /******/      var initExternal = (id) => {
  /******/        var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
  /******/        try {
  /******/          var module = __webpack_require__(id);
  /******/          if(!module) return;
  /******/          var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
  /******/          if(module.then) return promises.push(module.then(initFn, handleError));
  /******/          var initResult = initFn(module);
  /******/          if(initResult && initResult.then) return promises.push(initResult['catch'](handleError));
  /******/        } catch(err) { handleError(err); }
  /******/      }
  /******/      var promises = [];
  /******/      switch(name) {
  /******/        case "default": {
  /******/          register("vue", "3.3.7", () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! ../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js"))))));
  /******/          initExternal("webpack/container/reference/home");
  /******/        }
  /******/        break;
  /******/      }
  /******/      if(!promises.length) return initPromises[name] = 1;
  /******/      return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
  /******/    };
  /******/  })();

  /******/  
  /******/  /* webpack/runtime/consumes */
  /******/  (() => {
  /******/    var parseVersion = (str) => {
  /******/      // see webpack/lib/util/semver.js for original code
  /******/      var p=p=>{return p.split(".").map((p=>{return+p==p?+p:p}))},n=/^([^-+]+)?(?:-([^+]+))?(?:+(.+))?$/.exec(str),r=n[1]?p(n[1]):[];return n[2]&&(r.length++,r.push.apply(r,p(n[2]))),n[3]&&(r.push([]),r.push.apply(r,p(n[3]))),r;
  /******/    }
  /******/    var versionLt = (a, b) => {
  /******/      // see webpack/lib/util/semver.js for original code
  /******/      a=parseVersion(a),b=parseVersion(b);for(var r=0;;){if(r>=a.length)return r<b.length&&"u"!=(typeof b[r])[0];var e=a[r],n=(typeof e)[0];if(r>=b.length)return"u"==n;var t=b[r],f=(typeof t)[0];if(n!=f)return"o"==n&&"n"==f||("s"==f||"u"==n);if("o"!=n&&"u"!=n&&e!=t)return e<t;r++}
  /******/    }
  /******/    var rangeToString = (range) => {
  /******/      // see webpack/lib/util/semver.js for original code
  /******/      var r=range[0],n="";if(1===range.length)return"*";if(r+.5){n+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var e=1,a=1;a<range.length;a++){e--,n+="u"==(typeof(t=range[a]))[0]?"-":(e>0?".":"")+(e=2,t)}return n}var g=[];for(a=1;a<range.length;a++){var t=range[a];g.push(0===t?"not("+o()+")":1===t?"("+o()+" || "+o()+")":2===t?g.pop()+" "+g.pop():rangeToString(t))}return o();function o(){return g.pop().replace(/^((.+))$/,"$1")}
  /******/    }
  /******/    var satisfy = (range, version) => {
  /******/      // see webpack/lib/util/semver.js for original code
  /******/      if(0 in range){version=parseVersion(version);var e=range[0],r=e<0;r&&(e=-e-1);for(var n=0,i=1,a=!0;;i++,n++){var f,s,g=i<range.length?(typeof range[i])[0]:"";if(n>=version.length||"o"==(s=(typeof(f=version[n]))[0]))return!a||("u"==g?i>e&&!r:""==g!=r);if("u"==s){if(!a||"u"!=g)return!1}else if(a)if(g==s)if(i<=e){if(f!=range[i])return!1}else{if(r?f>range[i]:f<range[i])return!1;f!=range[i]&&(a=!1)}else if("s"!=g&&"n"!=g){if(r||i<=e)return!1;a=!1,i--}else{if(i<=e||s<g!=r)return!1;a=!1}else"s"!=g&&"n"!=g&&(a=!1,i--)}}var t=[],o=t.pop.bind(t);for(n=1;n<range.length;n++){var u=range[n];t.push(1==u?o()|o():2==u?o()&o():u?satisfy(u,version):!o())}return!!o();
  /******/    }
  /******/    var exists = (scope, key) => {
  /******/      return scope && __webpack_require__.o(scope, key);
  /******/    }
  /******/    var get = (entry) => {
  /******/      entry.loaded = 1;
  /******/      return entry.get()
  /******/    };
  /******/    var eagerOnly = (versions) => {
  /******/      return Object.keys(versions).reduce((filtered, version) => {
  /******/          if (versions[version].eager) {
  /******/            filtered[version] = versions[version];
  /******/          }
  /******/          return filtered;
  /******/      }, {});
  /******/    };
  /******/    var findLatestVersion = (scope, key, eager) => {
  /******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
  /******/      var key = Object.keys(versions).reduce((a, b) => {
  /******/        return !a || versionLt(a, b) ? b : a;
  /******/      }, 0);
  /******/      return key && versions[key];
  /******/    };
  /******/    var findSatisfyingVersion = (scope, key, requiredVersion, eager) => {
  /******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
  /******/      var key = Object.keys(versions).reduce((a, b) => {
  /******/        if (!satisfy(requiredVersion, b)) return a;
  /******/        return !a || versionLt(a, b) ? b : a;
  /******/      }, 0);
  /******/      return key && versions[key]
  /******/    };
  /******/    var findSingletonVersionKey = (scope, key, eager) => {
  /******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
  /******/      return Object.keys(versions).reduce((a, b) => {
  /******/        return !a || (!versions[a].loaded && versionLt(a, b)) ? b : a;
  /******/      }, 0);
  /******/    };
  /******/    var getInvalidSingletonVersionMessage = (scope, key, version, requiredVersion) => {
  /******/      return "Unsatisfied version " + version + " from " + (version && scope[key][version].from) + " of shared singleton module " + key + " (required " + rangeToString(requiredVersion) + ")"
  /******/    };
  /******/    var getInvalidVersionMessage = (scope, scopeName, key, requiredVersion, eager) => {
  /******/      var versions = scope[key];
  /******/      return "No satisfying version (" + rangeToString(requiredVersion) + ")" + (eager ? " for eager consumption" : "") + " of shared module " + key + " found in shared scope " + scopeName + ".\n" +
  /******/        "Available versions: " + Object.keys(versions).map((key) => {
  /******/        return key + " from " + versions[key].from;
  /******/      }).join(", ");
  /******/    };
  /******/    var fail = (msg) => {
  /******/      throw new Error(msg);
  /******/    }
  /******/    var failAsNotExist = (scopeName, key) => {
  /******/      return fail("Shared module " + key + " doesn't exist in shared scope " + scopeName);
  /******/    }
  /******/    var warn = /*#__PURE__*/ (msg) => {
  /******/      if (typeof console !== "undefined" && console.warn) console.warn(msg);
  /******/    };
  /******/    var init = (fn) => (function(scopeName, key, eager, c, d) {
  /******/      var promise = __webpack_require__.I(scopeName);
  /******/      if (promise && promise.then && !eager) {
  /******/        return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], key, false, c, d));
  /******/      }
  /******/      return fn(scopeName, __webpack_require__.S[scopeName], key, eager, c, d);
  /******/    });
  /******/    
  /******/    var useFallback = (scopeName, key, fallback) => {
  /******/      return fallback ? fallback() : failAsNotExist(scopeName, key);
  /******/    }
  /******/    var load = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      return get(findLatestVersion(scope, key, eager));
  /******/    });
  /******/    var loadVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
  /******/      if (satisfyingVersion) return get(satisfyingVersion);
  /******/      warn(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager))
  /******/      return get(findLatestVersion(scope, key, eager));
  /******/    });
  /******/    var loadStrictVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
  /******/      if (satisfyingVersion) return get(satisfyingVersion);
  /******/      if (fallback) return fallback();
  /******/      fail(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager));
  /******/    });
  /******/    var loadSingleton = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      var version = findSingletonVersionKey(scope, key, eager);
  /******/      return get(scope[key][version]);
  /******/    });
  /******/    var loadSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      var version = findSingletonVersionKey(scope, key, eager);
  /******/      if (!satisfy(requiredVersion, version)) {
  /******/        warn(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
  /******/      }
  /******/      return get(scope[key][version]);
  /******/    });
  /******/    var loadStrictSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
  /******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
  /******/      var version = findSingletonVersionKey(scope, key, eager);
  /******/      if (!satisfy(requiredVersion, version)) {
  /******/        fail(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
  /******/      }
  /******/      return get(scope[key][version]);
  /******/    });
  /******/    var installedModules = {};
  /******/    var moduleToHandlerMapping = {
  /******/      "webpack/sharing/consume/default/vue/vue": () => (loadSingletonVersion("default", "vue", false, [1,3,0,11], () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! vue */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js")))))))
  /******/    };
  /******/    // no consumes in initial chunks
  /******/    var chunkMapping = {
  /******/      "src_main_js": [
  /******/        "webpack/sharing/consume/default/vue/vue"
  /******/      ]
  /******/    };
  /******/    var startedInstallModules = {};
  /******/    __webpack_require__.f.consumes = (chunkId, promises) => {
  /******/      if(__webpack_require__.o(chunkMapping, chunkId)) {
  /******/        chunkMapping[chunkId].forEach((id) => {
  /******/          if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
  /******/          if(!startedInstallModules[id]) {
  /******/          var onFactory = (factory) => {
  /******/            installedModules[id] = 0;
  /******/            __webpack_require__.m[id] = (module) => {
  /******/              delete __webpack_require__.c[id];
  /******/              module.exports = factory();
  /******/            }
  /******/          };
  /******/          startedInstallModules[id] = true;
  /******/          var onError = (error) => {
  /******/            delete installedModules[id];
  /******/            __webpack_require__.m[id] = (module) => {
  /******/              delete __webpack_require__.c[id];
  /******/              throw error;
  /******/            }
  /******/          };
  /******/          try {
  /******/            var promise = moduleToHandlerMapping[id]();
  /******/            if(promise.then) {
  /******/              promises.push(installedModules[id] = promise.then(onFactory)['catch'](onError));
  /******/            } else onFactory(promise);
  /******/          } catch(e) { onError(e); }
  /******/          }
  /******/        });
  /******/      }
  /******/    }
  /******/  })();
  /******/  
  /******/  /* webpack/runtime/css loading */
  /******/  (() => {
  /******/    if (typeof document === "undefined") return;
  /******/    var createStylesheet = (chunkId, fullhref, oldTag, resolve, reject) => {
  /******/      var linkTag = document.createElement("link");
  /******/    
  /******/      linkTag.rel = "stylesheet";
  /******/      linkTag.type = "text/css";
  /******/      if (__webpack_require__.nc) {
  /******/        linkTag.nonce = __webpack_require__.nc;
  /******/      }
  /******/      var onLinkComplete = (event) => {
  /******/        // avoid mem leaks.
  /******/        linkTag.onerror = linkTag.onload = null;
  /******/        if (event.type === 'load') {
  /******/          resolve();
  /******/        } else {
  /******/          var errorType = event && event.type;
  /******/          var realHref = event && event.target && event.target.href || fullhref;
  /******/          var err = new Error("Loading CSS chunk " + chunkId + " failed.\n(" + errorType + ": " + realHref + ")");
  /******/          err.name = "ChunkLoadError";
  /******/          err.code = "CSS_CHUNK_LOAD_FAILED";
  /******/          err.type = errorType;
  /******/          err.request = realHref;
  /******/          if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag)
  /******/          reject(err);
  /******/        }
  /******/      }
  /******/      linkTag.onerror = linkTag.onload = onLinkComplete;
  /******/      linkTag.href = fullhref;
  /******/    
  /******/    
  /******/      if (oldTag) {
  /******/        oldTag.parentNode.insertBefore(linkTag, oldTag.nextSibling);
  /******/      } else {
  /******/        document.head.appendChild(linkTag);
  /******/      }
  /******/      return linkTag;
  /******/    };
  /******/    var findStylesheet = (href, fullhref) => {
  /******/      var existingLinkTags = document.getElementsByTagName("link");
  /******/      for(var i = 0; i < existingLinkTags.length; i++) {
  /******/        var tag = existingLinkTags[i];
  /******/        var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");
  /******/        if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return tag;
  /******/      }
  /******/      var existingStyleTags = document.getElementsByTagName("style");
  /******/      for(var i = 0; i < existingStyleTags.length; i++) {
  /******/        var tag = existingStyleTags[i];
  /******/        var dataHref = tag.getAttribute("data-href");
  /******/        if(dataHref === href || dataHref === fullhref) return tag;
  /******/      }
  /******/    };
  /******/    var loadStylesheet = (chunkId) => {
  /******/      return new Promise((resolve, reject) => {
  /******/        var href = __webpack_require__.miniCssF(chunkId);
  /******/        var fullhref = __webpack_require__.p + href;
  /******/        if(findStylesheet(href, fullhref)) return resolve();
  /******/        createStylesheet(chunkId, fullhref, null, resolve, reject);
  /******/      });
  /******/    }
  /******/    // object to store loaded CSS chunks
  /******/    var installedCssChunks = {
  /******/      "main": 0
  /******/    };
  /******/    
  /******/    __webpack_require__.f.miniCss = (chunkId, promises) => {
  /******/      var cssChunks = {"src_main_js":1};
  /******/      if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);
  /******/      else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {
  /******/        promises.push(installedCssChunks[chunkId] = loadStylesheet(chunkId).then(() => {
  /******/          installedCssChunks[chunkId] = 0;
  /******/        }, (e) => {
  /******/          delete installedCssChunks[chunkId];
  /******/          throw e;
  /******/        }));
  /******/      }
  /******/    };
  /******/    
  /******/    var oldTags = [];
  /******/    var newTags = [];
  /******/    var applyHandler = (options) => {
  /******/      return { dispose: () => {
  /******/        for(var i = 0; i < oldTags.length; i++) {
  /******/          var oldTag = oldTags[i];
  /******/          if(oldTag.parentNode) oldTag.parentNode.removeChild(oldTag);
  /******/        }
  /******/        oldTags.length = 0;
  /******/      }, apply: () => {
  /******/        for(var i = 0; i < newTags.length; i++) newTags[i].rel = "stylesheet";
  /******/        newTags.length = 0;
  /******/      } };
  /******/    }
  /******/    __webpack_require__.hmrC.miniCss = (chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList) => {
  /******/      applyHandlers.push(applyHandler);
  /******/      chunkIds.forEach((chunkId) => {
  /******/        var href = __webpack_require__.miniCssF(chunkId);
  /******/        var fullhref = __webpack_require__.p + href;
  /******/        var oldTag = findStylesheet(href, fullhref);
  /******/        if(!oldTag) return;
  /******/        promises.push(new Promise((resolve, reject) => {
  /******/          var tag = createStylesheet(chunkId, fullhref, oldTag, () => {
  /******/            tag.as = "style";
  /******/            tag.rel = "preload";
  /******/            resolve();
  /******/          }, reject);
  /******/          oldTags.push(oldTag);
  /******/          newTags.push(tag);
  /******/        }));
  /******/      });
  /******/    }
  /******/    
  /******/    // no prefetching
  /******/    
  /******/    // no preloaded
  /******/  })();
  /******/  
  /******/  /* webpack/runtime/jsonp chunk loading */
  /******/  (() => {
  /******/    // no baseURI
  /******/    
  /******/    // object to store loaded and loading chunks
  /******/    // undefined = chunk not loaded, null = chunk preloaded/prefetched
  /******/    // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
  /******/    var installedChunks = __webpack_require__.hmrS_jsonp = __webpack_require__.hmrS_jsonp || {
  /******/      "main": 0
  /******/    };
  /******/    
  /******/    __webpack_require__.f.j = (chunkId, promises) => {
  /******/        // JSONP chunk loading for javascript
  /******/        var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
  /******/        if(installedChunkData !== 0) { // 0 means "already installed".
  /******/    
  /******/          // a Promise means "currently loading".
  /******/          if(installedChunkData) {
  /******/            promises.push(installedChunkData[2]);
  /******/          } else {
  /******/            if(!/^webpack_container_remote_home_(Button|Content)$/.test(chunkId)) {
  /******/              // setup Promise in chunk cache
  /******/              var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
  /******/              promises.push(installedChunkData[2] = promise);
  /******/    
  /******/              // start chunk loading
  /******/              var url = __webpack_require__.p + __webpack_require__.u(chunkId);
  /******/              // create error before stack unwound to get useful stacktrace later
  /******/              var error = new Error();
  /******/              var loadingEnded = (event) => {
  /******/                if(__webpack_require__.o(installedChunks, chunkId)) {
  /******/                  installedChunkData = installedChunks[chunkId];
  /******/                  if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
  /******/                  if(installedChunkData) {
  /******/                    var errorType = event && (event.type === 'load' ? 'missing' : event.type);
  /******/                    var realSrc = event && event.target && event.target.src;
  /******/                    error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
  /******/                    error.name = 'ChunkLoadError';
  /******/                    error.type = errorType;
  /******/                    error.request = realSrc;
  /******/                    installedChunkData[1](error);
  /******/                  }
  /******/                }
  /******/              };
  /******/              __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
  /******/            } else installedChunks[chunkId] = 0;
  /******/          }
  /******/        }
  /******/    };
  /******/    
  /******/    // no prefetching
  /******/    
  /******/    // no preloaded
  /******/    

  /******/    
  /******/    self["webpackHotUpdatevue3_demo_layout"] = (chunkId, moreModules, runtime) => {
  /******/      for(var moduleId in moreModules) {
  /******/        if(__webpack_require__.o(moreModules, moduleId)) {
  /******/          currentUpdate[moduleId] = moreModules[moduleId];
  /******/          if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
  /******/        }
  /******/      }
  /******/      if(runtime) currentUpdateRuntime.push(runtime);
  /******/      if(waitingUpdateResolves[chunkId]) {
  /******/        waitingUpdateResolves[chunkId]();
  /******/        waitingUpdateResolves[chunkId] = undefined;
  /******/      }
  /******/    };
  /******/    

  /******/    
  /******/    // no on chunks loaded
  /******/    
  /******/    // install a JSONP callback for chunk loading
  /******/    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
  /******/      var [chunkIds, moreModules, runtime] = data;
  /******/      // add "moreModules" to the modules object,
  /******/      // then flag all "chunkIds" as loaded and fire callback
  /******/      var moduleId, chunkId, i = 0;
  /******/      if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
  /******/        for(moduleId in moreModules) {
  /******/          if(__webpack_require__.o(moreModules, moduleId)) {
  /******/            __webpack_require__.m[moduleId] = moreModules[moduleId];
  /******/          }
  /******/        }
  /******/        if(runtime) var result = runtime(__webpack_require__);
  /******/      }
  /******/      if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
  /******/      for(;i < chunkIds.length; i++) {
  /******/        chunkId = chunkIds[i];
  /******/        if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
  /******/          installedChunks[chunkId][0]();
  /******/        }
  /******/        installedChunks[chunkId] = 0;
  /******/      }
  /******/    
  /******/    }
  /******/    
  /******/    var chunkLoadingGlobal = self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || [];
  /******/    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
  /******/    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  /******/  })();
  /******/  var __webpack_exports__ = __webpack_require__("./src/index.js");
  /******/  
  /******/ })()
  ;
3.2.3.3 remoteEntry.js文件
var home;
/******/ (() => { // webpackBootstrap
/******/  var __webpack_modules__ = ({

/***/ "webpack/container/entry/home":
/*!***********************!*\
  !*** container entry ***!
  ***********************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {

"use strict";
var moduleMap = {
  "./Content": () => {
    return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_cddc0")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
  },
  "./Button": () => {
    return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
  }
};
var get = (module, getScope) => {
  __webpack_require__.R = getScope;
  getScope = (
    __webpack_require__.o(moduleMap, module)
      ? moduleMap[module]()
      : Promise.resolve().then(() => {
        throw new Error('Module "' + module + '" does not exist in container.');
      })
  );
  __webpack_require__.R = undefined;
  return getScope;
};
var init = (shareScope, initScope) => {
  if (!__webpack_require__.S) return;
  var name = "default"
  var oldScope = __webpack_require__.S[name];
  if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
  __webpack_require__.S[name] = shareScope;
  return __webpack_require__.I(name, initScope);
};

// This exports getters to disallow modifications
__webpack_require__.d(exports, {
  get: () => (get),
  init: () => (init)
});

/***/ })

/******/  });
/************************************************************************/
/******/  // The module cache
/******/  var __webpack_module_cache__ = {};
/******/  
/******/  // The require function
/******/  function __webpack_require__(moduleId) {
/******/    // Check if module is in cache
/******/    var cachedModule = __webpack_module_cache__[moduleId];
/******/    if (cachedModule !== undefined) {
/******/      return cachedModule.exports;
/******/    }
/******/    // Create a new module (and put it into the cache)
/******/    var module = __webpack_module_cache__[moduleId] = {
/******/      // no module.id needed
/******/      // no module.loaded needed
/******/      exports: {}
/******/    };
/******/  
/******/    // Execute the module function
/******/    var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
/******/    __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
/******/    module = execOptions.module;
/******/    execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
/******/  
/******/    // Return the exports of the module
/******/    return module.exports;
/******/  }
/******/  
/******/  // expose the modules object (__webpack_modules__)
/******/  __webpack_require__.m = __webpack_modules__;
/******/  
/******/  // expose the module cache
/******/  __webpack_require__.c = __webpack_module_cache__;
/******/  
/******/  // expose the module execution interceptor
/******/  __webpack_require__.i = [];
/******/  

/******/  
/******/  /* webpack/runtime/ensure chunk */
/******/  (() => {
/******/    __webpack_require__.f = {};
/******/    // This file contains only the entry chunk.
/******/    // The chunk loading function for additional chunks
/******/    __webpack_require__.e = (chunkId) => {
/******/      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/        __webpack_require__.f[key](chunkId, promises);
/******/        return promises;
/******/      }, []));
/******/    };
/******/  })();
/******/  
/******/  

/******/  
/******/  /* webpack/runtime/get mini-css chunk filename */
/******/  (() => {
/******/    // This function allow to reference async chunks
/******/    __webpack_require__.miniCssF = (chunkId) => {
/******/      // return url for filenames based on template
/******/      return undefined;
/******/    };
/******/  })();

/******/  
/******/  /* webpack/runtime/load script */
/******/  (() => {
/******/    var inProgress = {};
/******/    var dataWebpackPrefix = "vue3-demo_home:";
/******/    // loadScript function to load a script via script tag
/******/    __webpack_require__.l = (url, done, key, chunkId) => {
/******/      if(inProgress[url]) { inProgress[url].push(done); return; }
/******/      var script, needAttach;
/******/      if(key !== undefined) {
/******/        var scripts = document.getElementsByTagName("script");
/******/        for(var i = 0; i < scripts.length; i++) {
/******/          var s = scripts[i];
/******/          if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/        }
/******/      }
/******/      if(!script) {
/******/        needAttach = true;
/******/        script = document.createElement('script');
/******/    
/******/        script.charset = 'utf-8';
/******/        script.timeout = 120;
/******/        if (__webpack_require__.nc) {
/******/          script.setAttribute("nonce", __webpack_require__.nc);
/******/        }
/******/        script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/    
/******/        script.src = url;
/******/      }
/******/      inProgress[url] = [done];
/******/      var onScriptComplete = (prev, event) => {
/******/        // avoid mem leaks in IE.
/******/        script.onerror = script.onload = null;
/******/        clearTimeout(timeout);
/******/        var doneFns = inProgress[url];
/******/        delete inProgress[url];
/******/        script.parentNode && script.parentNode.removeChild(script);
/******/        doneFns && doneFns.forEach((fn) => (fn(event)));
/******/        if(prev) return prev(event);
/******/      }
/******/      var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/      script.onerror = onScriptComplete.bind(null, script.onerror);
/******/      script.onload = onScriptComplete.bind(null, script.onload);
/******/      needAttach && document.head.appendChild(script);
/******/    };
/******/  })();

/******/  
/******/  /* webpack/runtime/remotes loading */
/******/  (() => {
/******/    var chunkMapping = {};
/******/    var idToExternalAndNameMapping = {};
/******/    __webpack_require__.f.remotes = (chunkId, promises) => {
/******/      if(__webpack_require__.o(chunkMapping, chunkId)) {
/******/        chunkMapping[chunkId].forEach((id) => {
/******/          var getScope = __webpack_require__.R;
/******/          if(!getScope) getScope = [];
/******/          var data = idToExternalAndNameMapping[id];
/******/          if(getScope.indexOf(data) >= 0) return;
/******/          getScope.push(data);
/******/          if(data.p) return promises.push(data.p);
/******/          var onError = (error) => {
/******/            if(!error) error = new Error("Container missing");
/******/            if(typeof error.message === "string")
/******/              error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
/******/            __webpack_require__.m[id] = () => {
/******/              throw error;
/******/            }
/******/            data.p = 0;
/******/          };
/******/          var handleFunction = (fn, arg1, arg2, d, next, first) => {
/******/            try {
/******/              var promise = fn(arg1, arg2);
/******/              if(promise && promise.then) {
/******/                var p = promise.then((result) => (next(result, d)), onError);
/******/                if(first) promises.push(data.p = p); else return p;
/******/              } else {
/******/                return next(promise, d, first);
/******/              }
/******/            } catch(error) {
/******/              onError(error);
/******/            }
/******/          }
/******/          var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());
/******/          var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
/******/          var onFactory = (factory) => {
/******/            data.p = 1;
/******/            __webpack_require__.m[id] = (module) => {
/******/              module.exports = factory();
/******/            }
/******/          };
/******/          handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
/******/        });
/******/      }
/******/    }
/******/  })();
/******/  
/******/  /* webpack/runtime/sharing */
/******/  (() => {
/******/    __webpack_require__.S = {};
/******/    var initPromises = {};
/******/    var initTokens = {};
/******/    __webpack_require__.I = (name, initScope) => {
/******/      if(!initScope) initScope = [];
/******/      // handling circular init calls
/******/      var initToken = initTokens[name];
/******/      if(!initToken) initToken = initTokens[name] = {};
/******/      if(initScope.indexOf(initToken) >= 0) return;
/******/      initScope.push(initToken);
/******/      // only runs once
/******/      if(initPromises[name]) return initPromises[name];
/******/      // creates a new share scope if needed
/******/      if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
/******/      // runs all init snippets from all modules reachable
/******/      var scope = __webpack_require__.S[name];
/******/      var warn = (msg) => {
/******/        if (typeof console !== "undefined" && console.warn) console.warn(msg);
/******/      };
/******/      var uniqueName = "vue3-demo_home";
/******/      var register = (name, version, factory, eager) => {
/******/        var versions = scope[name] = scope[name] || {};
/******/        var activeVersion = versions[version];
/******/        if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
/******/      };
/******/      var initExternal = (id) => {
/******/        var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
/******/        try {
/******/          var module = __webpack_require__(id);
/******/          if(!module) return;
/******/          var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
/******/          if(module.then) return promises.push(module.then(initFn, handleError));
/******/          var initResult = initFn(module);
/******/          if(initResult && initResult.then) return promises.push(initResult['catch'](handleError));
/******/        } catch(err) { handleError(err); }
/******/      }
/******/      var promises = [];
/******/      switch(name) {
/******/        case "default": {
/******/          register("vue", "3.3.7", () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! ../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js"))))));
/******/        }
/******/        break;
/******/      }
/******/      if(!promises.length) return initPromises[name] = 1;
/******/      return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
/******/    };
/******/  })();
/******/  

/******/  /* webpack/runtime/consumes */
/******/  (() => {
/******/    var parseVersion = (str) => {
/******/      // see webpack/lib/util/semver.js for original code
/******/      var p=p=>{return p.split(".").map((p=>{return+p==p?+p:p}))},n=/^([^-+]+)?(?:-([^+]+))?(?:+(.+))?$/.exec(str),r=n[1]?p(n[1]):[];return n[2]&&(r.length++,r.push.apply(r,p(n[2]))),n[3]&&(r.push([]),r.push.apply(r,p(n[3]))),r;
/******/    }
/******/    var versionLt = (a, b) => {
/******/      // see webpack/lib/util/semver.js for original code
/******/      a=parseVersion(a),b=parseVersion(b);for(var r=0;;){if(r>=a.length)return r<b.length&&"u"!=(typeof b[r])[0];var e=a[r],n=(typeof e)[0];if(r>=b.length)return"u"==n;var t=b[r],f=(typeof t)[0];if(n!=f)return"o"==n&&"n"==f||("s"==f||"u"==n);if("o"!=n&&"u"!=n&&e!=t)return e<t;r++}
/******/    }
/******/    var rangeToString = (range) => {
/******/      // see webpack/lib/util/semver.js for original code
/******/      var r=range[0],n="";if(1===range.length)return"*";if(r+.5){n+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var e=1,a=1;a<range.length;a++){e--,n+="u"==(typeof(t=range[a]))[0]?"-":(e>0?".":"")+(e=2,t)}return n}var g=[];for(a=1;a<range.length;a++){var t=range[a];g.push(0===t?"not("+o()+")":1===t?"("+o()+" || "+o()+")":2===t?g.pop()+" "+g.pop():rangeToString(t))}return o();function o(){return g.pop().replace(/^((.+))$/,"$1")}
/******/    }
/******/    var satisfy = (range, version) => {
/******/      // see webpack/lib/util/semver.js for original code
/******/      if(0 in range){version=parseVersion(version);var e=range[0],r=e<0;r&&(e=-e-1);for(var n=0,i=1,a=!0;;i++,n++){var f,s,g=i<range.length?(typeof range[i])[0]:"";if(n>=version.length||"o"==(s=(typeof(f=version[n]))[0]))return!a||("u"==g?i>e&&!r:""==g!=r);if("u"==s){if(!a||"u"!=g)return!1}else if(a)if(g==s)if(i<=e){if(f!=range[i])return!1}else{if(r?f>range[i]:f<range[i])return!1;f!=range[i]&&(a=!1)}else if("s"!=g&&"n"!=g){if(r||i<=e)return!1;a=!1,i--}else{if(i<=e||s<g!=r)return!1;a=!1}else"s"!=g&&"n"!=g&&(a=!1,i--)}}var t=[],o=t.pop.bind(t);for(n=1;n<range.length;n++){var u=range[n];t.push(1==u?o()|o():2==u?o()&o():u?satisfy(u,version):!o())}return!!o();
/******/    }
/******/    var exists = (scope, key) => {
/******/      return scope && __webpack_require__.o(scope, key);
/******/    }
/******/    var get = (entry) => {
/******/      entry.loaded = 1;
/******/      return entry.get()
/******/    };
/******/    var eagerOnly = (versions) => {
/******/      return Object.keys(versions).reduce((filtered, version) => {
/******/          if (versions[version].eager) {
/******/            filtered[version] = versions[version];
/******/          }
/******/          return filtered;
/******/      }, {});
/******/    };
/******/    var findLatestVersion = (scope, key, eager) => {
/******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
/******/      var key = Object.keys(versions).reduce((a, b) => {
/******/        return !a || versionLt(a, b) ? b : a;
/******/      }, 0);
/******/      return key && versions[key];
/******/    };
/******/    var findSatisfyingVersion = (scope, key, requiredVersion, eager) => {
/******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
/******/      var key = Object.keys(versions).reduce((a, b) => {
/******/        if (!satisfy(requiredVersion, b)) return a;
/******/        return !a || versionLt(a, b) ? b : a;
/******/      }, 0);
/******/      return key && versions[key]
/******/    };
/******/    var findSingletonVersionKey = (scope, key, eager) => {
/******/      var versions = eager ? eagerOnly(scope[key]) : scope[key];
/******/      return Object.keys(versions).reduce((a, b) => {
/******/        return !a || (!versions[a].loaded && versionLt(a, b)) ? b : a;
/******/      }, 0);
/******/    };
/******/    var getInvalidSingletonVersionMessage = (scope, key, version, requiredVersion) => {
/******/      return "Unsatisfied version " + version + " from " + (version && scope[key][version].from) + " of shared singleton module " + key + " (required " + rangeToString(requiredVersion) + ")"
/******/    };
/******/    var getInvalidVersionMessage = (scope, scopeName, key, requiredVersion, eager) => {
/******/      var versions = scope[key];
/******/      return "No satisfying version (" + rangeToString(requiredVersion) + ")" + (eager ? " for eager consumption" : "") + " of shared module " + key + " found in shared scope " + scopeName + ".\n" +
/******/        "Available versions: " + Object.keys(versions).map((key) => {
/******/        return key + " from " + versions[key].from;
/******/      }).join(", ");
/******/    };
/******/    var fail = (msg) => {
/******/      throw new Error(msg);
/******/    }
/******/    var failAsNotExist = (scopeName, key) => {
/******/      return fail("Shared module " + key + " doesn't exist in shared scope " + scopeName);
/******/    }
/******/    var warn = /*#__PURE__*/ (msg) => {
/******/      if (typeof console !== "undefined" && console.warn) console.warn(msg);
/******/    };
/******/    var init = (fn) => (function(scopeName, key, eager, c, d) {
/******/      var promise = __webpack_require__.I(scopeName);
/******/      if (promise && promise.then && !eager) {
/******/        return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], key, false, c, d));
/******/      }
/******/      return fn(scopeName, __webpack_require__.S[scopeName], key, eager, c, d);
/******/    });
/******/    
/******/    var useFallback = (scopeName, key, fallback) => {
/******/      return fallback ? fallback() : failAsNotExist(scopeName, key);
/******/    }
/******/    var load = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      return get(findLatestVersion(scope, key, eager));
/******/    });
/******/    var loadVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
/******/      if (satisfyingVersion) return get(satisfyingVersion);
/******/      warn(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager))
/******/      return get(findLatestVersion(scope, key, eager));
/******/    });
/******/    var loadStrictVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
/******/      if (satisfyingVersion) return get(satisfyingVersion);
/******/      if (fallback) return fallback();
/******/      fail(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager));
/******/    });
/******/    var loadSingleton = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      var version = findSingletonVersionKey(scope, key, eager);
/******/      return get(scope[key][version]);
/******/    });
/******/    var loadSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      var version = findSingletonVersionKey(scope, key, eager);
/******/      if (!satisfy(requiredVersion, version)) {
/******/        warn(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
/******/      }
/******/      return get(scope[key][version]);
/******/    });
/******/    var loadStrictSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
/******/      if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
/******/      var version = findSingletonVersionKey(scope, key, eager);
/******/      if (!satisfy(requiredVersion, version)) {
/******/        fail(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
/******/      }
/******/      return get(scope[key][version]);
/******/    });
/******/    var installedModules = {};
/******/    var moduleToHandlerMapping = {
/******/      "webpack/sharing/consume/default/vue/vue": () => (loadSingletonVersion("default", "vue", false, [1,3,0,11], () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! vue */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js")))))))
/******/    };
/******/    // no consumes in initial chunks
/******/    var chunkMapping = {
/******/      "webpack_sharing_consume_default_vue_vue": [
/******/        "webpack/sharing/consume/default/vue/vue"
/******/      ]
/******/    };
/******/    var startedInstallModules = {};
/******/    __webpack_require__.f.consumes = (chunkId, promises) => {
/******/      if(__webpack_require__.o(chunkMapping, chunkId)) {
/******/        chunkMapping[chunkId].forEach((id) => {
/******/          if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
/******/          if(!startedInstallModules[id]) {
/******/          var onFactory = (factory) => {
/******/            installedModules[id] = 0;
/******/            __webpack_require__.m[id] = (module) => {
/******/              delete __webpack_require__.c[id];
/******/              module.exports = factory();
/******/            }
/******/          };
/******/          startedInstallModules[id] = true;
/******/          var onError = (error) => {
/******/            delete installedModules[id];
/******/            __webpack_require__.m[id] = (module) => {
/******/              delete __webpack_require__.c[id];
/******/              throw error;
/******/            }
/******/          };
/******/          try {
/******/            var promise = moduleToHandlerMapping[id]();
/******/            if(promise.then) {
/******/              promises.push(installedModules[id] = promise.then(onFactory)['catch'](onError));
/******/            } else onFactory(promise);
/******/          } catch(e) { onError(e); }
/******/          }
/******/        });
/******/      }
/******/    }
/******/  })();
/******/  

/******/  
/******/  /* webpack/runtime/jsonp chunk loading */
/******/  (() => {
/******/    // no baseURI
/******/    
/******/    // object to store loaded and loading chunks
/******/    // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/    // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/    var installedChunks = __webpack_require__.hmrS_jsonp = __webpack_require__.hmrS_jsonp || {
/******/      "home": 0
/******/    };
/******/    
/******/    __webpack_require__.f.j = (chunkId, promises) => {
/******/        // JSONP chunk loading for javascript
/******/        var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/        if(installedChunkData !== 0) { // 0 means "already installed".
/******/    
/******/          // a Promise means "currently loading".
/******/          if(installedChunkData) {
/******/            promises.push(installedChunkData[2]);
/******/          } else {
/******/            if("webpack_sharing_consume_default_vue_vue" != chunkId) {
/******/              // setup Promise in chunk cache
/******/              var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/              promises.push(installedChunkData[2] = promise);
/******/    
/******/              // start chunk loading
/******/              var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/              // create error before stack unwound to get useful stacktrace later
/******/              var error = new Error();
/******/              var loadingEnded = (event) => {
/******/                if(__webpack_require__.o(installedChunks, chunkId)) {
/******/                  installedChunkData = installedChunks[chunkId];
/******/                  if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/                  if(installedChunkData) {
/******/                    var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/                    var realSrc = event && event.target && event.target.src;
/******/                    error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/                    error.name = 'ChunkLoadError';
/******/                    error.type = errorType;
/******/                    error.request = realSrc;
/******/                    installedChunkData[1](error);
/******/                  }
/******/                }
/******/              };
/******/              __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/            } else installedChunks[chunkId] = 0;
/******/          }
/******/        }
/******/    };
/******/    
/******/    // no prefetching
/******/    
/******/    // no preloaded
/******/    
/******/    var currentUpdatedModulesList;
/******/    var waitingUpdateResolves = {};
/******/    function loadUpdateChunk(chunkId, updatedModulesList) {
/******/      currentUpdatedModulesList = updatedModulesList;
/******/      return new Promise((resolve, reject) => {
/******/        waitingUpdateResolves[chunkId] = resolve;
/******/        // start update chunk loading
/******/        var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
/******/        // create error before stack unwound to get useful stacktrace later
/******/        var error = new Error();
/******/        var loadingEnded = (event) => {
/******/          if(waitingUpdateResolves[chunkId]) {
/******/            waitingUpdateResolves[chunkId] = undefined
/******/            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/            var realSrc = event && event.target && event.target.src;
/******/            error.message = 'Loading hot update chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/            error.name = 'ChunkLoadError';
/******/            error.type = errorType;
/******/            error.request = realSrc;
/******/            reject(error);
/******/          }
/******/        };
/******/        __webpack_require__.l(url, loadingEnded);
/******/      });
/******/    }
/******/    
/******/    self["webpackHotUpdatevue3_demo_home"] = (chunkId, moreModules, runtime) => {
/******/      for(var moduleId in moreModules) {
/******/        if(__webpack_require__.o(moreModules, moduleId)) {
/******/          currentUpdate[moduleId] = moreModules[moduleId];
/******/          if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
/******/        }
/******/      }
/******/      if(runtime) currentUpdateRuntime.push(runtime);
/******/      if(waitingUpdateResolves[chunkId]) {
/******/        waitingUpdateResolves[chunkId]();
/******/        waitingUpdateResolves[chunkId] = undefined;
/******/      }
/******/    };

/******/    };

/******/    
/******/    
/******/    // no on chunks loaded
/******/    
/******/    // install a JSONP callback for chunk loading
/******/    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/      var [chunkIds, moreModules, runtime] = data;
/******/      // add "moreModules" to the modules object,
/******/      // then flag all "chunkIds" as loaded and fire callback
/******/      var moduleId, chunkId, i = 0;
/******/      if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/        for(moduleId in moreModules) {
/******/          if(__webpack_require__.o(moreModules, moduleId)) {
/******/            __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/          }
/******/        }
/******/        if(runtime) var result = runtime(__webpack_require__);
/******/      }
/******/      if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/      for(;i < chunkIds.length; i++) {
/******/        chunkId = chunkIds[i];
/******/        if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/          installedChunks[chunkId][0]();
/******/        }
/******/        installedChunks[chunkId] = 0;
/******/      }
/******/    
/******/    }
/******/    
/******/    var chunkLoadingGlobal = self["webpackChunkvue3_demo_home"] = self["webpackChunkvue3_demo_home"] || [];
/******/    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/  })();
/******/  
/************************************************************************/
/******/  
/******/  // module cache are used so entry inlining is disabled
/******/  // startup
/******/  // Load entry module and return exports
/******/  __webpack_require__("../../node_modules/.pnpm/webpack-dev-server@5.0.4_webpack-cli@5.1.4_webpack@5.96.1/node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=3002&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true");
/******/  __webpack_require__("../../node_modules/.pnpm/webpack@5.96.1_@swc+core@1.9.2_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js");
/******/  var __webpack_exports__ = __webpack_require__("webpack/container/entry/home");
/******/  home = __webpack_exports__;
/******/  
/******/ })()
;
3.2.3.4 main.js文件

<!---->

3.2.3.4.1 源码
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';

const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');
3.2.3.4.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || []).push([["src_main_js"],{

/***/ "./src/main.js":
/*!*********************!*\
  !*** ./src/main.js ***!
  *********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ "webpack/sharing/consume/default/vue/vue");
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(vue__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _Layout_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Layout.vue */ "./src/Layout.vue");

const Content = (0,vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(() => __webpack_require__.e(/*! import() */ "webpack_container_remote_home_Content").then(__webpack_require__.t.bind(__webpack_require__, /*! home/Content */ "webpack/container/remote/home/Content", 23)));
const Button = (0,vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(() => __webpack_require__.e(/*! import() */ "webpack_container_remote_home_Button").then(__webpack_require__.t.bind(__webpack_require__, /*! home/Button */ "webpack/container/remote/home/Button", 23)));

const app = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_Layout_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');

/***/ })

}]);

3.2.4 过程解析

<!---->

3.2.4.1 webpack异步模块的加载流程前置

前面流程跟webpack异步模块的加载流程相同,简单过一下

  1. 通过\_\_webpack\_require\_\_("./src/index.js"),方法,同步加载入口文件index.js
  2. 执行index.js里面异步加载'src/main.js'文件的逻辑,具体执行__webpack_require__.e函数,
  3. 执行并遍历\_webpack\_require\_\_.f对象上挂载的所有方法。

到遍历执行\_webpack\_require\_\_.f上所有方法时,开始不同了,

多了\_\_webpack\_require\_\_.f.remotes 和\_\_webpack\_require\_\_.f.resumes方法,

__webpack_require__.f.resumes
__webpack_require__.f.remotes
__webpack_require__.f.MiniCss
__webpack_require__.f.l
3.2.4.2 webpack\_require.f.consumes
var parseVersion = (str) => {
        // see webpack/lib/util/semver.js for original code
        var p=p=>{return p.split(".").map((p=>{return+p==p?+p:p}))},n=/^([^-+]+)?(?:-([^+]+))?(?:+(.+))?$/.exec(str),r=n[1]?p(n[1]):[];return n[2]&&(r.length++,r.push.apply(r,p(n[2]))),n[3]&&(r.push([]),r.push.apply(r,p(n[3]))),r;
}
var versionLt = (a, b) => {
        // see webpack/lib/util/semver.js for original code
        a=parseVersion(a),b=parseVersion(b);for(var r=0;;){if(r>=a.length)return r<b.length&&"u"!=(typeof b[r])[0];var e=a[r],n=(typeof e)[0];if(r>=b.length)return"u"==n;var t=b[r],f=(typeof t)[0];if(n!=f)return"o"==n&&"n"==f||("s"==f||"u"==n);if("o"!=n&&"u"!=n&&e!=t)return e<t;r++}
}
var rangeToString = (range) => {
        // see webpack/lib/util/semver.js for original code
        var r=range[0],n="";if(1===range.length)return"*";if(r+.5){n+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var e=1,a=1;a<range.length;a++){e--,n+="u"==(typeof(t=range[a]))[0]?"-":(e>0?".":"")+(e=2,t)}return n}var g=[];for(a=1;a<range.length;a++){var t=range[a];g.push(0===t?"not("+o()+")":1===t?"("+o()+" || "+o()+")":2===t?g.pop()+" "+g.pop():rangeToString(t))}return o();function o(){return g.pop().replace(/^((.+))$/,"$1")}
}
var satisfy = (range, version) => {
        // see webpack/lib/util/semver.js for original code
        if(0 in range){version=parseVersion(version);var e=range[0],r=e<0;r&&(e=-e-1);for(var n=0,i=1,a=!0;;i++,n++){var f,s,g=i<range.length?(typeof range[i])[0]:"";if(n>=version.length||"o"==(s=(typeof(f=version[n]))[0]))return!a||("u"==g?i>e&&!r:""==g!=r);if("u"==s){if(!a||"u"!=g)return!1}else if(a)if(g==s)if(i<=e){if(f!=range[i])return!1}else{if(r?f>range[i]:f<range[i])return!1;f!=range[i]&&(a=!1)}else if("s"!=g&&"n"!=g){if(r||i<=e)return!1;a=!1,i--}else{if(i<=e||s<g!=r)return!1;a=!1}else"s"!=g&&"n"!=g&&(a=!1,i--)}}var t=[],o=t.pop.bind(t);for(n=1;n<range.length;n++){var u=range[n];t.push(1==u?o()|o():2==u?o()&o():u?satisfy(u,version):!o())}return!!o();
}
var exists = (scope, key) => {
        return scope && __webpack_require__.o(scope, key);
}
var get = (entry) => {
        entry.loaded = 1;
        return entry.get()
};
var eagerOnly = (versions) => {
        return Object.keys(versions).reduce((filtered, version) => {
                        if (versions[version].eager) {
                                filtered[version] = versions[version];
                        }
                        return filtered;
        }, {});
};
var findLatestVersion = (scope, key, eager) => {
        var versions = eager ? eagerOnly(scope[key]) : scope[key];
        var key = Object.keys(versions).reduce((a, b) => {
                return !a || versionLt(a, b) ? b : a;
        }, 0);
        return key && versions[key];
};
var findSatisfyingVersion = (scope, key, requiredVersion, eager) => {
        var versions = eager ? eagerOnly(scope[key]) : scope[key];
        var key = Object.keys(versions).reduce((a, b) => {
                if (!satisfy(requiredVersion, b)) return a;
                return !a || versionLt(a, b) ? b : a;
        }, 0);
        return key && versions[key]
};
var findSingletonVersionKey = (scope, key, eager) => {
        var versions = eager ? eagerOnly(scope[key]) : scope[key];
        return Object.keys(versions).reduce((a, b) => {
                return !a || (!versions[a].loaded && versionLt(a, b)) ? b : a;
        }, 0);
};
var getInvalidSingletonVersionMessage = (scope, key, version, requiredVersion) => {
        return "Unsatisfied version " + version + " from " + (version && scope[key][version].from) + " of shared singleton module " + key + " (required " + rangeToString(requiredVersion) + ")"
};
var getInvalidVersionMessage = (scope, scopeName, key, requiredVersion, eager) => {
        var versions = scope[key];
        return "No satisfying version (" + rangeToString(requiredVersion) + ")" + (eager ? " for eager consumption" : "") + " of shared module " + key + " found in shared scope " + scopeName + ".\n" +
                "Available versions: " + Object.keys(versions).map((key) => {
                return key + " from " + versions[key].from;
        }).join(", ");
};
var fail = (msg) => {
        throw new Error(msg);
}
var failAsNotExist = (scopeName, key) => {
        return fail("Shared module " + key + " doesn't exist in shared scope " + scopeName);
}
var warn = /*#__PURE__*/ (msg) => {
        if (typeof console !== "undefined" && console.warn) console.warn(msg);
};
var init = (fn) => (function(scopeName, key, eager, c, d) {
        var promise = __webpack_require__.I(scopeName);
        if (promise && promise.then && !eager) {
                return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], key, false, c, d));
        }
        return fn(scopeName, __webpack_require__.S[scopeName], key, eager, c, d);
});

var useFallback = (scopeName, key, fallback) => {
        return fallback ? fallback() : failAsNotExist(scopeName, key);
}
var load = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        return get(findLatestVersion(scope, key, eager));
});
var loadVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
        if (satisfyingVersion) return get(satisfyingVersion);
        warn(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager))
        return get(findLatestVersion(scope, key, eager));
});
var loadStrictVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
        if (satisfyingVersion) return get(satisfyingVersion);
        if (fallback) return fallback();
        fail(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager));
});
var loadSingleton = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var version = findSingletonVersionKey(scope, key, eager);
        return get(scope[key][version]);
});
var loadSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var version = findSingletonVersionKey(scope, key, eager);
        if (!satisfy(requiredVersion, version)) {
                warn(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
        }
        return get(scope[key][version]);
});
var loadStrictSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var version = findSingletonVersionKey(scope, key, eager);
        if (!satisfy(requiredVersion, version)) {
                fail(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
        }
        return get(scope[key][version]);
});
var installedModules = {};
var moduleToHandlerMapping = {
        "webpack/sharing/consume/default/vue/vue": () => (loadSingletonVersion("default", "vue", false, [1,3,0,11], () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! vue */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js")))))))
};
// no consumes in initial chunks
// 模块依赖的共享依赖
var chunkMapping = {
        "src_main_js": [
                "webpack/sharing/consume/default/vue/vue"
        ]
};
var startedInstallModules = {};
__webpack_require__.f.consumes = (chunkId, promises) => {
        if(__webpack_require__.o(chunkMapping, chunkId)) {
                chunkMapping[chunkId].forEach((id) => {
                        if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
                        if(!startedInstallModules[id]) {
                        var onFactory = (factory) => {
                                installedModules[id] = 0;
                                __webpack_require__.m[id] = (module) => {
                                        delete __webpack_require__.c[id];
                                        module.exports = factory();
                                }
                        };
                        startedInstallModules[id] = true;
                        var onError = (error) => {
                                delete installedModules[id];
                                __webpack_require__.m[id] = (module) => {
                                        delete __webpack_require__.c[id];
                                        throw error;
                                }
                        };
                        try {
                                var promise = moduleToHandlerMapping[id]();
                                if(promise.then) {
                                        promises.push(installedModules[id] = promise.then(onFactory)['catch'](onError));
                                } else onFactory(promise);
                        } catch(e) { onError(e); }
                        }
                });
        }
}

项目里面shared 共享依赖,所以在执行到__webpack_require__.f.consumes开始检查模块是否有对应的共享依赖,这里可以发现就是webpack/sharing/consume/default/vue/vue

接着从moduleToHandlerMapping 对象里面,寻找共享依赖的加载和版本对比逻辑, 通过loadSingletonVersion 函数进行加载,主要我们在配置中设置了singleton: true

var moduleToHandlerMapping = {
        "webpack/sharing/consume/default/vue/vue": 
        () => (loadSingletonVersion("default", "vue", false, [1,3,0,11], () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! vue */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js")))))))
};

<!---->

var loadSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
        if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
        var version = findSingletonVersionKey(scope, key, eager);
        if (!satisfy(requiredVersion, version)) {
                warn(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
        }
        return get(scope[key][version]);
});

在执行 loadSingletonVersion之前,首先要执行了 init方法,

var init = (fn) => (function(scopeName, key, eager, c, d) {
        var promise = __webpack_require__.I(scopeName);
        if (promise && promise.then && !eager) {
                return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], key, false, c, d));
        }
        return fn(scopeName, __webpack_require__.S[scopeName], key, eager, c, d);
});

继续走到init方法,里面有__webpack_require__.I,接着看I函数,它是一个独立的模块,在runtime/share

3.2.4.3 webpack\_require.I
__webpack_require__.S = {};
var initPromises = {};
var initTokens = {};
__webpack_require__.I = (name, initScope) => {
        if(!initScope) initScope = [];
        // handling circular init calls
        var initToken = initTokens[name];
        if(!initToken) initToken = initTokens[name] = {};
        if(initScope.indexOf(initToken) >= 0) return;
        initScope.push(initToken);
        // only runs once
        if(initPromises[name]) return initPromises[name];
        // creates a new share scope if needed
        if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
        // runs all init snippets from all modules reachable
        var scope = __webpack_require__.S[name];
        var warn = (msg) => {
                if (typeof console !== "undefined" && console.warn) console.warn(msg);
        };
        var uniqueName = "vue3-demo_layout";
        var register = (name, version, factory, eager) => {
                var versions = scope[name] = scope[name] || {};
                var activeVersion = versions[version];
                if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
        };
        var initExternal = (id) => {
                var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
                try {
                        var module = __webpack_require__(id);
                        if(!module) return;
                        var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
                        if(module.then) return promises.push(module.then(initFn, handleError));
                        var initResult = initFn(module);
                        if(initResult && initResult.then) return promises.push(initResult['catch'](handleError));
                } catch(err) { handleError(err); }
        }
        var promises = [];
        switch(name) {
                case "default": {
                        register("vue", "3.3.7", () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! ../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js"))))));
                        initExternal("webpack/container/reference/home");
                }
                break;
        }
        if(!promises.length) return initPromises[name] = 1;
        return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
};

执行到register里面,可以看到还是将共享依赖,包括具体版本号,设置到了全局变量__webpack_require__.S上面,数据结构就是

__webpack_require__.S[version] = { get: factory, from: uniqueName, eager: !!eager }

里面包括了依赖的

  • 版本号3.3.7,
  • from:vue3-demo\_layout 来源、
  • Get: 获取方法 () => (__webpack_require__.e("vendors-

具体到我们的demo,可以看一下

{
    "default": {
    "vue": {
        "3.3.7": {
            "from": "vue3-demo_layout",
            "eager": false
            // get 包括了具体vue如何请求到
            get: () => (__webpack_require__.e("vendors-node_modules_pnpm_vue_3_3_7_typescript_5_6_3_node_modules_vue_dist_vue_runtime_esm-bu-3fdf17").then(() => (() => (__webpack_require__(/*! ../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js */ "../../node_modules/.pnpm/vue@3.3.7_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js")))))
        }
    }
}
}

<!---->

__webpack_require__ .S

register后,接着就是加载远程应用

__webpack_require__(webpack/container/reference/home),这个模块在前面已经注册过了

{
/***/ "webpack/container/reference/home":
/*!************************************************************!*\
  !*** external "home@http://localhost:3002/remoteEntry.js" ***!
  ************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {

"use strict";
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
        if(typeof home !== "undefined") return resolve();
        __webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {
                if(typeof home !== "undefined") return resolve();
                var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                var realSrc = event && event.target && event.target.src;
                __webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
                __webpack_error__.name = 'ScriptExternalLoadError';
                __webpack_error__.type = errorType;
                __webpack_error__.request = realSrc;
                reject(__webpack_error__);
        }, "home");
}).then(() => (home));

/***/ }

其实通过__webpack_require__.l 函数,加载远程模块入口文件http://localhost:3002/remoteEntry.js

var moduleMap = {
        "./Content": () => {
                return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_cddc0")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
        },
        "./Button": () => {
                return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
        }
};

var init = (shareScope, initScope) => {
        if (!__webpack_require__.S) return;
        var name = "default"
        var oldScope = __webpack_require__.S[name];
        if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
        __webpack_require__.S[name] = shareScope;
        return __webpack_require__.I(name, initScope);
};
var get = (module, getScope) => {
        __webpack_require__.R = getScope;
        getScope = (
                __webpack_require__.o(moduleMap, module)
                        ? moduleMap[module]()
                        : Promise.resolve().then(() => {
                                throw new Error('Module "' + module + '" does not exist in container.');
                        })
        );
        __webpack_require__.R = undefined;
        return getScope;
};

// This exports getters to disallow modifications
__webpack_require__.d(exports, {
        get: () => (get),
        init: () => (init)
});

加载完成后,执行加载模块remoteEntry.js里面暴露的init方法, 把当前上下文中的__webpack_require__.S和initScope作为参数

var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))

接下来执行init函数时,需要明确一下是在remoteEntry.js这个文件上下文中执行的。里面也有一套同样的webpack模块加载函数和存储变量,虽然命名一样。

init方法里面,通过内部\_\_webpack\_require\_\_.S进行存储共享依赖,name也是对应'default',直接复用前面注册在S上的vue依赖,再次调用\_\_webpack\_require.I\_\_, 注册自己的依赖。

__webpack_require__.S[name] = shareScope;

里面的这段很重要shareScope是在host应用中把__webpack_require__.S[name]作为参数传入的,本质上是src/main.js(host)文件和remoteEntry.js(remote)文件之间,通过全局变量,传递各自上下文中的内部变量(__webpack_require__.S)上的共享依赖。

所以等价于

remote___webpack_require__.S[name] = host___webpack_require__.S[name]

消费者和生产者里面注册的模块是同一个所以注册完成后\_\_webpack\_require\_\_.S里面还是只有一个3.3.7版本的vue

到这里共享依赖初始化完成了。

3.2.4.4 webpack\_require.f.j

继续执行f上挂载的方法remoes,此时src/main.js模块,没有在chunkMapping里面没有映射到对应的共享依赖,直接跳过了remotes方法,继续走后面的处理逻辑,这里可以直接跳过,最后通过\_\_webpack\_require\_\_.f.j 函数加载src/main.js文件。同前面异步加载逻辑一样,放到\_\_webpack\_module\_\_变量里面,

{
  /***/ "./src/main.js":
/*!*********************!*\
  !*** ./src/main.js ***!
  *********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ "webpack/sharing/consume/default/vue/vue");
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(vue__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _Layout_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Layout.vue */ "./src/Layout.vue");



const Content = (0,vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(() => __webpack_require__.e(/*! import() */ "webpack_container_remote_home_Content").then(__webpack_require__.t.bind(__webpack_require__, /*! home/Content */ "webpack/container/remote/home/Content", 23)));
const Button = (0,vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(() => __webpack_require__.e(/*! import() */ "webpack_container_remote_home_Button").then(__webpack_require__.t.bind(__webpack_require__, /*! home/Button */ "webpack/container/remote/home/Button", 23)));

const app = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_Layout_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');


/***/ })
}

main.js里面引用Content和Button组件,构建时转换成 __webpack_require__.e(/*! import() */ "webpack_container_remote_home_Content")函数,跟[前面的src\_main\_js加载类似],开始遍历执行\_\_webpack\_require\_\_.f上的挂载方法,执行到\_\_webpack\_require\_\_.f.consumes时,没有映射到共享依赖跳过,继续执行到__webpack_require__.f.remotes

3.2.4.5 webpack\_require.f.remotes
var chunkMapping = {
        "webpack_container_remote_home_Content": [
                "webpack/container/remote/home/Content"
        ],
        "webpack_container_remote_home_Button": [
                "webpack/container/remote/home/Button"
        ]
};
var idToExternalAndNameMapping = {
        "webpack/container/remote/home/Content": [
                "default",
                "./Content",
                "webpack/container/reference/home"
        ],
        "webpack/container/remote/home/Button": [
                "default",
                "./Button",
                "webpack/container/reference/home"
        ]
};
__webpack_require__.f.remotes = (chunkId, promises) => {
        if(__webpack_require__.o(chunkMapping, chunkId)) {
                chunkMapping[chunkId].forEach((id) => {
                        var getScope = __webpack_require__.R;
                        if(!getScope) getScope = [];
                        var data = idToExternalAndNameMapping[id];
                        if(getScope.indexOf(data) >= 0) return;
                        getScope.push(data);
                        if(data.p) return promises.push(data.p);
                        var onError = (error) => {
                                if(!error) error = new Error("Container missing");
                                if(typeof error.message === "string")
                                        error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
                                __webpack_require__.m[id] = () => {
                                        throw error;
                                }
                                data.p = 0;
                        };
                        var handleFunction = (fn, arg1, arg2, d, next, first) => {
                                try {
                                        var promise = fn(arg1, arg2);
                                        if(promise && promise.then) {
                                                var p = promise.then((result) => (next(result, d)), onError);
                                                if(first) promises.push(data.p = p); else return p;
                                        } else {
                                                return next(promise, d, first);
                                        }
                                } catch(error) {
                                        onError(error);
                                }
                        }
                        var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());
                        var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
                        var onFactory = (factory) => {
                                data.p = 1;
                                __webpack_require__.m[id] = (module) => {
                                        module.exports = factory();
                                }
                        };
                        handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
                });
        }
}

可以发现webpack_container_remote_home_Content在chunkMapping里面已经声明了,chunkMapping保存依赖的远程应用具体模块id,idToExternalAndNameMapping则保存远程模块的具体信息,看最后的执行入口函数

handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);

      var handleFunction = (fn, arg1, arg2, d, next, first) => {
                                try {
                                        var promise = fn(arg1, arg2);
                                        if(promise && promise.then) {
                                                var p = promise.then((result) => (next(result, d)), onError);
                                                if(first) promises.push(data.p = p); else return p;
                                        } else {
                                                return next(promise, d, first);
                                        }
                                } catch(error) {
                                        onError(error);
                                }
                        }

data[2]表示依赖的远程应用资源 "webpack/container/reference/home",直接执行

var promise = fn(arg1, arg2); 
=>
__webpack_require__("webpack/container/reference/home", 0)

这个require在前面已经执行完成了,主要请求remoteEntry.js文件,存在缓存,直接从__webpack_module_cache__里面获取执行结果,也就是export上抛出的get和init方法。

正在promise.then方法里面,执行传入的next函数,也就是第5个参数onExternal

    var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());

继续执行handleFunction函数

handleFunction(__webpack_require__.I, 'default', 0, external, onInitialized, first)

<!---->

var promise = fn(arg1, arg2); 
=>
__webpack_require__.I('default', 0),
就是前面在consumes方法里面,寻找并注册共享依赖,执行完成后继续执行next方法,也就是第5个参数onInitialized

<!---->

var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));

<!---->

handleFunction(external.get, './Content', getScope, 0, onFactory, first )

这里external就是第一次执行\_\_webpack\_require\_\_("webpack/container/reference/home", 0),返回的promise结果,也就是远程应用remoteEntry.js里面的内容

回到remoteEntry.js里面,可以发现get方法

var moduleMap = {
        "./Content": () => {
                return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_cddc0")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
        },
        "./Button": () => {
                return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
        }
};
var get = (module, getScope) => {
        __webpack_require__.R = getScope;
        getScope = (
                __webpack_require__.o(moduleMap, module)
                        ? moduleMap[module]()
                        : Promise.resolve().then(() => {
                                throw new Error('Module "' + module + '" does not exist in container.');
                        })
        );
        __webpack_require__.R = undefined;
        return getScope;
};

moduleMap['./Content'](),这里同样有__webpack_require__.e,但在是在远程应用的执行上下文里面,跟当前主应用的执行环境不同,这里是远程应用根据自己的webpack配置生成的webpack\_require相关函数,也是真正打包构建‘./Content’共享组件的地方。

继续递归,这个get方法执行完成后,promise.then里面的next方法,也就是onFactory函数,

  var onFactory = (factory) => {
                                data.p = 1;
                                __webpack_require__.m[id] = (module) => {
                                        module.exports = factory();
                                }
                        };
                        
                        
                        
__webpack_require__.m = __webpack_modules__;

把获取到的./Content函数主体,从远程应用的上下文,重新赋值到当前host应用的\_\_webpack\_modules\_\_变量下,

后续再遇到引入./Content组件的场景,直接从\_\_webpack\_modules\_\_上获取,

[回到src\_main\_js的实现部分],继续引入Button.js组件,执行后续操作

3.2.5 整体组件加载执行过程

image-6.png

3.3 Module Federation Runtime API 加载流程

3.3.1 概述

官方提供的Runtime API进行模块注册和加载,很大程度上打破了构建工具的限制,在一些老版本的项目上,也可以直接使用共享模块。从构建插件转向自定义的API提供加载,同时提供了更多的自定义配置和生命钩子函数。

深入理解API模型加载,可以发现实际上面模拟了前面构建时加载里面的动态创建script标签和通过全局的S变量进行共享库传递等核心功能。

3.3.2 初始化请求链路

跟前面module federation构建时加载流程有一点区别,remoteEntry.js文件的请求发起者是前置的基础库,也就是@module-federation/runtime-core。这是因为修改成Runtime API形式,使用官方库提供的loadRemote方法动态加载组件,而构建版本通过插件,在构建时已经将发起请求的逻辑注入到了前置的main.js文件中了。

这里主要关注host,也就是消费者(Layout)应用,在使用官方提供的API动态加载时的流程,所以在host应用中去掉webpack配置文件中的ModuleFederationPlugin 插件。

  //const { ModuleFederationPlugin } = require('webpack').container;
  
  ...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    // new ModuleFederationPlugin({
    //   name: 'layout',
    //   filename: 'remoteEntry.js',
    //   remotes: {
    //     home: 'home@http://localhost:3002/remoteEntry.js',
    //   },
    //   exposes: {},
    //   shared: {
    //     vue: {
    //       singleton: true,
    //     },
    //   },
    // }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './index.html'),
      chunks: ['main'],
    }),
    new VueLoaderPlugin(),
  ],

在入口main.js里面修改引入远程的组件的方法(红色部分为改动)

用了init和loadRemote两个Runtime API

import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
import { init, loadRemote } from '@module-federation/runtime';
init({
    name: 'layout',
    remotes: [
      {
        name: 'home',
        entry: 'http://localhost:3002/remoteEntry.js',
      },
    ],
    shared: {
      vue: {
        singleton: true,
      },
    },
  });

const Content = defineAsyncComponent(async () => await loadRemote('home/Content'));
const Button = defineAsyncComponent(async () => await loadRemote('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');

3.3.3 源码与构建后代码对照

大部分代码跟构建时类似,只有在正式初始化和加载远程组件时,需要调用前置加载的module-federation/runtime-core基础库

3.3.3.1 依赖库

http://localhost:3001/vendors-node_modules_pnpm_mini-css-extr...

 
 "use strict";
(self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || []).push([["vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2__swc_h-8958a1"],{
 "../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js":
/*!****************************************************************************************************************************!*\
  !*** ../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js ***!
  ****************************************************************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   Module: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.Module),
/* harmony export */   ModuleFederation: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.ModuleFederation),
/* harmony export */   createInstance: () => (/* binding */ createInstance),
/* harmony export */   getInstance: () => (/* binding */ getInstance),
/* harmony export */   getRemoteEntry: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.getRemoteEntry),
/* harmony export */   getRemoteInfo: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.getRemoteInfo),
/* harmony export */   init: () => (/* binding */ init),
/* harmony export */   loadRemote: () => (/* binding */ loadRemote),
/* harmony export */   loadScript: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.loadScript),
/* harmony export */   loadScriptNode: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.loadScriptNode),
/* harmony export */   loadShare: () => (/* binding */ loadShare),
/* harmony export */   loadShareSync: () => (/* binding */ loadShareSync),
/* harmony export */   preloadRemote: () => (/* binding */ preloadRemote),


...
})
3.3.3.2 main.js文件
3.3.3.2.1 源码
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
import { init, loadRemote } from '@module-federation/runtime';
init({
    name: 'layout',
    remotes: [
      {
        name: 'home',
        entry: 'http://localhost:3002/remoteEntry.js',
      },
    ],
    shared: {
      vue: {
        singleton: true,
      },
    },
  });

const Content = defineAsyncComponent(async () => await loadRemote('home/Content'));
const Button = defineAsyncComponent(async () => await loadRemote('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');
3.3.3.2.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || []).push([["src_main_js"], {

 /***/
    "./src/main.js": /*!*********************!*\
  !*** ./src/main.js ***!
  *********************/
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        /* harmony import */
        var _Layout_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Layout.vue */
        "./src/Layout.vue");
        /* harmony import */
        var _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @module-federation/runtime */
        "../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js");

        (0,
        _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.init)({
            name: 'layout',
            remotes: [{
                name: 'home',
                entry: 'http://localhost:3002/remoteEntry.js',
            }, ],
            shared: {
                vue: {
                    singleton: true,
                },
            },
        });

        const Content = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
        _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Content'));
        const Button = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
        _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Button'));

        const app = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_Layout_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);

        app.component('content-element', Content);
        app.component('button-element', Button);

        app.mount('#app');

        /***/
    }
    )

}]);

3.3.4 过程分析

分析这里的init和loadRemote API就不用从入口文件加载开始,按照顺序分析,重点关注方法本身就行,也就是@module-federation/runtime里面的代码

还是看一下原始的main.js里面,init和loadRemote方法构建后源码;

"./src/main.js": /*!*********************!*\
    /***/
    ( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        __webpack_require__.r(__webpack_exports__);
        /* harmony import */

        /* harmony import */
        var _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @module-federation/runtime */
        "../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js");

        (0,
        _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.init)({
            name: 'layout',
            remotes: [{
                name: 'home',
                entry: 'http://localhost:3002/remoteEntry.js',
            }, ],
            shared: {
                vue: {
                    singleton: true,
                },
            },
        });

        const Content = (0,
        vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
        _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Content'));
        
        /***/
    }
    )

提前获取@module-federation/runtime module,这个module在前置的公共chunk里面,也就是demo里面对应的http://localhost:3001/vendors-node_modules_pnpm_mini-css-extr... 文件。

异步模块加载的流程这里就不细讲了,聚焦到@module-federation/runtime仓库

3.3.4.1 init方法

<!---->

3.3.4.1.1 传入参数

查看源码位置:core/packages/runtime/src/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federation/c

export function createInstance(options: UserOptions) {
  // Retrieve debug constructor
  const ModuleFederationConstructor =
    getGlobalFederationConstructor() || ModuleFederation;
  const instance = new ModuleFederationConstructor(options);
  setGlobalFederationInstance(instance);
  return instance;
}


let FederationInstance: ModuleFederation | null = null;
/**
 * @deprecated Use createInstance or getInstance instead
 */
export function init(options: UserOptions): ModuleFederation {
  // Retrieve the same instance with the same name
  const instance = getGlobalFederationInstance(options.name, options.version);
  if (!instance) {
    FederationInstance = createInstance(options);
    return FederationInstance;
  } else {
    // Merge options
    instance.initOptions(options);
    if (!FederationInstance) {
      FederationInstance = instance;
    }
    return instance;
  }
}

官方文档里面推荐使用createInstance方法取代init.

init方法只做 一次单实例的前置校验,通过getGlobalFederationInstance方法,判断相同name命名的实例是否已经注册过。没有注册则用createInstance 传入配置项初始化一个实例

3.3.4.1.2 runtime-core中执行构造函数

实例对应的构造函数ModuleFederation,在另一个依赖里面@module-federation/runtime-core 实现,

对应源码位置 https://github.com/module-federation/core/blob/cfae7c06bd0f19aea0757fb2bcb7088ac29457cb/packages/runtime-core/src/core.ts#L171

  constructor(userOptions: UserOptions) {
    const plugins = USE_SNAPSHOT
      ? [snapshotPlugin(), generatePreloadAssetsPlugin()]
      : [];
    // TODO: Validate the details of the options
    // Initialize options with default values
    // 合并用户选项和默认选项
    const defaultOptions: Options = {
      id: getBuilderId(),
      name: userOptions.name,
      plugins,
      remotes: [],
      shared: {},
      inBrowser: isBrowserEnv(),
    };

    this.name = userOptions.name;
    this.options = defaultOptions;
    // 2. 初始化各个处理器
    this.snapshotHandler = new SnapshotHandler(this);
    this.sharedHandler = new SharedHandler(this);
    this.remoteHandler = new RemoteHandler(this);
    this.shareScopeMap = this.sharedHandler.shareScopeMap;
    
    // 3. 注册传入的插件
    this.registerPlugins([
      ...defaultOptions.plugins,
      ...(userOptions.plugins || []),
    ]);
    this.options = this.formatOptions(defaultOptions, userOptions);
  }

前面参数传递到这里的constructor并执行。

创建完成后setGlobalFederationInstance(instance); 存储到全局变量,方便后面的loadRemote使用

core/packages/runtime-core/src/global.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federa

export function setGlobalFederationInstance(
  FederationInstance: ModuleFederation,
): void {
  CurrentGlobal.__FEDERATION__.__INSTANCES__.push(FederationInstance);
}
3.3.4.2 loadRemote方法
3.3.4.2.1 外层调用查看

函数初始化在外层,具体代码:core/packages/runtime/src/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federation/c

export function loadRemote<T>(
  ...args: Parameters<ModuleFederation['loadRemote']>
): Promise<T | null> {
  assert(FederationInstance, getShortErrorMsg(RUNTIME_009, runtimeDescMap));
  const loadRemote: typeof FederationInstance.loadRemote<T> =
    FederationInstance.loadRemote;
  // eslint-disable-next-line prefer-spread
  return loadRemote.apply(FederationInstance, args);
}

继续转到runtime-core的实例构造函数里面查看真正的loadRemote方法。

core/packages/runtime-core/src/core.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federati

  async loadRemote<T>(
    id: string,
    options?: { loadFactory?: boolean; from: CallFrom },
  ): Promise<T | null> {
    return this.remoteHandler.loadRemote(id, options);
  }

this.remoteHandler 已经在前面实例化时,执行了初始化,再转到remote文件里面

github.com

async loadRemote<T>(
    id: string,
    options?: { loadFactory?: boolean; from: CallFrom },
  ): Promise<T | null> {
    const { host } = this;
    try {
      const { loadFactory = true } = options || {
        loadFactory: true,
      };

      //  1. 获取 Module 实例和相关配置信息
      const { module, moduleOptions, remoteMatchInfo } =
        await this.getRemoteModuleAndOptions({
          id,
        });
      ..

      // 2. 执行module.get
      const moduleOrFactory = (await module.get(
        idRes,
        expose,
        options,
        remoteSnapshot,
      )) as T;
      
      // 3. 调用对应的生命周期钩子
      const moduleWrapper = await this.hooks.lifecycle.onLoad.emit({
        id: idRes,
        pkgNameOrAlias,
        expose,
        exposeModule: loadFactory ? moduleOrFactory : undefined,
        exposeModuleFactory: loadFactory ? undefined : moduleOrFactory,
        remote,
        options: moduleOptions,
        moduleInstance: module,
        origin: host,
      });

      this.setIdToRemoteMap(id, remoteMatchInfo);
    

      // 4. 返回远程组件的具体实现
      return moduleOrFactory;
    } catch (error) {
 
      return failOver as T;
    }
  }

remoteHandle.loadRemote管理整个远程应用加载的执行顺序,并最终返回共享组件的具体内容。

3.3.4.2.2 New Module

通过getRemoteModuleAndOptions获取Module实例和相关配置信息,getRemoteModuleAndOptions里面除了初始化Module实例 new Module(moduleOptions);前面还有各种钩子函数的执行。

3.3.4.2.3 Module.get

核心逻辑都在Module.get函数里面,后面主要分析这个get方法

Module实例可以简单理解成管理和加载远程模块remoteEntry.js的构造函数,

具体看Module 构造函数后面的get方法

core/packages/runtime-core/src/module/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-

async get(
    id: string,
    expose: string,
    options?: { loadFactory?: boolean },
    remoteSnapshot?: ModuleInfo,
  ) {
    const { loadFactory = true } = options || { loadFactory: true };

    // 获取remoteEntry.js
    const remoteEntryExports = await this.getEntry();

      ....

      await remoteEntryExports.init(
        initContainerOptions.shareScope,
        initContainerOptions.initScope,
        initContainerOptions.remoteEntryInitOptions,
      );

      await this.host.hooks.lifecycle.initContainer.emit({
        ...initContainerOptions,
        id,
        remoteSnapshot,
        remoteEntryExports,
      });
    }

    this.lib = remoteEntryExports;
    this.inited = true;

    let moduleFactory;
    moduleFactory = await this.host.loaderHook.lifecycle.getModuleFactory.emit({
      remoteEntryExports,
      expose,
      moduleInfo: this.remoteInfo,
    });

    // get exposeGetter
    if (!moduleFactory) {
      moduleFactory = await remoteEntryExports.get(expose);
    }

    assert(
      moduleFactory,
      `${getFMId(this.remoteInfo)} remote don't export ${expose}.`,
    );

    // keep symbol for module name always one format
    const symbolName = processModuleAlias(this.remoteInfo.name, expose);
    const wrapModuleFactory = this.wraperFactory(moduleFactory, symbolName);

    if (!loadFactory) {
      return wrapModuleFactory;
    }
    const exposeContent = await wrapModuleFactory();

    return exposeContent;
  }

module.get里面也包括了对应的生命周期钩子函数,主要关注几个核心的流程,注意这里的钩子函数主要方便用户进行自定义的远程模块加载,而我们主要讨论的是runtime-core提供的默认加载顺序,所以暂时忽略这些钩子的影响

  1. this.getEntry(): 异步加载remoteEntry.js,在浏览器环境下通过动态创建script标签的方式异步加载远程应用,并执行
  2. remoteEntryExports.init:加载并执行完成后,拿到remoteEntry.js对外暴露出来的init方法并执行,这一步就是进行共享依赖传递融合,在远程应用中提前传入宿主的共享依赖库,并检查远程应用是否有统一的共享库,为后面的共享组件加载做准备
  3. remoteEntryExports.get(expose): 执行远程应用对我暴露的get方法,在远程应用的执行上下文中拿到共享组件

<!---->

3.3.4.2.3.1 通过getEntry,异步加载remoteEntry.js
 async getEntry(): Promise<RemoteEntryExports> {
    ...
    remoteEntryExports = await getRemoteEntry({
      origin: this.host,
      remoteInfo: this.remoteInfo,
      remoteEntryExports: this.remoteEntryExports,
    });
...
    this.remoteEntryExports = remoteEntryExports as RemoteEntryExports;
    return this.remoteEntryExports;
  }

core/packages/runtime-core/src/utils/load.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-fe

export async function getRemoteEntry(params: {
  origin: ModuleFederation;
  remoteInfo: RemoteInfo;
  remoteEntryExports?: RemoteEntryExports | undefined;
  getEntryUrl?: (url: string) => string;
  _inErrorHandling?: boolean; // Add flag to prevent recursion
}): Promise<RemoteEntryExports | false | void> {
 
  const uniqueKey = getRemoteEntryUniqueKey(remoteInfo);
 
  if (!globalLoading[uniqueKey]) {
    const loadEntryHook = origin.remoteHandler.hooks.lifecycle.loadEntry;
    const loaderHook = origin.loaderHook;
   
    globalLoading[uniqueKey] = loadEntryHook
      .emit({
        loaderHook,
        remoteInfo,
        remoteEntryExports,
      })
      .then((res) => {
        if (res) {
          return res;
        }
        // Use ENV_TARGET if defined, otherwise fallback to isBrowserEnv, must keep this
        const isWebEnvironment =
          typeof ENV_TARGET !== 'undefined'
            ? ENV_TARGET === 'web'
            : isBrowserEnv();

        return isWebEnvironment
          ? loadEntryDom({
              remoteInfo,
              remoteEntryExports,
              loaderHook,
              getEntryUrl,
            })
          : loadEntryNode({ remoteInfo, loaderHook });
      })
      .catch(async (err) => {
        throw err;
      });
  }

  return globalLoading[uniqueKey];
}

这里有个钩子loadEntryHook.emit,就是支持用户自定义加载remoteEntry.js函数,如果没有配置,则继续判断是否在浏览器环境,是则通过loadEntryDom进行动态创建script标签。

async function loadEntryDom({
  remoteInfo,
  remoteEntryExports,
  loaderHook,
  getEntryUrl,
}: {
  remoteInfo: RemoteInfo;
  remoteEntryExports?: RemoteEntryExports;
  loaderHook: ModuleFederation['loaderHook'];
  getEntryUrl?: (url: string) => string;
}) {
  const { entry, entryGlobalName: globalName, name, type } = remoteInfo;
  switch (type) {
    case 'esm':
    case 'module':
      return loadEsmEntry({ entry, remoteEntryExports });
    case 'system':
      return loadSystemJsEntry({ entry, remoteEntryExports });
    default:
      return loadEntryScript({
        entry,
        globalName,
        name,
        loaderHook,
        getEntryUrl,
      });
  }
}

loadEntryDom里面区别了不同的加载方法,默认走到loadEntryScript, 加载并执行remoteEntry.js

async function loadEntryScript({
  name,
  globalName,
  entry,
  loaderHook,
  getEntryUrl,
}: {
  name: string;
  globalName: string;
  entry: string;
  loaderHook: ModuleFederation['loaderHook'];
  getEntryUrl?: (url: string) => string;
}): Promise<RemoteEntryExports> {
  // if getEntryUrl is passed, use the getEntryUrl to get the entry url
  const url = getEntryUrl ? getEntryUrl(entry) : entry;
  return loadScript(url, {
    attrs: {},
    createScriptHook: (url, attrs) => {
      const res = loaderHook.lifecycle.createScript.emit({ url, attrs });
    },
  })
    .then(() => {
      return handleRemoteEntryLoaded(name, globalName, entry);
    })

}
3.3.4.2.3.2 执行remoteEntryExports.init,进行共享依赖传递融合

拿到remoteEntryExports内容后,继续Module.get后面的逻辑,后面开始处理远程应用的共享依赖shareScope了,其中shareScrope是我们初始化配置,同时也支持钩子函数设置,最后将处理的shareScope传入remoteEntryExports.init,开始共享依赖库跨应用传递

 await remoteEntryExports.init(
        initContainerOptions.shareScope,
        initContainerOptions.initScope,
        initContainerOptions.remoteEntryInitOptions,
      );

这里对应了前面构建时加载的[module.init], 有点类似

var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))

回到remoteEntry.js文件,再看一次里面的init和get方法

var moduleMap = {
    "./Content": () => {
        return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_94460")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
    },
    "./Button": () => {
        return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
    }
};
var get = (module, getScope) => {
    __webpack_require__.R = getScope;
    getScope = (
        __webpack_require__.o(moduleMap, module)
            ? moduleMap[module]()
            : Promise.resolve().then(() => {
                throw new Error('Module "' + module + '" does not exist in container.');
            })
    );
    __webpack_require__.R = undefined;
    return getScope;
};
var init = (shareScope, initScope) => {
    if (!__webpack_require__.S) return;
    var name = "default"
    var oldScope = __webpack_require__.S[name];
    if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
    __webpack_require__.S[name] = shareScope;
    return __webpack_require__.I(name, initScope);
};

这里执行的init函数

__webpack_require__.S是 Webpack 运行时内部用来存储所有共享作用域 (Share Scopes) 的地方,表示当前运行环境不支持共享功能,

var oldScope = __webpack_require__.S[name];表示远程应用上进行共享配置项目,并进行缓存处理,这里进行oldScope && oldScope !== shareScope判断,主要兼容remoteEntry.js被加载多次的场景,比如远程应用在当前页面被多个宿主(host)引入并初始化,host-a 和host-b在一个页面被加载,他们都依赖同一个remote,远程应用的运行时只能有统一的远程应用的share scope。

如何进行共享依赖呢?其实获取保存宿主传递过来的shareScope,__webpack_require__.S[name] = shareScope,这里通常是vue、react这些共享库。

这里共享依赖init函数就执行完成后,继续Module.get函数后面的流程

3.3.4.2.3.3 执行远程应用remoteEntryExports暴露出来的get方法,加载共享组件Content,并返回给宿主应用
moduleFactory = await remoteEntryExports.get(expose);

关键的一步就是执行remoteEntryExports.get('home/Content')

继续到moduleMap['./Content'](),找到content远程组件真实的地址,并通过__webpack_require__.e进行异步获取src_components_Content_vue-_94460

3.3.4.2.4 Return moduleFactory

到这里远程组件才真正获取完成。moduleFactory返回给宿主。

3.3.5 整体组件加载执行过程

image-9.png

4. 项目实操

<!---->

4.1 如何动态引入远程应用的remoteEntry.js如何进行控制?

  • 通过构建时注入变量进行控制,达到版本迭代的效果
  • 直接固定一个加时间戳的cdn资源,进行实时更新

4.2 远程应用加载失败如何处理?

  • 在runtime API里面,通过了自定义的钩子进行远程应用加载及加载失败的回调上报
  • 远程组件尽量包裹在React/Vue 的Suspense组件里面。

4.3 js/css作用域控制?

  • 人为约定控制,BEM、css scope、css in js、Shadow DOM

4.4 其他一些类似的模块组件级加载方案

<!---->

 const app = await Garfish.loadApp('vue-app', {
        cache: true,
        basename,
        domGetter: '#container',
        // 子应用的入口资源地址,支持 HTML 和 JS
        entry: 'http://localhost:8092',
      });
      setApp(app);

<!---->

   this.microApp = loadMicroApp({
      name: 'app1',
      entry: '//localhost:1234',
      container: this.containerRef.current,
      props: { brand: 'qiankun' },
    });

图片
在制造业数字化转型的“深水区”,企业正面临从“流程驱动”向“智能驱动”的跨越。面对SKU激增、跨部门协同滞后及人工流程低效等痛点,智能体(AI Agent)凭借“知识整合+数据联动+自动化执行”的闭环能力,正重构工业企业的运营逻辑。本文拆解了某中型制造企业在知识库构建、库存管理、智能报价三大核心场景的落地方法论,为企业提供可复用的智能化转型模板。
图片
传统ERP或MES系统属于“结构化数据仓库”,难以应对非结构化信息(如技术手册、经验法则)的处理。据行业调研,68%的制造企业正陷入以下“效率陷阱”:
1.知识孤岛化:累计5万+SKU的生命周期数据分散在PDF、纸质文档及资深员工大脑中,导致“找错型号”、“漏查停产”频发。
2.决策高延迟:老产品替代选型高度依赖人工经验,新人上手慢,客户询盘等待时间过长。
3.数据不联动:库存数据与前端报价脱节,响应周期长达2小时,极易错失瞬息万变的订单机会。智能体的核心价值:通过构建“统一知识底座”,实现从“被动查阅”到“主动赋能”的范式转变。
图片
场景 1:全生命周期产品知识库(RAG架构应用)核心逻辑:利用检索增强生成(RAG)技术,将海量非结构化技术文档转化为“会说话”的专家系统。
图片
专家提示:落地时需建立“数据动态更新机制”,确保研发侧的参数变更能实时同步至Agent知识库,避免信息滞后。
场景 2:实时库存感知智能体(数据链路打通)核心逻辑:Agent挂载API工具,实时联动ERP/WMS系统,实现“查询-核对-预警”全流程自动化。
• 实时可视化:员工查询产品时,Agent自动同步当前库存余量、库位信息及在途补货周期。
• 主动决策优化:设定安全库存阈值。当某SKU低于红线时,Agent自动推送预警至采购部。
• 价值体现:库存积压率降低30%,交期承诺准确度大幅提升。
场景 3:自动化智能报价引擎(端到端流程重构)核心逻辑:整合知识库、库存系统与成本规则,构建“秒级”响应的自动化报价链路。
1.报价算法模型:$$报价=(成本价\times (1+利润率)+物流成本)-阶梯优惠系数$$
2.动态因子接入:Agent根据实时库存周转率调整策略——库存高时自动触发促销折扣,加速回笼资金。
3.风控闭环:设置最低毛利红线,低于阈值自动触发人工审批,保障企业净利。
4.落地效果:响应周期从2小时压缩至10秒,订单转化率提升18%。
图片
制造企业无需追求“大而全”的推倒重来,建议遵循以下“4步走”策略:
1.场景切入(High-Frequency First):优先选择SKU查询、库存核对等高频痛点,1-2个月内实现快速验证。
2.复用现有资产(Leverage Assets):无需更换ERP,基于企业现有数据库,通过Agent实现数据聚合与跨系统联动。
3.低代码配置(No-Code Agility):选择支持可视化流转的Agent工具,让熟悉业务的“产品经理”直接参与逻辑搭建,而非单纯依赖技术外包。
4.小步迭代(Incremental Growth):上线后收集一线销售反馈,逐步扩展至供应商协同、订单跟踪等更深层领域。
图片
在工业4.0的浪潮下,智能体不再是实验性的“黑科技”,而是企业数字化韧性的必备基础设施。提前布局Agent落地,不仅是提升当下的运营效率,更是为未来“全自动化工厂”构筑智力大脑。

引言

对于开发者而言,离线数据开发中数据质量建设的核心挑战,从来不是“能否配置规则”,而是:质量规则能否像代码一样低成本、高可靠地融入研发交付全流程。当质量规则游离于开发链路之外,治理便退化为被动补救:SQL上线后补配质量规则、字段变更引发误报漏报、规则与代码版本脱节……最终导致规则越配越多,忙于补救的恶性循环。

开发和治理割裂流程下的工程代价

  • 治理滞后: 规则配置晚于数据上线,问题发现延迟
  • 迭代不同步: SQL口径逻辑变更后,规则未联动更新
  • 版本管理缺失: 规则脱离代码评审、Diff、回滚体系,难追踪
  • 信任成本攀升: 下游因数据约束不透明而反复确认,沟通负担加重

DataWorks 解法:以 Data Contracts 思想驱动“代码即质量”

DataWorks 数据质量引入 Data Contracts 理念,将质量规则以 YAML Spec 形式嵌入开发流程,实现“代码即质量”的一体化开发治理:

  • 开发即治理: 在 IDE 中直接为 SQL 节点编写质量 Spec,规则与代码同生命周期。
  • 工程化管理: Spec 支持版本控制、代码评审、Diff 对比,随发布流程自动部署至生产环境。
  • 闭环执行: 规则成为节点交付物的一部分,在调度中自动执行,确保质量保障前置化。

本文将从开发治理分离带来的问题出发,详细介绍 DataWorks 如何通过一体化开发治理流程,把质量规则变成节点交付物的一部分,并进一步说明为了实现这条链路,底层架构升级带来的外溢收益与后续规划。

一、当前困境:开发与治理分离

4e959395346848718b44642a22785dac.png

目前在常见工程化链路中,SQL开发与数据质量监控配置是分离的,现象包括:

  • 规则通常在数据上线后才补配置: 治理滞后,问题发现延迟。
  • SQL 迭代与规则不同步: 字段口径、过滤条件、分区逻辑变化后,规则仍停留在旧假设上,最终造成误报/漏报。
  • 质量治理变成“出了问题再补救”: 规则配置与修复工作被动插入到事故之后。
  • 规则与任务割裂: 规则不在代码评审链路里,难评审、难追踪、难回滚。

这就导致质量保障很难工程化:

  • 从“事前预防”退化为“事后补救”,影响数据消费者信任。
  • 生产者与消费者的预期难以对齐,沟通成本攀升。
  • 规则维护变成长期负担:一旦规模扩大,就会出现“规则越配越多,但可信度越配越低”的反直觉现象。

二、DataWorks 的解决方案:一体化开发治理

15a89d42bf104db2b510dcfae062fc69.png

DataWorks 数据质量借鉴当前业界中 Data Contracts 的思想,把数据质量的声明通过 Spec 的方法融入到整个数据开发流程中,让开发者可以一体化的维护数据加工代码和数据质量声明,二者能够及时的与数据开发代码一同变更,确保数据质量能够得到及时的保障。

2.1 核心思路:SQL与数据质量Spec一体化开发交付

53e1a0b29891403981b4205172816a51.png

在 DataWorks 新范式中,数据质量规则以 YAML Spec 形式存在,并具备与代码一致的工程化属性:

  • 在 IDE 中直接配置: 编写 SQL 的同时编写质量 Spec。
  • 天然支持版本管理: 规则随代码一起 Diff、评审、回滚。
  • 随发布自动执行: 规则不再依赖“事后补配置”,而是成为节点交付物的一部分,在生产调度中自动执行。

可以把它理解为:把“质量”从一个平台治理动作,变成研发交付链路中的标准步骤。

2.2 完整工作流

下面,我们结合首次开发 -> 测试验证 -> 提交发布 -> 调度运行 -> 迭代发布的流程,来说明如何做到SQL开发和数据质量保障一体化。

假设我们要开发一张表,建表语句如下:

CREATE TABLE IF NOT EXISTS dws_d_dqc_suggesion_demo(  
`id` BIGINT COMMENT '主键',  
`user_id` STRING COMMENT '用户ID',  
`item_id` STRING COMMENT '商品ID',  
`shop_id` STRING COMMENT '店铺ID',  
`name` STRING COMMENT '用户姓名',  
`family_name` STRING COMMENT '姓氏',  
`birth_time` DATETIME COMMENT '日期类型的生日',  
`order_url` STRING COMMENT '下单地址,是一个web页面地址',  
`create_time` DATETIME COMMENT '日期类型的下单时间',  
`order_time` STRING COMMENT '下单时间',  
`user_ip` STRING COMMENT '下单客户端ip',  
`user_mac` STRING COMMENT '下单客户端mac地址',  
`user_agent` STRING COMMENT '下单时的客户端标识',  
`email` STRING COMMENT '用户账号的邮箱',  
`phone_number` STRING COMMENT '用户的联系方式',  
`amount` STRING COMMENT '购买数量',  
`unit_price` DECIMAL(38,18) COMMENT '单价',  
`client_token` STRING COMMENT '下单时生成的全链路唯一标识,避免失败重试的重复下单',  
`status` STRING COMMENT '订单状态,Ready - 就绪、WaitingPayed - 待付款、Payed - 已付款待发货、Canceled - 已取消、Shipped - 已发货、WaitingCollecting - 已送达未领取、Delivered - 已收货、Confirmed - 已确认'
)
PARTITIONED BY(
    ds STRING COMMENT '日期分区,格式yyyymmdd'
)
LIFECYCLE 365;

2.2.1 在 IDE 中配置规则

在 SQL 开发完毕后,可以点击编辑器工具栏中的“质量测试”按钮,打开”质量测试“面板,开始定义数据质量监控 Spec。
20f9690331084b1ebb3f8b9c92b2741d.png

如下图所示,是一份同时监控两张表的Spec的结构。
51982d4945f348809085c738a2b1faa4.png

这里我们简单讨论一下数据质量监控定义方式上的取舍。在 DataWorks 既有的数据质量产品流程中,都是优先引导用户使用表单的方式来定义数据质量监控和规则,这种交互方式的好处在于上手门槛低,配合数据质量产品层面提供的智能化推荐能力,在大多数场景下可以做到一键配置。但是这种交互也有一定的问题:

1. 信息密度低,尤其是多表一次性多表监控场景下,需要填写多张表单,表单和表单之间可能还会有相互跳转,交互繁琐程度大大提升

2. 必须先有表才支持配置数据质量监控,否则会没有配置入口;在跨项目迁移、跨 region 迁移、搬站流程时这个问题会更加明显,在很多数据迁移场景中,会先迁代码再建表,表不存在时,无法把数据质量规则快速迁移到目标环境中与 SQL 节点一起验证

3. 可迁移能力差,如果大部分表都使用同一份配置,那么表单模式下,用户需要反复选表再填写表单。

引入 Spec 之后,上述问题都可以得到解决:

1. Spec 的信息密度很高,如果对于很多常用规则,只需要一到两行代码即可定义,整个数据质量监控也基本可以在十行代码之内搞定

2. 无需先搜索表再写 Spec,表名和所属数据源直接使用 Spec 定义,只需要确保在 Spec 执行时表存在即可;另外,DataWorks 的数据质量 Spec 兼顾了 AWS Glue Data Quality、Soda、Google Dataplex 等数据质量产品中的相关设计,可以把这些产品的数据质量配置转换成 DataWorks 数据质量 Spec,为搬站提供助力。

3. 可以快速的复制粘贴,快速拷贝能力

通过Agent配置规则

DataWorks Agent 智能体基于自然语言交互,结合大模型的深度认知与规划能力,能够完成复杂的数据集成、开发及治理任务,实现从需求到成果的端到端自动化,大幅提升工作效率。

Spec 的书写相对于表单式的配置门槛更高些,这里建议通过 DataWorks Agent 对话式的方式让 AI 辅助生成 Spec,AI 辅助生成时会感知 SQL 写入的表和分区,并生成合适的数据质量规则。
47b5e523edbf4adf9efc43886539fc72.png

如果默认生成的 Spec 需要调整,也可以通过对话的方式做调整,比如下图,基于上一步中生成的数据质量 Spec,我们让 AI 帮助去掉除了 id 字段之外其他字段的非空规则。
9a41cac6ac84453eab8dc901eb1f2769.png

当然,也可以手动编辑 Spec,我们提供了一些能力来提升手动编写的效率。

  • 模板插入
    735dd7bf2c82495cab91e67ece531ba2.png
  • 表/字段自动补全
    bcd5e5e5877646cd8ada480c7f82d77d.png
    75bc3dee493c4f9f8a2dad1d6da9ef8d.png

    2.2.2 开发与测试

数据质量Spec定义完毕后,可以在IDE中直接进行测试验证。
3cd9bfdcc3a84e8e9164268bd1f129d1.png
image.png

2.2.3 提交发布

测试符合预期后,执行任务的提交发布流程,数据质量Spec会跟随节点的代码一同被发布到调度,并一同被纳入版本管理。
cb4e60bff9f54c8e9e5ed8bf38469dae.png

这里注意,在运维中心的质量节点中也可以看到对应的配置。这就又带来了一个好处:依赖这个节点的开发可以更加明确的知道这个节点产出的数据的约束,增加下游节点对这个节点的信任程度。
f89e75bb03844a43803304f6320710ce.png

2.2.4 查看执行结果

上线后,生产调度会自动执行规则,你可以查看扫描日志与结果:
image.png
image.png

2.2.5 迭代开发

现在 dws_d_dqc_suggesion_demo 这张表的监控已经得到了保障,随着需求的推荐,我们需要为 dws_d_dqc_suggesion_demo 增加新的字段,此时在 SQL 开发完毕后,可以重新使用上述流程增量的修改数据质量监控 Spec,保证数据质量与 SQL 的一致性。
0fbf802734fe403e916e3d1575df2dc0.png

如图,我们添加 status_comment 字段的加工和监控逻辑,发布时可以看到版本变更中,不仅体现了 SQL 的变更,也体现了数据质量监控的变更,统一了版本管理。
8f92a53143af4ec992898b26024667e2.png

三、未来工作:更广覆盖、更低门槛、更主动治理

3.1 多引擎覆盖

当前能力已支持 MaxCompute SQL;其他类型 SQL 正在推进中,目标包括 EMR、Hologres、StarRocks 等,让不同引擎在质量能力上获得一致体验。

3.2 降低 Spec 门槛

我们会持续强化“对话式生成 + 局部自动修改”的编写方式;在现有高亮、表/字段提示能力基础上,进一步推进更强的代码级提示与批量化编辑能力,同时增强基于AI自动生成质量Spec能力,让 Spec 的编写成本持续下降。

3.3 更深融入 IDE

下一阶段会把质量治理更深度地融入研发工作流:在 IDE 中基于DataWorks Agent主动检测质量配置缺失,提供智能修复建议,将治理动作收敛至研发链路。

我们致力于让“质量随交付演进”成为离线数据开发的默认体验。欢迎各位开发者试用反馈,共同推动数据质量治理的工程化实践。

随着企业跨区域协作、全球化服务的快速增长,传统网络架构已难以承载新的业务需求。SD-WAN(软件定义广域网)作为现代网络架构,在国内市场持续增长。对于想要构建高可靠性、可控、智能的企业网络的人来说,选择合适的 SD-WAN 厂商已经成为必须了。

本文将带你从 SD-WAN 的概念讲起,到国内厂商排行榜、产品特点,再到如何选型,最后结合实际场景给出推荐。

一、SD-WAN(国际专线)是什么?哪些场景需要?

简单理解,SD-WAN 是一种通过软件定义方式管理广域网的网络架构,它突破了传统 MPLS 专线、VPN 的局限,将多条网络线路(包括专线、互联网宽带、4G/5G LTE)组合成一张智能、自适应的 WAN 网络。

相比传统 WAN:
自动选择最优路径
灵活组合带宽
精细化策略和流量管控
统一可视化管理

特别适合以下场景:

1、跨区域企业广域网
分公司、办事处、海外节点间需要稳定互联。

2、云业务接入
连接 AWS、Azure、阿里云、腾讯云等多个云环境。

3、跨境业务 & 出海服务
海外直播、SaaS 出海、社媒运营、外贸办公、AI 访问等对延迟与稳定性要求高。

4、混合办公场景
远程办公、移动终端分布广,需要统一安全接入。

简单来说,只要业务需要跨越多个地区节点、对稳定性、可控性有要求,SD-WAN 就非常有价值。

二、2026年SD-WAN 国内厂家排行榜

image.png

三、主流 SD-WAN 服务商介绍

  1. OSDWAN
    OSDWAN作为国内专业的跨境网络服务商,为出海企业提供合规、高速、稳定的网络解决方案,支持硬件、软件方案灵活部署。
    OSDWAN在全球的数据中心节点50个,POP节点超过200个,可以为出海企业提供海外加速、SaaS加速、SD-WAN组网、跨境组网、云专线等产品服务,助力中国企业开拓国际市场。
    OSDWAN入门版690元/年起,网络专线低至100元/M/月起。适用于社媒运营、TK直播、学术科研、跨境电商、品牌出海、外贸出口等各类行业场景。

2、电信国际SD-WAN
电信SD-WAN业务依托于中国电信国际优质的海外云网资源能力,在全球与超过300个服务供应商合作提供一站式的解决方案,允许用户快速搭建企业专网,实现组网、入云及应用的定向优化访问,支持组网、混合组网、公有云直连、移动办公。

3、移动SD-WAN
移动SD-WAN 解决方案通过云管理平台为企业客户提供易于管理,具高可用性和灵活的广域网(WAN)。其配合MPLS及互联网产品作为全球网络骨干传输,并利用宽带、互联网、LTE等多种类接入方式,连接SD-WAN网关节点与企业分支机构的客户端(CPE)。

image.png

4、腾讯云SD-WAN
腾讯云SD-WAN 接入服务(SD-WAN Access Service)助力多分支轻松实现与云、数据中心的任意互联,具有即插即用、多地域覆盖、智能管控等特性,为企业多分支提供了更简单、可靠、智能的一站式上云的体验。

image.png

5、联通国际专线

联通国际以太网专线 (IEPL) /国际专线(IPLC)为跨境、跨地域的客户提供专有国际数据实时传输应用。提供严格带宽保证、独享带宽、全透明的端到端专线服务。

四、SD-WAN 国内厂家哪家好?推荐 OSDWAN

相较于传统的SD-WAN服务商,OSDWAN跨境网络专线也有极大突出优势:更好用、更高性价比、更安全、更可控、更安心。

01、更好用:相比传统SD-WAN服务商只支持CPE设备,OSDWAN不仅提供多种型号的CPE设备,还支持经过安全认证的相应软件。支持Windows、Mac、iPhone、安卓、iPad,让您随时随地一键连接全球互联网。

02、更高性价比:相比传统SD-WAN服务商与运营商接近的高额网络费用,OSDWAN仅需一半不到的成本即可享受同等优质的网络线路。

03、更安全:OSDWAN采用自研双重加密机制,对数据进行多层加密处理,有效防止数据泄露、保证信息安全传输。

04、更可控:企业管理后台,可以管理员工子账号、限制使用设备数、管控访问范围、监管访问日志。

05、更安心:相比传统SD-WAN服务商需要5-8个工作日按照工单解决客户问题,OSDWAN提供专属售后支持,配备专属售后顾问。同时还提供分流解锁,路由优化等服务。让您的业务安心出海。

总的来说,OSDWAN兼具合规合法、稳定安全、简单易用、高性价比等优势,支持一键访问全球互联网。是企业办公、网络营销、跨境直播、社媒运营的不二之选。

image.png

五、SD-WAN 常见问题

1、什么是 SD-WAN 与传统 MPLS 区别?

SD-WAN 更智能、更灵活:

它可以在多条链路间根据策略动态调度,而 MPLS 是固定专线交换网络。

2、SD-WAN 是否一定比 MPLS 好?

不一定。

对于对等连接、安全策略统一要求高的场景,SD-WAN 通常更灵活;但 MPLS 在某些核心银行/政府系统仍有优势。

3、SD-WAN 会不会很贵?

成本取决于带宽、节点数量、功能要求。

智能调度 + 多链路组合使总体成本低于传统专线叠加方案。

4、SD-WAN 是否安全?

好的 SD-WAN 会结合:

加密传输

访问策略

从而达到企业级安全要求。

结语

对于追求全球稳定网络、跨境业务、高性价比和企业级管理能力的客户来说,OSDWAN 是一个值得认真评估的 SD-WAN 解决方案供应商。

OSDWAN是国内专业的跨境网络专线服务商,专注于为出海企业提供合规、稳定、低延迟的跨境网络解决方案。支持硬件部署与软件接入,满足不同规模企业的灵活组网需求。

目前已覆盖全球 50+ 数据中心节点,200+ POP 接入点,可提供包括海外加速、SaaS 加速、SD-WAN 组网、跨境专线、云专线互联等多种产品,帮助企业建立长期可持续的国际网络架构。

产品支持从入门版到企业级独享专线多种方案,适用于外贸办公、海外AI加速、社媒运营、跨境电商、品牌出海、跨境直播等多行业场景。

很多企业在拓展海外市场时,都会遇到一个问题:访问海外系统慢、视频会议卡顿、跨境传输不稳定,甚至影响客户体验。

这时候就会考虑使用“国际网络专线”。但紧接着第二个问题就来了——企业国际网络专线一年到底多少钱?本篇内容为大家详细介绍:

一、什么是企业国际网络专线?

简单理解,国际网络专线就是企业通过运营商或专业服务商建立的一条“专用国际通信通道”。

和普通宽带的区别在于:
延迟更低
丢包率更小
稳定性更高

适用于:
海外办公互联
跨境电商运营
海外直播
国际视频会议
访问海外云服务器(AWS、Azure等)
SaaS出海业务

如果你的业务对“稳定性”和“连续性”要求高,普通宽带通常不够用。

二、国际网络专线价格是怎么计算的?

很多人以为专线是统一价,其实并不是。

国际专线基本是“按需求定制报价”,主要受以下因素影响。

1、带宽大小
最核心的定价因素。
常见企业带宽规格:
10M、20M、50M、100M、200M及以上,带宽越大,价格越高。
但不是越大越好,要根据实际业务需求来选,避免浪费。

2、目标国家或地区
不同国家成本差异明显。

例如:
香港、日本、美国线路成熟,成本相对可控
中东、南美、非洲成本偏高

跨洲距离越远,资源越稀缺,价格越高,所以不用国家地区的价格不同。

3、线路类型

不同类型价格差距较大:
传统运营商国际专线(价格最高)
SD-WAN智能国际专线(部署简单,性价比更高)

现在很多企业会选择SD-WAN模式,因为可以在保证稳定性的同时降低成本。

4、是否独享

专线分为:
独享带宽
共享带宽

独享价格更高,但稳定性最好。

如果是跨境直播、金融数据传输,通常建议独享。

5、是否包含安全与管理功能

一些基础专线只提供“线路”。
比如OSDWAN包含:加密传输、企业管理后台、子账号权限控制、日志审计、路由优化,功能越多,成本会略高,但长期更安全。

三、企业国际网络专线一年多少钱?

下面以OSDWAN为例:

OSDWAN为大家提供多个版本,满足不同用户场景的使用需求,具体如下:
1、办公账号版:690元/年,适合外贸SOHO日常办公使用,比如国外网站访问、海外AI平台应用等等场景。
2、独享IP套餐:1500元/年起,适用于TikTok运营、社媒矩阵、店铺运营等场景,根据使用设备数和IP类型有不同套餐。
3、独立专线:按带宽计费,例如美区5M标准带宽专线是10000元/年,适合10人以内团队日常办公使用或普清TK直播。另有10M、20M、50M等不同带宽等线路套餐可选,不同国家地区费用有差别。
4、定制方案:企业按需定制线路,100+地区的线路和IP可选,自由组合配置。

image.png

四、企业该如何选择合适的国际网络专线?

选专线时建议重点看以下几点:

1、是否支持测试

正规服务商一般支持测试线路。

2、 延迟与丢包率

不要只听销售介绍,要求真实测试数据。

3、是否支持智能分流

办公流量和视频流量是否可以分开?

是否自动切换最优线路?

4、安全能力

是否支持加密?

是否可以管理访问权限?

5、售后响应

国际业务是实时场景,出现问题需要快速解决。

五、哪家企业国际网络专线更值得考虑?

在众多服务商中,OSDWAN跨境网络专线是一个比较有代表性的方案。OSDWAN产品优势如下:

相较于传统的SD-WAN服务商,OSDWAN跨境网络专线也有极大突出优势:更好用、更高性价比、更安全、更可控、更安心。

01、更好用:相比传统SD-WAN服务商只支持CPE设备,OSDWAN不仅提供多种型号的CPE设备,还支持经过安全认证的相应软件。支持Windows、Mac、iPhone、安卓、iPad,让您随时随地一键连接全球互联网。

02、更高性价比:相比传统SD-WAN服务商与运营商接近的高额网络费用,OSDWAN仅需一半不到的成本即可享受同等优质的网络线路。

03、更安全:OSDWAN采用自研双重加密机制,对数据进行多层加密处理,有效防止数据泄露、保证信息安全传输。

04、更可控:企业管理后台,可以管理员工子账号、限制使用设备数、管控访问范围、监管访问日志。

05、更安心:相比传统SD-WAN服务商需要5-8个工作日按照工单解决客户问题,OSDWAN提供专属售后支持,配备专属售后顾问。同时还提供分流解锁,路由优化等服务。让您的业务安心出海。

总的来说,OSDWAN兼具合规合法、稳定安全、简单易用、高性价比等优势,支持一键访问全球互联网。是企业办公、网络营销、跨境直播、社媒运营的不二之选。

七、总结

企业国际网络专线一年费用通常在几万元到几十万元之间,具体取决于:带宽、国家、线路类型、是否独享、是否包含安全与管理功能。

OSDWAN是国内专业的跨境网络专线服务商,专注于为出海企业提供合规、稳定、低延迟的跨境网络解决方案。支持硬件部署与软件接入,满足不同规模企业的灵活组网需求。

目前已覆盖全球 50+ 数据中心节点,200+ POP 接入点,可提供包括海外加速、SaaS 加速、SD-WAN 组网、跨境专线、云专线互联等多种产品,帮助企业建立长期可持续的国际网络架构。

产品支持从入门版到企业级独享专线多种方案,适用于外贸办公、海外AI加速、社媒运营、跨境电商、品牌出海、跨境直播等多行业场景。

上周,一名工程师从零开始,用 AI 模型重构了当下最流行的前端框架,最终得到了 vinext(读作“vee-next”)。它可作为 Next.js 的即插即用替代方案,基于 Vite 构建,仅需一条命令就能部署到 Cloudflare Workers。在初步基准测试中,生产环境应用的构建速度最高提升 4 倍,客户端打包体积最高减小 57%。目前已有客户在生产环境中正式使用。

整个项目仅花费约 1100 美元的 Token 费用。

Next.js 的部署难题

Next.js 是最流行的 React 框架,拥有数百万开发者用户,支撑着互联网上相当一部分生产环境应用 —— 这背后有着充分的理由:它拥有一流的开发者体验。

但当 Next.js 被用于更广泛的无服务器生态时,会出现部署问题。它的工具链是高度定制化的:尽管 Next.js 在 Turbopack 上投入了大量资源,但如果要部署到 Cloudflare、Netlify 或 AWS Lambda,仍需对构建输出进行改造才能适配目标平台的运行环境。

如果你在想:"这不就是 OpenNext 的功能吗?"——你说的没错。

这确实是 OpenNext 要解决的核心问题。包括 Cloudflare 在内的多家厂商都为 OpenNext 投入了大量工程资源。它虽然能运行,但很快就会遇到各种限制,变成一场打地鼠游戏。

基于 Next.js 输出进行构建早已被证明是一种困难且脆弱的方案。OpenNext 不得不对 Next.js 的构建输出做逆向工程,这会带来版本间不可预知的变更,需要做大量的修正工作。

Next.js 一直在开发一等适配器 API,我们也在与其合作,目前仍处于早期阶段。但即便有了适配器,构建过程依然要依赖高度定制化的 Turbopack 工具链。此外,适配器只覆盖构建与部署环节。在开发阶段,next dev 完全在 Node.js 中运行,无法接入其他运行时。如果你的应用使用了平台特定 API(如 Durable Objects、KV 或 AI 绑定),除非采用变通方案,否则无法在开发环境中对这些代码进行测试。

vinext 介绍

如果不采用适配 Next.js 输出的方式,而是直接基于 Vite 重新实现 Next.js 的 API 会怎样?Vite 是除 Next.js 外大多数前端生态所使用的构建工具,支撑着 Astro、SvelteKit、Nuxt、Remix 等框架。这是一次干净的重新实现,而非简单的封装或适配。老实说,我们原本并不认为这能成功。但如今已是 2026 年,软件构建的成本已经完全改变了。

我们取得的进展远超预期。

npm install vinext

只需将脚本中的 next 替换为 vinext ,其余保持不变即可。你现有的 app/pages/next.config.js无需修改就能正常工作。

这并非基于 Next.js 和 Turbopack 输出的封装层,而是对 API 的完整替代实现:路由、服务端渲染、React Server Components、服务端操作、缓存、中间件——全部基于 Vite 构建并以 Vite 插件的形式实现。最重要的是,得益于 Vite Environment API,Vite 的构建输出可在任意平台上运行。

数据表现

初步基准测试结果振奋人心。我们使用一个包含 33 条路由的共享 App Router 应用对 vinext 与 Next.js 16 进行了对比。两个框架执行相同的工作:编译、打包并准备服务端渲染路由。我们在 Next.js 构建中禁用了 TypeScript 类型检查和 ESLint(Vite 在构建期间不运行这些),并使用 force-dynamic 防止 Next.js 花费额外时间预渲染静态路由(避免拖慢其速度)。测试目标是仅测量打包器与编译速度,排除其他干扰因素。每次合并到主干分支时,都会在 GitHub CI 上运行基准测试。

生产构建时间:

客户端打包体积(gzip 压缩后):

这些基准测试测量的是编译与打包速度,而非生产服务性能。测试样本为一个包含 33 条路由的单一应用,并非所有生产应用的代表性样本。我们预计随着三个项目的持续迭代,相关数据会发生变化。完整的测试方法与历史结果均已公开,仅供作为方向性参考,而非绝对结论。

不过这一方向已展现出积极信号。Vite 的架构,尤其是即将在 Vite 8 中推出的、基于 Rust 开发的打包器 Rolldown,在构建性能上具备结构性优势,这一点在本次测试中体现得十分明显。

部署到 Cloudflare Workers

vinext 以 Cloudflare Workers 作为首要部署目标进行构建,只需一条命令就能从源码直接部署为可运行的 Worker:

vinext deploy

这会处理所有环节:构建应用、自动生成 Worker 配置并完成部署。App Router 和 Pages Router 均可在 Workers 上运行,支持完整的客户端 hydration、交互式组件、客户端路由导航与 React 状态。

在生产缓存方面,vinext 内置了 Cloudflare KV 缓存处理器,开箱即用支持 ISR(增量静态再生):

KV 是大多数应用的默认选择,但缓存层是可插拔的。你可以通过 setCacheHandler 替换为任意合适的后端。对于缓存负载较高或访问模式不同的应用,R2 可能会更合适。我们也在优化 Cache API,以提供更强大的缓存层并简化配置。我们的目标是保持灵活:让你选择适合自己应用的缓存策略。

实时运行示例:

我们还有一个 Cloudflare Agents 在 Next.js 应用中实时运行的示例,且无需 getPlatformProxy 这类变通方案——因为整个应用现在无论是开发还是部署阶段都运行在 workerd 中。这意味着可以无妥协地使用 Durable Objects、AI 绑定以及其他所有 Cloudflare 专属服务。可在此处查看。

框架开发是一场团队协作

当前的部署目标是 Cloudflare Workers,但这只是冰山一角。vinext 中约 95% 的代码都是纯 Vite 生态相关内容:路由、模块垫片、SSR 流程、RSC 集成——这些都不是 Cloudflare 特有的。

Cloudflare 希望与其他托管服务商合作,让客户们也能使用这套工具链(所需工作量极小——我们在不到 30 分钟内就完成了 Vercel 上的概念验证)。这是一个开源项目,为了长期发展,与整个生态伙伴合作、确保能够持续投入是至关重要的。我们欢迎来自其他平台的 PR。如果你有兴趣新增部署目标,欢迎提交 Issue 或联系我们。

状态:实验阶段

我们要明确说明的是:vinext 目前仍处于实验阶段。它诞生还不到一周,尚未经过大规模真实流量的实战检验。如果你考虑将其用于生产环境,请务必谨慎评估。

话虽如此,我们的测试套件已相当完备:包含超过 1700 个 Vitest 测试与 380 个 Playwright 端到端测试,其中部分测试直接移植自 Next.js 测试套件以及 OpenNext 的 Cloudflare 一致性测试套件。我们已通过 Next.js App Router Playground 完成验证,对 Next.js 16 API 的覆盖率达到 94%。来自真实用户的早期反馈同样鼓舞人心。我们一直在与 National Design Studio 合作,该团队致力于推动所有政府界面的现代化,目前已在其测试站点 CIO.gov 上运行 vinext,并已将其用于生产环境,构建时间与打包体积均得到显著优化。

README 中已如实列出目前不支持的功能与已知限制。我们希望保持坦诚,不过度承诺。

预渲染

vinext 已开箱即支持增量静态再生(ISR)。与 Next.js 一样,任意页面在首次请求后都会被缓存,并在后台重新验证。这一功能现已可用。

vinext 目前尚不支持构建时静态预渲染。在 Next.js 中,无动态数据的页面会在 next build 阶段被渲染并作为静态 HTML 提供;若存在动态路由,可通过 generateStaticParams() 枚举需要提前构建的页面。vinext 目前暂不支持这一功能,但也只是暂时的。

这是发布时我们有意做出的设计决策。该功能已列入路线图,但如果你的网站是 100% 预构建的静态 HTML,目前使用 vinext 可能不会带来太多收益。话虽如此,如果一位工程师能花 1100 美元的 Token 成本重建 Next.js,那你大概只需花 10 美元就能迁移到专为静态内容设计、基于 Vite 的框架,比如 Astro(它同样可以部署到 Cloudflare Workers)。

但对于并非纯静态的网站,我们相信我们能做得比在构建时预渲染所有内容更好。

流量感知预渲染

Next.js 会在构建期间预渲染 generateStaticParams() 中列出的每个页面。一个拥有 10000 个产品页面的网站,就意味着构建时要执行 10000 次渲染,即便其中 99% 的页面可能永远不会被访问。构建时间会随页面数量线性增长,这也是大型 Next.js 网站的构建时间最终会长达 30 分钟的原因。

因此我们构建了流量感知预渲染(Traffic-aware Pre-Rendering,TPR)。该功能目前仍处于实验阶段,我们计划在经过更多真实场景验证后将其设为默认模式。

理念很简单:Cloudflare 本身就是你网站的反向代理,我们掌握着你的流量数据,清楚哪些页面是实际被访问的。因此,vinext 既不会预渲染全部内容,也不会完全不预渲染,而是在部署时查询 Cloudflare 的区域分析,只预渲染那些关键页面。

对于一个拥有 10 万个产品页面的网站,幂律分布表明:90% 的流量通常只集中在 50 至 200 个页面上。这些页面可以在几秒内完成预渲染,其余内容则回退到按需 SSR,并在首次请求后通过 ISR 缓存。每次新部署都会根据当前流量模式刷新预渲染页面集合,爆款页面会被自动纳入。整个过程无需使用 generateStaticParams(),也不需要将构建过程与生产数据库绑定。

直面 Next.js 带来的挑战,但这一次用上了 AI

这类项目通常需要一支工程师团队耗费数月乃至数年才能完成。已有多家公司的多个团队尝试过,但涉及范围实在是太大了。Cloudflare 就曾尝试过一次:两套路由、33+ 个模块垫片、服务端渲染管道、RSC 流式传输、文件系统路由、中间件、缓存、静态导出。也难怪一直没人能真正做成。

这一次,我们仅用不到一周就完成了——由一名工程师(严格来说是工程经理)指导 AI 完成。

首次提交始于 2 月 13 日。当天晚上,Pages Router 与 App Router 就已实现基础 SSR、中间件、服务端操作与流式传输。次日下午,App Router Playground 已能渲染 11 条路由中的 10 条。第三天,vinext deploy 已可将应用部署到 Cloudflare Workers,并支持完整的客户端激活。剩下的时间则用于稳定性加固:修复边界场景、扩充测试套件、将 API 覆盖率提升至 94%。

与早期的尝试相比,究竟是什么发生了改变?是 AI 变得更强大了,强大得太多。

为什么这个问题适合用 AI 解决

并非每个项目都能进展得如此顺利。这个项目之所以能做到,是因为诸多因素在恰当的时机恰好形成了合力。

Next.js 的定义十分清晰。它拥有详尽的文档、庞大的用户群体以及多年来积累在 Stack Overflow 上的问答和教程。其 API 已经广泛存在于训练数据中。当你让 Claude 实现 getServerSideProps 或解释 useRouter 的工作原理时,它不会出现幻觉。它完全了解 Next 的运行机制。

Next.js 拥有完备的测试套件,其代码仓库中包含数千个覆盖所有功能与边界场景的端到端测试。我们直接从这些测试套件中移植了测试用例(你可以在代码中看到相关标注),这为我们提供了可机械验证的规范。

Vite 是非常优秀的基础框架。它解决了前端工具链的核心难点:快速热更新、原生 ESM、清晰的插件 API 以及生产构建。我们无需从零开发打包器,只需要让它适配 Next.js 的使用方式即可。@vitejs/plugin-rsc 目前仍处于早期阶段,但它已经为我们提供了 React Server Components 支持,不必从头实现 RSC。

模型能力终于跟上了。就在几个月前,我们还认为这是不可能实现的。早期的模型无法在如此规模的代码库中保持逻辑连贯,而新一代模型能够理解完整架构、推理模块间的交互,并足够稳定地生成正确代码,从而保证开发节奏。有时我看到它深入 Next、Vite 和 React 的内部机制去定位问题。最先进的模型表现令人惊叹,而且显然还在持续进步。

所有这些条件必须同时具备:目标 API 文档完善、测试套件全面、底层构建工具坚实可靠,再加上能够真正处理复杂逻辑的模型。缺少任何一环,效果都会大打折扣。

我们是如何实现的

几乎 vinext 的每一行代码都是由 AI 编写的。更重要的是:每一行代码都具备了与人工编写代码相同的质量标准。项目拥有 1700+ 个 Vitest 测试、380 个 Playwright E2E 测试,通过了 tsgo 完整的 TypeScript 类型检查与 oxlint 代码检查,持续集成会针对每个 PR 执行所有检查。建立一套完善的质量保障机制是让 AI 在代码库中高效发挥作用的关键。

整个过程从一份规划开始。我花了数小时在 OpenCode 里与 Claude 反复沟通,明确架构:要构建什么、按什么顺序推进、使用哪些抽象层。这份规划成了整个工作的北极星。从那之后,工作流程就变得非常清晰:

  1. 定义任务(“实现包含 usePathnameuseSearchParamsuseRouternext/navigation 垫片”)。

  2. 让 AI 来编写实现代码与测试用例。

  3. 运行测试套件。

  4. 如果测试通过就合并,不通过就将错误信息交给 AI 进行迭代修复。

  5. 重复这一过程。

我们还为代码审查设置了 AI 智能体。PR 提交后,智能体会自动执行审查;审查意见返回后,另一个智能体会处理这些意见。整个反馈流程基本实现了自动化。

它并非每次都能完美运行。有些 PR 本身就是错误的。AI 常会“自信”地实现一些看似正确、却与 Next.js 实际行为不符的逻辑。我必须定期纠正方向。架构决策、优先级排序、判断 AI 何时走入死胡同——这些都由我负责。当你给 AI 清晰的方向、充足的上下文和完善的约束时,它的效率会非常高。但最终,依然需要人类来掌舵。

对于浏览器级别的测试,我使用 agent-browser 来验证实际渲染输出、客户端导航与 hydration 行为。单元测试会遗漏许多细微的浏览器问题,而这套方案能将捕获到它们。

整个项目期间,我们在 OpenCode 中运行了 800 多个会话,总成本约为 1100 美元的 Claude API Token 费用。

这对软件来说意味着什么

为什么我们的技术栈会有这么多层?这个项目迫使我深入思考这个问题,并思考 AI 会如何影响答案。

软件中的大多数抽象都是为了给人类提供辅助。我们无法在脑中容纳整个系统,因此通过构建层次来管理复杂度。每一层都能让下一层开发者的工作更轻松。这也是你最终会看到框架之上再套框架、出现各种封装库、以及成千上万行胶水代码的原因。

AI 没有这样的限制。它可以在上下文里容纳整个系统,并直接编写代码。它不需要借助中间框架来维持结构,只需要一份规范和一个构建基础。

目前尚不清楚哪些抽象是真正基础的,哪些只是人类认知的拐杖。这条界限在未来几年将会大幅改变。而 vinext 就是一个例证:我们只提供了一份 API 契约、一个构建工具和一个 AI 模型,剩下的中间层全部由 AI 编写,无需额外的中间框架。我们相信这种模式会在大量软件中复现,多年来搭建的层层封装并不会全部保留下来。

试试看

vinext 包含一个用于处理迁移的 Agent Skill。它适用于 Claude Code、OpenCode、Cursor、Codex 等数十种 AI 编码工具。安装它,打开你的 Next.js 项目,告诉 AI 进行迁移:

npx skills add cloudflare/vinext

然后在任意支持的工具中打开你的 Next.js 项目,并输入:

migrate this project to vinext

这个 Skill 会处理兼容性检查、依赖安装、配置生成与开发服务器启动。它清楚 vinext 所支持的内容,并会标记出所有需要手动处理的部分。

或者如果你更喜欢手动操作:

源代码位于 github.com/cloudflare/vinext,欢迎提交 Issue、PR 与反馈。

【声明:本文由 InfoQ 翻译,未经许可禁止转载。】

原文链接:https://blog.cloudflare.com/vinext/

年化收入破 20 亿美元,Cursor 成最“吸金”AI 独角兽之一

 

上周,据《Bloomberg》报道,一位匿名的人士向其透露,Cursor 2025 年销售额突破 20 亿美元。该公司未来 12 个月的营收运行率(预测当前销售额)比三个月前翻了一番。

 

该人士表示,约 60% 的收入来自企业客户——包括首次使用 Cursor 的公司以及增加席位的现有客户。

其快速增长吸引了 Accel、Andreessen Horowitz 和 Thrive Capital 等风投巨头的关注。去年 11 月份由风险投资公司 Accel 和 Coatue 共同领投的一轮融资中估值达到 293 亿美元,使其成为美国最有价值的人工智能初创公司之一。

 

Cursor 拒绝置评。

 

为什么这家成立仅 5 年的公司会发展如此之快?这要从其明星创始团队说起。

 

Cursor 由四位麻省理工学院的校友于 2022 年创立,最初致力于构建模型,帮助机械工程师设计物理零件。但这些联合创始人当时并没有这方面的专业知识。他们迅速转型,最终推出了爆款产品:一款代码编辑器,并迅速走红。

Cursor CEO Michael Truell 在 2024 年接受《福布斯》采访时将 Cursor 描述为“程序员版的 Google Docs”,一个人类和人工智能共同改进代码的协作编辑器。

 

Truell 和他的联合创始人自 2020 年起就密切关注 OpenAI 的人工智能发展,甚至在 ChatGPT 发布之前就已开始关注。Truell 表示,他们当时就知道这个领域即将迎来爆发式增长。他们萌生创建一家人工智能编码初创公司的想法,源于他们目睹了微软 GitHub Copilot 的成功。

 

GitHub Copilot 的成功预示着随着人工智能模型的改进,许多复杂任务都可以实现自动化。“GitHub Copilot 是第一个真正有用的人工智能产品。它不是空想,也不是需要排队等候才能使用的产品。”他告诉《福布斯》杂志。

 

该公司超快的编码模型最终推动了 Vibe Coding 现象的兴起。

 

Cursor 的创始人以及其 400 名员工中的很大一部分都只有二十五六岁,这家初创公司与其说像一家企业,不如说更像一所精英大学校园。员工们进办公室前都要脱鞋。他们经常工作到午夜之后,在公司里洗澡,而且都住在离办公室几个街区远的地方。

 

正是这股拼劲儿和独特的产品理念使得该公司 2025 年初的年化收入达到了 1 亿美元,而到了 11 月,这一数字已突破 10 亿美元。最新一轮融资使公司估值接近 300 亿美元,四位联合创始人也因此跻身亿万富翁之列,Cursor 也因此跻身全球最有价值的 20 家私营公司之列。

Claude、Codex 的爆火严重冲击了 Cursor

 

但最近几个月,一些关于 Cursor 即将消失的声音甚嚣尘上。

 

2026 年 1 月 5 日,Cursor 公司的员工们结束假期周末返回公司,参加了一场全体员工大会,会上展示了一份题为“战时”的幻灯片。

 

休息期间,员工们试用了 Anthropic 公司最新款的 Opus 4.5,却意外地发现了一个令人不安的事实:它的编码能力已经发展到开发者无需再逐行检查代码的地步。开发者不再需要与 Cursor 代码编辑器中的 AI 助手协作,而是可以向自主运行的智能体发出高级指令,并接收已完成的功能——有时甚至能收到最终产品。而这恰恰是个问题。

 

Cursor 的构建理念有所不同。

 

Cursor 最终希望构建能够自动化工程师 95% 繁琐工作的工具,让他们能够将更多时间用于编码的创造性方面。

 

“我认为,不久之后,单个工程师就能构建出比现在强大的团队所能构建的系统复杂得多的系统,”Truell 曾在一次采访中如是说。

 

但如果人工智能不需要人类协作,那编辑器还有什么用呢?如果逐行编写和编辑代码不再是程序员工作流程的核心,那么 Cursor 的核心产品理念就突然受到了质疑

 

如今让人心惊的事实出现了:开发者可能不再需要代码编辑器了。

 

在全体员工大会上,Cursor 的领导层警告说,未来几个月将会动荡不安。项目可能会被取消,优先级也会发生变化

 

而这背后的根因在于AI 编程正在从“辅助写代码”走向“智能体完成任务”

 

去年年初,Anthropic 致电其当时最大的客户 Cursor,预览了一款名为 Claude Code 的新产品。Claude Code 是一款命令行工具,界面简洁,允许开发人员快速部署大量的编码代理。

 

乍一看,Claude Code 似乎不会直接与 Cursor 的代码编辑器展​​开竞争。但如今情况已截然不同。Claude Code 在短短六个月内年化收入​​就突破了 10 亿美元,上个月更是达到了 25 亿美元,超越了 Cursor。与此同时,OpenAI 也正朝着同样的方向努力。在 2025 年 4 月重新推出其编码代理 Codex 后,首席执行官 Sam Altman表示,该应用在发布后的第一周下载量就超过了 100 万次。

创业公司创始人告诉《福布斯》,这种转变意义深远。许多开发者现在不再一行一行地编写代码,而是操控代理程序——分配任务、审查输出、协调多个并行进程。

 

“这是软件开发自诞生以来最重大、最根本的变革,”人工智能语言辅导应用 Speak 的联合创始人兼首席技术官 Andrew Hsu 表示。该公司 50 名工程师组成的团队现在都使用编码代理(主要是 Claude Code,有时也用 Codex),将功能发布时间从几个月缩短到几周。他表示,Cursor 在代码审查中仍然发挥作用,但其重要性正在逐渐降低

Cursor 真的要消失了?

 

如此说来,曾经红极一时的 AI 编码工具在竞争对手强有力的夹击下真的要消失了吗?

 

2025 年 2 月份,在 Anthropic 发布了更先进的 Opus 版本之后,X 论坛开始涌入大量创业公司创始人,他们声称自己的团队已经放弃了 Cursor,并认为像 Anthropic 和 OpenAI 这样的模型制作商会自行吸收编码层。

 

“我提到的大多数公司……他们的观点是 Cursor 如今已经过时了,”Insight Partners 联合创始人兼前董事总经理 Jerry Murdock 上周在 20VC 播客节目中表示。

 

今年 2 月,抵押贷款服务初创公司 Valon 的 90 多名员工取消了 Cursor 的订阅。理由很简单:他们不再需要这款编辑器了。取而代之的是,他们转而使用 Claude Code 强大的代理程序,实现了端到端工作流程的完全自动化——包括系统间数据迁移、修复漏洞等等。Valon CEO Andrew Wang 表示,这些任务的完成速度提升了“10 倍”。

 

Cursor 面临的情况虽然十分严峻,但从一些数据上来看,“Cursor 将死”的观点又似乎站不住脚。

 

在近期 20VC 播客的另一场讨论中,嘉宾们针对 AI 编程工具市场呈现出的“体感落差”展开了深度辩论。尽管在社交媒体和初创开发圈中,似乎人人都在从Cursor转向Claude Code,但真实的数据却给出了一个令人震惊的反直觉答案。

播客提到,最近在 X 等平台上,开发者们普遍认为 Claude Code 已经全面压制了 Cursor。甚至在一些高增长初创公司的董事会上,年轻的 CTO 们会开玩笑说“只有爷爷辈的人还在用 Cursor”。

 

然而,传闻中的财务数据却击碎了这种“Cursor 已死”的叙事:Cursor 的年营收据称在过去 90 天内从10 亿美元翻倍至 20 亿美元,且正处于估值高达500 亿美元的新一轮融资传闻中。这种社交媒体上的“差评”与商业表现上的“狂飙”之间存在显著的脱节。

 

嘉宾们分析指出,这种脱节源于初创生态与企业级市场不同的运作逻辑。虽然个人开发者可以为了追求更新的模型、更快的体验今天用 Cursor 明天换 Claude Code,但大型金融机构或保守组织(如巴克莱银行)并非如此。主要因素有两点:

 

  • 极高的迁移门槛:像巴克莱银行这样的企业,批准一项智能编码工具需要经过漫长的采购流程、安全审查以及法务合同签署。一旦这些组织决定采用 Cursor,由于其已经集成了单点登录(SSO)、基于角色的访问控制(RBAC)、合规审计日志以及不保留数据的安全承诺,他们绝不会在第二天就因为某个新工具的出现而轻易更换。

  • 企业级运营的惯性:许多大公司签署的是年度合同,即便开发者个人有尝试新工具的冲动,公司在这一年内仍会继续稳步推广已获批的 Cursor。这种“巨大的市场拉力”正是其营收能够快速翻倍的底层驱动力。

 

企业信用卡公司 Ramp 和 Brex 的数据显示,截至 2 月份,Cursor 的收入仍在持续增长,尽管 Ramp 表示,企业在购买人工智能产品时采用 Cursor 的比例略有下降。

Cursor 自救之法:转向中国开源模型

 

Cursor 的领导层深知,软件开发的未来并非在于编写代码。为了应对这一趋势,他们一直在努力提升研发实力,力图通过发表研究成果和利用海量专有数据,在发布最佳编码模型方面超越 Anthropologie 和 OpenAI。此外,他们还开始优先考虑与大型企业签订合同,因为这类合同比面向消费者的订阅服务更加稳定。

 

目前,Cursor 的持续增长也伴随着巨大的焦虑。据知情人士透露,在公司内部,收入追踪过于分散精力,以至于公司停止在其 #numbers Slack 频道发布每日数据。

 

Cursor 的内部价值观中有一条直截了当的指令:“删除 Cursor”,并将新任务命名为“P0 #1”——优先级零:“构建最佳编码模型”,这表明公司未来的发展方向在于开发类似 Claude Code 和 Codex 的智能体。上周,Cursor 宣布对其“云智能体”产品进行重大更新。现在,多个智能体可以在各自专属的工作空间中同时处理不同的任务,并记录工作内容。

 

Cursor 内部的领导层认为,企业会重视不局限于单一模型提供商的产品,而随着模型功能日益增强,市场格局可能会对任何一方有利,因此,这对于开发人员来说是一个日益重要的问题。

 

与此同时,Cursor 也在努力减少对 Anthropic 和 OpenAI 的依赖。其理念是,即使竞争对手不断投资于规模更大的前沿模型,基于其专有数据训练的小型、专业化的编码模型也能有效参与竞争。

 

据知情人士透露,目前约有 20 名人工智能研究人员在 Cursor 的 Composer 模型上工作。这些模型基于 DeepSeek、Kimi 和 Qwen 等强大的中国开源模型构建,然后通过使用 Cursor 自有数据的额外训练和强化学习进行修改

 

这些努力已初见成效:Composer 1.5 速度很快,是该平台上第二受欢迎的模型,而且对于 Cursor 来说,运行成本远低于 Anthropic 的大型模型。但对于开发者而言,使用 Composer 1.5 仍然成本不菲:根据 Cursor 官网的数据,Composer 1.5 每百万个输入代币的成本为 3.5 美元,而 OpenAI 的 GPT-5.3 Codex 在 Cursor 平台上的成本为 1.75美元

 

成本始终是一个挑战。Cursor 的大型竞争对手愿意提供大幅补贴。据一位熟悉该公司内部分析的人士透露,Cursor 去年估计,每月 200 美元的 Claude Code 订阅可能需要高达 2000 美元的计算资源,这意味着 Anthropic 提供了大量的补贴。如今,这种补贴似乎更加激进,据另一位看过该公司计算支出模式分析的人士称,同样的 200 美元订阅计划可能需要消耗约 5000 美元的计算资源。

 

Cursor 也为部分用户提供补贴,但补贴力度似乎不如 Anthropic。据一位知情人士透露,Cursor 的消费者订阅业务利润率为负,但其企业套餐的利润率为正。使用 Cursor 的企业可以选择面向初创公司且易于取消的 Teams 套餐,也可以协商面向大型企业的企业合同。

 

拓展企业业务是实现稳定的途径之一。企业合同的签订速度较慢,但​​客户流失率较低。一位知情人士透露,Cursor 至今只流失过一两家企业客户。但这些备受青睐的企业合同历来在 Cursor 的业务中占比很小:据《福布斯》看到的资料显示,截至去年 11 月,Cursor 年度收入中只有 13.6% 来自企业合同。如今,据一位熟悉 Cursor 的人士透露,Cursor 约 60% 的收入来自企业客户,但《福布斯》无法确定其中有多少来自企业套餐。

 

该公司目前的员工构成体现了其对企业市场的重视:一半员工专注于市场推广职能。据知情人士透露,销售团队已与包括 Meta 和 Nvidia 在内的大客户签订了合同

 

现在,Cursor 正在努力寻找构建工具的最佳方案,以便管理数百个智能体同时协作,他们内部称之为“高效工作模式”。这其中涉及诸多复杂问题。他们需要找到为每个智能体分配特定角色的最佳方法。有时,智能体看到同事众多,就会变得懒散,工作效率下降——就像人类一样。

网友:没有护城河的产品走不到最后

 

在开发者社区中,围绕 Cursor 是否即将消亡的讨论也在迅速升温。一些开发者认为,这家曾经风头正劲的 AI 编程工具公司,如今正面临来自模型厂商的直接冲击。

 

有网友直言,Cursor 的“护城河”其实非常脆弱。一位用户调侃称,Cursor “曾经风光无限,但也就两分钟”,随着更强大的模型能力不断释放,其优势很快被削弱。在他看来,在 AI 编程工具领域,真正拥有长期护城河的往往是掌握底层模型能力的大公司,而不是基于模型构建工具的初创企业

 

也有开发者从产品体验角度给出了更为细致的评价。一位长期用户表示,自己目前主要使用 Claude Code 和 OpenCode,但仍认为 Cursor 的用户界面在同类产品中依然是最好的。不过,他认为 Cursor 在商业策略上的失误加速了用户流失。公司后来对订阅功能进行了较为严格的限制,导致不少用户开始寻找替代工具。

在技术层面,这位开发者依然肯定 Cursor 的一些核心能力。他指出,Cursor IDE 在代码索引和嵌入式检索方面表现出色,能够比传统的grep等工具更准确地定位代码上下文。此外,如果 Cursor 能进一步整合浏览器自动化能力,例如在 IDE 内置 Playwright 或 Chrome 的 MCP 接口,其开发工作流管理能力原本有机会变得更强。不过,他也强调,当前吸引开发者转向其他工具的关键因素,并不只是终端工具本身,而是模型性能的跃升——尤其是 Claude Opus 的表现。正是这一点,让越来越多开发者开始直接使用 Anthropic 的工具生态。

 

在价格和产品模式上,一些用户同样表达了不满。一位开发者指出,与其按月订阅 Cursor,不如直接使用模型厂商推出的编码工具。他评论道:

 

“通过 Codex 或 Claude Code,开发者可以用相同的费用获得更多 token 使用额度,并且通常还能获得按小时或按周计算的流量配额,如果需要更多算力,再额外付费即可。在这种模式下,传统的月度订阅看起来已经显得有些过时。”

 

与此同时,也有部分开发者对 Cursor 的未来持更为谨慎的态度。一位程序员表示,随着前沿模型实验室不断推出更强的编码智能体,Cursor 这样的工具确实可能面临被边缘化的风险。他提到,自己在尝试 Codex 几周后就取消了 Cursor 的订阅。不过,他也认为,在短期内 IDE 仍然不会消失。即便 AI 代理已经能够生成大量代码,他自己的项目中已有超过 90% 的代码由模型生成,但可靠的软件开发仍然需要严格的监督、系统化的代码审查以及工程化管理,而这些恰恰是 IDE 最擅长的领域。

 

当然,也有更为悲观的声音。一些网友甚至认为,Cursor 从一开始就只是一个“快速套现”的项目,并不具备长期发展空间。

此外,近期一位资深开发者在 Youtube 上发了一段视频,阐述了“退坑”Cursor 的原因,并揭示了这款明星产品在快速迭代中面临的工程化困境。

在 2024 年下半年,Cursor 凭借对 VS Code 插件生态的无缝兼容和堪称“黑魔法”的 Tab 代码补全模型赢得了大量忠实用户。当时的 Cursor 提供了极高的响应速度与极佳的性价比(Pro 订阅即可无限调用顶级大模型),让开发者在短短几个月内便能独自完成从代码评审器到复杂 AI 平台的多个项目,体验到了“超级赛亚人”般的开发快感。

 

随着版本的更迭,Cursor 的口碑开始出现滑坡。该开发者指出,随着微软加强对 VS Code 核心插件(如 Pylance)的控制,Cursor 的底层兼容性出现问题,甚至引发了系统崩溃。此外,其商业策略的变动也引发了信任危机:

 

  • 定价背信:取消了原有的无限快速请求,变相转为昂贵的 API 计费,且未能在第一时间透明化沟通。

  • UI 极度膨胀:2.0 版本强推的 Agent(智能体)窗口被指过于激进,频繁变动的布局破坏了开发者的肌肉记忆。

  • 性能黑洞: 多个 Chromium 进程导致硬件资源消耗剧增,昔日的生产力工具变成了拖慢系统节奏的“负担”。

 

回归终端,拥抱 CLI 智能体 促使该开发者彻底放弃 Cursor 的最后通牒,是 Claude Code 与 Codex 等 CLI(命令行)智能体的崛起。 在对比中发现,CLI 智能体在处理长程任务和并行开发时表现得更为自然,且订阅成本远低于 API 计费模式。该开发者最终选择了一套极简的“黑暗面”组合:Neovim + Tmux

 

这种转变代表了一种新的趋势:既然将智能体塞进编辑器会导致臃肿,不如将轻量化的编辑器直接搬进智能体所在的终端。

 

这位开发者最后给出的结论是:尽管 Cursor 对于追求可视化界面和单点任务的新手依然友好,但对于追求极致性能和自动化工作流的高级开发者来说,它已与最初的轻盈美感渐行渐远。在这场 AI 编程工具的竞赛中,更灵活、更透明的开源 CLI 工具正在成为效率派的新宠。

 

参考链接:

https://www.youtube.com/watch?v=7RHwE_M68BY

https://www.youtube.com/watch?v=dhUk6b0x_e0

https://forum.cursor.com/t/cursor-tab-is-not-working-on-windows-march-2026-please-help/154028

https://ischemist.com/writings/long-form/how-vibe-coding-killed-cursor

https://www.reddit.com/r/OpenAI/comments/1r22l1j/cursor_is_dying/

https://www.reddit.com/r/cursor/comments/1rjqupl/cursor_revenue_leak_2_billion_annual_sales_rate/

一、为什么需要ETL与Hudi集成

随着企业数据规模的爆发式增长,传统的数据仓库架构已难以满足业务对实时性和灵活性的需求。Apache Hudi作为新一代流式数据湖框架,将流处理的能力引入数据湖,实现了批流一体的数据管理范式。

然而,将业务数据高效写入Hudi数据湖并与现有ETL流程无缝衔接,是许多企业面临的技术挑战。传统的做法是通过多级数据搬运:先写入Kafka,再由Spark/Flink消费后写入Hudi。这种方案虽然可行,但架构复杂、延迟较高、维护成本居高不下。

1.传统方案痛点

架构复杂、延迟高、组件多、运维难

2.集成后优势

一站式写入、分钟级延迟、统一管理

3.业务价值

降本增效、数据实时可用、分析更灵活

image.png

二、Apache Hudi核心概念解析

Apache Hudi(Hive Update, Deletion, and Insertion)是Uber开源的流式数据湖框架,于2020年晋升为Apache顶级项目。它在HDFS/云存储之上提供了类似于数据库的ACID事务能力,支持增量处理和模式演化。

Hudi三大表类型

Copy On Write (COW)

写入时直接重写数据文件,无压缩合并。适合写少读多的场景,读取性能最优。

Merge On Read (MOR)

数据先写入日志文件,读取时合并。适合写多读少的场景,写入性能最优。

Log (仅MOR)

增量日志方式存储最新写入,兼顾实时性与压缩优化。

image.png

Hudi四种查询类型

c0e40bf8-25b8-40ed-8f1f-e2b939969ec1.png

三、ETLCloud集成Hudi实战

ETLCloud提供了开箱即用的Hudi集成能力,支持将任意数据源的数据直接写入Hudi数据湖。整个过程可视化配置,无需编写代码。

操作步骤一:创建Hudi数据目标

  • 在ETLCloud「数据目标」页面,选择「Hudi」类型;
  • 配置Hudi表参数:表名、存储路径(HDFS/S3)、表类型(COW/MOR);
  • 设置分区策略:按日期/业务ID/动态分区;
  • 配置写入参数:压缩策略、并发度、记录键字段;

操作步骤二:配置ETL转换流程

  • 拖拽创建「数据源」节点 → 「数据转换」节点 → 「Hudi输出」节点;
  • 在数据转换节点中配置字段映射、类型转换、数据清洗规则;
  • 设置Hudi输出节点的Upsert策略:Insert if Not Exists / Update if Exists;

操作步骤三:执行与监控

  • 点击「运行」按钮,任务将以Spark引擎执行;
  • 在「运行监控」页面查看写入进度、延迟、数据量;
  • 支持异常告警配置,数据写入失败自动通知;

四、最佳实践与性能优化

1.表类型选择建议

Copy On Write (COW)

适合读多写少场景,如数据仓库、历史数据分析。读取时无需合并,延迟更低。

Merge On Read (MOR)

适合写多读少场景,如实时数仓CDC写入。写入性能更高,存储更紧凑。

2.分区策略优化

  • 按日期分区:最常用策略,便于数据生命周期管理和历史数据清理
  • 按业务ID分区:避免小文件问题,提升查询性能
  • 动态分区:根据数据内容自动创建分区,减少元数据管理开销

3.写入性能调优

  • 调整并发度:根据集群资源合理配置写入并发,通常建议4-8个并发任务
  • 小文件合并:配置自动合并策略,避免小文件影响读取性能
  • 批量提交:合理设置commit间隔,在延迟与吞吐量间取得平衡

五、总结

ETL与数据湖Hudi的集成是构建现代流式数据架构的关键一环。通过ETLCloud的可视化配置,企业可以快速实现数据源到Hudi的高效写入,无需深入了解底层技术细节。掌握Hudi的表类型选择、分区策略和性能调优,将帮助企业更好地发挥数据湖的价值,支撑实时分析与AI数据需求。

背景依赖增多导致环境复杂度上升

如何才能在真正使用到某个依赖时动态加载该依赖呢?

import lazyllm
# 只用核心能力时,我们不希望触发 tools/rag 依赖检查
chat = lazyllm.OnlineChatModule(source="openai")
print("core ok")
# 一旦访问 Document,才会触发懒加载链路:
# lazyllm.Document
# -> import lazyllm.tools
# -> import lazyllm.tools.rag
# -> check_dependency_by_group('rag')
from lazyllm import Document  # 若缺少 rag 依赖,会在这里抛 ImportError

在较为复杂的 Python 项目中,常见问题是:代码尚未进入核心逻辑,运行环境会因为依赖冲突、缺失或版本不兼容而失败;不同模块往往依赖不同的第三方库。若将所有依赖统一安装,环境会迅速膨胀;若仅安装部分依赖,又容易在执行过程中因缺少依赖而中断。更进一步,即使框架自身依赖关系已经梳理清楚,也仍可能与用户本地环境产生版本或分发差异。这是 Python 生态中常见的依赖管理上的典型挑战。

难点依赖库难以管理,且相关报错不友好

业界常见的解决方案以及难点:

1️⃣全量安装依赖

import lazyllm
# 如果此句直接加载全量依赖会耗时很长,且会导致环境臃肿。用户不会用到的依赖白白占用空间。

2️⃣在用到某依赖时动态import

def func():
  from lazyllm import OnlineChatModule
  pass
# 调用到func时再加载某些依赖会过晚暴露问题,增加开发难度,降低开发者的用户体验

3️⃣缺少依赖或依赖冲突的导致的问题直接打印在海量日志中:关键依赖缺失信息会淹没在其他日志中,增加用户修复环境的成本。

解决方案按需加载与集中检查相结合

LazyLLM 采用按需加载策略:未使用的功能不预先要求安装 ;当用户首次调用相关能力时,框架对该功能组的依赖进行集中校验

以 rag 为例,当用户首次使用相关能力时,LazyLLM 会:

1.一次性检查该功能组所需的全部依赖

2.明确列出缺失的包列表

3.给出统一的安装指令:lazyllm install rag

依赖组的版本约束由 LazyLLM 的预置逻辑管理,用户无需手动查阅兼容矩阵或推断版本组合。总体体验可以概括为:未使用的能力不引入依赖;使用时一次性补齐依赖并可直接继续运行。

下文进入实现细节👇

机制总览

LazyLLM 的延迟加载三层模型:

接下来依次揭开他们的神秘面纱👇

1️⃣顶层懒加载:lazyllm.__getattr__

  • 解决的问题:

加载顶层模块时直接加载所有依赖。

  • lazy的方法:

通过__getattr__ 实现动态加载用户想要加载的子模块,在模块名合法的情况下动态调用该子模块内部的依赖加载流程,能让用户省略子模块路径。

getattr 的作用:当用户访问不存在的属性时,python会调用 obj.__getattr__(name) 以动态实现属性加载逻辑。

  • 关键代码:
# 文件:lazyllm/__init__.py
def __getattr__(name: str):
    if name == 'tools': # 调用中间模块的加载逻辑
        return importlib.import_module('lazyllm.tools')
    elif name in __all__:
        tools = importlib.import_module('lazyllm.tools')
        builtins.globals()[name] = value = getattr(tools, name)  # 导入后会缓存导入结果以避免后续重复导入
        return value
    raise AttributeError(f"module 'lazyllm' has no attribute '{name}'") # 保证加载在顶层init中声明/暴露出来的子模块
  • 效果:

👉顶层 API 暴露完整,但实际导入延后到首次访问
👉导入后写入 globals(),后续访问几乎无额外开销

2️⃣工具子模块懒加载:lazyllm.tools.__getattr__

  • 解决的问题:

加载子模块时直接加载所有依赖。

  • lazy的方法:

与顶层__init_.py 类似,lazyllm.tools 也不会一次性导入所有子模块,而是根据名称映射到具体模块并按需加载。

  • 关键代码:
# lazyllm/tools/__init__.py
def __getattr__(name: str):
    if name == 'fc_register':
        agent = import_module('.agent', package=__package__)
        globals()['fc_register'] = value = agent.register
    elif name in _SUBMOD_MAP:
        return import_module(f'.{name}', package=__package__)
    elif name in _SUBMOD_MAP_REVERSE:
        module = import_module(f'.{_SUBMOD_MAP_REVERSE[name]}', package=__package__)
        globals()[name] = value = getattr(module, name)
    return value

其中_SUBMOD_MAP, _SUBMOD_MAP_REVERSE 用于指定"类名 → 子模块"之间的映射。​​​​​​​

_SUBMOD_MAP = {
    'rag': ['Document', 'Reranker', 'Retriever', 'SentenceSplitter', 'LLMParser'],
    'agent': ['ToolManager', 'FunctionCall', 'ReactAgent', 'PlanAndSolveAgent', 'ReWOOAgent'],
    'sql': ['SqlManager', 'MongoDBManager', 'DBManager', 'DBResult', 'DBStatus'],
    # 其他模块略
}
  • 效果:

当用户编写 from lazyllm.tools import Document 时,才会实际导入 lazyllm.tools.rag。

3️⃣依赖集中检查:子模块导入时统一检测

  • 解决的问题:

👉实际使用依赖时才暴露缺少依赖。

👉多个同时需要的依赖出现异常时,提示分散在不同的场景、log、时间等维度。

  • lazy的方法:

在子模块被导入时触发整个依赖组的检查。

  • 关键代码:
# lazyllm/tools/rag/__init__.py
from lazyllm.thirdparty import check_dependency_by_group
check_dependency_by_group('rag') # 模块init中指定该子模块隶属于哪个依赖组
# lazyllm/thirdparty/__init__.py
def check_dependency_by_group(group_name: str):
    missing_pack = []
    for name in load_toml_dep_group(group_name): # 依赖分组信息直接从整个工程的配置toml文件中获取
        real_name = package_name_map_reverse.get(name, name)
        if not (config['init_doc'] and real_name in module_names or check_package_installed(real_name)):
            missing_pack.append(name)
    if len(missing_pack) > 0:
        msg = f'Missing package(s): {missing_pack}\nYou can install them by:\n    lazyllm install {group_name}'
        LOG.error(msg) # 提示安装依赖组的命令
        raise ImportError(msg)
  • 效果:

👉对功能组进行整体校验
👉一次性列出缺失依赖

LazyLLM 将功能组(如 rag、rag-advanced、agent-advanced)的依赖声明在 pyproject.toml 的 extras 中,同时 lazyllm install 基于同一份配置解析并安装对应版本约束。因此,报错信息与安装命令在同一来源上生成:提示缺什么、安装什么、版本约束是什么保持一致。

👉提供明确的安装命令

4️⃣BONUS: 第三方包的延迟导入封装

当用户确实需要使用特定的三方依赖时也可以复用lazyllm中的延迟加载逻辑

  • lazy的方法:

LazyLLM 对第三方库(例如 torch、transformers)也提供了延迟加载封装。在lazyllm.thirdparty 中使用__getattribute__ 动态检查依赖的安装情况。

  • 关键代码:
# lazyllm/thirdparty/__init__.py
class PackageWrapper(object):
    def __getattribute__(self, __name):
        if self._Wrapper__lib is None:
            try:
                self._Wrapper__lib = importlib.import_module(self._Wrapper__key, package=self._Wrapper__package)
            except ImportError:
                pip_cmd = get_pip_install_cmd([self._Wrapper__key])
                err_msg = f'Cannot import module `{self._Wrapper__key}`, please install it by `{pip_cmd}`'
                raise ImportError(err_msg) from None
        return getattr(self._Wrapper__lib, __name)
  • 效果:

👉该机制将"运行到一半才报错"的问题前置到"首次使用即报错",并提供可直接执行的安装建议(包含版本约束时亦可体现)。

👉这样导入时 from lazyllm.thirdparty import transformers,lazyllm并不会立即导入真实库;直到首次访问其属性时才进行导入,并在缺失时给出明确的安装建议。

使用方式(覆盖常见场景)

情况1️⃣:如果你想仅使用最简功能,可以仅加载顶层模块​​​​​​​

import lazyllm
llm = lazyllm.OnlineChatModule(source="openai")

核心模块属于 lazyllm 的基础依赖,不需要额外安装。

情况2️⃣:如果你的开发涉及 RAG,可以通过顶层模块加载子模块(推荐方式)​​​​​​​

from lazyllm import Document, Retriever
docs = Document(dataset_path="/path/to/data", embed=lazyllm.OnlineEmbeddingModule())
retriever = Retriever(docs)

首次导入 Document 时会触发 rag 依赖检查。若缺少依赖,将提示:​​​​​​​

Missing package(s): [...]
You can install them by:
    lazyllm install rag

然后用户可执行安装命令一次性补全依赖:

lazyllm install rag

情况3️⃣:如果你清楚的知道自己需要什么,可以直接使用子模块

from lazyllm.tools.rag import Document, SentenceSplitter

该方式会直接导入 lazyllm.tools.rag,因此依赖检查会立即触发。

情况4️⃣:如果你需要使用全部 RAG 或 Agent的能力,则需要手动安装相关依赖组

若使用向量数据库、Embedding 微调或 MCP 等高级能力,可安装对应扩展组:​​​​​​​

lazyllm install rag-advanced
lazyllm install agent-advanced

这些扩展组来自 pyproject.toml 的 extras 配置,安装时会自动应用版本约束,通常无需人工拼装依赖版本。更多可安装的依赖场景见官网**安装引导文档(https://docs.lazyllm.ai/zh-cn/stable/)**或项目根目录中 pyproject.toml 的配置。

情况5️⃣:如果你想使用某个具体的第三方库,可以使用lazyllm.thirdparty 导入​​​​​​​

from lazyllm.thirdparty import transformers
tokenizer = transformers.AutoTokenizer.from_pretrained("...")

若缺少依赖,会直接提示执行带版本约束的 pip install ...。

小结

通过上面的介绍,相信你已经对 LazyLLM 这套"按需加载 + 集中检查"的依赖管理思路有了更直观的认识:不用的能力不必提前安装,真正用到时再一次性把依赖校验清楚,并给出可直接执行的安装指令。

简单回顾一下它解决的核心问题:

  • 让环境更轻量: 未使用的功能不引入额外依赖,避免安装集合不断膨胀。
  • 让报错更友好: 缺失依赖在首次使用时就明确提示,并一次性列全,减少反复试错。
  • 让安装更稳定: 依赖组与版本约束统一来源于同一份配置,提示与安装行为一致。

如果你也在维护一个功能模块多、依赖差异大的 Python 项目,这种设计思路通常会带来非常实际的收益:启动更快、环境更稳、问题暴露更早,用户使用门槛也更低。

欢迎升级体验 LazyLLM最新版本,请大家去github上点一个免费的star,支持一下~(也欢迎关注LazyLLM gzh哦!)

LazyLLM项目仓库链接🔗:

背景依赖增多导致环境复杂度上升

如何才能在真正使用到某个依赖时动态加载该依赖呢?

import lazyllm
# 只用核心能力时,我们不希望触发 tools/rag 依赖检查
chat = lazyllm.OnlineChatModule(source="openai")
print("core ok")
# 一旦访问 Document,才会触发懒加载链路:
# lazyllm.Document
# -> import lazyllm.tools
# -> import lazyllm.tools.rag
# -> check_dependency_by_group('rag')
from lazyllm import Document  # 若缺少 rag 依赖,会在这里抛 ImportError

在较为复杂的 Python 项目中,常见问题是:代码尚未进入核心逻辑,运行环境会因为依赖冲突、缺失或版本不兼容而失败;不同模块往往依赖不同的第三方库。若将所有依赖统一安装,环境会迅速膨胀;若仅安装部分依赖,又容易在执行过程中因缺少依赖而中断。更进一步,即使框架自身依赖关系已经梳理清楚,也仍可能与用户本地环境产生版本或分发差异。这是 Python 生态中常见的依赖管理上的典型挑战。

难点依赖库难以管理,且相关报错不友好

业界常见的解决方案以及难点:

1️⃣全量安装依赖

import lazyllm
# 如果此句直接加载全量依赖会耗时很长,且会导致环境臃肿。用户不会用到的依赖白白占用空间。

2️⃣在用到某依赖时动态import

def func():
  from lazyllm import OnlineChatModule
  pass
# 调用到func时再加载某些依赖会过晚暴露问题,增加开发难度,降低开发者的用户体验

3️⃣缺少依赖或依赖冲突的导致的问题直接打印在海量日志中:关键依赖缺失信息会淹没在其他日志中,增加用户修复环境的成本。

解决方案按需加载与集中检查相结合

LazyLLM 采用按需加载策略:未使用的功能不预先要求安装 ;当用户首次调用相关能力时,框架对该功能组的依赖进行集中校验

以 rag 为例,当用户首次使用相关能力时,LazyLLM 会:

1.一次性检查该功能组所需的全部依赖

2.明确列出缺失的包列表

3.给出统一的安装指令:lazyllm install rag

依赖组的版本约束由 LazyLLM 的预置逻辑管理,用户无需手动查阅兼容矩阵或推断版本组合。总体体验可以概括为:未使用的能力不引入依赖;使用时一次性补齐依赖并可直接继续运行。

下文进入实现细节👇

机制总览

LazyLLM 的延迟加载三层模型:

接下来依次揭开他们的神秘面纱👇

1️⃣顶层懒加载:lazyllm.__getattr__

  • 解决的问题:

加载顶层模块时直接加载所有依赖。

  • lazy的方法:

通过__getattr__ 实现动态加载用户想要加载的子模块,在模块名合法的情况下动态调用该子模块内部的依赖加载流程,能让用户省略子模块路径。

getattr 的作用:当用户访问不存在的属性时,python会调用 obj.__getattr__(name) 以动态实现属性加载逻辑。

  • 关键代码:
# 文件:lazyllm/__init__.py
def __getattr__(name: str):
    if name == 'tools': # 调用中间模块的加载逻辑
        return importlib.import_module('lazyllm.tools')
    elif name in __all__:
        tools = importlib.import_module('lazyllm.tools')
        builtins.globals()[name] = value = getattr(tools, name)  # 导入后会缓存导入结果以避免后续重复导入
        return value
    raise AttributeError(f"module 'lazyllm' has no attribute '{name}'") # 保证加载在顶层init中声明/暴露出来的子模块
  • 效果:

👉顶层 API 暴露完整,但实际导入延后到首次访问
👉导入后写入 globals(),后续访问几乎无额外开销

2️⃣工具子模块懒加载:lazyllm.tools.__getattr__

  • 解决的问题:

加载子模块时直接加载所有依赖。

  • lazy的方法:

与顶层__init_.py 类似,lazyllm.tools 也不会一次性导入所有子模块,而是根据名称映射到具体模块并按需加载。

  • 关键代码:
# lazyllm/tools/__init__.py
def __getattr__(name: str):
    if name == 'fc_register':
        agent = import_module('.agent', package=__package__)
        globals()['fc_register'] = value = agent.register
    elif name in _SUBMOD_MAP:
        return import_module(f'.{name}', package=__package__)
    elif name in _SUBMOD_MAP_REVERSE:
        module = import_module(f'.{_SUBMOD_MAP_REVERSE[name]}', package=__package__)
        globals()[name] = value = getattr(module, name)
    return value

其中_SUBMOD_MAP, _SUBMOD_MAP_REVERSE 用于指定"类名 → 子模块"之间的映射。​​​​​​​

_SUBMOD_MAP = {
    'rag': ['Document', 'Reranker', 'Retriever', 'SentenceSplitter', 'LLMParser'],
    'agent': ['ToolManager', 'FunctionCall', 'ReactAgent', 'PlanAndSolveAgent', 'ReWOOAgent'],
    'sql': ['SqlManager', 'MongoDBManager', 'DBManager', 'DBResult', 'DBStatus'],
    # 其他模块略
}
  • 效果:

当用户编写 from lazyllm.tools import Document 时,才会实际导入 lazyllm.tools.rag。

3️⃣依赖集中检查:子模块导入时统一检测

  • 解决的问题:

👉实际使用依赖时才暴露缺少依赖。

👉多个同时需要的依赖出现异常时,提示分散在不同的场景、log、时间等维度。

  • lazy的方法:

在子模块被导入时触发整个依赖组的检查。

  • 关键代码:
# lazyllm/tools/rag/__init__.py
from lazyllm.thirdparty import check_dependency_by_group
check_dependency_by_group('rag') # 模块init中指定该子模块隶属于哪个依赖组
# lazyllm/thirdparty/__init__.py
def check_dependency_by_group(group_name: str):
    missing_pack = []
    for name in load_toml_dep_group(group_name): # 依赖分组信息直接从整个工程的配置toml文件中获取
        real_name = package_name_map_reverse.get(name, name)
        if not (config['init_doc'] and real_name in module_names or check_package_installed(real_name)):
            missing_pack.append(name)
    if len(missing_pack) > 0:
        msg = f'Missing package(s): {missing_pack}\nYou can install them by:\n    lazyllm install {group_name}'
        LOG.error(msg) # 提示安装依赖组的命令
        raise ImportError(msg)
  • 效果:

👉对功能组进行整体校验
👉一次性列出缺失依赖

LazyLLM 将功能组(如 rag、rag-advanced、agent-advanced)的依赖声明在 pyproject.toml 的 extras 中,同时 lazyllm install 基于同一份配置解析并安装对应版本约束。因此,报错信息与安装命令在同一来源上生成:提示缺什么、安装什么、版本约束是什么保持一致。

👉提供明确的安装命令

4️⃣BONUS: 第三方包的延迟导入封装

当用户确实需要使用特定的三方依赖时也可以复用lazyllm中的延迟加载逻辑

  • lazy的方法:

LazyLLM 对第三方库(例如 torch、transformers)也提供了延迟加载封装。在lazyllm.thirdparty 中使用__getattribute__ 动态检查依赖的安装情况。

  • 关键代码:
# lazyllm/thirdparty/__init__.py
class PackageWrapper(object):
    def __getattribute__(self, __name):
        if self._Wrapper__lib is None:
            try:
                self._Wrapper__lib = importlib.import_module(self._Wrapper__key, package=self._Wrapper__package)
            except ImportError:
                pip_cmd = get_pip_install_cmd([self._Wrapper__key])
                err_msg = f'Cannot import module `{self._Wrapper__key}`, please install it by `{pip_cmd}`'
                raise ImportError(err_msg) from None
        return getattr(self._Wrapper__lib, __name)
  • 效果:

👉该机制将"运行到一半才报错"的问题前置到"首次使用即报错",并提供可直接执行的安装建议(包含版本约束时亦可体现)。

👉这样导入时 from lazyllm.thirdparty import transformers,lazyllm并不会立即导入真实库;直到首次访问其属性时才进行导入,并在缺失时给出明确的安装建议。

使用方式(覆盖常见场景)

情况1️⃣:如果你想仅使用最简功能,可以仅加载顶层模块​​​​​​​

import lazyllm
llm = lazyllm.OnlineChatModule(source="openai")

核心模块属于 lazyllm 的基础依赖,不需要额外安装。

情况2️⃣:如果你的开发涉及 RAG,可以通过顶层模块加载子模块(推荐方式)​​​​​​​

from lazyllm import Document, Retriever
docs = Document(dataset_path="/path/to/data", embed=lazyllm.OnlineEmbeddingModule())
retriever = Retriever(docs)

首次导入 Document 时会触发 rag 依赖检查。若缺少依赖,将提示:​​​​​​​

Missing package(s): [...]
You can install them by:
    lazyllm install rag

然后用户可执行安装命令一次性补全依赖:

lazyllm install rag

情况3️⃣:如果你清楚的知道自己需要什么,可以直接使用子模块

from lazyllm.tools.rag import Document, SentenceSplitter

该方式会直接导入 lazyllm.tools.rag,因此依赖检查会立即触发。

情况4️⃣:如果你需要使用全部 RAG 或 Agent的能力,则需要手动安装相关依赖组

若使用向量数据库、Embedding 微调或 MCP 等高级能力,可安装对应扩展组:​​​​​​​

lazyllm install rag-advanced
lazyllm install agent-advanced

这些扩展组来自 pyproject.toml 的 extras 配置,安装时会自动应用版本约束,通常无需人工拼装依赖版本。更多可安装的依赖场景见官网**安装引导文档(https://docs.lazyllm.ai/zh-cn/stable/)**或项目根目录中 pyproject.toml 的配置。

情况5️⃣:如果你想使用某个具体的第三方库,可以使用lazyllm.thirdparty 导入​​​​​​​

from lazyllm.thirdparty import transformers
tokenizer = transformers.AutoTokenizer.from_pretrained("...")

若缺少依赖,会直接提示执行带版本约束的 pip install ...。

小结

通过上面的介绍,相信你已经对 LazyLLM 这套"按需加载 + 集中检查"的依赖管理思路有了更直观的认识:不用的能力不必提前安装,真正用到时再一次性把依赖校验清楚,并给出可直接执行的安装指令。

简单回顾一下它解决的核心问题:

  • 让环境更轻量: 未使用的功能不引入额外依赖,避免安装集合不断膨胀。
  • 让报错更友好: 缺失依赖在首次使用时就明确提示,并一次性列全,减少反复试错。
  • 让安装更稳定: 依赖组与版本约束统一来源于同一份配置,提示与安装行为一致。

如果你也在维护一个功能模块多、依赖差异大的 Python 项目,这种设计思路通常会带来非常实际的收益:启动更快、环境更稳、问题暴露更早,用户使用门槛也更低。

欢迎升级体验 LazyLLM最新版本,请大家去github上点一个免费的star,支持一下~(也欢迎关注LazyLLM gzh哦!)

LazyLLM项目仓库链接🔗:

在轻工行业摸爬滚打多年,无论是家居、日化、纺织还是五金、文体领域,身边几乎所有同行都有一个共同的感慨:做产品容易,做客户难。轻工行业天生带着“SKU繁杂、渠道分散、经销商层级多、终端触达弱”的基因,客户全流程管理从线索获取到售后复购,每一个环节都藏着看不见的坑。

我们常常陷入这样的困境:业务员离职,手里的客户资源就跟着“蒸发”,新接手的人要重新摸索,客户早已被竞品挖走多级经销商层层加价,窜货现象屡禁不止,总部却看不到终端真实的销售数据,不知道货到底卖给了谁;订单跟进全靠口头沟通,样品寄送、报价协商没有痕迹可查,出错率居高不下营销活动只会盲目降价促销,不知道哪些客户是高价值群体,哪些沉睡客户可以唤醒,投入大量成本却收效甚微

这些痛点不是个例,而是轻工行业客户管理的普遍现状。随着数字化转型成为行业必答题,90%的B2B交易从线上开始,85%的客户会通过社媒研究产品,传统的“Excel管理+人工跟进”模式,早已跟不上行业发展的节奏,甚至成为制约企业增长的瓶颈。很多企业意识到,客户不是“一次性交易”的对象,而是需要长期经营的资产,唯有打通客户全流程管理的堵点,才能在存量竞争中站稳脚跟

那么,轻工企业该如何跳出“被动救火”的怪圈,实现客户管理的规范化、高效化?其实,答案就藏在“数字化工具+精细化运营”的组合中。接下来,我以珍客AI CRM为例,从技术落地和场景适配的角度,跟大家聊聊它是怎么解决这些实际痛点的。

在先说说最让人头疼的客户信息散乱问题。从技术层面来讲,珍客AI CRM的核心逻辑,就是把分散的客户资产集中沉淀下来。不管是经销商、二批商,还是终端门店、消费者,以前散落在业务员个人手里的信息,都能统一录入系统,系统会自动整合工商信息、历史订单、沟通记录这些数据,自动生成完整的客户档案

这里有个很实用的点,就是业务员离职后,客户资源不会跟着流失,新员工登录系统,就能快速摸清客户的所有情况,无缝衔接跟进,再也不用重新摸索。而且系统会自动给客户打标签、分层,不用人工一个个分类,避免了“眉毛胡子一把抓”的尴尬,这其实就是AI技术在客户分层上的基础应用,不复杂,但特别实用。

再看渠道管控的难题,轻工行业多级经销的模式,一直以来都很难打通数据壁垒。珍客AI CRM在这一块的设计,其实是抓住了“全渠道数据打通”这个核心——通过技术对接,总部能实时看到终端的销售数据、库存情况,货物流向一目了然,窜货、乱价的问题,就能从源头得到管控。

珍客CRM 线索管理

比如家居、日化这些需要触达C端消费者的领域,系统还能对接小程序、公众号,把终端用户的数据沉淀下来,打破了以前“只做经销商生意、不接触消费者”的局限。我见过一家纺织企业,用这个系统做面料样品的数字化管理,客户在线就能检索样品,不用再寄实体样品,成本直接降了70%多,沟通效率也提上去了,这就是技术适配场景的实际价值。

销售过程的透明化,也是很多企业的需求。以前跟单全靠人工记,漏跟进、重复跟进是常事,新业务员上手慢,老业务员效率也上不去。珍客AI CRM的做法,不是搞复杂的流程,而是把跟进过程数字化——销售能实时记录沟通细节,系统还能设置跟进提醒,更重要的是,AI能根据客户的行为、沟通记录,自动判断客户意向,给业务员提跟进建议。

珍客CRM 全流程智能提醒,辅助销售

这样一来,业务员就能把精力放在高潜力客户身上,成单周期也能缩短。另外,系统能打通订单、生产、库存、发货的联动,SKU再多,也能精准匹配规格、材质,减少订单出错率,不用再天天应付客户的催单,这一点,对于SKU繁杂的轻工企业来说,真的太刚需了。

还有营销复购的问题,很多轻工企业都陷入了“降价促销=有效营销”的误区。从技术层面看,珍客AI CRM的核心优势,就是AI驱动的精准运营——系统会分析客户的历史消费数据、兴趣偏好,自动识别高价值客户、沉睡客户和潜在流失客户,针对性推送新品、活动,不用再盲目群发。

比如对长期合作的高价值经销商,推送专属返利政策;对沉睡的终端客户,推个优惠活动唤醒需求,让营销投入真正花在刀刃上。其实这就是AI在用户画像分析上的应用,没有什么高大上的噱头,就是实实在在帮企业提升复购率,减少客户流失

还有一个容易被忽略的点,就是数据孤岛。很多轻工企业的销量、回款、客户增长这些数据,都是靠人工汇总报表,不仅滞后,还容易出错,老板做决策全靠经验,心里没底。珍客AI CRM能自动整合销售、渠道、售后等全链路数据,生成可视化报表,核心指标一目了然,老板能实时掌握经营情况,不用再靠“拍脑袋”决策。

珍客CRM 自定义可视化BI报表

而且它能和金蝶、用友这些ERP系统,还有企业微信对接,数据能在各部门之间顺畅流转,跨部门协同效率也能提上去。这里要说明一点,不是说这个系统有多完美,而是它的技术设计,刚好踩中了轻工行业的痛点,没有搞那些华而不实的功能,贴合实际业务场景,这才是最关键的。

其实站在第三方产品技术的角度来看,轻工企业的客户管理,从来都不是“靠人硬扛”,而是靠系统赋能。我们见过太多企业,花大价钱上复杂的系统,最后因为不贴合场景,根本用不起来。珍客AI CRM之所以能被不少轻工企业认可,核心就是它懂行业、接地气,把复杂的客户管理流程简化、智能化,让企业能把精力放在产品和客户需求上,而不是繁琐的人工工作里。

现在轻工行业的竞争,早就从产品竞争变成了客户竞争,谁能管好客户、服务好客户,谁就能站稳脚跟。摆脱“被动救火”的客户管理模式,借助AI CRM这样贴合场景的数字化工具,实现精细化运营,才能让客户资产持续增值,企业才能在行业洗牌中走得更稳。毕竟,对轻工企业来说,最好的增长,从来都是把现有客户服务好,这也是数字化工具真正的价值所在。

3 月 9 日,Anthropic 推出了一款新的代码审查产品 Code Review,主打在人工介入之前,先用 AI 自动检查 Pull Request 中的问题。 这项服务面向团队和企业用户,瞄准的是软件开发生命周期(SDLC)中一个越来越突出的新环节:代码写得越来越快,但代码审查正逐渐成为瓶颈。

 

和传统 IDE 内置的审查插件不同,Code Review 主要运行在 GitHub 和 CI 流程中,而不是开发者本地的 IDE 里。也就是说,它更像是被放在代码提交之后、人工审查之前的一道自动检查关卡。乍一看,这确实像是个很有吸引力的新功能——毕竟在 AI 持续抬高代码产能之后,代码审查已经成了越来越多团队不得不面对的新问题。

 

更贵,也更慢:Claude 开始收“审代码税”

 

实际上,Anthropic 的 Claude 模型本身已经具备按需进行代码审查的能力——甚至可以通过让 Claude 审查自己写的代码,来评估 AI 生成代码的质量。此外,公司还提供了 Claude Code GitHub Action,可以在 CI/CD 流水线中自动触发代码审查。而新的 Code Review 服务,则把这种能力做得更深入——当然,成本也更高。

“代码审查按 token 使用量计费,通常平均每次在 15 到 25 美元之间,具体费用取决于 Pull Request 的规模和复杂度。”

注意,Claude Code 创始人 Boris 强调了这是每个 Pull Request 的价格

 

从开发者实际测试来看,这个价格并不只是纸面数字。开发者 Sinai Gross 表示,他测试了 Claude Code 的代码审查功能,一次共审查了 3 个 Pull Request:其中两个 PR 各修改了约 750 行代码,另一个修改了约 100 行,系统给出的平均费用是 18.39 美元。

 

问题在于,这样的价格一旦放到真实的工程规模里,成本会迅速放大。网友 DanT(@uyintans) 就直接指出,如果一家中大型公司每天都会产生大量 Pull Request,而每个 PR 都要花 15 到 25 美元来审查,那么规模化之后,这笔费用很容易滚到每年数百万美元。

 

换句话说,假设一个团队一天产生 100 个 PR——这在大型工程团队里并不算夸张。按“每位工程师每天提交 1 到 2 个 PR”估算,一个 50 人的工程团队,一天就可能接近这个数量。若按平均 20 美元一次审查计算,一天就是 2000 美元,一年大约就是 70 多万美元。如果是更大的工程组织,PR 数量再翻几倍,整体成本很快就会迈入百万美元级别。

 

对比之下,做 AI 代码审查的 CodeRabbit,月费也不过 24 美元。也就是说,Claude 这套多跑两次 PR,花的钱可能就已经超过别人一个月。

 

Q:每次 PR 审查要 15–20 美元,对于一个个人项目来说实在太贵了。有没有更便宜甚至免费的替代方案,同时还能在测试阶段防止新的功能把生产环境搞崩?

A:CodeRabbit 对开源项目是免费的。

而且,Code Review 的速度也不算快。Anthropic 表示,具体耗时取决于 Pull Request 的规模,但平均一次审查需要约 20 分钟

 

原因在于,这些 Agent 并不只分析 PR 的改动部分,而是会把整个代码库作为上下文来分析。

 

Anthropic Claude Code 产品负责人 Cat Wu 认为,这样可以避免某个文件的改动在其他文件中引入新的 bug——例如不同模块之间存在一些意想不到的交互关系。

 

她解释说,他们希望这个系统“非常智能、非常彻底,而目前实现这一点的方式,就是让它运行得更久一些。”

 

“不过换来的结果是更可靠的输出。而且每个 Agent 不只是查看你修改的代码,它们可以灵活地遍历整个代码库。”

 

目前,这个工具只会在 Pull Request 创建时自动运行。对于简单的 PR,系统只会进行一次“轻量级扫描”;而对于复杂的 PR,则会调用更多 Agent,进行更深入的分析。

 

并行 Agent,效果还不差

 

在实际运行中,Code Review 会派出一组并行工作的 Agent,每个 Agent 负责寻找不同类型的错误。任务完成后,它们会在 Pull Request 中留下评论,总结发现的问题,并在必要时给出修复建议。

 

不过,这些 Agent 不会自动批准 Pull Request——最终是否通过,仍然由人类工程师决定。

 

这些 Agent 的重点是发现逻辑错误(logic errors),而不是代码风格问题。这是一个刻意的设计选择。 Cat Wu 表示,这样做可以减少误报。

 

她解释说:“很多时候人类在做代码审查时,不仅会发现逻辑错误,还会提出大量代码风格上的问题。我们发现,在 AI 生成的代码审查中,开发者最关心的其实是逻辑错误,所以这也是系统的核心关注点。”

 

“大家对误报非常敏感。如果我们只专注于逻辑错误和真正的 bug,那么误报率就会很低。因为一旦发现 bug,几乎肯定就应该修复。”

 

Anthropic 表示,他们已经在内部使用 Code Review 数月,并取得了相当不错的成果。公司称:

  • 对于超过 1000 行变更的大型 Pull Request,84% 的自动审查会发现值得关注的问题,平均 7.5 个问题。

  • 对于少于 50 行的小型 Pull Request,31% 会收到评论,平均 0.5 个问题。

而在人类开发者的反馈中,被 Claude 标记的问题里,不到 1% 会被工程师否决。

 

一些参与测试的客户也获得了实际收益。例如,当 TrueNAS 在其开源中间件中进行 ZFS 加密模块重构时,这个 AI 审查服务在相邻代码中发现了一个 bug:一个类型不匹配的问题,可能在同步操作时导致加密密钥缓存被清空。

Anthropic 还举了一个内部案例:Code Review 曾发现一处看似无害的一行代码修改,但如果合入生产环境,实际上会破坏服务的认证机制。该公司表示:“这个问题在代码合并之前就被修复了。事后这位工程师也表示,如果没有 AI 审查,他很可能自己发现不了。”

 

既当运动员,又当裁判

 

Anthropic 还强调说,过去一年,自家工程师的代码产量增长了 200%,代码审查反而成了新的瓶颈。公司还表示,很多客户也在遇到同样的问题:开发者已经忙不过来,很多 PR 最后只能草草看一眼,很难做深入审查。

 

这其实也是行业普遍现象:AI 让代码写得越来越快,但审代码的人并没有变多,PR 越来越多,审查自然就跟不上了。

 

不过网友的质疑也很直接:既然 Claude 能审代码,为什么不一开始就把代码写对?有人吐槽,现在看起来像是 Claude 先写代码,再让 Claude 自己审代码,最后还抓出一堆 Claude 自己写出来的 bug。还有人说得更犀利:这会不会违背了“裁判不能同时当刽子手(既当运动员,又当裁判)”的基本原则?万一 Claude Code 先制造出问题,再靠修复这些问题继续收费呢?

 

说白了,这看上去更像是在给模型自己的不稳定再加一层补丁——而且这层补丁,还要再收一笔钱。

 

参考链接:

https://x.com/bcherny/status/2031089411820228645

https://claude.com/blog/code-review

https://www.theregister.com/2026/03/09/anthropic_debuts_code_review/

最近,GitHub 发布了一份与 Anders Hejlsberg 的深度访谈,你可能不知道这个名字,但你肯定听说过 Turbo Pascal、Delphi、C# 和 TypeScript。

对,他就是 Turbo Pascal 和 Delphi 的创建者,C# 的首席架构师,以及 TypeScript 的设计者。

从这次深度访谈中,我总结出了一套构建能够经受规模化考验的系统模式。以下是我学到的 7 个经验:

1. 快速反馈:最为重要

Hejlsberg 说,Turbo Pascal 的成功不是因为 Pascal 语言本身有多好,而是因为它能让开发者能“立刻”看到结果。

同样,TypeScript 的价值不仅在于语言本身,更在于其强大的工具链:增量检查、快速响应,哪怕是在大型代码库。

image.png

所以快速反馈能够改变行为

当错误能够立刻出现,开发者会进行更多的重构和测试,于是问题在出现之初就被解决。

反之,当反馈延迟,团队则会通过通过约定俗成的规则、变通方案以及额外的流程开销来弥补。 

所以无论选择编程语言、框架还是内部工具,响应速度都至关重要。能够缩短编写代码与理解其后果之间距离的工具往往更受信任。

2. 团队协作:放下个人偏好

当 Hejlsberg 从单独工作转向带领团队,最难的调整不是技术层面,而是学会放弃个人偏好。

Anders Hejlsberg 说:“你必须接受事情的进展与你的预期有所不同。即便解决了这个问题,也改变不了事情的本质。”

这种思维方式远不止于适用于语言设计。

任何需要跨团队扩展的系统都需要从个人品味转向共同目标。

目标不再是编写符合你个人风格的代码,而是编写多人都能理解、维护和共同演进的代码。

C# 的诞生并非源于一个全新的理想,而是源于各种相互冲突的需求。Visual Basic 开发者追求易用性,C++ 开发者追求强大功能,而 Windows 则要求务实。

最终的结果并非理论上的纯粹,而是一种足够多的人能够有效使用的语言。

语言的成功并非源于其完美无缺的设计,而是源于其能够适应团队实际的工作方式。

image.png

3. 顺势而为:为什么 TypeScript 选择扩展 JavaScript

TypeScript 为什么能成功?

不是因为它比 JavaScript 更"完美",而是因为它选择了"扩展"而不是"替代"。

当时很多团队为了用上静态类型,直接用其他语言编译成 JavaScript。这种"推倒重来"的做法,要求开发者放弃现有的工具、库、思维模式——成本太高了!

TypeScript 的聪明之处就在于:我不要你放弃任何东西,我只是在你现有的基础上加点内容。

这背后其实也是妥协。

尊重现有工作流程的改进往往得到传播,需要全面替换的改进则很少能实现。

有意义的进展往往来自于让你已经依赖的系统变得更强大,而不是试图重新开始。

image.png

4. 透明化:公开透明建立信任

TypeScript 团队在 2014 年做了一个重要决定:完全开放开发过程,所有讨论都在 GitHub 上进行,让全世界都能看到他们是怎么做决策的。

这样做有什么好处?

开发者不仅能看到最终成果,还能理解为什么这么做。信任就这样建立起来了。

对团队来说,这也改变了工作优先级。他们可以直接查看开发者关心的问题,而不是猜测什么最重要。

所以最有效的开源项目不仅仅是分享代码。它们使决策过程可视化。这样贡献者和用户就能理解如何设定优先级,以及为什么做出权衡。

image.png

5. 必要的突破:什么时候该彻底重做

TypeScript 团队曾经用 JavaScript 来写 TypeScript 编译器,这在小项目时没问题。但随着项目越来越大,JavaScript 的单线程特性成了瓶颈。

这时他们做了一个艰难决定:把编译器用 Go 语言重写

这不是为了炫技,而是因为技术限制已经到了不突破就无法继续发展的地步。

而这次重写的目标是语义保真度。新编译器需要表现得与旧编译器完全一样,包括怪癖和边缘情况。

结果就是带来了显著的性能收益,而社区也不必重新学习编译器。

所以有时最负责任的选择不是雄心勃勃,完全重写,而是保持最小化破坏,并移除无法通过增量优化克服的硬限制。

6. AI 时代:基础比想象力更重要

Hejlsberg 对 AI 时代的编程有个很深刻的观点:

在 AI 能生成代码的时代,工具的价值不在于创造,而在于约束。

想象一下,如果 AI 可以写代码了,那程序员的价值在哪里?就在于他们知道什么时候约束 AI,知道什么是正确的,什么是错误的。

所以AI 辅助工作流程中最有价值的工具不是生成最多代码的工具,而是正确约束它的工具。

强大的类型系统、可靠的重构工具和准确的语义模型成为必不可少的护栏。它们提供了允许 AI 输出被审查、验证和有效纠正而不是盲目信任的结构。

image.png

7. 开放协作:至关重要

尽管面临资金和维护的挑战,Hejlsberg 对开放协作依然保持乐观。一个原因是制度记忆。哪怕是多年前的讨论、决策和权衡,仍然可以搜索和可见。

“我们有 12 年的历史记录在我们的项目中,”他解释道。“如果有人记得发生了讨论,我们通常能找到它。上下文不会消失在电子邮件或私人系统中。”

这种可见性改变了系统如何演变。设计时的辩论、被拒绝的想法和权衡在个人决策做出后很长时间仍然可访问。

对于以后加入项目的开发者来说,这种共享上下文常常与代码本身同样重要。

总结

你会发现,在 Anders Hejlsberg 40 年语言设计的历程中,同样的主题反复出现:

  • 快速反馈比优雅更重要
  • 系统需要容纳许多人编写不完美的代码
  • 行为兼容性往往比架构纯粹性更重要
  • 透明化的权衡取舍能够建立信任

这些并非次要因素,而是决定工具能否随着用户群体增长而不断适应的根本性决策

此外,它们也为创新奠定了基础,确保新想法能够在不破坏现有有效机制的前提下生根发芽。

对于任何致力于打造经久不衰的工具的人来说,这些基本要素与任何突破性功能都同样重要。而这或许才是最重要的一课。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

3 月 6 日,GPT-5.4 发布,那个熟悉的 OpenAI 又回来了。

 

GPT-5.4 是一款新的前沿模型,把 OpenAI 过去一段时间在推理能力(GPT-5.2)、顶级编程能力(GPT-5.3-Codex)以及原生计算机使用能力上的进展,整合到了同一个版本里。

 

这次发布的分量很重,光是“原生电脑操作”这一点,就已经足够吸引眼球,而当它再叠加顶级的专业知识工作能力、100 万 token 的上下文窗口,以及明显提升的工具使用效率时,对所有希望用 AI 工作、与 AI 协作,或者基于 AI 搭建系统的人来说,这都意味着一次真正意义上的能力跃升。

 

OpenAI 开始抢 OpenClaw 的地盘?

 

在这个新模型上,最大的变化就是原生电脑操作能力的到来。OpenAI 的原话是,GPT-5.4 是其“首个原生具备电脑操作能力的通用模型”。

 

OSWorld Verified 的 computer use 基准测试上从 47.3%提升到了 75%,而 BrowseComp 的准确率从 65.8% 提升至 82.7%。

 

这不只是“跑几个 shell 命令”那么简单,真正的意义在于:它可以进入你的桌面、访问网页,基本上能够在你的电脑上完成很多原本只有人来操作的事情,而这些事通常是我们平时通过网页端 ChatGPT 做不到的。

 

尤其是像 OpenClaw 这样的产品,在最近几个月,甚至可以说最近几周,突然变得非常火,核心原因就在于,它已经改变了我们使用 AI 模型的方式。过去,我们更多只是停留在网页端,通过 web app 和模型对话,电脑本地几乎没有真正参与进来。但现在,这种局面已经从根本上发生了变化。

 

从 OpenAI 给出的示例中,我们可以看到 GPT-5.4 可以熟练使用计算机,包括查看浏览器用户界面截图、点击界面、发送电子邮件以及安排日历。

 

另一个新的实验功能 “Playwright (Interactive)”,允许 Codex 实时进行 Web 和 Electron 应用的可视化调试,甚至能在构建应用的同时直接测试——这正是借助它的原生电脑操作能力实现的。

 

OpenAI 研究员 SQ Mah 表示,这背后主要有两项关键能力支撑:一是 CUA(computer use,计算机操作能力),二是通过图像输入生成高质量网站的能力。

 

与 GPT-5.3 Codex 相比,GPT-5.4 在使用 CUA 时,不再需要额外拉起一个全新的环境来执行操作。在 3D 游戏中,CUA 会自己点击游戏界面,移动象棋位置,甚至通过实际操作来验证规则是否正确生效。

 

在网站生成场景中,模型会调用 image gen 工具,生成图片,然后通过 CUA 来检查自己的工作:打开生成的图片、检查图片内容、打开网站页面也看一遍,然后把它们并排对比,确保生成的网站尽可能接近输入的那张图。

 

SQ Mah 还强调说,通过持久化的 CUA,他们发现,在一些让模型测试自己工作的场景中,token 使用量实际上下降了三分之二。

 

其实,OpenAI 早在去年 1 月就推出了 CUA,但出于安全性和准确性的考量,这个项目并没有真正被重视起来。

 

甚至一度让人怀疑,OpenAI 是否已经放弃了这条路线。特别是在 GPT-4o 等项目吸引了几乎全部关注的那段时间里,CUA 基本处于一种“销声匿迹”的状态。

他们是不是放弃这个项目了?现在一点消息都没有了。我其实一直在用 Azure/OpenAI,它已经预览好几个月了。虽然我申请了,但一直没能获得批准。

 

与 GPT-4o 等项目铺天盖地的宣传相比,CUA 基本上销声匿迹了。而且它目前仍处于预览阶段,这意味着访问权限受到严格限制,许多人甚至都无法尝试......不过我不认为这条路线已经失败。一旦“浏览器优先”的方案在稳定性、隐蔽性以及内置安全机制上真正成熟,它很可能会成为 agent 工作流的一次重大跃迁。

 

但从今天 GPT-5.4 的发布来看,情况显然变了。OpenAI 不仅重新把这项能力带回到台前,还在 GitHub 上新发布了一些的 CUA sample app。

 

CUA 让 ChatGPT 5.4 可以直接使用我们的电脑,这一点和 OpenClaw 的思路非常接近:本质上,大家都在争夺同一个入口——让 AI 直接使用电脑,而不再继续受限于 API 和聊天窗口。不同的是,OpenClaw 更像是在模型之外搭建的一层 computer-use 框架,而 GPT-5.4 走得更直接:它把电脑操作能力原生整合进了模型本身。

 

这意味着,一旦模型自身已经具备了这类能力,而且还能被各种软件、平台和企业系统直接集成调用,它的竞争力就会迅速放大。对于那些年营收做到千万、上亿,甚至百亿的公司来说,它们完全可以基于这样的模型能力,做出自己的“OpenClaw 版本”——而且往往会更安全、更快,也更可靠。

 

从这个角度看,OpenClaw 这样的开源项目依然很有价值,因为它们率先验证了“AI 直接使用电脑”这条路线;但当模型厂商开始把这种能力原生做进模型里,整个竞争的重心就会发生变化。大家比拼的将不再只是一个外部框架,而是谁能更快把这项能力产品化、平台化,并真正接入真实工作流。

 

所以在 agentic AI 能力这件事上,现在确实是一个非常令人兴奋的阶段。

 

一边降成本,一边降幻觉

 

这次升级明显是在“照顾开发者和重度用户”,其中一个关键原因是 GPT-5.4 带来了工具搜索(tool search):模型不再把所有工具的完整定义一次性塞进上下文(这可能导致每次请求额外烧掉数万 token),而是只拿到一个轻量列表,需要用哪个工具时再按需检索具体定义。

 

在 Scale 的 MCP Atlas 基准中,启用 36 个 MCP 服务器、测试 250 个任务时,tool-search 配置在不降低准确率的情况下,把总 token 使用量减少了 47%。对构建大型 agent 系统的开发者来说,这几乎等同于:成本更低、响应更快。

 

幻觉问题也显著下降。按 OpenAI 的说法,GPT-5.4 的单条事实陈述比 GPT-5.2 更不容易出错(错误概率降低 33%),整体回答包含错误的概率也降低了 18%——这对依赖准确输出的专业用户来说,是非常实用的一次升级。

 

与此同时,在 Harvey 的 BigLaw Bench(法律文档评测)中,GPT-5.4 的准确率达到了 91%。

 

编程能力也更强了

 

GPT-5.4 现在也成为 OpenAI 的主力编程模型——在大多数任务中,你不再需要在 ChatGPT 与 Codex 之间纠结选哪一个。

 

它在 SWE-Bench Pro 上与 GPT-5.3-Codex 持平或更强,同时也更快,尤其是在较低推理强度设置下。在对话里,你可以直接开始写代码,无需额外选择。

 

Codex 还新增了 fast mode,在所有支持的模型上带来最高 1.5 倍速度提升。OpenAI 还强调 GPT-5.4 在复杂前端任务上明显更强,输出既更精致好看,也更符合功能正确性。这一点,也已经从不少开发者的实际反馈中得到了印证。

 

能力升级,价格也升级

 

在 API 中,OpenAI 表示 GPT-5.4 Thinking 对应的模型名称为 gpt-5.4,而 GPT-5.4 Pro 则对应 gpt-5.4-pro。价格如下:

GPT-5.4:

  • 输入:$2.50 / 每 100 万 token

  • 输出:$15 / 每 100 万 token

GPT-5.4 Pro:

  • 输入:$30 / 每 100 万 token

  • 输出:$180 / 每 100 万 token

 

从整体来看,与目前市面上的模型相比,GPT-5.4 在 API 运行成本上属于较高的一档,如下表所示。

还有一个重要变化:在 GPT-5.4 中,如果请求的 输入 token 超过 272,000,费用将按正常价格的 2 倍计算,这反映了它支持比以往模型更大的提示上下文。

 

在 Codex 中,默认的 compaction(压缩)上限是 272k token。只有当输入超过 272k 时,才会触发更高的长上下文价格。这意味着开发者只要把提示控制在这个范围内,就不会触发额外费用;如果需要更长上下文,也可以通过提高 compaction 上限来实现,但只有这些更大的请求才会按更高费率计费。

 

OpenAI 发言人还表示,在 API 中 最大输出长度为 128,000 token,与之前的模型保持一致。

至于为什么 GPT-5.4 的基础价格更高,OpenAI 的解释主要有三个原因:

  1. 在复杂任务上的能力显著提升,包括编程、计算机操作、深度研究、高级文档生成和工具调用等;

  2. 来自 OpenAI 技术路线图的一系列研究突破;

  3. 推理效率更高,在完成相同任务时需要更少的推理 token。

 

同时他们也强调,即使价格有所上调,GPT-5.4 的定价仍然低于许多同级别的前沿模型。

 

参考链接:

https://www.isitgoodai.com/blog/openai-gpt-5-4-review

 

https://openai.com/zh-Hans-CN/index/computer-using-agent/

https://www.reddit.com/r/OpenAI/comments/1mwc03q/openai_computer_user_agent_cua/

https://venturebeat.com/technology/openai-launches-gpt-5-4-with-native-computer-use-mode-financial-plugins-for

一、AI 时代编程太闹心?全新协作方式来救场
传统多语言开发的那些小烦恼:不同编程语言像各说各的,兼容性特别差;开发工具也不互通有无,没有统一的协作平台;每天还要写大量重复代码,效率提不上来,根本赶不上业务迭代的节奏。
AI辅助编程也有小短板:现在的 AI 代码生成工具,质量好坏参差不齐,写出来的代码要么有逻辑漏洞,要么读起来费劲;要是太依赖 AI,程序员就没了主导权,只能被动跟着 AI 走,根本没法发挥自己的创新想法。
image.png
二、铁三角组合,撑起 Polyglot Singularity 生态
Phoenix OSE:生态里的核心担当,它不是 AI 语言,主要用来承载核心业务逻辑和程序员的创新思路,既能从一开始就保证代码质量,还能让程序员牢牢掌握主导权,不让 AI 过度插手核心开发。
Rainbow:生态里的桥梁担当,说白了就是转译工具,专门打通 Phoenix OSE 和 Vim8 编辑器的隔阂,让两种环境下的代码能无缝转换、运行和调试,再也不用为工具不兼容头疼。
Feather:生态里的贴心助手,本质是 AI 辅助层,专门帮程序员扛下那些重复、没技术含量的编码活,还能自动生成好读的代码和配套文档,大大减轻程序员的负担,让大家能偷个懒。
image.png
三、人机配合的黄金套路,重新定义编程协作
分工清清楚楚:主打AI 打辅助,人类来主导,AI 负责干那些繁重、重复的基础编码活,程序员则专心搞代码审查、做核心决策、挖创新思路,双方各展所长、配合默契。
流程闭环不脱节:形成AI 辅助生成—人类审核优化—Rainbow 转译运行的完整流程,每一步都衔接顺畅,既能提高开发效率,又能避免 AI 生成代码的质量坑。
价值不跑偏:别再担心AI 会替代程序员啦!通过合理的分工和架构设计,能稳稳保住程序员在自动化编程里的核心地位,不让大家被技术边缘化,毕竟人类的创新能力,AI 可替代不了。
image.png
四、从 Singularity 奇点出发,解锁编程新玩法
让编程语言百花齐放还更智能:以 Polyglot Singularity 生态为模板,打破单一语言的限制,让多种语言能协同工作,AI 和编程语言深度绑定,让编程生态变得更丰富、更智能。
重塑软件开发的新姿势:给软件开发提供人机协同的新思路,让开发模式从主要靠人工编码变成AI 辅助+人类创新,未来整个软件行业的开发效率和产品质量,都会因此发生大变化。

在现代商务和教育环境中,PowerPoint 演示文稿是传递信息和展示成果的重要工具。无论是产品发布会、培训课程还是项目汇报,一个生动有趣的演示文稿能够更好地吸引观众注意力,增强信息传达效果。然而,手动为每个幻灯片添加动画效果不仅耗时,而且难以保持风格的一致性。当需要制作大量演示文稿或定期更新内容时,重复的手动操作会大大降低工作效率。Python 作为一种强大的编程语言,结合专业的演示文稿处理库,可以实现动画效果的自动化添加,既保证了演示质量,又大幅提升了工作效率。

本文将使用 Free Spire.Presentation for Python 展示如何在 PowerPoint 演示文稿中为形状和文本添加各种动画效果,包括入场动画、退出动画、文本动画以及自定义路径动画,帮助你快速掌握演示文稿动画自动化技能。


1. 环境准备与库安装

首先需要安装 Free Spire.Presentation for Python:

pip install spire.presentation.free

安装完成后,我们可以开始创建 PowerPoint 演示文稿并添加动画效果。下面是一个创建简单演示文稿的示例:

from spire.presentation.common import *
from spire.presentation import *

# 创建演示文稿对象
ppt = Presentation()
# 获取第一张幻灯片
slide = ppt.Slides[0]

# 保存初始文件
ppt.SaveToFile("BasicPresentation.pptx", FileFormat.Pptx2013)
ppt.Dispose()
print("演示文稿已创建:BasicPresentation.pptx")

说明
Presentation 对象代表整个 PowerPoint 演示文稿,Slides[0] 获取第一张幻灯片。这里我们创建了一个基础的演示文稿,为后续添加形状和动画做好准备。


2. 为形状添加入场动画

入场动画是演示文稿中最常用的动画类型,能够吸引观众的注意力。我们以添加一个五角星形状并应用淡入旋转动画为例:

from spire.presentation.common import *
from spire.presentation import *

outputFile = "EntranceAnimation.pptx"

# 创建演示文稿对象
ppt = Presentation()
# 获取第一张幻灯片
slide = ppt.Slides[0]

# 设置背景图片
ImageFile = "bg.png"
rect = RectangleF.FromLTRB(0, 0, ppt.SlideSize.Size.Width, ppt.SlideSize.Size.Height)
slide.Shapes.AppendEmbedImageByPath(ShapeType.Rectangle, ImageFile, rect)
slide.Shapes[0].Line.FillFormat.SolidFillColor.Color = Color.get_FloralWhite()

# 添加五角星形状
starShape = slide.Shapes.AppendShape(ShapeType.FivePointedStar, RectangleF.FromLTRB(250, 100, 450, 300))
starShape.Fill.FillType = FillFormatType.Solid
starShape.Fill.SolidColor.KnownColor = KnownColors.LightBlue

# 为形状添加淡入旋转动画效果
slide.Timeline.MainSequence.AddEffect(starShape, AnimationEffectType.FadedSwivel)

# 保存演示文稿
ppt.SaveToFile(outputFile, FileFormat.Pptx2013)
ppt.Dispose()
print("入场动画已添加:EntranceAnimation.pptx")

幻灯片预览:

为形状添加入场动画

说明
通过 slide.Timeline.MainSequence.AddEffect() 方法为形状添加动画效果,AnimationEffectType.FadedSwivel 指定动画类型为淡入旋转。此步骤为形状添加了动态效果,使演示文稿更加生动。


3. 为形状添加退出动画

退出动画用于控制对象在幻灯片上的消失方式,能够创造出流畅的过渡效果。我们将为形状添加随机条形退出动画:

from spire.presentation.common import *
from spire.presentation import *

outputFile = "ExitAnimation.pptx"

# 创建演示文稿对象
ppt = Presentation()
# 获取第一张幻灯片
slide = ppt.Slides[0]

# 设置背景图片
ImageFile = "./Data/bg.png"
rect = RectangleF.FromLTRB(0, 0, ppt.SlideSize.Size.Width, ppt.SlideSize.Size.Height)
slide.Shapes.AppendEmbedImageByPath(ShapeType.Rectangle, ImageFile, rect)
slide.Shapes[0].Line.FillFormat.SolidFillColor.Color = Color.get_FloralWhite()

# 添加五角星形状
starShape = slide.Shapes.AppendShape(ShapeType.FivePointedStar, RectangleF.FromLTRB(250, 100, 450, 300))
starShape.Fill.FillType = FillFormatType.Solid
starShape.Fill.SolidColor.KnownColor = KnownColors.LightBlue

# 为形状添加随机条形效果
effect = slide.Timeline.MainSequence.AddEffect(starShape, AnimationEffectType.RandomBars)
# 将效果类型从入场改为退出
effect.PresetClassType = TimeNodePresetClassType.Exit

# 保存演示文稿
ppt.SaveToFile(outputFile, FileFormat.Pptx2013)
ppt.Dispose()
print("退出动画已添加:ExitAnimation.pptx")

幻灯片预览:

为形状添加退出动画

说明
使用 AnimationEffectType.RandomBars 添加随机条形效果,然后通过 effect.PresetClassType = TimeNodePresetClassType.Exit 将动画类型设置为退出动画。此功能适用于需要对象以特定方式消失的场景。


4. 为文本添加动画

文本动画能够突出重要信息,引导观众的注意力。我们将为文本框中的文字添加浮动动画:

from spire.presentation.common import *
from spire.presentation import *

outputFile = "TextAnimation.pptx"

# 创建演示文稿对象
ppt = Presentation()
# 获取第一张幻灯片
slide = ppt.Slides[0]

# 设置背景图片
ImageFile = "./Data/bg.png"
rect = RectangleF.FromLTRB(0, 0, ppt.SlideSize.Size.Width, ppt.SlideSize.Size.Height)
slide.Shapes.AppendEmbedImageByPath(ShapeType.Rectangle, ImageFile, rect)
slide.Shapes[0].Line.FillFormat.SolidFillColor.Color = Color.get_FloralWhite()

# 添加矩形形状
shape = slide.Shapes.AppendShape(ShapeType.Rectangle, RectangleF.FromLTRB(250, 150, 450, 250))
shape.Fill.FillType = FillFormatType.Solid
shape.Fill.SolidColor.Color = Color.get_LightBlue()
shape.ShapeStyle.LineColor.Color = Color.get_White()
shape.AppendTextFrame("此示例展示如何在 PPT 文档中为文本应用动画。")

# 为形状中的文本应用浮动动画
animation = shape.Slide.Timeline.MainSequence.AddEffect(shape, AnimationEffectType.Float)
# 设置动画应用于特定段落
animation.SetStartEndParagraphs(0, 0)

# 保存演示文稿
ppt.SaveToFile(outputFile, FileFormat.Pptx2013)
ppt.Dispose()
print("文本动画已添加:TextAnimation.pptx")

幻灯片预览:

为文本添加动画

说明
使用 AnimationEffectType.Float 为文本添加浮动效果,animation.SetStartEndParagraphs(0, 0) 指定动画应用于第一段文本。此功能适用于需要逐段展示文本内容的场景。


5. 设置文本动画的逐字显示效果

逐字显示动画能够让文本以更精细的方式呈现,增强视觉冲击力。我们将设置文本的逐字动画类型和时间间隔:

from spire.presentation.common import *
from spire.presentation import *

inputFile = "./Data/Animation.pptx"
outputFile = "LetterAnimation.pptx"

# 创建演示文稿对象
ppt = Presentation()
# 加载文件
ppt.LoadFromFile(inputFile)

# 将动画类型设置为逐字
ppt.Slides[0].Timeline.MainSequence[0].IterateType = AnimateType.Letter
# 设置逐字动画的时间间隔值
ppt.Slides[0].Timeline.MainSequence[0].IterateTimeValue = 10

# 保存演示文稿
ppt.SaveToFile(outputFile, FileFormat.Pptx2013)
ppt.Dispose()
print("逐字动画已设置:LetterAnimation.pptx")

幻灯片预览:

设置文本逐字动画

说明
通过 IterateType = AnimateType.Letter 设置逐字动画类型,IterateTimeValue = 10 设置每个字符出现的时间间隔。此功能适用于需要逐字展示文本的场景,能够创造出打字机效果。


6. 设置动画重复类型

动画重复类型控制动画的播放次数,能够增强演示效果。我们将设置动画持续到幻灯片结束:

from spire.presentation.common import *
from spire.presentation import *

inputFile = "./Data/Animation.pptx"
outputFile = "RepeatAnimation.pptx"

# 创建演示文稿对象
presentation = Presentation()
# 加载文件
presentation.LoadFromFile(inputFile)

# 获取第一张幻灯片
slide = presentation.Slides[0]
animations = slide.Timeline.MainSequence
# 设置动画重复类型为持续到幻灯片结束
animations[0].Timing.AnimationRepeatType = AnimationRepeatType.UtilEndOfSlide

# 保存文件
presentation.SaveToFile(outputFile, FileFormat.Pptx2013)
presentation.Dispose()
print("动画重复类型已设置:RepeatAnimation.pptx")

幻灯片预览:

设置动画重复类型

说明
使用 AnimationRepeatType.UtilEndOfSlide 设置动画重复类型为持续到幻灯片结束。此功能适用于需要动画在幻灯片播放期间持续循环的场景。


7. 创建自定义路径动画

自定义路径动画能够让对象按照指定的轨迹移动,创造出独特的视觉效果。我们将创建一个自定义的运动路径动画:

from spire.presentation.common import *
from spire.presentation import *

outputFile = "CustomPathAnimation.pptx"

# 创建 PPT 文档
ppt = Presentation()
# 添加形状
shape = ppt.Slides[0].Shapes.AppendShape(ShapeType.Rectangle, RectangleF.FromLTRB(0, 0, 200, 200))
# 添加动画
effect = ppt.Slides[0].Timeline.MainSequence.AddEffect(shape, AnimationEffectType.PathUser)
common = effect.CommonBehaviorCollection
motion = common[0]
motion.Origin = AnimationMotionOrigin.Layout
motion.PathEditMode = AnimationMotionPathEditMode.Relative

# 添加运动路径
moinPath = MotionPath()
p1 = PointF(0.0, 0.0)
p2 = PointF(0.1, 0.1)
p3 = PointF(-0.1, 0.2)
moinPath.Add(MotionCommandPathType.MoveTo, [p1], MotionPathPointsType.CurveAuto, True)
moinPath.Add(MotionCommandPathType.LineTo, [p2], MotionPathPointsType.CurveAuto, True)
moinPath.Add(MotionCommandPathType.LineTo, [p3], MotionPathPointsType.CurveAuto, True)
moinPath.Add(MotionCommandPathType.End, [], MotionPathPointsType.CurveStraight, True)
motion.Path = moinPath

# 保存演示文稿
ppt.SaveToFile(outputFile, FileFormat.Pptx2010)
ppt.Dispose()
print("自定义路径动画已创建:CustomPathAnimation.pptx")

幻灯片预览:

创建自定义路径动画

说明
使用 AnimationEffectType.PathUser 创建用户自定义路径动画,通过 MotionPath 对象定义运动轨迹。MotionCommandPathType.MoveToMotionCommandPathType.LineTo 指定路径点,MotionCommandPathType.End 结束路径定义。此功能适用于需要对象按照特定轨迹移动的场景。


8. 技术细节总结与关键类方法概览

在前面的章节中,我们展示了如何使用 Free Spire.Presentation for Python 为 PowerPoint 演示文稿添加各种动画效果。从技术实现角度来看,动画添加的核心流程可以总结为以下几个关键步骤:

Python PowerPoint 动画添加步骤总结

  1. 创建演示文稿对象
    使用 Presentation() 创建演示文稿对象,通过 ppt.Slides[0] 获取幻灯片对象。
  2. 添加形状或文本
    使用 slide.Shapes.AppendShape() 添加形状,通过 shape.AppendTextFrame() 添加文本内容。
  3. 设置背景和格式
    使用 slide.Shapes.AppendEmbedImageByPath() 设置背景图片,通过 FillSolidColor 属性设置形状颜色。
  4. 添加动画效果
    使用 slide.Timeline.MainSequence.AddEffect() 为形状或文本添加动画效果,通过 AnimationEffectType 指定动画类型。
  5. 配置动画属性
    通过 effect.PresetClassType 设置动画类型(入场、退出、强调),使用 animation.SetStartEndParagraphs() 设置文本动画范围。
  6. 设置动画细节
    使用 IterateType 设置逐字动画,通过 IterateTimeValue 设置时间间隔,使用 AnimationRepeatType 设置重复类型。
  7. 创建自定义路径
    使用 AnimationEffectType.PathUser 创建自定义路径动画,通过 MotionPath 对象定义运动轨迹。
  8. 保存演示文稿
    使用 ppt.SaveToFile() 将生成的演示文稿保存到指定文件。

关键类、方法与属性

类 / 方法 / 属性说明
PresentationPowerPoint 演示文稿对象,支持创建、加载和保存文件
Presentation.LoadFromFile()从本地文件加载演示文稿
Presentation.SaveToFile()保存演示文稿到指定路径
Slide表示单个幻灯片,是操作形状和动画的主体对象
slide.Shapes.AppendShape()在幻灯片中添加形状
shape.AppendTextFrame()为形状添加文本内容
slide.Timeline.MainSequence.AddEffect()为形状或文本添加动画效果
AnimationEffectType枚举类型,指定动画效果类型(淡入、浮动、随机条形等)
effect.PresetClassType设置动画类型(入场、退出、强调)
animation.SetStartEndParagraphs()设置文本动画应用的段落范围
AnimateType.Letter设置动画类型为逐字显示
animation.IterateTimeValue设置逐字动画的时间间隔值
AnimationRepeatType.UtilEndOfSlide设置动画重复类型为持续到幻灯片结束
AnimationEffectType.PathUser创建用户自定义路径动画
MotionPath表示自定义运动路径
MotionCommandPathType枚举类型,指定路径命令类型(移动到、线到、结束)

通过理解上述关键类、方法和属性,你可以灵活地为 PowerPoint 演示文稿添加各种动画效果,并根据演示需求进行精细定制。掌握这些技术细节,能让你在实际项目中快速生成高质量、视觉吸引力强的演示文稿,同时保持代码简洁和可维护性。


总结

本文以实际演示场景为例,展示了如何使用 Free Spire.Presentation for Python 在 PowerPoint 演示文稿中为形状和文本添加各种动画效果,包括入场动画、退出动画、文本动画、逐字动画、重复动画以及自定义路径动画。通过编程方式添加动画,不仅避免了手动操作的繁琐和易错问题,还能轻松应对批量演示文稿制作和风格统一的需求。

掌握这一技能后,你可以将演示文稿制作完全自动化,从而节省时间,提高效率,并为观众呈现更加生动有趣的演示效果。结合 Free Spire.Presentation 的其他功能,如幻灯片管理、图表插入和格式设置,可以进一步打造智能化的演示文稿自动化工做流,让企业的演示效果提升到新的高度。更多 Python 操作 PowerPoint 方法,请参考 Spire.Presentation for Python 官方教程

当今社会,网络安全已成为企业稳健发展的重要保障,为网站部署SSL证书并启用HTTPS加密,绝大多数企业几乎已达成一致共识。然而,当企业面对市场上价格从免费到几千乃至几万元的各种SSL证书时,往往难以做出明确抉择。数字证书的价格差异为何如此显著?低价SSL证书是否值得信赖?高价证书的价值体现在哪里?什么样的证书才是适合自己的?JoySSL市场部经理指出,SSL证书的价格差异,并非单纯由品牌溢价或宣传策略决定,而是验证深度、安全覆盖范围、技术支持以及法律效力等综合因素的体现。唯有理解SSL证书价格差异化背后的核心原因,才有助于企业作出更明智的选择,有效规避因过度追求低价可能带来的风险,从而真正建立起包含数字信任的防护屏障。

数字证书定价差异核心影响因素

验证等级是影响SSL证书价格最重要的因素,有着从“域名控制”到“法律实体认证”的本质区别。免费或低价的DV证书仅能实现基本的加密功能,OV证书则为企业提供可验证的商业身份,而EV证书采用最严格的身份审核标准,为企业提供最高级别的可视化信任标识。

证书覆盖范围广泛,从保护单一域名到全方位覆盖域名群体,适用于各式场景,价格也因此而受到影响。除此以外,服务质量亦成为证书定价差异的标准因素。低价的基础证书无主动监测,无服务保障。而企业级证书涵盖全天候专业技术支持,具备自动化管理工具及高额度责任赔偿,价值远超证书本身。

证书价格差异影响企业商业决策

采用免费或低成本DV证书,只能助力企业实现基础加密,无法明确身份。在金融、电商和政务等特殊领域颇受掣肘,可能使企业的品牌信任度大幅降低。相比之下,选择OV或EV证书,可将抽象的安全转化为直观的企业身份展示,增强用户信任感,提升竞争优势。

廉价证书通常不包含责任保障,因免费证书加密故障导致数据泄露,企业需要承担所有经济和法律责任。企业级证书则附加高额赔偿保障,可在关键时刻为企业提供财务保障,实际价值远超证书费用。

为众多子域管理低成本证书,或可带来隐性人力投入与风险,采用支持自动化管理与集中监控的SSL证书解决方案,可显著提高工作效率,避免重复劳动,降低长期运维成本。

透明定价推动企业有效选择证书

面对竞争激烈的SSL证书市场,JoySSL首席技术专家指出,透明的证书定价更有利于企业做出合理抉择,建立信任纽带。坚持以价值为核心,充分发挥数字证书的加密与验证功能,方可满足多样化需求,覆盖更广阔的市场。同时,透明的定价配合专业的服务,有利于将单次证书选购转化为长期投资,建立信任基础,助力企业未来发展。

价格仅为参考 SSL价值才是核心

企业选择SSL证书时,需正确理解价格与价值之间的关系,突破定价的片面逻辑,基于业务需求、风险评估、运营效率及合规要求,做出精准决策,让安全投资转化为可信赖的价值,为企业创造持续的商业回报。