头图

Article first published Personal blog: Mr. Gao's blog

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 of 061162d9779cac; the upgrade span is larger ; Here I will give a brief summary of the problems encountered in the recent SDK upgrade.

Question 1: How does RN subcontract

Preface

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

RN subcontracting, 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:

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:

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 subcontract 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 subcontracting scheme

As the name suggests, the 061162d9779f4c 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
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

  1. common-entry.js entry file
// 按照你的需求导入你所需的放入公共包中的npm 模块
import "react";
import "react-native";
require("react-native/Libraries/Core/checkNativeVersion");
  1. can write createModuleIdFactory
function createCommonModuleIdFactory() {
  let nextId = 0;
  const fileToIdMap = new Map();

  return (path) => {
    // module id使用名称作为唯一表示
    if (!moduleIdByIndex) {
      const name = getModuleIdByName(base, path);
      const relPath = pathM.relative(base, path);
      if (!commonModules.includes(relPath)) {
        // 记录路径
        commonModules.push(relPath);
        fs.writeFileSync(commonModulesFileName, JSON.stringify(commonModules));
      }
      return name;
    }
    let id = fileToIdMap.get(path);

    if (typeof id !== "number") {
      // 使用数字进行模块id,并将路径和id进行记录下来,以供后面业务包进行分包使用,过滤出公共包
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
      const relPath = pathM.relative(base, path);
      if (!commonModulesIndexMap[relPath]) {
        // 记录路径和id的关系
        commonModulesIndexMap[relPath] = id;
        fs.writeFileSync(
          commonModulesIndexMapFileName,
          JSON.stringify(commonModulesIndexMap)
        );
      }
    }
    return id;
  };
}
  1. write metro.common.config.js
const metroCfg = require("./compile/metro-base");
metroCfg.clearFileInfo();
module.exports = {
  serializer: {
    createModuleIdFactory: metroCfg.createCommonModuleIdFactory,
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};
  1. Run packaging 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

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;
  1. Numbers are more secure, if the app is attacked, it is impossible to know exactly which module the code is

business package and subcontracting 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:

{
  "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
function createModuleIdFactory() {
  // 为什么使用一个随机数?是为了避免因为moduleId相同导致单例模式下rn module冲突问题
  let nextId = randomNum;
  const fileToIdMap = new Map();

  return (path) => {
    // 使用name的方式作为id
    if (!moduleIdByIndex) {
      const name = getModuleIdByName(base, path);
      return name;
    }
    const relPath = pathM.relative(base, path);
    // 当前模块是否已经在基础包中,如果在公共包中则直接使用对应的id;否则使用业务包分包的逻辑
    if (commonModulesIndexMap[relPath]) {
      return commonModulesIndexMap[relPath];
    }
    // 业务包的Id
    let id = fileToIdMap.get(path);
    if (typeof id !== "number") {
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}
  1. Write to filter the specified module
// processModuleFilter
function processModuleFilter(module) {
  const { path } = module;
  const relPath = pathM.relative(base, path);
  // 一些简单通用的已经放在common包中了
  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;
  }
  // 使用name的情况
  if (!moduleIdByIndex) {
    if (commonModules.includes(relPath)) {
      return false;
    }
  } else {
    // 在公共包中的模块,则直接过滤掉
    if (commonModulesIndexMap[relPath]) {
      return false;
    }
  }
  // 否则其他的情况则是业务包中
  return true;
}
  1. Run commands 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

The packaged effect 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);

General code for subcontracting

How RN performs dynamic subcontracting and dynamic loading, please see: https://github.com/MrGaoGang/react-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 :

        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:

// 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
Bundle bundle = new Bundle();
// 获取cookie,因为跨进程获取cookie,所以一般来说是会出现问题的,重新种一次要
String cookie = WebUtil.getCookie("https://example.a.com");
bundle.putString("Cookie", cookie);

// 启动的时候
rootView.startReactApplication(manager, jsComponentName, bundle);
  1. The second step, RN gets the Cookie
// this.props是RN  根组件的props
document.cookie = this.props.Cookie;
  1. The third step is to set cookies to the client
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

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

    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:
// 业务代码
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");

mrgaogang
8 声望7 粉丝