使用Babel变换在构建时注入分析代码

2020-07-07 02:23:15

在2018年末,我们决定增加对Reaction Native in Heap的一流支持。这意味着将Heap的自动捕获理念引入React Native平台:在Reaction Native应用程序上安装Heap应该意味着捕获与该应用程序的所有用户交互。这包括点击、更改文本字段等。

这篇文章将介绍我们是如何做到这一点的:添加定制代码作为应用程序构建过程的一部分。我们将讨论抽象语法树、如何构建Babel插件以将代码注入到Reaction Native,以及我们在此过程中使用的一些工具。

在Web上,Heap通过向整个DOM添加onClick事件侦听器来捕获站点点击(Siteclicks)等上的所有用户行为。在iOS上,Heap通过方法切换安装了几个关键UIKit API的自定义实现。但是Reaction Native没有提供任何类型的全局钩子,我们可以使用它来自动捕获用户交互。

使自动捕获工作的一种简单方法是创建我们自己的Reaction Native repo分支,并发布一个包含自定义代码更改的包。然而,这将是一个很大的维护负担,因为每次Facebook发布新的Reaction Native版本时(不仅仅是当该特定的Reaction Native代码更改时),我们都需要发布新的Reaction Native Heap SDK版本。更广泛地说,这将是一个糟糕的开发人员体验。

集思广益之后,我们想出了一个更好的主意:在构建时将代码注入到Reaction Native。Reaction Native Metro捆绑程序捆绑了几乎所有Reaction Native应用程序的Javascript代码,因为它是框架的默认Javascript捆绑程序。对于像JSX这样的语法特性,Metro捆绑器使用Babel,这是一个源代码到源代码编译器,可以将像JSX和实验性语言特性这样的东西转换成普通的Javascript。

Babel在内部对抽象语法树进行操作以执行其编译。抽象语法树(AST)是源代码的语法结构以树的形式表示。很多处理代码的工具都使用AST:编译器、解释器、链接器和格式器。例如,代码格式化程序更漂亮,它通过将代码解析成AST,然后以预定义的样式重新打印AST来自动格式化源代码。

在研究和探索AST时,我们发现ASTExplorer.net特别有用。它允许您粘贴源代码并选择用于源代码(语言、解析器、转换)的配置,它将为该代码生成AST,并且对于转换,显示结果代码。

我们将在这篇文章中经常使用ASTExplorer,因为我们展示了如何为Heap构建解决方案。

就其本身而言,Babel不会做任何事情;但它实际上是相同的代码-&>;相同的代码。要让它做任何事情,我们需要插件,这些插件将Babel配置为以特定方式对AST执行操作。Babel公开了许多允许用户遍历和修改AST节点的API。

您可以使用现有的插件,也可以编写自己的插件。例如,现有的求幂运算符插件采用如下代码:

巴别塔变换使用访问者模式。当Babel访问特定类型的节点时,Babel调用为该节点类型提供的相应函数。Babel将路径对象(到被访问节点的路径表示)传递给该函数,以允许访问被访问节点。

对于求幂运算符示例,我们可以通过实现BinaryExpression节点的函数,将看起来像x**y的二进制表达式转换成看起来像Math.pow(x,y)的代码:

现在我们已经具备了使用Babel修改源代码的AST所需的基本工具,让我们注入一些插装代码。

我们可以捕获的最基本的交互是触摸可触摸的组件。这些是用户可以通过触摸它们与之交互的组件。示例包括TouchableOpacity、TouchableNativeFeedback和TouchableHighlight。在这篇文章中,我们将重点关注TouchableOpacitys。

如果您手动标记一个可触摸组件,您可能会向该可触摸组件的onPress处理程序添加一些类似analytics.trace(‘Toured Button’)的代码。例如:

我们不想将插装注入到应用程序代码中(就像我们在上面添加了跟踪代码的onPress应用程序处理程序),因为代码结构在不同的应用程序之间可能会有很大差异,而且我们无法看到代码的实际外观。

相反,我们希望在Reaction Native库中找到一个在触摸TouchableOpacity时始终会触发的位置。一个很好的位置可能是在TouchableOpacity组件中调用onPress的地方:

既然我们知道了要将检测代码注入到哪里,那么让我们编写一些代码来实现这一点。

因此,我们希望将一些代码注入到这个特定的方法中,但是我们如何以编程方式将该方法标识为正确的位置呢?当然,它调用onPress,但我们不能只检测调用另一个名为onPress的函数的所有函数。让我们更多地看看周围的代码:

使用上下文,我们可以找出几个地标,告诉我们这就是我们想要进行测试的地方:

该方法是TouchableHandlePress,它作为渲染的Animated.View的onclick道具传入。

为简单起见,让我们删除一些不相关的代码行,如其他方法、注释和导入:

让我们找出AST中与每个要点相对应的部分来进行推理:

有一个键名为Mixins的ObjectProperty,在该子树中有一个标识符Touchable

有一个ID名为TouchableOpacity的VariableDeclarator。但是,由于我们希望最终将我们的解决方案应用于其他Touchable,因此我们将忽略这一点。

虽然这些都是相关的,但检测的目标节点是touch chableHandlePress函数。让我们重新表述这些AST功能以与此节点相关:

该节点有一个父节点,该父节点是CallExpression,其被调用方名称为createReactClass。

正如您可能认为的那样,这种方法有点启发式。Reaction Native库中的代码可以并且确实会更改,我们有时确实需要更新我们的插件来处理这些代码更改。类似地,如果非反应原生代码与我们的插件正在寻找的AST模式匹配,我们可能也会检测该代码,尽管这是不太可能的。

现在我们知道了要在AST中查找什么模式,让我们编写一些代码来查找我们的目标节点。

让我们从向基本的Babel转换访问器添加一个方法开始。在我们的示例中,我们正在寻找一个ObjectProperty节点,因此让我们从一个为所有ObjectPropertys执行的函数开始:

接下来,我们知道我们正在查找的节点有一个键名为TouchableHandlePress,所以让我们添加一个条件来检查这一点:

接下来,我们希望查看该节点是否有一个父节点,该父节点是被调用方名称为createReactClass的CallExpression。我们可以使用巴别塔路径上的findParent方法来完成此操作:

最后,我们要检查该节点是否有与Touble Mixin同级的节点。让我们在帮助器中实现此逻辑。

我们可以访问路径容器字段中的节点同级数组。我们可以通过检查节点是否属于ObjectProperty类型,以及是否具有键名Mixins和值类型ArrayExpression来在此数组中搜索Mixins节点。

找到Mixins节点后,我们需要检查它是否包含可触摸的标识符。我们可以通过使用另一个Babel访问器调用Traverse来遍历节点子树来实现这一点,并提取一些状态:

我们现在知道当前节点是我们需要注入代码的位置。所以让我们注入我们的仪器。

我们将使用babel-types软件包为我们的检测创建新的AST节点:

让我们从包装原始函数并调用它开始。如果我们正常编写代码,则可以通过调用函数的call_property来调用函数对象:

让我们对此函数执行此操作。我们将首先构建一个成员表达式(即访问call属性),然后使用下面的参数和event参数调用该表达式:

接下来,让我们构建要注入的代码。我们可以通过创建多个AST节点来创建此CallExpression,但为了简单和可读性起见,我们使用Babel模板来完成此操作:

现在让我们把它们放在一起。我们也将对此使用模板:

最后,让我们从刚刚创建的函数体构建一个新函数:

现在,我们有了一个新函数,它包装了原始函数,并包含了一些插装代码,但它实际上还不是AST的一部分-它只是我们创建的一个新AST。我们需要使用这个新函数来替换旧函数:

就是这样!“我们已经用我们的检测代码用等价的函数替换了原始函数。点击此处查看完整的插件。

现在我们已经编写了插件代码,让我们对其进行测试。我们可以从对Reaction Native库中的TouchableOpacity.js文件运行此转换开始。以下是该文件在没有转换的情况下的外观:

让我们通过默认插件(即模块中包含的插件:Metro-Reaction-Native-Babel-Preset Preset)和我们的插件运行该文件。我们可以使用Babel CLI执行此操作:

看起来起作用了!*让我们实现检测处理程序并运行应用程序:

在这里,我们可以从中提取元数据(表示用户触摸的组件)和电子邮件(交互触发的事件),以创建并发送一个原始事件,我们可以稍后使用它进行分析。

正如我们已经看到的,巴别塔插件可以是强大的。您可以将我们今天讨论的方法应用于应用程序性能检测之类的事情,比如自动计时onPress处理程序。或者您可能想要构建一个插件来创建一些新的Javascript语法,比如求幂。或者,您可以开发自己的ESLint规则。或者,如果您需要自动化大规模的代码更改,比如使用新的API,在升级依赖项之后自动修复中断更改,或者需要进行大型重构,您可以使用像jcodeshift和codemode-js这样的工具来使用Babel转换来更新整个代码库。

如果您希望构建自己的插件,或者想更深入地了解编写Babel插件,请务必阅读Babel插件手册。在我学习巴别塔的时候,这个资源是无价的。