Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何编写一个babel插件 #69

Open
jyzwf opened this issue Mar 3, 2019 · 0 comments
Open

如何编写一个babel插件 #69

jyzwf opened this issue Mar 3, 2019 · 0 comments

Comments

@jyzwf
Copy link
Owner

jyzwf commented Mar 3, 2019

对于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

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

  • allowImportExportEverywhere:一般来说我们将import 只能写在代码的顶部,但如果将其配置为 true,那么你可以写在任何一个允许声明的地方
  • allowAwaitOutsideFunction:是否允许在async 函数外写await
  • allowSuperOutsideMethod:是否允许在类或者对象之外使用super
  • plugins:编译时使用的插件,如:plugins:['jsx']:支持jsx语法编译

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插件

@jyzwf jyzwf changed the title 如何编写一个babel插件 如何编写一个babel插件 Mar 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant