普通视图
美团 LongCat 发布 AMO-Bench:突破 AIME 评测饱和困境,重新定义 LLM 数学上限
前端下午茶!看看炫酷的动画,轻松一下!
前言
之前说了会更新 gsap 动画教程,我们先来个开胃菜,看看最近练习的 demo 是否有同学愿意一起学习和交流。
既然你都点进来看帖子了,来都来了,留下来喝个小茶,看个小动画再走呗!
视频滚动动画
视频被切为了动画帧,随着滚动鼠标滚动或者触控板滑动而不断播放视频。
![]()
图片滚动动画
图片随着滚动鼠标滚动或者触控板滑动而不断变化,第二屏会有一定的视差滚动的效果。
主要涉及 clip-path 属性的变化。
![]()
![]()
transform 动画
图片随着滚动鼠标滚动或者触控板滑动而不断变化,主要是 transform 属性上的变化。
![]()
![]()
欢迎加入交流群
欢迎加入交流群一起进步!
不仅免费,还开源?这个 AI Mock 神器我必须曝光它
前两周我做了一个零侵入的接口 Mock 插件。还写了篇掘金文章记录了一下:juejin.cn/post/757098…
此前用 Popup 弹窗做联调,窗小、易误关、操作繁琐;
于是我重构为 Sidebar 常驻侧栏:规则随时可见,刷新也不丢。
换成 Sidebar 侧边栏:钉在页面右边,想开就开,数据都存着,再也不怕手抖关掉。即使刷新页面,规则也不会丢,重新打开就能继续用
接入 AI 自动生成:描述你想要的数据,或者直接贴接口的 TypeScript 类型,AI 秒出结构化数据。手写?不存在的
支持延时和状态码:可以模拟慢接口(延时 3 秒)、接口报错(返回 500)等真实场景
现在用起来爽多了,前端开发不用干等后端,自己就能把页面跑起来。
下文附安装与使用指南,欢迎试用与反馈。
如果你现在还在用 Popup 弹窗做 Mock,或者还在一个字一个字敲 JSON, 真心建议试试 Sidebar + AI 这套,用完你就知道什么叫"真香"。
演示效果
![]()
插件功能预览
- 拦截能力:覆盖 fetch 与 XMLHttpRequest ,规则实时生效,零侵入
- 匹配规则:支持模糊匹配和完全匹配、正则表达式
- 响应控制:支持按 HTTP 方法过滤(GET/POST/PUT/DELETE)
- 异常模拟:支持模拟200/400/401/403/500等状态码
- AI 生成:输入描述或贴 TS 类型,秒出结构化数据,支持模板与一键覆写
持续优化中,欢迎反馈与建议-
建设中: 调试与管理:导入导出、规则分组、Network 面板标识等;性能与匹配体验持续优化,欢迎反馈
什么时候你会需要它?
- 后端还在写接口:你不用干等,Mock 一下先把前端页面撸出来
- 测试加载状态:设置延时 2 秒,看看你的 Loading 动画、骨架屏是不是真的在转
- 测试异常处理:一键切换 200/400/401/403/500,看看你的错误提示是否友好
- 测试防重复提交:延时 3 秒 + 返回失败,看看用户会不会疯狂点击
- 给老板/客户演示:不用搭环境、不用连服务器,打开浏览器就能演示
注:支持拦截ajax和fetch请求
🚀 快速开始
获取代码
- GitHub:github.com/Teernage/AI… ⭐ 如果对您有帮助欢迎Star
- Gitee:gitee.com/xuzhenxin11…
打开代码仓库,按以下方操作下载插件包。安装包为 zip,请先解压。
下载与解压
- 在仓库的 Releases 或打包入口下载插件包(zip)。
- 将 zip 解压到任意目录,得到 AI-Mock-v1.0.0 文件夹。
参考示意图:
![]()
一键安装
- 打开Chrome浏览器
3.. 地址栏输入:
chrome://extensions/ - 打开右上角"开发者模式"
- 点击"加载已解压的扩展程序"
- 选择刚刚解压的AI-Mock-v1.0.0文件夹
- 搞定!扩展安装完成!
参考示意图:
![]()
gitee 仓库链接的同理,在仓库页点击“发行版(Release)”, 进入最新版本页面,下载插件包即可
AI 前置准备
- 使用 AI 生成前需配置 DeepSeek API Key。为便于体验,提供一个限额试用 Key;额度用尽或失效后,请前往 DeepSeek 官网申请个人 Key 并在侧栏替换
- 试用 Key(仅用于体验,随时可能失效):
sk-9fa67c84581d4f67b61039ff8b199baa
示例:
![]()
DeepSeek 官网申请key示例:
![]()
设计与实现
1. Sidebar 侧边栏 vs popup弹窗
| 特性 | Popup | Sidebar |
|---|---|---|
| 显示空间 | 小(~400x600px) | 大(可调整宽度) |
| 操作便捷性 | 点击外部就关闭 | 固定显示,不怕误触 |
| 与页面交互 | 互斥 | 可同时显示 |
| 数据持久化 | 关闭即丢失 | 刷新页面也保留 |
2. AI 生成 Mock 数据
这块就两件事:
- 你给一个 URL,我把它转成正则,用来拦截这个接口
- 你给数据类型或者一句话描述,AI 直接把 Mock 数据“现做现给”
1. 根据描述输出 Mock 数据
- 只要一句话就够:
- 输入:用户列表,包含姓名、年龄、邮箱,来 10 条
效果图:![]()
2. 根据类型输出 Mock 数据
- 或者直接贴 数据的类型:
interface User {
id: number;
name: string;
age: number;
email: string;
}
效果图:
![]()
3. 根据输入的地址生成正则表达式
目的:把带占位符的路径,转成更稳的正则,方便在开发态拦截请求
常用示例:
- 输入: /api/users/:id
- 正则: ^/api/users/[^/]+/?$
- 可拦截: /api/users/1 、 /api/users/abc
效果图:
![]()
修改 Mock 数据
-
这块很直观,给你三种编辑入口,改完就是生效:
- 规则编辑里改:打开“编辑规则”,在 Mock 数据 (JSON) 文本框里直接改,支持“格式化/验证/复制/清空”,点“保存”后立即应用
-
放大弹窗改:点击“放大”,全屏 JSON 编辑器更适合改大结构或深层嵌套,改完“验证”一下再“保存”
-
卡片内联改:在规则卡片下方的“Mock 响应”树视图,鼠标双击某个值就能直接编辑,编辑完进行回车保存。这个适合改小字段(比如把 data.hits.total 从 1 改成 20 )
效果图:
![]()
同步规则
- 三个地方改的都是同一份数据,任意入口保存后,其他入口会显示最新值
- 修改仅作用于“当前这条规则”,不会影响其他规则或接口
推荐使用场景
- 小改动(单字段/少量数值):用“卡片内联”最快
- 中等改动(几个字段/一层结构):在“编辑规则”里改,配合“格式化/验证”
- 大改动(数组长度、层级结构、批量替换):用“放大弹窗”编辑 JSON,更清晰、更不容易漏
操作建议
- 先“验证”再“保存”:能快速提示 JSON 语法、字段类型不匹配等问题
- 用“格式化”提升可读性,避免缩进错乱
- 需要回到初始或重置时,用“清空”或点击“生成”重新出一版数据
- 要分享或备份当前数据,用“复制”一键拷贝
4. 延时请求 - 测试加载状态
设置延时 3 秒,模拟慢接口:
- 测试 Loading 动画是否正常
- 测试骨架屏效果
- 测试用户会不会重复点击
- 测试超时逻辑
演示效果图:
![]()
4. 状态码 - 测试异常场景
一键切换不同状态码:
- 200:测试成功流程
- 400:测试参数校验
- 401:测试登录跳转
- 403:测试权限提示
- 500:测试错误兜底
效果图:
![]()
核心实现
1. Manifest V3:从 Popup 到 Sidebar
配置文件:
{
"manifest_version": 3,
"name": "AI Mock",
"version": "2.0.0",
"side_panel": {
"default_path": "sidepanel.html"
},
"permissions": [
"sidePanel",
"storage",
"scripting"
],
"host_permissions": [
"<all_urls>"
]
}
打开侧边栏:
// background.ts
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ windowId: tab.windowId });
});
关键区别:
- Popup:窗口小、点击外部就关闭、关闭即销毁
- Sidebar:空间大、可固定显示、数据持久化
2. 拦截 fetch / XHR(支持延时和状态码)
劫持实现原理参考我上篇文章:【前端效率工具】再也不用 APIfox 联调!零侵入 Mock,全程不改代码、不开代理juejin.cn/post/757098…
核心代码:
injected.ts
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
const url = typeof input === 'string' ? input : input.url;
const method = init?.method || 'GET';
// 查找匹配的规则
const rule = await findMatchedRule(url, method);
if (rule) {
// 延时处理
if (rule.delay > 0) {
await new Promise(resolve => setTimeout(resolve, rule.delay));
}
// 返回指定状态码
return new Response(
JSON.stringify(rule.data),
{
status: rule.statusCode || 200,
statusText: getStatusText(rule.statusCode),
headers: { 'Content-Type': 'application/json' }
}
);
}
// 不拦截,调用原始 fetch
return originalFetch.apply(this, arguments as any);
};
// 状态码文本映射
function getStatusText(code: number): string {
const statusTexts: Record<number, string> = {
200: 'OK',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout'
};
return statusTexts[code] || 'Unknown';
}
匹配规则:
async function findMatchedRule(url: string, method: string) {
// 从 storage 获取规则
const { rules } = await chrome.storage.local.get('rules');
return rules.find((rule: Rule) => {
if (!rule.enabled) return false;
// 方法匹配
if (rule.method !== 'ALL' && rule.method !== method) {
return false;
}
// URL 匹配
if (rule.matchMode === 'contains') {
return url.includes(rule.url);
} else if (rule.matchMode === 'exact') {
return url === rule.url;
} else if (rule.matchMode === 'regex') {
return new RegExp(rule.url).test(url);
}
return false;
});
}
支持的匹配模式:
-
包含匹配:
/api/user可以匹配/api/user/list、/api/user/detail - 完整匹配:必须完全一致
-
正则匹配:
/api/user/\d+可以匹配/api/user/123
3. AI 生成 Mock 数据
Prompt 设计:
const buildPrompt = (userInput: string) => {
return `
你是一个专业的 Mock 数据生成助手。
用户需求:${userInput}
请生成符合以下规范的 JSON 数据:
1. 必须是有效的 JSON 格式
2. 包含常见的响应结构(code, msg, data)
3. 数据要真实合理(中文姓名、真实邮箱格式等)
4. 如果是列表,生成 2-3 条示例数据
只返回 JSON,不要有其他说明文字。
`.trim();
};
生成逻辑:
const generateMockData = async () => {
if (!aiPrompt.value) {
ElMessage.warning('请输入数据描述');
return;
}
generating.value = true;
try {
const response = await fetch('YOUR_AI_API_ENDPOINT', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: buildPrompt(aiPrompt.value),
model: 'gpt-3.5-turbo'
})
});
const data = await response.json();
// 解析 AI 返回的 JSON
const mockData = JSON.parse(data.choices[0].message.content);
// 填充到编辑器
form.data = JSON.stringify(mockData, null, 2);
ElMessage.success('✨ AI 生成成功');
} catch (error) {
ElMessage.error('生成失败:' + error.message);
} finally {
generating.value = false;
}
};
4. 规则数据结构
interface Rule {
id: string;
remark: string; // 规则名称
url: string; // URL 匹配
matchMode: 'contains' | 'exact' | 'regex';
method: 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE';
statusCode: number; // 🆕 状态码(默认 200)
delay: number; // 🆕 延时(毫秒,默认 0)
data: Record<string, any>; // Mock 数据
enabled: boolean; // 是否启用
createdAt: number; // 创建时间
}
存储和读取:
// 保存规则
const saveRules = async () => {
await chrome.storage.local.set({ rules: rules.value });
};
// 加载规则
const loadRules = async () => {
const result = await chrome.storage.local.get('rules');
rules.value = result.rules || [];
};
// 监听变化
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local' && changes.rules) {
rules.value = changes.rules.newValue;
}
});
总结
这次重构主要做了四件事:
- Sidebar 替代 Popup:更大的空间、更好的体验
- AI 自动生成:一句话描述即可生成 Mock 数据
- 延时请求:测试 Loading、骨架屏、超时逻辑
- 自定义状态码:测试各种异常场景
这四个功能组合起来,基本覆盖了前端开发中 90% 的 Mock 需求。
如果你也在等后端接口,或者被 Popup 小窗口折磨过, 真的可以试试这个插件,保证你用了就回不去了。
unipush推送入门:10分钟搞定UniPush在线消息集成
引言
消息推送是提升 App 用户活跃度和留存率的关键能力。对于采用 UniApp 开发的跨端应用,DCloud 官方提供的 UniPush 服务无疑是集成推送功能最高效选择。
本文将以一名开发者的视角,手把手带你完成 从项目初始化到成功接收第一条推送消息 的完整流程。你将学会:
- UniPush 的正确接入姿势与核心配置
- 开发阶段必须使用的自定义调试基座
- 如何获取设备标识(CID)并进行在线推送测试
- 避开新手常见“坑点”的实用技巧
技术栈:UniApp + UniPush(华为、小米等厂商推送配置将在下篇介绍)
一、 开发环境与项目初始化
1.1 项目创建与模块配置
- 项目创建:在 HBuilderX 中新建一个 UniApp 项目(过程略)。
-
启用 UniPush:打开项目根目录的
manifest.json文件,进入 “App模块配置” 选项卡,勾选 “Push(消息推送)” 模块,并确保选中 “UniPush” 作为具体服务。
1.2 制作自定义调试基座
为什么必须使用自定义基座?
UniApp 的标准运行基座(标准真机运行包)所使用的 DCloud 公有证书,其推送标识(CID)无法用于正式推送。只有使用你自己 App 的正式证书(或调试证书)打包的自定义基座,才能获得真实、有效的 CID。
制作步骤:
-
在 HBuilderX 顶部菜单,选择 运行 -> 运行到手机或模拟器 -> 制作自定义调试基座。
-
在弹出的界面中,你需要为 Android 平台提供应用签名证书。
- 证书生成:可以使用在线工具如 香蕉云编 快速生成调试证书(.keystore 文件),妥善保存密码及别名信息。
- 证书填写:将生成的证书文件路径、密码、别名等信息,准确填入 HBuilderX 的对应输入框。
-
点击 “打包” ,等待云端完成自定义基座的制作。
如何使用:
制作完成后,运行项目时,务必在运行菜单中选择“自定义调试基座” ,而非“运行标准基座”。只有运行在自定义基座上,后续的推送测试才有效。
![]()
二、 UniPush 服务开通与配置
推送能力依赖于 DCloud 的后台服务,因此需要在开发者中心完成配置。
-
进入开发者中心:访问 DCloud 开发者中心,在 “应用列表” 中找到你的项目。
-
配置应用信息:
- 如果未创建,请先为你的 App 创建一个应用。
- 进入应用详情,在 “各平台信息” 中配置 Android 平台信息。
- 这里需要填写的包名、应用签名,必须与上一步制作自定义基座时使用的证书信息完全一致。你可以在“香蕉云编”等工具生成证书的页面找到对应的 MD5 或 SHA1 签名。
![]()
三、 核心代码实现与在线推送测试
3.1 获取设备推送标识 (CID)
CID 是 UniPush 服务识别每台设备的唯一 ID。在客户端,我们使用 uni.getPushClientId API 获取。
关键注意事项(时序问题):
推送服务初始化需要时间,应用启动后立即调用此 API 可能返回空值。一个稳健的做法是延时获取或在监听事件中获取。
推荐实现方案:
// 在 App.vue 的 onLaunch 中,或首个页面的 onLoad 中调用
function initUniPush() {
// 方案1:延时获取(简单直接)
setTimeout(() => {
uni.getPushClientId({
success: (res) => {
const cid = res.cid;
console.log('设备推送标识CID:', cid);
// 重要:将 CID 发送到你的业务服务器进行关联存储
// uploadCidToServer(cid);
// 可通过 Vuex 或全局事件更新 UI 状态
uni.$emit('push-cid-ready', cid);
},
fail: (err) => {
console.error('获取CID失败:', err);
// 处理失败逻辑,如重试
}
});
}, 3000); // 延迟 3 秒,可根据实际情况调整
// 方案2:监听事件(更精准)
// uni.onPushMessage((res) => {...}); // 接收推送消息
// 也可以在服务初始化成功的事件中获取CID
}
3.2 发送你的第一条推送
-
登录 DCloud 后台:进入你的应用,找到 “UniPush” 功能模块。
-
创建推送:
- 点击 “在线推送” 或 “推送消息” 。
- 推送方式选择 “单播(CID)” ,将上一步获取的 CID 填入。
- 填写推送标题、内容。内容应尽量模拟真实场景(如“欢迎使用”、“订单已发货”),避免使用“test”、“aaaa”等测试文本,以防被系统或厂商通道误判为骚扰信息而拦截。
- 点击发送。
![]()
-
验证结果:
- 发送后,在 “推送记录” 中可以查看状态。显示“已接收”即表示推送已成功抵达客户端。
- 确保你的 App 正处于前台运行或后台存活状态。此时,你应该能在设备上收到通知栏消息。
![]()
四、 总结与注意事项
至此,你已经完成了 UniPush 在线推送的核心流程。我们来总结一下关键点:
✅ 正确流程:创建项目 → 启用模块 → 制作自定义基座(关键!) → 平台配置 → 获取CID → 后台推送。
⚠️ 核心要点:
- 调试必须用自定义基座:这是获得有效 CID 的前提。
- CID 获取有时序:使用延时或监听事件确保获取成功。
- 信息一致:开发者中心配置的包名、签名需与打包证书一致。
- 推送内容需“真实” :避免纯测试内容被过滤。
- 在线推送的局限性:目前实现的是“在线推送”,即 App 进程存活(前台或后台)才能接收。要实现 App 完全关闭后仍能接收(离线推送),需要集成各大手机厂商的推送通道(华为、小米、OPPO、vivo 等)
理解 CSS backface-visibility:卡片翻转效果背后的属性
前段时间在做一个产品展示页,需要实现卡片翻转效果。本以为很简单,结果翻转的时候总是"穿帮"——正面和背面同时显示,或者背面的文字是镜像的。折腾了半天才发现,原来是 backface-visibility 这个属性没搞明白。
今天就来聊聊这个 CSS 属性。
🎮 在线演示:frontend-learning.pages.dev/animation/d…
建议边看文章边打开演示页面,所有效果都可以直接交互体验。
先说结论
backface-visibility 控制的是:当一个元素的背面朝向你时,这个背面是否可见。
听起来有点绕?没关系,我们慢慢来。
理解"背面"这个概念
这是理解整个属性的关键。很多人(包括我)一开始都没搞清楚这个"背面"到底是什么。
关键理解:每个 HTML 元素就像一张纸,天生就有正面和背面。当你用 rotateY(180deg) 翻转它时,你看到的是它的背面(就像纸翻过来,文字是镜像的)。
来看个最简单的例子:
<div
style="
background: linear-gradient(135deg, #10b981, #059669);
color: white;
padding: 40px;
transform: rotateY(180deg);
"
>
HELLO
</div>
你会看到什么?镜像的 HELLO。这就是元素的背面。
backface-visibility: hidden 的作用就是:当背面朝向你时,让它变透明。
四个场景,逐步理解
我做了四个对比演示,帮你理解这个属性是怎么工作的。
场景 1:正常叠加
<div style="position: relative;">
<div class="front" style="position: absolute;">FRONT</div>
<div class="back" style="position: absolute;">BACK</div>
</div>
结果:只看到 BACK
原因:两个都是 absolute,BACK 在上层覆盖了 FRONT
这个很好理解,就是普通的 DOM 层叠。
场景 2:BACK 翻转 180°
.back {
transform: rotateY(180deg);
}
结果:看到镜像的 BACK 原因:BACK 翻转后,你看到的是它的背面(文字镜像)
这里就出现问题了!虽然 BACK 翻转了,但你看到的是它的背面,文字是反的。
场景 3:隐藏 BACK 的背面
.back {
transform: rotateY(180deg);
backface-visibility: hidden; /* 关键!*/
}
结果:✅ 只看到 FRONT 原因:BACK 的背面被隐藏,露出下面的 FRONT
现在好了!BACK 的背面不可见了,所以你能看到下面的 FRONT。
场景 4:完美翻转
.front,
.back {
backface-visibility: hidden; /* 两面都隐藏背面 */
}
.back {
transform: rotateY(180deg);
}
/* 悬停时翻转父容器 */
.card-container:hover .card {
transform: rotateY(180deg);
}
结果:✅ 悬停完美翻转 原理:
- 初始状态:FRONT 正面朝向你(显示),BACK 背面朝向你(隐藏)
- 翻转后:FRONT 背面朝向你(隐藏),BACK 正面朝向你(显示)
这就是完美的卡片翻转效果!
语法很简单
.element {
backface-visibility: visible | hidden;
}
-
visible:默认值,背面可见 -
hidden:背面不可见
就这两个值,没别的了。
一个常见误区:为什么不能分别旋转子元素?
这是我当时最困惑的地方。既然翻转后正面和背面都旋转了 180°,为什么不能直接这样写?
/* ❌ 错误写法 */
.card-container:hover .front {
transform: rotateY(180deg);
}
.card-container:hover .back {
transform: rotateY(180deg);
}
看起来很合理对吧?但实际上完全不行。
问题在哪?
关键在于:CSS 的 transform 属性会被完全覆盖,而不是累加。
/* 初始状态 */
.front {
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
/* 悬停后 */
.card-container:hover .front {
transform: rotateY(180deg); /* ✓ 从 0° 变成 180° */
}
.card-container:hover .back {
transform: rotateY(180deg); /* ✗ 从 180° 变成 180°,没变化! */
}
背面的初始值 rotateY(180deg) 被新值 rotateY(180deg) 覆盖,但因为值相同,所以背面根本没动!
正确做法:旋转父容器
/* ✅ 正确写法 */
.card-container:hover .card {
transform: rotateY(180deg); /* 旋转整个父容器 */
}
这样做的原理是:子元素的 transform 是相对于父元素的坐标系。
初始状态:
.card (0°)
├── .front: rotateY(0°) 相对于 .card → 绝对位置 0°
└── .back: rotateY(180°) 相对于 .card → 绝对位置 180°
悬停后:
.card (180°) ← 整个坐标系旋转了
├── .front: rotateY(0°) 相对于 .card → 绝对位置 0° + 180° = 180°
└── .back: rotateY(180°) 相对于 .card → 绝对位置 180° + 180° = 360° = 0°
父元素旋转时,子元素的相对角度不变,但绝对角度会改变。这才是正确的翻转逻辑。
为什么需要三层结构?
标准的卡片翻转需要三层 DOM 结构:
<div class="爷容器">
<!-- perspective(观察点) -->
<div class="父容器">
<!-- transform-style + rotateY(翻转者) -->
<div class="正面"></div>
<!-- backface-visibility(被翻转的面) -->
<div class="背面"></div>
<!-- backface-visibility(被翻转的面) -->
</div>
</div>
每一层的职责
爷容器:设置 perspective
.card-container {
perspective: 1000px; /* 必须在父元素上 */
}
- 定义观察者的位置,创建 3D 空间
- 类比:你站在舞台前看表演,这个属性决定你站在哪里看
-
为什么在外层?
perspective必须设置在父元素上,才能对子元素的 3D 变换生效
父容器:设置 transform-style 和执行 rotateY
.card {
transform-style: preserve-3d; /* 保持 3D 空间 */
transition: transform 0.6s;
}
.card-container:hover .card {
transform: rotateY(180deg); /* 翻转整个卡片 */
}
-
preserve-3d:让子元素保持在 3D 空间中(而不是被压平) - 类比:这是舞台上的转盘,带着上面的演员一起旋转
子元素:正面和背面
.card-front,
.card-back {
position: absolute;
backface-visibility: hidden; /* 关键!隐藏背面 */
}
.card-back {
transform: rotateY(180deg); /* 背面预先翻转 */
}
-
position: absolute:让两个面重叠在同一位置 - 背面预先翻转 180°:这样当父容器翻转 180° 时,背面刚好正面朝向你
为什么不能少一层?
如果只有两层:
/* ❌ 错误 */
.card {
perspective: 1000px;
transform: rotateY(180deg);
}
问题:perspective 和 transform 在同一个元素上,perspective 不会对自己的 transform 生效,只会对子元素生效。结果就是没有透视效果。
完整的卡片翻转模板
直接复制这个模板,改改样式就能用:
<!-- HTML 结构 -->
<div class="card-container">
<!-- 爷容器:观察点 -->
<div class="card">
<!-- 父容器:翻转者 -->
<div class="card-front">正面内容</div>
<div class="card-back">背面内容</div>
</div>
</div>
/* 第 1 层:爷容器 - 设置观察点 */
.card-container {
perspective: 1000px; /* 必须在父元素上 */
}
/* 第 2 层:父容器 - 执行翻转 */
.card {
transform-style: preserve-3d; /* 保持 3D 空间 */
transition: transform 0.6s;
}
.card-container:hover .card {
transform: rotateY(180deg); /* 翻转整个卡片 */
}
/* 第 3 层:子元素 - 正面和背面 */
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden; /* 关键!隐藏背面 */
}
.card-back {
transform: rotateY(180deg); /* 背面预先翻转 */
}
这个模板是标准写法,三层结构缺一不可。
几个注意事项
-
必须配合 3D 变换:
backface-visibility只在 3D 变换(如rotateY,rotateX)中有意义,2D 旋转(rotate)不会产生背面 -
性能优化:设置为
hidden可以让浏览器跳过背面的渲染,提升性能 -
浏览器兼容:现代浏览器都支持,旧版本可能需要
-webkit-前缀
总结
backface-visibility 这个属性本身很简单,就两个值。但要真正理解它,需要搞清楚几个概念:
- 每个元素天生就有正面和背面
-
backface-visibility: hidden让背面朝向你时变透明 - 卡片翻转必须在父元素上执行旋转
- 标准的三层结构不能省略
最后再推荐一次我做的演示页面,里面有所有场景的交互演示和详细说明:
【vue高频面试题】第 20 题:Vue3 生命周期 + watch 执行顺序
第 20 题:Vue3 生命周期 + watch 执行顺序
🎯 一、核心问题
问:Vue3 中生命周期函数的执行顺序是怎样的?watch / onMounted / onBeforeUnmount 如何配合使用?
-
面试官重点考察:
- 生命周期顺序
- watch 执行时机
- 响应式更新与生命周期的关系
🎯 二、Vue3 生命周期顺序(组合式 API)
-
setup() 执行
- 初始化 props、state、ref、computed、watch
- 可以在 setup 中注册生命周期函数
-
onBeforeMount
- 组件挂载前触发
- 还未渲染到 DOM
-
onMounted
- 组件挂载完成,DOM 已渲染
- 可以操作真实 DOM
-
响应式数据变化触发 watch
- 如果 watch 在 setup 中注册
- flush 默认是
'pre',在 DOM 更新前执行 - flush:
'post'→ 在 DOM 更新后执行
-
onBeforeUnmount
- 组件卸载前触发
- 可清理定时器、事件监听等
-
onUnmounted
- 组件已卸载
- DOM 已销毁
🎯 三、执行顺序图示
setup() ← 初始化 state / computed / watch
│
onBeforeMount() ← 挂载前
│
DOM 渲染
│
onMounted() ← 挂载完成,可操作真实 DOM
│
响应式数据变化
│
watch() ← flush: pre/post/sync 执行
│
用户操作 / 卸载组件
│
onBeforeUnmount()
│
onUnmounted()
🎯 四、代码示例
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
const count = ref(0)
watch(count, (newVal) => {
console.log('watch count:', newVal)
}, { flush: 'post' })
onMounted(() => {
console.log('mounted')
})
onBeforeUnmount(() => {
console.log('beforeUnmount')
})
setTimeout(() => count.value++, 1000)
</script>
输出顺序:
mounted
watch count: 1 ← flush: post,DOM 已更新
beforeUnmount ← 卸载组件时
🎯 五、面试官常见追问
追问 1:watch flush 参数含义?
-
'pre'(默认) → 在 DOM 更新前执行 -
'post'→ 在 DOM 更新后执行 -
'sync'→ 同步执行,立即触发
追问 2:setup 内 watch 和生命周期执行顺序?
- setup 先执行
- 生命周期注册在 setup 后,但会在挂载阶段依次触发
- watch flush
'pre'在 onMounted 前可能执行一次(第一次访问依赖会立即触发)
追问 3:onMounted 中可以访问 DOM 吗?
- ✅ 可以
- 因为组件已经挂载完成
- 在 setup 或 onBeforeMount 中访问 DOM 是 null
追问 4:onBeforeUnmount 与 watch 清理有关系吗?
- watch 可返回 stop 函数
- 在 onBeforeUnmount 中调用 stop() 可手动停止 watch
- 避免组件卸载后副作用仍然触发
🎯 六、一句话总结
Vue3 生命周期顺序:
setup → onBeforeMount → onMounted → 响应式 watch → onBeforeUnmount → onUnmounted;watch flush 决定响应式回调执行时机,可精确控制 DOM 更新前后。
【vue高频面试题】第 19 题:Vue3 性能优化技巧
第 19 题:Vue3 性能优化技巧
🎯 一、核心问题
问:在 Vue3 中,如何优化性能?尤其是响应式系统、虚拟 DOM、静态节点、列表渲染等方面。
- 面试官考察你对 Vue3 原理和实战性能优化的理解
🎯 二、优化方法
1️⃣ 响应式优化
-
减少不必要的响应式对象
-
reactive会递归代理对象,避免对大量静态数据使用 reactive - 对于静态对象,可直接使用普通对象或
readonly
-
-
使用
shallowReactive/shallowRef- 只对最外层做响应式,减少深层 Proxy 开销
-
避免过度监听
- watch / computed 只监听必要字段
- watchEffect 谨慎使用大对象
-
computed 缓存
- 对计算量大的逻辑使用 computed,避免重复计算
2️⃣ 虚拟 DOM 优化
-
PatchFlag
- 使用模板编译优化,Vue3 自动生成 PatchFlag
- 减少无效 Diff
-
静态节点提升
- 不依赖响应式数据的节点在编译阶段提升
- 避免每次渲染创建 VNode
-
合理使用 key
- 列表渲染用 key 区分节点,优化复用,减少 DOM 移动
3️⃣ 列表渲染优化
-
v-for + key
- 唯一标识元素,提高 Diff 准确性
<li v-for="item in list" :key="item.id">{{ item.name }}</li> -
虚拟滚动 / 懒渲染
- 大量列表只渲染可视区域,提高渲染性能
- 可使用
vue-virtual-scroller等组件
-
避免嵌套 v-for
- 嵌套循环会产生指数级渲染开销
- 可拆分组件,局部渲染优化
4️⃣ 事件和组件优化
-
v-on / 方法绑定
- 避免在模板中直接写内联函数,导致每次渲染生成新函数
<!-- 不推荐 --> <button @click="count++"></button> <!-- 推荐 --> <button @click="handleClick"></button> -
组件拆分 / 懒加载
- 使用
<Suspense>或动态 import 懒加载大型组件
const AsyncComponent = defineAsyncComponent(() => import('./MyComponent.vue')) - 使用
🎯 三、综合示例
<template>
<ul>
<li v-for="item in visibleList" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup>
import { ref, computed } from 'vue'
const list = ref([...Array(10000).keys()].map(i => ({ id: i, name: `Item ${i}` })))
// 虚拟滚动或分页,仅显示部分
const visibleList = computed(() => list.value.slice(0, 100))
</script>
- 只渲染 100 条数据,而不是全部 10000 条
- 减少 DOM 数量 → 提升性能
🎯 四、面试官常见追问
追问 1:Vue3 响应式性能比 Vue2 好在哪?
- Proxy 代替 defineProperty
- 深度递归惰性代理,避免初始化成本高
- 精准依赖收集,减少无效 effect 触发
追问 2:静态节点提升与 PatchFlag 怎么协同优化?
- 静态节点 → 减少 VNode 创建和 Diff
- PatchFlag → 减少动态节点 Diff 范围
- 两者结合 → 最大化性能优化
追问 3:大量列表渲染时,Diff 性能瓶颈如何解决?
- 使用 key 优化复用
- 分页 / 虚拟滚动减少渲染节点数量
- 尽量避免嵌套循环
追问 4:computed 和 watch 在性能优化上有什么作用?
- computed → 缓存计算结果,避免重复计算
- watch → 精准监听变化,执行副作用,不触发不必要渲染
追问 5:响应式对象深层次修改会影响性能吗?
- 深层 reactive 会对每层对象递归代理
- 对大对象或数组,建议使用 shallowReactive / shallowRef 或只代理必要层级
🎯 五、一句话总结(面试必背)
Vue3 性能优化核心:精准响应式(Proxy + computed 缓存)、虚拟 DOM PatchFlag + 静态节点提升、列表渲染优化(v-for + key + 虚拟滚动),结合组件拆分与懒加载,实现高效渲染。
【vue高频面试题】第 18 题:Vue3 响应式原理中的 effect、依赖收集与依赖触发
第 18 题:Vue3 响应式原理中的 effect、依赖收集与依赖触发
🎯 一、核心问题
问:Vue3 响应式系统中 effect 是什么?依赖是如何收集和触发的?
-
面试官希望你理解:
- 响应式依赖收集机制
- effect 栈的作用
- 数据变化如何触发对应组件重新渲染
🎯 二、核心概念
-
effect
- 本质是一个函数,用于包裹 副作用(如模板渲染函数、computed getter)
- 当依赖的数据发生变化时,effect 会被重新执行
-
依赖收集(track)
- 当 effect 执行过程中访问响应式对象属性时
- Proxy 的
get会调用track - 将当前激活的 effect 保存到该属性的依赖集合中
-
依赖触发(trigger)
- 当属性变化时,通过 Proxy 的
set调用trigger - 触发依赖集合中的所有 effect 执行
- 更新模板 / computed / watch 回调
- 当属性变化时,通过 Proxy 的
-
effect 栈
- 用来处理嵌套 effect
- 确保 track 时收集的始终是 当前激活的 effect
- 避免不同 effect 混淆依赖
🎯 三、深度图解(文字版图)
effectStack = []
1. 创建 effect:
effect(() => { console.log(state.count) })
2. 执行 effect:
└─ push effect 到 effectStack
│
▼
Proxy get 拦截 state.count
│
▼
track(effectStack.top) → 保存依赖
3. 数据变化:
state.count++
└─ Proxy set 拦截
│
▼
trigger(state.count.deps) → 执行依赖的 effect
│
▼
effectStack.top 被重新执行 → DOM / computed 更新
关系总结:
响应式对象 property → Set(effect1, effect2)
effect1 / effect2 → function(callback)
依赖变化 → trigger → 执行 effect
🎯 四、示例代码(最简单的实现效果)
import { reactive, effect } from 'vue'
const state = reactive({ count: 0 })
effect(() => {
console.log('count =', state.count)
})
state.count++ // 自动触发 effect
state.count++ // 再次触发 effect
分析:
-
effect执行 → push 到 effectStack -
state.count的get调用 track → 收集 effect -
state.count++的set调用 trigger → 执行 effect → 输出最新值
🎯 五、面试官常见追问
追问 1:effect 栈为什么必要?
- 支持嵌套 effect
- 确保 track 时收集的是当前激活 effect
- 避免 effect A 调用 effect B 时混乱收集依赖
追问 2:依赖收集如何避免重复?
- 每个属性依赖使用 Set 存储 effect
- 同一个 effect 多次访问同一属性只会收集一次
追问 3:computed 内部如何利用 effect?
- computed getter 包裹 effect
- effect 被标记
lazy = true - 访问
.value时触发执行并收集依赖 - 依赖变化 → 标记 dirty → 下次访问重新计算
追问 4:watch 是如何利用依赖触发的?
- watch 内部创建 getter effect
- 监听响应式对象 / getter
- 数据变化 → trigger → 执行回调
- 可获取新旧值,执行异步副作用
追问 5:为什么 Vue3 响应式性能比 Vue2 高?
- Proxy 不需要递归 defineProperty → 节省初始化时间
- 依赖收集精准 → 最小化 effect 执行
- effect + dirty + PatchFlag → 精准更新
🎯 六、一句话总结(面试必背)
Vue3 响应式通过 Proxy 拦截访问,effect 栈管理当前激活 effect,track 收集依赖,trigger 触发依赖执行,实现高性能、精准响应式更新。
【vue高频面试题】第 17 题:Vue3 虚拟 DOM 与 PatchFlag 原理 + 静态节点提升
第 17 题:Vue3 虚拟 DOM 与 PatchFlag 原理 + 静态节点提升
🎯 一、核心问题
问:Vue3 中虚拟 DOM 是如何优化性能的?PatchFlag 和静态节点提升的作用是什么?
这是面试官非常喜欢问的高级题,尤其考察对 Vue3 编译器优化的理解。
🎯 二、标准回答(面试官满意版)
-
虚拟 DOM 本质
- Vue3 将模板编译成 VNode(虚拟节点)
- 每次响应式数据变化时,生成新的 VNode
- 对比旧 VNode → 通过 Diff 算法最小化真实 DOM 更新
-
PatchFlag(编译标记)
-
在编译阶段,为 VNode 添加 标记字段,标识节点变化类型
-
作用:
- 告诉渲染器哪些属性可能变化
- 避免对不变的节点重复 diff
-
典型标记:
-
TEXT→ 文本变化 -
CLASS→ class 变化 -
STYLE→ style 变化 -
PROPS→ 普通 props 变化 -
FULL_PROPS→ 所有 props 都可能变化 -
HYDRATE_EVENTS→ 事件变化
-
-
-
静态节点提升(Hoist Static)
- 编译器把模板中 不依赖响应式数据的节点 提前生成 VNode
- 渲染时直接复用,不需要每次重新创建
- 节约内存和计算,减少 Diff
🎯 三、工作流程示意
模板 → 编译器 → VNode + PatchFlag + 静态提升
响应式数据变化 → 生成新 VNode
│
▼
Diff + Patch
│
▼
仅更新变化的节点/属性
🎯 四、代码示例(编译器优化效果)
<template>
<div>
<h1>{{ title }}</h1> <!-- 动态内容 → TEXT PatchFlag -->
<p class="desc">静态描述</p> <!-- 静态节点 → Hoist Static -->
</div>
</template>
-
<h1>会每次响应式数据变化时 diff -
<p>被提升为 静态节点,不会重复创建或比较
如果用 Vue3 编译输出 VNode,会生成类似:
const _hoisted_1 = /*#__PURE__*/createVNode("p", { class: "desc" }, "静态描述")
...
createVNode("div", null, [
createVNode("h1", null, toDisplayString(title), 1 /* TEXT */),
_hoisted_1
])
🎯 五、面试官常见追问(高频)
追问 1:PatchFlag 为什么比 Vue2 性能高?
- Vue2 diff 每次都会比较整个 VNode 树
- Vue3 PatchFlag 提前告诉渲染器哪些属性可能变化
- 避免了大量无用比较 → 大幅减少 diff 计算
追问 2:静态节点提升有什么好处?
- 静态节点只创建一次
- 不参与每次渲染的 Diff
- 节省内存分配 + CPU 时间
- 特别适合大模板或列表的静态内容
追问 3:PatchFlag 和 key 的关系?
-
PatchFlag 用于标记节点内部变化类型
-
key 用于 列表节点快速定位变化
-
结合使用:
- PatchFlag 优化单节点属性更新
- key 优化列表的重新排序和复用
追问 4:如何在开发中查看 PatchFlag?
- 编译时输出
__DEV__模式下 VNode - Vue3 编译器会在 VNode 对象中显示 PatchFlag
- 也可以用
vue-next的源码分析
追问 5:PatchFlag 有哪些限制?
- 仅对模板编译生成 VNode 生效
- 手写 render 函数需要手动优化
- 对复杂动态结构,PatchFlag 无法覆盖全部变化,需要配合 key 和列表 diff
🎯 六、一句话总结(背面试官即可)
Vue3 通过 PatchFlag 标记节点变化类型和静态节点提升,减少不必要的 Diff 和 VNode 创建,从而显著提升渲染性能。
【vue高频面试题】第 16 题:Vue3 响应式原理深度解析(Proxy + effect 栈 + 依赖追踪)
第 16 题:Vue3 响应式原理深度解析(Proxy + effect 栈 + 依赖追踪)
🎯 一、核心问题
问:Vue3 响应式是如何实现的?Proxy、effect 栈和依赖追踪是怎么协作的?
这是高频面试深度题,面试官一般会追问 “为什么 Vue3 性能比 Vue2 好”。
🎯 二、标准回答(面试官满意版)
-
响应式核心是 Proxy
- Vue3 不再使用 defineProperty,而是使用 Proxy 代理对象
- Proxy 可以拦截
get、set、deleteProperty等操作 - 支持新增属性、数组下标变化、删除属性等
-
依赖收集(Track)
- 当访问响应式对象的属性时,Proxy 的
get会被触发 - Vue 内部将当前执行的 effect(函数)记录到 依赖集合
- 这样当属性变化时,可以精准触发依赖的 effect
- 当访问响应式对象的属性时,Proxy 的
-
effect 栈
- Vue3 用栈结构维护当前激活的 effect
- 当一个 effect 执行时,push 到栈顶
- 在
get触发时,依赖收集使用栈顶 effect - 结束后 pop 出栈
- 解决嵌套 effect 的依赖追踪问题
-
触发依赖(Trigger)
- 当属性通过 Proxy
set改变时 - Vue 会查找依赖集合,并依次执行 effect
- 实现组件重新渲染或 computed 更新
- 当属性通过 Proxy
🎯 三、简化流程图
响应式对象被访问
│
▼
Proxy get 拦截
│
▼
track(effect) 收集依赖
│
▼
数据变化
│
▼
Proxy set 拦截
│
▼
trigger(effect) 触发依赖执行
🎯 四、示例代码(原理演示)
import { reactive, effect } from 'vue'
const state = reactive({ count: 0 })
effect(() => {
console.log('count =', state.count)
})
state.count++ // 自动触发 effect,打印最新值
解释:
-
reactive创建 Proxy -
effect包裹回调函数并入栈 - 访问
state.count→ track 收集依赖 - 修改
state.count→ trigger 执行 effect
🎯 五、面试官常见追问(高频)
追问 1:为什么 Vue3 使用 Proxy,比 Vue2 defineProperty 性能更好?
- 不需要递归遍历整个对象(惰性代理)
- 支持新增 / 删除属性、数组下标
- 更少内存开销
- 依赖收集更精准,减少无效更新
追问 2:effect 栈有什么作用?
- 支持嵌套 effect
- 避免误收集 effect
- 保证 track 时总是收集当前激活的 effect
追问 3:依赖追踪如何避免重复收集?
- 每个属性依赖使用 Set 保存 effect
- 同一个 effect 不会重复加入依赖集合
追问 4:computed 是怎么利用 effect 栈的?
- computed 内部也用 effect 包裹 getter
- effect 栈保证依赖收集在计算时有效
- 依赖变化时,computed 被标记 dirty,下次访问重新计算
追问 5:响应式循环引用怎么办?
- Vue3 Proxy 本身支持循环对象
- 每次 reactive 调用会 缓存已代理对象
- 避免重复代理和无限递归
🎯 六、一句话总结(面试官必背)
Vue3 响应式原理:通过 Proxy 拦截对象操作 + effect 栈管理当前执行上下文 + track/trigger 精准收集依赖,实现高性能、可缓存、可嵌套的响应式系统。
解释watch和computed的原理
1️⃣ 核心区别表
| 特性 | computed | watch |
|---|---|---|
| 用途 | 计算衍生数据 | 监听变化执行副作用 |
| 返回值 | 会返回值并缓存 | 无返回值(主要触发回调) |
| 执行时机 | 访问 .value 时才执行 |
数据变化时立即触发(flush 可控制) |
| 缓存机制 | ✅ 会缓存,依赖未变不重新计算 | ❌ 不缓存,每次依赖变化都会触发回调 |
| 依赖收集 | 自动收集依赖 | 需手动指定监听对象(或 watchEffect 自动收集) |
| 可获取 oldValue | ❌ | ✅ |
| 异步支持 | ❌ 仅同步计算 | ✅ 可做异步副作用(API、DOM 操作等) |
| 适合场景 | 纯逻辑计算、UI 展示数据 | 副作用逻辑、异步请求、操作 DOM 等 |
2️⃣ 理解方式
-
computed = 计算属性(惰性求值 + 缓存)
- 主要用于根据已有数据计算出新的数据
- 访问 value 时才触发计算
- 不适合做副作用
-
watch = 数据侦听器(观察者模式)
- 用于监听某个响应式变量变化
- 触发回调执行副作用(API 请求、DOM 操作、日志等)
- 可访问新旧值
- 可以做异步操作
3️⃣ 原理上的差异
-
computed
- 内部有
effect+dirtyflag - 依赖变化时标记 dirty = true
- 下一次访问 value 时重新计算
- 可以缓存,减少重复计算
- 内部有
-
watch
- 直接对响应式数据注册 effect
- 数据变化时立即触发回调
- 回调不返回值
- 可选择 flush:
pre/post/sync控制执行时机
computed 的原理
computed 本质上是一个 带缓存的响应式副作用:
-
依赖收集
- 当访问
computed.value时,Vue 会触发其内部effect,追踪它依赖的响应式数据(ref 或 reactive 对象)。
- 当访问
-
惰性求值(lazy)
- 初次访问时执行 getter,生成值并缓存
- 标记
dirty = false
-
缓存机制
- 当依赖变化时,响应式系统会将
dirty = true - 下次访问
.value时重新计算并更新缓存
- 当依赖变化时,响应式系统会将
-
不可获取旧值
- 因为 computed 只关注最新值,内部只存当前缓存
示意流程:
访问 computed.value → 检查 dirty
├─ dirty = true → 执行 getter → 缓存值 → dirty = false
└─ dirty = false → 直接返回缓存值
依赖变化 → effect 标记 dirty = true
示例代码:
import { ref, computed } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
console.log(double.value) // 2
count.value++
console.log(double.value) // 4,重新计算
2️⃣ watch 的原理
watch 本质是一个 响应式数据的观察者(Watcher) ,用于执行副作用:
-
手动依赖选择
- 你传入要监听的响应式对象(ref / reactive / getter)
- Vue 内部会对其注册 effect
-
响应式触发
- 当被监听的数据发生变化时,effect 会触发回调函数
-
可获取 oldValue / newValue
- watch 在触发时会提供新值和旧值
-
异步可选
- 默认 flush: 'pre'
- 可以指定 'post'(DOM 更新后)或 'sync'(同步执行)
-
没有缓存
- 每次数据变化都会调用回调函数
- 适合副作用逻辑,不用于纯计算
示意流程:
watch(reactiveData, callback)
└─ 内部创建 effect
依赖变化 → 触发 effect → callback(newValue, oldValue)
示例代码:
import { ref, watch } from 'vue'
const count = ref(1)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
count.value++ // 触发 watch 回调
3️⃣ 原理总结对比
| 特性 | computed | watch |
|---|---|---|
| 内部机制 | effect + dirty flag | effect 直接触发回调 |
| 触发时机 | 访问 .value 时(懒执行) |
数据变化时立即触发 |
| 缓存 | ✅ 缓存上次计算值 | ❌ 不缓存,每次触发回调 |
| oldValue | ❌ 无 | ✅ 有 |
| 使用目的 | 衍生数据计算 | 执行副作用(异步、DOM 操作等) |
【vue高频面试题】第 15 题:computed vs watch 区别 + 使用场景
第 15 题:computed vs watch 区别 + 使用场景
🎯 一、标准回答(面试官必答点)
| 特性 | computed | watch |
|---|---|---|
| 依赖收集 | 自动依赖追踪 | 需要手动指定依赖 |
| 返回值 | 会缓存(lazy evaluation) | 不缓存 |
| 用途 | 计算衍生数据 | 执行副作用(API、DOM操作等) |
| 执行时机 | 访问时才执行 | 数据变化时触发 |
| 获取 oldValue | ❌ | ✔ 可以获取 |
| 适用场景 | 纯计算逻辑 | 监听变化做异步或副作用处理 |
🎯 二、代码示例
1️⃣ computed 示例(缓存计算)
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
console.log(double.value) // 2
count.value++
console.log(double.value) // 4,computed 会自动更新并缓存
</script>
特点:
- 只有访问
double.value时才执行 - 如果依赖未变化,直接返回缓存值
2️⃣ watch 示例(副作用监听)
<script setup>
import { ref, watch } from 'vue'
const count = ref(1)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
count.value++ // 触发 watch 回调
特点:
- 触发副作用(日志、请求 API、操作 DOM)
- 可以获取
oldVal - 默认 flush: 'pre'(可指定 post 或 sync)
🎯 三、面试官追问(高频)
追问 1:computed 能做异步吗?
答:
- computed 不支持异步(只计算同步逻辑)
- 异步逻辑应该用
watch或watchEffect
追问 2:computed 的懒执行原理?
答:
- 内部依赖收集 + dirty flag
- 依赖变化时标记 dirty = true
- 下次访问 value 时才重新计算并缓存结果
追问 3:computed vs watchEffect 区别?
| 特性 | computed | watchEffect |
|---|---|---|
| 返回值 | 缓存返回值 | 无返回值 |
| 依赖 | 自动收集 | 自动收集 |
| 执行时机 | 访问 value 时才执行 | 默认立即执行一次 |
| 用途 | 衍生数据 | 副作用逻辑 |
追问 4:watch 可以替代 computed 吗?
答:
- 技术上可以,但不推荐
- watch 不缓存,每次访问都可能重新执行
- computed 更轻量且性能高
追问 5:computed 可以和 ref 联动吗?
const count = ref(1)
const double = computed(() => count.value * 2)
double.value++ // ❌ 报错,computed 默认只读
-
解决方法:使用
computed({ get, set })创建可写 computed
const double = computed({
get: () => count.value * 2,
set: val => count.value = val / 2
})
🎯 四、一句话总结(背面试官即可)
computed 用于缓存衍生数据,懒执行、自动依赖收集;watch 用于监听数据变化触发副作用,可获取新旧值。computed 更轻量,watch 更灵活,适合异步或副作用逻辑。
【vue高频面试题】第 14 题:Vue3 中虚拟 DOM 是什么?为什么要用?如何提升性能?
第 14 题:Vue3 中虚拟 DOM 是什么?为什么要用?如何提升性能?
🎯 一、标准回答(面试官必问核心点)
1️⃣ 什么是虚拟 DOM(VNode)
虚拟 DOM 是 Vue 内部的一种 JavaScript 对象表示的 DOM 树,抽象出真实 DOM 的结构和属性。
示例:
{
tag: 'div',
props: { id: 'app' },
children: [
{ tag: 'span', children: 'Hello' }
]
}
它是内存中的 DOM 描述,不是浏览器真实 DOM。
2️⃣ 为什么要用虚拟 DOM
- 避免直接操作真实 DOM,性能高
- 跨平台渲染(可渲染到浏览器、Native、Canvas 等)
- Diff 算法:对比新旧 VNode,只更新变化部分
- 组合式渲染优化:更方便管理响应式更新
🎯 二、虚拟 DOM 优势(高分必答点)
| 优势 | 解释 |
|---|---|
| 性能优化 | 批量 diff → 最小化真实 DOM 更新 |
| 平台无关 | 可渲染在浏览器、服务端、Native、Canvas |
| 响应式结合 | Vue 响应式系统修改数据 → 生成新 VNode → diff → patch |
| 可调试 | 内存中对象易于分析、调试、测试 |
| 跨渲染器 | Vue3 可以通过 renderer API 渲染不同平台 |
🎯 三、Vue3 中虚拟 DOM 流程(高频面试考点)
- 模板 / render 函数 → 生成 VNode
- 响应式数据变化 → 触发 effect → 重新生成新的 VNode
- Diff 算法 → 对比新旧 VNode
- Patch 更新 → 最小化真实 DOM 更新
🎯 四、代码示例(render 函数)
import { h, render } from 'vue'
const vnode = h('div', { id: 'app' }, [
h('span', 'Hello Vue3')
])
render(vnode, document.getElementById('root'))
流程解释:
-
h()→ 创建 VNode -
render()→ 挂载或更新 DOM - 数据变更 → diff → patch → 最小更新
🎯 五、面试官常见追问
追问 1:虚拟 DOM 对性能真的有提升吗?
回答:
- 对小型应用差异不大,但对于频繁修改 DOM 或大规模渲染时,diff 算法减少大量 DOM 操作,性能提升明显。
- Vue3 中 静态节点提升 + PatchFlag 进一步优化性能。
追问 2:Vue3 Diff 算法怎么优化的?
回答:
-
PatchFlag 静态提升
- 静态节点不需要重新渲染
-
Keyed Diff
- 用
key快速比较列表节点
- 用
-
Fragment 支持
- 减少额外 DOM 包裹元素
-
Teleport / Suspense
- 提升复杂场景渲染效率
追问 3:虚拟 DOM 会消耗大量内存吗?
回答:
- 是的,但 Vue3 使用 Proxy + effect + PatchFlag 减少重复创建
- 对于大规模列表,建议使用
v-for key+ 虚拟滚动等优化
追问 4:Vue3 为什么虚拟 DOM 不像 Vue2 那么慢?
回答:
- Vue3 采用 Proxy + Composition API + Compiler 优化
- 静态节点提升 + PatchFlag + Fragment → 减少 diff 量
- 更少中间对象生成 → 性能显著提升
🎯 六、killer 问题(面试官爱问的进阶题)
❓ “虚拟 DOM 和真实 DOM 操作相比的优劣?”
答案要点:
- 优点:批量处理、跨平台、可控更新
- 缺点:比直接操作真实 DOM 多一次中间层
- 结论:在大多数复杂应用中优势大于劣势,尤其配合 Vue3 编译优化
❓ “PatchFlag 是什么?为什么 Vue3 需要它?”
- PatchFlag:编译阶段标记 VNode 的变化类型
- 作用:让渲染器只对变化的部分进行 patch
- 优化性能 → 减少 diff 计算
🎯 七、一句话总结(背面试官即可)
虚拟 DOM 是 Vue 内存中 DOM 描述对象,通过 diff 算法实现最小化更新,避免直接操作真实 DOM,Vue3 通过 PatchFlag、静态提升和 Proxy 响应式极大优化了性能。
【vue高频面试题】第 13 题:Vue 的 `nextTick` 原理是什么?为什么需要它?
第 13 题:Vue 的 nextTick 原理是什么?为什么需要它?
🎯 一、标准回答(面试必须说的核心)
nextTick 用来在 下一轮 DOM 更新完成之后 执行回调。
因为 Vue 的 DOM 更新是 异步的、批处理的,当你修改响应式数据时,DOM 并不会立即更新,而是放到一个任务队列里,等同一轮事件循环结束后统一更新。因此,如果你想操作“更新后的 DOM”,必须使用 nextTick。
🎯 二、底层原理(高频考点,务必背)
Vue 的 nextTick 本质上是:
使用 微任务(Promise.then)优先 实现的异步回调机制,将任务推入事件循环的下一 tick 执行。
Vue3 实现优先级:
- Promise.then(微任务)
- 如果 Promise 不可用 → 再降级到 MutationObserver
- 最后才使用 setTimeout(宏任务)
为什么用微任务?
- 优先级比 setTimeout 高
- 执行非常快
- 能保证 DOM 更新后尽快执行回调
这句话面试官很爱听:
Vue3 中 DOM 更新是异步批处理的,而 nextTick 就是让代码在 DOM 更新完后再执行。
🎯 三、为什么 DOM 更新是异步的?
这是面试官 100% 会追问的。
✔ 同步更新 DOM 性能太差
频繁数据变化会导致频繁 DOM 重绘、回流,性能崩溃。
✔ 异步批处理可以合并更新
多次状态修改:
count.value++
count.value++
count.value++
Vue 不会更新 3 次,而是:
合并所有状态变更 → 执行一次 DOM 更新
✔ nextTick 用于等待这个更新完成
非常好记:
“数据改完不是马上改 DOM,需要 nextTick 等 Vue 批量改完 DOM 后再执行代码。”
🎯 四、简单理解 nextTick 的执行机制(半白话)
你连续提交很多 DOM 更新,但 Vue 不会立即执行,而是推入任务队列。
然后:
- 当前同步代码执行完
- 微任务队列执行(Promise.then)
- Vue 批量更新 DOM
- 执行 nextTick 回调
顺序大概是:
同步代码 → Vue 收集更新 → 微任务执行 → DOM 更新 → nextTick 执行
🎯 五、代码示例(面试官通常让你举例)
count.value++
await nextTick()
console.log(divRef.value.innerText) // 拿到的是更新后的 DOM
🎯 六、手写一个 nextTick(必加分)
面试官看到你能手写,直接对你印象爆炸加分。
const callbacks = []
let pending = false
function nextTick(cb) {
callbacks.push(cb)
if (!pending) {
pending = true
Promise.resolve().then(() => {
pending = false
callbacks.forEach(fn => fn())
callbacks.length = 0
})
}
}
解释:
- 存储回调
- 批处理
- 利用 Promise.then 在微任务执行
🎯 七、面试官常见追问(附最佳回答)
追问 1:Vue3 DOM 更新是同步还是异步?
回答:
异步。Vue3 会将多次数据修改合并在一起,通过微任务在下一个 tick 统一更新 DOM。
追问 2:为什么 nextTick(() => {}) 有时还是拿不到 DOM?
回答:
因为你监听的是数据变化,而不是 DOM 更新;你需要确保 DOM 渲染参与了更新。
某些场景必须使用await nextTick()连续两次,或用flush: 'post'。
追问 3:watch 的 flush: 'post' 和 nextTick 的区别?
回答:
-
nextTick:等待 DOM 更新之后 执行 -
flush: 'post':让回调在 DOM 更新之后执行(类似 nextTick)
例如:
watch(count, () => {
console.log('DOM 已更新')
}, { flush: 'post' })
二者都可以“等待 DOM 更新”,但:
-
nextTick用于数据修改后手动等待 DOM -
post用于 侦听器自动等待 DOM
追问 4:nextTick 是不是 Vue 特有的?
回答:
不是,React 也有类似机制,所有虚拟 DOM 框架都有异步批处理。
🎯 八、一句话总结(背下来)
Vue 的 nextTick 利用 Promise 微任务,将回调延迟到 DOM 异步批处理更新完成后执行,用于保证你获取到的是最新 DOM。
来详细解释 Vue3 中 watch 和 computed 的区别,从原理、用途和执行方式多维度展开,让你面试答题时一说就懂。
【vue高频面试题】第 12 题:Vue(尤其 Vue3)中父子组件通信方式有哪些?区别是什么?
第 12 题:Vue(尤其 Vue3)中父子组件通信方式有哪些?区别是什么?
这是 Vue 面试必考题,至少 90% 面试官会问。
🎯 一、标准答案(你在面试里应该先说的)
Vue3 中父子组件通信有 七 大常用方式:
父 → 子
- Props(最常见)
- Expose(父通过 ref 调用子方法)
子 → 父
- Emit 事件
父 ↔ 子
- v-model(语法糖,本质是 props + emit)
跨层级
- provide / inject
更大范围
- Pinia / Vuex(状态管理)
更底层/特殊情况
- EventBus(Vue3 不推荐,但面试会问)
🎯 二、父 → 子通信方式
1️⃣ Props(最常用)
父组件:
<Child :msg="text" />
子组件:
defineProps({
msg: String
})
Props 特性:
- 单向数据流(父 → 子)
- 子不允许修改 props(会直接报错)
- 适合传递配置类、静态数据
2️⃣ defineExpose(父组件通过 ref 调用子组件方法)
子组件:
const submit = () => { ... }
defineExpose({ submit })
父组件:
<Child ref="childRef" />
childRef.value.submit()
使用场景:
- 子组件希望暴露一些内部方法给父组件
- 提交表单、重置、校验等
🎯 三、子 → 父通信方式
3️⃣ Emit 事件
子组件:
const emit = defineEmits(['update'])
emit('update', newValue)
父组件:
<Child @update="handleUpdate" />
特点:
- 单向数据流(子 → 父)
- 组件解耦,官方推荐方式
🎯 四、父 ↔ 子 双向通信
4️⃣ v-model(语法糖)
父组件:
<Child v-model="value" />
子组件:
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
emit('update:modelValue', 123)
本质只是 props + emit 的语法糖。
🎯 五、跨层级通信
5️⃣ provide / inject
祖先组件:
provide('theme', 'dark')
任意层子组件:
const theme = inject('theme')
特点:
- 多层级,不需要一层一层传 props
- 常用于配置类、上下文类数据(如表单组件库)
🎯 六、全局状态通信
6️⃣ Pinia(或 Vuex)
适合全局数据:
- 用户信息
- 权限状态
- 全局设置
- 缓存数据
Pinia 是 Vue3 官方主推状态库。
🎯 七、特殊情况
7️⃣ EventBus(Vue3 不推荐,但要知道)
适合简单的事件广播场景:
import mitt from 'mitt'
export const bus = mitt()
组件 A:
bus.emit('hello', 123)
组件 B:
bus.on('hello', val => {})
🔥 六、面试官常见追问(附高能回答)
追问 1:父子通信和 provide/inject 的区别是什么?什么时候用哪个?
你应该这样答:
- 父子 props/emit:适合直接父子通信,数据清晰可控
- provide/inject:适合跨层级通信,比如 3、5 层以上
- provide/inject 数据不是响应式(除非你传 ref 或 reactive)
✔ 高分补充:
provide/inject 更像“依赖注入”,在 UI 组件库中用得非常多(Element Plus、Ant Design Vue)。
追问 2:v-model 为什么能实现双向绑定?底层是什么?
标准回答:
v-model 本质是 props(modelValue) + emit(update:modelValue)
就是:
父给子:modelValue
子返父:update:modelValue
追问 3:defineExpose 是什么时候用?和 props/emit 的关系?
答:
-
expose 用来暴露方法给父组件调用
-
和 props/emit 不一样:
- props/emit 用于 数据
- expose 用于 方法、行为
例如表单组件:
defineExpose({
validate,
reset
})
追问 4:provide 是不是响应式的?如何让它变成响应式?
默认 不是响应式(非常多人不知道!)
必须这样传:
const theme = ref('dark')
provide('theme', theme)
inject 处能响应变化。
追问 5:EventBus 为什么不推荐?
- 容易造成事件混乱
- 不易追踪
- 大项目可维护性差
- TypeScript 支持差
面试官会觉得你非常专业。
🎯 七、一句话总结(背下来就能面试用)
Vue3 父子通信分:props、emit、v-model、expose,跨层级用 provide/inject,全局用 Pinia,少量事件可用 EventBus。
项目性能优化实践:深入FMP算法原理探索|得物技术
什么是模块联邦?(Module Federation)
一、前端模块共享的传统困境
在大型前端项目或多应用协作场景中,“模块共享” 是绕不开的需求 —— 比如多个应用复用同一套按钮组件、工具函数,或不同团队协作开发时共享基础模块。但在模块联邦(Module Federation)出现前,传统方案始终存在难以解决的痛点:
1. npm 包共享:迭代效率低下
将公共组件打包成 npm 包是最常见的方式,但流程繁琐:修改组件后需重新发布版本,所有依赖该包的应用都要手动更新依赖、重新构建部署。对于频繁迭代的内部组件库,这种 “发布 - 安装 - 重构” 的循环严重拖慢开发效率,且容易出现 “版本不一致导致的兼容问题”。
2. CDN 直接引入:粗糙且易出问题
为了跳过 npm 发布流程,部分团队会将组件打包成 umd 格式丢到 CDN,消费方通过
- 依赖需手动管理:如果共享组件依赖 React、Vue 等库,消费方必须手动引入对应版本,否则会出现 “模块未定义” 的运行时错误;
- 全局变量污染:umd 包通常挂载到 window 上,多个模块可能出现命名冲突;
- 无法按需加载:
- 无版本控制:CDN 资源更新后,需手动处理缓存或版本号,容易出现 “旧版本缓存导致的功能异常”。
3. 早期微前端:耦合度高,共享粒度粗
早期微前端方案(如 qiankun)更侧重 “应用级嵌入”—— 将多个独立应用整合成一个整体,但跨应用共享组件 / 模块时需额外适配(如通过自定义事件传递状态),集成成本高,且共享粒度仅停留在 “整个应用”,无法实现细粒度的组件 / 工具函数共享。
这些痛点的核心矛盾的是:传统方案无法在 “高效迭代”“按需加载”“依赖自动管理” 之间找到平衡,而模块联邦的出现,正是为了解决这一核心矛盾。
二、模块联邦:前端模块共享的终极方案
模块联邦(Module Federation,简称 MF)是 Webpack 5 推出的核心特性,其核心目标是:打破应用边界,让多个独立构建的前端应用,像使用本地模块一样按需共享组件、工具函数,且无需手动处理依赖和部署流程。
简单来说,它就像一个 “前端模块的共享枢纽”—— 每个应用都可以作为 “模块提供者”(暴露自身模块)或 “模块消费者”(加载其他应用的模块),甚至两者兼具,形成灵活的模块共享生态。
三、模块联邦的核心原理(结合通俗理解)
很多人第一次接触模块联邦时,会有这样的直观认知:“是不是把共享组件放到 CDN 上,消费方通过 Webpack 识别 import 语句,再请求 CDN 资源并注入使用?”—— 这个理解方向完全正确,我们可以基于这个认知,拆解其底层原理:
核心原理一句话总结
模块联邦本质是:将共享模块(带依赖元信息)部署到远程服务器(如 CDN),Webpack 通过 “编译时标记 + 运行时加载”,让消费方用原生 import 语法加载远程模块;当代码执行到该 import 时,自动请求远程资源,经格式适配和依赖处理后,注入宿主应用的模块系统,实现 “本地使用” 的体验。
原理拆解(分 4 步)
1. 模块提供者:打包 “带元信息的共享模块”
模块提供者(称为 Remote 应用)打包时,Webpack 会做两件关键事:
- 生成 remoteEntry.js:这不是模块本身,而是 “模块导航文件”—— 包含模块清单(暴露了哪些模块、模块的真实地址)、依赖元信息(模块依赖的库如 React/Vue)、加载器函数(用于后续解析模块);
- 拆分模块 chunk:将暴露的组件 / 工具函数拆成独立的 JS chunk(如 Button 组件对应 123.js),并部署到 CDN 或远程服务器。
这里的关键是:共享的不是 “裸模块”,而是 “带依赖元信息的模块单元” ,避免了传统 CDN 引入时 “依赖手动管理” 的问题。
2. 模块消费者:编译时标记远程模块
模块消费者(称为 Host 应用)配置 Webpack 时,会声明要加载的远程应用(如 app2: 'app2@cdn.example.com/remoteEntry…')。Webpack 编译时会识别这类远程模块的 import 语句(如 import 'app2/Button'),并做 “标记”—— 告诉运行时:“这个模块不是本地的,需要从远程加载”。
这一步就像你理解的 “Webpack 的 switch 功能”:编译时给远程模块打标签,运行时遇到标签就切换到 “远程加载逻辑”,而非本地模块的 “文件读取逻辑”。
3. 运行时:异步请求远程资源(非简单 script 插入)
当宿主应用运行到远程模块的 import 语句时,会触发以下流程:
- 第一步:加载 remoteEntry.js:通过 Webpack 内置的异步加载逻辑(而非直接插入
- 第二步:解析模块地址:remoteEntry.js 执行后,通过其内置的模块清单,找到目标模块(如 app2/Button)对应的真实 CDN 地址(如 cdn.example.com/123.js);
- 第三步:加载目标模块:通过 Webpack 封装的异步加载函数(如 webpack_require.e)请求目标模块的 JS chunk,这一步同样不是简单插入
4. 注入使用:依赖自动处理 + 格式适配
目标模块加载完成后,Webpack 会做最后两件事:
- 依赖自动复用:如果远程模块依赖的库(如 React),宿主应用已加载,则直接复用,避免重复打包;若未加载,则自动加载共享依赖(通过 shared 配置控制);
- 格式适配与注入:将远程模块的代码格式,转换成宿主应用能识别的模块格式(避免全局污染),并注入宿主的模块系统 —— 此时,远程模块就像本地模块一样,可直接使用。
与 “简单 CDN 引入” 的核心区别
| 对比维度 | 简单 CDN 引入(umd 包) | 模块联邦 |
|---|---|---|
| 依赖管理 | 手动引入,易冲突 | 自动识别依赖,共享复用 |
| 加载方式 | 插入 | 异步请求 Webpack 格式 chunk,无全局污染 |
| 按需加载 | 不支持,同步加载 | 支持,用到才加载 |
| 版本控制 | 需手动管理版本号 / 缓存 | 通过 shared 配置控制版本兼容 |
| 使用体验 | 需从 window 取模块,体验割裂 | 原生 import 语法,和本地模块一致 |
四、模块联邦的实际使用方法(React 示例)
下面用两个应用演示核心用法:app1(宿主应用,加载远程模块)和 app2(远程应用,暴露共享组件)。
前置条件
- 构建工具:Webpack 5+(仅 Webpack 5 原生支持模块联邦);
- 技术栈:React(Vue 用法类似,核心配置一致)。
1. 远程应用(app2:模块提供者)
步骤 1:配置 Webpack(webpack.config.js)
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devServer: { port: 3002 }, // 远程应用运行端口
output: {
uniqueName: 'app2', // 远程应用唯一标识(避免冲突)
},
plugins: [
new ModuleFederationPlugin({
name: 'app2', // 远程应用名称(宿主需通过该名称引用)
filename: 'remoteEntry.js', // 模块导航文件(必须叫这个名字)
exposes: {
// 暴露的模块:key 是宿主引用的路径,value 是本地模块路径
'./Button': './src/Button', // 暴露 Button 组件
'./utils': './src/utils', // 暴露工具函数
},
// 共享依赖:避免 React/ReactDOM 重复打包
shared: {
react: { singleton: true, eager: true }, // singleton:单例模式;eager:优先加载
'react-dom': { singleton: true, eager: true },
},
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
};
步骤 2:编写共享模块
创建 src/Button.jsx(共享组件):
export default function App2Button() {
return <button style={{ color: 'red', fontSize: '20px' }}>我是 app2 的共享按钮</button>;
}
步骤 3:启动远程应用
运行 npm run dev,远程应用会启动在 http://localhost:3002,此时可通过 http://localhost:3002/remoteEntry.js 访问导航文件。
2. 宿主应用(app1:模块消费者)
步骤 1:配置 Webpack(webpack.config.js)
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devServer: { port: 3001 }, // 宿主应用运行端口
plugins: [
new ModuleFederationPlugin({
name: 'app1', // 宿主应用名称(仅用于标识)
// 配置要加载的远程应用:key 是远程应用名称,value 是 "远程名称@导航文件地址"
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
// 共享依赖:和远程应用保持一致,避免重复打包
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
};
步骤 2:使用远程模块
创建 src/App.jsx,用原生 import 语法加载远程模块:
import React, { lazy, Suspense } from 'react';
// 按需加载远程模块(推荐):用 lazy + Suspense 处理加载状态
const App2Button = lazy(() => import('app2/Button')); // 格式:远程应用名称/暴露的模块 key
const App2Utils = lazy(() => import('app2/utils'));
function App() {
return (
<div style={{ padding: '20px' }}>
<h1>我是宿主应用(app1)</h1>
{/* 远程模块加载时显示 fallback */}
<Suspense fallback="加载中...">
<App2Button />
</Suspense>
</div>
);
}
export default App;
步骤 3:启动宿主应用
运行 npm run dev,访问 http://localhost:3001,即可看到来自 app2 的红色按钮 —— 此时,宿主应用已成功加载并使用远程模块,且完全感知不到 “这是远程资源”。
五、模块联邦的核心特性与适用场景
核心特性
- 独立部署:宿主和远程应用可各自独立开发、构建、部署,修改远程模块后无需重构宿主;
- 双向共享:一个应用既可以是宿主(加载模块),也可以是远程(暴露模块);
- 依赖去重:共享依赖自动复用,避免重复打包,减小包体积;
- 按需加载:远程模块仅在使用时才加载,优化首屏性能。
适用场景
- 微前端架构:实现细粒度的组件 / 模块共享,替代传统粗粒度的应用嵌入;
- 大型应用拆分:将巨型应用拆分为多个独立构建的子应用(如首页、订单页),各自迭代;
- 内部组件库共享:无需发布 npm 包,多个应用实时使用最新版公共组件;
- 跨团队协作:不同团队维护不同子应用,通过模块联邦无缝集成,降低协作成本。
六、使用注意事项
- 依赖版本兼容:共享依赖(如 React)需保证版本兼容,可通过 requiredVersion 配置限制版本;
- 构建工具限制:仅 Webpack 5+ 原生支持,Vite 需用 vite-plugin-federation 插件,Rollup 支持有限;
- 缓存与降级:给 remoteEntry.js 和模块 chunk 加哈希值(如 remoteEntry.[hash].js),避免缓存问题;同时需做好降级方案(如远程应用挂掉时,显示本地备用组件);
- 样式隔离:共享组件的样式需避免污染宿主,可使用 CSS Modules、Shadow DOM 等方案。
总结
模块联邦的核心价值,是让前端模块共享从 “繁琐的手动管理” 走向 “自动化、按需化、低耦合”。它没有改变开发者的使用习惯(仍用 import 语法),却解决了传统方案的核心痛点,让大型前端项目的拆分与协作变得更高效。
如果你正在面临 “多应用共享组件”“大型项目拆分” 等问题,模块联邦无疑是当前最理想的解决方案 —— 它不仅是一种技术,更是一种 “前端架构的设计思想”:打破边界,让模块自由流动。
写埋点、扒 SDK、改框架:JS 函数复写 10 连招实战手册
# JS 中“复写函数”的 10 种姿势(附完整可运行 Demo)
在前端工程里,“复写一个函数”(Hook / Patch / Override)是非常常见的需求,尤其是在:
- 调试黑盒 SDK
- 做监控埋点
- 改第三方库行为但又不能直接改源码
本文把常见的 10 种手法整理成一个“武器清单”,**每种都给出可直接跑的 Demo**,方便你复制到控制台 / Node 里试。
---
## 🟦 一、直接覆写:最简单粗暴
**思路**:如果函数本来就是某个对象上的方法,直接重新赋值即可。
### 示例场景
已有一个业务对象:
```js
const userService = {
getUserName(id) {
console.log('[orig] getUserName', id);
return 'Alice';
}
};
// 原始调用
console.log('--- 原始调用 ---');
console.log(userService.getUserName(1));
现在想完全替换掉 getUserName 的实现:
// 直接覆写
userService.getUserName = function (id) {
console.log('[new] getUserName', id);
return 'Bob';
};
console.log('--- 覆写后 ---');
console.log(userService.getUserName(1));
特点:
- 简单直接
- 不保留原逻辑(除非你自己先存一份引用)
🟩 二、Monkey Patch:保留原逻辑再“加一层壳”(最常用)
思路:保存原函数引用 → 用同名函数包装一层。
示例:给 SDK 方法加埋点
// 模拟一个 SDK
const sdk = {
track(eventName, payload) {
console.log('[orig track]', eventName, payload);
return { ok: true };
}
};
// 原始使用
console.log('--- 原始调用 ---');
sdk.track('page_view', { path: '/home' });
// 开始 Monkey Patch
const origTrack = sdk.track;
sdk.track = function (...args) {
console.log('[before track]', ...args);
const result = origTrack.apply(this, args);
console.log('[after track]', result);
return result;
};
console.log('--- Patch 之后 ---');
sdk.track('page_view', { path: '/detail' });
适用:
- 想在原逻辑前后“插入”自己的逻辑
- Hook 黑盒 SDK、监控、埋点等
🟧 三、Proxy:拦截整个对象(高级黑盒分析)
思路:用 Proxy 代理整个对象,在 get 里统一拦截函数调用。
示例:拦截所有方法调用做日志
// 一个“黑盒”对象,方法名可能会变
const api = {
login(user, pwd) {
console.log('login...', user);
return { token: 'xxx' };
},
logout() {
console.log('logout...');
}
};
// 用 Proxy 包一层
const apiWithLog = new Proxy(api, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function (...args) {
console.log(`[Proxy] call ${String(prop)} with`, args);
const result = value.apply(this, args);
console.log(`[Proxy] ${String(prop)} result`, result);
return result;
};
}
return value;
}
});
// 使用方式跟原来一样
console.log('--- 使用 Proxy 后 ---');
apiWithLog.login('alice', '123456');
apiWithLog.logout();
适用:
- 你不知道具体方法名(SDK 里一堆方法)
- 想统一拦截所有方法调用(sniff)
🟨 四、Object.defineProperty:强行改 getter / setter / 不可枚举属性
思路:当属性不可写、或是 getter / setter,需要用 defineProperty 操作描述符。
示例 1:把只读属性改成可写函数
const obj = {};
// 初始定义:只读属性
Object.defineProperty(obj, 'foo', {
value: 'readonly',
writable: false,
configurable: true,
enumerable: true
});
console.log('原始 foo =', obj.foo);
// 直接赋值会失败(严格模式下报错,非严格模式下无效)
obj.foo = 'new';
console.log('直接赋值后 foo =', obj.foo); // 还是 readonly
// 用 defineProperty 强行改成函数
Object.defineProperty(obj, 'foo', {
configurable: true,
writable: true,
value: function () {
console.log('I am new foo()');
}
});
console.log('--- 改成函数之后 ---');
obj.foo();
示例 2:用 getter 做懒加载 Hook
const service = {};
Object.defineProperty(service, 'expensiveApi', {
configurable: true,
get() {
console.log('[getter] 初始化 expensiveApi');
// 真正创建对象
const realApi = {
call() {
console.log('real api called');
}
};
// 用普通属性替换掉 getter(只初始化一次)
Object.defineProperty(service, 'expensiveApi', {
value: realApi,
writable: false,
configurable: false
});
return realApi;
}
});
// 第一次访问会触发 getter
console.log('--- 第一次访问 ---');
service.expensiveApi.call();
// 第二次访问直接用缓存
console.log('--- 第二次访问 ---');
service.expensiveApi.call();
🟥 五、Hook 全局内置方法:网络、事件、路由等(非常强)
示例 1:拦截 fetch(浏览器)
// 保存原始 fetch
const origFetch = window.fetch;
// 覆写
window.fetch = async (...args) => {
console.log('[Hook fetch] 请求参数:', args);
const res = await origFetch(...args);
// 复制一份 Response 查看内容(注意:这里只是示意,实际需考虑流只能读一次)
const clone = res.clone();
clone.text().then(text => {
console.log('[Hook fetch] 响应文本截断:', text.slice(0, 100));
});
return res;
};
// 使用:之后所有 fetch 都会经过 Hook
// fetch('https://jsonplaceholder.typicode.com/todos/1');
示例 2:拦截所有 DOM 事件监听
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options) {
console.log('[Hook addEventListener]', type, 'on', this);
// 也可以在这里包一层 listener,实现事件埋点
return origAdd.call(this, type, listener, options);
};
// Demo:添加一个 click 事件
document.body.addEventListener('click', () => {
console.log('body clicked');
});
还能 Hook:
XMLHttpRequest.prototype.sendWebSocket.prototype.send-
history.pushState / replaceState(前端路由监控) -
console.log(调试场景)
🟪 六、代理模块加载:require Hook / 构建期 alias
6.1 Node:Hook require(CommonJS)
// hook-require.js
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function (path) {
console.log('[require]', path);
const exports = originalRequire.apply(this, arguments);
// 针对指定模块做 Monkey Patch
if (path === './sdk') {
const origDo = exports.doSomething;
exports.doSomething = function (...args) {
console.log('[patched sdk.doSomething]', args);
return origDo.apply(this, args);
};
}
return exports;
};
// 主入口
require('./hook-require');
const sdk = require('./sdk');
sdk.doSomething('hello');
// sdk.js
exports.doSomething = function (msg) {
console.log('[orig sdk.doSomething]', msg);
};
运行 node main.js 即可看到 require 日志 + patch 效果。
6.2 前端:通过 alias 替换整个模块
以 Vite 为例(Webpack 思路类似)。
步骤 1:配置 alias
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: {
'some-sdk': '/src/sdk-wrapped.js' // 用你自己的 wrapper 替换原 SDK
}
}
});
步骤 2:在 wrapper 里再 Monkey Patch
// src/sdk-wrapped.js
import * as realSdk from 'some-sdk-original'; // 假设这是原 SDK 包名
const sdk = { ...realSdk };
const origInit = sdk.init;
sdk.init = function (...args) {
console.log('[wrapped sdk.init]', args);
return origInit.apply(this, args);
};
export default sdk;
export * from 'some-sdk-original'; // 其余原样导出
业务代码:
// import sdk from 'some-sdk'; // 实际拿到的是 sdk-wrapped.js
// sdk.init(...);
🟫 七、ES6 class 继承:在可控框架里“正统扩展”
思路:当你能控制实例创建时,直接用 extends 继承原类,覆写方法并按需 super.foo()。
示例:扩展一个 UI 组件类
class Button {
click() {
console.log('[Button] clicked');
}
}
// 原始使用
const btn = new Button();
btn.click();
// 新类:在原 click 前后加逻辑
class LogButton extends Button {
click() {
console.log('[LogButton] before click');
const result = super.click();
console.log('[LogButton] after click');
return result;
}
}
const logBtn = new LogButton();
logBtn.click();
适合:
- 你能自己 new 子类,而不是框架内部帮你 new
- 比如内部业务类、自己封装的组件体系
🟩 八、Proxy + construct:拦截 new 的构造行为
思路:用 Proxy 代理“类本身”,在 construct 里拦截所有实例创建。
示例:给每个实例自动打 Patch
class Service {
foo() {
console.log('[Service.foo]');
}
}
const PatchedService = new Proxy(Service, {
construct(target, args, newTarget) {
console.log('[Proxy construct] 创建 Service 实例,参数:', args);
// 创建原始实例
const instance = Reflect.construct(target, args, newTarget);
// 给实例打补丁
const origFoo = instance.foo;
instance.foo = function (...fooArgs) {
console.log('[patched foo] before', fooArgs);
const result = origFoo.apply(this, fooArgs);
console.log('[patched foo] after', result);
return result;
};
return instance;
}
});
// 之后统一用 PatchedService,而不是 Service
const s = new PatchedService();
s.foo();
适合:
- 第三方库暴露的是“类”,你可以控制
new的地方 - 想让所有实例都应用同一套 Patch
🟦 九、Hook 原型:对所有实例生效
思路:直接改 Class.prototype.xxx 或内置类型原型,例如 Array.prototype.push。
示例 1:改自定义类的原型方法
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log('Hi, I am', this.name);
};
const p1 = new Person('Alice');
p1.sayHi();
// Patch 原型
const origSayHi = Person.prototype.sayHi;
Person.prototype.sayHi = function () {
console.log('[patched] before sayHi');
origSayHi.call(this);
console.log('[patched] after sayHi');
};
const p2 = new Person('Bob');
console.log('--- Patch 之后 ---');
p1.sayHi(); // 旧实例也会受影响
p2.sayHi();
示例 2:给所有数组的 push 打日志(谨慎使用)
const origPush = Array.prototype.push;
Array.prototype.push = function (...items) {
console.log('[Array.push patched] this =', this, 'items =', items);
return origPush.apply(this, items);
};
const arr = [1, 2];
arr.push(3); // 会打印日志
注意:改内置原型影响范围非常大,要在可控环境下使用。
🟣 十、动态注入 <script>:在不能改源码时复写全局函数
思路:在浏览器通过脚本注入的方式覆盖某个全局函数(油猴脚本、Chrome 插件常用)。
示例:复写页面里已有的 window.someFn
假设目标页面有:
window.someFn = function () {
console.log('[page] someFn');
};
你在自己的脚本里注入一段:
(function inject() {
const script = document.createElement('script');
script.innerHTML = `
(function () {
const orig = window.someFn;
window.someFn = function (...args) {
console.log('[injected] before someFn', args);
const result = orig && orig.apply(this, args);
console.log('[injected] after someFn', result);
return result;
};
})();
`;
document.documentElement.appendChild(script);
script.remove();
})();
执行后,页面中所有对 someFn 的调用都会走你注入的版本。
适用:
- 你不能改页面源码,但能注入 JS(油猴脚本 / 浏览器扩展 / WebView 注入等)
🔥 总结:复写函数的“武器清单”
| 方法 | 能力/复杂度 | 典型场景 |
|---|---|---|
| 直接覆写 | 最简单 | 你能直接访问对象和方法名 |
| Monkey Patch | ⭐最常用 | Hook SDK、埋点、调试 |
| Proxy(拦截对象) | ⭐高级黑盒分析 | 不知道具体方法名,想全量 sniff |
defineProperty |
强力 | 只读属性、getter/setter 代理 |
| Hook 全局 API | ⭐非常强 | 网络、事件、路由、日志监控 |
| require Hook / alias | 构建期 | 替换整个 SDK 模块 |
class 继承 |
正统扩展 | 自己可控的类/组件 |
Proxy + construct
|
高级 | 拦截所有实例的创建过程 |
| 原型 Hook | 全局影响 | 对所有实例生效(谨慎) |
| script 注入 | 浏览器插件/油猴场景 | 不能改源码、只能运行时注入 |
--