写点什么

【转】前端开发之 React Native SDK 升级问题

  • 2021 年 12 月 29 日
  • 本文字数:6649 字

    阅读完需:约 22 分钟

​问题 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);需要手动删除最后一行代码;


common-entry.js 入口文件

// 按照你的需求导入你所需的放入公共包中的npm 模块import "react";import "react-native";require("react-native/Libraries/Core/checkNativeVersion");
复制代码


编写 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;
复制代码


};}


编写 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,      },    }),  },};
复制代码


运行打包命令

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 可能会存在模块冲突的问题;如果使用数字则不会,因为数字是使用随机的;

数字更加安全,如果 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;否则使用业务包分包的逻辑;


编写 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; };}
复制代码


编写对指定的模块进行过滤

// 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;}
复制代码


运行命令进行打包

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 如何进行动态分包及动态加载


问题 2: Cookie 失效问题

背景

以 Android 为例,常见会将 Cookie 使用 android 的 CookieManager 进行管理;但是我们内部却没有使用其进行管理;在 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

我们采用的是方案二:


第一步,客户端将 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);

复制代码


第二步, RN 拿到 Cookie

// this.props是RN  根组件的propsdocument.cookie = this.props.Cookie;
复制代码


第三步,设置 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 模块了


其中相对 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 进行全局数据的管理,则需要对数据进行隔离;业界通用的方式是使用微前端 qiankun 对 window 进行 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");
复制代码

原创作者:MrGaoGang

用户头像

关注尚硅谷,轻松学IT 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
【转】前端开发之React Native SDK 升级问题