返回 登录
1

《程序员》杂志精选:被低估的 Babel

阅读5621

作者: 袁德鑫,就职于饿了么大前端,目前在探索前端预编译阶段的无痕埋点。
责编:陈秋歌,关注前端开发领域,寻求报道或者投稿请发邮件chenqg#csdn.net。
本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅《程序员》

不仅仅是语法解析器,Babel更是一个平台。丰富的插件,让它的扩展变得无限可能。目前饿了么大前端部门正通过插件开发,将Babel应用到无痕埋点、错误日志收集等业务场景中。

概览

Babel 是一个 JavaScript 语法解析器。提到它,相信大家的第一反应是把我们写的 ECMAScript 6 代码转换成浏览器可识别的 ECMAScript 5 代码,而这也正是 Babel 的前身 6to5 名字的由来。随着 Babel 不断的发展,现在它已经不再仅仅为了 6to5 而存在。本文旨在跟大家聊一聊如今的 Babel 是怎样处理我们的代码,以及我们还能用 Babel 做些什么。

谁动了我的代码?

Babel 只是语法解析器

实际上说 Babel 是语法解析器并不严谨,因为 babel-core 仅仅是对外暴露了一些 API,而真实的解析器是 Babylon,但本文旨在以功能划分 Babel 的几个重要部分,遂将 Babylon 归入 babel-core。

说到 Babel,有一个不得不提的概念就是抽象语法树(Abstract Syntax Tree 或缩写为 AST),即代码的抽象语法结构的树状表现形式。Babel 就是通过 AST 来理解你的代码,并根据预设的规则来对这棵“树”进行编辑,然后将新的 AST 转换为代码。

图片描述

图1 Babel 的流程图

那么是 Babel 改动了我的代码吗?不。Babel 自 6.0 起,就不再对代码进行修改。作为一个平台,它本身只负责图1中的 parse 和 generate 流程,修改代码的 transform 过程全都交给插件去做。也就是说,Babel 只是一个语法解析器。很多初次使用 Babel 的开发者会问,“为什么我不配置插件 Babel 就不生效”,正是这个原因。

Plugin——转换的执行者

上面说到 Babel 的 plugin 才是真正改动代码的“元凶”,现在我们就来讲讲 Babel 是怎样改动代码的。

Babel 插件中有一个观察者(visitor)机制,我们可以在 visitor 中预设想要观察的 babel-types,然后对其进行操作。下面以源代码 foo === bar 为例:

module.exports = function ({ types: t }) {
  return {
    visitor: {
      BinaryExpression (path) {
        const isMatchCondition = path.node.operator !== '===' && // 若操作符不为 === 则不做任何操作
          t.isIdentifier(path.node.left, { name: 'foo' }) && // 若操作符左侧不为变量或变量名不为 foo 则不做任何操作
          t.isIdentifier(path.node.right, { name: 'bar' }) // 若操作符右侧不为变量或变量名不为 bar 则不做任何操作

        if (isMatchCondition) {
          path.node.left = t.identifier('sebmck') // 把操作符左侧的变量名改为 sebmck
          path.node.right = t.identifier('dork') // 把操作符右侧的变量名改为 dork
        }
      }
    }
  }
}

编写如上插件,我们的代码经过编译就会得到 sebmck === dork,一个最基本的 Babel Plugin 就大功告成啦。

Preset——转换规则的集合

相信大家对于 Babel Preset 都不陌生。很多开发者的项目中,Babel 的配置都会有一段 "presets": ["es2015"]。接下来我们就以 babel-preset-es2015 为例,一起来看看 preset 内部究竟是什么样子的。

打开 babel-preset-es2015 的源代码,你会发现整个 preset 其实就是一些插件的集合。如果说 plugin 是处理代码的规则,那么 preset 就是一组规则的集合。你完全可以自己拼装不同的插件,生成一个新的预设。

通过 Babel 可以做什么?

Babel 最基础和广泛的用法就是将 ES6 代码转换为 ES5 代码。除此之外,官方还提供了 JSX 语法支持、Flow 语法支持等插件。接下来我们要聊的是一些由社区开发,基于 Babel 或与 Babel 息息相关的开源插件。

Prepack——代码性能优化

