模块系统与打包工具

共 48 题
📑 题目列表 48 题
#
★★★

1. package.json exports 条件导出与类型解析(exports.types 条件、typesVersions 回退)的工程实践与常见踩坑

package.json 的 exports 条件导出如何配置?类型解析中 exports.types 条件与 typesVersions 回退各在什么场景使用,工程实践中有哪些常见踩坑?

  • exports 字段的条件导出语法与通配符子路径映射
  • exports.types 条件与 typesVersions 在 TS 类型解析中的优先级
  • 双格式发布、require/import 条件与打包器解析的常见踩坑

exports 字段以"包名"为根声明子路径与条件的映射,如 "."、"./utils",每个目标可带 import/require/types/default 等条件,Node.js 按顺序匹配第一个成立的条件,未声明的子路径直接禁止访问(区别于 main/files 的宽松放行)。类型解析上,TS 先看 exports 内 types 条件(或 typesVersions 映射),再回退到顶层 types 字段;typesVersions 主要用于旧版本 TS 或无法用 exports 表达的历史路径重映射,现代包通常优先在 exports 中声明 "types" 条件并置于 import/require 之前,因为 TS 按条件顺序取第一个命中项。

常见踩坑:把 "types" 放在 import 之后导致 ESM 消费者解析到 .d.ts 失败;对 CJS/ESM 双产物分别提供 .d.ts 与 .d.mts,却忘记在 require 条件中声明类型导致 require 侧 any;exports 中通配符与精确路径冲突;以及在 exports 中使用 "./package.json" 之外未导出的路径造成运行时 Error [ERR_PACKAGE_PATH_NOT_EXPORTED]。工程建议是双产物各自带类型、把 types 条件放最前,并用 arethetypeswrong 工具校验发布的类型解析结果。

本题考察对"条件导出"作为现代包入口契约的完整理解:exports 不仅控制运行时解析,还参与类型解析,types 条件与 typesVersions 是两条互补的兼容路径。答题时应先讲清条件匹配顺序,再落到双格式发布的类型文件配对与常见错误,体现真实发布踩坑经验。

{
  "name": "my-lib",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}
#
★★★

2. dual package hazard(CJS/ESM 双实例)的成因(同一包被两种格式加载导致状态不一致)与治理(单实例设计、isomorphic 包)

dual package hazard(CJS/ESM 双实例)是如何产生的?为什么会导致状态不一致?有哪些治理手段?

  • 同一包以 CJS 与 ESM 两种格式分别被实例化的机制
  • 实例状态(单例缓存、全局可变数据)被复制导致的 bug
  • 单实例设计、isomorphic 包、边界归一化等治理策略

dual package hazard 指同一个包因 exports 条件同时提供 CJS 与 ESM 两种产物,而应用的不同模块一个 require 它、另一个 import 它,Node.js 将两个产物分别加载,产生两份独立的模块实例。若该包内部持有单例状态(如全局缓存、连接池、配置对象),两份实例各自维护一份状态,导致"改 A 实例、读 B 实例"的不一致,典型症状是 instanceof 判断失败、事件监听失效或配置不生效。

治理手段:一是"单实例设计",包内尽量不持有模块级可变状态,需要共享时写入显式服务或外部存储;二是"isomorphic 包",让两个入口尽量共享同一份核心实现(如 ESM 入口 re-export CJS 实现),使行为一致;三是应用侧统一模块格式(统一用 ESM 或统一用 CJS)避免混用;四是依赖注入,由宿主把共享实例传入。在包发布层面,也可考虑只发布单一格式或用打包器做格式转换,从根源消除双格式并存。

本题考察对模块系统互操作的深层理解:问题本质是"同一逻辑两份实例",而非简单的格式差异。回答应依次说明成因(条件导出双产物 + 混用加载)、危害(状态分裂)与三层治理(包内单例设计、isomorphic 包装、宿主统一格式),体现系统性工程视角。

// 错误示范:模块级单例状态被双实例复制
const cache = new Map();
export function set(key, val) { cache.set(key, val); }
export function get(key) { return cache.get(key); }
// 治理:状态交由宿主注入或使用单一格式加载
#
★★★

3. Rspack 的 SWC 集成与自定义 loader 的 Rust 迁移路径

Rspack 如何集成 SWC 做转译?团队自定义 loader 迁移到 Rust(builtin loader)的路径是怎样的?

  • Rspack 内置 SWC 转译(JSX/TS/压缩)替代 Babel 的机制
  • 自定义 JS loader 与 builtin:xxx Rust loader 的边界
  • JS loader 渐进迁移到 Rust 的兼容策略

Rspack 用 Rust 实现核心编译链路,内置基于 SWC 的转译能力(jsx/ts/装饰器等通过内置 swc-loader 或 experiments.swc 配置启用),常见场景可直接替代 babel-loader + terser-webpack-plugin,获得数量级提升的构建性能。SWC 内置转译对绝大多数 TS/JSX 场景足够,但 Babel 生态的插件(如某些 babel-plugin 魔法变换)无法直接复用,需要改写为 SWC 插件或保留 babel-loader 处理特定文件。

自定义 loader 的 Rust 迁移路径:先保留 JS loader 保证功能正确,通过 experiments.builtinLightningCssLoader、builtin:swc-loader 等内置能力替换高频通用 loader;再用 Rspack 的 JS API 编写等价 loader 对比产物,最后针对瓶颈把纯计算型 loader 用 napi-rs 编写 Rust 版本注册为自定义 builtin loader。迁移中需注意 loader 的执行顺序(pitch 与 normal 阶段)、异步获取模块信息(this.getModuleInfo 等 API 差异)与错误信息格式差异,并保留旧构建做金丝雀对比。

本题考察对"Rust 化工具链"演进路径的认知:SWC 集成解决通用转译,自定义 loader 迁移则应遵循"先能力等价、再性能替换"的渐进策略,同时关注 loader API 差异导致的语义变化,体现对构建器内部机制与迁移风险的理解。

#
★★★

4. TC39 Deferred Imports 提案 import defer(Stage 3)的声明式懒加载

TC39 Deferred Imports 提案(import defer,Stage 3)是什么?它与动态 import() 在语义上有何区别,工程上如何使用?

  • import defer 的语法与"延迟执行、按需实例化"语义
  • 与动态 import() 的触发时机差异(解析与求值分离)
  • 对打包器分块与首屏性能的影响

import defer(Stage 3 提案)提供声明式延迟导入语法 import defer * as mod from "...",它只把模块的解析(parse 与链接)推迟到模块实际首次被访问时执行,而模块绑定(引用)在编译期就已建立,是静态的;相比动态 import() 在运行时触发网络加载与实例化,defer 仍由打包器静态分析并分块,因此错误检查、tree-shaking 与代码分割都能保留,只是把实例化延迟到首次使用。

工程价值:对"启动路径上被引用但通常用不到"的模块(如重型组件、polyfill、按需工具库),用 import defer 声明式降级其初始化开销,避免手写动态 import 的复杂判断逻辑;同时因为是静态语法,条件分支中也能用,且打包器可生成更优的加载时序。需注意 defer 只推迟实例化不推迟模块存在性检查(语法错误在链接期即报出),且对依赖它的模块作用域语义(如 TDZ)有特殊规则,使用前需确认运行环境支持或经转译降级。

本题考察对提案阶段特性的准确理解:import defer 的核心是"解析与求值分离、首次访问才实例化",区别于动态 import 的运行时异步触发。回答强调静态性与按需实例化的组合优势,并点出错误检查时机差异,体现对规范细节的把握。

// 声明式延迟:模块在首次访问时才被实例化
import defer * as heavy from "./heavy-module.js";
export function maybeUse() {
  if (needHeavy) {
    return heavy.doWork(); // 此时才执行模块代码
  }
}
#
★★★

5. Rollup 的库打包与 esbuild 的转译职责

Rollup 与 esbuild 在打包链中各自的职责是什么?为什么常用"esbuild 转译 + Rollup 打包"的组合?

  • Rollup 以 ESM 为中心的产物优化与 tree-shaking
  • esbuild 极快转译(TS/JSX)与依赖预打包的定位
  • 两者在库打包与依赖预构建中的分工协作

Rollup 定位是"以 ESM 为中心的打包器",擅长按模块图做精确 tree-shaking、输出多种格式(ESM/CJS/UMD/IIFE)、生成 .d.ts 映射与按需加载的 chunk 结构,是库打包(library mode)与 Vite 生产构建的基础;esbuild 定位是"极速转译 + 打包"工具,用 Go 并行解析,速度比传统 JS 打包器快一个数量级,但其 tree-shaking 与产物精细度(如 interop、格式控制)不如 Rollup,且插件生态较弱。

因此工程中常见分工:开发期用 esbuild 做依赖预构建(Vite prebundle)与 TS/JSX 转译,换取毫秒级冷启动与 HMR;生产构建用 Rollup(或 Rolldown)做产物优化与格式输出。Vite 5 及以前正是"esbuild 转译 + Rollup 打包"的典型组合;库作者则常直接用 Rollup + 插件(如 @rollup/plugin-node-resolve、terser 或 SWC minify)产出多格式库包,再配合 tsc 或 api-extractor 生成类型。

本题考察工具定位辨析:Rollup 赢在产物质量与格式灵活性,esbuild 赢在速度但精细度有限,二者并非互斥而是互补。回答按"开发期/生产期、依赖/源码、库打包"三个维度展开分工,体现对现代前端工具链架构的理解。

#
★★★

6. Vite 的 SSR load/transform 钩子与 ssrLoadModule 在服务端模块转换的工程应用

Vite SSR 中 load/transform 插件钩子如何工作?ssrLoadModule 在服务端模块转换与运行时加载中有什么工程应用?

  • SSR 模式下 transform/load 钩子对源码与依赖的处理差异
  • ssrLoadModule 的按需转换与模块图复用机制
  • 在 SSR 框架集成、按需加载与依赖外置(ssr.noExternal)中的实践

Vite 的 SSR 模式复用同一套插件体系:transform 钩子按需转换模块内容(如 JSX/TS/样式),load 钩子在模块未被任何插件转换时提供原始内容;区别在于 SSR 下模块按"服务端环境"解析,Node 内置模块与某些依赖默认走外部化(externalize),由 ssrExternal/ssr.noExternal 控制,避免把 node_modules 依赖也打进 SSR bundle 造成体积与执行问题。

ssrLoadModule 是 Vite SSR 运行时的核心 API:给定模块 URL,它在服务端进程内按需转换并执行模块(支持 import.meta.env、别名等 Vite 特性),且复用 dev server 的模块图实现缓存,因此框架(如 Nuxt、Astro、SvelteKit)用它实现按路由加载组件、执行 SSR 入口;工程上还可用它做服务端按需加载(如按请求动态加载页面模块)、在 Node 端直接调用带 Vite 变换的代码(如 .vue/.tsx 文件),并配合 load/transform 钩子注入服务端专用处理(如注入环境变量、替换浏览器 API)。

本题考察 Vite SSR 内部机制:load/transform 是转换链路的钩子,ssrLoadModule 是把转换与执行串起来的运行时 API,二者配合实现"服务端也能跑带 Vite 特性的代码"。回答要点出外部化策略与模块图缓存,体现对 SSR 转换链路和性能影响的理解。

#
★★★

7. Vite SSR import.meta.env.SSR/.CLIENT 在 SSR 导入分支的工程价值

Vite SSR 中的 import.meta.env.SSR 与 import.meta.env.CLIENT 是什么?在 SSR 导入分支上有什么工程价值?

  • import.meta.env.SSR/.CLIENT 的编译期替换语义
  • 同一代码库按环境区分导入分支(浏览器/服务端专用模块)
  • 与打包器 tree-shaking、SSR 双端产物配合的价值

import.meta.env.SSR 是 Vite 在编译期静态替换的常量:SSR 构建时为 true、客户端构建时为 false,import.meta.env.CLIENT 与之相反。因为替换发生在编译期且条件分支可被静态分析,打包器能够把未命中分支的导入完全 tree-shake 掉,从而在"同一份源码"中安全地编写双端分支:客户端分支引用 window/localStorage 等浏览器 API,服务端分支引用 fs/process 等 Node API,且不会把对方环境的依赖带入产物。

工程价值:一是消除"双端代码分离维护"的重复成本,一份组件源码即可表达两端行为(如 SSR 首屏用服务端数据源、客户端水合后切换浏览器存储);二是实现依赖级别的按环境隔离,避免浏览器包引入 Node 模块或反之;三是配合条件编译让 Node 特有的动态 require、原生模块只存在于 SSR 产物中。注意事项:分支必须写在顶层可静态分析的条件里(如 if (import.meta.env.SSR)),不能通过变量间接引用,否则无法被替换与摇树。

本题考察 Vite SSR 条件编译机制:SSR/CLIENT 是编译期常量而非运行时变量,其价值建立在"静态替换 + tree-shaking"之上。回答应强调分支必须静态可分析,否则双端隔离失效,体现对机制前提的理解。

export function getStore() {
  if (import.meta.env.SSR) {
    return createServerStore(); // 仅进 SSR bundle
  }
  return new ClientStore(); // 仅进客户端 bundle
}
#
★★★

8. Vite 8 的 import.meta.hot.accept 与 HMR API 在大型应用工程价值

Vite 8 中 import.meta.hot.accept 等 HMR API 的工作原理是什么?在大型应用中有什么工程价值与注意事项?

  • import.meta.hot.accept 的模块自更新与依赖更新语义
  • 自定义 HMR 处理(accept 回调、dispose、invalidate)的工程场景
  • 大型应用中 HMR 边界、状态保持与全量 reload 的权衡

import.meta.hot 是 Vite 暴露给模块的 HMR 接口:import.meta.hot.accept() 声明"本模块自身更新后可原地替换",accept('./dep', cb) 声明"依赖更新时执行回调并保留本模块实例",dispose 用于清理旧模块资源(事件监听、定时器),invalidate 则主动失效以触发重新加载链路。浏览器端由 HMR runtime 建立模块图,热更时按边界逐级向上找 accept 声明,找不到就向上冒泡直至整页 reload。

大型应用中的工程价值:自定义 accept 回调可保存组件状态(如编辑器内容)、重放请求、实现局部刷新而不丢失页面上下文;dispose 可防止热更后资源泄漏与重复订阅;配合 import.meta.glob 与框架层(React Fast Refresh、Vue HMR)可实现按模块粒度热更。注意事项:接受依赖更新的回调要保持纯函数化避免闭包污染;对带副作用的模块(如初始化全局单例、动态注册路由)要谨慎声明 accept,否则热更后状态漂移难以排查;模块图过深时冒泡链过长,需在边界模块显式 accept 以收敛刷新范围。

本题考察 HMR 协议的边界机制:核心是"冒泡至最近的 accept 边界"与"dispose 清理旧实例"。回答应结合大型应用场景讲价值(状态保持、资源清理、刷新范围收敛),并指出副作用模块的风险,体现对热更边界与工程取舍的理解。

if (import.meta.hot) {
  import.meta.hot.accept((newMod) => {
    rerender(newMod.render);
  });
  import.meta.hot.dispose(() => {
    editor.destroy(); // 清理旧模块资源
  });
}
#
★★★

9. Turbopack 的 Rust 增量构建与 Webpack 兼容层

Turbopack 的 Rust 增量构建原理是什么?它的 Webpack 兼容层覆盖哪些能力,边界在哪里?

  • Turbopack 基于增量图与持久化缓存的构建模型
  • 与 Webpack 的 loader/plugin 生态兼容范围
  • Next.js 中启用 Turbopack 的取舍与边界

Turbopack 由 Vercel 用 Rust 实现,核心是"增量计算图":构建任务被拆成可缓存的细粒度单元(模块解析、转译、代码生成),每步结果按内容寻址持久化,源码变更时只重算受影响子图,配合多进程并行,实现接近 O(受影响范围) 的增量构建;其持久化缓存跨进程、跨机器可复用,理论上冷启动与热更新都远快于 Webpack 的 JS 实现。

兼容层方面,Turbopack 提供对 Webpack 主要配置项(resolve.alias、loaders 的常见用法、插件系统)的兼容努力:Next.js 15 起 Turbopack 在 dev 与 build 中稳定可用,支持 babel-loader 之外的常用 loader 与多数 webpack 插件,但完整 loader/plugin 生态(自定义 compilation hooks、复杂 plugin 生命周期)并不完全兼容,需要插件改造或等待官方适配。工程上通常在 Next.js 中开启 Turbopack 获得构建加速,对不兼容的插件保留 Webpack 作为回退,并用产物对比与 E2E 测试验证行为一致。

本题考察新一代构建器设计:强调"增量图 + 内容寻址缓存"带来的范式差异,再讲清兼容层的"可用但不完全"状态。回答应落到迁移策略(先 dev 后 build、双引擎对比验证),体现工程判断力。

#
★★★

10. Babel/SWC/Oxc(Oxidation Compiler)

Babel、SWC 与 Oxc(Oxidation Compiler)在转译与解析上的定位有何区别?如何根据项目选型?

  • 三者实现语言(JS 生态 / Rust / Rust)与性能差异
  • 插件生态、标准化程度与工具链整合能力
  • 转译、Lint、格式化等能力的覆盖范围与选型依据

Babel 是最成熟的 JS 转译器,生态插件最全(语法插件、babel-plugin 变换),支持自定义 AST 变换,但用 JS 实现,转译性能最慢,配置链(preset/env、polyfill 注入)复杂度高;SWC 用 Rust 实现,解析与转译快一个数量级,提供 swc-loader、@swc/core API,可替代 Babel 完成 TS/JSX/压缩,但部分 Babel 插件的魔法变换需改写或缺失;Oxc 是新兴的 Rust 工具链集合(Oxidation Compiler),同时覆盖解析器(oxc-parser)、Lint(oxlint)、转译与格式化,速度极快,正被 ESLint/Prettier 生态关注并逐步集成。

选型建议:追求生态完整与自定义变换用 Babel;追求性能、TS/JSX 转译与压缩为主用 SWC;需要一体化、超快 Lint 或想统一 Rust 工具链用 Oxc(如 oxlint 替代 ESLint 做大仓静态检查)。三者可并存:例如 Vite 用 esbuild/SWC 做转译,同时保留 Babel 插件做特定变换,Oxc 负责 Lint,按"职责分层"选择而不是二选一。

本题考察工具链演进脉络:Babel(生态)、SWC(性能)、Oxc(一体化)代表三代方案。回答应横向对比实现语言、性能、生态与能力边界,并给出"按职责组合选型"的结论,避免非此即彼的简单化回答。

#
★★★

11. Vite 最新版的 optimizeDeps.esbuildOptions 在依赖预构建含 CJS/ESM 互操作的工程价值

Vite 最新版中 optimizeDeps.esbuildOptions 的作用是什么?在依赖预构建的 CJS/ESM 互操作场景下有什么工程价值?

  • optimizeDeps 依赖预构建(prebundle)的机制与目的
  • esbuildOptions 对转译、define、plugin 等行为的定制
  • CJS/ESM 互操作(interop)问题与 external/平台适配的治理

Vite 的依赖预构建用 esbuild 把 node_modules 依赖预打包成 ESM,并合并扁平化依赖(如多个包共享的 react 只保留一份),目的是让浏览器原生 ESM 能直接加载、减少请求数并统一互操作语义。optimizeDeps.esbuildOptions 允许向 esbuild 传递额外选项:如 plugins(自定义解析与替换)、define(注入全局替换,如 process.env.NODE_ENV)、tsconfigRaw、external 等,从而在预构建阶段定制转换行为。

在 CJS/ESM 互操作场景下其价值尤为明显:许多依赖是 CJS(如某些老库),esbuild 预构建时会把 CJS 转换为 ESM 并生成 interop 包装(default 导出与命名导出映射),esbuildOptions 可通过 plugins 处理特殊格式的依赖、用 external 排除不应预构建的包(避免双实例)、或用 define 消除依赖内部对 process/buffer 等 Node 全局的引用。工程上它提供"不修改依赖源码"的兼容治理入口,是处理互操作问题的最后一道拦截层;若依赖无法被 esbuild 正确转换,可改用 optimizeDeps.exclude 让其走浏览器原生加载并配合 external 处理。

本题考察对 Vite 预构建细节的掌握:esbuildOptions 是预构建行为的定制窗口,核心场景是 CJS 依赖的 ESM 化与互操作治理。回答应把"预构建机制 - 定制入口 - 互操作问题"串成一条线,并给出 exclude 等兜底手段。

#
★★★

12. Webpack 仅作为备选 flag 的迁移策略

"Webpack 仅作为备选 flag"的迁移策略是什么?在从 Webpack 迁移到新构建器时如何设计双引擎并存与回退机制?

  • 以 flag/环境变量切换构建引擎的渐进迁移设计
  • 双引擎产物的语义对比与金丝雀验证
  • 回退条件、特性对齐清单与迁移完成判定

"Webpack 仅作为备选 flag"指在迁移到新构建器(如 Rspack/Turbopack/Vite)时,不一次性切换,而是通过环境变量或 CLI flag(如 BUILD_ENGINE=webpack|rspack)保留 Webpack 作为可随时回退的备选引擎:默认跑新引擎,遇到问题切回 Webpack 排查,形成"新引擎为主、旧引擎兜底"的灰度结构。

落地的关键:一是配置收敛,把两份构建配置抽成共享的解析/转译/分割规则,只让引擎壳不同,避免两套行为漂移;二是产物语义验证,用双构建金丝雀(CI 中对同一提交分别构建、对比产物 hash 与运行结果)确认产物等价而非只比构建耗时,重点覆盖 loader 顺序、tree-shaking 差异、CSS 提取与 chunk 命名;三是特性对齐清单,把用到的 loader/插件/钩子逐项标记"已兼容/需适配/不可用",未对齐项即为回退触发条件;四是定义完成标准(如连续 N 天无回退、产物 diff 为零、性能达标)后才移除 Webpack 依赖。

本题考察大型构建器迁移的方法论:核心不是"换工具"而是"可控灰度"。回答应突出 flag 双引擎机制、产物语义金丝雀验证与特性对齐清单三个抓手,并说明回退不是失败而是流程的一部分。

#
★★★

13. ESM 与 CJS 在 Node.js 与浏览器的工程边界与互操作

ESM 与 CJS 在 Node.js 与浏览器中各自处于什么位置?两者的工程边界如何划分,互操作时有哪些要点?

  • ESM 静态分析与 CJS 动态语义的本质差异
  • Node.js 中 require(ESM) 的限制与双向互操作规则
  • 浏览器原生 ESM 与打包器产物、双格式发布的边界

ESM 是标准化的静态模块系统:导入导出在编译期确定,支持 tree-shaking、循环依赖的合理处理与顶层 await;CJS 是 Node.js 的动态模块系统,module.exports 在运行时计算,支持条件导出、动态 require 与运行时 monkey-patch。工程边界上,新代码优先 ESM(面向未来、利于摇树),存量 CJS 代码通过互操作层渐进迁移,浏览器只能原生加载 ESM(

本题考察模块系统的体系化认知:先讲两种格式的语义根基(静态 vs 动态),再分别落到 Node 互操作规则与浏览器边界,最后给治理原则。能答出 cjs-module-lexer 与 require(ESM) 等细节体现对现代 Node 行为的最新掌握。

#
★★★

14. Vite 6+ 相比 Webpack 5 在开发体验(HMR、启动速度)

Vite 6+ 相比 Webpack 5 在开发体验(HMR、启动速度)上有哪些本质差异?这些差异来自什么机制?

  • 原生 ESM 开发服务器与 bundle 型 dev server 的架构差异
  • 依赖预构建 + 按需转换带来的冷启动与 HMR 速度优势
  • HMR 边界与大型项目中的体验代价

Webpack 5 的 dev server 采用"全量打包"模型:启动时遍历整个依赖图打包成 bundle,源码变更时按模块粒度增量重编译再通过 HMR runtime 推送,首次启动与大型项目的热更新仍受 JS 打包开销制约;Vite 6+ 采用"原生 ESM 服务器"模型:源码不打包,浏览器直接请求单个模块文件,开发服务器按需转换(esbuild 转译 TS/JSX),仅依赖被预构建成 ESM,因此冷启动几乎不随项目规模增长,HMR 只需精确替换变更模块及其引用链,速度稳定在毫秒级。

差异本质是"预先打包全部"与"运行时按需转换"两种架构:Vite 把构建成本从"启动时全量"摊薄到"访问时按需",配合 esbuild/Rolldown 的高性能转换获得数量级体验提升。注意事项:Vite 的按需转换在模块数极大、依赖图极深时首访延迟可能放大,且其 HMR 依赖 import.meta.hot 边界,第三方库若不带热更支持只能整页刷新;工程上可用 optimizeDeps 预热、SSR 时用 ssrLoadModule 缓存等缓解。

本题考察 dev 体验差异的架构根源:bundle 型(启动即全量)与原生 ESM 型(按需转换)是两种范式。回答要落到机制(依赖预构建、按需转换、模块级 HMR),并客观指出 Vite 的边界,避免只夸性能。

#
★★★

15. Rolldown(Rust 实现的 Rollup 兼容打包器)

Rolldown 是什么?它如何做到 Rollup 兼容?在 Vite 演进中承担什么角色?

  • Rolldown 的 Rust 实现与 Rollup API 兼容设计
  • 对 Rollup 插件生态的兼容方式(钩子、this 上下文)
  • Rolldown 作为 Vite 生产构建底座的演进路径

Rolldown 是 Vite 团队(Rolldown 工作组)用 Rust 实现的打包器,目标是"Rollup 兼容 + 极高性能":它复刻 Rollup 的产物结构与 API(build 阶段、输出阶段、chunk 计算逻辑),让绝大多数 Rollup 插件不经修改即可运行,同时利用 Rust 的并行与零拷贝获得数倍到数十倍的生产构建提速。它把 Rollup 的 JS 插件协议(钩子函数、this 上下文如 this.resolve/this.getModuleInfo、输出钩子)在 Rust 内核上实现为兼容层,插件仍以 JS 编写、通过 N-API 桥接执行。

在 Vite 演进中,Rolldown 是 Vite 6+ 规划中的生产构建底座(Vite 8 起生产构建默认由 Rolldown 提供):它保留 Vite 的插件心智模型与 Rollup 生态,同时消除 esbuild 转译 + Rollup 打包两套引擎的不一致,统一依赖预构建与生产构建的语义。工程影响:现有 Rollup 插件基本可用,但依赖钩子执行顺序、this.getModuleInfo 精确语义、virtual module 与 manualChunks 的插件在迁移到 Rolldown 后需回归验证,少数依赖内部 Rollup API 的插件需适配。

本题考察工具链演进的核心项目:要点是"兼容不是重写",Rolldown 通过 Rust 内核 + JS 插件桥接保留生态又获得性能。回答应覆盖兼容机制、在 Vite 中的角色与迁移注意点,体现对路线图的了解。

#
★★★

16. Vite 的 import.meta.glob 在多文件批量导入的工程价值

Vite 的 import.meta.glob 如何实现多文件批量导入?它在工程中有哪些典型应用与注意事项?

  • import.meta.glob 的 glob 语法与懒加载/急切加载模式
  • 批量导入在路由、组件注册、数据文件加载中的应用
  • 静态分析限制(变量路径、排除项)与类型生成

import.meta.glob 是 Vite 提供的编译期批量导入 API:import.meta.glob('./pages/*.vue') 返回以路径为键的"懒加载导入函数"映射,默认只加载函数(模块内容在调用时才请求);传 { eager: true } 则直接得到模块导出对象,适合需要同步访问的场景。它由 Vite 在编译期扫描文件系统生成静态映射,因此路径必须是字面量 glob 模式,不能用变量拼接。

工程价值:一是路由级批量注册(按目录约定生成路由表)、二是组件/图标批量引入(避免手写 import 清单)、三是目录型数据加载(如 i18n 语言文件、文档目录)。配合 import.meta.glob(..., { query: '?raw', import: 'default' }) 可定制导入内容与导出字段。注意事项:glob 结果在构建期固定,新增文件需重启 dev server 或触发重新扫描;排除用 exclude 选项;在 SSR 中同样可用但注意服务端文件系统差异;类型安全可用 vite/client 的类型声明增强。

本题考察 Vite 的编译期动态导入能力:核心是"静态扫描生成映射",懒/急两种模式对应不同场景。回答应给出典型应用(路由、组件、数据目录)并强调字面量路径与构建期固定的限制,体现对机制边界的理解。

const routes = import.meta.glob('./pages/**/*.vue');
// routes 形如 { './pages/home.vue': () => import('./pages/home.vue'), ... }
const allModules = import.meta.glob('./locales/*.json', { eager: true });
#
★★★

17. Module Federation 2.0 的微前端共享与远程模块加载

Module Federation 2.0 如何实现微前端的模块共享与远程模块加载?相比 1.0 有哪些演进?

  • Module Federation 的运行时远程模块解析与共享作用域
  • Module Federation 2.0 的 API 演进(@module-federation/runtime、动态远程)
  • 共享依赖版本策略、类型提示与联邦边界治理

Module Federation 允许构建产物在运行时声明"共享模块"与"远程模块":宿主构建时把远程模块的 URL 映射打进运行时,浏览器端通过 federation runtime 按需加载远程 chunk;共享依赖(如 react、react-dom)通过 shared 配置声明,运行时按 semver 范围协商出单例版本,避免多实例。1.0 的局限是共享配置在构建期固化、远程必须在构建时声明、插件绑定 webpack。

Module Federation 2.0(@module-federation/runtime 与 @module-federation/core)将联邦能力独立为运行时库,不绑定单一打包器(支持 webpack/Rspack/Vite):提供动态远程(运行时注册与切换远程 URL,支持灰度)、增强的类型提示(自动生成远程模块类型)、以及更细粒度的共享策略(按模块级 shared)。工程价值:让"独立部署、运行时组合"的微前端更接近原生模块体验,同时支持将整个应用作为远程模块(MF 应用间嵌套)与版本平滑升级。注意点:共享库版本协商失败的重复实例、远程加载失败的降级、以及跨团队共享契约(exports 面)的治理。

本题考察微前端运行时组合机制:先讲清 1.0 的共享作用域与远程 chunk 模型,再对比 2.0 的"运行时化、打包器无关、动态远程"演进。回答应覆盖版本协商与降级治理,体现工程落地的完整视角。

#
★★★

18. Turbopack 在 Next.js 15(Turbopack 稳定版)

Next.js 15 中 Turbopack 稳定版提供了什么?在开发与构建中如何使用并做工程取舍?

  • Turbopack 在 Next.js 15 的稳定范围(dev/build)
  • 启用方式(--turbopack)与回退机制
  • 构建性能收益与兼容边界(配置项、插件)

Next.js 15 把 Turbopack 标记为稳定:dev 与 production build 均可通过 next dev --turbopack / next build --turbopack 启用(15.3 起 build 也默认支持),官方目标是让 Turbopack 成为默认打包器,提供更快的冷启动、热更新与生产构建,并改进内存占用。其增量图与持久化缓存在大型 App Router 项目中的收益最明显:改一处源码只重编译受影响模块,HMR 几乎瞬时。

工程取舍:收益是构建时间下降(大型项目可显著缩短 CI 与本地等待)、缓存复用;代价是兼容边界——部分 webpack 配置(自定义 plugin、部分 loader)与较冷门插件在 Turbopack 下不可用或行为不同,需要按官方兼容性矩阵核对;部分依赖 webpack 内部 API 的第三方包需要回退。实践建议:先在小范围(新功能模块或 staging)启用,监控构建产物与线上行为差异,遇到不兼容点切回 webpack 并记录原因,同时关注 Next.js 版本升级中 Turbopack 兼容列表的扩张。

本题考察框架默认构建器的切换时机:要点是"稳定不等于全兼容"。回答应给出启用方式、收益量化维度与回退策略,体现对框架路线图与工程风险的平衡判断。

#
★★★

19. Rspack 2.1.x(Rust 实现、兼容 Webpack 生态)

Rspack 2.1.x 作为 Rust 实现的打包器如何兼容 Webpack 生态?迁移到 Rspack 的收益与风险是什么?

  • Rspack 对 webpack 配置与 loader/plugin 体系的兼容设计
  • 构建性能提升的来源(Rust 并行、增量缓存)
  • 迁移盘点清单与产物验证策略

Rspack 2.1.x 由字节跳动开源,用 Rust 实现 webpack 的核心打包链路,目标是"drop-in 替代":兼容 webpack 的配置项(resolve、optimization.splitChunks、experiments 等)、loader 体系(通过 JS loader 支持,绝大多数 webpack loader 可直接使用)与插件 API(提供 webpack-compatible 的 Tapable 钩子),同时内置基于 SWC 的转译与压缩,常用场景可直接替换 webpack,构建速度提升数倍到十倍,并支持内置持久化缓存。

迁移收益:构建与 CI 提速、内存占用下降、开发 HMR 更快;风险点:依赖 webpack 内部 API 的第三方插件(compiler/compilation 深度钩子、自定义 runtime 修改)可能行为差异;loader 链中依赖 this.getOptions、pitch 语义的细节需回归;Module Federation、HMR 与 source map 的兼容层需要验证。迁移策略:先用兼容性盘点清单(配置文件、loader 列表、插件列表)逐项标记,切换后以"双构建金丝雀"对比产物语义(chunk 内容、CSS 提取、source map 映射),而非只比较构建耗时,再逐步扩大灰度范围。

本题考察打包器替代的落地方法论:Rspack 的卖点是"生态兼容 + Rust 性能"。回答应说明兼容机制(JS loader/插件桥接)、性能来源,并把迁移风险落到可执行的盘点与双构建验证流程上。

#
★★★

20. 浏览器 ESM、动态 import() 与依赖图构建

浏览器原生 ESM、动态 import() 与依赖图构建是如何工作的?对前端构建与加载有什么影响?

浏览器原生 ESM 以

本题考察浏览器原生模块机制与打包器的关系:核心是"依赖图"概念在浏览器侧由运行时建立、在构建侧由打包器建立,两者通过产物格式衔接。回答应覆盖加载时序、按需分块与瀑布请求治理,体现端到端视角。

#
★★★

21. Vite 的环境变量 import.meta.env 与类型生成 在大型团队的可维护性

Vite 的环境变量机制(import.meta.env)与类型生成如何提升大型团队的可维护性?有哪些最佳实践?

  • import.meta.env 的静态替换语义与 .env 文件体系
  • vite/client 与 env.d.ts 类型增强的协作
  • 大型团队的环境变量治理(命名、校验、文档化)

Vite 通过 .env 文件体系(.env、.env.development、.env.production 及模式文件)提供环境变量,只有 VITE_ 前缀的变量会暴露给客户端,代码中以 import.meta.env.VITE_XXX 访问,构建时被静态替换为字面量,因此使用位置必须静态可读。类型生成方面,vite/client 提供 import.meta.env 的基础类型,团队可在 src/vite-env.d.ts 中用 ImportMetaEnv 接口声明自定义变量的类型,让 IDE 提示与编译检查覆盖所有环境变量引用。

大型团队可维护性最佳实践:一是"单一来源",把环境变量清单、默认值与说明维护在受版本控制的环境示例文件(.env.example)中;二是"类型先行",为每个变量声明类型并区分必填/可选,缺失时在启动脚本或插件中校验;三是"命名收敛",用统一前缀与分组命名(如 VITE_API_、VITE_FEATURE_)避免命名漂移;四是"禁止运行时读取",把 import.meta.env 的使用收敛到配置模块,避免散落各处导致环境切换时行为不一致。

本题考察工程化协作细节:重点是"编译期替换"对使用方式的约束,以及类型声明、示例文件、命名与收敛四类治理手段。回答体现对"小机制、大协作"的理解,能区分 VITE_ 前缀与 Node 侧环境变量的边界。

// src/vite-env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_BASE: string;
  readonly VITE_FEATURE_SSR?: string;
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}
#
★★★

22. vite-plugin-legacy/@vitejs/plugin-legacy 在传统浏览器兼容的工程取舍

@vitejs/plugin-legacy 如何实现传统浏览器兼容?使用它需要做哪些工程取舍?

  • 插件生成 legacy 产物与 polyfill 注入的机制
  • 双产物加载策略(nomodule 回退)与性能代价
  • 兼容目标、体积与维护成本的取舍

@vitejs/plugin-legacy 通过 babel 把现代产物转译出 legacy 版本(ES2015 及以下),并基于 browserslist 目标生成对应的 polyfill 包(SystemJS + core-js),在 HTML 中同时输出

本题考察浏览器兼容策略的工程化:机制上要讲清"双产物 + nomodule 回退 + polyfill",取舍上要量化体积与维护成本并给出目标驱动的决策方法。回答体现对渐进增强与成本收益的理解。

#
★★★

23. Vite 的 defineConfig/define 与环境变量 在大型项目构建链的工程价值

Vite 的 defineConfig 与 define 配置在大型项目构建链中有什么工程价值?与环境变量如何配合?

  • defineConfig 的类型提示、工具函数(loadEnv)与配置组织
  • define 的编译期常量替换语义及与 import.meta.env 的关系
  • 大型项目多环境、多入口的配置治理

defineConfig 提供类型提示与 IDE 校验,并支持接受函数(接收 env 参数)或返回 Promise,配合 loadEnv 读取指定前缀的环境变量,让构建链在启动阶段就能拿到环境信息(如 API 地址、部署模式)注入配置。define 用于定义编译期替换的全局常量(如 APP_VERSION、process.env.NODE_ENV 的替代),替换发生在构建期,产物中直接是字面量,因此适合注入"构建时已知、运行时不变"的信息,性能上优于运行时读取。

大型项目的工程价值:把"构建链上下文"集中治理——通过模式(mode)与 .env 文件区分 dev/test/staging/prod,用 define 统一注入版本号、提交 hash、特性开关等元信息,避免代码里散落 magic string;defineConfig 的组织上可用数组/函数拆分公共配置与场景配置(如 base、build.target、experimental),配合环境变量矩阵生成多平台产物。注意 define 的键必须是完整静态标识符、值会被 JSON 序列化或函数替换,误用字符串拼接会导致替换失效。

本题考察构建配置的工程化组织:defineConfig 是"配置入口"、define 是"编译期注入"、环境变量是"上下文来源",三者构成大型构建链的配置骨架。回答应强调编译期替换语义与多环境治理,体现对配置即代码的理解。

#
★★

24. Turbopack 在 Next.js 15 的工程边界与性能取舍

Turbopack 在 Next.js 15 中可用性的工程边界是什么?性能收益与取舍如何权衡?

  • 可用范围(dev/build、App/Pages Router)与配置限制
  • 性能收益的测量维度(冷启动、HMR、构建时长)
  • 不兼容特性清单与回退决策

Turbopack 在 Next.js 15 中 dev 与 build 均已稳定,但"稳定"指核心链路,工程边界仍在:部分 webpack 配置项与插件(如自定义 webpack plugin 深度钩子、部分 loader)不兼容或需适配;对 legacy Pages Router 的某些高级特性支持不及 App Router 完整;第三方库若依赖 webpack 特定行为可能异常。启用前应核对官方兼容矩阵,把项目实际使用的配置与插件逐项过一遍。

性能取舍:收益主要在构建时长与 HMR 延迟(增量缓存 + Rust 并行),适合大型应用与频繁构建的 CI;代价是迁移适配成本与少数场景的行为差异。实践上建议以数据驱动决策:先小规模试点,用同版本 Next.js 分别测量 dev 冷启动、热更时延与 CI build 时长,并做产物级验证(页面渲染、路由、SSR 行为一致),收益不达阈值或风险不可控时保留 webpack 模式。

本题考察对新特性"可用性边界"的判断力:既要说清收益(构建性能),也要量化边界(配置兼容、路由模式差异)。回答应给出"核对矩阵 + 数据对比 + 回退机制"的可执行流程。

#
★★

25. esbuild 在依赖预打包(Vite prebundle)的工程价值

esbuild 在 Vite 依赖预打包(prebundle)中承担什么角色?预打包解决了哪些工程问题?

  • Vite prebundle 的目标(ESM 化、依赖扁平化、请求收敛)
  • esbuild 作为预构建引擎的速度与能力边界
  • 预打包对开发与生产构建一致性的影响

Vite 的依赖预打包(prebundle)用 esbuild 把 node_modules 中的依赖转换为浏览器可直接加载的 ESM 格式并聚合:一是解决"裸模块说明符浏览器无法解析"的问题;二是把 CJS 依赖转换为 ESM(生成 interop 包装);三是依赖扁平化,让多个包共享的同一依赖(如 react)只保留一份实例,避免双实例;四是合并大量小依赖为少数 chunk,减少浏览器请求数。整个过程在 dev server 启动时基于 lockfile 内容执行,结果缓存在 node_modules/.vite,lockfile 不变即复用缓存。

工程价值:把"构建期才有的模块系统兼容工作"前置到预构建阶段,让源码侧可以放心地裸导入 npm 包;esbuild 的 Go 实现使预构建通常在数百毫秒到数秒内完成,冷启动体验接近无感。注意事项:预构建产物与生产构建(Rollup/Rolldown)的转换语义可能略有差异(如 CJS interop 细节),当依赖升级或 lockfile 变更时需触发重新预构建(Vite 会检测并在 dev 中自动重启);对无法正确转换的依赖可用 optimizeDeps.exclude 排除。

