前端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 05月31日 17:12

Garfish 沙箱隔离如何实现?快照沙箱和 Proxy 沙箱怎么选?

Garfish 沙箱隔离主要解决子应用污染全局环境的问题。微前端里,每个子应用都可能写 window、注册事件、启动定时器、插入样式或改写全局对象;如果没有隔离,A 应用卸载后的副作用会影响 B 应用。Garfish 的思路是运行时拦截全局访问,卸载时清理可追踪副作用,但它不是安全容器,不能替代鉴权、CSP 和代码审查。快照沙箱:成本低,但更像事后恢复快照沙箱会在子应用挂载前记录一份全局环境,运行后对比变化,卸载时恢复。它的好处是理解简单,对旧浏览器更友好,也适合一次只激活一个子应用的老系统改造。边界是它不阻止运行期间的污染;如果多个子应用同时活跃,某个应用写入的全局变量可能已经被另一个应用读到了。Proxy 沙箱:隔离更强,适合多实例Proxy 沙箱给子应用提供代理后的全局对象。写入 window.foo 时,数据优先落在子应用自己的记录里;读取时,再按规则从沙箱或真实 window 中取。它更适合现代浏览器和多个子应用同时存在的场景。function createSandbox(rawWindow) { const fakeWindow = Object.create(null) return new Proxy(fakeWindow, { get(target, key) { return key in target ? target[key] : rawWindow[key] }, set(target, key, value) { target[key] = value return true } })}真实实现还要处理 this 指向、不可配置属性、白名单变量和动态脚本执行上下文。踩坑点是某些第三方库强依赖真实 window,Proxy 下会出现边界行为,需要白名单透传或改为主应用共享依赖。严格隔离还要清理副作用只隔离变量不够。事件监听、定时器、WebSocket、全局弹层和样式标签都可能留下副作用。子应用应在生命周期中主动释放资源,不要完全依赖框架兜底。let timerexport function mount() { timer = window.setInterval(refresh, 5000) window.addEventListener('resize', handleResize)}export function unmount() { clearInterval(timer) window.removeEventListener('resize', handleResize)}还有一个容易忽略的边界是异步任务。子应用卸载后,之前发出的请求可能仍会回调并修改 DOM 或状态,因此要用 AbortController、请求序号或 mounted 标记做保护。否则用户快速切换路由时,会出现旧页面数据覆盖新页面的诡异问题。这类问题不一定被沙箱捕获,因为请求回调属于业务逻辑。框架能帮你隔离全局副作用,但无法判断哪个接口结果已经过期。追问快照沙箱和 Proxy 沙箱怎么选?只运行一个子应用、还要兼容旧环境时,快照沙箱更容易落地。多个子应用同时运行,或团队希望更强隔离时,应优先考虑 Proxy 沙箱。取舍点是兼容性、隔离强度和第三方库适配成本。沙箱能保证子应用安全吗?不能。Garfish 沙箱主要防止应用间副作用污染,不是浏览器级安全边界。恶意代码如果能执行,仍可能发请求、读可访问数据或操作 DOM,所以敏感能力必须靠权限、CSP 和后端校验控制。为什么卸载后还有内存泄漏?因为泄漏常来自业务自己创建的定时器、订阅、闭包引用和未关闭连接。沙箱能记录一部分全局副作用,但无法理解每个业务异步任务的意图。排查时可以反复进入和离开子应用,观察 listener、timer、DOM 节点数量是否持续上涨。第三方库在沙箱里异常怎么办?先确认它是否依赖真实 window、全局单例或不可配置属性。能改配置就让它使用子应用容器,不能改时再考虑白名单透传。不要因为一个库异常就整体关闭沙箱,否则隔离边界会被打穿。
前端阅读 05月31日 17:12

Garfish 主子应用如何通信?状态共享有哪些边界和坑?

Garfish 主子应用通信不要一开始就设计成“大而全的全局 store”。更稳的做法是按数据生命周期拆分:挂载时需要的上下文用 props,跨应用通知用事件总线,长期共享的登录态、主题、权限用平台服务封装。这样子应用仍能独立开发和部署,主应用也不会变成所有业务状态的垃圾桶。props 传稳定上下文props 适合传用户信息读取函数、API 前缀、路由 basename、权限快照和平台服务入口。它简单、可测试,子应用挂载时就能拿到,不需要等待异步事件。边界是 props 不适合高频变化数据,比如未读数、协同编辑状态或实时价格;这些数据用 props 传,会让刷新和同步逻辑很难维护。Garfish.run({ apps: [{ name: 'order', entry: 'https://cdn.example.com/order/', activeWhen: '/order', props: { basename: '/order', services: { auth: { getToken: () => auth.getToken(), logout: () => auth.logout() }, theme: { get: () => theme.current } } } }]})这里传函数比传固定 token 更安全。登录刷新后,子应用再次调用 getToken 能拿到新值,不会继续使用旧凭证。事件总线传“发生了什么”事件适合通知主题切换、登录过期、订单创建成功、侧边栏收起这类动作。它的优点是解耦,一对多传播很方便。踩坑点是事件不是状态存储,晚挂载的子应用可能错过历史事件,所以关键状态仍要能主动读取。export const platformBus = { emitTheme(theme) { Garfish.channel.emit('platform:theme-change', { theme }) }, onTheme(handler) { Garfish.channel.on('platform:theme-change', handler) return () => Garfish.channel.off('platform:theme-change', handler) }}每次订阅都要在卸载时取消,否则重复进入子应用后会出现多次触发,看起来像接口重复请求,实际是监听器泄漏。共享状态要少而稳定真正值得共享的状态通常只有登录、权限、主题、语言、租户和少量平台配置。表格筛选、弹窗开关、详情页临时数据应留在子应用内部。共享越多,子应用越难独立运行,灰度发布和回滚也越麻烦。通信契约最好放在单独包里维护,至少包含事件名、payload 类型和服务方法签名。这样主应用升级时,旧子应用能通过 TypeScript 或测试提前发现不兼容,而不是上线后才暴露。对于跨团队协作,契约比口头约定更可靠。如果担心类型包升级影响发布,可以让契约保持向后兼容:新增字段可以,删除字段要经过灰度期。事件名也不要随业务文案变化而变化,最好用稳定的领域语义。追问props、事件总线和共享服务怎么取舍?props 用来给子应用启动参数,事件总线用来通知变化,共享服务用来读取长期状态。判断标准是数据是否需要被晚挂载应用读取;如果需要,就不能只靠事件。实践里常见组合是 props 传服务入口,事件做变更通知,服务保存当前值。子应用之间能直接通信吗?技术上可以,但不建议作为主链路。A 应用直接依赖 B 应用的事件名,会让两个团队发布节奏绑在一起。更好的边界是由主应用定义平台事件或领域服务,子应用只依赖稳定契约。token 应该作为 props 直接传吗?短期可以,但长期更推荐传 getToken 函数或认证服务。直接传字符串容易在刷新、退出登录、切换账号后变成旧值。踩坑最多的是子应用自己缓存 token,主应用已经退出,它还在继续发请求。如何避免通信导致内存泄漏?订阅事件、WebSocket、轮询都必须在 unmount 或组件卸载钩子里释放。不要用匿名函数订阅后无法 off,也不要在 render 中重复订阅。排查重复触发时,先看监听器数量,再看接口层。
前端阅读 05月31日 17:12

Garfish 样式隔离怎么做?Shadow DOM 和 scoped CSS 如何取舍?

Garfish 的样式隔离不是把 CSS 变成“绝对安全”,而是给每个子应用划出清晰作用域,减少全局 reset、组件库类名、运行时 style 标签互相覆盖。实际落地时,最常见的是 scoped CSS、Shadow DOM 和动态样式清理三类方案。它们解决的问题不同,取舍也不同:scoped CSS 兼容性好,Shadow DOM 隔离更强,动态清理负责避免卸载后的残留污染。scoped CSS 适合多数业务系统scoped CSS 通常通过构建工具给选择器加容器前缀,例如把 .button 变成 [data-garfish-app="order"] .button。它不改变 DOM 结构和事件模型,对 Ant Design、Element Plus 这类组件库比较友好,所以中后台系统优先选它。边界是它只能处理被构建工具改写过的样式,管不住第三方库运行时直接插到 document.head 的全局样式。module.exports = { plugins: { 'postcss-prefix-selector': { prefix: '[data-garfish-app="order"]', transform(prefix, selector) { if (/^(html|body)/.test(selector)) return selector return `${prefix} ${selector}` } } }}Shadow DOM 隔离更强,但更挑场景Shadow DOM 使用浏览器原生边界,外部样式不容易影响子应用,子应用样式也不容易泄漏出去。它适合边界很清楚的独立模块,比如嵌入式配置台、低频运营后台或独立小工具。踩坑点是弹窗、Tooltip、全局 message、主题变量和 E2E 选择器都可能要额外适配;如果组件库大量依赖 document.body 挂载弹层,Shadow DOM 的维护成本会明显上升。const host = document.querySelector('#subapp')const root = host.attachShadow({ mode: 'open' })root.appendChild(document.createElement('div'))卸载清理决定长期稳定性很多线上样式问题不是首次加载发生,而是切换几次子应用后才出现。原因通常是旧应用的 <style>、<link>、弹层 DOM 没有移除。Garfish 可以管理部分资源生命周期,但子应用仍应在 unmount 中清理自己创建的节点和组件库全局实例。const links = []export function mount() { const link = document.createElement('link') link.rel = 'stylesheet' link.href = '/order/index.css' document.head.appendChild(link) links.push(link)}export function unmount() { links.splice(0).forEach(node => node.remove())}追问scoped CSS 和 Shadow DOM 应该怎么选?多数业务系统先选 scoped CSS,因为它改造成本低,和现有组件库、埋点、自动化测试的冲突少。Shadow DOM 更适合子应用边界稳定、弹层少、主题依赖少的场景。取舍点不是谁更先进,而是隔离强度和接入成本能否平衡。为什么弹窗样式经常隔离失败?很多组件库默认把 Modal、Popover、Message 挂到 document.body,它已经脱离了子应用容器。scoped CSS 的前缀匹配不到它,Shadow DOM 内部样式也覆盖不到它。解决时要统一配置弹层容器,例如 getPopupContainer,不要只修某一个组件。主应用主题变量要不要给子应用用?如果主题是平台能力,应该用 CSS 变量透传,例如 --color-primary、--font-size-base。但主应用不要直接覆盖子应用内部类名,否则会形成隐性耦合。边界是共享稳定 token,不共享具体 DOM 结构。样式隔离会影响性能吗?scoped CSS 的主要成本在构建期,运行时影响通常很小。Shadow DOM 也不是主要瓶颈,真正要注意的是重复加载组件库样式和卸载后残留节点。不要为了省一点 CSS 体积放弃隔离,否则线上排查污染更贵。
前端阅读 05月31日 17:12

Garfish 微前端加载慢该怎么优化?

Garfish 性能优化要分两段看:子应用加载前,重点是入口、资源、缓存和预加载;子应用运行后,重点是卸载清理、渲染开销、共享依赖和监控定位。不要一上来就堆 preload,真正有效的做法是先量化首屏、子应用加载耗时、资源体积和错误率,再针对瓶颈处理。微前端的性能问题经常不是 Garfish 本身慢,而是每个子应用都带一份 React、组件库和图表库,最后门户像同时打开了几个完整系统。先把加载链路量出来主应用应该记录从路由命中到子应用 mount 完成的耗时,并区分资源下载、脚本执行和渲染时间。没有数据时,优化很容易变成猜谜:有人说缓存,有人说分包,最后谁都证明不了收益。建议在 beforeLoad、afterLoad、mount 前后打点,并把应用名、版本、网络状态一起上报。边界是埋点不能阻塞加载,失败时要静默降级。const perf = new Map<string, number>();Garfish.run({ beforeLoad(app) { perf.set(app.name, performance.now()); }, afterMount(app) { const cost = performance.now() - (perf.get(app.name) || performance.now()); report('garfish_subapp_mount_cost', { app: app.name, cost }); },});资源体积和缓存怎么控?子应用要做路由级分包,首屏只加载必要 chunk,大型图表、编辑器、地图组件延后加载。静态资源使用带 hash 的文件名,HTML 或 manifest 保持短缓存,JS/CSS 走长缓存,这样既能更新入口,又能复用稳定资源。取舍是缓存越激进,发布回滚越依赖版本管理;如果没有版本化入口,用户可能拿到新 HTML 加旧 JS。常见坑是所有子应用都打包同一套依赖,导致首屏下载重复,应该通过 externals、共享依赖或构建约束处理。// nginx 示例:入口短缓存,hash 资源长缓存location /app/index.html { add_header Cache-Control "no-cache";}location ~* \.(js|css)$ { add_header Cache-Control "public, max-age=31536000, immutable";}预加载不是越多越好Garfish 可以在空闲时间预加载高概率访问的子应用,但预加载会占网络、CPU 和内存。更稳的策略是只预加载当前用户角色最可能进入的 1-2 个应用,并在弱网、低端设备或首屏未稳定时跳过。边界是移动端和海外网络下,盲目预加载可能让当前页面更慢。踩坑最多的是把所有子应用启动时一起 preload,看似后续切换快了,首屏却被拖垮。运行时性能别忽略卸载微前端页面切换频繁,如果子应用卸载不彻底,内存会慢慢涨,定时器和事件监听也会重复执行。React root、Vue app、全局事件、WebSocket、轮询、AbortController 都要在 unmount 里处理。取舍是生命周期代码会稍微啰嗦,但比线上定位“偶发卡顿”省事。边界是有些全局资源本来就该复用,比如登录态和主题,不要为了清理把共享能力也销毁掉。追问Garfish 加载慢时先查什么?先查子应用入口是否慢、资源是否过大、是否重复下载公共依赖,再看脚本执行和渲染耗时。不要一开始就怀疑框架,因为很多慢是构建和资源策略造成的。取舍是网络层优化见效快,但如果 JS 执行太重,缓存再好也解决不了卡顿。边界是本地开发环境的耗时不能代表生产,需要看真实用户监控。预加载应该按什么规则开启?按访问概率、用户角色和设备条件来开。比如用户进入工作台后,大概率访问报表,就可以在浏览器空闲时预加载报表子应用。取舍是提前消耗资源换切换速度,适合高频路径,不适合所有路径。踩坑是忽略弱网和低端设备,导致当前页面交互被预加载拖慢。共享依赖一定能提升性能吗?不一定。共享 React、Vue、组件库可以减少重复下载,但会增加版本协调成本。取舍是稳定基础依赖适合共享,业务库或变化频繁的包不一定适合。边界是多个子应用依赖不同大版本时,强行共享可能引入兼容问题,比重复下载更危险。如何避免切换页面后越来越卡?重点检查 unmount 是否清理事件、定时器、订阅、WebSocket 和未完成请求。可以用 Performance 和 Memory 面板反复切换路由,看 DOM 节点和监听器是否持续增长。取舍是清理逻辑需要统一封装,否则每个子应用各写各的容易漏。常见坑是只卸载组件树,忘了组件外创建的全局副作用。性能预算怎么定才合理?不要拍脑袋定一个绝对值,要按业务场景、用户网络和设备分层。比如后台门户可以容忍首次进入稍慢,但应用内切换应该稳定;高频运营页面则首屏资源要更克制。取舍是预算太严会拖慢开发,太松又没有约束力。建议把入口 HTML、首屏 JS、子应用 mount 耗时和错误率纳入流水线或发布看板。
前端阅读 05月31日 17:12

Garfish、qiankun 和 single-spa 该怎么选?

Garfish、qiankun 和 single-spa 都能做微前端,但它们解决问题的层级不同。single-spa 更像底座,只管应用注册、激活和生命周期;qiankun 在它之上补齐沙箱、资源加载和工程实践;Garfish 也提供开箱能力,同时更强调运行时加载、插件化和相对轻量的接入体验。选型时别只看“谁更先进”,要看团队是否需要高度定制、是否已有历史应用、是否能承担框架生态和运维成本。三者核心差异是什么?如果团队刚开始做微前端,single-spa 的自由度最高,但很多事情要自己补,比如资源加载、样式隔离、全局变量污染处理和错误兜底。qiankun 的优势是成熟,资料多,历史项目验证多,适合需要稳定社区经验的团队。Garfish 的优势在于配置更直接,插件机制和应用调度能力比较顺手,适合希望快速把多个独立应用纳入同一门户的场景。取舍也很明显:生态越成熟,约束和历史包袱越多;框架越轻,团队自己补规范的责任越大。| 维度 | Garfish | qiankun | single-spa ||---|---|---|---|| 定位 | 开箱式微前端框架 | 成熟微前端方案 | 生命周期底座 || 接入成本 | 中低 | 中 | 高 || 自定义空间 | 较高 | 中 | 最高 || 沙箱与隔离 | 内置能力较完整 | 方案成熟 | 需自行实现 || 社区资料 | 中等 | 较多 | 国际化资料多 |Garfish 的优势在哪里?Garfish 对主应用注册、子应用加载、生命周期和沙箱做了较完整封装,配置量比裸用 single-spa 少。对于已有多个 React、Vue 或普通 SPA 的团队,它能让子应用在较少改造下接入。另一个优势是运行时灵活性,入口可以按环境、租户、版本动态切换,这对灰度发布有帮助。边界是框架不能替你解决组织问题,如果应用间契约、样式规范和发布流程没定好,换任何框架都会乱。Garfish.run({ apps: [{ name: 'crm', entry: window.__entries__.crm, activeWhen: '/crm', props: { tenantId: getTenantId() }, }], plugins: [monitorPlugin(), authFallbackPlugin()],});什么时候不适合选 Garfish?如果团队已经深度使用 qiankun,且现有沙箱、构建、监控和发布链路都稳定,迁移到 Garfish 未必划算。迁移收益必须覆盖测试、适配和培训成本。另一个边界是如果你只是想共享几个组件或工具函数,Module Federation 或普通 npm 包可能更合适,没必要上完整微前端。常见踩坑是把微前端当成性能优化手段,结果增加了运行时加载和治理成本。追问Garfish 和 qiankun 最大的选择差异是什么?差异不只是 API,而是生态成熟度和团队治理方式。qiankun 的资料、案例和踩坑经验更多,Garfish 的接入体验和扩展方式更轻。取舍是前者更稳妥,后者在新项目里更容易按自己的平台规则搭起来。边界是如果公司已有 qiankun 基建,除非有明确痛点,否则不要为了换框架而换框架。single-spa 为什么还值得了解?single-spa 是很多微前端方案背后的生命周期模型,理解它能帮助你判断框架到底替你做了什么。直接使用它适合架构团队很强、需要完全定制加载和隔离策略的项目。取舍是自由度换来了工程成本,业务团队可能会被大量底层细节拖住。踩坑点是只接了生命周期,却忘了资源隔离和错误恢复,线上问题会很难定位。Module Federation 能替代 Garfish 吗?它们关注点不同。Module Federation 更适合模块级共享,比如多个应用共用组件、工具库或业务模块;Garfish 更适合应用级集成,比如把多个独立 SPA 放到同一个门户下。取舍是 Federation 对构建体系要求更高,Garfish 的运行时边界更清楚。边界是两者可以组合使用,但要管好依赖版本,否则共享依赖会变成隐形耦合。老项目迁移到 Garfish 应该怎么做?不要一次性全迁,先挑一个边界清晰、依赖少、访问量可控的子应用试点。迁移时先保证独立运行,再接入主应用,最后补监控和灰度。取舍是分阶段会拉长周期,但能把风险压住。常见坑是只改入口和生命周期,不处理全局样式、定时器和跨应用跳转,结果测试环境正常、生产环境互相污染。面试或评审里怎么回答选型问题?先讲业务约束,再讲框架能力,不要直接说某个框架更好。比如团队是否多技术栈、是否要独立发布、是否已有基建、是否要求强隔离。边界是微前端不是默认答案,小团队单体 SPA 可能更简单。能说清这些取舍,比背一张对比表更有说服力。
前端阅读 05月31日 17:12

Garfish 实际项目怎么落地才不容易失控?

Garfish 落地微前端,关键不是把子应用跑起来,而是先把主应用边界、子应用生命周期、发布规则和故障兜底定清楚。主应用只做平台能力:注册应用、路由分发、登录态、主题、全局错误处理和监控埋点;业务逻辑尽量留在子应用里。这样做的取舍是,前期规范会多一点,但后续团队扩张、独立发布、灰度回滚都会轻很多。主应用应该管什么?主应用最好保持“薄壳”角色,不要把订单、用户、报表这类业务判断塞进去。它可以统一提供 userInfo、权限、语言、主题、接口前缀和埋点方法,再通过 props 传给子应用。边界要写清:主应用负责“能不能进入”和“挂在哪里”,子应用负责“进去以后怎么展示”。一个常见坑是主应用为了方便到处暴露全局对象,最后每个子应用都依赖它,独立运行和本地调试会很痛苦。import Garfish from 'garfish';Garfish.run({ basename: '/', apps: [{ name: 'user-center', entry: process.env.USER_CENTER_ENTRY!, activeWhen: '/user', props: { getUser: () => window.__portal_user__, track: (event: string, data?: unknown) => sendLog(event, data), }, }], sandbox: { snapshot: true, fixBaseUrl: true },});子应用生命周期怎么写才稳?子应用要保证 mount 可重复执行,unmount 能把 DOM、事件、定时器、请求订阅清干净。React、Vue、Svelte 都可以接入,但不要假设自己永远运行在完整页面里。实际项目里最容易踩坑的是全局事件和轮询任务:页面切走后还在请求接口,用户再回来就出现重复订阅。建议把清理函数集中放到一个数组里,卸载时统一执行,避免遗漏。const disposers: Array<() => void> = [];export async function mount({ dom, props }) { const onResize = () => props.track('resize'); window.addEventListener('resize', onResize); disposers.push(() => window.removeEventListener('resize', onResize)); root = createRoot(dom.querySelector('#root')!); root.render(<App user={props.getUser()} />);}export async function unmount() { root?.unmount(); disposers.splice(0).forEach(fn => fn());}发布和回滚要提前约定每个子应用独立发布,但入口地址必须可控,最好通过环境配置或版本化 manifest 管理。直接把生产 entry 写死在主应用里,看起来简单,回滚时却要重新发主应用。更稳的方式是主应用读取配置中心,子应用发布后只更新版本指针。边界是配置中心也要有缓存和降级,否则它挂了会导致所有子应用加载失败。发布前还要约定资产命名、CDN 缓存、灰度比例和回滚触发条件。比如新版本错误率超过阈值时,先切回旧 entry,再排查子应用代码,而不是让主应用临时加兼容逻辑。追问主应用为什么不能承载太多业务逻辑?因为微前端的收益来自团队和发布边界,而不是页面拆分本身。主应用一旦包含大量业务判断,子应用每次变更都要等主应用配合,独立发布就失效了。取舍是主应用要多做一些平台抽象,但这比后期拆耦成本低得多。踩坑点是“临时加一个判断”很容易变成长期依赖,所以要把例外也纳入评审。子应用是否必须能独立运行?最好能独立运行,至少本地开发和基础页面渲染不应依赖主应用。独立运行会增加一层 mock props 或本地启动配置,但可以显著降低联调成本。边界是登录、权限、埋点这类平台能力可以用模拟实现,不必在本地完整复刻。常见坑是子应用直接读取主应用全局变量,离开主应用就白屏。团队之间怎么约定通信方式?优先用 props 传稳定能力,用事件总线传一次性通知,少用共享可变状态。共享状态看起来方便,但边界不清时会让数据来源变得混乱。取舍是事件会让调用链不如直接函数清晰,所以事件名、payload 类型和取消订阅规则要写进规范。踩坑最多的是忘记 off,导致重复弹窗、重复请求或内存泄漏。样式隔离应该一开始就上 Shadow DOM 吗?不一定。后台类系统用 CSS Modules、BEM 前缀或 scoped CSS 往往够用,Shadow DOM 更适合冲突严重、第三方样式复杂的场景。它的边界是弹窗、下拉、主题变量和全局字体可能需要额外适配。取舍是隔离越强,跨应用统一视觉和调试成本也越高。接入 Garfish 后如何做质量验收?验收不只看页面能否打开,还要测路由切换、重复进入、卸载清理、异常加载和灰度回滚。建议 E2E 覆盖跨应用主流程,单测覆盖生命周期函数。边界是不要把所有子应用组合都测一遍,那会拖慢流水线;更实际的是主链路必测、风险应用加测。常见坑是只测首次加载,忽略第二次进入才暴露的重复订阅问题。
前端阅读 05月31日 16:17

MobX 的核心概念是什么?它是怎么自动更新视图的?

MobX 解决的是一个很具体的问题:状态变了以后,哪些地方应该跟着变,不需要你手动列清单。它把应用里的数据看成一张依赖图,组件、计算值和副作用只要在运行时读过某个状态,就会被记录为这个状态的消费者。之后状态被修改,MobX 沿着这张图通知真正受影响的部分,所以它看起来像“自动更新”,实际靠的是运行时依赖追踪。MobX 的几个核心概念observable 是可观察状态,通常是对象属性、数组或 Map。它的关键不是“存数据”,而是让读写行为能被 MobX 捕获。computed 是从状态派生出来的值,比如 fullName、过滤后的列表、购物车总价。它默认惰性计算,只有被读取时才执行,并且依赖没变时直接复用缓存。这里的取舍很明显:computed 适合纯计算,不适合发请求、写日志这类副作用。action 是修改状态的边界。MobX 6 推荐把状态修改放在 action 里,因为 action 会批量提交变更,避免中间状态触发多次 reaction。团队项目里最好开启 enforceActions: "always",否则代码越写越散,很难追踪是谁改了状态。reaction / autorun / when 负责处理副作用。autorun 会立即执行并自动追踪用到的状态,reaction 可以精确指定观察的数据,when 在条件满足后执行一次就销毁。import { makeAutoObservable, configure } from 'mobx'configure({ enforceActions: 'always' })class TodoStore { todos: { id: number; text: string; done: boolean }[] = [] filter: 'all' | 'done' | 'active' = 'all' constructor() { makeAutoObservable(this) } get visibleTodos() { if (this.filter === 'done') return this.todos.filter(t => t.done) if (this.filter === 'active') return this.todos.filter(t => !t.done) return this.todos } addTodo(text: string) { this.todos.push({ id: Date.now(), text, done: false }) }}它为什么能自动更新视图在 React 里,observer 会包住组件渲染过程。组件渲染时读取了 store.visibleTodos,MobX 就知道这个组件依赖了对应的 computed;computed 又依赖 todos 和 filter。当 addTodo 或 filter 变化时,依赖链被标记为过期,组件才重新渲染。import { observer } from 'mobx-react-lite'export const TodoList = observer(({ store }: { store: TodoStore }) => ( <ul>{store.visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>))边界也要清楚:MobX 只能追踪“运行时实际读取”的 observable。如果你提前把值解构成普通变量,再在组件外传来传去,依赖关系可能丢失。异步代码里也要注意,await 之后再修改状态,仍然需要在 action 或 runInAction 里完成。项目里怎么落地实际项目不建议把所有状态塞进一个全局 store。更稳的做法是按业务边界拆分,比如用户、权限、编辑器、购物车各自维护自己的状态,再在页面层组合使用。这样做的好处是更新范围小,测试也更容易写。边界是跨模块共享的数据不要随意互相 import,否则 store 之间会形成隐式依赖,后面重构很难拆。调试时可以配合 spy 或 MobX DevTools 观察 action 和 reaction,但不要把调试工具当成架构。真正能降低维护成本的,还是明确哪些方法能改状态、哪些 getter 只能派生数据。追问MobX 和 Redux 应该怎么选?MobX 更适合对象关系复杂、局部更新频繁的业务,比如表单编辑器、低代码画布、后台配置台。Redux 的优势是约束强,状态变更路径清楚,适合需要审计、回放和严格团队规范的项目。取舍点不在谁更先进,而在团队是否愿意用约束换可预测性。小团队快速迭代时 MobX 很舒服,但多人长期维护时要补上 action 规范和目录约定。computed 和普通函数有什么区别?普通函数每次调用都会重新执行,computed 会根据依赖做缓存。只有依赖变化并且有人读取它时,computed 才重新计算,这对列表过滤、聚合统计很有用。边界是 computed 必须保持纯净,不要在里面改状态或发请求。踩坑点是 computed 没有消费者时不会主动运行,所以不要指望它替你触发业务流程。autorun 和 reaction 有什么区别?autorun 适合“用到什么就追踪什么”的简单副作用,比如调试日志。reaction 更适合生产代码,因为它把数据选择和副作用分开,只在选择结果变化时触发。取舍是 autorun 写起来快,但依赖容易变得隐式;reaction 啰嗦一点,却更可控。项目里如果副作用会发请求或写本地缓存,优先用 reaction。使用 MobX 最容易踩什么坑?最常见的是把 observable 过早解构,导致 observer 组件没有在渲染阶段读取状态。另一个坑是异步请求回来后直接赋值,在严格 action 模式下会报错。边界处理方式是:组件里读 store,异步结果用 runInAction 合并回状态。还有一点,reaction 创建后要保留 disposer,在组件卸载或模块销毁时释放,否则会有隐性内存泄漏。MobX 适合所有状态吗?不适合。服务端缓存、分页请求、重试状态这类数据,用 TanStack Query 一类工具通常更合适。MobX 更适合客户端本地状态,尤其是用户正在编辑、拖拽、筛选、组合的状态。取舍上可以把远端数据交给请求缓存库,把前端交互状态交给 MobX,两者不要硬塞进同一个 store。
前端阅读 05月31日 15:55

MobX 和 Redux 到底该怎么选?适合哪些场景?

MobX 和 Redux 的区别不只是 API 写法不同,而是状态管理哲学不同。Redux 强调显式数据流:组件 dispatch action,reducer 生成新 state,状态变化可以被记录和回放。MobX 强调响应式模型:你修改 observable,系统自动知道哪些 computed、reaction 或 observer 组件需要更新。如果用一句话选型:需要强约束、审计和统一协作时偏 Redux;需要快速建模复杂业务对象、减少样板代码时偏 MobX。现在 Redux Toolkit 已经大幅减少模板代码,所以不能再简单说“Redux 一定啰嗦”。但 MobX 在深层对象、表单状态和局部复杂交互里仍然很顺手。// Redux Toolkitconst slice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { inc: state => { state.value += 1; } }});// MobXclass CounterStore { value = 0; constructor() { makeAutoObservable(this); } inc() { this.value += 1; }}Redux Toolkit 里看起来也能“直接改 state”,但那是 Immer 帮你生成不可变结果。MobX 的直接修改则是它本身的响应式模型,依赖追踪发生在读取和写入之间。两者都能写得很现代,真正影响选择的是团队调试方式、业务复杂度和长期维护成本。还有一个现实因素是招聘和交接成本。Redux 的资料、范式和候选人经验更多,新人即使没接触过项目,也容易顺着 action、slice、selector 找到入口。MobX 项目如果 store 设计得好,上手同样很快;如果设计得随意,新人需要先理解一套隐式依赖网络。选型时把团队未来一年的人数变化也算进去,往往比单纯比较代码量更实际。追问Redux 的优势现在还明显吗?明显,尤其是在多人协作和复杂状态审计场景里。Redux 的 action 日志、DevTools、时间旅行调试仍然很强,线上问题复盘时能看到状态如何一步步变化。取舍是你要接受更明确的流程和更多约束,哪怕 Redux Toolkit 已经减少了不少样板。金融、交易、权限流转这类系统,显式数据流带来的可追溯性通常比少写几行代码更重要。MobX 更适合哪些业务?MobX 适合状态结构像业务对象一样自然变化的场景,比如复杂表单、编辑器、看板、低代码配置器和局部交互很多的后台页面。它允许你用 class 表达领域模型,用 computed 表达派生值,用 observer 自动连接 UI。边界是自由度越高,团队规范越重要。若大家随手在任意位置改 observable,又不给 action 命名,后期排查会比 Redux 更痛苦。性能上 MobX 一定比 Redux 更好吗?不一定,但 MobX 的默认更新粒度通常更细。它追踪组件实际读取的 observable 字段,所以某个字段变化只影响真正用到它的组件。Redux 依赖 selector 和引用比较,写得好同样很快,写得差则容易因为新对象、新数组导致重复渲染。取舍在于 MobX 把优化自动化,Redux 把优化显式化;前者省心,后者更可控。TypeScript 项目选哪一个更舒服?MobX 的 class 模型和 TypeScript 搭配很自然,字段、方法、getter 的类型就是业务模型本身。Redux Toolkit 的类型体验也已经比旧 Redux 好很多,createSlice 能推断 action 和 state,但异步 thunk、RootState、Dispatch 仍然需要一些模板。取舍是 MobX 写业务模型更顺,Redux 写团队规范更统一。大型团队里,类型舒服不一定是唯一目标,统一的数据流和工具链也很值钱。能不能在一个项目里同时用 MobX 和 Redux?可以,但要非常克制。比如全局登录态、权限、审计相关状态放 Redux,某个复杂编辑器内部用 MobX 管局部模型,这是有边界的混用。踩坑点是没有划清职责,导致同一份数据在两个 store 里各存一份,最终同步逻辑比状态管理本身还复杂。除非收益明确,否则更建议选一个主方案,再用局部 React state 或轻量库补足边角。如果项目并不复杂,却又觉得 Redux 和 MobX 都偏重,也可以把 Zustand、Jotai、Valtio 这类轻量方案纳入比较。Zustand API 简单,适合轻量全局状态;Jotai 更偏原子化组合;Valtio 则接近可变对象代理的体验。这里的取舍是生态、团队熟悉度和调试能力,不要只看示例代码短不短。状态管理选型最怕为了“新”而换,最后业务复杂度没降,团队学习成本反而升了。落地时可以先画出状态的生命周期:哪些状态跨页面共享,哪些只服务某个复杂组件,哪些需要被审计或回放。跨团队、跨流程的状态更适合 Redux 这种强约束方案;局部领域模型、频繁编辑和深层对象更适合 MobX。这个判断比“哪个库更流行”靠谱,因为状态管理的问题通常不是 API 不够漂亮,而是边界没有定义清楚。所以 MobX 和 Redux 没有绝对胜负。Redux 像一套清晰的交通规则,MobX 像更灵活的自动导航;项目越重协作和审计,越需要规则,项目越重局部复杂交互,越能体现 MobX 的效率。
前端阅读 05月31日 15:55

MobX 依赖追踪到底是怎么知道该更新谁的?

MobX 依赖追踪的核心可以用一句话概括:谁在运行时读了 observable,谁就会被登记为它的依赖;以后这个 observable 变了,只通知登记过的人。这里的“谁”可能是 autorun、reaction、computed,也可能是被 observer 包装的 React 组件。MobX 不靠你手写依赖数组,而是靠运行时读取行为建立依赖图。一个最小例子如下。autorun 第一次执行时会读取 store.count,MobX 会把当前 reaction 和 count 这个可观察字段连起来。之后 count 改变,autorun 会重新执行;但如果改的是 name,这个 reaction 不会受影响。import { autorun, makeAutoObservable } from "mobx";class Store { count = 0; name = "MobX"; constructor() { makeAutoObservable(this); }}const store = new Store();const dispose = autorun(() => { console.log(store.count);});store.count++; // 触发 autorunstore.name = "Redux"; // 不触发上面的 autorundispose();从内部看,observable 字段像一个最小的发布者,reaction 像订阅者。运行 reaction 时,MobX 会设置一个“当前正在追踪的上下文”,字段 getter 被访问后就把这个上下文记录下来。执行结束后,旧依赖会被对比和清理,所以条件分支切换时,MobX 不会永远订阅已经不再读取的字段。依赖追踪还有一个经常被低估的边界:MobX 只追踪同步执行期间的读取。你在 reaction 里读到的 observable 会被记录,但 setTimeout、Promise 回调或事件监听器里后来才读到的字段,不会自动算进同一次 reaction 的依赖。这个规则解释了很多“明明在函数里用了状态,为什么没更新”的问题。数组、Map 和对象属性也有类似细节。读取 todos.length 追踪的是长度变化,读取 todos[0].title 追踪的是第一个元素及其 title,遍历整个数组则会建立更宽的依赖。依赖越宽,更新越容易触发;依赖越窄,性能越好但也更依赖你把读取位置写对。真实项目里,列表筛选建议放 computed,组件只读取最终结果,这样依赖和缓存都更清楚。最后要记住,自动追踪不是自动设计架构。MobX 能帮你找到“谁读了谁”,但不能替你决定 store 怎么拆、异步流程怎么收口、哪些状态应该全局共享。依赖图一旦跨模块乱连,短期很方便,长期会变成难以拆解的网。追问MobX 和 React 的依赖数组有什么区别?React 的 useEffect 依赖数组是你手动声明的,写漏了会闭包过期,写多了又可能重复执行。MobX 的依赖来自运行时读取,读了什么就追踪什么,不需要人工维护列表。取舍是 MobX 更省心,但依赖关系不总是一眼能从代码声明处看出来。团队里如果大量使用隐式读取,就要配合 trace、命名 reaction 和清晰的 store 边界。computed 为什么能缓存,什么时候会重新算?computed 本质上是一个有缓存的派生值,它会记住自己上次计算时读取了哪些 observable。只要这些依赖没有变化,再次读取 computed 会直接返回缓存结果。边界是 computed 必须是纯计算,不能在里面发请求、改状态或写日志埋点这类副作用。踩坑最多的是把带参数的筛选逻辑硬塞进 computed,参数变化不属于 observable 时,缓存行为就不符合预期。条件分支里的依赖会不会追踪错?不会静态追踪所有分支,只追踪当前这次运行实际访问到的字段。比如 showAge ? user.age : user.name,当 showAge 为 false 时,age 不会成为当前依赖。取舍是这让更新更精准,但也要求你理解“依赖是动态变化的”。如果条件切换后旧依赖没有清理,通常说明 reaction 没有重新执行,或者读取发生在了追踪上下文之外。为什么不建议在 reaction 里做太多事情?reaction 适合连接“状态变化”和“副作用”,但不适合承载大段业务流程。它会随依赖变化自动执行,逻辑过重时很容易出现重复请求、状态互相触发甚至循环更新。项目里的边界可以这样划:状态修改放 action,派生值放 computed,真正需要同步到外部系统时才用 reaction。踩坑点是忘记保存 dispose,页面卸载后 reaction 还活着,就会造成内存泄漏或幽灵请求。如何调试一个组件为什么重渲染?可以先在组件或 computed 中调用 trace(),观察 MobX 认为它依赖了哪些 observable。再看这些 observable 是不是在不该修改的时候被 action 改了,尤其是表单初始化、接口回填和路由切换。这个排查有个取舍:MobX 的自动追踪减少了手写优化,但问题出现时要从“读取链路”而不是“dispatch 链路”入手。实战中给 store action 起清楚的名字,比事后猜哪个字段触发更新要省很多时间。autorun(() => { trace(); console.log(store.visibleUserName);});MobX 的依赖追踪并不神秘,它只是把“读”和“写”都接管了。理解这一点后,很多问题都能归结为两个检查:状态有没有被观察,读取有没有发生在 reaction 执行期间。
前端阅读 05月31日 15:55

MobX 的 toJS、toJSON 和 observable.shallow 到底该怎么选?

MobX 里经常让人混淆的不是 observable 本身,而是数据离开 MobX 时该怎么处理。toJS、toJSON 和 observable.shallow 都和“普通对象”有关,但位置不同:toJS 负责把响应式数据深度转成普通对象,toJSON 负责控制序列化输出,observable.shallow 则是在创建 observable 时只追踪顶层。## 先记住一句话把 MobX 数据交给 API、缓存或第三方库时,用 toJS;控制 JSON.stringify 输出时,写自定义 toJSON;数据量很大且整批替换时,才考虑 observable.shallow。三者不是替代关系。import { makeAutoObservable, observable, toJS } from 'mobx';class UserStore { profile = { name: 'Alice', meta: { role: 'admin' } }; rows = observable.shallow.array(); constructor() { makeAutoObservable(this, { rows: observable.shallow }); } save() { return api.updateUser(toJS(this.profile)); } toJSON() { return { profile: toJS(this.profile) }; }}三者的边界在哪里toJS 会递归剥离 observable,新对象不再被 MobX 追踪。它适合提交表单、生成快照、写 localStorage。不要在 render 或 computed 里反复调用。toJSON 不是 MobX 的深转换工具,更多时候是 JavaScript 的序列化钩子。JSON.stringify(obj) 会自动调用对象自己的 toJSON。它适合做字段白名单、脱敏和输出格式控制。observable.shallow 不负责导出数据,它只改变 observable 的创建策略。默认 deep 会追踪嵌套对象,shallow 只看顶层引用。大型表格、分页结果适合它;可编辑表单通常不适合。追问为什么不建议在 React render 里直接 toJS?因为 toJS 是深拷贝,不是只读一下引用。组件每次响应式更新都会生成新对象,子组件的浅比较、memo 和虚拟列表优化都会被破坏。它的取舍是换来干净对象,但放弃引用稳定性和部分性能。边界处用一次很合理,渲染路径里反复用就是踩坑。JSON.stringify observable 时还需要先 toJS 吗?多数普通场景不需要,MobX observable 通常可以被 JSON.stringify 序列化。真正需要 toJS 的情况,是你要先加工数据、交给只接受 plain object 的库,或明确断开响应式引用。另一个边界是敏感字段,这时自定义 toJSON 更合适。坑在于以为 toJSON 会自动深度剥离所有 observable,实际它只按你的返回值工作。observable.shallow 为什么会导致 UI 不更新?shallow 只追踪顶层引用,store.config.theme = 'dark' 没换掉 config 引用,所以 MobX 不会通知观察者。正确写法是替换整个顶层对象:store.config = { ...store.config, theme: 'dark' }。它的好处是减少深层追踪开销,代价是必须使用不可变更新风格。团队里如果有人习惯直接改嵌套字段,就很容易出现数据变了但页面没动。store.config.theme = 'light';store.config = { ...store.config, theme: 'light' };大列表一定要用 observable.shallow 吗?不一定,要看列表里的元素会不会被单独编辑。如果列表只是接口返回后展示、筛选、整页替换,shallow 很合适。如果每一行都有选中、编辑、校验状态,deep 或拆成行级 store 更稳。边界判断很简单:业务是否关心元素内部字段变化触发 UI。toJS 得到的数据能再改回 store 吗?可以,但要把它当快照,不要当双向绑定对象。toJS(store.user) 改出来的 plain object 不会影响原 store,想写回必须在 action 里显式赋值。这个边界在表单草稿里很有用:先复制一份编辑,保存时再提交。踩坑点是把快照传来传去后误以为还在响应式系统内,结果修改没有任何观察者收到通知。
前端阅读 05月31日 15:55

MobX 应用应该怎么测试 Store 和组件?

测试 MobX 应用时,重点不是验证“observable 是否真的能响应”,而是验证业务状态在 action、computed、reaction 和组件渲染之间有没有按预期流动。优先测试 Store,因为大部分规则都在那里;组件测试只覆盖用户能看到和能操作的行为。API 边界可以用 mock 或 MSW 隔离,reaction 这类副作用要记得释放 disposer。不要把每个 observable 字段都测一遍,那会让测试和实现细节绑死,重构一次就碎一片。一个比较稳的分层是:Store 单元测试覆盖状态变化和异常分支,组件测试覆盖点击、输入、展示,少量集成测试覆盖多个 store 的协作。MobX 的响应式机制本身已经由框架保证,业务测试没必要重复证明它存在。真正容易出问题的是异步 action 的 loading 恢复、错误消息、reaction 泄漏、组件没被 observer 包裹,以及 mock 数据和真实接口结构不一致。class UserStore { user = null; loading = false; error = ''; constructor(private api) { makeAutoObservable(this); } get isLoggedIn() { return Boolean(this.user); } async login(payload) { this.loading = true; try { this.user = await this.api.login(payload); } catch (e) { this.error = e.message; throw e; } finally { this.loading = false; } }}追问Store 测试应该测状态字段还是业务行为?优先测业务行为,也就是调用 action 之后,对外可观察的状态和 computed 结果是否正确。直接断言每个内部字段会让测试过度依赖实现,比如后来把两个字段合并成一个对象,功能没坏但测试全红。边界是错误状态、loading、权限标记这类会直接影响 UI 的字段,仍然应该明确断言。写法上可以把 store 当成普通类实例,不需要为了测试专门启动 React。取舍是测试会少覆盖一些内部细节,但更能支持后续重构。MobX 的异步 action 有哪些测试坑?最大的坑是没有等待异步结束就断言,导致测试偶发通过或失败。await store.login() 后再检查 user、loading、error,比用固定 setTimeout 稳得多。另一个坑是错误分支只测抛错,不测状态恢复,结果线上 loading 一直转。异步 action 里如果用了 runInAction,测试仍然关注最终行为,不必断言 MobX 的内部调度顺序。边界是包含防抖、轮询或取消请求的 action,需要同时控制 timer 和请求 mock,否则失败会很随机。组件测试里要不要 mock 整个 Store?如果目标是验证组件交互,mock 一个最小 Store 很有用,速度快,也能把失败范围限制在组件。若要验证多个 store 协作、路由跳转或真实 reaction,就应该使用接近真实的 store 实例,甚至配合 MSW mock 网络。过度 mock 的问题是组件看起来测过了,但和真实 store 的 observable 更新方式不一致。一个实用取舍是:单组件用轻量 mock,关键业务流用真实 store 加 mock API。还要注意组件必须在测试里保持和生产一致的 Provider 结构,否则通过的测试可能只是绕开了依赖注入问题。reaction、autorun 这类副作用怎么测?先触发会改变依赖的 action,再断言副作用是否发生,例如调用埋点、写 localStorage 或更新另一个 store。测试结束时一定要执行 disposer,否则后续用例可能被上一个 reaction 影响。这里的边界是不要测试 MobX 会不会追踪依赖,应该测试你注册的副作用是否符合业务预期。若副作用包含防抖或定时器,需要配合 fake timers,并在用例后恢复真实 timer。踩坑点是 reaction 默认可能立即执行,也可能等依赖变化才执行,测试前要明确当前配置。spy、trace、isObservable 适合放进断言吗?spy 和 trace 更适合排查响应链路,不建议长期作为核心断言,因为它们会让测试贴着 MobX 实现细节跑。isObservable 可以在库代码或 store 工厂里做少量保护,但业务测试里通常没必要。真正有价值的断言是用户状态、组件输出和副作用结果。踩坑点是为了提高“测试覆盖率”去检查 observable 类型,最后覆盖率上去了,坏业务却没被测到。取舍上,调试工具可以临时打开,但最终测试最好回到业务可见结果。写段测试it('logs in and resets loading', async () => { const api = { login: jest.fn().mockResolvedValue({ name: 'Ada' }) }; const store = new UserStore(api); await store.login({ username: 'ada' }); expect(api.login).toHaveBeenCalledWith({ username: 'ada' }); expect(store.user.name).toBe('Ada'); expect(store.isLoggedIn).toBe(true); expect(store.loading).toBe(false);});MobX 测试的主线很简单:Store 测规则,组件测行为,reaction 测副作用,API 边界用可控替身。只要少测实现细节,多测状态变化带来的可见结果,测试既能挡住回归,也不会在重构时变成负担。遇到测试不稳定时,先检查异步等待、disposer、timer 和 mock 清理,通常比怀疑 MobX 本身更接近答案。
前端阅读 05月31日 11:08

什么是 Tauri 框架?它的核心架构如何工作?

Tauri 是一个用 Web 前端加 Rust 后端构建跨平台桌面应用的框架。它让你继续使用 React、Vue、Svelte 或普通 HTML 写界面,同时把文件系统、窗口、菜单、通知、自动更新等系统能力放到 Rust 和插件侧处理。和 Electron 最大的不同是,Tauri 不随应用打包完整 Chromium,而是使用操作系统自带 WebView 渲染界面。Tauri 的三层架构第一层是前端层。它就是一个 Web 应用,可以用 Vite、React、Vue、Svelte,也可以不用框架。前端负责 UI、交互和状态管理,但默认不能随意访问系统资源。第二层是 Rust 核心层。这里放业务命令、系统调用、插件接入和性能敏感逻辑。前端通过 IPC 调用 Rust 命令,Rust 再决定是否读取文件、访问数据库或执行系统操作。第三层是 WebView 层。macOS 使用 WKWebView,Windows 使用 WebView2,Linux 通常依赖 WebKitGTK。WebView 负责把前端页面显示出来,Tauri 则负责把它和桌面窗口、权限系统、打包流程连接起来。一个最小项目长什么样创建项目可以直接用官方脚手架:npm create tauri-app@latestcd tauri-demonpm installnpm run tauri dev典型目录会包含前端源码和 src-tauri:src/ App.tsxsrc-tauri/ src/main.rs tauri.conf.json Cargo.toml前端调用 Rust 的方式很直接:import { invoke } from '@tauri-apps/api/core';const text = await invoke<string>('greet', { name: 'Tauri' });Rust 端定义命令并注册:#[tauri::command]fn greet(name: String) -> String { format!("Hello, {name}")}fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("failed to run tauri app");}为什么它的包体更小Electron 通常把 Chromium 和 Node.js 一起带上,所以体积和内存占用较高。Tauri 借用系统 WebView,只打包应用代码、Rust 二进制和必要资源,因此简单应用可以非常小。边界是系统 WebView 的行为不完全一致,复杂前端能力要在目标平台上测试。权限和安全是架构的一部分Tauri 默认遵循最小权限原则。前端想用文件系统、Shell、剪贴板、Dialog 等能力,需要配置权限或 capability。这个设计让桌面应用不必把所有系统能力暴露给页面,也能限制某个窗口只能做它该做的事。{ "permissions": ["core:default", "dialog:default"]}真正的安全边界还包括 CSP、自定义命令校验和插件审计。不要因为使用 Tauri 就默认安全,Rust 命令如果直接相信前端参数,同样可能出问题。配置文件决定开发和生产怎么衔接tauri.conf.json 不是简单的项目说明文件,它决定开发服务器、生产资源、窗口、安全策略和打包信息。开发阶段常见配置是让 Tauri 先启动 Vite,再加载 devUrl;生产阶段则加载 frontendDist 指向的静态文件。两者路径不一致时,开发正常、打包白屏是最典型的症状。{ "build": { "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build", "devUrl": "http://localhost:1420", "frontendDist": "../dist" }}窗口配置也要尽早确定,比如初始大小、最小尺寸、是否可调整、是否隐藏标题栏。桌面应用不像网页,窗口体验会直接影响用户对“原生感”的判断。不要等功能写完才处理这些细节,否则前端布局可能要跟着返工。插件让架构更像能力拼装Tauri 的很多系统能力通过插件提供,例如 dialog、fs、shell、updater、notification。插件的好处是不用自己写所有平台代码,但每个插件都要配权限。更稳的理解方式是:前端提出意图,插件和 Rust 负责执行,capability 负责划线。这样项目会比“所有能力都挂在 window 上”更清楚,也更容易做安全审计。追问Tauri 是不是 Electron 的轻量替代品?可以这么理解,但不完全准确。Tauri 解决了 Electron 包体大、权限面宽的一些痛点,但也带来 Rust、系统 WebView 和平台依赖的成本。边界在于:如果你的应用强依赖完整 Chromium 或 Node 生态,Electron 仍然可能更合适。Tauri 更像另一种架构选择,而不是无脑升级版。前端框架在 Tauri 里有什么限制?大多数前端框架都能用,限制主要来自桌面环境和静态构建。比如前端路由最好考虑 hash 模式,打包后不能假设有开发服务器,浏览器 API 也要看 WebView 支持情况。踩坑点是把 Web 项目原样搬进来,结果生产环境资源路径、刷新路由或远程脚本加载出问题。先让项目稳定 npm run build,再接入 Tauri 会顺很多。Rust 后端一定要写很多代码吗?不一定。简单应用可能只需要几个命令,比如读配置、保存文件、打开系统对话框。复杂应用才会把数据库、文件索引、加密、压缩等逻辑放到 Rust。取舍是:写得越少,上手越快;写得越多,性能和系统能力越强,但团队维护成本也越高。Tauri 适合哪些应用?它适合本地工具、开发者工具、轻量客户端、文件处理工具和对包体敏感的桌面应用。它也适合前端界面不复杂,但需要可靠系统集成的产品。边界是重度浏览器应用、复杂在线协作工具或依赖大量 Node 原生模块的项目,未必能省事。选型时要看核心功能,不要只看首页示例。学 Tauri 应该先看哪部分?先理解项目结构、devUrl/frontendDist、invoke 命令和权限配置。然后再看窗口、菜单、插件、自动更新和打包签名。常见坑是还没搞清楚 IPC 和权限,就开始堆前端页面,后面调系统能力时会频繁返工。把最小闭环跑通:一个按钮调用 Rust、一个权限受控的系统能力、一次生产构建,这比先读完整文档更有效。
前端阅读 05月31日 11:08

Tauri 和 Electron 有什么区别?该怎么选?

Tauri 和 Electron 都能用 Web 技术做桌面应用,但它们的默认假设完全不同。Electron 把 Chromium 和 Node.js 一起打进应用,换来一致的浏览器能力和成熟生态;Tauri 使用系统 WebView 加 Rust 后端,换来更小体积、更低权限暴露和更接近原生的系统边界。选哪个,不是简单看“谁更先进”,而是看团队、产品和运行环境能接受哪些取舍。架构差异Electron 的应用通常包含 Chromium、Node.js、主进程和渲染进程。好处是跨平台表现更一致,坏处是每个应用都带一套浏览器运行时。Tauri 不打包完整浏览器,而是使用系统 WebView:macOS 是 WKWebView,Windows 是 WebView2,Linux 常见是 WebKitGTK。这带来一个直接差异:Electron 更像“自己带厨房”,Tauri 更像“用系统厨房”。Electron 可控性更强,Tauri 包体更小,但也要接受不同系统 WebView 的细节差异。体积、内存和启动速度很多项目关注 Tauri,是因为安装包明显更小。Electron 应用常见体积在 100MB 以上,Tauri 简单应用可以做到几 MB 到十几 MB。内存方面,Tauri 通常也更省,因为不需要为每个应用携带完整 Chromium。不过这不是绝对结论。如果你的 Tauri 应用前端本身很重、加载大量资源、启动时做复杂初始化,它同样会慢。性能优化不能只靠框架名,资源拆分、懒加载、Rust 任务调度和前端渲染策略都要一起看。开发体验和生态Electron 最大优势是成熟。调试、自动更新、托盘、菜单、协议注册、崩溃收集、第三方库,基本都有大量案例。团队如果全是前端和 Node.js 背景,Electron 的上手成本低很多。Tauri 的 Rust 后端更适合需要系统能力、性能敏感或安全边界清晰的项目。代价是团队要能维护 Rust 代码,CI 环境也要处理 Rust toolchain、系统依赖和签名打包。这个成本在小工具里可能很低,在大型商业软件里需要提前评估。安全模型不同Electron 可以做得很安全,但需要主动关闭 Node 集成、启用 contextIsolation、设计 preload 边界。Tauri 默认更收敛,前端必须通过权限和命令访问系统能力。默认安全不等于绝对安全,自定义 Rust 命令写得太宽,一样会暴露风险。一个最小 Tauri 调用示例import { invoke } from '@tauri-apps/api/core';const version = await invoke<string>('app_version');#[tauri::command]fn app_version() -> String { env!("CARGO_PKG_VERSION").to_string()}Electron 里同类能力通常通过主进程和 preload 暴露:contextBridge.exposeInMainWorld('app', { version: () => ipcRenderer.invoke('app-version')});打包和发布链路也不同Electron 的打包生态非常成熟,electron-builder、electron-forge、自动更新方案和大量 CI 示例都能直接参考。Tauri 的打包链路更轻,但你需要处理 Rust target、系统依赖、平台签名和 WebView 运行时要求。Windows 上要考虑 WebView2 runtime,Linux 上要考虑 WebKitGTK 依赖,macOS 上要处理签名、公证和权限提示。团队如果没有桌面发布经验,这些工作不比写业务代码轻。npm run tauri build这条命令能生成安装包,但离稳定发布还有距离。你还要确认图标、版本号、更新签名、崩溃日志、安装路径和回滚策略。Electron 因为案例多,遇到问题更容易搜索到答案;Tauri 的问题通常更贴近 Rust 或系统环境,排查时需要看 Cargo 输出和平台文档。迁移时不要只迁 UI从 Electron 迁到 Tauri,最容易低估的是主进程逻辑。Electron 里很多能力来自 Node:文件遍历、子进程、原生模块、托盘菜单、系统代理、协议处理。迁移到 Tauri 后,这些要么换插件,要么改写 Rust 命令。边界是前端页面迁移可能很快,但系统能力迁移才是真成本。最好先列出 Electron 主进程和 preload 暴露的 API,再逐个判断是否有 Tauri 插件或需要自研。追问只看包体积就应该选 Tauri 吗?不应该。包体积是 Tauri 的强项,但不是唯一指标。若你的产品依赖 Chromium 特性、复杂 DevTools 能力或大量 Node 原生模块,Electron 可能更省时间。取舍是 Tauri 节省分发和资源成本,Electron 节省生态适配成本。Tauri 的系统 WebView 会不会导致兼容问题?会有这个边界。现代 macOS 和 Windows 通常问题不大,但 Linux 发行版差异、WebKitGTK 版本和用户环境可能带来额外测试成本。Electron 因为自带 Chromium,渲染一致性更强。踩坑点是只在开发机验证 Tauri,没覆盖目标用户的旧系统和企业环境。团队不会 Rust 能用 Tauri 吗?可以,但要谨慎。简单应用只写少量命令,Rust 成本不高;一旦涉及文件处理、系统集成、插件开发和崩溃排查,就需要真正理解 Rust 和平台差异。边界在于:如果后端逻辑只是轻量胶水,Tauri 很合适;如果团队完全排斥 Rust,长期维护会变成隐性风险。Electron 安全性一定比 Tauri 差吗?不是。Electron 的默认历史包袱更多,但按最佳实践配置也可以很安全。Tauri 的默认权限更小,但自定义命令和插件权限仍然需要审计。真正的区别是起点不同:Electron 需要你主动收紧,Tauri 需要你谨慎放开。安全结果取决于工程纪律,而不是框架宣传语。实际项目怎么做选择?如果是聊天、协作、设计工具这类高度依赖浏览器能力和成熟生态的产品,Electron 仍然很稳。如果是本地工具、开发者工具、文件处理、小型客户端,Tauri 的体积和安全模型很有吸引力。还要看发布渠道:企业内网、应用商店、自动更新、代码签名都会影响决策。最稳的方式是用核心场景做一个两周原型,测包体、启动、关键 API 和打包链路,而不是只看 hello world。
前端阅读 05月31日 02:05

Tauri 常用系统 API 应该怎么选才安全?

Tauri 的系统 API 覆盖文件、窗口、对话框、通知、剪贴板、shell、快捷键和事件通信。它们看起来像前端函数,背后却是在调用系统能力,所以真正的问题不是“有哪些 API”,而是“哪些能力该开放给前端,开放到什么范围”。权限给大了,桌面应用会变成安全黑盒;权限给小了,功能又会在生产环境里突然不可用。文件和路径 API 怎么用?文件读写不要直接让前端传任意绝对路径,优先使用 app data、cache、document 等受控目录。用户主动选择的文件可以通过 dialog 获取路径,再交给后端命令处理。这样牺牲了一点自由度,但能避免前端 bug 误删用户文件。import { open } from '@tauri-apps/plugin-dialog';import { invoke } from '@tauri-apps/api/core';const path = await open({ multiple: false });if (typeof path === 'string') { await invoke('import_file', { path });}#[tauri::command]fn import_file(path: String) -> Result<String, String> { std::fs::read_to_string(path).map_err(|e| e.to_string())}Shell API 为什么要谨慎?打开链接和执行命令是两回事。open 外部 URL 通常风险较低,但执行系统命令必须限制参数、白名单程序和用户输入。不要把前端传来的字符串直接拼进 shell,这类问题在桌面应用里同样会变成命令注入。import { open } from '@tauri-apps/plugin-shell';await open('https://example.com/help');如果确实要执行命令,尽量在 Rust 侧封装固定动作,而不是暴露一个“runCommand”。边界是开发工具类应用可能需要更强 shell 能力,但也应该把工作区、命令集合和参数格式限制清楚。窗口、通知和剪贴板适合放前端吗?窗口最小化、聚焦、拖拽、主题切换这类 UI 能力放前端比较自然。通知和剪贴板要考虑用户预期:后台悄悄写剪贴板很冒犯,通知也要避免刷屏。更稳的做法是把这些能力集中封装成一层服务,统一处理权限、失败提示和平台差异。import { getCurrentWindow } from '@tauri-apps/api/window';const win = getCurrentWindow();await win.minimize();capability 配置应该怎么收敛?Tauri v2 推荐用 capability 声明窗口能访问哪些权限。开发期可以开得宽一些,但发布前要按功能逐项收敛。很多“本地正常、打包失败”的 API 问题,本质是 capability 没带进安装包,或者只给了主窗口权限,忘了给设置窗口、弹窗或托盘窗口。{ "identifier": "main-capability", "windows": ["main"], "permissions": [ "dialog:allow-open", "shell:allow-open", "notification:default" ]}多窗口应用要单独设计权限吗?需要。主窗口、设置窗口、登录窗口和托盘唤起的小窗口,能做的事情往往不一样。把所有窗口都塞进同一个 capability 最省事,但也会让低风险页面拿到高风险能力;按窗口拆权限更麻烦,却能减少误调用和安全暴露。实际项目里可以先按功能域拆,比如文件导入窗口只给 dialog 和受限文件权限,帮助页只允许打开外链。这样后续排查也更清楚。权限文件也应该进入代码评审范围。每新增一个系统能力,都要能说清楚由哪个窗口调用、为什么需要、失败时如何降级。追问文件 API 能不能直接开放全部权限?技术上可以,但不建议。全量文件权限会让任何前端漏洞都变得更危险,也会让安全审查很难解释。取舍是内部工具可以稍宽,面向普通用户的应用应尽量限定目录、扩展名和用户主动选择的路径。Dialog 选中文件后为什么还读不了?选择文件只是拿到路径,不代表你的读写权限、scope 或后端逻辑都正确。Tauri v2 下 capability、插件权限和命令实现都要匹配,否则开发时能跑,生产包可能失败。踩坑点是多窗口应用里权限只给了 main,设置页或二级窗口调用同一 API 就报错。Shell API 和 Rust Command 哪个更安全?固定业务动作优先写 Rust command,因为你可以在后端校验参数、限制路径和处理错误。Shell API 适合打开链接、启动外部应用这类明确行为,不适合承载复杂业务逻辑。边界是开发者工具需要调用 git、node、docker 时,可以做命令白名单,而不是开放任意命令。剪贴板和通知为什么要做用户提示?它们都属于用户能明显感知或事后追溯困难的能力。剪贴板被覆盖会影响用户正在做的事,通知过多会让应用显得像广告软件。更好的做法是在用户点击复制、导出完成、后台任务失败时触发,并提供明确反馈。API 迁移到 Tauri v2 最大的坑是什么?很多能力从内置 allowlist 迁到了独立插件和 capability 权限,导入路径也变了。只改 TypeScript import 不够,还要安装插件、在 Rust builder 注册,并写权限文件。迁移时建议一类 API 一类 API 地测,不要一次性替换全部,否则很难定位是权限、插件还是业务代码出了问题。
前端阅读 05月31日 02:05

Tauri 适合做哪些桌面应用?项目里怎么取舍?

Tauri 适合做需要桌面能力、包体控制和系统集成的应用,例如开发工具、内部管理客户端、数据可视化工具、文件处理工具、轻量系统监控、API 调试工具和跨平台小型生产力软件。它的核心思路是前端用 Web 技术做界面,后端用 Rust 暴露系统能力,再通过 IPC 连接两边。相比 Electron,Tauri 通常包更小、资源占用更低;但它依赖系统 WebView,不同平台渲染差异和调试成本也更明显。开发工具和文件类应用很适合代码编辑器、Markdown 编辑器、Git 客户端、API 测试工具都适合 Tauri。前端可以使用 React、Vue、Svelte 加 Monaco Editor 或 CodeMirror,Rust 侧处理文件读写、Git 命令、HTTP 请求和本地配置。优势是系统权限收口在 Rust 命令里,安全边界更清楚。踩坑点是不要把任意路径、任意 shell 命令直接暴露给前端,否则桌面应用会变成高权限网页。#[tauri::command]async fn read_text(path: String) -> Result<String, String> { std::fs::read_to_string(path).map_err(|e| e.to_string())}数据处理和可视化要注意 IPC 成本Tauri 很适合做日志查看器、CSV 清洗工具、数据库客户端和报表看板。Rust 负责解析大文件、压缩、加密、SQLite 查询或并行计算,前端负责图表和交互。边界在 IPC:如果每 10ms 往前端传一次大 JSON,性能会被序列化和通信拖垮。更好的做法是 Rust 侧批处理,前端分页拉取,必要时传二进制或只传聚合结果。npm create tauri-app@latestnpm run tauri devnpm run tauri build企业内部工具看重交付和权限CRM、库存管理、客服工作台、设备管理客户端也能用 Tauri 做。它可以把 Web 管理后台包装成桌面应用,同时补上扫码枪、串口、文件系统、托盘、通知等能力。取舍点是团队是否能维护 Rust 代码,以及目标用户机器上的 WebView 环境是否可控。企业内部分发时还要考虑签名、自动更新和权限白名单,不只是“能跑起来”。不适合所有桌面软件重度 3D、专业音视频剪辑、强实时游戏编辑器不一定适合 Tauri,因为核心能力不在 WebView。需要极致一致 UI 的产品也要谨慎,不同系统 WebView 的字体、滚动和输入法表现可能有差异。Tauri 的优势是轻、可控、能接系统能力;如果应用主要是一个复杂网页外壳,而且包体不是问题,Electron 的生态和调试体验可能更省事。还有一类项目要谨慎:团队完全没有 Rust 经验,却希望短期内做大量原生能力。Tauri 可以降低桌面开发门槛,但不会消除跨平台测试、权限模型和系统 API 差异。追问Tauri 和 Electron 怎么选?如果你在意包体、内存占用和安全边界,Tauri 很有吸引力。如果项目依赖大量 Node.js 原生模块、需要 Chromium 行为完全一致,Electron 通常更稳。Tauri 的坑在于系统 WebView 差异,Windows、macOS、Linux 上要分别测试。简单说,Tauri 更像“Web UI + Rust 系统能力”,Electron 更像“自带浏览器和 Node 运行时”。Tauri 适合做大型企业客户端吗?可以,但前提是架构边界要清楚。复杂业务 UI 放前端,文件、数据库、加密、设备访问放 Rust 命令,状态同步不要全靠频繁 IPC。企业场景还要补齐签名、自动更新、崩溃日志和权限配置,否则试点能跑,规模化分发会很痛。它适合中后台和生产力客户端,不代表可以低成本复刻所有传统桌面软件。使用 Tauri 调系统能力有什么安全坑?最大的坑是把 Rust 命令做成“万能后门”,比如前端传什么路径就读什么文件,传什么命令就执行什么 shell。正确做法是限制目录、校验参数、最小化 allowlist,并把危险能力拆成明确命令。用户输入必须当成不可信数据处理,即使应用不是浏览器也一样。桌面端权限更高,安全事故的破坏面通常比普通网页更大。Tauri 做数据可视化为什么可能变慢?慢点不一定在 Rust 计算,常见瓶颈是把大数组反复序列化成 JSON 传给前端。前端一次渲染几十万点也会卡,图表库和 DOM 更新都会吃掉时间。项目里应先聚合、抽样、分页,再把真正需要展示的数据传过去。Rust 负责重活,WebView 负责交互,这是比较稳的分工。Tauri 项目上线前要检查什么?至少要检查跨平台构建、代码签名、自动更新、权限配置和崩溃日志。macOS 要处理签名和 notarization,Windows 要考虑证书和安装包格式,Linux 要确认 WebKitGTK 依赖。构建命令本身不复杂,难的是不同平台政策和用户环境。上线前最好用干净虚拟机测安装、升级和卸载流程。
前端阅读 05月30日 23:35

Module Federation 是什么?它为什么能运行时加载模块?

Module Federation 是 Webpack 5 提供的运行时模块联邦能力,它允许一个应用在运行时加载另一个应用暴露出来的模块。简单说,remote 负责把组件、页面或工具函数暴露成可被消费的模块,host 负责在需要时加载 remoteEntry.js,再从远程容器里取模块执行。它和传统 npm 包最大的区别是:npm 包在构建前就固定进产物,Module Federation 可以在运行时拿到远程应用刚发布的代码。追问remoteEntry.js 在里面扮演什么角色?remoteEntry.js 可以理解为远程应用的模块目录和运行时入口,它记录了 exposes 暴露了哪些模块,以及这些模块对应的异步 chunk 怎么加载。host 先加载这个入口,拿到远程容器,再调用 container.get('./Button') 获取模块工厂。边界是 remoteEntry 不应该太大,它只是入口和映射,不该把大量业务实现塞进去。踩坑是 CDN 缓存了旧 remoteEntry,而新 chunk 已经发布,host 会按旧映射请求不存在的文件。Host 和 Remote 必须互相知道对方吗?Remote 不需要知道谁会消费它,只要暴露稳定的模块路径和依赖约定即可。Host 需要知道 remote 的容器名、入口地址和模块路径,这些可以写死在构建配置里,也可以通过 manifest 动态下发。取舍是静态配置简单可靠,但灰度和多环境切换不灵活;动态配置灵活,却要求配置服务、白名单和失败兜底更完善。对外暴露的模块路径最好当成 API 管理,随便改名会让 host 运行时直接失败。new ModuleFederationPlugin({ name: 'profile', filename: 'remoteEntry.js', exposes: { './UserCard': './src/UserCard' }, shared: { react: { singleton: true, requiredVersion: '^18.2.0' } }})shared 依赖为什么是它的核心能力?如果没有 shared,每个 remote 都会带自己的 React、组件库和工具库,微前端很快变成“重复下载大赛”。shared 让应用在运行时协商依赖版本,尽量复用已经加载的实例,尤其适合 React 这类需要单例的库。边界是它只能解决依赖共享,不保证业务状态天然一致,也不会自动处理破坏性升级。版本范围写得太宽会埋兼容性雷,写得太死又会让团队升级困难。Module Federation 和 npm 包复用怎么取舍?npm 包适合稳定、通用、发布频率可控的代码,比如工具函数、基础组件和 SDK。Module Federation 适合需要独立部署、跨团队实时交付、和页面强绑定的业务模块。取舍是 npm 更确定、更容易测试,MF 更灵活但运行时风险更多。一个实用边界是:基础能力先做 npm 包,变化快的业务页面或可插拔模块再考虑 MF。它适合所有微前端项目吗?不适合。团队技术栈统一、构建链路可控、需要模块级共享时,Module Federation 很合适;如果主要诉求是接入历史系统、隔离全局变量和样式,qiankun 这类应用级方案可能更省心。它带来的真正成本在治理:远程模块契约、shared 版本、监控告警、缓存策略和回滚机制都要有人负责。把这些边界想清楚,Module Federation 才是架构能力,而不是线上随机加载脚本。
前端阅读 05月30日 23:35

Module Federation、qiankun 和 single-spa 应该怎么选?

Module Federation、qiankun 和 single-spa 都能做微前端,但它们解决的问题层级不一样。Module Federation 更像“模块级运行时共享和发布机制”,擅长跨应用复用组件、页面和依赖;qiankun 更像“应用级接入框架”,帮你加载、隔离和管理子应用;single-spa 更偏底层编排,负责不同应用的生命周期注册和路由调度。选型时不要先问谁更先进,而要先问团队需要共享模块,还是需要托管一堆完整子应用。追问三者最大的差异是什么?Module Federation 的边界在构建系统和模块加载,它依赖 Webpack 5 或兼容实现,核心能力是 remote、exposes、shared。qiankun 的边界在浏览器运行时的应用沙箱,它关心子应用怎么挂载、卸载、隔离全局变量和样式。single-spa 更基础,提供生命周期协议,但很多加载、沙箱和样式治理要自己补。取舍是 MF 更适合同构建体系协作,qiankun 更适合旧系统整合,single-spa 适合愿意自己搭平台的团队。如果公司里 React、Vue、Angular 都有,选哪个?异构技术栈很多时,qiankun 或 single-spa 通常更自然,因为它们把子应用当完整应用接入,不要求模块层面的依赖共享。Module Federation 也能接异构应用,但跨框架共享组件的价值会下降,反而要处理运行时、样式和通信边界。边界是:如果只是把 Vue 页面挂到 React 主站,应用级微前端更省心;如果多个 React 团队要共享业务组件和设计系统,MF 更有优势。不要为了追求“更细粒度”而把异构老系统硬拆成 remote 模块。性能上 Module Federation 一定更好吗?不一定。MF 可以通过 shared 减少重复依赖,也可以按需加载模块,所以在同技术栈、治理良好的情况下性能很好。可如果 remote 拆得过碎、remoteEntry 缓存混乱、共享依赖版本不统一,它也会带来更多网络请求和运行时协商成本。qiankun 加载完整子应用看起来重,但对低频后台页面可能足够简单稳定。性能选型要看访问路径、缓存命中和发布频率,不要只看框架宣传。样式隔离和全局变量谁处理得更好?qiankun 在沙箱和样式隔离上提供了更直接的方案,适合接入历史子应用。Module Federation 默认不解决样式隔离,它只是把模块拿过来执行,CSS 命名冲突、全局状态污染仍要靠 CSS Modules、Shadow DOM、约定或设计系统治理。single-spa 也需要自己补齐这些能力。踩坑是用 MF 后误以为天然隔离,结果 remote 的 reset.css 改了 host 全站样式。实际项目怎么组合使用?它们不是绝对互斥的,大型平台里常见做法是 qiankun 托管历史完整子应用,新的同栈业务用 Module Federation 暴露页面或组件。这样能兼顾迁移成本和长期复用,但平台复杂度会上升,需要统一路由、权限、监控和发布规范。取舍是组合方案灵活,却要求架构团队持续维护边界文档。最怕的是没有治理地混用,最后每个子应用既有沙箱问题,又有 shared 版本问题。
前端阅读 05月30日 23:35

Module Federation shared 配置如何处理依赖版本冲突?

shared 配置的作用,是让多个独立构建的应用在运行时协商依赖,尽量复用同一个包,而不是每个 remote 都带一份 React、Vue 或 UI 组件库。它不是简单的“去重开关”,而是一套运行时共享作用域机制:应用启动时初始化 shareScope,容器把自己可提供的依赖和版本注册进去,消费方再按 requiredVersion、singleton、strictVersion 等规则选择。理解这点,才能知道版本冲突为什么有时只是 warning,有时会直接炸。追问singleton 到底什么时候必须开?singleton 适合那些进程里只能有一个实例的库,比如 React、react-dom、Vue、路由实例相关库和某些全局状态库。不开 singleton 时,不同 remote 可能各自加载一份 React,轻则包体变大,重则 Hooks 报错或上下文不互通。取舍是 singleton 会提升一致性,但也会让高版本覆盖低版本的问题更集中。边界很简单:工具函数库、日期库、小型纯函数包不一定要 singleton,框架运行时通常要。shared: { react: { singleton: true, requiredVersion: '^18.2.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.2.0' }}requiredVersion 和 strictVersion 有什么区别?requiredVersion 表达“我希望拿到什么版本范围”,strictVersion 表达“不满足就不要勉强运行”。默认情况下版本不完全满足时,Webpack 可能给 warning 并选择一个可用版本,业务还能跑但风险需要自己承担。打开 strictVersion 后问题暴露更早,适合设计系统、核心 SDK 这类兼容性要求高的依赖。取舍是严格版本更安全,但发布节奏会变慢,多个团队必须同步升级。eager 为什么经常导致报错?eager 会把共享依赖放进初始包同步加载,适合极少数启动前必须存在的依赖,但多数场景不该开。常见报错是“Shared module is not available for eager consumption”,本质是消费方太早同步读取共享依赖,而共享作用域还没初始化完。边界是:如果你只是想减少一次异步请求,不要用 eager 解决,先看拆包和预加载。踩坑最多的是 host 和 remote 都 eager react,最后不仅没省体积,还让初始化顺序更难控制。多个 remote 依赖不同 React 版本怎么办?最稳的做法是把 React 这类基础依赖纳入团队级版本基线,要求所有应用在同一兼容范围内发布。短期无法统一时,可以让个别历史 remote 独立打包自己的 React,但不要让它和 host 共享组件上下文。取舍是独立打包牺牲体积,换取隔离和稳定;强行共享牺牲稳定,换取表面上的去重。真正危险的是半共享状态:组件能渲染,但 Context、路由或 Hooks 在边界处出现偶发问题。shared 配置应该怎么治理?不要每个团队各写一份 shared,最好抽成公共配置或由构建插件统一生成。依赖升级时先在测试环境验证 shareScope 实际选择了哪个版本,而不是只看 package.json。可以在启动时打印关键共享依赖版本,线上采样上报,方便定位“某个租户加载了旧 remote”的问题。治理边界是别把所有包都纳入共享,shared 越多,版本协商面越大,发布时的隐性耦合也越多。
前端阅读 05月30日 23:35

Module Federation 动态加载是怎么实现的?

Module Federation 的动态加载,本质是 host 在运行时先加载 remoteEntry.js,再从远程容器里取出指定模块工厂,最后执行工厂拿到组件或函数。它的优势是部署和加载都更灵活:用户没访问某个功能,就不必下载对应代码;remote 更新后,也不一定要求 host 重新构建。但动态加载不是免费午餐,它会引入网络失败、版本协商、加载顺序和降级体验这些运行时问题。追问它和普通 import() 有什么区别?普通 import() 加载的是当前构建产物里的异步 chunk,构建时 Webpack 已经知道依赖图。Module Federation 的 import('remote/Button') 则会通过容器引用去远程应用拿模块,host 构建时只知道远程容器名和暴露路径。取舍是它换来了跨应用复用和独立部署,但也把一部分确定性从构建时挪到了运行时。踩坑是本地开发能加载,不代表生产可用,生产还要处理域名、CORS、缓存和版本地址。运行时远程地址可以动态决定吗?可以,常见做法是用 promise remote 或在启动前拉一份 manifest,根据环境、租户、灰度批次决定 remoteEntry 地址。这样适合多环境部署和灰度发布,但配置源必须高可用,否则 host 连入口都找不到。边界是不要把远程地址完全交给用户输入或不可信接口,避免加载未知脚本带来安全风险。实际项目里通常会做白名单、版本签名和超时兜底。remotes: { shop: `promise new Promise(resolve => { const url = window.__REMOTE_MANIFEST__.shop; const s = document.createElement('script'); s.src = url; s.onload = () => resolve(window.shop); document.head.appendChild(s); })`}React 里动态加载 remote 组件怎么做更稳?React.lazy 可以直接包远程模块,但必须配合 Suspense 和 ErrorBoundary,否则网络失败时页面会白屏。加载态要按业务重要性设计,主流程组件失败时应该给重试或降级入口,边缘运营组件失败可以直接隐藏。取舍是通用加载器能减少重复代码,但过度封装会掩盖具体错误,排查时反而困难。建议在加载器里统一打点 remote 名称、模块名、耗时和异常类型。动态加载会不会影响首屏?如果首屏依赖 remote,它当然会影响,因为浏览器必须先拿 remoteEntry,再拿模块 chunk,链路比本地 chunk 更长。解决方式不是一律禁止首屏 remote,而是把关键 remote 做预连接、预加载或服务端下发就近 CDN 地址。边界是首页骨架、导航和错误提示最好由 host 自己掌握,不能让远程失败拖垮整个壳。很多团队踩过的坑是把布局组件也远程化,结果某个 remote 挂了,全站都打不开。动态加载适合哪些场景?它适合权限差异大、访问频率不均、团队需要独立发布的功能,比如后台插件、低频设置页、营销活动页和大型可视化组件。不适合强一致、强首屏、频繁跨模块同步状态的核心链路,除非团队能接受额外的治理成本。优势在组织协作上很明显,但技术边界也要讲清楚:动态加载解决的是代码交付问题,不自动解决状态管理、样式隔离和接口契约问题。把它当成模块级发布能力,而不是微前端万能胶,会少踩很多坑。
前端阅读 05月30日 23:35

Module Federation 性能优化应该从哪些地方下手?

Module Federation 的性能优化不是只压缩 remoteEntry.js,而是控制远程模块什么时候加载、加载多少、依赖是否重复,以及失败时页面能不能优雅降级。实践里最常见的问题是:为了拆微前端把模块拆得很碎,结果请求数、共享依赖协商和首屏等待一起变多。比较稳的做法是把首屏必须展示的模块留在 host 或提前预热,把低频功能、重组件、运营位、后台管理页交给 remote。追问remoteEntry.js 很大时应该怎么优化?remoteEntry.js 主要保存容器运行时和暴露模块映射,它不应该承载大量业务代码。如果它明显变大,通常是 exposes 指向了聚合入口,或者把太多公共逻辑打进了 remote 的入口链路。取舍是:暴露粒度太细会增加维护成本,暴露太粗又会让首包变重,建议按页面级或稳定业务组件暴露,不要把整个 src/index 暴露出去。还要确认生产构建开启 tree shaking,package.json 里正确声明 sideEffects,否则看似没用的模块仍可能被保留下来。new ModuleFederationPlugin({ name: 'catalog', filename: 'remoteEntry.js', exposes: { './ProductCard': './src/ProductCard' }, shared: { react: { singleton: true, requiredVersion: '^18.2.0' } }})远程模块要不要预加载?预加载适合“很可能马上用到、但不是首屏阻塞项”的模块,比如用户登录后大概率进入的仪表盘。可以在路由 hover、首屏空闲或权限确认后加载 remoteEntry,但不要一进站就把所有 remote 都 preload,那只是把异步成本提前了。边界在于网络环境和业务路径:移动端弱网更应该谨慎,后台系统内网环境可以更激进。踩坑是只预加载 remoteEntry,却没有预热真正的 chunk,首次渲染仍会卡一下。requestIdleCallback?.(() => import('catalog/ProductCard'))shared 依赖能带来多少性能收益?shared 的价值是避免 React、Vue、UI 库这类大依赖重复下载和重复初始化。收益取决于团队是否真的使用兼容版本,如果每个 remote 都锁不同大版本,运行时仍可能退回本地副本。取舍是 singleton 能减少体积,但会把版本升级风险集中到一个共享实例上,尤其 React、状态库和设计系统要更谨慎。性能优化时先用 bundle analyzer 看重复依赖,再决定哪些库 shared,不要把所有依赖都共享。CDN 和缓存应该怎么配?业务 chunk 可以用内容哈希长期缓存,remoteEntry.js 则要短缓存或配合版本化地址,因为它负责告诉 host 最新模块在哪里。一个常见坑是 remoteEntry.js 被 CDN 缓太久,remote 已发布新 chunk,host 还拿旧映射,结果线上 404。更稳的方案是 remoteEntry 短 TTL,chunk 长 TTL,并在发布后保留一段时间的旧 chunk。这样会多占一些存储,但换来灰度和回滚时的稳定性。性能优化怎么验证是否有效?不要只看构建产物大小,还要看首屏 LCP、远程模块首开耗时、chunk 请求瀑布和错误率。Module Federation 的问题经常出在运行时,所以 Lighthouse 只能给一部分答案,真实用户监控更关键。可以埋点记录 remoteEntry 下载、container init、module get 和组件渲染耗时。边界是埋点本身不能阻塞主链路,失败日志也要采样,否则优化工具会变成新的性能负担。