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; the upgrade span is larger ; Here I will give a brief summary of the problems encountered in the recent SDK upgrade.
Question 1: How to split RN bundle
Preface
In the previous version RN
, metro
does not currently support the use of processModuleFilter
for module filtering; if you google
for RN split bundle, you will find it difficult to have an article detailing how RN performs split bundle; this article will detail how to perform RN split bundle.
RN split bundle, 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 split bundle 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 split bundle scheme
As the name suggests, the 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
-
common-entry.js
entry file
// some module that you want
import "react";
import "react-native";
require("react-native/Libraries/Core/checkNativeVersion");
-
can write createModuleIdFactory
function createCommonModuleIdFactory() {
let nextId = 0;
const fileToIdMap = new Map();
return (path) => {
if (!moduleIdByIndex) {
const name = getModuleIdByName(base, path);
const relPath = pathM.relative(base, path);
if (!commonModules.includes(relPath)) {
// record path
commonModules.push(relPath);
fs.writeFileSync(commonModulesFileName, JSON.stringify(commonModules));
}
return name;
}
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
// Use numbers for module id, and record the path and id for subsequent subcontracting of business packages, and filter out public packages
id = nextId + 1;
nextId = nextId + 1;
fileToIdMap.set(path, id);
const relPath = pathM.relative(base, path);
if (!commonModulesIndexMap[relPath]) {
commonModulesIndexMap[relPath] = id;
fs.writeFileSync(
commonModulesIndexMapFileName,
JSON.stringify(commonModulesIndexMap)
);
}
}
return id;
};
}
-
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,
},
}),
},
};
-
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:
above does not use
processModuleFilter
, since forcommon-entry.js
inlet, all modules are required;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;
- Numbers are more secure, if the app is attacked, it is impossible to know exactly which module the code is
business package and split bundle 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;
- write createModuleIdFactory
function createModuleIdFactory() {
// Why use a random number? It is to avoid rn module conflicts in singleton mode due to the same moduleId
let nextId = randomNum;
const fileToIdMap = new Map();
return (path) => {
// Use name as id
if (!moduleIdByIndex) {
const name = getModuleIdByName(base, path);
return name;
}
const relPath = pathM.relative(base, 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 split bundle
if (commonModulesIndexMap[relPath]) {
return commonModulesIndexMap[relPath];
}
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId + 1;
nextId = nextId + 1;
fileToIdMap.set(path, id);
}
return id;
};
}
- Write to filter the specified module
// processModuleFilter
function processModuleFilter(module) {
const { path } = module;
const relPath = pathM.relative(base, path);
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;
}
if (!moduleIdByIndex) {
if (commonModules.includes(relPath)) {
return false;
}
} else {
// The modules in the public package are directly filtered out
if (commonModulesIndexMap[relPath]) {
return false;
}
}
return true;
}
- 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 toReactNative
injectionCookie
it?
solution
- One possible idea is that clients have their own
CookieManager
when synchronizing updateandroid.CookieManager
; but this scheme is the need for client support students; - 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:
- The first step, the client will
cookie
byprops
passed to the RN
Bundle bundle = new Bundle();
// get cookie in native
String cookie = WebUtil.getCookie("https://example.a.com");
bundle.putString("Cookie", cookie);
// start
rootView.startReactApplication(manager, jsComponentName, bundle);
- The second step, RN gets the Cookie
// this.props is the rn root component props
document.cookie = this.props.Cookie;
- 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:
// business code
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");
Top comments (0)