2
头图
Author of this article : Shopee Digital Purchase front-end team.

1. Background

React Native (hereinafter referred to as RN) is currently widely used by the Shopee front-end team. Although RN has many advantages, its development and debugging process is not as friendly as Mobile Web, especially debugging at runtime.

In development mode, although RN provides official debugging tools, its function is weaker than the pure front-end browser Devtool; in non-development mode, such as Test and UAT environments, RN code is packaged into a Bundle , At this time, the official debugging tools will not come in handy, which not only hinders the reproduction of the problems of the test students, but also makes the problem positioning of the development students more difficult.

At present, although there are tools for RN debugging in the industry, there are more or less defects (as shown in the figure below), and these tools are all for debugging in development mode, and the debugging of the packaged production environment often still needs to rely on human flesh. To do it, the efficiency is relatively low.

Therefore, a tool that can help locate problems in a non-development environment is particularly important, and Luna came into being. This article will introduce the design and implementation of the key technologies of this RN tool.

2. Function introduction

Let's take a look at Luna through the following pictures:

As can be seen from the picture, Luna is an in-app debugging tool for RN, which is more inclined to solve the pain point of debugging in production environment .

Luna consists of an orange trigger button and a body that occupies half of the screen. Ontology includes four sections, Log, Network, Redux, and Shopee, which respectively carry the functions of logging, network request viewing, Redux tree viewing, and Shopee-related information viewing.

Among them, Log and Network exist as core modules, while Shopee and Redux are introduced as public plug-ins provided by Luna. This Core-Plugin mode is the current operating mode of Luna: Log, Network and other functions are provided by default, and users are also supported to write custom modules and import them into Luna.

The functions of the four sections are as follows:

1) Log section

The Log section takes over console.log, collects all logs and uncaught errors into Luna, and displays them in reverse order. It supports filtering by Log type, and also supports fuzzy search for Log. As shown below:

2) Network section

The Network section collects the request information sent by the page, including request status, request time, request headers, request bodies, and response headers and response bodies, etc. Users can easily view API requests.

3) Shopee section

The Shopee section provides some Shopee App-related functions, such as convenient translation and copy switching, cookies viewing, DataStore storage viewing and deletion, as well as user ID/name and device system information, as well as version number-related information viewing. These functions can help developers to debug applications more easily, and also facilitate QA to reproduce and locate bugs faster.

4) Redux section

The Redux section displays the Store (shared data repository) tree, allowing users to view the state of the entire Store.

3. Schematic design

3.1 Overall Design

As a monorepo multi-package single-warehouse architecture project, Luna includes three package modules: Core, Shopee Plugin and Redux Plugin.

Among them, the Core core module includes three parts: the Log log section, the Network section, and the Plugins plug-in access section. The design of each module will be introduced one by one below.

3.2 Core

The Core module is the core module of Luna. It exists as a separate npm package and provides the most basic functions and plug-in access capabilities. The Core module is nested at the root of the component tree as a Provider, accepts business code, and inserts Luna into it. Core uses mobx as storage to maintain the collection and display of Log logs and Network records, as well as the control of custom plugins and other functions.

3.2.1 Access plan

The inspiration of Luna comes from the two open-source debugging tools, vConsole and Eruda on the Web side. However, in the selection of Luna's access solutions, we encountered a problem that has never been encountered in Mobile Web: in modern Web development, Whether it is Vue or React, as long as it is a single-page application, there will be a root node for mounting, and the entire component tree will be built with this root node as the starting point. Therefore, the debugging tool only needs to hang under a certain root node to perceive the state of the entire application:

In React Native, each page (View) has its own root node (as shown in the figure below), and there is no common ancestor node between different pages. If you want to ensure that each page can access Luna, It is necessary to perform a separate injection on each page, not only the access cost increases sharply, but also data retention has become a major problem.

Therefore, how to ensure that Luna can be accessed on each page, and also retain the data of different pages, and not affect Luna when an error occurs, and also reduce the cost of page access, has become a difficult problem. So how does Luna do it?

First, Luna decouples initialization from page registration, and prepends Luna.init to application initialization. This separates data collection from page registration, ensuring that page switching will not result in data loss.

import Luna from "@shopee/luna";
Luna.init();

Then, Luna used the Shopee Plugin to rewrite the method for registering the Shopee RN Page, wrapped the incoming page component with a new component, and also included Luna in it, returning the component to the outer layer in the form of HOC. Every page registered using this method of registering a page will automatically include Luna in the page, so there is no need to manually introduce Luna on each page, and each page can also access Luna.

Finally, Luna also wraps a layer of ErrorBoundary on the incoming Component to capture the runtime errors generated by the page, so that Luna can still be accessed when the page generates errors, and the error information can be seen in Luna.

3.2.2 Log

log collection

The Log module, as the name suggests, is used to display logs printed by the system and users.

Luna hijacks the global variable global.console to collect various types of Logs; at the same time, Luna also hijacks console.tron.log to collect related logs printed by Reactotron during development; Luna also hijacks ErrorUtils, which will not be captured errors are also collected in the log store. These three types of logs are the data sources of the Log section.

Luna hijacks the global console in a way similar to middleware, adds it to the Log store during the hijacking process, and then executes its original execution function. The main code is as follows:

export const overrideConsole = (consoleStore) => {
  const mixinType = [
    LOG_TYPE.LOG,
    LOG_TYPE.ERROR,
    LOG_TYPE.WARN,
    LOG_TYPE.DEBUG,
    LOG_TYPE.INFO,
  ];
  mixinType.forEach((type) => {
    // @ts-ignore
    const originConsoleFun = global.console[type];
    // @ts-ignore
    global.console[type] = (...params) => {
      consoleStore.addLog(params, type);
      originConsoleFun(...params);
    };
  });
};

logs show

The Log log includes type filtering, search box and log list. Because Luna logs have many types, complex content and are always in a dynamic update state, performance problems are easy to occur. Therefore, in the display part of the log list, we have done a lot of performance optimization, mainly including two parts, as shown in the following figure:

1) Nested type display optimization

Due to the compatibility problem of the tree display library of the open source solution, we chose to write the tree display component by ourselves to solve the display problems caused by complex data types and large amounts of data. It has the following characteristics:

  • Support the expansion and contraction of multi-line text, and only display part of the content when shrinking;
  • The lazy loading scheme is adopted for large arrays and objects. After expansion, only the content of less than 100 lines is displayed. Each time the user clicks the remaining part (N), the next N*100 pieces of data are displayed. This approach avoids performance problems caused by big data display;
  • Control the line wrapping of a line of super-long text, keep each Log no more than three lines, and ensure that the number of Logs per screen is controlled.

2) List sliding performance optimization

The Log of Luna is not loaded at one time, but generated in real time. This makes it very likely that new data will be generated at the same time during the scrolling process of the list, and users often need to swipe down to find the Log they printed out. So Luna has also made some specific optimizations for the performance of sliding:

  • Luna uses FlatList to render the Log list, and also generates an ID implicitly when the Log is collected, which acts on the keyExtractor of the FlatList to improve rendering efficiency;
  • Since the Log is dynamically generated, this has a considerable impact on the FlatList's performance. In response to this, Luna displays the Log list in reverse order, puts the last generated data, that is, the data that the user cares about most when clicking on Luna, at the top of the FlatList, and prints the time at the same time. This reduces the frequency of user swiping;
  • We also plan to carry out stricter log paging loading for Luna, separate the displayed and stored log lists, and obtain the "next page" of the stored log list when sliding to the end, completely guaranteeing the list sliding in the process of dynamic data generation performance.

3.2.3 Network

Data collection for the Network module originates from XMLHttpRequest . Luna hijacks React Native's XMLHttpRequest, rewrites the open, send and setRequestHeader methods, and stores each request and request-related fields in the Network list. Since the bottom layer of RN's Fetch actually uses XHR, hijacking XHR can achieve full coverage. The main code of Network hijacking looks like this:

export const overrideNetwork = (consoleStore) => {
  originOpen = XMLHttpRequest.prototype.open;
  const originSetHeader = XMLHttpRequest.prototype.setRequestHeader;
  XMLHttpRequest.prototype.open = function (...args) {
    this._xmlItem = { openData: args };
    this.addEventListener("load", () => {
      const xmlItem = this._xmlItem;
      const requestHeaders = this._requestHeaders;
      const endTime = new Date().getTime();
      const time = endTime - xmlItem.startTime;
      consoleStore.addNetworkLog({
        url: this.responseURL,
        method: xmlItem.openData[0],
        status: this.status,
        rspHeader: this.getAllResponseHeaders(),
        response: this.response,
        body: xmlItem.sendData,
      });
    });
    originOpen.apply(this, args);
  };
};

In the display plan of the Network list, we have added a lot of detailed considerations, such as:

  • The last Path of the requested URL is displayed first;
  • Set different background colors according to the status code of the response;
  • Displays different time units based on the length of the request.

These details are incremental improvements over time, and they really take the actual user experience of Luna to the next level.

3.3 Plugins

3.3.1 Plug-in mechanism

need a plugin mechanism?

Before introducing what the plug-in mechanism is, you may have a question in your heart, why is there a plug-in mechanism? The reason is that when Luna implements functions, some functions are realized by relying on Shopee's SDK; other functions such as Redux are optional, and the state management framework used by users may be mbox; in order to maintain the purity of Luna's core modules , and to retain Luna's extensibility under the non-Shopee framework, we uncoupled these unnecessary couplings, and transformed the Shopee module and the Redux module into a plug-in mechanism for users to refer to as needed.

What is the plugin mechanism?

In addition to the core modules of Luna, Core also supports custom plugins. Luna provides two first-party plug-ins: Redux Plugin and Shopee Plugin. If you have customized requirements for your own App, you can easily write your own plug-ins and import them into Luna, as shown in the following figure.

3.3.2 Official Plugins

Luna also uses the plugin mechanism to provide two official plugins: Redux Plugin and Shopee Plugin. These two packages are introduced as separate npm packages for users who need them. in:

  • Redux Plugin exists as a Redux middleware, obtains the state of Redux through Store.getState, and displays it on the interface. Users can easily find the current Redux storage value.
  • Shopee Plugin is a plug-in based on Shopee React Native SDK, which is specially developed for projects in Shopee App. It provides many functions through Shopee's SDK. This plug-in is mainly for the internal development and testing students of Shopee, which is convenient for them to debug in the Shopee App.

3.3.2 Develop custom plug-ins

In addition to official plugins, users can also extend plugins by themselves. How to develop a Luna plugin? Luna's plugin mechanism is very similar to Vue's install-use mechanism, but it omits the install step of the Vue plugin, as long as the component content needs to be injected into the use method provided by Luna. So in fact, the steps are very simple, only need two steps:

  • write your component, declare name;
  • components and names to instances of Luna Core.

Luna can recognize your component and display it on the main interface, and then you can add the functions you need in the plug-in.

4. Future Outlook

At this stage, Luna has been running stably in some Shopee businesses, and has been well received by the developers and testers who use it. In the future, we will work towards two major goals:

1) Automated Luna access

At this stage, Luna's access is still intrusive manual code access. In the future, we plan to use the deployment platform to automatically access Luna during deployment, and it will only take effect in the development and testing environments, not only 0 code can be achieved. The access cost does not affect the production environment, and it also reduces the size of the packaged code.

2) Component tree status viewer

Almost every developer on the web side will use React Devtool, and one of the most beloved is the Components module, which displays the entire component tree during development, as well as the Props, State, and Hooks associated with each component. On the React Native side, there is currently no development and debugging tool as easy as React Devtool, and viewing the status of RN is a major pain point for developers. Therefore, Luna plans to add a viewer for the component tree and component status in the future. , it will no longer be difficult to view the Log, Network and component status on the RN at the same time.


Shopee技术团队
88 声望45 粉丝

如何在海外多元、复杂场景下实践创新探索,解决技术难题?Shopee技术团队将与你一起探讨前沿技术思考与应用。