也就前两天,面试大厂,其中有那么一个问题:
- 你了解过Babel吗?
了解过抽象语法树,又称AST,有学习过,也写过一个基于AST的乞丐版模板引擎,先是词法解析token,然后生产抽象语法树,然后更改抽象语法树,当然这是插件做的事情,最后根据新的AST生成代码。
- 写过Babel插件吗
没有,只是看过相关文档
- 如果让你写一个插件,你能写的出来吗?
应该可以吧…
遂卒…
开玩笑的,既然提到了,又没回答上来什么,哎哟我这暴脾气,一想到今晚就睡不着,连夜把它撸了。
那么我们来从零写个插件吧。
写一个预计算简单表达式的插件
预览
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
这里我们看到 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树
你会发现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
大概是最简单的Babel插件入门教程了吧,4点了,睡觉
赞,不错! 来自react native cnode
如果只是单纯因为没用过babel把你淘汰了,我觉的这个面试官水平也有问题。维度太低了。
来自酷炫的 CNodeMD
大厂就这个格局?
写的不错。但原因绝不会只因为这个。有兴趣可以来我这边试试。
南宁的老弟,不容易
学到了
面试的职位是哪个方面啊?
写的不错。但原因绝不会只因为这个。有兴趣可以来我这边试试。
赞,学习到了
AST 的用处挺广的,除了 Babel,还会用在 WebPack,CodeMod,ESLint 等地方。 我写过后面 2 个:
- CodeMod( jscodeshift)用于编写代码自动升级工具
- ESLint 用于自定义静态代码检查,如最近刚写了个:
感觉楼主的面试回答没毛病啊。
为楼主的深夜撸码点个赞
@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 应用即可。
ts + egg 这块的 RFC 我正在编写中,回头欢迎加入一起讨论:
- 不需要手写 d.ts,egg loader 导致的类型丢失,可以通过 egg-bin 这层自动生成 d.ts 解决
- ts-node 那个我也看了,直接加载 ts 文件的话要修改 loader,这块还需要 @popomore 那边加进来讨论,现在是直接 tsc 编译即可。
效果如下面这张动图:
没写过babel插件,不过会用babylon+babel-traverse+babel-types+babel-generator进行js的增删改查,或者用parse5进行html的增删改查。插件估计就是套个function然后return一下generator.code吧?如果我面试被问到的话估计会这么回答
回答 babylon 会不会加分
@liaoyinglong dom.childNodes.find+dom.nodeName.match 你自己查一下api
好帖子,顶一下。
面阿里的时候也被问过。。。。就模糊的答了下AST
厉害,一直都不知道Balel也可以自己写插件
@liaoyinglong find功能不就是类似于traverse 的visitor吗?你是有什么功能用find不能实现吗?还有这也不是直接操作dom啊。。。只不过api比较像而已啊,你仔细读读文档
感觉凭楼主这暴脾气,以后定当前途无量!北京小厂欢迎你
.......
什么级别的职位?
@atian25 看你github上最近关注了他,没想到这么快就下手了😂
@flyguolai 最近老板给了不少 HC,有兴趣的欢迎勾搭
@atian25 二维码过期了T-T 求新的
@sfyr111 done
就需要楼主这种暴脾气。谢谢,学到了。
@atian25 vscode 主题是什么?
@yangchongduo 默认的
@atian25 T-T 超过100人扫不进去了,微信号 sfyr111 求拉一波^^
@sfyr111 我拉你~
群主是 @thonatos 你们后面找他就好了,我只是一个潜水的。
头像 罪恶王冠 好评!!!!
以前研究过一阵子,但是后来放弃了。打算最近再来温习一下、、。、
学习了 ,以前只知道es6 要用babel转义下,转成es5
能不能也拉我下,微信号zwg536165796 @thonatos
@AsceticBoy 👌🏻 From Noder