React Native SDK upgrade issues and split jsbundle
Tags: react, reactnative
Published: 2021-08-13 09:51:41


**Background:**

Our team has always integrated `ReactNative` (hereinafter referred to as RN) as a sub-module into the existing `android/ios` application; the original `RN` version used was `0.55` ; as the times change, RN has `0.65` the version; the upgrade span is larger ; Here I will give a brief summary of the problems encountered in the recent SDK upgrade.

## Question 1: How to split RN bundle

### Preface

In the previous version `RN` , `metro` does not currently support the use of `processModuleFilter` for module filtering; if you `google` for RN split bundle, you will find it difficult to have an article detailing how RN performs split bundle; this article will detail how to perform RN split bundle.

RN split bundle, in the new version of `metro` , in fact, most of us only need to pay attention to the two apis of metro:

- `createModuleIdFactory` : Create a unique id for each module of RN;
- `processModuleFilter` : Select which modules are needed for the current build

First, let's talk about how to give an Id name to a module. The name according to the id that comes with metro is self-increasing according to the number:

```js
function createModuleIdFactory() {
  const fileToIdMap = new Map();
  let nextId = 0;
  return (path) => {
    let id = fileToIdMap.get(path);
    if (typeof id !== "number") {
      id = nextId++;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}
```

According to this, the moduleId will be incremented from 0 in turn;

Let's talk about `processModuleFilter` , a simplest `processModuleFilter` as follows:

```js
function processModuleFilter(module) {
  return true;
}
```

It means that all modules of RN are needed, and there is no need to filter some modules;

With the above foundation, let's start to consider how to split bundle the RN; I believe everyone is clear about the general situation. We divide the entire jsbundle into the `common` package and the `bussiness` package; the `common` package is generally built into the App; and the `bussiness` package It is issued dynamically. Following this line of thinking, let's start subcontracting;

### common package split bundle scheme

As the name suggests, the package is a common resource for all RN pages. Generally, there are several requirements for `common`

- **module will not change frequently**
- **module is universal**
- **generally does not put all npm packages under node_modules in the base package**

According to the above requirements, a project basis we will generally `react` , `react-native` , `redux` , `react-redux` and other changes infrequently general public npm package on package; So how do we divide public bag? There are generally two ways:

- Scheme 1 [PASS]. **to analyze service entry as an entry packet, in `processModuleFilter` (module.path) to manually remove the module through the module path past**

```js
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;
  };
}
```

If you follow this way, trust me, you will definitely give up. Because it has a huge disadvantage: **needs to manually handle the dependencies of packages such as react/react-native** ; that is to say, it’s not that you wrote 4 modules and packaged these 4 modules. It is possible that these 4 modules depend on others. Module, so when running the common package, the basic package will directly report an error.

This led to the second plan:

> Create a public package entry in the root directory and import the modules you need; use this entry when packaging.

Note: **provides an entry file for the public package, so the code after packaging will report error `Module AppRegistry is not registered callable module (calling runApplication)` ; you need to manually delete the last line of code** ;

For detailed code, please see: [react-native-dynamic-load](https%3A%2F%2Fgithub.com%2FMrGaoGang%2Freact-native-dynamic-load)

1.  `common-entry.js` entry file

```js
// some module that you want
import "react";
import "react-native";
require("react-native/Libraries/Core/checkNativeVersion");
```

2.  `can write createModuleIdFactory`

```js
function createCommonModuleIdFactory() {
  let nextId = 0;
  const fileToIdMap = new Map();

  return (path) => {
    if (!moduleIdByIndex) {
      const name = getModuleIdByName(base, path);
      const relPath = pathM.relative(base, path);
      if (!commonModules.includes(relPath)) {
        // record path
        commonModules.push(relPath);
        fs.writeFileSync(commonModulesFileName, JSON.stringify(commonModules));
      }
      return name;
    }
    let id = fileToIdMap.get(path);

    if (typeof id !== "number") {
      // Use numbers for module id, and record the path and id for subsequent subcontracting of business packages, and filter out public packages
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
      const relPath = pathM.relative(base, path);
      if (!commonModulesIndexMap[relPath]) {
        commonModulesIndexMap[relPath] = id;
        fs.writeFileSync(
          commonModulesIndexMapFileName,
          JSON.stringify(commonModulesIndexMap)
        );
      }
    }
    return id;
  };
}
```

3.  `write metro.common.config.js`

```js
const metroCfg = require("./compile/metro-base");
metroCfg.clearFileInfo();
module.exports = {
  serializer: {
    createModuleIdFactory: metroCfg.createCommonModuleIdFactory,
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};
```

4.  `Run packaging command`

```bash
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
```

be careful:

1.  **above does not use `processModuleFilter` , since for `common-entry.js` inlet, all modules are required;**

2.  There are two ways to generate moduleId in the above implementation: one is a number, the other is a path; the difference between the two is not big, but it is recommended to use a number. The reasons are as follows:

