精华 面试官: 你了解过Babel吗?写过Babel插件吗? 答: 没有。卒
发布于 7 年前 作者 axetroy 20137 次浏览 来自 分享

也就前两天,面试大厂,其中有那么一个问题:

  1. 你了解过Babel吗?

了解过抽象语法树,又称AST,有学习过,也写过一个基于AST的乞丐版模板引擎,先是词法解析token,然后生产抽象语法树,然后更改抽象语法树,当然这是插件做的事情,最后根据新的AST生成代码。

  1. 写过Babel插件吗

没有,只是看过相关文档

  1. 如果让你写一个插件,你能写的出来吗?

应该可以吧…

遂卒…

开玩笑的,既然提到了,又没回答上来什么,哎哟我这暴脾气,一想到今晚就睡不着,连夜把它撸了。

那么我们来从零写个插件吧。

写一个预计算简单表达式的插件

预览

Before:

const result = 1 + 2 + 3 + 4 + 5;

After:

const result = 15;

以上的例子可能大家不会经常遇到,因为傻x才会这么写,但是有可能你会这么写

setTimeout(function(){
  // do something
}, 1000 * 2) // 插件要做的事,就是把 1000 * 2 替换成 2000

前提条件

开工

再写代码之前,你需要明白Babel它的原理,简单点说: Babel解析成AST,然后插件更改AST,最后由Babel输出代码

那么Babel的插件模块需要你暴露一个function,function内返回visitor

module.export = function(babel){
  return {
    visitor:{
    }
  }
}

visitor是对各类型的AST节点做处理的地方,那么我们怎么知道Babel生成了的AST有哪些节点呢?

很简单,你可以把Babel转换的结果打印出来,或者这里有传送门: AST explorer

1

这里我们看到 const result = 1 + 2中的1 + 1是一个BinaryExpression节点,那么在visitor中,我们就处理这个节点

var babel = require('babel-core');
var t = require('babel-types');

const visitor = {
  BinaryExpression(path) {
    const node = path.node;
    let result;
    // 判断表达式两边,是否都是数字
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
      // 根据不同的操作符作运算
      switch (node.operator) {
        case "+":
          result = node.left.value + node.right.value;
          break
        case "-":
          result = node.left.value - node.right.value;
          break;
        case "*":
          result =  node.left.value * node.right.value;
          break;
        case "/":
          result =  node.left.value / node.right.value;
          break;
        case "**":
          let i = node.right.value;
          while (--i) {
            result = result || node.left.value;
            result =  result * node.left.value;
          }
          break;
        default:
      }
    }

    // 如果上面的运算有结果的话
    if (result !== undefined) {
      // 把表达式节点替换成number字面量
      path.replaceWith(t.numericLiteral(result));
    }
  }
};

module.exports = function (babel) {
  return {
    visitor
  };
}

插件写好了,我们运行下插件试试

const babel = require("babel-core");

const result = babel.transform("const result = 1 + 2;",{
  plugins:[
    require("./index")
  ]
});

console.log(result.code); // const result = 3;

与预期一致,那么转换 const result = 1 + 2 + 3 + 4 + 5;呢?

结果是: const result = 3 + 3 + 4 + 5;

这就奇怪了,为什么只计算了1 + 2之后,就没有继续往下运算了?

我们看一下这个表达式的AST树

2

你会发现Babel解析成表达式里面再嵌套表达式。

表达式( 表达式( 表达式( 表达式(1 + 2) + 3) + 4) + 5)

而我们的判断条件并不符合所有的,只符合1 + 2

    // 判断表达式两边,是否都是数字
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {}

那么我们得改一改

第一次计算1 + 2之后,我们会得到这样的表达式

表达式( 表达式( 表达式(3 + 3) + 4) + 5)

其中 3 + 3又符合了我们的条件, 我们通过向上递归的方式遍历父级节点

又转换成这样:

表达式( 表达式(6 + 4) + 5)
表达式(10 + 5)
15
    // 如果上面的运算有结果的话
    if (result !== undefined) {
      // 把表达式节点替换成number字面量
      path.replaceWith(t.numericLiteral(result));

      let parentPath = path.parentPath;

      // 向上遍历父级节点
      parentPath && visitor.BinaryExpression.call(this, parentPath);
    }

