babel自定义插件

2,122 阅读12分钟

前期准备

babel解析过程

Babel 的三个主要处理步骤分别是: 解析(parse)转换(transform)生成(generate) 。.

解析

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:**词法分析(Lexical Analysis) **和 语法分析(Syntactic Analysis) 。.

词法分析

词法分析阶段把字符串形式的代码转换为 令牌(tokens)  流。.

你可以把令牌看作是一个扁平的语法片段数组:

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

每一个 type 有一组属性来描述该令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

和 AST 节点一样它们也有 startendloc 属性。

  • 像这样的结构叫做节点(Node) 。一个AST是由多个或单个这样的节点组成,节点内部可以有多个这样的子节点,构成一颗语法树,这样就可以描述用于静态分析的程序语法。
{ "type": "BlockStatement", "start": 19, "end": 40, "body": [...] }

语法分析

语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

转换

转换在这个阶段,Babel接受得到AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分。

生成

代码生成将经过转换的AST通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串,同时还会创建源码映射(source maps)

遍历

想要转换 AST 你需要进行递归的树形遍历

比方说我们有一个 FunctionDeclaration 类型。它有几个属性:idparams,和 body,每一个都有一些内嵌节点。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

于是我们从 FunctionDeclaration 开始并且我们知道它的内部属性(即:idparamsbody),所以我们依次访问每一个属性及它们的子节点。

接着我们来到 id,它是一个 IdentifierIdentifier 没有任何子节点属性,所以我们继续。

之后是 params,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier 类型的单一节点,然后我们继续。

此时我们来到了 body,这是一个 BlockStatement 并且也有一个 body节点,而且也是一个数组节点,我们继续访问其中的每一个。

这里唯一的一个属性是 ReturnStatement 节点,它有一个 argument,我们访问 argument 就找到了 BinaryExpression。.

BinaryExpression 有一个 operator,一个 left,和一个 right。 Operator 不是一个节点,它只是一个值因此我们不用继续向内遍历,我们只需要访问 left 和 right。.

Babel 的转换步骤全都是这样的遍历过程。

了解visitor

当Babel处理一个节点时,是以访问者的形式获取节点信息,并进行相关操作,这种方式是通过一个visitor对象来完成的,在visitor对象中定义了对于各种节点的访问函数,这样就可以针对不同的节点做出不同的处理。我们编写的Babel插件其实也是通过定义一个实例化visitor对象处理一系列的AST节点来完成我们对代码的修改操作。

  • 例子
const MyVisitor = {
 Identifier: {
    enter(path, state) {
      console.log("Entered!");
    },
    exit(path, state) {
      console.log("Exited!");
    }
  }
};

// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
  • 这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。该Identifier方法有进入或者出去的方法

所以在下面的代码中 Identifier() 方法会被调用四次(包括 square 在内,总共有四个 Identifier)。).调用的顺序其实是采用类似二叉树的中序遍历

了解path

从上面的visitor对象中,可以看到每次访问节点方法时,都会传入一个path参数,这个path参数中包含了节点的信息以及节点和所在的位置,以供对特定节点进行操作。具体来说Path 是表示两个节点之间连接的对象。这个对象不仅包含了当前节点的信息,也有当前节点的父节点的信息,同时也包含了添加、更新、移动和删除节点有关的其他很多方法。具体地,Path对象包含的属性和方法主要如下:

── 属性 
- node 当前节点
- parent 父节点 
- parentPath 父path 
- scope 作用域 
- context 上下文 - ... 
── 方法 
- get 当前节点 
- findParent 向父节点搜寻节点 
- getSibling 获取兄弟节点 
- replaceWith 用AST节点替换该节点 
- replaceWithMultiple 用多个AST节点替换该节点 
- insertBefore 在节点前插入节点 
- insertAfter 在节点后插入节点 
- remove 删除节点 - ...
  • 例子
import { Ajax } from '../lib/utils';
  • visitor对象:
visitor: { 
ImportDeclaration (path, state){ 
  console.log(path.node); // do something } 
}
  • 打印结果
Node {
  type: 'ImportDeclaration',
  start: 5,
  end: 41,
  loc: 
   SourceLocation {
     start: Position { line: 2, column: 4 },
     end: Position { line: 2, column: 40 } },
  specifiers: 
   [ Node {
       type: 'ImportSpecifier',
       start: 14,
       end: 18,
       loc: [SourceLocation],
       imported: [Node],
       local: [Node] } ],
  source: 
   Node {
     type: 'StringLiteral',
     start: 26,
     end: 40,
     loc: SourceLocation { start: [Position], end: [Position] },
     extra: { rawValue: '../lib/utils', raw: '\'../lib/utils\'' },
     value: '../lib/utils'
    }
}

其中specifiers表示import导入的变量组成的节点数组,source表示导出模块的来源节点。这里再说一下specifier中的imported和local字段,imported表示从导出模块导出的变量,local表示导入后当前模块的变量

state

state就是一系列状态的集合,包含诸如当前plugin的信息、plugin传入的配置参数信息,甚至当前节点的path信息也能获取到,当然也可以把babel插件处理过程中的自定义状态存储到state对象中。

scope

本质就是js作用域,都有各自的的作用域,内部作用域可以和外部重名,并且内部可引用外部作用域

function One() {
  var one = "t";

  function Two() {
    var one = "m";
  }
}

当编写一个转换时,必须小心作用域。我们得确保在改变代码的各个部分时不会破坏已经存在的代码。

我们在添加一个新的引用时需要确保新增加的引用名字和已有的所有引用不冲突。 或者我们仅仅想找出使用一个变量的所有引用, 我们只想在给定的作用域(Scope)中找出这些引用。

作用域可以被表示为如下形式:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

Bindings(绑定)

所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding) 。.

当你创建一个新的作用域时,需要给出它的路径和父作用域,之后在遍历过程中它会在该作用域内收集所有的引用(“绑定”)。

一旦引用收集完毕,你就可以在作用域(Scopes)上使用各种方法,稍后我们会了解这些方法。

babel工具集

具体可以参考: github.com/jamiebuilds…

转换

参考文档: github.com/jamiebuilds…

自定义插件

简单入门

先从一个接收了当前babel对象作为参数的 function开始。

export default function(babel) {
  // plugin contents
}

由于你将会经常这样使用,所以直接取出 babel.types 会更方便:(译注:这是 ES2015 语法中的对象解构,即 Destructuring)

export default function({ types: t }) {
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要访问者。Visitor 中的每个函数接收2个参数:path 和 state

export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};
  • 例子
a === b
  • 分析AST
{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "a"
  },
  right: {
    type: "Identifier",
    name: "b"
  }
}
  • visitor
export default function({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path, state) {
          if(path.node.operator !== '==='){
             return 
          }
          path.node.left = t.identifier('c')
          path.node.right = t.identifier('d')
      },
    }
  };
};

复杂案例

  • 例子: 箭头函数和this变量
var func = ()=>{ console.log(this.b) };
  • 编译出来结果
var _this = this
func = function () {
    console.log(_this.b, 3);
};
  1. 分析前后AST(参考这个可以得到具体的AST:astexplorer.net/)
{
      "type": "ExpressionStatement",
      "start": 178,
      "end": 219,
      "expression": {
        "type": "AssignmentExpression",
        "start": 178,
        "end": 218,
        "operator": "=",
        "left": {
          "type": "Identifier",
          "start": 178,
          "end": 182,
          "name": "func"
        },
        "right": {
          "type": "ArrowFunctionExpression",
          "start": 185,
          "end": 218,
          "id": null,
          "expression": false,
          "generator": false,
          "async": false,
          "params": [],
          "body": {
            "type": "BlockStatement",
            "start": 189,
            "end": 218,
            "body": [
              {
                "type": "ExpressionStatement",
                "start": 195,
                "end": 216,
                "expression": {
                  "type": "CallExpression",
                  "start": 195,
                  "end": 216,
                  "callee": {
                    "type": "MemberExpression",
                    "start": 195,
                    "end": 206,
                    "object": {
                      "type": "Identifier",
                      "start": 195,
                      "end": 202,
                      "name": "console"
                    },
                    "property": {
                      "type": "Identifier",
                      "start": 203,
                      "end": 206,
                      "name": "log"
                    },
                    "computed": false,
                    "optional": false
                  },
                  "arguments": [
                    {
                      "type": "MemberExpression",
                      "start": 207,
                      "end": 213,
                      "object": {
                        "type": "ThisExpression",
                        "start": 207,
                        "end": 211
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 212,
                        "end": 213,
                        "name": "b"
                      },
                      "computed": false,
                      "optional": false
                    },
                    {
                      "type": "Literal",
                      "start": 214,
                      "end": 215,
                      "value": 3,
                      "raw": "3"
                    }
                  ],
                  "optional": false
                }
              }
            ]
          }
        }
      }
    },
  • 后(_this变量)

 {
      "type": "VariableDeclaration",
      "start": 220,
      "end": 236,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 224,
          "end": 236,
          "id": {
            "type": "Identifier",
            "start": 224,
            "end": 229,
            "name": "_this"
          },
          "init": {
            "type": "ThisExpression",
            "start": 232,
            "end": 236
          }
        }
      ],
      "kind": "var"
    },
  • 后(箭头函数)
{
   "type": "ExpressionStatement",
   "start": 237,
   "end": 289,
   "expression": {
     "type": "AssignmentExpression",
     "start": 237,
     "end": 288,
     "operator": "=",
     "left": {
       "type": "Identifier",
       "start": 237,
       "end": 241,
       "name": "func"
     },
     "right": {
       "type": "FunctionExpression",
       "start": 244,
       "end": 288,
       "id": null,
       "expression": false,
       "generator": false,
       "async": false,
       "params": [],
       "body": {
         "type": "BlockStatement",
         "start": 256,
         "end": 288,
         "body": [
           {
             "type": "ExpressionStatement",
             "start": 262,
             "end": 286,
             "expression": {
               "type": "CallExpression",
               "start": 262,
               "end": 285,
               "callee": {
                 "type": "MemberExpression",
                 "start": 262,
                 "end": 273,
                 "object": {
                   "type": "Identifier",
                   "start": 262,
                   "end": 269,
                   "name": "console"
                 },
                 "property": {
                   "type": "Identifier",
                   "start": 270,
                   "end": 273,
                   "name": "log"
                 },
                 "computed": false,
                 "optional": false
               },
               "arguments": [
                 {
                   "type": "MemberExpression",
                   "start": 274,
                   "end": 281,
                   "object": {
                     "type": "Identifier",
                     "start": 274,
                     "end": 279,
                     "name": "_this"
                   },
                   "property": {
                     "type": "Identifier",
                     "start": 280,
                     "end": 281,
                     "name": "b"
                   },
                   "computed": false,
                   "optional": false
                 },
                 {
                   "type": "Literal",
                   "start": 283,
                   "end": 284,
                   "value": 3,
                   "raw": "3"
                 }
               ],
               "optional": false
             }
           }
         ]
       }
     }
   }
 }
],

