标签

Babel

Babel(以前称为 6to5)是一个 JavaScript 编译器,它将 ES6+/ES2015 代码转换为 ES5 代码。

Babel
服务端5月29日 23:47
如何优化 Babel 编译性能?哪些配置最有效?优化 Babel 编译性能,优先做三件事:少编译、用缓存、少插件。少编译就是用 `include` 精准限定源码目录,不要把整个 node_modules 丢给 Babel;用缓存就是开启 babel-loader 的 `cacheDirectory` 和配置层的 `api.cache(true)`;少插件就是只保留必要转换,能交给 esbuild/SWC 的纯语法转换就别强行走 Babel。 再往下才是 targets、polyfill 和并行。`@babel/preset-env` 的 targets 越准确,Babel 做的无用转换越少;`useBuiltIns: 'usage'` 可以减少 polyfill 体积;大型 Webpack 项目可配合 thread-loader,但小项目并行成本可能比收益还高。 ## 追问 ### 为什么 include 往往比 exclude 更稳? exclude 容易漏掉特殊目录,include 是白名单,只编译明确需要处理的源码和少数第三方包,性能和可控性都更好。 ### cacheDirectory 能解决什么问题? 它会缓存 babel-loader 的转换结果,二次构建只处理变更文件。开发环境收益最大,常能把重复编译时间降很多。 ### targets 配错会怎样? 目标浏览器写得太旧,Babel 会做大量降级和 polyfill;写得太新,旧环境可能跑不起来。性能优化不能牺牲兼容性。 ### 什么时候用 SWC 或 esbuild 替代 Babel? 如果只是 JSX、TS 或 ES 语法转换,SWC/esbuild 更快。如果依赖 Babel 插件生态,比如自定义 AST 插件,仍然要用 Babel。 ## 写段代码 ```javascript module.exports = api => { api.cache(true); return { presets: [['@babel/preset-env', { targets: '>0.5%, not dead', modules: false }]] }; }; ```
服务端5月29日 23:47
Babel 如何接入 Webpack、Vite 和 Rollup?Babel 接入构建工具的核心思路是:让构建工具负责文件扫描、依赖图和打包,Babel 只负责把指定源码转换成目标语法。Webpack 通常用 `babel-loader`,Vite 默认走 esbuild,只有需要 Babel 插件时才通过 React 插件或 Babel 插件接入,Rollup 用 `@rollup/plugin-babel`,库开发还要特别处理 helpers。 关键不是“能不能接”,而是“什么时候该接”。如果只是语法降级,Vite/esbuild 或 SWC 往往更快;如果要 decorators、宏、React 特定转换、自定义 AST 插件,才值得引入 Babel。 ## 追问 ### Webpack 里 Babel 放在哪一层? 放在 module rules 的 loader 阶段。Webpack 匹配 js/jsx/ts/tsx 文件后交给 babel-loader,Babel 根据 preset 和 plugin 输出新代码。 ### Vite 为什么默认不依赖 Babel? 因为 Vite 开发阶段追求速度,默认用 esbuild 做转译。只有遇到 Babel 生态里的插件能力,比如 legacy decorators,才需要补 Babel。 ### Rollup 里 babelHelpers 怎么选? 应用打包可用 `bundled`,helpers 直接打进产物。库开发更推荐 `runtime`,并把 `@babel/runtime` 设为 external,避免每个包重复注入 helpers。 ### 实际项目里容易踩什么坑? 最常见是把整个 node_modules 都交给 Babel,构建变慢。更稳的做法是用 include 精准指定 src 和少数需要转译的第三方包。 ## 写段代码 ```javascript // webpack.config.js module.exports = { module: { rules: [{ test: /\.[jt]sx?$/, include: /src/, use: { loader: 'babel-loader', options: { cacheDirectory: true } } }] } }; ```
服务端5月29日 23:47
什么是 Babel AST?如何用它写自定义插件?Babel AST 是源码解析后的抽象语法树,Babel 插件本质上就是“遍历这棵树,找到目标节点,再改掉它”。完整流程是:parser 把代码转成 AST,traverse 按 visitor 访问节点,插件通过 path.remove、replaceWith、insertBefore 等方法修改节点,最后 generator 再生成代码。 写插件时不要直接乱改 node,优先用 path,因为 path 带着父节点、作用域、替换/删除能力。比如删掉 console.log,要访问 CallExpression,判断 callee 是否是 console.log,然后 remove。 ## 追问 ### AST 和普通字符串替换有什么区别? 字符串替换只看文本,容易误伤注释、变量名或字符串。AST 能理解语法结构,比如只删除真正的函数调用,不会碰到字符串里的 `console.log`。 ### visitor 为什么按节点类型写? 因为 Babel 遍历 AST 时会按节点类型触发回调。你关心函数调用就写 CallExpression,关心变量名就写 Identifier。 ### path.scope 有什么用? 它用于处理作用域绑定,比如判断变量是否在当前作用域声明、重命名变量、生成不冲突的临时变量。写复杂插件时这是避免变量污染的关键。 ### 项目里最容易踩什么坑? 最常见的是替换节点后又被继续遍历,导致重复处理。可以在必要时用 `path.skip()`,或者给新节点加标记避免二次转换。 ## 写段代码 ```javascript module.exports = ({ types: t }) => ({ name: 'remove-console-log', visitor: { CallExpression(path) { const c = path.node.callee; if ( t.isMemberExpression(c) && t.isIdentifier(c.object, { name: 'console' }) && t.isIdentifier(c.property, { name: 'log' }) ) path.remove(); } } }); ```
服务端5月29日 01:38
Babel 是什么?它的编译流程和 polyfill 方案有哪些?Babel 是 JavaScript 转译器,将 ES6+ 语法转为向后兼容的 ES5 代码,确保在旧浏览器中正常运行。编译流程分三步:parse(@babel/parser 将源码转 AST)→ transform(插件遍历 AST 并修改节点)→ generate(@babel/generator 将 AST 还原为代码)。注意 Babel 只转换语法,不补齐新 API(如 Promise、Array.includes),需要 polyfill 方案配合。 ## 追问 **Babel 的 plugin 和 preset 是什么关系?** plugin 是最小转换单元(如 @babel/plugin-transform-arrow-functions),preset 是插件集合(如 @babel/preset-env 包含所有 ES6+ 转换插件)。执行顺序:plugins 先于 presets,plugins 正序执行,presets 逆序执行。 **@babel/preset-env 的工作原理是什么?** 根据 browserslist 配置的目标环境,按需引入转换插件和 polyfill,避免对已支持语法的多余转换。配合 useBuiltIns: 'usage' 可实现按需注入 polyfill。 **polyfill 方案有哪些?core-js 和 transform-runtime 有什么区别?** core-js 直接在全局注入缺失 API,适合应用项目;@babel/plugin-transform-runtime 将 polyfill 抽离为模块引用(不污染全局),适合库开发,还能避免 helpers 重复打包。 **Babel 能替代 Webpack 吗?** 不能。Babel 只做语法转译,不做模块打包、代码分割、Tree Shaking、资源处理,这些是 bundler 的职责。两者配合使用。 ## 写段代码 ```javascript // babel.config.js module.exports = { presets: [ ['@babel/preset-env', { targets: '> 0.25%, not dead', useBuiltIns: 'usage', corejs: 3 }] ], plugins: ['@babel/plugin-transform-runtime'] }; ```
服务端5月28日 02:34
Babel 中 preset 和 plugin 的区别是什么?如何配置?## 核心区别 **Plugin 是 Babel 转换的最小单元,Preset 是 Plugin 的集合。** 打个比方:Plugin 是单品菜,Preset 是套餐。`@babel/plugin-transform-arrow-functions` 只做一件事——把箭头函数转成普通函数;而 `@babel/preset-env` 是一份根据你的目标环境自动搭配的套餐,内部打包了几十个 Plugin。 这个区别决定了三件事: 1. **粒度不同**——Plugin 精确到单个语法转换,Preset 按场景批量组合 2. **配置方式不同**——Plugin 放 `plugins` 数组,Preset 放 `presets` 数组 3. **执行顺序不同**——Plugin 先于 Preset 执行;多个 Plugin 从前往后,多个 Preset 从后往前 ## 配置方式 ### Plugin 配置 ```javascript // babel.config.js module.exports = { plugins: [ // 无参数 '@babel/plugin-transform-arrow-functions', // 带参数,用数组包裹 ['@babel/plugin-transform-runtime', { corejs: 3, helpers: true }] ] }; ``` 单独使用 Plugin 的场景不多,通常只在 Preset 覆盖不到时补充,比如自定义转换逻辑或处理实验性语法。 ### Preset 配置 ```javascript // babel.config.js module.exports = { presets: [ // 带参数配置 ['@babel/preset-env', { targets: '> 0.25%, not dead', useBuiltIns: 'usage', corejs: 3 }], // 无参数,直接写字符串 '@babel/preset-react' ] }; ``` 三个常用 Preset 的职责: | Preset | 作用 | |--------|------| | `@babel/preset-env` | 根据目标环境自动选择需要的转换插件 | | `@babel/preset-react` | 处理 JSX 语法 | | `@babel/preset-typescript` | 处理 TypeScript 语法 | ## 执行顺序:面试高频追问 执行顺序是这道题最常被追问的点,记住三条规则: 1. **Plugin 先于 Preset 执行** 2. **多个 Plugin 按声明顺序从前到后执行** 3. **多个 Preset 按声明顺序从后到前执行** ```javascript module.exports = { plugins: [ 'plugin-a', // 第 1 个执行 'plugin-b' // 第 2 个执行 ], presets: [ 'preset-b', // 第 4 个执行(逆序) 'preset-a' // 第 3 个执行 ] }; // 实际顺序:plugin-a → plugin-b → preset-a → preset-b ``` **Preset 为什么逆序?** 这不是设计失误,而是实用考量。在 Babel 6 时代,Stage Preset 按提案阶段编号命名(stage-0 到 stage-3),stage-0 包含所有提案语法,stage-3 只包含最成熟的。逆序执行意味着写在后面的 Preset 先跑,这样 `presets: ['stage-3', 'stage-0']` 中 stage-0 先执行(包含最多),stage-3 后执行(覆盖最成熟的部分),符合"先宽后窄"的直觉。Babel 7 虽然废弃了 Stage Preset,但逆序规则保留了下来。 ## preset-env 的两个关键配置 `@babel/preset-env` 是日常使用最多的 Preset,其中 `useBuiltIns` 和 `corejs` 两个参数经常被问到。 ### useBuiltIns 控制如何注入 polyfill: - `false`:不注入,需要手动引入 `@babel/polyfill`(已废弃) - `'entry'`:在入口文件 `import 'core-js'` 处,根据 targets 替换为精确的 polyfill - `'usage'`:按需注入,代码中用到了哪个 API 就自动引入对应的 polyfill ### corejs 指定 core-js 版本: ```javascript ['@babel/preset-env', { targets: { chrome: '80' }, useBuiltIns: 'usage', corejs: 3 // 必须显式声明,否则 polyfill 不生效 }] ``` `corejs: 3` 对应 core-js@3,支持更多 API(如 `Array.flat`、`Object.fromEntries`)。如果设为 2,很多新 API 的 polyfill 不会注入。 ### useBuiltIns 的局限与 plugin-transform-runtime `useBuiltIns: 'usage'` 注入的 polyfill 是模块级别的,会污染全局作用域。在开发库(library)时,这会影响使用方项目的全局环境。`@babel/plugin-transform-runtime` 解决了这个问题——它将 polyfill 以引用方式注入,不污染全局: ```javascript // useBuiltIns: 'usage' 的输出(污染全局) require("core-js/modules/es.array.includes.js"); [1, 2, 3].includes(2); // plugin-transform-runtime 的输出(不污染全局) var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes")); (0, _includes.default)([1, 2, 3]).call([1, 2, 3], 2); ``` 简单原则:**应用项目用 `useBuiltIns`,库项目用 `plugin-transform-runtime`**。 ## 自定义 Plugin 和 Preset ### 自定义 Plugin Plugin 本质是一个返回 visitor 对象的函数,通过 AST 访问者模式实现转换: ```javascript module.exports = function(babel) { const { types: t } = babel; return { name: 'remove-console-plugin', visitor: { CallExpression(path) { const callee = path.node.callee; if ( t.isMemberExpression(callee) && callee.object.name === 'console' ) { path.remove(); } } } }; }; ``` ### 自定义 Preset Preset 是返回 Plugin 数组的函数: ```javascript module.exports = function() { return { plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3 }], 'remove-console-plugin' ] }; }; ``` 团队内可以将通用配置封装为自定义 Preset,在不同项目中复用。 ## 面试常见追问 **Q:Babel 的编译流程是什么?** Babel 的编译分为三步:**parse**(将源码转成 AST)→ **transform**(Plugin 在这步遍历并修改 AST)→ **generate**(将修改后的 AST 生成目标代码)。Plugin 和 Preset 都作用于 transform 阶段。 **Q:如何查看 Babel 实际使用了哪些 Plugin?** 在终端执行 `npx babel --debug your-file.js`,或在代码中设置环境变量 `DEBUG=babel*` 运行构建,可以看到每个 Plugin 的加载和执行情况。 **Q:Babel 7 相比 Babel 6 在配置上有哪些变化?** 三个主要变化:所有包统一到 `@babel` 作用域下(`babel-preset-env` → `@babel/preset-env`);废弃 Stage Preset,实验性语法需单独安装 Plugin;`@babel/polyfill` 被废弃,改用 `core-js` + `useBuiltIns` 的组合。
服务端5月28日 01:54
@babel/preset-env 是如何工作的?useBuiltIns 选项有什么区别?## @babel/preset-env 到底做了什么? 面试中被问到 Babel 配置,preset-env 几乎是必考项。很多人能说出 useBuiltIns 有三个值,但说不清它们背后的处理逻辑——这恰恰是区分"用过"和"理解"的分界线。 先给结论:@babel/preset-env 的核心职责只有一件事——根据你声明的目标环境,自动决定需要哪些语法转换插件和 polyfill。你不用再手动罗列 @babel/plugin-transform-arrow-functions、@babel/plugin-transform-classes 这一个个插件了。 ## 它是怎么知道该用哪些插件的? preset-env 的工作依赖一个关键数据源:@babel/compat-data。这个包维护了一份"哪些浏览器版本支持哪些 ES 特性"的映射表,数据来源于 mdn-browser-compat-data 和 electron-to-chromium。 当你配置了 targets: ```javascript // babel.config.js module.exports = { presets: [ ['@babel/preset-env', { targets: { browsers: ['> 1%', 'last 2 versions', 'not dead'], node: 'current' }, useBuiltIns: 'usage', corejs: 3 }] ] }; ``` 处理流程是这样的: 1. **解析 targets**:通过 browserslist 将 `> 1%` 这类查询语句转成具体的浏览器版本列表,比如 chrome 90, firefox 88, safari 14 2. **查询 compat-data**:对每个 ES 特性,查表判断目标环境是否已原生支持 3. **选择插件**:不支持的特性,启用对应的转换插件。比如目标环境不支持可选链,就加上 @babel/plugin-proposal-optional-chaining 4. **处理 polyfill**:这一步由 useBuiltIns 选项控制,下面详细说 如果没有指定 targets,preset-env 不会做任何转换——等价于没配。这是个常见的坑。 ## useBuiltIns 三个选项到底有什么区别? 这是面试最核心的问题,也是实际配置中最容易搞混的地方。 ### false — 不处理 polyfill 默认值。preset-env 只做语法转换,不管 polyfill。你需要自己手动在入口文件引入全量 core-js: ```javascript import 'core-js/stable'; import 'regenerator-runtime/runtime'; ``` 缺点很明显:不管你用没用到 Array.prototype.includes,全量包里都有它。包体积大,而且会污染全局原型。只在不需要 polyfill 的场景下有用——比如你的目标环境已经全部支持 ES2020+。 ### entry — 按目标环境过滤,替换入口导入 你需要在入口文件写一行 `import 'core-js/stable'`,preset-env 会把它替换成目标环境实际需要的模块列表: ```javascript // 你写的 import 'core-js/stable'; // 转换后(假设目标是 chrome 90) import 'core-js/modules/es.array.flat'; import 'core-js/modules/es.object.from-entries'; // ... 只包含 chrome 90 不支持的特性 ``` entry 的过滤维度只有一个:目标环境缺什么就补什么。但它不考虑你的代码实际用了哪些 API——哪怕你一行 Promise 都没写,只要目标环境不支持 Promise,polyfill 就会包含进去。 ### usage — 按代码实际使用按需引入 usage 是最精确的模式。它会扫描你的源代码,找到实际使用的 API,然后只引入对应的 polyfill: ```javascript // 你写的 const arr = [1, 2, 3]; arr.includes(2); const p = Promise.resolve(1); // 转换后 import 'core-js/modules/es.array.includes'; import 'core-js/modules/es.promise'; var arr = [1, 2, 3]; arr.includes(2); var p = Promise.resolve(1); ``` 包体积最小,也不需要手动写 import。但 usage 有一个需要注意的问题:**它不会为 node_modules 里的依赖注入 polyfill**。如果你的第三方库用到了某些新 API 但自身没做 polyfill,运行时可能报错。这种场景下 entry 更安全。 ## 三个选项对比 | 选项 | 引入方式 | 包体积 | 全局污染 | 是否考虑实际使用 | 适用场景 | |------|----------|--------|----------|------------------|----------| | false | 手动全量引入 | 最大 | 是 | 否 | 极少使用 | | entry | 替换入口导入 | 中等 | 是 | 否(按环境过滤) | 应用开发,依赖多且未编译 | | usage | 按需自动注入 | 最小 | 是 | 是(按代码扫描) | 应用开发(推荐) | 所有模式都会污染全局原型。如果你在开发库,不应该用 useBuiltIns。 ## 库开发为什么不用 useBuiltIns? 库的代码会被其他项目引用。如果库修改了全局的 Array.prototype.includes,而宿主项目也引入了相同 polyfill,就会冲突。更糟的是,宿主项目可能根本不想污染全局。 库开发的正确姿势是用 @babel/plugin-transform-runtime: ```javascript // babel.config.js module.exports = { presets: ['@babel/preset-env'], plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3, helpers: true, regenerator: true }] ] }; ``` 它通过引用 @babel/runtime-corejs3 里的辅助函数来实现 polyfill,不会触碰全局原型: ```javascript // 转换前 arr.includes(2); // 转换后 import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes"; _includesInstanceProperty(arr).call(arr, 2); ``` 代价是每个用到 polyfill 的文件都会引入一段辅助代码,包体积会比 usage 稍大。但对库来说,不污染全局是比体积更重要的约束。 ## 实际配置建议 **应用项目**:targets + useBuiltIns: 'usage' + corejs: 3,这是最省心的组合。如果第三方依赖有 polyfill 缺失问题,再切换到 entry。 ```javascript module.exports = { presets: [ ['@babel/preset-env', { targets: '> 1%, last 2 versions, not dead', useBuiltIns: 'usage', corejs: { version: 3, proposals: true } }] ] }; ``` **库项目**:preset-env 只管语法转换,polyfill 交给 transform-runtime。 ```javascript module.exports = { presets: [['@babel/preset-env', { targets: { node: 'current' } }]], plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3 }] ] }; ``` **调试方法**:加上 `debug: true` 可以在终端看到每个文件用了哪些插件和 polyfill,排查问题非常方便: ```bash # 或者用环境变量 DEBUG=@babel/preset-env npx babel src/index.js ``` ## 面试追问方向 - preset-env 不配 targets 会怎样?——什么都不转换,等于白装 - usage 模式下第三方依赖的 polyfill 怎么办?——entry 更安全,或者确保依赖自身做了 polyfill - corejs 配置为什么要写具体版本号?——写 `3` 会用最新版,可能导致构建不确定;写 `3.40` 可以锁定版本
服务端5月28日 01:45
Babel 的编译流程是怎样的?Babel 是 JavaScript 编译器,核心职责是将新版语法转换为向后兼容代码。整个编译流程分为三个阶段:解析、转换、生成。 ## 解析(Parsing) 解析阶段将源代码字符串转为抽象语法树(AST),分为两步: **词法分析(Lexical Analysis)**:将代码字符串拆分为 token 流。每个 token 是最小语法单元,如关键字、标识符、运算符、标点等。 ```javascript // 源代码 const age = 25; // 词法分析产生的 token 流 [ { type: 'Keyword', value: 'const' }, { type: 'Identifier', value: 'age' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '25' }, { type: 'Punctuator', value: ';' } ] ``` **语法分析(Syntactic Analysis)**:根据 token 流构建 AST,描述代码的层级结构和语义关系。 ```javascript // 箭头函数的 AST 简化表示 const add = (a, b) => a + b; // AST 核心结构 { type: "VariableDeclaration", declarations: [{ type: "VariableDeclarator", id: { type: "Identifier", name: "add" }, init: { type: "ArrowFunctionExpression", params: [ { type: "Identifier", name: "a" }, { type: "Identifier", name: "b" } ], body: { type: "BinaryExpression", operator: "+", left: { type: "Identifier", name: "a" }, right: { type: "Identifier", name: "b" } } } }] } ``` 对应包:`@babel/parser`(早期基于 Babylon,后合并入 Babel 官方维护)。 ## 转换(Transforming) 转换阶段是 Babel 的核心。它深度优先遍历 AST,通过插件对目标节点进行增删改操作。 **访问者模式(Visitor Pattern)** 是转换机制的基石。为每种 AST 节点类型注册访问者函数,遍历到该类型节点时自动触发。 ```javascript // 插件示例:箭头函数转普通函数 module.exports = function(babel) { const { types: t } = babel; return { visitor: { ArrowFunctionExpression(path) { const { node } = path; // 箭头函数体如果不是 BlockStatement,包装一层 const body = t.isBlockStatement(node.body) ? node.body : t.blockStatement([t.returnStatement(node.body)]); path.replaceWith( t.functionExpression( node.id, node.params, body, node.generator, node.async ) ); } } }; }; ``` **Path 对象**:不是节点的简单引用,而是节点在树中位置的完整描述。它提供 `replaceWith`、`remove`、`insertBefore` 等操作方法,还持有父节点、作用域信息,支持向上查找绑定。 **插件执行顺序**: - 插件按声明顺序从前往后执行 - Preset 按声明顺序从后往前执行(先声明的 preset 最后执行) - 同一节点可能被多个插件访问,先进入的插件修改后的结果会传给后续插件 **Preset 机制**:Preset 是插件的集合与配置快捷方式。`@babel/preset-env` 根据目标环境(browserslist 配置)自动引入所需的转换插件,避免手动逐个配置。 ## 生成(Generating) 将转换后的 AST 重新生成为代码字符串,同时可产出 Source Map。 生成器递归遍历 AST 节点,根据节点类型拼接对应的代码文本。例如遇到 `VariableDeclaration` 节点,输出 `var`/`let`/`const`,然后递归处理声明列表。 ```javascript // 转换前 const add = (a, b) => a + b; // 转换后 var add = function add(a, b) { return a + b; }; ``` 对应包:`@babel/generator`。通过 `sourceMaps: true` 选项可生成 Source Map,方便调试时映射回源码位置。 ## 完整流程 ``` 源代码 → @babel/parser → AST → @babel/traverse(插件) → 修改后AST → @babel/generator → 目标代码 + SourceMap ``` 实际上在进入三阶段之前,还有一个**配置阶段**:Babel 读取 `babel.config.js`、`.babelrc` 或 `package.json` 中的 babel 配置,加载并合并 plugins 和 presets,确定最终的转换规则链。 ## 面试常问要点 **Q:Babel 和编译器(如 V8)的区别?** Babel 只做语法转译(syntax transform),不涉及类型检查和代码执行。TypeScript 编译器既转译语法也做类型检查,V8 则将代码编译为机器码执行。 **Q:为什么 Babel 用 AST 而不是正则替换?** 正则无法理解代码语义,容易误替换(如字符串内的箭头符号)。AST 完整描述代码结构,能精确定位和修改目标节点,保证语义正确。 **Q:plugin 和 preset 的区别?** Plugin 是单个转换规则,preset 是插件集合。preset-env 按目标环境自动选择插件,preset-react 处理 JSX,preset-typescript 处理 TS 语法。
服务端5月28日 01:18
Babel 配置 TypeScript 和 React 的完整方案是什么?## Babel 如何编译 TypeScript 和 JSX? Babel 的编译流程是:解析(Parse)→ 转换(Transform)→ 生成(Generate)。`@babel/preset-typescript` 的工作方式是**直接剥离类型注解**,而非像 `tsc` 那样做类型检查后编译。`@babel/preset-react` 负责将 JSX 转换为 `React.createElement` 调用(React 17+ 使用新的 `jsx` 运行时自动导入)。 这意味着 Babel 只负责语法转译,**不做类型校验**。项目中需要额外运行 `tsc --noEmit` 来完成类型检查。 ## 基础配置方案 ### 安装依赖 ```bash # 核心依赖 npm install --save-dev @babel/core @babel/cli @babel/preset-env # TypeScript 支持 npm install --save-dev @babel/preset-typescript # React JSX 支持 npm install --save-dev @babel/preset-react # 运行时优化(减少重复 helper 代码) npm install --save @babel/runtime npm install --save-dev @babel/plugin-transform-runtime ``` ### babel.config.js 完整配置 ```javascript module.exports = { presets: [ ['@babel/preset-env', { targets: { browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'] }, useBuiltIns: 'usage', corejs: 3 }], '@babel/preset-typescript', ['@babel/preset-react', { runtime: 'automatic', // React 17+ 自动导入 jsx 运行时 development: process.env.NODE_ENV === 'development' }] ], plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3, helpers: true, regenerator: true }] ] }; ``` ### preset 执行顺序 Babel preset 的执行顺序是**从后往前**:先执行 `preset-react`(转译 JSX),再执行 `preset-typescript`(剥离类型),最后执行 `preset-env`(降级语法)。这个顺序保证了每一步拿到的代码都是合法的上一阶段输出。 ## Webpack 集成配置 ```javascript // webpack.config.js module.exports = { module: { rules: [ { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react' ] } } } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] } }; ``` ## tsconfig.json 关键配置 ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "jsx": "preserve", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "node", "isolatedModules": true, "noEmit": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` 核心要点: - `"jsx": "preserve"` 让 TS 保留 JSX 原样,由 Babel 负责转换 - `"noEmit": true` 让 TS 只做类型检查,不输出文件,编译交给 Babel - `"isolatedModules": true` 确保每个文件可独立转译,符合 Babel 单文件处理的工作模式 ## Babel vs tsc:为什么项目选择 Babel? | 对比项 | Babel | tsc | |--------|-------|-----| | 类型检查 | 不做 | 完整检查 | | 编译速度 | 快(单文件转译) | 较慢(全量类型分析) | | 语法降级 | 支持(preset-env + corejs) | 仅 target 降级 | | Polyfill | 按需注入(useBuiltIns) | 不提供 | | JSX 转换 | preset-react | jsx 选项 | | 插件生态 | 丰富(装饰器、路径别名等) | 有限 | 实际项目中通常**两者配合使用**:Babel 负责编译和 polyfill,`tsc --noEmit` 负责类型检查。 ```json // package.json { "scripts": { "type-check": "tsc --noEmit", "build": "npm run type-check && babel src --out-dir dist --extensions '.ts,.tsx'" } } ``` ## 高级配置 ### 环境区分 ```javascript // babel.config.js module.exports = { presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], env: { development: { plugins: ['react-refresh/babel'] }, production: { plugins: ['transform-remove-console'] }, test: { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }] ] } } }; ``` ### 路径别名 ```javascript // babel.config.js 添加 plugins: [ ['module-resolver', { root: ['./src'], alias: { '@': './src', '@components': './src/components', '@utils': './src/utils' } }] ] ``` ```json // tsconfig.json 同步配置 { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@utils/*": ["src/utils/*"] } } } ``` 路径别名必须同时在 Babel 和 TypeScript 两处配置,否则编译通过但类型检查报错。 ### 装饰器支持 ```bash npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties ``` ```javascript plugins: [ ['@babel/plugin-proposal-decorators', { legacy: true }], ['@babel/plugin-proposal-class-properties', { loose: true }] ] ``` ### CSS Module 类型声明 ```typescript // src/types/global.d.ts declare module '*.module.scss' { const classes: { readonly [key: string]: string }; export default classes; } declare module '*.css' { const content: { [className: string]: string }; export default content; } ``` ## 热更新配置 ```javascript // webpack.config.js const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); module.exports = (env) => { const isDev = env.NODE_ENV === 'development'; return { module: { rules: [{ test: /\.(ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { plugins: [isDev && 'react-refresh/babel'].filter(Boolean) } } }] }, plugins: [isDev && new ReactRefreshWebpackPlugin()].filter(Boolean) }; }; ``` ## 常见踩坑点 **类型检查和编译分离后忘记运行 tsc**:Babel 编译通过不代表类型正确,必须在 CI 中加入 `tsc --noEmit` 步骤。 **preset-react 的 runtime 选项**:`classic` 模式下每个 JSX 文件需要手动 `import React`,`automatic` 模式自动注入,React 17+ 项目务必使用 `automatic`。 **corejs 版本不匹配**:`@babel/preset-env` 的 `corejs` 选项必须与实际安装的 `core-js` 大版本一致(2 或 3),否则 polyfill 注入失败。 **路径别名不同步**:修改 `tsconfig.json` 的 `paths` 后必须同步修改 Babel 的 `module-resolver` 配置,否则运行时找不到模块。