到这里,我们就得出了结果 const result = 15;

那么其他运算呢:

const result = 100 + 10 - 50 >>> const result = 60;

const result = (100 / 2) + 50 >>> const result = 100;

const result = (((100 / 2) + 50 * 2) / 50) ** 2 >>> const result = 9;

完结

到这里,已经向你大概的讲解了,如何编写一个Babel插件,再也不怕面试官问我答不出什么了哈…

你以为这就完了吗?

并没有

如果转换这样呢: const result = 0.1 + 0.2;

预期肯定是0.3, 但是实际上,Javascript有浮点计算误差,得出的结果是0.30000000000000004

那是不是这个插件就没卵用?

这就需要你去矫正浮点运算误差了,可以使用Big.js;

比如: result = node.left.value + node.right.value; 改成 result = +new Big(node.left.value).plus(node.right.value);

你以为完了吗? 这个插件还可以做很多

比如: Math.PI * 2 >>> 6.283185307179586

比如: Math.pow(2, 2) >>> 4

最后上项目地址: https://github.com/axetroy/babel-plugin-pre-calculate-number

51 回复

大概是最简单的Babel插件入门教程了吧,4点了,睡觉

帮我关下灯,谢谢

如果只是单纯因为没用过babel把你淘汰了,我觉的这个面试官水平也有问题。维度太低了。

来自酷炫的 CNodeMD

大厂就这个格局?

写的不错。但原因绝不会只因为这个。有兴趣可以来我这边试试。

南宁的老弟,不容易

面试的职位是哪个方面啊?

写的不错。但原因绝不会只因为这个。有兴趣可以来我这边试试。

赞,学习到了

AST 的用处挺广的,除了 Babel,还会用在 WebPack,CodeMod,ESLint 等地方。 我写过后面 2 个:

感觉楼主的面试回答没毛病啊。

为楼主的深夜撸码点个赞

@zy445566 @yuu2lee4 @atian25

主要是其他的没回答好,能力有限,面试官很nice了

  • 你是怎么在项目做工程化的

  • 你项目的架构是怎么样

小公司哪有什么架构…

  • 如何编写一个公共库

根据自己编写过的库的经验回答,但是在大厂看来,是不规范的

  • 你了解过Javascript哪些前沿的技术

Prettier、Prepack、(区块链,机器学习…), 刚讲完前面那两个,估计觉得太low,没让我讲下去了2333

@axetroy 来广州的话可以试试我这边

@atian25 egg-bin check这个,以前我就有这样的困惑,既然"约定大于规范", 都是约定好的。

那么开发者怎么知道你约定了哪些,每次都得去翻阅文档,不然很容易写错,这不像Typescript有IDE的提示。

同样的还有service。service写在指定目录,然后this.ctx.xxx获取服务,但是IDE无法提示这个服务有哪些方法,开发阶段无法发现错误,是否可以通过VSCode的插件做这个。像这样,使用Typescript的依赖注入就很好的解决了问题

@atian25 广州哪里可以投递呢

@atian25 感觉扫描的约定是不错,但是对 TS 的支持不太好,因为通过 load 加载会类型丢失,还要自己加类型声明,我觉得应该 TS 优先会更好,要是有什么机制可以把这些声明自动加到 tsserver 里面就好了。直接加入 ts-node/register 就可以支持导入 ts ,但是因为我看 loadconfig 里面都是把后缀给写死了的,所以 config 根本加载不进去。

@axetroy 你在 「Egg 非官方自助交流群」 么?在的话加下我微信。

egg-bin check这个,以前我就有这样的困惑,既然"约定大于规范", 都是约定好的。 那么开发者怎么知道你约定了哪些,每次都得去翻阅文档,不然很容易写错,这不像Typescript有IDE的提示。

我曾经试过去写 vscode 扩展,写了一半就停了,其实直接写 d.ts 就可以了。 当时想过通过 jsdoc + vscode 扩展等去支持智能提示,后面觉得太费劲了,还不如直接用 TS 写 egg 应用即可。