本题考察 Vite 架构的关键一环:prebundle 解决"开发期也能像构建期一样使用 ESM"的问题。回答应覆盖四个目标(ESM 化、扁平化、请求收敛、CJS interop),并指出缓存与一致性边界。

#
★★

26. Rspack 的 experiments.outputModule 与 ESM 产物 在大型团队的可维护性

Rspack 的 experiments.outputModule 与 ESM 产物对大型团队有什么可维护性价值?启用时有哪些注意事项?

  • experiments.outputModule 开启原生 ESM 产物的语义
  • ESM 产物在 tree-shaking、格式一致性上的优势
  • 启用条件(target、CJS 依赖、浏览器支持)与迁移注意点

experiments.outputModule 让 Rspack(以及 webpack 的对应 experiments)输出原生 ESM 产物(

本题考察"产物格式即工程质量"的视角:ESM 产物带来的可维护性来自静态语义与生态对齐。回答要讲清机制(experiments.outputModule)、价值(tree-shaking、缓存、一致性)与迁移风险(CJS interop、浏览器兼容)。

#
★★

27. Bun/Deno 与 Node 在打包链中的互操作边界(模块解析算法、内置模块、lockfile 格式差异)

Bun、Deno 与 Node 在打包链中的互操作边界是什么?模块解析算法、内置模块与 lockfile 格式差异会带来哪些问题?

  • 三者的模块解析算法差异(Node 目录解析、Bun 的增强解析、Deno 的 URL 解析)
  • 内置模块 API 差异(Node 内置模块在 Bun/Deno 中的兼容度)
  • lockfile 格式(package-lock/pnpm-lock/bun.lock)与工具链互操作

Node 采用经典目录解析(node_modules 逐层查找 + 扩展名/目录 index 回退),Bun 兼容 Node 解析并增强(如 TS/JSX 直接执行、JSON import、显式扩展名提示),Deno 则默认 URL 化解析(npm: 说明符、裸名需 import map),三者对"同一段依赖代码"的解析结果可能不同,导致同一项目在不同运行时行为差异。内置模块方面:Bun 覆盖大部分 Node API,但部分内部实现(如某些 fs 细节、worker 行为)有差异;Deno 的 Node 兼容层(node: 前缀)仍在完善,部分内置模块缺失或行为不同。

lockfile 差异是团队协作的实际痛点:npm 生成 package-lock.json(lockfileVersion 演进)、pnpm 用 pnpm-lock.yaml(含 importer 与 snapshot 结构)、Bun 用 bun.lockb/bun.lock(二进制或新文本格式),混用时需要转换或重建,且各自的依赖提升策略不同会导致 node_modules 布局不同,进而影响打包器的解析顺序。治理建议:全团队锁定单一包管理器与 lockfile 格式(经 packageManager 字段强制),跨运行时工具链以 Node 为基准做兼容验证,必要时用 CI 矩阵同时跑 Node/Bun 确认行为一致。

本题考察多运行时时代的兼容治理:解析算法、内置模块、lockfile 三个维度都会破坏"换运行时即可跑"的假设。回答应点出各运行时差异的具体表现,并落到"锁定工具链 + 兼容验证"的治理手段。

#
★★

28. SWC 在代码转译(Babel 替代)的工程性能与生态取舍

SWC 作为 Babel 的替代方案在代码转译上有什么性能与生态取舍?何时适合切换?

  • SWC 的 Rust 实现带来的转译性能优势
  • 与 Babel 生态(插件、preset)的兼容差异
  • 切换评估维度(TS/JSX 转译、polyfill、自定义变换)

SWC 用 Rust 实现解析与转译,单文件转译速度通常比 Babel 快一个数量级,且支持并行转译,在大型项目中能把整体转译时间从分钟级降到秒级;它内置 TS、JSX、装饰器等现代语法支持,并提供 minify(swc 压缩)能力,可替代 babel-loader + terser 的组合。生态方面:SWC 提供官方 swc-loader、@swc/core API 与 playground,社区插件生态远小于 Babel,Babel 的"魔法变换"插件(如 babel-plugin-import、自定义 AST 变换)大多无法直接复用,需要改写为 SWC 插件(相对较少)或在特定文件上保留 Babel。

取舍建议:以"标准转译为主"的项目(TS/JSX/现代语法 + preset-env 级别的兼容)适合切换到 SWC 获得性能收益;依赖大量自定义 Babel 插件或需要细粒度 AST 变换的项目保留 Babel;也可混合使用——SWC 做常规转译、Babel 仅处理特定插件场景。切换前用"产物对比 + 构建耗时对比"量化收益,并回归测试 polyfill 注入(SWC 通过 env 配置注入 core-js 与 Babel useBuiltIns 对应)。

本题考察转译器选型:核心是"性能换生态"的权衡。回答应量化 SWC 的性能优势、说明生态差异的具体表现(插件不可复用、preset 对应关系),并给出按项目类型分类的切换建议。

#
★★

29. Vite 6+ 的 optimizeDeps 在依赖预构建的工程配置边界

Vite 6+ 中 optimizeDeps 的工程配置边界是什么?include/exclude 等选项在什么场景下使用?

  • optimizeDeps.include 强制预构建的适用场景
  • optimizeDeps.exclude 的副作用与使用前提
  • 预构建与依赖发现(detect)机制的边界

Vite 默认在 dev 启动时扫描并预构建直接依赖,但依赖发现机制有边界:动态 import 的依赖、通过变量间接引用的依赖、或某些只在特定分支使用的依赖可能未被扫描到;此时用 optimizeDeps.include 显式声明强制预构建,避免运行时"依赖优化器重新加载"的抖动。exclude 则把依赖排除出预构建,适用于:无法被 esbuild 正确转换的依赖(如依赖动态 require 语义、需要原生加载)、或希望依赖保持原始格式被浏览器直接加载的场景。

配置边界还包括:optimizeDeps.entries 自定义扫描入口、optimizeDeps.holdUntilCrawlEnd 控制等待爬取完成、以及 force 强制重新预构建(配合缓存目录 node_modules/.vite)。工程注意点:include 声明应保持与 lockfile 同步(依赖升级后需更新);exclude 会放弃 ESM 化与扁平化收益,可能引入浏览器无法解析的裸模块或增加请求数;预构建行为在 dev 与 build 之间的一致性需通过依赖的真实产物验证,避免出现"dev 正常、build 报错"的落差。

本题考察预构建的精细治理:include/exclude 是两条相反的边界控制手段。回答应讲清各自的适用场景(扫描遗漏 vs 转换失败)与代价(抖动 vs 失去优化),体现对 dev 启动机制细节的掌握。

#
★★

30. Vite 6+ 的 build.rollupOptions 在自定义打包配置的现代实践

Vite 6+ 中 build.rollupOptions 如何自定义打包行为?在现代工程中有哪些典型实践?

  • rollupOptions 覆盖的维度(入口、输出、插件、manualChunks)
  • 与 Vite 高阶配置(build.*)的配合与优先级
  • 现代实践:manualChunks 治理、多入口、插件注入

build.rollupOptions 直接把 Rollup 的配置透传给生产构建:input 定义多入口、output 控制产物格式与命名(如 entryFileNames、chunkFileNames、assetFileNames 的 hash 规则)、plugins 注入构建期插件(如自定义产物处理)、manualChunks 做依赖分组。Vite 提供 build.* 快捷配置(target、minify、cssCodeSplit 等),rollupOptions 用于表达 Rollup 特有或需要精细控制的配置,二者合并后传给底层引擎(Vite 6+ 中为 Rollup,Vite 8 起为 Rolldown 兼容层)。

现代实践:一是用 manualChunks 把体积大、更新少的第三方库拆成稳定 chunk 以优化缓存命中;二是多入口构建(如 MPA 或多页面应用)在 input 中声明所有页面;三是通过 rollupOptions.plugins 注入产物后处理(如压缩、hash 重写、环境标记);四是用 output.generatedCode、output.sanitizeFileName 等控制产物细节。注意 manualChunks 在 Rolldown 时代的兼容性差异需回归验证,且过度拆分反而增加请求数与内存开销,应基于产物分析(bundle analyzer)数据决策。

本题考察生产构建的定制入口:rollupOptions 是"Rollup 能力透传层"。回答应覆盖 input/output/plugins/manualChunks 四个维度,并强调现代实践中的数据驱动(基于产物分析做拆分决策),避免拍脑袋配置。

#
★★

31. Bun 1.2 作为运行时与打包器相较 Node.js 的工程取舍

Bun 1.2 作为运行时与打包器相比 Node.js 有什么工程取舍?什么场景适合引入?

  • Bun 的运行时兼容度(Node API 覆盖)与性能来源
  • Bun 内置打包器(bun build)与 esbuild 的关系
  • 引入 Bun 的迁移成本与生态风险

Bun 1.2 是自带运行时、打包器、包管理器与测试器的"一体化工具链":运行时基于 JavaScriptCore 与原生实现,启动快、IO 与脚本执行性能通常优于 Node;它对 Node API 的覆盖度很高(大部分内置模块可用),但仍有边缘差异(如某些 fs 流细节、worker 行为、第三方原生模块的预编译兼容),生产服务与依赖原生模块(node-gyp 编译)的包是主要风险点。打包器方面,bun build 兼容 esbuild 的 API 设计(bun build --bundle),适合快速打包 CLI/单文件产物。

工程取舍:收益是工具链统一(install/build/test/run 一条龙)与性能;代价是生态迁移成本——CI 镜像、原生模块、监控与部署平台的兼容,以及团队知识储备。建议场景:新项目、以纯 JS/TS 为主的脚本与工具链、对启动速度敏感的 CLI;不建议的场景:重度依赖 Node 原生模块生态或需要严格 Node 行为保证的核心服务。稳妥路径是在工具链层面先引入(如 bun install/run 脚本),运行时层面的替换用灰度验证。

本题考察新运行时引入的决策框架:既要认可性能与一体化价值,也要量化兼容风险。回答应区分"工具链替换"与"运行时替换"两个层面,给出分场景建议,体现工程上的渐进引入思维。

#
★★

32. Nitro 2(Nuxt 3 底层)在跨平台部署的工程价值

Nitro 2(Nuxt 3 底层)是什么?它在跨平台部署上有哪些工程价值?

  • Nitro 的 server engine 定位与预设(preset)体系
  • 跨平台部署目标(Node、Serverless、Edge、容器)的产物适配
  • 与 Nuxt/独立使用场景的集成方式

Nitro 是 Nuxt 3 的底层服务端引擎,也是一个独立的全栈服务框架:它把应用打包成"与平台无关"的 server bundle,通过预设(preset)体系为不同部署目标生成适配产物——Node 服务器、Vercel/Netlify/Cloudflare Workers/Deno Deploy 等 Serverless 与 Edge 平台、Docker 容器、以及 nitro 自有 dev 运行时,同一份代码可切换目标部署而无需改动业务逻辑,这是其跨平台部署的核心价值。

工程价值:一是"一次编写、多处部署"的抽象层,配合运行时配置(env、路由、存储驱动)适配不同平台;二是自动处理服务端与静态产物的混合(SSR + 静态化)、增量缓存(如 ISR 类似的 SWR 缓存)、以及路由与中间件体系;三是与 Nuxt 深度集成(Nuxt 的 server/api 目录直接由 Nitro 承载),也可独立用于纯 API 服务。注意点:不同平台的运行时限制(Edge 无 Node API、Serverless 无长连接)需要代码层面遵守平台契约,预设之间的行为差异需在 CI 中按目标平台做集成测试。

本题考察服务端框架的部署抽象:核心是"preset 预设体系把平台差异封装在构建产物层"。回答应说明 Nitro 的定位、跨平台机制(预设)、与 Nuxt 的关系,并提醒平台契约差异需要测试保障。

#
★★

33. Vite 6+ 的 SSR 配置(ssr.noExternal、ssr.target)的工程应用

Vite 6+ 的 SSR 配置(ssr.noExternal、ssr.target)在工程中如何应用?各解决什么问题?

  • ssr.noExternal 控制依赖是否打进 SSR bundle 的语义
  • ssr.target 指定 SSR 产物的运行环境(node/webworker)
  • 依赖处理策略对 SSR 性能与兼容性的影响

ssr.noExternal 决定哪些依赖在 SSR 构建中"不打成 external"(即打入 bundle 而非要求运行时 require):默认情况下 Node 侧依赖(如 express)走 external 保持原生 require,而需要经过 Vite 转换的依赖(含 ESM-only、含 JSX/TS、依赖别名或浏览器 API 的包)应列入 noExternal 以统一转换;配置为 true 表示全部打进 bundle。当依赖只提供 ESM 且 Node 版本不支持、或依赖内部引用 import.meta.env 等 Vite 特性时,不列 noExternal 会直接运行时报错。

ssr.target 指定 SSR 产物面向的运行环境:'node'(默认,产物含 Node 兼容封装)或 'webworker'(面向服务端 worker 如 Cloudflare Workers 场景)。工程应用:用 noExternal 收敛"需要转换的依赖白名单"保证 SSR 运行一致性;用 external 列表(ssr.external)排除不应打包的依赖(如原生模块、体积过大的工具库);配合 optimizeDeps 与 ssrLoadModule 做按需转换。注意 SSR 产物中 Node 内置模块(fs/path)不应被打进 bundle,需依赖 external 机制保持 require 引用。

本题考察 SSR 构建的依赖治理:noExternal/external 本质是"哪些依赖由 Vite 转换打包、哪些保持运行时 require"的分界。回答要结合具体触发场景(ESM-only 依赖、Vite 特性依赖)说明配置动机,并提到 ssr.target 的环境适配。

#
★★

34. Vite 6+ 的 resolve.alias 与 resolve.extensions 在路径解析的工程实践

Vite 6+ 的 resolve.alias 与 resolve.extensions 在路径解析中如何配置?有哪些工程实践要点?

  • resolve.alias 的别名映射机制($ 结尾精确匹配、正则)
  • resolve.extensions 的扩展名解析顺序与性能影响
  • 别名与打包器、SSR、类型解析的一致性治理

resolve.alias 把导入路径映射到实际路径,常用来定义 @/src、@components 等业务别名,也用于替换库实现(如把某个包指向本地 fork);支持 $ 后缀实现精确匹配(仅匹配完整路径)、支持正则与数组(多候选回退)。resolve.extensions 定义自动补全扩展名的尝试顺序(默认 ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']),按顺序逐个尝试直到命中;顺序影响解析结果——同名文件存在时靠前扩展名优先,也影响解析耗时。

工程实践:一是别名统一收敛——把别名清单维护在共享配置并同步到 tsconfig paths 与编辑器设置(jsconfig),避免"Vite 能解析、TS 报错"或 IDE 无法跳转;二是别名用于目录级路径(@/components)而避免过度精细的别名(每个模块一个),减少维护成本;三是 extensions 顺序保持默认并在项目内统一(含 .mjs/.mts 等现代扩展),避免不同工具解析不一致;四是注意 alias 对 SSR 同样生效,但排除 node_modules 内部解析(alias 默认仅作用于源码导入,可用 find 参数控制)。

本题考察路径解析配置的"一致性治理":alias 与 extensions 本身简单,难点在于让 Vite、TS、编辑器三处解析结果一致。回答应覆盖语法要点($ 精确匹配、顺序语义)与工程化手段(共享配置、同步 tsconfig)。

#
★★

35. Tree-shaking 在 ESM 与 CJS 的工程差异与 sideEffects 字段的应用

Tree-shaking 在 ESM 与 CJS 中的效果有什么差异?sideEffects 字段如何帮助摇树?

  • ESM 静态导出与 CJS 动态导出的摇树前提差异
  • sideEffects: false 的语义与误用风险
  • 打包器对纯函数、具名导出的分析与限制

Tree-shaking 依赖静态分析:ESM 的 import/export 在编译期确定,打包器能精确判定"未使用的导出"并删除;CJS 的 module.exports 是运行时赋值的动态对象,打包器无法可靠判断哪些属性被使用,通常只能做保守的整模块保留或启发式分析(如 webpack 对 CJS 的部分分析),因此同样代码 CJS 形态的摇树效果远差于 ESM——这也是"新库必须发 ESM"的原因之一。

sideEffects 字段告诉打包器"该模块/包的执行是否产生副作用":声明 sideEffects: false 后,若某模块的导出全部未被使用,打包器可安全地整模块删除;sideEffects: ["./src/polyfill.js"] 形式可保留指定副作用文件。工程应用:库作者声明 sideEffects: false 能显著提升使用方的摇树率;但误用风险极高——若模块顶层代码实际有副作用(如全局注册、CSS 导入、原型扩展)却声明 false,会被错误删除导致运行时 bug。实践上应基于真实副作用精确声明,或用数组白名单粒度到文件,并在发布前用打包器产物验证。

本题考察摇树机制的深层差异:ESM 静态 vs CJS 动态决定摇树上限,sideEffects 是"信任声明"机制。回答应说明两者差异的根源、sideEffects 的正确用法与误用风险,体现对"声明与真实行为一致性"的工程纪律。

{
  "name": "my-lib",
  "sideEffects": ["./src/polyfill.js", "**/*.css"]
}
#
★★

36. Vite 6+ 的 CSS Modules、PostCSS 与 CSS 预处理器(Sass)

Vite 6+ 如何支持 CSS Modules、PostCSS 与 CSS 预处理器(Sass)?它们在工程中如何协作?

  • CSS Modules 的启用方式(*.module.css)与类型支持
  • PostCSS 插件链(autoprefixer、tailwind)的配置机制
  • Sass/Less 预处理器与 Vite 内置 preprocessorOptions 的协作

Vite 对 CSS 开箱即用:以 .module.css/.module.scss 命名的文件自动启用 CSS Modules(类名经 hash 作用域化,JS 侧获得映射对象,配合 vite/client 或 css-modules 类型声明获得 TS 类型);普通 .css 直接引入并注入产物;PostCSS 通过 postcss.config.js 或 vite.config 的 css.postcss 配置插件链(如 autoprefixer 补全浏览器前缀、tailwindcss v4 的 @tailwindcss/postcss、postcss-preset-env),Vite 会为每个 CSS 文件跑 PostCSS 处理。

预处理器方面:安装 sass(或 less/stylus)后,.scss/.less 文件被 Vite 内置流程编译(底层用 postcss 前先经预处理),可通过 css.preprocessorOptions.scss.additionalData 注入全局变量/mixin、配置 api 版本(legacy/modern-compiler)等。工程协作要点:Modules 负责作用域隔离、PostCSS 负责转换与兼容、预处理器负责语法增强,三者分层不冲突;注意全局样式(reset、design token)用普通 css 引入,业务样式用 Module;Sass 的 @use/@forward 与 Vite 的解析配合(含 alias 解析 node_modules 中的 sass 库),且生产构建 CSS 会做压缩与代码分割(cssCodeSplit)。

本题考察 Vite CSS 体系的层次:Modules(作用域)、PostCSS(转换)、预处理器(语法)三层职责分明。回答应按层说明机制与配置入口,并给出"全局样式与业务样式分层、类型声明、additionalData"等实践要点。

#
★★

37. 代码分割(dynamic import)的 chunk 命名与缓存策略的工程应用

基于 dynamic import 的代码分割如何做 chunk 命名?hash 命名与缓存策略如何配合?

  • dynamic import 生成 chunk 的命名规则(magic comments / entryFileNames)
  • contenthash 与缓存命中(长期缓存、CDN)的关系
  • chunk 命名对产物可读性与故障排查的影响

代码分割通过 dynamic import 让打包器把异步模块拆成独立 chunk;命名上 Webpack 用 magic comments(/* webpackChunkName: "detail" */),Vite/Rollup 用 output.chunkFileNames 模板(如 assets/[name]-[hash].js,name 取 async 块或函数名)。合理命名让产物可读、便于定位线上故障文件;hash 策略上使用 contenthash(内容哈希)——内容不变文件名不变,从而被浏览器与 CDN 长期缓存,内容变化只使受影响 chunk 失效,是"缓存优先"的代码分割组合。

工程应用:一是稳定命名 + 内容哈希,保证可排查性与缓存命中率;二是把"基本不变的第三方依赖"与"频繁迭代的业务代码"分开(配合 manualChunks/splitChunks),避免业务变更导致 vendor chunk 缓存失效;三是 hash 长度与唯一性权衡(短 hash 减小体积但碰撞风险高);四是配合 HTTP 缓存头(immutable + 长 max-age)让 hash 文件名获得最大缓存收益,HTML 自身短缓存以保证入口可更新。注意点:服务端渲染场景下 chunk 文件需可被 SSR 引用,命名模板要同时被客户端与 SSR 产物使用。

本题考察代码分割与缓存的联动设计:chunk 命名解决"可读性",contenthash 解决"缓存命中率"。回答应把 magic comments、文件名模板、内容哈希与 HTTP 缓存头串成一条完整链路,体现工程闭环思维。

#
★★

38. Vite 6+ 的 legacy 插件在老旧浏览器兼容的现代实践

Vite 6+ 使用 legacy 插件兼容老旧浏览器的现代实践是什么?如何控制兼容成本?

  • @vitejs/plugin-legacy 的 targets 与双产物机制
  • 兼容范围评估(用户画像、数据驱动)
  • 移除/收窄 legacy 的时机与方法

现代实践的核心是"数据驱动收窄兼容范围":先用 analytics 与 caniuse 数据评估目标用户浏览器的真实占比与版本分布,再设置准确的 targets(如 'chrome >= 87'、'safari >= 14'),避免为极低占比的远古浏览器支付双份产物体积与构建时间成本。legacy 插件按 targets 用 babel 转译并注入对应 polyfill,通过 nomodule 回退加载,实践中可进一步用现代特性检测(如不支持原生 ESM 才加载 legacy)精细控制。

控制成本的实践:一是区分"语法降级"与"polyfill 注入",按需配置(renderLegacyChunks 控制是否生成 legacy 异步 chunk);二是 legacy 产物仅服务真正需要的浏览器,同时用现代产物提供最佳性能;三是建立"兼容预算"——监控 legacy 产物体积占比,随旧浏览器占比下降逐步收窄 targets;四是定期评审(季度数据回顾)决定是否整体移除 legacy 支持,切换后可通过入口加载监控(nomodule 请求量)验证无用户受损。注意 legacy 与 modern 的 SRI、缓存头需分开配置。

本题考察兼容策略的现代化治理:从"全量降级"转向"数据驱动的预算制"。回答应体现"评估-配置-监控-收窄"的闭环流程,而不是机械地配置插件。

#
★★

39. Vite 6+ 的 import.meta.glob 在动态资源加载的工程应用

Vite 6+ 中 import.meta.glob 在动态资源加载(图片、音频、组件等)上有哪些工程应用?

  • glob 模式匹配多种资源类型与 query 定制
  • 懒加载资源(?url、?raw、?worker)与构建产物映射
  • 动态资源目录化治理与性能平衡

import.meta.glob 可匹配任意文件资源,并通过 query 定制导入语义:默认得到模块导入函数(懒加载);加 { eager: true } 同步获取;配合查询参数 ?url 得到资源 URL、?raw 得到原始字符串、?worker 得到 worker 构造器,还可用 import: 'default' 选取导出字段。这让"目录化资源"(图标库目录、素材目录、语言包目录)能以声明式方式批量接入构建产物,每个文件按需或按规则打包,避免手写 import 清单的遗漏与重复。

工程应用:一是图标/图片目录批量引用(配合组件封装按名称渲染);二是主题/语言包按需加载(懒加载 + eager 双模式组合);三是按目录约定批量注册路由与组件;四是配合后端约定目录做动态页面装配。注意事项:glob 在构建期静态扫描,新增文件需重启/重新构建;查询参数组合(如 { query: '?url&raw' } 不可同时用多个资源语义)有限制;对超大目录应配合 exclude 与分区 glob 控制构建产物规模,避免把无关文件全部打进产物。

本题考察 glob 的资源导入能力矩阵:重点是 query 参数对导入语义的定制(url/raw/worker)与懒加载组合。回答应给出典型应用场景并提醒构建期静态性与目录规模治理。

#
★★

40. Source maps 在生产环境(hidden source maps)

生产环境中的 source maps 如何配置?hidden source maps 解决什么问题?

  • source map 类型(source-map、hidden-source-map、nosources)的差异
  • 生产部署错误定位与源码保护(不暴露源码)的权衡
  • 与错误监控平台(Sentry 等)的上传协作

生产环境使用 source map 的核心矛盾是"可调试性与源码安全性":dev 用 eval 等快速类型即可;生产常用 hidden-source-map——产物中不含 //# sourceMappingURL 注释(浏览器不会主动加载映射),map 文件单独上传到错误监控平台(如 Sentry、Bugsnag),报错堆栈在平台侧用 map 还原为源码位置,线上用户拿不到源码映射,兼顾定位效率与源码保护。nosources-source-map 则只保留位置映射、剥离源码内容,进一步防泄露。

工程实践:一是构建产物与 map 分离部署(map 不进 CDN 公共目录),map 文件加入访问控制;二是 CI 中把 map 上传到监控平台并清理(或保留短期);三是配合 source map 的 includeContent: false 减少 map 体积;四是验证"还原准确性"——用生产样本堆栈在监控平台检查符号化结果。注意点:hidden 模式若上传失败会失去定位能力,应把上传纳入 CI 门禁;对极致安全要求可用 nosources,但会牺牲变量名还原。

本题考察生产可观测性的基础设施细节:hidden source maps 是"定位能力与源码安全"的平衡解。回答应覆盖类型对比、部署分离、平台上传协作与安全权衡,体现对线上故障定位链路的完整理解。

#
★★

41. Vite 6+ 的依赖升级(vite deps)与自动 lockfile 更新的工程实践

Vite 6+ 的依赖升级与 lockfile 自动更新有哪些工程实践?如何避免升级引入的构建问题?

  • vite deps 相关命令与依赖缓存失效机制
  • lockfile 更新(升级版本)与预构建缓存的关系
  • 升级后的回归验证(产物对比、预构建重建)

Vite 的依赖预构建结果缓存在 node_modules/.vite,缓存键基于 lockfile 内容与配置:依赖版本变化时缓存失效并自动重建。工程实践上,依赖升级后应确认预构建已触发重建(dev 启动日志或手动 vite optimize),否则可能出现"lockfile 已更新但缓存还是旧依赖"的异常(Vite 会在检测到锁文件变化时自动失效,必要时用 --force 强制重建)。vite deps 相关命令(如 vite optimize)可显式触发预构建与审计。

自动更新 lockfile 的工程实践:一是用 Dependabot/Renovate 生成依赖升级 PR,PR 中应包含 lockfile 变更与预构建影响的评估(体积、版本兼容);二是升级 CI 中增加"构建产物对比"步骤——升级前后产物 hash diff 过大时人工审查;三是对构建关键依赖(打包器、转译器、样式工具)设置独立的升级节奏与回归测试;四是维护"升级回退预案":lockfile 变更可快速 revert,配合固定版本与变更记录,避免升级后的构建问题阻塞发布。

本题考察依赖变更的工程化管控:核心是"lockfile 变更"与"预构建缓存、构建行为"的联动。回答应覆盖缓存失效机制、自动升级 PR 流程与升级后验证,体现对变更风险的全链路管理。

#
★★

42. Vite 6+ 与 Vitest 单元测试的工程协作与配置复用

Vite 6+ 与 Vitest 如何协作?测试配置与构建配置的复用与隔离如何设计?

  • Vitest 复用 Vite 配置(resolve、plugins、alias)的机制
  • test 专用配置的覆盖与隔离(test 字段、环境)
  • 单元测试与构建链共享依赖图带来的行为一致性

Vitest 直接构建在 Vite 之上:默认读取 vite.config 中的 resolve.alias、plugins、css 等配置,让测试代码与业务代码使用完全相同的模块解析与转换链路,避免"构建正常、测试解析失败"的双轨差异;vitest 的 test 字段(环境 jsdom/node、include、setupFiles、coverage)独立配置,不会污染构建,配置文件中用 defineConfig(vitest/config)同时声明两者。jsdom/happy-dom 模拟浏览器 DOM,node 环境用于纯逻辑与 SSR 侧测试。

工程协作要点:一是把公共配置(alias、plugins)留在根级,test 相关放 test 字段,实现"共享而不耦合";二是用 workspace/projects(Vitest workspace)按包或按环境拆分测试项目;三是性能协作——Vitest 复用 Vite 的依赖预构建与模块图缓存,冷启动快,且可与构建链共享同一份配置避免漂移;四是注意部分构建插件(如压缩、产物处理)应通过 apply 或条件判断排除在测试链路外,避免测试环境执行构建期插件。

本题考察"同一配置心智模型"的工程价值:Vitest 与 Vite 共享模块图是最大优势,风险在插件污染测试链路。回答应说明复用机制、test 配置隔离与性能协作,并指出需要排除的构建期插件。

#

43. Vite 切换 Rolldown 后,依赖 Rollup 插件钩子顺序、this.getModuleInfo、virtual module 或 manualChunks 的插件可能出现哪些兼容差异

Vite 切换 Rolldown 后,依赖 Rollup 插件钩子顺序、this.getModuleInfo、virtual module 或 manualChunks 的插件可能出现哪些兼容差异?

  • Rolldown 对 Rollup 插件钩子执行顺序与语义的重现差异
  • this.getModuleInfo 返回信息与 virtual module 行为差异
  • manualChunks 在 Rust 内核下的实现差异与回归验证

Rolldown 以"Rollup 兼容"为目标,但兼容是渐进过程,部分插件会出现差异:一是钩子执行顺序——Rolldown 的 Rust 管线对 resolveId/load/transform/buildEnd 等钩子的调用次序、并行粒度与重入规则可能与 Rollup 不同(尤其 moduleParsed、renderChunk 等阶段边界),依赖"钩子顺序假设"的插件(如先 load 后 transform 的时序、buildEnd 的触发次数)行为可能漂移;二是 this.getModuleInfo 的返回结构差异——某些字段(如 meta、导入导出信息、ast 相关)在 Rolldown 中的填充时机与内容可能不完整或不同,插件若据此做深度判断需适配;三是 virtual module 的 \0 前缀约定与模块 id 解析在 Rolldown 中的边界处理(如该模块是否进入依赖图、是否被摇树)可能有差异;四是 manualChunks 的对象/函数两种形态与 chunk 归属算法不同,可能导致 chunk 拆分结果与 Rollup 不一致。

治理方法:升级后对依赖这些 API 的插件做"行为对比测试"——同一输入分别在 Rollup 与 Rolldown 下运行,对比钩子调用日志、getModuleInfo 输出与 chunk 产物;官方维护的兼容清单逐项核对;对差异项用插件适配层(条件分支)兼容两引擎,并纳入 CI 金丝雀。

本题考察引擎迁移的兼容性工程:问题点集中在"钩子时序、API 返回结构、虚拟模块与 chunk 算法"四个面。回答应具体列出差异面并给出"行为对比 + 适配层 + 金丝雀"的验证方法。

#

44. Vite 6/7 中依赖预构建使用 esbuild、生产构建使用 Rollup 时,CJS 动态 require、条件导出和同一依赖的双实例问题应如何定位,并保证 dev 与 build 行为一致

Vite 6/7 中依赖预构建用 esbuild、生产构建用 Rollup 时,CJS 动态 require、条件导出和双实例问题如何定位?如何保证 dev 与 build 行为一致?

  • 两引擎对 CJS 动态 require 与条件导出的处理差异
  • 双实例(dual package)在 dev/build 的触发面差异
  • 保证 dev/build 一致性的验证与配置手段

Vite 6/7 的架构是"开发期预构建用 esbuild、生产构建用 Rollup",两个引擎对同一依赖的处理可能不同:esbuild 预构建把 CJS 转 ESM 时对动态 require(字符串拼接、条件 require)只能做保守处理(保留 require 调用或转成运行时兼容代码),Rollup 生产构建对 CJS 依赖的转换与 interop 语义(default/命名导出映射、循环依赖处理)可能不同;条件导出(exports 的 import/require 分支)在两个引擎中的命中顺序与缓存方式也可能不一致,导致同一依赖 dev 与 build 行为不同。

定位方法:先把问题归类——"解析差异"(用 engines 的模块解析日志对比 resolveId 结果)、"互操作差异"(对比 dev 产物与 build 产物中该依赖的包装代码)、"实例差异"(用全局注册表或 symbol 标记检测实例数,配合 instanceof 与共享状态检查);再用最小复现剥离干扰。保证一致的手段:一是把 dev 预构建与 build 的依赖处理策略对齐(optimizeDeps 的 include/exclude 与 build 的 noExternal/external 配置成互补关系,避免"dev 打了包、build 走了 external"造成格式不一致);二是对问题依赖统一入口格式(强制单一格式加载或用 alias 指向统一构建产物);三是 CI 中增加"dev/build 行为一致性测试"(同一依赖在两种模式下的导出形状与实例标识断言)。

本题考察双引擎架构下的一致性工程:核心是"差异可定位 + 配置可对齐 + 测试可断言"。回答应分别列出 CJS 动态 require、条件导出、双实例的差异面,再给出三层次治理手段。

#

45. 从 Webpack 迁移到 Rspack 2.1.x 时,如何盘点自定义 loader、compiler/compilation hooks、Module Federation、HMR 与 source map 的兼容层,并设计双构建金丝雀验证产物语义而非只比较构建耗时

从 Webpack 迁移到 Rspack 2.1.x 时,如何盘点自定义 loader、compiler/compilation hooks、Module Federation、HMR 与 source map 的兼容层?如何设计双构建金丝雀验证?

  • 五类兼容面(loader、hooks、MF、HMR、source map)的盘点方法
  • 双构建金丝雀的设计(产物语义对比而非耗时对比)
  • 迁移完成判定与回退机制

盘点兼容层时按五类逐项建立清单:一是自定义 loader——核对 loader 使用的 API(this.getOptions、this.emitFile、pitch 语义、异步回调),在 Rspack 的 loader 支持矩阵中逐项验证;二是 compiler/compilation hooks——对用到 tapable 钩子(compile、make、emit、afterEmit 等)的插件标注触发时机差异,Rspack 对部分深度钩子的实现不完全一致;三是 Module Federation——核对 shared 配置、运行时 API 与 remote 加载行为(Rspack 提供 MF 兼容能力,但细节需回归);四是 HMR——自定义热更新逻辑(module.hot.accept 边界、Fast Refresh 集成)与 Rspack 的 HMR runtime 对齐;五是 source map——对比 devtool 各模式(eval/source-map/cheap)下产物映射的正确性,尤其是 loader 转换后的映射。

双构建金丝雀设计:对同一提交分别用 webpack 与 Rspack 构建,对比维度按"语义优先"排列——入口与 chunk 的模块内容(归一化后 diff)、依赖图结构(哪些模块进了哪些 chunk)、CSS 提取结果、运行时行为(加载顺序、全局变量、exports 形状)、source map 可还原性;构建耗时只作参考不作通过标准。产物 diff 为空的模块范围即为可信区间,差异项转入人工审查;CI 中持续跑金丝雀直至 diff 归零或差异项全部解释并被测试覆盖,才允许全量切换,并保留 webpack 分支作为回退。

本题考察迁移验证的方法论:盘点(五类兼容面)+ 金丝雀(语义 diff)+ 判定(diff 归零/差异可解释)。回答要突出"不要只比耗时"的核心纪律,以及把差异项转测试覆盖的闭环。

#

46. Tailwind 4.x 中用 logical-property utilities 替换 left/right、margin-left 等物理属性时,如何验证 LTR/RTL、vertical writing mode 与旧浏览器降级,并防止组件库仍输出相互冲突的物理属性

Tailwind 4.x 中用 logical-property utilities 替换物理属性时,如何验证 LTR/RTL、vertical writing mode 与旧浏览器降级?如何防止组件库与业务代码输出冲突的物理属性?

  • logical properties(ms-/me-/ps-/pe- 等)与 writing mode 的对应关系
  • 双向与竖排文本下的验证方法与降级策略
  • 组件库与业务代码中物理/逻辑属性混用的冲突治理

Tailwind 4.x 提供完整的 logical property utilities:ms-/me-(margin-inline-start/end)、ps-/pe-(padding)、start-/end-(inset-inline)以及 text-start/text-end 等,它们在 LTR 下映射到左/右,在 RTL 与 vertical writing mode(writing-mode: vertical-rl)下自动翻转方向,实现"一套样式适配多书写方向"。验证方法:在测试环境切换 dir="rtl" 与 writing-mode 建立视觉回归(Playwright 截图对比),覆盖典型组件(表单、导航、弹窗)与极端场景(长文本、混合方向内容);对不支持 logical properties 的旧浏览器,可用 Tailwind 的 fallback 机制或 PostCSS 插件生成物理属性回退,或限定支持的浏览器范围。

防止冲突的治理:一是约定层——组件库与业务代码统一只使用 logical utilities(lint 规则禁止 left/right/margin-left 等物理属性,如 eslint-plugin-tailwindcss 或 stylelint 检查);二是输出层——构建产物中用插件/检查器扫描冲突(同一元素同时出现物理与逻辑方向属性时告警);三是设计令牌层——间距/定位语义化命名(inline-start 而非 left),从源头消除方向假设;四是在组件库测试中内置 LTR/RTL 双向快照,任何回归都会被 CI 拦截。

本题考察多方向文本的工程治理:从"替换"到"验证"再到"防冲突"是完整闭环。回答应覆盖 logical utilities 映射、双向验证方法、旧浏览器降级与 lint/输出层治理。

#

47. 如何在 Tailwind 4.x 用 @custom-variant/@variant 表达 data-state、supports 和容器状态,同时统一原生 scrollbar-* 与 WebKit scrollbar 样式,并通过生成 CSS 检查控制变体爆炸和跨浏览器回退

Tailwind 4.x 中如何用 @custom-variant/@variant 表达 data-state、supports 与容器状态?如何统一原生与 WebKit scrollbar 样式并控制变体爆炸?

  • @custom-variant 定义自定义变体的机制(data-*/supports/容器查询)
  • 原生 scrollbar-color 与 ::-webkit-scrollbar 的统一封装
  • 生成 CSS 检查(变体数量、回退策略)的工程手段

Tailwind 4.x 用 CSS 优先的配置:@custom-variant 定义自定义变体,如 @custom-variant data-active ([data-state="active"] &)、@custom-variant supports-grid (@supports (display: grid) &)、以及容器状态变体(@container 与 @custom-variant with-container),使类名(如 data-active:bg-blue-500)在生成 CSS 时展开为对应选择器;@variant 则用于在自定义 CSS 中复用变体逻辑。这样把"状态驱动样式"以变体形式声明式表达,避免手写选择器散落。

scrollbar 统一方面:现代浏览器用原生 scrollbar-color/scrollbar-width,WebKit 用 ::-webkit-scrollbar 伪元素,可用一个 @utility 或组件类同时输出两套规则(@supports 分支包裹原生属性 + ::-webkit-scrollbar 块),实现"一套样式、双引擎适配"。控制变体爆炸:@custom-variant 的组合会指数增长生成量,应通过 build 后检查(生成 CSS 体积、变体数量统计脚本)、限制组合层数、用 @theme 收敛语义化令牌,以及 CSS 层(@layer)组织来治理;跨浏览器回退用 @supports 与浏览器目标列表约束,必要时提供无变体兜底类。

本题考察 Tailwind 4 的高级定制:@custom-variant 是状态样式的声明式入口,scrollbar 统一是双引擎适配的代表场景。回答应说明变体机制、统一封装方法与生成量治理,体现对"配置即产出"的理解。

#

48. esbuild 的 transform/build API 与插件在单文件转译与 bundle 产物的性能取舍

esbuild 的 transform 与 build API 分别用于什么场景?插件在单文件转译与 bundle 产物中的性能取舍如何?

  • transform 与 build 的语义差异(单文件转译 vs 整体打包)
  • 插件系统(onResolve/onLoad)在两种模式下的能力边界
  • 按场景选型(转译工具、构建工具、按需加载)

esbuild 提供两个入口:transform 对"单段代码"做纯转译(TS/JSX/压缩),输入是字符串或文件内容,不解析 import 依赖,适合做"代码转换器"(如自定义 loader、按需转译服务、编译器前端);build 则进行完整打包——解析依赖图、应用 loader 链、bundle 与代码分割,输出产物文件,是 esbuild 作为"构建工具"的入口。transform 快且轻(无依赖图开销),build 功能全但开销大。

插件方面:build 支持完整插件体系(setup + onResolve/onLoad/onEnd,可自定义解析与加载、虚拟模块、外部化),transform 本身不经过插件系统(插件钩子主要在 build 的解析/加载阶段生效),因此"插件化定制"属于 build 场景。性能取舍:单文件高频转译(每次请求都转译)用 transform 避免依赖图与缓存开销;需要别名、外部化、多格式产物用 build 并利用其缓存(--watch、增量 rebuild API);工程上常见组合是"transform 做转译内核、build 做打包壳",Vite 预构建即用 build API 打包依赖,而源码按需转换用 transform 语义。选型依据是"是否需要依赖图与产物管理"。

本题考察 esbuild 双 API 的定位:transform 是"无图转译",build 是"带图打包",插件属于 build 体系。回答应对比语义、场景与性能,并给出组合使用的方式,体现对工具 API 设计意图的理解。