- The number is smaller than the string, the smaller the bundle size;
- Multiple modules may have the same name, and the use of strings may cause module conflicts in multiple modules; if you use numbers, you won’t, because the numbers are random;

3.  Numbers are more secure, if the app is attacked, it is impossible to know exactly which module the code is

### business package and split bundle plan

I talked about the subcontracting of the public package. When the public package is subcontracted, the module path and module id in the public package will be recorded; for example:

```json
{
  "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
  // ...
}
```

In this way, when subcontracting the business package, you can judge by the path whether the current module is already in the basic package, if it is in the public package, use the corresponding id directly; otherwise, use the logic of business package subcontracting;

1.  **write createModuleIdFactory**

```js
function createModuleIdFactory() {
  // Why use a random number? It is to avoid rn module conflicts in singleton mode due to the same moduleId
  let nextId = randomNum;
  const fileToIdMap = new Map();

  return (path) => {
    // Use name as id
    if (!moduleIdByIndex) {
      const name = getModuleIdByName(base, path);
      return name;
    }
    const relPath = pathM.relative(base, path);
    // Whether the current module is already in the basic package, if it is in the public package, use the corresponding id directly; otherwise, use the logic of business package split bundle
    if (commonModulesIndexMap[relPath]) {
      return commonModulesIndexMap[relPath];
    }

    let id = fileToIdMap.get(path);
    if (typeof id !== "number") {
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}
```

2.  **Write to filter the specified module**

```js
// processModuleFilter
function processModuleFilter(module) {
  const { path } = module;
  const relPath = pathM.relative(base, path);
  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;
  }
  if (!moduleIdByIndex) {
    if (commonModules.includes(relPath)) {
      return false;
    }
  } else {
    // The modules in the public package are directly filtered out
    if (commonModulesIndexMap[relPath]) {
      return false;
    }
  }
  return true;
}
```

3.  **Run commands to package**

```bash
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
```

The packaged effect is as follows:

```js
// 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);
```

### General code for subcontracting

How RN performs dynamic subcontracting and dynamic loading, please see: [https://github.com/MrGaoGang/react-native-dynamic-load](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2FMrGaoGang%2Freact-native-dynamic-load)

## Question 2: Cookie expiration problem

### background

To `Android` for example, the common will `Cookie` use `android` of `CookieManager` manage; but we did not use it for internal management; the 0.55 version of the initialization time when you can set up a RN `CookieProxy` :

```java
        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());
```

Among them, `ReactCookieProxyImpl` can be implemented by yourself, or you can control how the Cookie is written to RN;

But in the latest RN, `okhttp` is used for network request, and andrid's `CookieManager` used for management; the code is as follows:

```java
// 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() {
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;
}
return Collections.emptyList();
}
}
```

Then there is no use `android.CookieManager`case of how to`ReactNative`injection`Cookie` it?

### solution

1.  One possible idea is that clients have their own `CookieManager` when synchronizing update `android.CookieManager` ; but this scheme is the need for client support students;
2.  The client gets the cookie and passes it to RN, and RN uses jsb to pass the cookie to `android/ios`

We adopted the second option:

1. The first step, the client will `cookie` by `props` passed to the RN

```java

Bundle bundle = new Bundle();
// get cookie in native
String cookie = WebUtil.getCookie("https://example.a.com");
bundle.putString("Cookie", cookie);
// start
rootView.startReactApplication(manager, jsComponentName, bundle);

```

2.  The second step, RN gets the Cookie

```js
// this.props is the rn root component props
document.cookie = this.props.Cookie;
```

3.  The third step is to set cookies to the client

```js
const { RNCookieManagerAndroid } = NativeModules;
if (Platform.OS === "android") {
  RNCookieManagerAndroid.setFromResponse(
    "https://example.a.com",
    `${document.cookie}`
  ).then((res) => {
    // `res` will be true or false depending on success.
    console.log("RN_NOW: 设置 CookieManager.setFromResponse =>", res);
  });
}
```

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

[https://github.com/MrGaoGang/cookies](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2FMrGaoGang%2Fcookies%2Fblob%2Fmaster%2Fandroid%2Fsrc%2Fmain%2Fjava%2Fcom%2Freactnativecommunity%2Fcookies%2FCookieManagerModule.java)

Among them, the version of the rn community is mainly modified. **Android cookies cannot be set at one time. You need to set**

```java
    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);
        }
    }
```

## Question 3: Window isolation problem in singleton mode

Background In the RN singleton mode, if each page uses the window for global data management, the data needs to be isolated; the industry-wide method is to use the micro front end `qiankun` to proxy `window` This is indeed a good method, but it may be more responsible in RN; the method used by the author is:

> Use babel to replace global variables, so that you can ensure that for different pages, setting and using window have different effects on the following; for example:

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

The code after escaping 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");
```