@MiYogurt @axetroy

ts + egg 这块的 RFC 我正在编写中,回头欢迎加入一起讨论:

  • 不需要手写 d.ts,egg loader 导致的类型丢失,可以通过 egg-bin 这层自动生成 d.ts 解决
  • ts-node 那个我也看了,直接加载 ts 文件的话要修改 loader,这块还需要 @popomore 那边加进来讨论,现在是直接 tsc 编译即可。

效果如下面这张动图: ts-egg

@MiYogurt 装饰器这块,是一个语法糖类型的,目前不是我思考 ts-egg 的关键,主要是为了解决 egg loader 导致的类型丢失问题和一些开发辅助。

至于装饰器的,社区也讨论下,就是一个额外的 lib 而已。

内部有一些团队如 @okoala 那边是比较重度使用装饰器,这点我个人还持保留意见, 我个人倾向于使用几个小语法糖就够了,不会像 nest 那样过度使用,最多也就是 @Query<SomeType> 这样的入参校验和路由。

@atian25 不在哦, 哪里申请

来自酷炫的 CNodeMD

@atian25 加了

来自酷炫的 CNodeMD

没写过babel插件,不过会用babylon+babel-traverse+babel-types+babel-generator进行js的增删改查,或者用parse5进行html的增删改查。插件估计就是套个function然后return一下generator.code吧?如果我面试被问到的话估计会这么回答

回答 babylon 会不会加分

@xtx1130 老哥,问一下,parse5生成的ast树,有什么好的方法遍历吗?有无类似babel-traverse的这个方法暴露出来?

最近在做一个将html文件的中文替换成英文的事情。

如果没有其他方法只能自己递归了 = =

@liaoyinglong dom.childNodes.find+dom.nodeName.match 你自己查一下api

@xtx1130 3q 这个看过了,是类似于jq这种直接操作dom的,不太想要这种方法。

有无方法直接遍历ast树的?

好帖子,顶一下。

面阿里的时候也被问过。。。。就模糊的答了下AST

厉害,一直都不知道Balel也可以自己写插件

@liaoyinglong find功能不就是类似于traverse 的visitor吗?你是有什么功能用find不能实现吗?还有这也不是直接操作dom啊。。。只不过api比较像而已啊,你仔细读读文档

感觉凭楼主这暴脾气,以后定当前途无量!北京小厂欢迎你

什么级别的职位?

@atian25 看你github上最近关注了他,没想到这么快就下手了😂

@flyguolai 最近老板给了不少 HC,有兴趣的欢迎勾搭

@atian25 二维码过期了T-T 求新的

就需要楼主这种暴脾气。谢谢,学到了。

@atian25 vscode 主题是什么?

@atian25 T-T 超过100人扫不进去了,微信号 sfyr111 求拉一波^^

群主是 @thonatos 你们后面找他就好了,我只是一个潜水的。

以前研究过一阵子,但是后来放弃了。打算最近再来温习一下、、。、

学习了 ,以前只知道es6 要用babel转义下,转成es5

能不能也拉我下,微信号zwg536165796 @thonatos

赞 写的很明白 多谢

// 单纯的计算的话,在退出节点时处理就行了
visitor: {
    BinaryExpression: {
	// 退出节点
        exit(path) {
            const node = path.node;
            let result;
            // 判断表达式两边,是否都是数字
            if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
                // 根据不同的操作符作运算
                switch (node.operator) {
                    case "+":
                        result = node.left.value + node.right.value;
                        break;
                    case "-":
                        result = node.left.value - node.right.value;
                        break;
                    case "*":
                        result = node.left.value * node.right.value;
                        break;
                    case "/":
                        result = node.left.value / node.right.value;
                        break;
                    case "**":
                        let i = node.right.value;
                        while (--i) {
                            result = result || node.left.value;
                            result = result * node.left.value;
                        }
                        break;
                    default:
                }
            }

            // 如果上面的运算有结果的话
            if (result !== undefined) {
                // 把表达式节点替换成number字面量
                path.replaceWith(t.numericLiteral(result));
            }
        }
    }
}
回到顶部