内容简介:In late 2018, we decided to add first-class support for React Native in Heap. This meant bringing Heap’s autocapture philosophy to the React Native platform: installing Heap on a React Native app should mean that all user interactions with the app are capt
In late 2018, we decided to add first-class support for React Native in Heap. This meant bringing Heap’s autocapture philosophy to the React Native platform: installing Heap on a React Native app should mean that all user interactions with the app are captured. This includes taps, changes to text fields, and more.
This post will look at how we did this: adding custom code as part of the app build process. We’ll talk about abstract syntax trees, how we built a Babel plugin to inject code into React Native, and some of the tools we used along the way.
Why code injection?
On the web, Heap captures all user behavior on a siteclicks, etc. by adding an onclick event listener to the entire DOM. On iOS, Heap installs custom implementations of a few key UIKit APIs via method swizzling . But React Native doesn’t provide any sort of global hook we can use to autocapture user interactions.
An easy way to make autocapture work would be to create our own fork of the React Native repo, and release a package with the custom code changes. This would be a large maintenance burden, however, since we’d need to release a new React Native Heap SDK version every time Facebook releases a new React Native version (not just when that particular piece of React Native code changes). More generally, this would be a bad developer experience.
After brainstorming, we came up with a better idea: inject code into React Native at build time . The React Native Metro Bundler bundles Javascript code for almost all React Native apps, since it is the default Javascript bundler for the framework. For syntax features like JSX , the Metro Bundler uses Babel , a source-to-source compiler that transforms things like JSX and experimental language features into plain Javascript.
Abstract Syntax Trees and ASTExplorer
Babel operates on Abstract Syntax Trees internally to perform its compilation. An Abstract Syntax Tree (AST) is a representation of the syntactic structure of source code in the form of a tree. Lots of tools that work with code use ASTs: compilers, interpreters, linters, and formatters. For example, the code formatter Prettier auto-formats source code by parsing the code into an AST, then re-printing the AST in a predefined style.
When looking at and exploring ASTs, we found ASTExplorer.net to be especially useful. It allows you to paste in source code and select the config used for the source code (language, parser, transforms) and it will generate the AST for that code, and, for transforms, show the resultant code.
We’ll be using ASTExplorer often in this post as we show how we built the solution for Heap.
Using Babel to modify code
On its own, Babel doesn’t do anything; it’s effectively same code in -> same code out. To make it do anything, we need plugins , which configure Babel to perform operations against the AST in a specific way. Babel exposes a number of APIs that allow the user to traverse and modify AST nodes.
You can either use existing plugins, or write your own. For example, the existing exponentiation-operator plugin takes code that looks like this:
and makes it look like this:
Babel transforms use a visitor pattern. When Babel visits a node of a specific type, Babel calls the corresponding function provided for that node type. Babel passes a path
object (the representation of the path to the visited node) to this function to allow for accessing the visited node.
For the exponentiation operator example, we can transform binary expressions that look like x ** y
into code that looks like Math.pow(x, y)
by implementing a function for BinaryExpression
nodes:
Now that we have the basic tools we need to modify source code’s AST using Babel, let’s inject some instrumentation code.
So we want to inject some code – but where?
The most basic interaction we can capture is a touch on a Touchable
component . These are components that users can interact with by touching on them. Examples include TouchableOpacity
, TouchableNativeFeedback
, and TouchableHighlight
. For the purposes of this post, we’ll be focusing on TouchableOpacity
s .
If you were manually tagging a Touchable
component, you’d probably add some code that looks like analytics.track(‘touched button’)
to the onPress
handler for that Touchable
. For example:
We don’t want to inject instrumentation into app code (like the onPress
handler we added tracking code to above), since code structure can vary widely between apps, and we don’t have visibility into what the code would actually look like.
Instead, we want to find a spot in the React Native library that will always fire when a TouchableOpacity
is touched. A good spot is probably where onPress
is called within the TouchableOpacity
component:
Check out the React Native source code here .
Now that we know where we want to inject our instrumentation code, let’s write some code to do that.
Writing the Babel Plugin
So we want to inject some code into this particular method, but how do we programmatically identify this method as the right spot? Sure, it calls onPress
, but we can’t just instrument all functions that call another function called onPress
. Let’s look at a bit more of the surrounding code:
Using the context, we can pull out a few landmarks that tell us this is where we want to instrument:
- It’s a function inside an object assigned to a var called
TouchableOpacity
. - That object is passed to
createReactClass
- There’s a
Touchable
object inside themixins
array - The method is
touchableHandlePress
, which is passed in as theonClick
prop for the renderedAnimated.View
.
Let’s transfer these landmarks over to the AST for this component.
First, we’ll copy the source file contents into AST Explorer:
For simplicity, let’s remove some of the irrelevant lines of code, like other methods, comments, and imports:
Now we have this AST:
Let’s identify the parts of the AST that correspond to each bullet point for our reasoning:
- There’s an
ObjectProperty
with a key name oftouchableHandlePress
- There’s a
CallExpression
where the callee name iscreateReactClass
- There’s an
ObjectProperty
with a key name ofmixins
, and within that subtree, there’s an identifier ofTouchable
- There’s a
VariableDeclarator
with an id name ofTouchableOpacity
. However, since we want to eventually apply our solution to other Touchables, we’ll ignore this.
While each of these is relevant, the target node for instrumentation is the touchableHandlePress
function. Let’s rephrase these AST features to relate to this node:
- We’re looking for an
ObjectProperty
node where the key name istouchableHandlePress
- That node has a parent that is a
CallExpression
with a callee name ofcreateReactClass
- The node has a sibling
ObjectProperty
node with the following properties:- Has a key name of
mixins
- Has a value that is of type
ArrayExpression
- Contains an identifier with name
Touchable
within thatArrayExpression
- Has a key name of
As you might be thinking, this approach is a bit of a heuristic. Code in the React Native library can and does change, and we do occasionally need to update our plugin to handle these code changes. Similarly, if non-React Native code matches the AST pattern our plugin is looking for, we would potentially instrument this code, too, though this is unlikely.
Now that we know what pattern to look for in the AST, let’s write some code to find our target node .
Let’s start by adding a method to a basic Babel transform visitor. In our case, we’re looking for an ObjectProperty
node, so let’s start with a function that executes for all ObjectProperty
s:
Next, we know that the node we’re looking for has a key name of touchableHandlePress
, so let’s add a conditional that checks this:
Next, we want to see if the node has a parent that’s a CallExpression
with a callee name of createReactClass
. We can do this using the findParent
method on the Babel path
:
Finally, we want to check if this node has a sibling with the Touchable
mixin. Let’s implement this logic in a helper.
We can access an array of node siblings in the path
s container
field. We can search this array for the mixins
node by checking if the node is of type ObjectProperty
, and has key name mixins
and value type ArrayExpression
.
Once we find the mixins
node, we need to check if it contains a Touchable
identifier. We can do this by traversing the node subtree by calling traverse
with another babel visitor, and extract some state:
See the source code for the solution up to this point here .
Injecting the Code
We now know the current node is where we need to inject code. So let’s inject our instrumentation.
We want to create a new function that:
- Calls the Heap library with event metadata
- Calls the original function
We’ll be using the babel-types
package to create new AST nodes for our instrumentation:
Let’s start by wrapping the original function, and calling it. If we were writing code normally, we could call a function object by calling the function’s call
property :
Let’s do that for this function. We’ll start by building a member expression (i.e. accessing the call
property), and then call that expression with this
and the event
argument:
Next, let’s build out the code to inject. We could create this CallExpression
by creating a number of AST nodes, but for simplicity and readability, let’s use Babel templating to do this:
Now let’s put it all together. We’ll use templating for this, too:
Lastly, let’s build a new function from the function body we’ve just created:
Now, we have a new function that wraps the original function, and contains some instrumentation code, but it’s not actually part of the AST yet – it’s just a new AST we’ve created. We need to use this new function to replace the old function:
And that’s it! We’ve replaced the original function with an equivalent function with our instrumentation code. Check out the complete plugin here .
Testing it out
Now that we’ve written the plugin code, let’s test it out. We can start by running this transform against the TouchableOpacity.js
file in the React Native library. This is what that file looks like with no transformation:
Let’s run this file through default plugins (i.e. the plugins included in the module:metro-react-native-babel-preset
preset) and our plugin. We can do this with the Babel CLI:
This should output the following:
Looks like it works! Let’s implement the instrumentation handler and run the app:
Check out the full solution here .
From here, we can extract metadata from this
(which represents the component the user touched) and e
(the event the interaction triggered) to create and send a raw event we can use later for analysis.
Conclusion / Wrapping it up
As we’ve seen, Babel plugins can be powerful. You could apply the approach we discussed today to things like application performance instrumentation, like automatically timing your onPress
handlers. Or maybe you want to build a plugin to create some new Javascript syntax, like exponentiation. Or you could develop your own ESLint rule . Or, if you need to automate a large-scale code change, such as using a new API, automating fixes for breaking changes after upgrading a dependency, or a large refactor, you can use tools like jscodeshift and codemode-js to use babel transforms to update an entire codebase.
If you’re looking to build a plugin of your own, or want to get a little bit more in-depth about writing Babel plugins, be sure to check out the Babel Plugin Handbook . This resource was invaluable when I was learning Babel.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
How Tomcat Works
Budi Kurniawan、Paul Deck / BrainySoftware / 2004-4-1 / USD 54.95
A Guide to Developing Your Own Java Servlet Container一起来看看 《How Tomcat Works》 这本书的介绍吧!