babel插件的写法及应用(二)

19232次浏览

前言

本文紧接着上一篇文章babel代码编译流程介绍,主要讲解如何自己写一个babel插件。上一篇文章通过babel的parse、transform、generate 我们完成了一个小的案例,假如这个小案例通过npm发布,共享这个功能,那么我们必须用到babel插件,那么如何封装一个babel插件呢?本文着重写下babel插件的封装。

babel的使用

项目中用到babel,你可以看到,首先是在项目根目录下面有个.babelrc 文件,这个是babel的配置文件。格式如下:

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-runtime", "external-helpers","transform-class-properties"]
}

presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

# ES2015 转码规则
$ npm install --save-dev babel-preset-es2015

# react 转码规则
$ npm install --save-dev babel-preset-react

# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

执行顺序

preset 和 plugin 从形式上差不多,但是应用顺序不同。

babel 会按照如下顺序处理插件和 preset:

先应用 plugin,再应用 preset

plugin 从前到后,preset 从后到前

这个顺序是 babel 的规定。

preset 和 plugin的关系

plugin 是单个转换功能的实现,当 plugin 比较多或者 plugin 的 options 比较多的时候就会导致使用成本升高。这时候可以封装成一个 preset,用户可以通过 preset 来批量引入 plugin 并进行一些配置。preset 就是对 babel 配置的一层封装。

preset 格式和 plugin 一样,也是可以是一个对象,或者是一个函数,函数的参数也是一样的 api 和 options,区别只是 preset 返回的是配置对象,包含 plugins、presets 等配置。

preset 的封装格式如下:

export default function(api, options) {
  return {
      plugins: ['pluginA'],
      presets: [['presetsB', { options: 'haorooms'}]]
  }
}

也可以

export default obj = {
      plugins: ['pluginA'],
      presets: [['presetsB', { options: 'bbb'}]]
}

@babel/core 的包提供了 createConfigItem 的 api,用于创建配置项。我们之前都是字面量的方式创建的,当需要把配置抽离出去时,可以使用 createConfigItem。

const pluginA = createConfigItem('pluginA);
const presetB = createConfigItem('presetsB', { options: 'haorooms'})

export default obj = {
      plugins: [ pluginA ],
      presets: [ presetB ]
  }
}

preset 的简单的封装就是如上。本文着重讲解plugins 的封装

babel插件的封装

上文简单写了preset 封装已经和plugin 的区别。那么如何封装一个babel插件呢?

封装babel plugin 也有两种格式:

第一种是一个函数返回一个对象的格式,对象里有 visitor、pre、post、inherits、manipulateOptions 等属性。

export default function(api, options, dirname) {
  return {
    inherits: parentPlugin,
    manipulateOptions(options, parserOptions) {
        options.xxx = '';
    },
    pre(file) {
      this.cache = new Map();
    },
    visitor: {
      StringLiteral(path, state) {
        this.cache.set(path.node.value, 1);
      }
    },
    post(file) {
      console.log(this.cache);
    }
  };
} 

第二种格式就是直接写一个对象,不用函数包裹,这种方式用于不需要处理参数的情况。

export default plugin =  {
    pre(state) {
      this.cache = new Map();
    },
    visitor: {
      StringLiteral(path, state) {
        this.cache.set(path.node.value, 1);
      }
    },
    post(state) {
      console.log(this.cache);
    }
};

平时用第一种的用的比较多。我个人推荐第一种方式:

针对第一种方式,函数有 3 个参数,api、options、dirname。

options 就是外面传入的参数

dirname 是目录名(不常用)

api 里包含了各种 babel 的 api,比如 types、template 等,这些包就不用在插件里单独单独引入了,直接取来用就行。

返回的对象有 inherits、manipulateOptions、pre、visitor、post 等属性。

inherits 指定继承某个插件,和当前插件的 options 合并,通过 Object.assign 的方式。

visitor 指定 traverse 时调用的函数。

pre 和 post 分别在遍历前后调用,可以做一些插件调用前后的逻辑,比如可以往 file(表示文件的对象,在插件里面通过 state.file 拿到)中放一些东西,在遍历的过程中取出来。

manipulateOptions 用于修改 options,是在插件里面修改配置的方式,比如 syntaxt plugin一般都会修改 parser options

插件做的事情就是通过 api 拿到 types、template 等,通过 state.opts 拿到参数,然后通过 path 来修改 AST。可以通过 state 放一些遍历过程中共享的数据,通过 file 放一些整个插件都能访问到的一些数据,除了这两种之外,还可以通过 this 来传递本对象共享的数据。

命名规则

针对babel插件,我们最好包含 babel-plugin 字样

例如 haorooms博客插件,可以这么写

 babel-plugin-haorooms

preset也是一样,例如官方命名 @babel/preset-env

我们可以命名

preset-haorooms

不过也可以简写,官方自动补齐,例如:

写 babel 内置的 plugin 和 preset 的时候也可以简化,比如 @babel/preset-env 可以直接写@babel/env,babel 会自动补充为 @babel/preset-env。

举例插件的名字补齐规则

babel plugin 名字的补全有这些规则:

如果是 ./ 开头的相对路径,不添加 babel plugin,比如 ./dir/plugin.js

如果是绝对路径,不添加 babel plugin,比如 /dir/plugin.js

如果是单独的名字 aa,会添加为 babel-plugin-aa,所以插件名字可以简写为 aa

如果是单独的名字 aa,但以 module 开头,则不添加 babel plugin,比如 module:aa

如果 @scope 开头,不包含 plugin,则会添加 babel-plugin,比如 @scope/mod 会变为 @scope/babel-plugin-mod

babel 自己的 @babel 开头的包,会自动添加 plugin,比如 @babel/aa 会变成 @babel/plugin-aa

插件封装demo

我们将上一节课程的例子,改成插件,如下:

const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);

const HaoroomsInsertConsolePlugin = ({ types, template }, options, dirname) => {
    return {
        visitor: {
            CallExpression(path, state) {
                if (path.node.isNew) {
                    return;
                }
                const calleeName = path.get('callee').toString();
                 if (targetCalleeName.includes(calleeName)) {
                    const { line, column } = path.node.loc.start;
                    const newNode = template.expression(`console.log("${state.filename || 'unkown filename'}: (${line}, ${column})")`)();
                    newNode.isNew = true;

                    if (path.findParent(path => path.isJSXElement())) {
                        path.replaceWith(types.arrayExpression([newNode, path.node]))
                        path.skip();
                    } else {
                        path.insertBefore(newNode);
                    }
                }
            }
        }
    }
}
module.exports = HaoroomsInsertConsolePlugin;

小结

本节课程就到这来,欢迎关注haorooms前端博客,通过本课程之后,你可以自己简单封装babel插件了。可以做一些简单的插件demo,针对自己的特殊场景使用。

相关文章: