前言
作为一名前端开发程序员,React 与 Vue 的选择自然是津津乐道的一环,无论是随意为之还是谨慎思量,我们总会在某个时间认真思考这个问题:我为什么选择先写 React/Vue?
后来,我慢慢了解到了两者在核心设计哲学上的差别,React 力图使用 JSX 这种语法扩展贯彻 All In JS 理念,而 Vue 更倾向于使用 v-if
,v-for
等指令增强 HTML 的能力。
了解到这里,我对两者都深入学习了一下,而关于 JSX 我了解到:虽然 JSX 这种语法糖看起来很像 HTML,但其实 在编译时被构建工具(如 Babel)转化成了 JavaScript 对象。
嗯?被转化为对象,那它具体是怎么转换的呢?让我们一步步往下了解:
正文
Babel 如何工作
Babel 的核心功能是 JavaScript 编译器 。主要负责将 依据最新 ECMAScript 标准编写的代码 ,以及像 JSX 这样的语法扩展,转换为 向后兼容的 JavaScript 代码,确保代码能在当前和旧版浏览器或环境中稳定运行。
其工作流程大致分为三个阶段:
1. 解析 | Babel 使用解析器(如 @babel/parser )将源代码转换成一种称为"抽象语法树"(AST)的数据结构,AST 是代码的树状表示,便于程序进行理解和操作 |
2. 转换 | 这是 Babel 的核心所在。它遍历 AST,并通过一系列 插件 plugins 和 预设 Presets 对 AST 进行修改。每个插件都负责转换一种特定的新语法。例如,@babel/plugin-transform-arrow-functions 会将箭头函数转换为普通的 function 表达式 |
3. 生成 | 转换完成后,Babel 使用代码生成器(如 @babel/generator )将修改后的 AST 转换回字符串形式的、兼容性强的 JavaScript 代码 |
预设如何工作
以 create-react-app 为例,CRA 默认使用一个名为 babel-preset-react-app
的预设配置,预设是配件的集合,提供一个经过精心策划和测试的 Babel 插件列表,可以理解为一个 插件工具包
全程由 Babel 核心引擎 @babel/core
驱动,流程如下:
1. CRA 配置 | 在 CRA 内部 Webpack 配置中,babel-loader 被指定使用 babel-preset-react-app 这个预设 CRA 在 Webpack 中配置 babel-loader,参见本文后面 Webpack 部分 |
2. Babel 引擎读取预设 | 当 Babel 准备编译你的代码时,它会去 node_modules 找到 babel-preset-react-app |
3. 加载插件列表 | Babel 执行 babel-preset-react-app 里的 JS 文件,从而动态生成一个配置对象,最关键的部分就是一个 plugins 数组 |
4. 定位并执行插件 | Babel 引擎拿到这个插件列表后,会根据列表中的名字(例如 @babel/plugin-transform-react-jsx ),再去 node_modules/@babel/ 文件夹下找到对应的插件包。然后,Babel 引擎会依次加载并执行这些插件,将每个插件的转换规则应用到你的源代码上。 |
多层预设带来的关注点分离与复用性
查看 babel-preset-react-app/package.json
源码 不难发现,此预设并未直接引用 @babel/plugin-transform-react-jsx
插件
这是因为 @babel/plugin-transform-react-jsx
这种与 React 相关的基础通用转换被包含在 @babel/preset-react
这个官方维护的预设里,babel-preset-react-app
只需要在此基础上新增其他的插件来聚焦其他需求即可
@babel/plugin-transform-react-jsx 源码分析
@babel/plugin-transform-react-jsx
插件会扫描代码,一旦找到 JSX 语法,就将其转换为常规 JavaScript 函数调用
工厂方法模式
工厂模式定义一个用于创建对象的接口,但由子类决定实例化的类是哪一个,从而使得一个类的实例化延迟到其子类,具体分为:简单工厂模式、工厂方法模式、抽象工厂模式,此插件遵循 工厂方法模式。
具体来说,对于此插件:
- 产品 :
React.createElement()
或_jsx()
调用的CallExpression AST
节点 - 抽象工厂 :由
createPlugin
函数返回的、具有visitor
接口的插件对象 - 具体工厂 :由
runtime
和development
的不同配置组合而成的不同模式 - 工厂方法 :
buildCreateElementCall
和buildJSXElementCall
等内部函数,根据当前配置执行对应逻辑
对于具体工厂:
-
runtime:
classic / automatic
,由版本控制,引导转换目标为React.createElement()
还是jsx()
-
development:
true / false
,由入口文件控制,实现 开发/生产分离,开发模式包含更多信息],生产环境则更精简
React 版本更迭带来的不同情况
React 17 前:转换为 React.createElement()
JSX 编译后被转换为函数 React.createElement()
,该函数创建一个描述 UI 的 JavaScript 对象(React Element),该对象是虚拟 DOM 的基本组成单位
JavaScript
// 源代码
const element = <h1 className="greeting">Hello, world!</h1>;
// Babel编译后
import React from 'react';
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React 17 及之后:转换为 jsx() / jsxs()
官方引入的新的转换机制会自动从 react/jsx-runtime
包中导入一个特殊的 jsx
或 jsxs
函数,开发者无需在每个使用 JSX 的文件中手动引入 React
以支持 React.createElement
的调用。
同时,由于从 引入主包 变为 引入特定包,编译产物的体积略微减小,做到了底层架构上广泛的性能优化。
该转换机制需要在 Babel 配置文件 babel.config.js
中设置 @babel/preset-react
的 runtime
为 automatic
,不过对于 CRA,该设置已默认启用。
javascript
// 同样的源代码
const element = <h1 className="greeting">Hello, world!</h1>;
// Babel编译后
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { children: 'Hello, World!' });
Babel 版本关键更迭
Babel 大版本从 6.x
到 7.x
是一次重大的架构升级,这点体现在多个方面:
- 包管理
7.x
版本的包管理方式由之前的 独立包 (babel-core
, babel-preset-es2015
) 变成了现在的 作用域包 (@babel/core
, @babel/preset-env
)。
这代表所有官方包统一出现在 @babel
命名空间下,有效解决了包名混乱和抢注的问题,提升了项目的规范性和可维护性。
- 核心预设
7.x
版本的核心预设由之前的 按年份划分 (preset-es2015
) 和 按阶段划分 (preset-stage-0
) 变成了现在的 @babel/preset-env
。
开发者现在不需要手动选择各种预设,Babel 会自动根据提供的 targets
(如 "> 0.25%, not dead"
或 { "browsers": ["last 2 versions"] }
)智能地按需引入必要的插件和 Polyfill,实现更小、更优化的打包体积。
- 配置文件
7.x
版本的核心预设由之前的 仅作用于单个包 的 .babelrc
变成了现在的 可作用于整个项目 babel.config.js
,这对于 Monorepo 项目尤其重要。
- 性能
Babel 7 对转换过程进行了大量优化,编译速度更快,提升了开发和构建效率。
- TypeScript 支持
Babel 7 原生支持 TypeScript 的转译(仅移除类型,不进行类型检查),在同一项目中使用 TypeScript 和 Babel 变得极其容易。
Webpack 如何配合 Babel 预设的工作
流程概览
Webpack 的工作可以理解为一条流水线,从一个入口文件开始,处理所有依赖,最终打包输出。
1. 入口 | Webpack 从 entry: paths.appIndexJs 开始工作,通常是项目中的 src/index.js 文件 |
2. 模块解析与处理 | Webpack 从入门文件开始,每遇到 import 语句就根据 module.rules 的规则来决定如何处理 module.rules 中包含一个 oneOf 数组,Webpack 从上到下尝试匹配规则,一旦匹配成功,就使用对应的加载器 (loader) 处理,不再继续寻找。例如 : ● 遇到图片 (如 .png, .jpg),会使用 type: 'asset' 规则处理 ● 遇到 .svg 文件,会使用 @svgr/webpack 和 file-loader 处理 ● 遇到 .css 文件,会使用 getStyleLoaders 函数生成的一系列 loader (如 style-loader, css-loader, postcss-loader) 来处理 ● 当遇到 .js 或 .jsx 等文件时,就会匹配到 babel-loader 规则 |
3. 打包输出 | Webpack 将所有文件都用 loader 处理成 能理解的模块后,就将它们打包 (bundle) 起来 output 配置项决定: ● 打包后文件存放地址(path: paths.appBuild) ● 命名方式(eg: 生产环境下 filename: 'static/js/[name].[contenthash:8].js') |
4. 插件增强 | Webpack 调用 plugins 数组 中的插件,从而执行各种额外的任务,例如 : ● HtmlWebpackPlugin 会生成一个 index.html 文件,并自动将打包好的 JS 和 CSS 文件通过 <script> 和 <link> 标签注入进去 ● MiniCssExtractPlugin 会在生产环境中将 CSS 从 JS 中提取成独立的文件 |
重点分析
加载 babel-loader
JavaScript
{
// 1. 匹配所有JS/TS相关文件
test: /.(js|mjs|jsx|ts|tsx)$/,
// 2. 限定只处理 src 目录下的文件
include: paths.appSrc,
// 3. 指定使用 babel-loader
loader: require.resolve('babel-loader'),
// 4. loader 的配置项
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
// 5. 告诉 Babel 使用哪个预设
presets: [
[
// 6. 明确指定 babel-preset-react-app
require.resolve('babel-preset-react-app'),
{
// 7. 向预设传递版本选项,设置具体工厂
// hasJsxRuntime 变量在文件顶部通过 require.resolve('react/jsx-runtime') 取得
// 表示该项目是否支持 React 17+ 的新 JSX 转换。
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
// @remove-on-eject-begin
babelrc: false,
configFile: false,
cacheIdentifier: getCacheIdentifier(
isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
[
'babel-plugin-named-asset-import',
'babel-preset-react-app',
'react-dev-utils',
'react-scripts',
]
),
// @remove-on-eject-end
plugins: [
// 在开发模式下添加 'react-refresh/babel' 用于热更新
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// 启用缓存,加速后续构建
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
}
后记
差不多就是这样,对于编译打包这块,本文章涉及的还是太基础了,随着之后继续学习可能会再出一些相关的笔记😭
诶,可能有同学会问:老师老师人家 Webpack 或者 Babel 插件 源码里有 TypeScript ,看不懂啊怎么办?
有的兄弟有的,往期回顾:
[前端] Leader:可以不用但要知道😠一文速查 TypeScript 基础知识点,字典式速查,全文干货! - 掘金
什么?晦涩的编译过程太难了,你只想狠狠写代码?
有的兄弟也有的,往期回顾:
shadcn/ui:我到底是不是组件库啊😭图文 + 多个场景案例详解 shadcn + tailwind 颠覆性组件开发,小伙伴直呼高端 - 掘金
那就这样,我是 Sawtone,前端新手一枚,祝你开心。
附录
环境相关
react: 19.1.0
webpack: 5.99.9
@babel/core: 7.27.1
相关官方网站
Babel --> www.babeljs.cn/
Webpack --> webpack.docschina.org/