The introduction

For the TARO framework, I believe that most small program developers have a certain understanding. With the Taro framework, developers can develop small programs using React, and implement a set of code that can be adapted to each side of the small program. This ability to drive down development costs has made Taro available to developers of all sizes. There is a difference between a small program packaged with Taro and a native one. Growingio’s native SDK is not yet strong enough to be used directly in Taro. It needs to be adapted to the framework’s specific features. This has been perfectly adapted in the Taro2 period, but after Taro3, due to the adjustment of the Taro team’s overall architecture, the previous method has been unable to achieve accurate no-buried points, prompting this exploration.

background

There are two core issues with GrowingIO applet SDK implementation:

  1. How do I intercept the triggering methods of user events
  2. How do I generate a unique and stable identifier for a node

As long as you can deal with these two problems, then you can achieve a stable small program no buried SDK. In TARO2, the framework does different things at compile time and run time. The compiler is mainly the Taro code through Babel into small program code, such as: JS, WXML, WXSS, JSON. At runtime, TARO2 provides two core APICreateApps, CreateComponent, to create applet apps and implement applet page building.

The GrowingIO applet SDK overwrites the CreateComponent method to intercept user events on the page. After intercepting the method, we can retrieve the triggered node information and method name when the event is triggered. If the node has an ID, we use ID + method name as the identifier, otherwise we use the method name as the identifier. The SDK doesn’t do anything about getting the method name, because it’s already done at TARO2 compile time. It leaves the user method names intact, and for anonymous methods, the arrow functions are numbered to give the appropriate method names.

However, after TARO3, the whole core of TARO has changed dramatically. Both the compile time and the run time are different from before. The CreateApp and CreateComponent interfaces are no longer available, and user methods are compressed at compile time, with user method names no longer preserved and anonymous methods no longer numbered. This prevents the existing Growingio applet SDK from implementing burrowless capabilities on the TARo3.

Problem analysis

In the face of this change of Taro3, Growingio has made adaptations before. In the analysis of the Taro3 runtime code, it is found that Taro3 assigns a relatively stable ID to all the nodes in the page, and all the event listening methods on the nodes are the eh methods in the page instance. Under this condition, GrowingIO intercepts the EH method in the way the native applet SDK handles it, and gets the ID on the node when the user event is triggered to generate a unique identifier. This approach also addresses, in part, two of the core issues of the buried point free SDK.

It’s not hard to imagine that GrowingIO was not able to get a stable node identifier. When the order of the nodes in the page changes, or some nodes are dynamically added or deleted, TARO3 will assign a new ID to the node, which will not provide a stable identifier, causing the unburied point event defined in the previous circle to fail.

If you want to handle the failure of defined buried point events, you must be able to provide a stable identifier. The analogy with TARO2 is that if you can also get the user’s method name when the event is intercepted, that’s fine. That is to say, as long as you can deal with the following two problems, you can achieve this goal.

  1. The runtime SDK can intercept user methods
  2. The ability to preserve user method names in production

One by one break

Get the user method

Let’s look at the first problem, how the SDK gets the user binding method and blocks it. Analysis of the source of TARO3, it is not difficult to solve.

All page configurations are returned via the createPageConfig method, and each page configuration has an EH, from which you can get the binding method. See the EventHandler, DispatchEvent methods in the Taro-Runtime source code.

Export function EventHandler (Event: function EventHandler) MpEvent) {if (event.currentTarget == null) {event.currentTarget = event.target} // The runtime document is defined by TARO 3.0. Const node = document.getElementById(event.currentTarget. Id) if (node! = null) {// trigger event node.DispatchEvent (createEvent(event, node))}} Class TaroElement extends Taronode {... public dispatchEvent (event: Cancelable) {const cancelable = event.cancelable // The __handlers attribute is key. Const Listeners = this.__Handlers [event.type] // Listeners = this.__Handlers [event.type] // Listeners Omit many return Listeners! = null } ... }

The structure of __handlers is as follows:

function hookDispatchEvent(dispatch) { return function() { const event = arguments[0] let node = Document.getElementById (Event.currentTarget. Id); // Handlers = Node.__Handlers (); Return dispatch.apply(this, arguments)}} if (document? .tagName === '#DOCUMENT' && !! document.getElementById) { const TaroNode = document.__proto__.__proto__ const dispatchEvent = TaroNode.dispatchEvent Object.defineProperty(TaroNode, 'dispatchEvent', { value: hookDispatchEvent(dispatchEvent), enumerable: false, configurable: false }) }

Reserved method name

Let’s take a look at the status quo. User methods are available in the steps above. User methods can be divided into the following categories:

Methods classification

  • A named method
function signName() {}
  • Anonymous methods
const anonymousFunction = function () {}
  • Arrow function
const arrowsFunction = () => {}
  • Inline arrow functions
<View onClick={() => {}}></View>
  • Class method
class Index extends Component {
  hasName() {}
}
  • The class fields syntax method
class Index extends Component {
  arrowFunction = () => {}
}

For both named and class methods, it is possible to get the method name from Function.name, but not directly from the other methods. So how do you get the names of these methods?

Given what is currently actionable, it is impossible to get the method names of these methods at runtime. Because TARO3 is compressed in the build environment, anonymous methods are not numbered as TARO2 is. Since you can’t do this at runtime, you’ll have to focus on compile-time.

Leave the method name

Taro3 still has to be handled with Babel at compile time, so implementing a Babel plugin to give these anonymous methods an appropriate method name will solve this problem. The guide to plug-in development can be found in Handbook, and you can see the structure of this tree visually through the AST Explorer. Now that you know the basics of Babel plug-in development, it’s time to choose the right time to visit the tree.

The initial consideration was to set the access point to Function so that any method of any type could be intercepted, and then to preserve the method name according to certain rules. There is no problem with this idea, and it can be used after trying to implement it, but it will have the following two problems:

  • The scope is too large to convert non-event listening methods, which is unnecessary
  • In the face of code compression is still helpless, can only be configured to retain the function name of the compression mode to deal with, the final package volume will be affected

Let’s take a look at the JSX syntax and consider that all user methods are bound to listen for elements in the form of onXXX, as follows

<Button onClick={handler}></Button>

The AST structure is shown below, so you can think of setting the access point to a JSXAttribute and simply giving the method of its value an appropriate name. The JSX-related types are visibleJSX/AST, md, a lot



The overall framework of the plug-in can be as follows

function visitorComponent(path, State) {path.traverse({// access element attribute jsxAttribute (path) {let attrName = path.get('name').node.name let valueExpression =  path.get('value.expression') if (! // ^on[a-z][a-za-z]+/.test(attrName)) return // ReplaceWithCallStatement (valueExpression)}})} module.exports = function ({ template }) { return { name: 'babel-plugin-setname', // React components can be Class and Function // Inside the component during the JSXAttribute visit: {Function: visitorComponent, Class: visitorComponent } } }

As long as the plug-in handles the value expression in the JSXAttribute and can set the appropriate method name for various types of user methods, the task of preserving the method name can be accomplished.

Babel plugin functionality implementation

The plug-in mainly realizes the following functions

  • Access user methods in JSXAttribute
  • Get the appropriate method name
  • Injects the code that sets the method name

The final result is as follows

_GIO_DI_NAME_ sets the method name for the function through Object.defineProperty. The plug-in provides a default implementation, which can also be customized.

Object.defineProperty(func, 'name', {
  value: name,
  writable: false,
  configurable: false
})

You may notice that handleClick is already named in the converted code, so setting it again is unnecessary. But don’t forget that the production code is still compressed, so you don’t know what the function name is.

The following describes the handling of different event binding modes, and basically covers the various notations in React.

identifier

Identifiers are the identifiers used on JSX properties, and there is no limit to how a function can be declared.

<Button onClick={varIdentifier}></Button>

The AST structure is as follows



In this case, the method name directly takes the name value of the identifier.

Member expression

  • Ordinary member expressions such as the methods in the following member expressions
<Button onClick={parent.props.arrowsFunction}></Button>

Will be converted to the following form

_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("parent_props_arrowsFunction", parent.props.arrowsFunction)
})

The AST structure of a member expression looks something like this. The plug-in takes all member identifiers and uses _ join as the method name.

  • This expression takes special care that it will not retain the rest of this, as follows
<Button onClick={this.arrowsFunction}></Button>

It’s going to be converted to

_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("arrowsFunction", this.arrowsFunction)
})

Function execution expression

An execution expression is a call to a function, like this

<Button onClick={this.handlerClick.bind(this)}></Button>

The bind() here is a CallExpression, and the plug-in handles it with the following result

_reactJsxRuntime.jsx("button", {
  onClick: _GIO_DI_NAME_("handlerClick", this.handlerClick.bind(this))
})

Execution expressions may be more complex, such as a page in which several listeners are generated by the same higher-order function with different parameters, in which case it is necessary to preserve parameter information. The following

<Button onClick={getHandler('tab1')}></Button>
<Button onClick={getHandler(h1)}></Button>
<Button onClick={getHandler(['test'])}></Button>

Need to be converted to the following form

// getHandler('tab1')
_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("getHandler$tab1", getHandler('tab1')),
  children: ""
})
// getHandler(h1)
_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("getHandler$h1", getHandler(h1)),
  children: ""
})
// getHandler(['test'])
_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("getHandler$$$1", getHandler(['test'])),
  children: ""
})

There are different processing methods for different parameter types. The whole idea is to join the high-order function name and parameter to form the method name.

The AST structure of a CallExpression is as follows

Transform. Js [60-73] The above is just a direct function execution expression, consider the following case

<Button onClick={factory.buildHandler('tab2')}></Button>

If you look at the AST structure here, the callee part will be a member expression, and the values will follow the above member expression

The results of the conversion are as follows

_reactJsxRuntime.jsx(Button, {
  onClick: _GIO_DI_NAME_("factory_buildHandler$tab2", factory.buildHandler('tab2')),
  children: ""
})

Functional expression

It’s going to be a little tricky to handle, but let’s see how many forms it has

<Button onClick={function(){}}/> <Button onClick={function(){}}/> <Button onClick={function(){}}/> The following will be the most common <Button onClick={() => this.doonClick ()}/>

Take a look at the output after the above code conversion

_reactJsxRuntime.jsx(Button, { onClick: _GIO_DI_NAME_("HomeFunc0", function () {}) }) _reactJsxRuntime.jsx(Button, { onClick: _GIO_DI_NAME_("name", function name() {}) }) _reactJsxRuntime.jsx(Button, { onClick: _GIO_DI_NAME_("HomeFunc1", function () { return _this2.doOnClick(); })})

It can be seen that the named function will be named directly, and the anonymous function will be numbered with a fixed prefix. As long as you control the numbering, you can get a fairly stable method name.

Anonymous function number

In the previous case, the method name is obtained from some user identifier, but in the anonymous function, there is no direct identification, can only generate the method name according to certain rules. Here are the rules:

  • A single component has been incremented as a boundary
  • The method name consists of the component name, keyword and increment number. If the function number is HomeFunc0, a method to increment the ID under the component can be generated directly when accessing the component, as follows
Function getIncrementid (prefix = '_') {let I = 0 return function () {return prefix + I ++}} // getIncrementId(compName + 'Func')

You just need to get rid of the component name again and you’re done. Here are some common AST structures that declare components:

Based on the AST structure above, the component name can be obtained in the following ways:

function getComponentName(componentPath) { let name let id = componentPath.node.id if (id) { name = id.name } else { name = componentPath.parent && componentPath.parent.id && componentPath.parent.id.name } return name || COMPONENT_FLAG; // For others that cannot get the name of the Component, use Component instead of}.

At this point, you can assign a stable method name to an anonymous function.

conclusion

The GrowingIO applet SDK starts at runtime and compile time. Event interception is implemented at runtime, and user method names are preserved at compile time. In this way, the GrowingIO applet SDK can achieve stable unburied function. Specific ways of use can be seen:The GrowingIO applet SDK is integrated with TARo3. With the TARO3 burieless support, GrowingIO’s burieless implementation has also been extended from run-time only operation to compile time. This is a new approach and will likely be further optimized in this direction in the future to provide more stable burieless functionality. The related Babel plugins are open source and available in the repository:growingio/growing-babel-plugin-setname