Article first personal blog: Mr. Gao’s blog
Background:
Our team has been integrating ReactNative (hereinafter referred to as RN) into existing Android /ios applications as a sub-module. The original RN version used was 0.55; Over time, RN has moved to version 0.65; Upgrade span is large; Here’s a quick summary of some of the issues I’ve encountered with recent SDK updates.
Question 1: How does RN subcontract
preface
Metro in previous versions of RN does not yet support processModuleFilter for module filtering; If you Google RN subcontracting, it’s hard to find an article that goes into detail about how RN does subcontracting; This article describes RN subcontracting in detail;
RN subcontracting. In the new version of Metro, most of us only need to pay attention to the two apis of Metro:
createModuleIdFactory
: Creates a unique ID for each module in RN;processModuleFilter
: Select which modules are required for the current build
First, let’s talk about how to give a module an Id, as metro’s own Id is incrementing in numbers:
function createModuleIdFactory() {
const fileToIdMap = new Map(a);let nextId = 0;
return (path) = > {
let id = fileToIdMap.get(path);
if (typeofid ! = ="number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
Copy the code
As such, the moduleId is incrementing from 0;
Let’s talk about processModuleFilter again. The simplest processModuleFilter is as follows:
function processModuleFilter(module) {
return true;
}
Copy the code
This means that all modules in RN are required, and there is no need to filter some modules;
With that in mind, let’s start thinking about how to subcontract RN; I’m sure you’re familiar with the general case where we divide the whole JsBundle into the common package and the bussiness package; The Common package is built into the App; The bussiness package is dynamically delivered. Following this idea, we began to subcontract;
Common package subcontracting scheme
As the name implies, the common package is a common resource for all RN pages. There are several requirements for extracting the common package:
- Modules don’t change very often
- Modules are generic
- It is not common to put all NPM packages under node_modules in the base package
According to the above requirements, a basic project we usually react,react-native,redux,react-redux and other common NPM packages in the public package; So how do we divide the common package? Generally, there are two ways:
- Solution 1 [PASS] : Analyze the package with the service entry as the entry point, and manually remove the related module through the module.path in processModuleFilter
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;
};
}
Copy the code
If you go this way, trust me, you’ll give up. Because it has one huge disadvantage: you need to handle the react/ React-native dependencies manually; This means that either you write four modules and package them, or they depend on other modules, so when you run the common package, the base package will report an error.
This led to the second plan:
Create a common package entry in the root directory and import the modules you need. Use this entry when packing.
Module AppRegistry is not registered Callable Module (Calling runApplication); You need to manually delete the last line of code;
For detailed code, see React-native-dynamic-load
common-entry.js
Entrance to the file
// Import the NPM modules you need to put into the common package according to your requirements
import "react";
import "react-native";
require("react-native/Libraries/Core/checkNativeVersion");
Copy the code
Write createModuleIdFactory
function createCommonModuleIdFactory() {
let nextId = 0;
const fileToIdMap = new Map(a);return (path) = > {
// Module ID is uniquely represented by a name
if(! moduleIdByIndex) {const name = getModuleIdByName(base, path);
const relPath = pathM.relative(base, path);
if(! commonModules.includes(relPath)) {// Record the path
commonModules.push(relPath);
fs.writeFileSync(commonModulesFileName, JSON.stringify(commonModules));
}
return name;
}
let id = fileToIdMap.get(path);
if (typeofid ! = ="number") {
// Use numbers for module IDS, and record the path and ID for subcontracting of later business packages to filter out public packages
id = nextId + 1;
nextId = nextId + 1;
fileToIdMap.set(path, id);
const relPath = pathM.relative(base, path);
if(! commonModulesIndexMap[relPath]) {// Record the relationship between path and ID
commonModulesIndexMap[relPath] = id;
fs.writeFileSync(
commonModulesIndexMapFileName,
JSON.stringify(commonModulesIndexMap) ); }}return id;
};
}
Copy the code
Write metro.com mon. Config. Js
const metroCfg = require("./compile/metro-base");
metroCfg.clearFileInfo();
module.exports = {
serializer: {
createModuleIdFactory: metroCfg.createCommonModuleIdFactory,
},
transformer: {
getTransformOptions: async() = > ({transform: {
experimentalImportSupport: false.inlineRequires: true,},}),},};Copy the code
Running the package 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
Copy the code
Note:
- It’s not used
processModuleFilter
, because forcommon-entry.js
For entry, all modules are needed; - Above, two methods are implemented to generate moduleId: one is in digital mode, the other is in path mode; There is not much difference between the two, but a numerical approach is recommended. Here’s why:
- If the number is smaller than the string, the bundle size is smaller;
- Multiple Modules may have the same name. Using a string will cause module conflicts. Not if you use numbers, because numbers are used randomly;
- Numbers are more secure, and if the app is attacked, it’s impossible to know exactly which module the code is
Business subcontracting scheme
The subcontracting of the public package is discussed earlier. The module path and module ID in the public package will be recorded during the subcontracting. Such as:
{
"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
// ...
}
Copy the code
In this way, when dividing service packages, the path can be used to determine whether the current module is in the base package. If it is in the common package, the corresponding ID can be directly used. Otherwise use business subcontracting logic;
- Write createModuleIdFactory
function createModuleIdFactory() {
// Why use a random number? To avoid conflicts between Rn Modules in singleton mode due to the same moduleId
let nextId = randomNum;
const fileToIdMap = new Map(a);return (path) = > {
// Use name as id
if(! moduleIdByIndex) {const name = getModuleIdByName(base, path);
return name;
}
const relPath = pathM.relative(base, path);
// If the current module is already in the base package, use the corresponding ID if it is in the common package; Otherwise use business package subcontracting logic
if (commonModulesIndexMap[relPath]) {
return commonModulesIndexMap[relPath];
}
// Id of the service package
let id = fileToIdMap.get(path);
if (typeofid ! = ="number") {
id = nextId + 1;
nextId = nextId + 1;
fileToIdMap.set(path, id);
}
return id;
};
}
Copy the code
- Writes a filter for the specified module
// processModuleFilter
function processModuleFilter(module) {
const { path } = module;
const relPath = pathM.relative(base, path);
// Some simple and common ones are already in the common package
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;
}
// Use name
if(! moduleIdByIndex) {if (commonModules.includes(relPath)) {
return false; }}else {
// If the module is in the common package, filter it out directly
if (commonModulesIndexMap[relPath]) {
return false; }}// Otherwise, it is in the service package
return true;
}
Copy the code
- Run the command 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
Copy the code
The result 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);
Copy the code
Common code for subcontracting
RN for dynamic subcontracting and dynamic loading, please see :github.com/MrGaoGang/r…
Problem 2: Invalid cookies
background
Take Android as an example, it is common to use Android CookieManager to manage cookies; But we don’t manage it internally; In version 0.55, you can set a CookieProxy when initializing RN:
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());
Copy the code
ReactCookieProxyImpl can be implemented by yourself, and you can control how the Cookie is written to RN;
But in the latest RN, is the use of OKHTTP for network requests, and the use of andrid CookieManager 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(a) {
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;
}
returnCollections.emptyList(); }}Copy the code
So how do you inject cookies into ReactNative without using Android.Cookiemanager?
The solution
- One possible idea is that the client has its own
CookieManager
Synchronizes the updateandroid.CookieManager
; However, this solution needs the support of the clients; - The client gets the cookie, passes it to RN, and RN uses JSB to pass the cookie to
android/ios
We adopted plan two:
- Step one, the client will
cookie
throughprops
Pass to RN
Bundle bundle = new Bundle();
// Get the cookie, because the cookie is obtained across the process, so generally there is a problem, reseed
String cookie = WebUtil.getCookie("https://example.a.com");
bundle.putString("Cookie", cookie);
// At startup
rootView.startReactApplication(manager, jsComponentName, bundle);
Copy the code
- Step two, RN gets the Cookie
// this.props is the props for the root component of RN
document.cookie = this.props.Cookie;
Copy the code
- Step 3: Set the Cookie to the client
const { RNCookieManagerAndroid } = NativeModules;
if (Platform.OS === "android") {
RNCookieManagerAndroid.setFromResponse(
"https://example.a.com".`The ${document.cookie}`
).then((res) = > {
// `res` will be true or false depending on success.
console.log("RN_NOW: set the CookieManager. SetFromResponse = >", res);
});
}
Copy the code
The premise of use is that the client already has the corresponding native module. For details, see:
Github.com/MrGaoGang/c…
The version of rn community is mainly modified. The android cookies cannot be set once, but need to be set one by one
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); }}Copy the code
Problem 3: Window isolation in singleton mode
Context In RN singleton mode, if Windows are used for global data management on each page, data needs to be isolated. The common way in the industry is to use the Micro front-end Qiankun to Proxy the window. That’s a good idea, but it’s probably more responsible in RN; The method adopted by the author is:
Use Babel for global variable substitution, which ensures that setting and using window are different for different pages; Such as:
// Business code
window.rnid = (clientInfo && clientInfo.rnid) || 0;
window.bundleRoot = (clientInfo && clientInfo.bundleRoot) || "";
window.clientInfo = clientInfo;
window.localStorage = localStorage = {
getItem: () = > {},
setItem: () = >{}};localStorage.getItem("test");
Copy the code
The escaped code 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");
Copy the code