Rodrick

vuePress-theme-reco Rodrick    2022
Rodrick Rodrick

Choose mode

  • dark
  • auto
  • light
Home
Category
  • CS基础
  • 数据库
  • 前端
  • 其他
Tag
About
Timeline
D&T
  • 官方文档

    • Vue
    • Vue3
    • Webpack
    • MDN
    • Node中文网
    • React
    • 小程序
    • FineReport
  • 学习面试

    • 现代JavaScript教程
    • ES6
    • 阿西河
    • LeetCode
    • 牛客网
  • 工具

    • bejson
Contact
  • Github
  • Gitee
author-avatar

Rodrick

62

Article

18

Tag

Home
Category
  • CS基础
  • 数据库
  • 前端
  • 其他
Tag
About
Timeline
D&T
  • 官方文档

    • Vue
    • Vue3
    • Webpack
    • MDN
    • Node中文网
    • React
    • 小程序
    • FineReport
  • 学习面试

    • 现代JavaScript教程
    • ES6
    • 阿西河
    • LeetCode
    • 牛客网
  • 工具

    • bejson
Contact
  • Github
  • Gitee
  • JS

  • CSS

  • 其他

    • HTML面试题整理
    • Web安全策略
    • HTTP缓存
    • 手写题整理
    • Webpack之手把手教你玩loader_plugin
    • Webpack从0到1学会code_splitting
    • Webpack常用记录
    • babel强化

babel强化

vuePress-theme-reco Rodrick    2022

babel强化

Rodrick 2022-07-23 Babel

# 前言

babel主要用于做代码的静态分析, 所有的处理转换变化基本都来源于各种各样的plugin, 本次的重点也是在于强化对于babel插件的认识和使用.

# 再说AST

AST, 抽象语法树, babel的分析能力基本完全依赖于它, babel的AST不是一个标准的ES tree , 具体的标准在这里 .
一般我们会借助 https://astexplorer.net/ 来协助分析AST的内容, 比如
function fn(str){return str + "end"}
会被转换为:

{
  "type": "Program",
  "start": 0,
  "end": 39,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 39,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 11,
        "name": "fn"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 12,
          "end": 15,
          "name": "str"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 16,
        "end": 39,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 19,
            "end": 37,
            "argument": {
              "type": "BinaryExpression",
              "start": 26,
              "end": 37,
              "left": {
                "type": "Identifier",
                "start": 26,
                "end": 29,
                "name": "str"
              },
              "operator": "+",
              "right": {
                "type": "Literal",
                "start": 32,
                "end": 37,
                "value": "end",
                "raw": "\"end\""
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

可以发现,主要用来描述的结构属性大体有这些:


  "body": [
    {
      "type": "",
      "start": ,
      "end": ,
      "id": {},
      "params": [],
      "argument":{},
    }
  ],

type 主要是描述节点的类型,比如: "FunctionDeclaration","Identifier",或 "BinaryExpression". 后面我们会依赖他来判断对不同类型节点做处理
其他的比如 strart/end 描述位置, argument表示参数,operator标识操作符 等等

# babel的处理过程

基本处理过程就是解析(parse),转换(transform),生成(generate),每一个过程都会对应一个babel的插件,这里就这三个插件我们来研究一下

# 解析

解析的过程分成词法分析和**语法分析 **
词法分析是将静态的代码转换为tokens令牌流, 比如 n * n

[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

经过词法分析的令牌流对每一个令牌有一个type属性用来描述该节点
而语法分析阶段就是将令牌流转换为 AST 的过程
这个阶段的过程: 静态代码 -> tokens -> AST

# @babel/parser

@babel/parser (以前是 babylon) 是babel提供的基于 acorn 的一个 AST 转换库(webpack的AST生成也是基于acorn), 让我们简单试用一下:

const babelParser = require("@babel/parser");

const code = `function square(n) {
  return n * n;
}`;

const ast = babelParser.parse(code, {
  sourceType: "module", // default: "script"
  plugins: [], // default: [] can use ["jsx",......]
});

/**
 * use options to traverse JSX
 */
// const code = `<div>{123}</div>`;

// const ast = babelParser.parse(code,{
//   sourceType: "module", // default: "script"
//   plugins: ["jsx"] // default: [] can use ["jsx",......]
// });

console.log(ast.program.body);

/** 打印结果
[
  Node {
    type: 'FunctionDeclaration',
    start: 0,
    end: 38,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    id: Node {
      type: 'Identifier',
      start: 9,
      end: 15,
      loc: [SourceLocation],
      name: 'square'
    },
    generator: false,
    async: false,
    params: [ [Node] ],
    body: Node {
      type: 'BlockStatement',
      start: 19,
      end: 38,
      loc: [SourceLocation],
      body: [Array],
      directives: []
    }
  }
]
*/

还可以利用option参数配置jsx等, sourceType 可以是 "module" 或者 "script", "module" 将会在严格模式下解析并且允许模块定义(import/export),"script" 则不会。

parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});

# 转换

转换阶段对于开发人员是一个比较重要的阶段, 比如 jsx/ts/ES6+ 代码的转换,都是发生在这个阶段, 这个阶段也是各个插件的处理阶段,转换的过程是一个深度遍历的过程, 在遍历AST的时候,每个节点都会经过进出两个阶段:

Identifier: {
  enter(path) {
    console.log("Entered!");
  },
  exit(path) {
    console.log("Exited!");
  }
}

每次进出节点的时候, 我们接收的第一个参数是path, path表示的是当前节点所在的这个路径, 而不是节点本身, 举个例子:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}

如上的这个AST, 如果我们打印一下 path 的keys, 可以看到有很多属性, 包括本身的节点 node, 父节点 parent, 作用域 scope 等, 还有一些增删改节点的方法属性也在path中

Identifier: {
  enter(path) {
    console.log(Object.keys(path));
  },
},
/**
  [
  'contexts',  'state',
  'opts',      '_traverseFlags',
  'skipKeys',  'parentPath',
  'container', 'listKey',
  'key',       'node',
  'type',      'parent',
  'hub',       'data',
  'context',   'scope'
]
*/

# @babel/traverse

@babel/traverse 就是用来转换的官方提供的库, 接下来利用他来完善一下之前的例子:

const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse");

const code = `function square(n) {
  return n * n;
}`;

const ast = babelParser.parse(code, {
  sourceType: "module", // default: "script"
  plugins: [], // default: [] can use ["jsx",......]
});

/**
 * use options to traverse JSX
 */
// const code = `<div>{123}</div>`;

// const ast = babelParser.parse(code,{
//   sourceType: "module", // default: "script"
//   plugins: ["jsx"] // default: [] can use ["jsx",......]
// });

console.log(ast.program.body);

traverse.default(ast, {
  FunctionDeclaration(path) {
    path.node.id.name = "x";
  },

  // ======== same as =======
  // FunctionDeclaration: {
  //   enter(path) {
  //     path.node.id.name = "x";
  //   },
  // },
});

# @babel/types

@babel/types 是一个用来处理AST节点的工具库,具体api可以参考官方文档 , 这里用它来修改一下刚才的转换过程:

const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse");
const t = require("@babel/types");

const code = `function square(n) {
  return n * n;
}`;

const ast = babelParser.parse(code, {
  sourceType: "module", // default: "script"
  plugins: [], // default: [] can use ["jsx",......]
});

/**
 * use options to traverse JSX
 */
// const code = `<div>{123}</div>`;

// const ast = babelParser.parse(code,{
//   sourceType: "module", // default: "script"
//   plugins: ["jsx"] // default: [] can use ["jsx",......]
// });

console.log(ast.program.body);

traverse.default(ast, {
  // FunctionDeclaration(path) {
  //   path.node.id.name = "x";
  // },

  // ======== same as =======
  // FunctionDeclaration: {
  //   enter(path) {
  //     path.node.id.name = "x";
  //   },
  // },

  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});

# 生成

生成阶段就是将转换后的AST再创建为代码的过程,这个阶段同时会创建sourceMap

# @babel/generator

第二个参数可以传递各种参数

const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse");
const t = require("@babel/types");
const generator = require("@babel/generator");

let code = `function square(n) {
  return n * n;
}`;

const ast = babelParser.parse(code, {
  sourceType: "module", // default: "script"
  plugins: [], // default: [] can use ["jsx",......]
});

/**
 * use options to traverse JSX
 */
// const code = `<div>{123}</div>`;

// const ast = babelParser.parse(code,{
//   sourceType: "module", // default: "script"
//   plugins: ["jsx"] // default: [] can use ["jsx",......]
// });

console.log(ast.program.body);

/**
 * traverse
 */
traverse.default(ast, {
  // FunctionDeclaration(path) {
  //   path.node.id.name = "x";
  // },

  // ======== same as =======
  // FunctionDeclaration: {
  //   enter(path) {
  //     path.node.id.name = "x";
  //   },
  // },

  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  },
});

/**
 * generator
 */
const result = generator.default(
  ast,
  {
    /* options */
    sourceMaps: true,
  },
  code
);

console.log(result);

/** 
{
  code: 'function square(x) {\n  return x * x;\n}',
  decodedMap: {
    version: 3,
    file: undefined,
    names: [],
    sourceRoot: undefined,
    sources: [ undefined ],
    sourcesContent: [ 'function square(n) {\n  return n * n;\n}' ],
    mappings: []
  },
  map: [Getter/Setter],
  rawMappings: [Getter/Setter]
}
*/

# 编写一个插件

上面我们说了babel的各个过程和每个阶段提供的不同的包的独立使用, 那么在实际编写babel插件的时候,如何把上面的过程融入进去

export default function ({ types: t }) {
  return {
    visitor: {
      BinaryExpression: {
        enter(path) {
          // if (path.node.left.name === 'x') return
          // 判断当前的 BinaryExpression 是不是接在一个 return 后面
          if (t.isReturnStatement(path.parent)) {
            path.replaceWith(
              t.binaryExpression(path.node.operator, t.identifier("x"), t.identifier("x"))
            );
            // 因为我们使用替换当前 binaryExpression 使用的是也是binaryExpression,所以会无线触发enter方法
            // 所以在替换完成后加上 path.skip()
            // 或者是在一开始加上 if (path.node.left.name === 'x') return
            path.skip()
          }
        },
      },
    },
  };
}

代码git: https://github.com/rodrick278/my-babel-plugin

# 参考:

  • babel-handbook
  • astexplorer
欢迎来到 Rodrick
看板娘