跳到主要内容

babel✅

日常有使用过 babel 么,用在哪些场景?

答案

Babel 主要用于实现在低版本浏览器或其他运行环境中使用高版本 JavaScript 特性,或者使用一些新的语法特性,比如 ES6/7/8/9/10/11/12 等,或者使用 JSX、TypeScript 等语言,将其转换为浏览器可以识别的代码。典型的使用场景包括

  1. 转换语法,通过配置 babel, 实现在低版本浏览器中使用高版本 JavaScript 语法,比如箭头函数、解构赋值
  2. 自定义插件,通过编写自定义插件,实现一些特定的功能,比如代码检查、代码转换等
  3. ployfill,通过引入 @babel/polyfill 实现对新 API 的支持,比如 PromiseSet

延伸阅读

面试官视角

这是 babel 的热身问题,通过答题者对 babel 的整体认识,判断是否需要继续追问细节。

能够基本知道 babel 的作用和能力,对 babel 的插件和预设有一定了解 能够说出 babel 整体的工作流程,涉及的概念及常用的 API 等,比如能说出 AST 的结构,常用节点类型,各环节涉及的核心 API, 常用配置等

babel 是如何工作的?

答案

整个 babel 的工作流程简述为三个阶段

  1. 解析(parser),输入字符串,输出抽象语法树(AST)
  2. 转换(transformer),对 AST 进行遍历,转换成新的 AST
  3. 生成(generator),将 AST 重新转换成字符串
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')

// 示例代码: ES6 代码 -> ES5 代码
const sourceCode = `
const greeting = name => {
  console.log(\`Hello, \${name}!\`);
};
`

// 1. 解析(Parser)
// 将源代码解析成 AST
const ast = parser.parse(sourceCode, {
  sourceType: 'module'
})

// 2. 转换(Transform)
// 遍历 AST 并进行转换
traverse(ast, {
  // 访问箭头函数
  ArrowFunctionExpression (path) {
    // 将箭头函数转换为普通函数
    const node = path.node
    const body = node.body

    // 创建新的函数表达式
    const functionExpression = t.functionExpression(
      null, // 函数名,这里为 null 因为是匿名函数
      node.params,
      body,
      false, // generator
      false // async
    )

    // 替换原节点
    path.replaceWith(functionExpression)
  },

  // 访问模板字符串
  TemplateLiteral (path) {
    const { expressions, quasis } = path.node

    // 将模板字符串转换为字符串拼接
    const strings = quasis.map(quasi => quasi.value.raw)
    const nodes = []

    strings.forEach((str, i) => {
      nodes.push(t.stringLiteral(str))
      if (expressions[i]) {
        nodes.push(expressions[i])
      }
    })

    // 使用 + 运算符拼接字符串
    let current = nodes[0]
    for (let i = 1; i < nodes.length; i++) {
      current = t.binaryExpression('+', current, nodes[i])
    }

    path.replaceWith(current)
  }
})

// 3. 生成(Generate)
// 将转换后的 AST 重新生成代码
const output = generate(ast, {}, sourceCode)

console.log('转换前的代码:')
console.log(sourceCode)
console.log('\n转换后的代码:')
console.log(output.code)

Open browser consoleTerminal

延伸阅读

如何编写 babel 插件

答案

参看示例,该插件实现了提取代码中的 todo 注释,生成为 todo.md 的功能

const fs = require('fs')
const path = require('path')

module.exports = function (babel) {
  return {
    name: 'babel-plugin-todo-collector',

    pre (state) {
      console.log('🚀 Entering babel-plugin-todo-collector')
      const options = state.opts || {}
      this.todos = []
      this.outputFile = options.outputFile || 'todo.md'
      this.projectRoot = options.projectRoot || process.cwd()
    },

    visitor: {
      Program: {
        enter (path, state) {
          console.log(`📑 Processing file: ${state.file.opts.filename}`)

          const comments = path.container.comments || []
          const filename = state.file.opts.filename

          comments.forEach(comment => {
            const todoMatch = comment.value.match(/TODO:?\s*(.*)/i)
            if (todoMatch) {
              const todo = todoMatch[1].trim()
              const { line, column } = comment.loc.start

              this.todos.push({
                content: todo,
                file: filename,
                line,
                column
              })
            }
          })
        },

        exit () {
          console.log('✅ Finished processing file')
        }
      }
    },

    post (state) {
      if (this.todos.length > 0) {
        const markdown = this.todos.map(todo => {
          const relativePath = path.relative(this.projectRoot, todo.file)
          const fileName = path.basename(todo.file)
          return `* [ ] ${todo.content}, 详见 [${fileName}](${relativePath}#L${todo.line},${todo.column})`
        }).join('\n')

        fs.writeFileSync(
          path.join(process.cwd(), this.outputFile),
          markdown,
          'utf-8'
        )

        console.log(`✅ Collected ${this.todos.length} TODOs into ${this.outputFile}`)
      }
      console.log('👋 Exiting babel-plugin-todo-collector')
    }
  }
}

Open browser consoleTests

插件开发的核心流程如下

  1. 创建插件文件:在项目中创建一个新的 JavaScript 文件,用于编写自定义插件的代码。核心配置包括
    1. name ,插件名,一般以 babel-plugin- 开头,例如示例中的 babel-plugin-todo-collector, 详细规范参考 命名格式
    2. vistor ,访问者对象,包含了对 AST 节点的处理函数,其中涉及的核心概念包括
      1. NodeType 节点类型,如 Identifier, CallExpression
      2. path 路径对象,表示 AST 中的一个路径,你可以通过该对象访问和操作 AST 节点
      3. state 状态对象,表示插件的状态,可以用来共享数据和配置
      4. enter、exit,表示对节点的处理时机,enter 表示进入节点时的处理,exit 表示退出节点时的处理
    3. prepost 选项,表示插件的执行时机,pre 表示在转换之前执行,post 表示在转换之后执行, 可选
    4. 其他配置详见 pluginObject 配置
  2. 消费插件可以直接本地引入,或发布为包进行消费
提示

注意插件引用的执行顺序为从左到右,而 preset 的执行顺序为从右到左

延伸阅读

22%