jyzwf / blog

在Issues里记录技术得点滴
17 stars 3 forks source link

如何编写一个babel插件 #69

Open jyzwf opened 5 years ago

jyzwf commented 5 years ago

对于babel,相信没有哪个前端不知,它是当下前端开发的标配,可以让我们提早使用es6/7/8等新的js特性。它将使用最新标准编写的js代码向下编译使之能在各个浏览器中运行,实现源码到源码的编译。 本文主要介绍一些babel的基础,毕竟有了基础,才能熟练的使用或者编写出babel的插件,同时编写一个很是easy的babel的插件。

babel的一些常用包简介

babel-cli

它允许用户在命令行中编译js代码,可以直接运行 babel index.js,来编译js文件:

  1. --out-file 或者 -o 来指定编译后的输出文件
  2. --watch 或者 -w 来进行实时编译
  3. --source-maps 来生成 sourceMap 此外还能将 presetsplugins等写在命令行中,但并不推荐,最好使用 .babelrc或者在 package.json中的babel字段

babel-core

babel的核心功能就在这个包中,例如 transform 方法,这样我们就能在代码中直接使用编程的方式来转化js代码,实现形如组件库中代码的组件代码的预览功能。

babel-parser

babel的解析器,生成babel的ast树:

const parser = require('@babel/parser');
const code = `function square(n) {
  return n * n;
}`;
parser.parse(code);

image

它也接受一些配置项,如:

babel-traverse

用于遍历ast树,同时负责替换,移除或者添加节点,可以根据各个节点的不同类型,做一些不同的事

babel-types

babel的工具库,可以帮助我们了解ast的构造变换或者验证,编写babel插件免不了与他打交道。 该模块拥有每一个单一类型节点的定义,包括节点包含哪些属性,什么是合法值,如何构建节点、遍历节点,以及节点的别名等信息。babel Definitions

babel-generator

将ast 转化为代码并生成sourceMap:


const generate = require('@babel/generator').default;

const ast = {
    type: 'Program',
    start: 0,
    end: 38,
    body: [
        {
            type: 'FunctionDeclaration',
            start: 0,
            end: 38,
            id: {
                type: 'Identifier',
                start: 9,
                end: 15,
                name: 'square',
            },
            expression: false,
            generator: false,
            params: [
                {
                    type: 'Identifier',
                    start: 16,
                    end: 17,
                    name: 'n',
                },
            ],
            body: {
                type: 'BlockStatement',
                start: 19,
                end: 38,
                body: [
                    {
                        type: 'ReturnStatement',
                        start: 23,
                        end: 36,
                        argument: {
                            type: 'BinaryExpression',
                            start: 30,
                            end: 35,
                            left: {
                                type: 'Identifier',
                                start: 30,
                                end: 31,
                                name: 'n',
                            },
                            operator: '*',
                            right: {
                                type: 'Identifier',
                                start: 34,
                                end: 35,
                                name: 'n',
                            },
                        },
                    },
                ],
            },
        },
    ],
    sourceType: 'module',
};

const { code } = generate(ast);

console.log(code);
// output:
/*
function square(n) {
  return n * n;
}
*/

babel-template

这个类似于模板替换,将模板中的字符串转化为指定值:

const template = require('@babel/template').de;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({
    IMPORT_NAME: t.identifier('myModule'),
    SOURCE: t.stringLiteral('my-module'),
});

console.log(generate(ast).code);  // var myModule = require("my-module");

presets与plugins

presets

相当于一个帮你封装了plugins以及另外的presets的简便操作,省的你要将babel的配置到处重写一遍,它是从后往前执行的,区别于plugin从前往后执行,简单看下babel-preset-react中的配置:

import { declare } from "@babel/helper-plugin-utils";
import transformReactJSX from "@babel/plugin-transform-react-jsx";
import transformReactDisplayName from "@babel/plugin-transform-react-display-name";
import transformReactJSXSource from "@babel/plugin-transform-react-jsx-source";
import transformReactJSXSelf from "@babel/plugin-transform-react-jsx-self";

export default declare((api, opts) => {
  api.assertVersion(7);

  const pragma = opts.pragma || "React.createElement";  // 当编译jsx表达式的时候使用的替换函数
  const pragmaFrag = opts.pragmaFrag || "React.Fragment";  // 当编译 JSX fragments 时使用的替换组件
  const throwIfNamespace =
    opts.throwIfNamespace === undefined ? true : !!opts.throwIfNamespace;
  const development = !!opts.development;
  const useBuiltIns = !!opts.useBuiltIns;

  if (typeof development !== "boolean") {
    throw new Error(
      "@babel/preset-react 'development' option must be a boolean.",
    );
  }

  return {
    plugins: [
      [
        transformReactJSX,
        { pragma, pragmaFrag, throwIfNamespace, useBuiltIns },
      ],
      transformReactDisplayName,

      development && transformReactJSXSource,
      development && transformReactJSXSelf,
    ].filter(Boolean),
  };
});

可以看出,该preset内置了plugin-transform-react-jsxplugin-transform-react-display-name插件,同时在开发环境中内置了 plugin-transform-react-jsx-sourceplugin-transform-react-jsx-self

好了介绍了一些基础知识,就该来编写一个插件了。 之前在项目开发的时候,经常要在代码中写debugger来打断点,但写着写着,很容易忘记把这些debugger给注释或者删除,所以我们来写一个删除debugger的babel plugin

我们先来看看,有debugger时的代码其ast树是如何的: image

可以看见,在ast中存在一个 DebuggerStatement类型的节点,当我们把该debugger注释或者删除之后,看下其ast类型是如何的: image 所以,该插件只要把该节点给删除了就好了,下面是直接加一个文件中演示的结果,可以看见,它返回结果是把debugger给删除了: image

看起来OK,接着我们将其在实际项目中试试,babel的插件必须以 babel-plugin-* 开头,同时由于该插件是转化了代码,所以我们就将其命名为 babel-plugin-transform-remove-debugger

module.exports = function(babel){
    return {
        visitor:{
             DebuggerStatement(path, state) { // 用户的配置项可以通过state来获取
                 path.remove();
             },
        }
    }
}

我们直接在create-react-app中看看效果:

class App extends Component {
    clickImage = () => {
        debugger;
        console.log('点击了');
    };
    render() {
        return (
            <div className="App">
                <button onClick={this.clickImage}>点我</button>
            </div>
        );
    }
}

将我们的插件配置其中: image 在点击按钮的时候可以看见,并没有出现断点,所以我们的插件是ok的,至此我们就完成了一个插件,是不是很简单??后面当我想把该插件发布的时候,发先npm上已经有一个了,这就勾起了我的好奇心,想知道实现上有何不同,果然实现思路都是一样,就多了一个命名与严格模式,哈哈。

关于babel,自己还会继续深入学习,看看其他著名的插件是如何实现的,并会做记录。

下面的 Babel 用户手册Babel 插件手册是干货,读完之后你会更加深刻学习到babel的内部,同时如何更好的实现一个plugin

参考资料

Babel 用户手册 Babel 插件手册 从零开始编写一个babel插件