Article first personal blog: Mr. Gao’s blog

Background:

Our team has been integrating ReactNative (hereinafter referred to as RN) into existing Android /ios applications as a sub-module. The original RN version used was 0.55; Over time, RN has moved to version 0.65; Upgrade span is large; Here’s a quick summary of some of the issues I’ve encountered with recent SDK updates.

Question 1: How does RN subcontract

preface

Metro in previous versions of RN does not yet support processModuleFilter for module filtering; If you Google RN subcontracting, it’s hard to find an article that goes into detail about how RN does subcontracting; This article describes RN subcontracting in detail;

RN subcontracting. In the new version of Metro, most of us only need to pay attention to the two apis of Metro:

  • createModuleIdFactory: Creates a unique ID for each module in RN;
  • processModuleFilter: Select which modules are required for the current build

First, let’s talk about how to give a module an Id, as metro’s own Id is incrementing in numbers:

function createModuleIdFactory() {
  const fileToIdMap = new Map(a);let nextId = 0;
  return (path) = > {
    let id = fileToIdMap.get(path);
    if (typeofid ! = ="number") {
      id = nextId++;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}
Copy the code

As such, the moduleId is incrementing from 0;

Let’s talk about processModuleFilter again. The simplest processModuleFilter is as follows:

function processModuleFilter(module) {
  return true;
}
Copy the code

This means that all modules in RN are required, and there is no need to filter some modules;

With that in mind, let’s start thinking about how to subcontract RN; I’m sure you’re familiar with the general case where we divide the whole JsBundle into the common package and the bussiness package; The Common package is built into the App; The bussiness package is dynamically delivered. Following this idea, we began to subcontract;

Common package subcontracting scheme

As the name implies, the common package is a common resource for all RN pages. There are several requirements for extracting the common package:

  • Modules don’t change very often
  • Modules are generic
  • It is not common to put all NPM packages under node_modules in the base package

According to the above requirements, a basic project we usually react,react-native,redux,react-redux and other common NPM packages in the public package; So how do we divide the common package? Generally, there are two ways:

  • Solution 1 [PASS] : Analyze the package with the service entry as the entry point, and manually remove the related module through the module.path in processModuleFilter
const commonModules = ["react"."react-native"."redux"."react-redux"];
function processModuleFilter(type) {
  return (module) = > {
    if (module.path.indexOf("__prelude__")! = = -1) {
      return true;
    }
    for (const ele of commonModules) {
      if (module.path.indexOf(`node_modules/${ele}/ `)! = = -1) {
        return true; }}return false;
  };
}
Copy the code

If you go this way, trust me, you’ll give up. Because it has one huge disadvantage: you need to handle the react/ React-native dependencies manually; This means that either you write four modules and package them, or they depend on other modules, so when you run the common package, the base package will report an error.

This led to the second plan:

Create a common package entry in the root directory and import the modules you need. Use this entry when packing.

Module AppRegistry is not registered Callable Module (Calling runApplication); You need to manually delete the last line of code;

For detailed code, see React-native-dynamic-load

  1. common-entry.jsEntrance to the file
// Import the NPM modules you need to put into the common package according to your requirements
import "react";
import "react-native";
require("react-native/Libraries/Core/checkNativeVersion");
Copy the code
  1. Write createModuleIdFactory
function createCommonModuleIdFactory() {
  let nextId = 0;
  const fileToIdMap = new Map(a);return (path) = > {
    // Module ID is uniquely represented by a name
    if(! moduleIdByIndex) {const name = getModuleIdByName(base, path);
      const relPath = pathM.relative(base, path);
      if(! commonModules.includes(relPath)) {// Record the path
        commonModules.push(relPath);
        fs.writeFileSync(commonModulesFileName, JSON.stringify(commonModules));
      }
      return name;
    }
    let id = fileToIdMap.get(path);

    if (typeofid ! = ="number") {
      // Use numbers for module IDS, and record the path and ID for subcontracting of later business packages to filter out public packages
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
      const relPath = pathM.relative(base, path);
      if(! commonModulesIndexMap[relPath]) {// Record the relationship between path and ID
        commonModulesIndexMap[relPath] = id;
        fs.writeFileSync(
          commonModulesIndexMapFileName,
          JSON.stringify(commonModulesIndexMap) ); }}return id;
  };
}
Copy the code
  1. Write metro.com mon. Config. Js
const metroCfg = require("./compile/metro-base");
metroCfg.clearFileInfo();
module.exports = {
  serializer: {
    createModuleIdFactory: metroCfg.createCommonModuleIdFactory,
  },
  transformer: {
    getTransformOptions: async() = > ({transform: {
        experimentalImportSupport: false.inlineRequires: true,},}),},};Copy the code
  1. Running the package Command
react-native bundle --platform android --dev false --entry-file  common-entry.js --bundle-output android/app/src/main/assets/common.android.bundle --assets-dest android/app/src/main/assets --config ./metro.base.config.js --reset-cache && node ./compile/split-common.js android/app/src/main/assets/common.android.bundle
Copy the code

Note:

  1. It’s not usedprocessModuleFilter, because forcommon-entry.jsFor entry, all modules are needed;
  2. Above, two methods are implemented to generate moduleId: one is in digital mode, the other is in path mode; There is not much difference between the two, but a numerical approach is recommended. Here’s why:
  • If the number is smaller than the string, the bundle size is smaller;
  • Multiple Modules may have the same name. Using a string will cause module conflicts. Not if you use numbers, because numbers are used randomly;
  1. Numbers are more secure, and if the app is attacked, it’s impossible to know exactly which module the code is

Business subcontracting scheme

The subcontracting of the public package is discussed earlier. The module path and module ID in the public package will be recorded during the subcontracting. Such as:

{
  "common-entry.js": 1."node_modules/react/index.js": 2."node_modules/react/cjs/react.production.min.js": 3."node_modules/object-assign/index.js": 4."node_modules/@babel/runtime/helpers/extends.js": 5."node_modules/react-native/index.js": 6."node_modules/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js": 7."node_modules/@babel/runtime/helpers/interopRequireDefault.js": 8."node_modules/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js": 9
  // ...
}
Copy the code

In this way, when dividing service packages, the path can be used to determine whether the current module is in the base package. If it is in the common package, the corresponding ID can be directly used. Otherwise use business subcontracting logic;

  1. Write createModuleIdFactory
function createModuleIdFactory() {
  // Why use a random number? To avoid conflicts between Rn Modules in singleton mode due to the same moduleId
  let nextId = randomNum;
  const fileToIdMap = new Map(a);return (path) = > {
    // Use name as id
    if(! moduleIdByIndex) {const name = getModuleIdByName(base, path);
      return name;
    }
    const relPath = pathM.relative(base, path);
    // If the current module is already in the base package, use the corresponding ID if it is in the common package; Otherwise use business package subcontracting logic
    if (commonModulesIndexMap[relPath]) {
      return commonModulesIndexMap[relPath];
    }
    // Id of the service package
    let id = fileToIdMap.get(path);
    if (typeofid ! = ="number") {
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}
Copy the code
  1. Writes a filter for the specified module
// processModuleFilter
function processModuleFilter(module) {
  const { path } = module;
  const relPath = pathM.relative(base, path);
  // Some simple and common ones are already in the common package
  if (
    path.indexOf("__prelude__")! = = -1 ||
    path.indexOf("/node_modules/react-native/Libraries/polyfills")! = = -1 ||
    path.indexOf("source-map")! = = -1 ||
    path.indexOf("/node_modules/metro-runtime/src/polyfills/require.js")! = = -1
  ) {
    return false;
  }
  // Use name
  if(! moduleIdByIndex) {if (commonModules.includes(relPath)) {
      return false; }}else {
    // If the module is in the common package, filter it out directly
    if (commonModulesIndexMap[relPath]) {
      return false; }}// Otherwise, it is in the service package
  return true;
}
Copy the code
  1. Run the command to package
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/business.android.bundle --assets-dest android/app/src/main/assets --config ./metro.business.config.js  --reset-cache
Copy the code

The result is as follows:

// bussiness.android.js
__d(function(g,r,i,a,m,e,d){var t=r(d[0]),n=r(d[1])(r(d[2])); t.AppRegistry.registerComponent('ReactNativeDynamic'.function(){return n.default})},832929992[6.8.832929993]);
// ...
__d(function(g,r,i,a,m,e,d){Object.defineProperty(e,"__esModule",
__r(832929992);
Copy the code

Common code for subcontracting

RN for dynamic subcontracting and dynamic loading, please see :github.com/MrGaoGang/r…

Problem 2: Invalid cookies

background

Take Android as an example, it is common to use Android CookieManager to manage cookies; But we don’t manage it internally; In version 0.55, you can set a CookieProxy when initializing RN:

        ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
                .setApplication(application)
                .setUseDeveloperSupport(DebugSwitch.RN_DEV)
                .setJavaScriptExecutorFactory(null)
                .setUIImplementationProvider(new UIImplementationProvider())
                .setNativeModuleCallExceptionHandler(new NowExceptionHandler())
                .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
                .setReactCookieProxy(new ReactCookieProxyImpl());
Copy the code

ReactCookieProxyImpl can be implemented by yourself, and you can control how the Cookie is written to RN;

But in the latest RN, is the use of OKHTTP for network requests, and the use of andrid CookieManager management; The code is as follows:

// OkHttpClientProvider
    OkHttpClient.Builder client = new OkHttpClient.Builder()
      .connectTimeout(0, TimeUnit.MILLISECONDS)
      .readTimeout(0, TimeUnit.MILLISECONDS)
      .writeTimeout(0, TimeUnit.MILLISECONDS)
      .cookieJar(new ReactCookieJarContainer());

// ReactCookieJarContainer
public class ReactCookieJarContainer implements CookieJarContainer {

  @Nullable
  private CookieJar cookieJar = null;

  @Override
  public void setCookieJar(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @Override
  public void removeCookieJar(a) {
    this.cookieJar = null;
  }

  @Override
  public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    if(cookieJar ! =null) { cookieJar.saveFromResponse(url, cookies); }}@Override
  public List<Cookie> loadForRequest(HttpUrl url) {
    if(cookieJar ! =null) {
      List<Cookie> cookies = cookieJar.loadForRequest(url);
      ArrayList<Cookie> validatedCookies = new ArrayList<>();
      for (Cookie cookie : cookies) {
        try {
          Headers.Builder cookieChecker = new Headers.Builder();
          cookieChecker.add(cookie.name(), cookie.value());
          validatedCookies.add(cookie);
        } catch (IllegalArgumentException ignored) {
        }
      }
      return validatedCookies;
    }
    returnCollections.emptyList(); }}Copy the code

So how do you inject cookies into ReactNative without using Android.Cookiemanager?

The solution

  1. One possible idea is that the client has its ownCookieManagerSynchronizes the updateandroid.CookieManager; However, this solution needs the support of the clients;
  2. The client gets the cookie, passes it to RN, and RN uses JSB to pass the cookie toandroid/ios

We adopted plan two:

  1. Step one, the client willcookiethroughpropsPass to RN
Bundle bundle = new Bundle();
// Get the cookie, because the cookie is obtained across the process, so generally there is a problem, reseed
String cookie = WebUtil.getCookie("https://example.a.com");
bundle.putString("Cookie", cookie);

// At startup
rootView.startReactApplication(manager, jsComponentName, bundle);

Copy the code
  1. Step two, RN gets the Cookie
// this.props is the props for the root component of RN
document.cookie = this.props.Cookie;
Copy the code
  1. Step 3: Set the Cookie to the client
const { RNCookieManagerAndroid } = NativeModules;
if (Platform.OS === "android") {
  RNCookieManagerAndroid.setFromResponse(
    "https://example.a.com".`The ${document.cookie}`
  ).then((res) = > {
    // `res` will be true or false depending on success.
    console.log("RN_NOW: set the CookieManager. SetFromResponse = >", res);
  });
}
Copy the code

The premise of use is that the client already has the corresponding native module. For details, see:

Github.com/MrGaoGang/c…

The version of rn community is mainly modified. The android cookies cannot be set once, but need to be set one by one

    private void addCookies(String url, String cookieString, final Promise promise) {
        try {
            CookieManager cookieManager = getCookieManager();
            if (USES_LEGACY_STORE) {
                // cookieManager.setCookie(url, cookieString);
                String[] values = cookieString.split(";");
                for (String value : values) {
                    cookieManager.setCookie(url, value);
                }
                mCookieSyncManager.sync();
                promise.resolve(true);
            } else {
                // cookieManager.setCookie(url, cookieString, new ValueCallback<Boolean>() {
                // @Override
                // public void onReceiveValue(Boolean value) {
                // promise.resolve(value);
                / /}
                // });
                String[] values = cookieString.split(";");
                for (String value : values) {
                    cookieManager.setCookie(url, value);
                }
                promise.resolve(true); cookieManager.flush(); }}catch(Exception e) { promise.reject(e); }}Copy the code

Problem 3: Window isolation in singleton mode

Context In RN singleton mode, if Windows are used for global data management on each page, data needs to be isolated. The common way in the industry is to use the Micro front-end Qiankun to Proxy the window. That’s a good idea, but it’s probably more responsible in RN; The method adopted by the author is:

Use Babel for global variable substitution, which ensures that setting and using window are different for different pages; Such as:

// Business code
window.rnid = (clientInfo && clientInfo.rnid) || 0;
window.bundleRoot = (clientInfo && clientInfo.bundleRoot) || "";
window.clientInfo = clientInfo;
window.localStorage = localStorage = {
  getItem: () = > {},
  setItem: () = >{}};localStorage.getItem("test");
Copy the code

The escaped code is:

import _window from "babel-plugin-js-global-variable-replace-babel7/lib/components/window.js";

_window.window.rnid = (clientInfo && clientInfo.rnid) || 0;
_window.window.bundleRoot = (clientInfo && clientInfo.bundleRoot) || "";
_window.window.clientInfo = clientInfo;
_window.window.localStorage = _window.localStorage = {
  getItem: () = > {},
  setItem: () = >{}}; _window.localStorage.getItem("test");
Copy the code