跳到主要内容

babel✅

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

答案

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

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

延伸阅读

面试官视角

这是 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. pre 和 post 选项,表示插件的执行时机,pre 表示在转换之前执行,post 表示在转换之后执行, 可选
    4. 其他配置详见 pluginObject 配置
  2. 消费插件可以直接本地引入,或发布为包进行消费
提示

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

延伸阅读