结果: 观察得到全局增加了一个变量(父节点),thisExpress进行替换成我们_this变量,箭头函数替换成我们普通函数(涉及知识replaceWith-替换,insertBefore-插入,findParent-查找父节点) (参考文档: github.com/jamiebuilds…)

  1. 先npm init 一个package.json文件
  2. 安装依赖包@babel/types,babel-core,@babel/cli,babylon
  3. 对package.json进行配置或者直接使用node启动main.js入口文件
{

"name": "plugin-demo",

"version": "1.0.0",

"description": "自定义babel插件",

"main": "index.js",

"scripts": {

"build": "babel main -d lib"

},

"repository": {

"type": "git",

"url": ""

},

"keywords": [

"babel"

],

"author": "feile",

"license": "ISC",

"devDependencies": {

"@babel/cli": "^7.16.0",

"@babel/core": "^7.16.0",

"@babel/types": "^7.16.0",

"babylon": "^6.18.0"

},

"dependencies": {

"babel-core": "^6.26.3"

}

}
  • mai/index.js
var babel = require('babel-core');

var t = require('babel-types')


var code = `func = ()=>{

console.log(this.b,3)

};`



// const visitor = {

// Identifier(path){

// console.log(path.node.name,2)

// }


//整体思路就是先分析两者的AST的区别,其实主要是集中在_this变量的定义和thisExpress的替换里面,
//还有就是箭头函数的转换成普通函数

// 1.先构建var _this = this, 2.找到父节点,然后插入_this并且替换老的thisExpress,3.最后替换箭头函数的

const Visitor = {

ThisExpression(path){

//先构建var _this = this

let node = t.variableDeclaration('var',[

t.variableDeclarator(t.identifier('_this'),t.identifier('this'))

]),

str = t.identifier('_this')

//查找父子节点

parentPath = path.findParent((path) => path.isVariableDeclaration());

if(parentPath){

parentPath.replaceWith(str)

path.insertBefore(node)

}else{

return

}

},

//ArrowFunctionExpression替换

ArrowFunctionExpression(path){

var node = path.node

path.replaceWith(t.functionExpression(node.id,node.params,node.body))

}

};


var result = babel.transform(code,{plugins: [{

//前面的Visitor

visitor: Visitor

}]})

console.log(result.code,1)

/**
  var _this = this
*func = function () {

console.log(_this.b, 3);

}; 

*/
  • 该demo涉及到编译这块自己,编译出来是箭头函数,没有this这块,代码中主要是想借鉴一下稍微复杂一点的逻辑,学得话会更加深入点

  • 例子 :按需加载ui组件(核心思路其实就是导入的时候导入我们具体的路径)

import { Select as MySelect, Pagination } from 'xxx-ui';

import * as UI from 'xxx-ui';

AST :

