[前端] 面试官:Babel = 比巴卜,想吃了是吧?👿一文速通 Babel 基础知识点,全文干货,干净不废话👋

前言

作为一名前端开发程序员,React 与 Vue 的选择自然是津津乐道的一环,无论是随意为之还是谨慎思量,我们总会在某个时间认真思考这个问题:我为什么选择先写 React/Vue?

后来,我慢慢了解到了两者在核心设计哲学上的差别,React 力图使用 JSX 这种语法扩展贯彻 All In JS 理念,而 Vue 更倾向于使用 v-ifv-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 接口的插件对象
  • 具体工厂 :由 runtimedevelopment 的不同配置组合而成的不同模式
  • 工厂方法buildCreateElementCallbuildJSXElementCall 等内部函数,根据当前配置执行对应逻辑

对于具体工厂:

  • 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 包中导入一个特殊的 jsxjsxs 函数,开发者无需在每个使用 JSX 的文件中手动引入 React 以支持 React.createElement 的调用。

同时,由于从 引入主包 变为 引入特定包,编译产物的体积略微减小,做到了底层架构上广泛的性能优化。

该转换机制需要在 Babel 配置文件 babel.config.js 中设置 @babel/preset-reactruntimeautomatic,不过对于 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.x7.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/

相关推荐
洛卡卡了21 分钟前
面试官问限流降级,我项目根本没做过,咋办?
后端·面试·架构
天才熊猫君1 小时前
npm 和 pnpm 的一些理解
前端
飞飞飞仔1 小时前
从 Cursor AI 到 Claude Code AI:我的辅助编程转型之路
前端
qb1 小时前
vue3.5.18源码:调试方式
前端·vue.js·架构
落叶的悲哀1 小时前
面试问题11
java·数据库·面试
Spider_Man1 小时前
缓存策略大乱斗:让你的页面快到飞起!
前端·http·node.js
前端老鹰1 小时前
CSS overscroll-behavior:解决滚动穿透的 “边界控制” 专家
前端·css·html
一叶怎知秋1 小时前
【openlayers框架学习】九:openlayers中的交互类(select和draw)
前端·javascript·笔记·学习·交互
allenlluo2 小时前
浅谈Web Components
前端·javascript
Mintopia2 小时前
把猫咪装进 public/ 文件夹:Next.js 静态资源管理的魔幻漂流
前端·javascript·next.js