babel插件开发

270 阅读5分钟

babel插件开发

一. babel 原理

1. 什么是 babel

Babel 是一个 JavaScript 编译器, 可以进行 语法转换、源码转换 等,主要用于将 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以实现浏览器兼容。

2. 转换原理与 ast

转换原理:

原代码 ==解析==》 ast抽象语法树 ==修改==》ast抽象语法树 ==生成==》新代码

ast 简单来说就是由许多节点(node)组成的树状结构,node 之间的关系用 path 标示,详细记录了每个节点的属性与内容。

我们要做的就是使用 babel 将代码解析后,修改 ast ,再生成处理后的代码。

ast 结构可以通过 astexplorer.net 查看。

3. babel 的架构

babel 是通过插件(plugins)构成,简单来说就是每个插件负责只处理代码中的某一类问题。

例如:

插件功能
@babel/plugin-transform-arrow-functions将箭头函数转换为function
@babel/plugin-transform-spread除理展开运算符 ...

将所有需要的功能的插件配置入 babel.config.js 中,即可完成语法的转换。

但是一般需要许多插件,配置繁琐,所以可以使用配置好的功能集合——也就是预设(presets)的方式引入

例如:

预设功能
@babel/preset-env包含了常用插件,并可根据配置(需兼容的浏览器等)进一步控制其中导出的插件
@babel/preset-react包含了react相关插件

二. 准备工作

1. 相关工具

首选了解一下 babel 相关的包

预设功能
@babel/cli可以通过命令行执行babel
@babel/core可以在js中调用执行babel
@babel/types提供了开发babel插件时的常用方法(类似于lodash)

2.目录结构

|-- dist // 转换后代码目录
|-- src
  |-- index.js // 插件源码
  |-- test.code.js // 需要转换的测试代码
  |-- test.js // 使用 @babel/core 转换代码
|-- babel.config.jschunk // babel配置
|-- package.json

babel.config.js 只需引入我们要开发的插件即可,配置如下

// ./babel.config.js
module.exports = {
  plugins: [
    './src/index.js'
  ],
}

我们可以通过 @babel/core 直接转换代码,并输出 ast, 方便我们进行观察 ast 结构以进行调整

// test.js
const babel = require("@babel/core");
const path = require('path');

(async () => {
  // babel.transformFileAsync(file) 直接转换file文件中的代码 (默认使用 babel.config.js 配置)
  const result = await babel.transformFileAsync(path.join(__dirname, './test.code.js'),{
    ast: true, // 生成ast,默认不生成
  });
  // debug
  console.log(result)
})();

设置 NODE_ENV ,使 @babel/cli 可以区分环境

// package.json
{
  ...,
  "scripts": {
    "build": "cross-env NODE_ENV=production babel ./src/test.code.js --out-dir dist",
    "build:dev": "cross-env NODE_ENV=development babel ./src/test.code.js --out-dir dist"
  }
}

三. 插件开发

1. 插件1 -- 区分环境、替换字符串、删除代码段

1.预期插件功能

// ./src/test.code.js
if (DEBUG) {
  console.log(111);
}
// 注释
const b = 10;
const square = (n) => {
  return n * n;
}
console.log(square(b))

区分开发环境与生产环境

  • 开发环境下,可执行 if 语句, 输出 111;
  • 生产环境下,删除 if 语句

实现:

  • 根据 process.env.NODE_ENV 区分环境
  • 开发环境下,将 DEBUG 关键字替换为字符串 “DEBUG” ,使 if 语句可执行
  • 生产环境下,将包含 DEBUG 的 if 语句节点删除

2. ast 分析

使用 astexplorer.net/ 解析测试代码,可以看到输出的 ast 大致如下 (去除了位置数据)

// 递归删除位置数据
const delByKeys = (obj, needDelKeys = ['start', 'end', 'loc']) => {
  return Object.keys(obj).reduce((pv, cv) => {
    if (needDelKeys.includes(cv)) {
      return pv;
    }
    if (obj[cv] instanceof Array) {
      return {
        ...pv,
        [cv]: obj[cv].map(item => delByKeys(item, needDelKeys))
      }
    }
    if (obj[cv] instanceof Object) {
      return {
        ...pv,
        [cv]: delByKeys(obj[cv], needDelKeys),
      }
    }
    return {
      ...pv,
      [cv]: obj[cv],
    }
  }, {});
};
{
    "type":"File",
    "errors":[

    ],
    "program":{
        "type":"Program",
        "sourceType":"module",
        "interpreter":null,
        "body":[
            {
                "type":"IfStatement",
                "test":{
                    "type":"Identifier",
                    "name":"DEBUG"
                },
                "consequent":{
                    "type":"BlockStatement",
                    "body":[
                        {
                            "type":"ExpressionStatement",
                            "expression":{
                                "type":"CallExpression",
                                "callee":{
                                    "type":"MemberExpression",
                                    "object":{
                                        "type":"Identifier",
                                        "name":"console"
                                    },
                                    "computed":false,
                                    "property":{
                                        "type":"Identifier",
                                        "name":"log"
                                    }
                                },
                                "arguments":[
                                    {
                                        "type":"NumericLiteral",
                                        "extra":{
                                            "rawValue":111,
                                            "raw":"111"
                                        },
                                        "value":111
                                    }
                                ]
                            }
                        }
                    ],
                    "directives":[

                    ]
                },
                "alternate":null,
                "trailingComments":[
                    {
                        "type":"CommentLine",
                        "value":" 注释"
                    }
                ]
            },
            {
                "type":"VariableDeclaration",
                "declarations":[
                    {
                        "type":"VariableDeclarator",
                        "id":{
                            "type":"Identifier",
                            "name":"b"
                        },
                        "init":{
                            "type":"NumericLiteral",
                            "extra":{
                                "rawValue":10,
                                "raw":"10"
                            },
                            "value":10
                        }
                    }
                ],
                "kind":"const",
                "leadingComments":[
                    {
                        "type":"CommentLine",
                        "value":" 注释"
                    }
                ]
            },
            {
                "type":"VariableDeclaration",
                "declarations":[
                    {
                        "type":"VariableDeclarator",
                        "id":{
                            "type":"Identifier",
                            "name":"square"
                        },
                        "init":{
                            "type":"ArrowFunctionExpression",
                            "id":null,
                            "generator":false,
                            "async":false,
                            "params":[
                                {
                                    "type":"Identifier",
                                    "name":"n"
                                }
                            ],
                            "body":{
                                "type":"BlockStatement",
                                "body":[
                                    {
                                        "type":"ReturnStatement",
                                        "argument":{
                                            "type":"BinaryExpression",
                                            "left":{
                                                "type":"Identifier",
                                                "name":"n"
                                            },
                                            "operator":"*",
                                            "right":{
                                                "type":"Identifier",
                                                "name":"n"
                                            }
                                        }
                                    }
                                ],
                                "directives":[

                                ]
                            }
                        }
                    }
                ],
                "kind":"const"
            },
            {
                "type":"ExpressionStatement",
                "expression":{
                    "type":"CallExpression",
                    "callee":{
                        "type":"MemberExpression",
                        "object":{
                            "type":"Identifier",
                            "name":"console"
                        },
                        "computed":false,
                        "property":{
                            "type":"Identifier",
                            "name":"log"
                        }
                    },
                    "arguments":[
                        {
                            "type":"CallExpression",
                            "callee":{
                                "type":"Identifier",
                                "name":"square"
                            },
                            "arguments":[
                                {
                                    "type":"Identifier",
                                    "name":"b"
                                }
                            ]
                        }
                    ]
                }
            }
        ],
        "directives":[

        ]
    },
    "comments":[
        {
            "type":"CommentLine",
            "value":" 注释"
        }
    ]
}

我们只需要找到 if(DEBUG) 部分进行处理即可

3. Visitors(访问者)

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。

module.exports = () => {
  return {
    visitor: {
      // 遍历过程中,每遇到一个 Identifier 节点就会触发
      Identifier: {
        enter(path) {

        }
      },
      // 遍历过程中,每遇到一个 FunctionDeclaration 节点就会触发
      // FunctionDeclaration() { ... } 是 FunctionDeclaration: { enter() { ... } } 的简写形式。.
      FunctionDeclaration() {},
    }
  }
}

4. Paths(路径)

Path 是表示两个节点之间连接的对象, 描述了节点的属性与节点之间的关系, 包含了一些属性和方法

