React Native SDKのアップグレードの問題とパッケージスキーム

15731 ワード

記事の最初の個人ブログ:高さんのブログ
背景:
当社のチームは、ReactNative(以下、RNと略称する)をサブモジュールとして既存のandroid/iosアプリケーションに統合してきました.最初に使用されたRNバージョンは0.55であった.時代の変遷に伴い、RNは0.65のバージョンになった.アップグレードのスパンが大きい.ここでは、最近SDKのアップグレードで遭遇した問題について簡単にまとめます.
質問1:RNはどのようにパケット化するか
前言
以前の旧バージョンRNmetroは、processModuleFilterを使用したモジュールフィルタリングを一時的にサポートしていない.google RNパケットを見ると、RNがどのようにパケットを分割するかを詳しく紹介する文章が難しいことに気づきます.この文書では、RNパケットの作成方法について詳しく説明します.
RNパケットは、新版のmetroでは、metroの2つの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パケットはすべてベースパケットに
  • 配置されている.
    上記の要求に従って、基礎的なプロジェクトは一般的にreactreact-nativereduxreact-reduxprocessModuleFilterなどのあまり変更されていない汎用npmパッケージを公共パッケージに入れます.では、私たちはどのように公共バッグを分けますか?一般的には2つの方法があります.
  • 案1【PASS】.業務入口を入口としてパッケージの解析を行い、Module AppRegistry is not registered callable module (calling runApplication)において過去のモジュールパス(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パッケージを実行するときに、ベースパッケージが直接エラーを報告します.
    これにより、2つ目のシナリオが発表されました.
    ルートディレクトリの下にパブリックパッケージのエントリを作成し、必要なモジュールをインポートします.梱包の際にこの入り口を使えばいいです.
    注意点:パブリックパッケージにエントリファイルを与えるため、パッケージ化後のコード実行はcommon-entry.jsとエラーが発生します.最後の行のコードを手動で削除する必要があります.
    詳細コードは、react-native-dynamic-loadを参照してください.
  • createModuleIdFactory エントリファイル
  • //                    npm   
    import "react";
    import "react-native";
    require("react-native/Libraries/Core/checkNativeVersion");
  • metro.common.config.js
  • 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;
      };
    }
  • const metroCfg = require("./compile/metro-base");
    metroCfg.clearFileInfo();
    module.exports = {
      serializer: {
        createModuleIdFactory: metroCfg.createCommonModuleIdFactory,
      },
      transformer: {
        getTransformOptions: async () => ({
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
        }),
      },
    };
  • processModuleFilter
  • 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

    注意点:
  • の上にはcommon-entry.jsは使用されていない.Androidが入口であるため、すべてのモジュールが必要である.
  • では、moduleIdを生成する2つの方法が実現されています.1つは数字であり、1つは経路である.どちらの違いも大きくありませんが、数字の使い方をお勧めします.原因は以下の通りである:
  • の数字は文字列より小さく、bundleの体積は小さい.
  • 複数のmoduleは、名前が同じであるため、文字列を使用すると、複数のmoduleがモジュール競合の問題を引き起こす可能性があります.数字を使用すると、ランダムな数字を使用するため、数字は使用されません.
  • の数字はもっと安全で、appが攻撃されたらコードがそのモジュール
  • であることを正確に知ることができません.
    ビジネスパッケージスキーム
    前述したように、パブリックパケットのパケット化については、パブリックパケットのモジュールパスとモジュール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;
      };
    }
  • 指定モジュールをフィルタする
  • を記述する.
    // 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;
    }
  • コマンド実行パッケージ
  • 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失効問題
    背景Cookieを例にとると、androidCookieManagerCookieProxyを使用して管理されることが多い.しかし、私たちの内部では管理に使用されていません.0.55のバージョンでRNを初期化するときにReactCookieProxyImplを設定できます.
            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());

    そのうちokhttpは自分で実現することができ、CookieがRNにどのように書き込むかを自分で制御することができる.
    しかし、最新のRNでは、CookieManagerを使用してネットワークリクエストを行い、andridのandroid.CookieManagerを使用して管理しています.コードは次のとおりです.
    // 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 cookies) {
        if (cookieJar != null) {
          cookieJar.saveFromResponse(url, cookies);
        }
      }
    
      @Override
      public List loadForRequest(HttpUrl url) {
        if (cookieJar != null) {
          List cookies = cookieJar.loadForRequest(url);
          ArrayList 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();
      }
    }
    

    では、ReactNativeを使用していない場合、CookieCookieManagerをどのように注入するのでしょうか.
    ソリューション
  • 実行可能な構想は、クライアントが自分のandroid.CookieManagerを持っている場合、android/iosを同期的に更新することである.しかし、この案はクライアントの学生のサポートが必要です.
  • クライアントはcookieを取得し、RNに渡し、RNはjsbを使用してcookieをcookie
  • に渡す.
    シナリオ2を採用しています.
  • の第1のステップでは、クライアントは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);
    
  • 第2ステップRNはCookie
  • を取得する
    // this.props RN      props
    document.cookie = this.props.Cookie;
  • 第3ステップでは、クライアント
  • に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エンドクッキーは一度に設定できず、1つずつ設定する必要があります.
        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() {
                    //     @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");