前一阵子由 Facebook 团队推出的 Prepack 在前端圈可谓一石激起千层浪,在各类技术社区中也引发了激烈的讨论。本文暂且不谈现阶段 Prepack 是否适用于业务生产环境,只根据 Prepack 的特性和原理,聊一聊 Prepack 与 Babel 擦出了怎样的火花。

Prepack 是一个 JavaScript 代码优化工具。它能够完成一些可以在编译阶段执行的计算工作,将一部分代码替换为等价但更简单的赋值语句,从而省去大量的计算和对象分配工作。

我们先通过一张 Sebastian McKenzie 在 React-Europe 2016 分享 Prepack 的相关视频截图,来看一下 Prepack 的工作原理,如图2所示。

图片描述

图2 Prepack的工作原理

从上图可见,Prepack 的 Code => ASTAST => Code 都是通过 Babel 来完成的。当你想要在预编译阶段做一些事情的时候,自己做一个符合 ECMAScript 标准的编译器是不太明智的。Babel 完美地解决了这个问题,它既可以为业务开发者服务,又可以为编译工具开发者服务。

babel-plugin-import——指定库的按需加载

在 Webpack 2 推出 Tree Shaking 之前,按需引入一直是前端工程中令人头疼的问题,即使你写的是:

import { module } from 'library'

编译后的结果也会是:

var module = require('library').module

也就是说,模块并没有按需引入,依然加载了所有的代码。为此,ant-design 团队做了一个叫做 babel-plugin-import 的 Babel 插件,通过简单的配置就可以把如下代码:

import { Button } from 'antd'

编译成:

var _button = require('antd/lib/button')

这样就避免了只想用其中一个很小的模块,却要引入整个类库的尴尬。

具体的实现大家可以看它的源代码

babel-eslint——代码风格检查

相信大家对 ESLint 不会很陌生。ESLint 也是通过 AST 对代码进行解析,但 ESLint 团队使用的是自己开发的 Espree。这就导致当 Babel 支持了一种新的自定义语法(如 Flow)时,ESLint 无法直接支持,因此 babel-eslint 就出现了。

ESLint 团队在 Why another parser 中写道:

ESLint had been relying on Esprima as its parser from the beginning. While that was fine when the JavaScript language was evolving slowly, the pace of development increased dramatically and Esprima had fallen behind. ESLint, like many other tools reliant on Esprima, has been stuck in using new JavaScript language features until Esprima updates, and that caused our users frustration.

ESLint 团队曾经为了跟随 JavaScript 的快速发展而选择自行开发语法解析器,现在却又被其拖累,不得不再推出一个使用 Babel 作为解析器的版本。

定义一套语法

经常听到有开发者报怨 JavaScript 语言设计得太烂了,要是能用 XXX 语言来写前端就好了。那么现在机会来了,虽然 Babel 不能识别不符合 ECMAScript 标准的语法,但是它允许你自定义规则。也就是说,你完全可以自己定义一套语法,愉快地用 XXX 语言来做前端开发!

在饿了么大前端的一些实践

Babel 的平台化和插件化使它变得极易扩展。这种灵活性使得我们可以对它寄予无限的期望:不管你是想对代码进行转换,还是仅仅想引入一个语法解析器,Babel 都可以胜任。
除了社区提供的工具,目前饿了么大前端部门在尝试通过 Babel 插件实现一些对业务的扩展,比如:

  • 无痕埋点:目前主流的前端埋点方式还是手工打点。这样做的效率不高,而且每次添加新的埋点还需要发版和等待数据收集。我们正在尝试通过 Babel 插件注入打点代码,并在筛选数据时通过给函数名做标记的方式,随时获取过去一段时间内的埋点信息。

  • 错误日志收集:目前市面上的前端错误日志收集主要还是依赖于捕获错误的堆栈信息。生产环境上的代码都经过了混淆,这给识别工作带来了很大的麻烦。我们正在尝试在 catch 语句中直接注入源代码的信息,尽可能收集更加准确和详细的错误信息。

总结

说了这么多,就是希望大家改变对 Babel 的看法,不再仅仅把它当成一个 6to5 的工具来用。如果你想在预编译阶段做些什么,Babel 是你最好的选择。

近几年,前端在预编译阶段的应用取得了很不错的成绩,我相信在未来的一段时间,会有越来越多与 Babel 相关的好工具出现。

欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码申请入群。
图片描述

评论