api功能
node当前节点的数据
parent父节点
replaceWith(node)将当前节点替换为node
replaceWithMultiple([node])将当前节点替换为多个node
remove()移除当前节点(包括其下的所有节点)
traverse(vistor)在当前path下再嵌套visitor,避免处理打预期外的节点

5. 实现

通过执行 node ./src/test.js ,可以输出代码的 ast,然后进行调整

module.exports = ({types: t}) => {
  return {
    visitor: {
      // 遍历过程中,每遇到一个Identifier节点就会触发
      Identifier: {
        enter(path) {
          // 判断该节点是 DEBUG,并且在 if 里
          if (path.node.name === 'DEBUG' && t.isIfStatement(path.parent)) {
            // 开发环境
            if (process.env.NODE_ENV === 'development') {
              // 替换为 新创建的 string节点
              path.replaceWith(t.stringLiteral('MY_DEBUG'));
            } else {
              // 移除
              path.parentPath.remove();
            }
          }
        }
      }
    }
  }
}

2. 插件2 -- 使用 require(xx) 时加上 default,已经有 default 的不处理

1.预期插件功能

// ./src/test.code.js 
const m = require('./test.require.js'); // 转换为=> const m = require('./test.require.js').default;
const n = require('./test.require2.js').default; // 已有default,不作处理

2. ast 分析

{
    "type":"File",
    "errors":[

    ],
    "program":{
        "type":"Program",
        "sourceType":"module",
        "interpreter":null,
        "body":[
            {
                "type":"VariableDeclaration",
                "declarations":[
                    {
                        "type":"VariableDeclarator",
                        "id":{
                            "type":"Identifier",
                            "name":"m"
                        },
                        "init":{
                            "type":"CallExpression",
                            "callee":{
                                "type":"Identifier",
                                "name":"require"
                            },
                            "arguments":[
                                {
                                    "type":"StringLiteral",
                                    "extra":{
                                        "rawValue":"./test.require.js",
                                        "raw":"'./test.require.js'"
                                    },
                                    "value":"./test.require.js"
                                }
                            ]
                        }
                    }
                ],
                "kind":"const"
            },
            {
                "type":"VariableDeclaration",
                "declarations":[
                    {
                        "type":"VariableDeclarator",
                        "id":{
                            "type":"Identifier",
                            "name":"n"
                        },
                        "init":{
                            "type":"MemberExpression",
                            "object":{
                                "type":"CallExpression",
                                "callee":{
                                    "type":"Identifier",
                                    "name":"require"
                                },
                                "arguments":[
                                    {
                                        "type":"StringLiteral",
                                        "extra":{
                                            "rawValue":"./test.require.js",
                                            "raw":"'./test.require.js'"
                                        },
                                        "value":"./test.require.js"
                                    }
                                ]
                            },
                            "computed":false,
                            "property":{
                                "type":"Identifier",
                                "name":"default"
                            }
                        }
                    }
                ],
                "kind":"const"
            }
        ],
        "directives":[

        ]
    },
    "comments":[

    ]
}

可以看到,

  • require 关键字类型是 CallExpression
  • 有 default 的 require 的父节点类型为 MemberExpression

思路

  • 仿照 require('./test.require2.js').default 的 ast 创建 MemberExpression 节点,替换掉 require('./test.require.js') 的 ast

3. 实现

module.exports = ({types: t}) => {
  return {
    visitor: {
      CallExpression: {
        enter(path) {
          // 遍历 CallExpression ,并找到 所有require
          if (path.node.callee.name === 'require') { 
            // 已经有 .default的,不改动
            if (t.isIdentifier(path.parent.property) && path.parent.property.name === 'default'){
              return;
            }else {
              // 没有default, 新建 MemberExpression 节点;其中包含 require callExpression节点(子节点为 identifier)、require中的参数(stringLiteral节点)、 default属性(identifier)
              const newNode = t.memberExpression(t.callExpression(t.identifier('require'), path.node.arguments.map(item => t.stringLiteral(item.value))), t.identifier('default'));
              // 替换
              path.replaceWith(newNode);
            }
          }
        }
      }
    }
  }
}

四. 参考

插件编写教材: github.com/jamiebuilds…

@babel/types api: babeljs.io/docs/en/bab…