A. Unpacking

There are generally three ways to unpack the packages, namely Facebook’s Metro, Ctrip’s Moles – Packer and Diff Patch (Google’s Diff-match-Patch can be used). But the best way for now may be Metro. In the process of investigation, the earliest and most complete example I came into contact with was the React-native multibundler. In this example, I even developed visual tools for unpacking and packaging.

Type of bundle code split: base package and service package. Base package: Bundle some duplicate JS code into a package with third-party dependencies. Service package: Split one or more packages according to different service logic within the application.

1. The Metro installation

Metro is already installed with React Native when NPM install is installed, but it may not be the latest version. To use Metro, install Metro separately.

npm install --save-dev metro metro-core
Copy the code

or

yarn add --dev metro metro-core
Copy the code

2. The Metro configuration

There are three methods to configure Metro: metro.config.js, metro.config.json, and package.json to add the Metro field.

The Metro configuration internal structure looks something like this:

module.exports = {
  resolver: {
    /* resolver options */
  },
  transformer: {
    /* transformer options */
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */
  }

  /* general options */
};
Copy the code

Each Optoins has a number of configuration options, but for starters, the most important ones are the createModuleIdFactory and processModuleFilter options in the Serializer option.

As shown in figure:

CreateModuleIdFactory: After V0.24.1,Metro supports this method to configure a custom module ID. It also supports string ID, which is used to generate the module ID of the require statement. Its type is () => (path: String) => number(functions that return functions with return parameters), where path is the full path for each module. Another use of this method is to generate the same ID for the same Module when packaging multiple times, so that the Module cannot be found due to a different ID when the next update is released.

ProcessModuleFilter: Filters modules based on the specified conditions to remove unnecessary modules. Module: Array< module > => Boolean (module: Array< module >) => Boolean, where module is an output module with corresponding parameters. Return false to filter and do not import the bundle.

Next code:

Function createModuleIdFactory() {// Get the directory where the command line is executed, __dirname is the variable provided by nodejs const projectRootPath = __dirname; return (path) => { let name = ''; // If (path.indexof ('node_modules' + pathSep + 'react-native' + pathSep +) if ('node_modules' + pathSep + 'react-native' + pathSep + 'Libraries' + pathSep) > 0) {// // React Native // name = path.substr(path.lastIndexof (pathSep) + 1); //} if (path.indexof (projectRootPath) == 0) {/* React Native is an absolute path with device information. To avoid duplicate names, you can keep node_modules until the end, such as /{User}/{username}/{userdir}/node_modules/xxx.js you need to cut off the device information */ name = path.substr(projectRootPath.length + 1); // name = name.replace('.js', ''); // name = name.replace('.png', ''); // Replace the slash with the underscore let regExp = pathSep == '\\'? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm"); name = name.replace(regExp, '_'); // Encrypt the name if (isEncrypt) {name = MD5 (name); } return name; }; }Copy the code

Need to produce what kind of module ID, can according to own situation and be fond of, whether it is encrypted, stitching, can even to get directly to the path of return, the only note is unified rules, otherwise you will can’t find the corresponding module, of course module, the longer the ID of the final bundle file, ID length or wants moderate, However, after MD5 encryption, the length does not matter.

When sending service packages, you can use filter to filter existing modules in the basic package and reduce the size of the bundle file.

If (module['path'].indexof ('__prelude__') >= function processModuleFilter(module) {// Filter some modules whose path is __prelude__ 0) { return false; If (module['path'].indexof (pathSep + 'node_modules' + pathSep) > 0) {/* However, modules whose output type is JS /script/virtual cannot be filtered. Generally, files of this type are core files, such as initializecore.js. This is needed every time the bundle is loaded. */ if ('js' + pathSep + 'script' + pathSep + 'virtual' == module['output'][0]['type']) { return true; } return false; } // Return true; }Copy the code

After adding the above two methods to the xxx.config.js file, introduce the methods to serializerOptions within module.exports.

module.exports = {
  serializer: {
    createModuleIdFactory: config.createModuleIdFactory,
    processModuleFilter: config.processModuleFilter
    /* serializer options */
  }
}
Copy the code

3. The Metro

Add the –config parameter to package the corresponding entry file according to the service package of the basic package. The Metro official document indicates that config files of other paths are supported, but it has not been successful so far. I can only add config files to the root directory of the project. Maybe I added the path in the wrong way.

Base packages: Import the required third-party dependency packages, React Native packages, and JS files into a JS file, such as basics. Js, and pass in the basics. Config. js parameter to –confg.

Use the terminal to switch to the project root directory and run the following command:

react-native bundle --platform android --dev false --entry-file src/basics/basics.js --bundle-output ./android/app/src/main/assets/basics.android.bundle --assets-dest android/app/src/main/res/ --config basics.config.js
Copy the code

Business package: Separate out different business entries according to the business logic of your application, and use AppRegistry to register the main Component of the business, such as index1.js, with business.config.js passed into –config.

The command is as follows:

react-native bundle --platform android --dev false --entry-file src/index/index1.js --bundle-output ./android/app/src/main/assets/business1.android.bundle --assets-dest android/app/src/main/res/ --config business.config.js
Copy the code

Replace the paths in the preceding two commands with your own paths. If several service packages are divided, you need to execute several commands. You can connect the commands with && and write them into a script file, such as a. Sh file in Linux or a. Bat file in Windows, and execute the script file.

You can run the react-native bundle -h command to view the corresponding parameter configuration options. –entry-file is the loaded entry file, as shown in the figure:

The createModuleIdFactory log output is as follows:

In-app JS:

The first act intercepts the path in the method. The second act intercepts the name based on whether the React Native file or the three-party library file. The third line is the name with the suffix removed

If not encrypted, you can remove the project directory, otherwise the bundle file will expose the project structure.

Encrypted before:

After the encryption:

2.Android native loading

: Sparkles: The Demo uses Koltin. If Java is required, set isUseKotlin to false in build.gradle and click Sync.

1. Source code analysis

React Native can load bundles from assets, local File, and Metro local Server Delta bundles. When you’re developing and running in the emulator, when you double click on R, you’re using the Delta bundle. Loading bundle interface file, then seek call interface to complete the load of different business packages, and at the time of the call createReactContextInBackground base package will be loaded.

Every React Native page inherits the ReactActivity. In the onCreate method, it calls mdelegate. onCreate, which creates a RootView and sets it to the ContentView.

Take a look at the source logic:

Delegate.loadApp->ReactRootView.startReactApplication->
attachToReactInstanceManager->ReactInstanceManager.attachRootView->
attachRootViewToInstance->ReactRootView.runApplication->
catalystInstance.getJSModule(AppRegistry.class).runApplication
Copy the code

If the bundle is not loaded, Application XXX has not been registered. “, simply load the bundle before calling runApplication. The CatalystInstance interface inherits an interface called JSBundleLoaderDelegate, The interface of the three methods of loadScriptFromAssets respectively, loadScriptFromFile, loadScriptFromDeltaBundle, you can see by name is used to load different positions of the bundle.

In ReactRootView runApplication, CatalystInstance is invoked ReactContext getCatalystInstance method to obtain, And within ReactContext CatalystInstance is in its creation from ReactInstanceManager createReactContext method by CatalystInstanceImpl Builder in the new.

ReactContext can ReactApplication. GetReactNativeHost. GetReactInstanceManager. GetCurrentReactContext access, so can write their own a utility class directly, Load the bundles in the tool class in advance.

2. Function realization

The Demo uses two ways to do this, both of which require the use of the utility class JsLoaderUtil.

One is to create a new class as a base class, it inherits ReactActivity, and rewrite the createReactActivityDelegate and getMainComponentName two methods, When building ReactActivityDelegate in createReactActivityDelegate method onCreate method call before super load through tools will be agreed by the component. The advantage of this approach is that you don’t need to worry about the code for loading the bundle in the subclass or before you enter the subclass. The base class is already written, so you just need to tell the base class which business bundle file to load, such as Business1Activity. Another use of this approach is to tell the utility class the bundle to load directly before entering a subclass, while in a subclass you don’t need to add any code and just inherit BaseReactActivity, such as Business2Activity.

public class BaseReactActivity extends ReactActivity { @Override protected ReactActivityDelegate createReactActivityDelegate() { String localBundleName = getBundleName(); if (! TextUtils.isEmpty(localBundleName)) { JsLoaderUtil.jsState.bundleName = localBundleName; } return new ReactActivityDelegate(this, getMainComponentName()) { @Override protected void onCreate(Bundle savedInstanceState) { JsLoaderUtil.load(getApplication(), () -> super.onCreate(savedInstanceState)); }}; } @Nullable @Override protected String getMainComponentName() { return JsLoaderUtil.jsState.componentName; } protected String getBundleName() { return ""; }}Copy the code

The alternative in Demo is to have the subclass inherit the ReactActivity directly and use the utility class to load the required business bundle files before entering the subclass. The advantage of this approach is that you don’t have to stick to the inherited parent class, but be careful that the business package must be loaded before entering the page, otherwise an error will be reported. Examples include Business3Activity and Business4Activity.

3.Double Tap R

At this point, our bundle file is loaded, but it’s not always possible to package debugging, and you still need to double-click R to load hot updates during development. However, the JS code has been split, and the Application only returns the bundle of the base package to React Native, which is scattered over various business logic. A switch is required to load the bundle or the Delta bundle, which can be done in three or four steps.

The first step is to import the split service package into the index.js file, which is equivalent to registering all service modules at one time.

import './src/index/index1';
import './src/index/index2';
import './src/index/index3';
import './src/index/index4';
Copy the code

The second step, increase judgment within JsLoaderUtil tools, if is Dev mode, direct return, not loaded bundles and do not call createReactContextInBackground.

The third step is to return the Dev mode flag in the getUseDeveloperSupport method of ReactNativeHost within the MainApplication and the previous index.js name in the getJSMainModuleName method. Tell React Native that this is an entry file.

At this time, you can enter a business page and double-click R to update the content of the page. However, when you restart the application during the switch, it cannot be reload normally. Even if you enter the page, there will be an error crash resulting in the killed process, and then you can enter the application. Instead of letting it crash, either kill the application and restart it, or add a fourth step.

Step 4: In the onDestroy component of the MainActivity, call System.exit(0), switch on and restart the application.

4. Special instructions

Each JS File is equivalent to a Module. React Native does not load the loaded Module again. In other words, if the bundle inside assets is loaded first and then the bundle in the local File is loaded, the bundle inside assets will still be displayed. This will only take effect if the local File bundle is loaded after the process is restarted. There is no good solution to this problem. Please tell me in issue if you know how to solve it.

The bundle.zip package in the Assets directory is a service package with File characters. It is used to test the function of loading files from local files. Other business bundle files within assets, such as Business1.android. bundle, are bundle packages with assets text, which are used to test the loading function from assets. JS code, such as business1.js, is a business with Runtime text, used to test hot update by double-clicking the R key during development.

5. Effect demonstration:

3. IOS native loading

1. Access source code

Loading multiple bundles is easier on iOS than on Android. You only need to expose the following interfaces to the RCTBridge extension:

-(void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;

2. Practice (following is a base package and a business package test)

1. Import the basic package and service package into the project

Figure 1:

2. Loading the base package at App startup Figure 2:

3. Preload the service package on the details page or after loading the basic package

Figure 3:

4. Output: The basic package is loaded, and the service package is loaded successfully, and the page and logic are normal

Figure 4:

Iv. Functional outlook

Above is all the content of the Demo, for the next step function outlook is passed on to the tool in the class of different Key with the server the scheduled module, to download the bundle content of different, can also according to different Key, the download images resources need to be updated, copy from class tool to specify a local directory, supply with updated loading.

Making address:Github.com/yxyhail/Met…