React Native SDKのアップグレードの問題とパッケージスキーム
15731 ワード
記事の最初の個人ブログ:高さんのブログ
背景:
当社のチームは、
質問1:RNはどのようにパケット化するか
前言
以前の旧バージョン
RNパケットは、新版の まず、モジュールにIdの名前を付ける方法についてお話しします.metroが持っているidの名前は数字に基づいて自己成長しています.
このようにmoduleIdは0から順次増加する.
RNのすべてのモジュールが必要であり、いくつかのモジュールをフィルタする必要がないことを意味します.
上の基礎があって、次はRNのパケットをどのように行うかを考え始めました.一般的な状況をよく知っていると思います.jsbundle全体を
commonパケット分割スキーム
名前の通りモジュールは常に変動する モジュールは汎用の である.一般的にnode_modulesの下のnpmパケットはすべてベースパケットに 配置されている.
上記の要求に従って、基礎的なプロジェクトは一般的に案1【PASS】.業務入口を入口としてパッケージの解析を行い、 を手動で除去する.
もしあなたがこのような方法で、私を信じて、あなたはきっと放弃します.react/react-nativeなどのパケットの依存を手動で処理する必要があるという大きな欠点がある.つまり、4つのモジュールをパッケージ化した後にこの4つのモジュールを書いたのではなく、この4つのモジュールが他のモジュールに依存している可能性があるので、commonパッケージを実行するときに、ベースパッケージが直接エラーを報告します.
これにより、2つ目のシナリオが発表されました.
ルートディレクトリの下にパブリックパッケージのエントリを作成し、必要なモジュールをインポートします.梱包の際にこの入り口を使えばいいです.
注意点:パブリックパッケージにエントリファイルを与えるため、パッケージ化後のコード実行は
詳細コードは、react-native-dynamic-loadを参照してください.
注意点:の上には では、moduleIdを生成する2つの方法が実現されています.1つは数字であり、1つは経路である.どちらの違いも大きくありませんが、数字の使い方をお勧めします.原因は以下の通りである: の数字は文字列より小さく、bundleの体積は小さい. 複数のmoduleは、名前が同じであるため、文字列を使用すると、複数のmoduleがモジュール競合の問題を引き起こす可能性があります.数字を使用すると、ランダムな数字を使用するため、数字は使用されません. の数字はもっと安全で、appが攻撃されたらコードがそのモジュール であることを正確に知ることができません.
ビジネスパッケージスキーム
前述したように、パブリックパケットのパケット化については、パブリックパケットのモジュールパスとモジュールidを記録する.例:
このように、分業パケットの場合、現在のモジュールがベースパケットに既に存在するか否かを経路的に判断し、共通パケットであれば対応するidを直接使用することができる.そうでなければ、ビジネスパッケージを使用してパッケージを分割する論理. createModuleIdFactory を記述指定モジュールをフィルタする を記述する.コマンド実行パッケージ
梱包後の効果は以下の通りです.
パケット共通コード
RNが動的パケット化および動的ロードを行う方法については、https://github.com/MrGaoGang/react-native-dynamic-loadを参照してください.
問題2:Cookie失効問題
背景
そのうち
しかし、最新のRNでは、
では、
ソリューション実行可能な構想は、クライアントが自分の クライアントはcookieを取得し、RNに渡し、RNはjsbを使用してcookieを に渡す.
シナリオ2を採用しています.の第1のステップでは、クライアントは に渡す.第2ステップRNはCookie を取得する第3ステップでは、クライアント にCookieを設定する.
使用する前提はクライアントがすでに対応するnativeモジュールを持っていることであり、詳細は以下を参照してください.
https://github.com/MrGaoGang/cookies
ここでrnコミュニティのバージョンに対して主に変更され、androidエンドクッキーは一度に設定できず、1つずつ設定する必要があります.
問題3:単例モードでwindow分離問題
背景RN単例モードでは、各ページにwindowを使用してグローバルデータの管理がある場合は、データを隔離する必要があります.業界共通の方式は、マイクロフロントエンド
babelを使用してグローバル変数の置換を行い、異なるページに対してwindowを設定し、使用することを保証することができます.例:
エスケープ後のコードは次のとおりです.
背景:
当社のチームは、
ReactNative
(以下、RNと略称する)をサブモジュールとして既存のandroid/ios
アプリケーションに統合してきました.最初に使用されたRN
バージョンは0.55
であった.時代の変遷に伴い、RNは0.65
のバージョンになった.アップグレードのスパンが大きい.ここでは、最近SDKのアップグレードで遭遇した問題について簡単にまとめます.質問1:RNはどのようにパケット化するか
前言
以前の旧バージョン
RN
のmetro
は、processModuleFilter
を使用したモジュールフィルタリングを一時的にサポートしていない.google
RNパケットを見ると、RNがどのようにパケットを分割するかを詳しく紹介する文章が難しいことに気づきます.この文書では、RNパケットの作成方法について詳しく説明します.RNパケットは、新版の
metro
では、metroの2つのapiに注目する必要があります.createModuleIdFactory
:RNの各モジュールに一意のidを作成する.processModuleFilter
:現在の構築に必要なモジュールを選択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ページで共通のリソースであり、一般的には共通のパケットから抽出するにはいくつかの要求があります.上記の要求に従って、基礎的なプロジェクトは一般的に
react
、react-native
、redux
、react-redux
、processModuleFilter
などのあまり変更されていない汎用npmパッケージを公共パッケージに入れます.では、私たちはどのように公共バッグを分けますか?一般的には2つの方法があります.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
が入口であるため、すべてのモジュールが必要である.ビジネスパッケージスキーム
前述したように、パブリックパケットのパケット化については、パブリックパケットのモジュールパスとモジュール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を直接使用することができる.そうでなければ、ビジネスパッケージを使用してパッケージを分割する論理.
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
を例にとると、android
はCookieManager
のCookieProxy
を使用して管理されることが多い.しかし、私たちの内部では管理に使用されていません.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
を使用していない場合、Cookie
にCookieManager
をどのように注入するのでしょうか.ソリューション
android.CookieManager
を持っている場合、android/ios
を同期的に更新することである.しかし、この案はクライアントの学生のサポートが必要です.cookie
シナリオ2を採用しています.
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);
// this.props RN props
document.cookie = this.props.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");