{
  "type": "Program",
  "start": 0,
  "end": 267,
  "body": [
    {
      "type": "ImportDeclaration",
      "start": 180,
      "end": 236,
      "specifiers": [
        {
          "type": "ImportSpecifier",
          "start": 189,
          "end": 207,
          "imported": {
            "type": "Identifier",
            "start": 189,
            "end": 195,
            "name": "Select"
          },
          "local": {
            "type": "Identifier",
            "start": 199,
            "end": 207,
            "name": "MySelect"
          }
        },
        {
          "type": "ImportSpecifier",
          "start": 209,
          "end": 219,
          "imported": {
            "type": "Identifier",
            "start": 209,
            "end": 219,
            "name": "Pagination"
          },
          "local": {
            "type": "Identifier",
            "start": 209,
            "end": 219,
            "name": "Pagination"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 227,
        "end": 235,
        "value": "xxx-ui",
        "raw": "'xxx-ui'"
      }
    },
    {
      "type": "ImportDeclaration",
      "start": 237,
      "end": 266,
      "specifiers": [
        {
          "type": "ImportNamespaceSpecifier",
          "start": 244,
          "end": 251,
          "local": {
            "type": "Identifier",
            "start": 249,
            "end": 251,
            "name": "UI"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 257,
        "end": 265,
        "value": "xxx-ui",
        "raw": "'xxx-ui'"
      }
    }
  ],
  "sourceType": "module"
}

解析以后


import MySelect from './xxx-ui/src/components/ui-base/select/select';
import Pagination from './xxx-ui/src/components/ui-base/pagination/pagination';
import * as UI from 'xxx-ui';

AST

{
  "type": "Program",
  "start": 0,
  "end": 331,
  "body": [
    {
      "type": "ImportDeclaration",
      "start": 180,
      "end": 249,
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "start": 187,
          "end": 195,
          "local": {
            "type": "Identifier",
            "start": 187,
            "end": 195,
            "name": "MySelect"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 201,
        "end": 248,
        "value": "./xxx-ui/src/components/ui-base/select/select",
        "raw": "'./xxx-ui/src/components/ui-base/select/select'"
      }
    },
    {
      "type": "ImportDeclaration",
      "start": 250,
      "end": 329,
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "start": 257,
          "end": 267,
          "local": {
            "type": "Identifier",
            "start": 257,
            "end": 267,
            "name": "Pagination"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 273,
        "end": 328,
        "value": "./xxx-ui/src/components/ui-base/pagination/pagination",
        "raw": "'./xxx-ui/src/components/ui-base/pagination/pagination'"
      }
    }
  ],
  "sourceType": "module"
}

使用工具对比AST得出下列结论:

  1. 把每一个按需导入ImportSpecifier 和 source拿到然后抽离出来成为单独的一项ImportDeclaration,包含(specifiers,source)

2.在specifiers数组第一个是不是存在默认导入或者as这种情况下, 把ImportSpecifier改成ImportDefaultSpecifier,StringLiteral

const code = `import { Select as MySelect, Pagination } from 'xxx-ui';

import * as UI from 'xxx-ui';`


const visitor = {
 ImportDeclaration(path, { opts }) {
  //specifiers
  var specifiers = path.node.specifiers
  //source
  var source = path.node.source;
  var opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts
  var camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined' ? false : true

   var camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined' ? false : true
   if (!t.isImportDefaultSpecifier(specifiers[0] && !t.isImportNamespaceSpecifier(specifiers[0]))) {
    var declarations = specifiers.map((specifier) => {
    //console.log(specifier.imported.name)
    var transformName = camel2UnderlineComponentName ? camel2Underline(specifier.imported.name)  : camel2DashComponentName ? camel2Dash(specifier.imported.name) : specifier.imported.name
    console.log(transformName)
   //拿到每一项赋值给ImportDeclaration
    return t.ImportDeclaration([t.ImportDefaultSpecifier(specifier.local)],       t.StringLiteral(opt.customSourceFunc(opt.customSourceFunc(transformName))))

  })

    // 将当前节点替换成新建的ImportDeclaration节点组

    path.replaceWithMultiple(declarations);

  }
 }
}

const result = babel.transform(code, {

plugins: [

     [{//前面的Visitor
          visitor: visitor
       },

     {

        "libraryName": "xxx-ui",

        "camel2DashComponentName": true,

        "camel2UnderlineComponentName": true,

         "customSourceFunc": componentName => (`./xxx-ui/src/components/ui-base/${componentName}/${componentName}`)

     }]

   ]

})
  • 中间遇到的问题: 刚开始是想简写不要一些变量的判断,大概编译出来一个结果就行,但是中间plugin插件配置我是直接使用的解构赋值,所一直报错说找不到option,后面因为逻辑判断里面assert推断验证,去看作者的源码,后面就报不能在模块外部使用import语句,这里就有问题,看源码是package.json里面配置keyWord: 'import',但是本人还没生效,然后百度找了一种直接在package.json里面使用type: module,require引用是有问题,但是这种方法是可以的,我们直接使用return ({ types }) => ({visitor:{}),如果想直接验证逻辑的直接去掉assert验证也可

参考文档: juejin.cn/post/684490…
juejin.cn/post/684490…