文章首发集体博客: 高学生的博客

背景:

咱们团队始终是将 ReactNative(下文简称 RN)当做一个子模块集成到现有的 android/ios 利用中;起初应用的 RN 版本是 0.55;随着时代的变迁,RN 曾经到 0.65 的版本了;降级跨度较大;上面我这边就最近 SDK 降级所遇到的问题进行一个简略的总结。

问题 1: RN 如何进行分包

前言

在之前的旧版本 RN 中的 metro 临时还不反对应用processModuleFilter 进行模块过滤;如果你 google 一下 RN 分包,会发现很难有一篇文章具体去介绍 RN 怎么进行分包;本文将具体讲述如何进行 RN 分包;

RN 分包,在新版的 metro 中其实大多数咱们只须要关注 metro 的两个 api:

  • createModuleIdFactory: 给 RN 的每个模块创立一个惟一的 id;
  • processModuleFilter: 抉择以后构建须要哪些模块

首先咱们来谈一谈如何给给个模块取一个 Id 名称,依照 metro 自带的 id 取名是依照数字进行自增长的:

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

依照这样,moduleId 会顺次从 0 开始进行递增;

咱们再来谈一谈processModuleFilter,一个最简略的processModuleFilter如下:

function processModuleFilter(module) {  return true;}

意味着 RN 的所有模块都是须要的,无需过滤一些模块;

有了下面的根底,上面咱们开始着手思考如何进行 RN 分包了;置信大家都比较清楚个别的状况咱们将整个 jsbundle 分为common包和bussiness包;common 包个别会内置到 App 内;而 bussiness 包则是动静下发的。依照这样的思路上面咱们开始分包;

common 包分包计划

顾名思义 common 包是所有的 RN 页面都会专用的资源,个别抽离公共包有几个要求:

  • 模块不会常常变动
  • 模块是通用的
  • 个别不会将 node_modules 下的所有 npm 包都放在根底包中

依照下面的要求,一个根底的我的项目咱们个别会react,react-native,redux,react-redux等不常更改的通用 npm 包放在公共包中;那么咱们如何进行分公共包呢?个别有两种形式:

  • 计划 1【PASS】. 以业务入口为入口进行包的剖析,在processModuleFilter中通过过来模块门路(module.path)手动移除相干模块
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;  };}

如果你依照这样的形式,置信我,你肯定会放弃的。因为其有一个微小的毛病:须要手动解决 react/react-native 等包的依赖;也就是说不是你写了 4 个模块打包后就是这 4 个模块,有可能这 4 个模块依赖了其余的模块,所以在运行 common 包的时候,根底包会间接报错。

由此推出了第二个计划:

在根目录下建设一个公共包的入口,导入你所须要的模块;在打包的时候应用此入口即可。

留神点: 因为给公共包一个入口文件,这样打包之后的代码运行会报错Module AppRegistry is not registered callable module (calling runApplication);须要手动删除最初一行代码;

具体代码请见:react-native-dynamic-load

  1. common-entry.js入口文件
// 依照你的需要导入你所需的放入公共包中的npm 模块import "react";import "react-native";require("react-native/Libraries/Core/checkNativeVersion");
  1. 编写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. 编写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. 运行打包命令
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

留神点:

  1. 下面并没有应用processModuleFilter,因为针对common-entry.js为入口而言,所有的模块都是须要的;
  2. 下面实现了两种形式生成 moduleId:一种是以数字的形式,一种是以门路的形式;两者区别都不大,然而倡议应用数字的形式。起因如下:
  • 数字相比字符串更小,bundle 体积越小;
  • 多个 module 可能因为名称雷同,应用字符串的形式会造成多个 module 可能会存在模块抵触的问题;如果应用数字则不会,因为数字是应用随机的;
  1. 数字更加平安,如果 app 被攻打则无奈精确晓得代码是那个模块

business 包分包计划

后面谈到了公共包的分包,在公共包分包的时候会将公共包中的模块门路和模块 id 进行记录;比方:

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

这样在分业务包的时候,则能够通过门路的形式判断,以后模块是否曾经在根底包中,如果在公共包中则间接应用对应的 id;否则应用业务包分包的逻辑;

  1. 编写 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. 编写对指定的模块进行过滤
// processModuleFilterfunction 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. 运行命令进行打包
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

打包后的成果如下:

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

分包通用代码

RN 如何进行动静分包及动静加载,请见:https://github.com/MrGaoGang/react-native-dynamic-load

问题 2: Cookie 生效问题

背景

Android 为例,常见会将 Cookie 应用 androidCookieManager 进行治理;然而咱们外部却没有应用其进行治理;在 0.55 的版本的时候在初始化 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());

其中 ReactCookieProxyImpl是能够本人进行实现的,也能够本人管制 Cookie 如何写入 RN;

然而在最新的 RN 外面,是应用 okhttp 进行网络申请的, 且应用的是 andrid 的 CookieManager 进行治理;代码如下:

// OkHttpClientProvider    OkHttpClient.Builder client = new OkHttpClient.Builder()      .connectTimeout(0, TimeUnit.MILLISECONDS)      .readTimeout(0, TimeUnit.MILLISECONDS)      .writeTimeout(0, TimeUnit.MILLISECONDS)      .cookieJar(new ReactCookieJarContainer());// ReactCookieJarContainerpublic 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();  }}

那么在 没有应用android.CookieManager 的状况下,如何给 ReactNative 注入 Cookie 呢?

解决方案

  1. 一个可行的思路是客户端是有本人的 CookieManager 的时候,同步更新 android.CookieManager; 然而此计划是须要客户端同学的反对;
  2. 客户端拿到 cookie,传递给 RN,RN 应用 jsb 将 cookie 传递给 android/ios

咱们采纳的是计划二:

  1. 第一步,客户端将 cookie 通过 props 传递给 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. 第二步, RN 拿到 Cookie
// this.props是RN  根组件的propsdocument.cookie = this.props.Cookie;
  1. 第三步,设置 Cookie 给客户端
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);  });}

应用的前提是客户端曾经有对应的 native 模块了,具体请见:

https://github.com/MrGaoGang/cookies

其中绝对 rn 社区的版本次要批改,android 端 cookie 不能一次性设置,须要一一设置

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

问题 3: 单例模式下 window 隔离问题

背景在 RN 单例模式下,每一个页面如果有应用到 window 进行全局数据的治理,则须要对数据进行隔离;业界通用的形式是应用微前端qiankunwindow进行 Proxy。这确实是一个好办法,然而在 RN 中兴许较为负责;笔者采纳的形式是:

应用 babel 进行全局变量替换,这样能够保障对于不同的页面,设置和应用 window 即在不同的作用于上面;比方:
// 业务代码window.rnid = (clientInfo && clientInfo.rnid) || 0;window.bundleRoot = (clientInfo && clientInfo.bundleRoot) || "";window.clientInfo = clientInfo;window.localStorage = localStorage = {  getItem: () => {},  setItem: () => {},};localStorage.getItem("test");

本义之后的代码为:

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");