preface

The last article explained how to unpack JS files for the React Native project. This time we will use an example to show how to load unpacked files on demand.

The target

As shown above, the final goal is to achieve the following effect:

  1. usingPrevious articleThe React Native app is packed into threebundle:

Base.bundle. js contains only the base library:

import 'react'
import 'react-native'
Copy the code

Home.bundle. js contains the following contents:

// Package the entry file index.js
import {AppRegistry} from 'react-native'
import Home from './App'

AppRegistry.registerComponent('home'.() = > Home)

// App.js
import React, {useEffect} from 'react'
import {View, Text, Button, StyleSheet, NativeModules} from 'react-native'

const Home = () = > {
  return (
    <View>
      <View>
        <Text>Home</Text>
      </View>
      <View>
        <Button
          title='Go To Business1'
          onPress={()= > {
            NativeModules.Navigator.push('business1')
          }}
        />
      </View>
    </View>)}export default Home
Copy the code

Note that we implemented a Native Module Navigator, which we’ll cover later.

Business1.bundle. js contains the following contents:

// Package the entry file index.js
import {AppRegistry} from 'react-native'
import Business1 from './App'

AppRegistry.registerComponent('business1'.() = > Business1)

// App.js
import React, {useEffect} from 'react'
import {View, Text, StyleSheet, Alert} from 'react-native'

const Business1 = () = > {
  useEffect(() = > {
    Alert.alert(global.name)
  }, [])
  return (
    <View>
      <Text>Business1</Text>
    </View>)}export default Business1
Copy the code
  1. When entering the application, load and run it firstbase.bundle.js(including,reactreact-nativeAnd so on base library), and then load runhome.bundle.js, the page displays home-related content.
  2. Click on the Home pageGo To Business1Jump to business1 page, which will load and runbusiness1.bundle.jsThe Business1 page is then displayed.

Front knowledge

A brief introduction to objective-C syntax

Objective-c (OC) is a strongly typed language that requires declaring the types of variables:

NSSttring * appDelegateClassName;
Copy the code

Function calls in OC are very strange, wrapped in [] :

self.view.backgroundColor = [UIColor whiteColor];
Copy the code

OC also supports functions as arguments:

[Helper loadBundleWithURL:bundleUrl onComplete:^{
  [Helper runApplicationOnView:view];
}]
Copy the code

OC also has the concept of classes:

// Class declaration file viewController.h (inherited from UIViewController)
@interface ViewController : UIViewController.@end

// Class implementation file viewController.m
@implementation ViewController
- (void)viewDidLoad {

}
@end
Copy the code

More knowledge please add your own.

Brief introduction to iOS development

UIView

UIView is the most basic view class that manages the display of certain content on the screen. As a parent class of various view types, UIView provides some basic capabilities, such as appearance, event, etc. It can layout and manage child views. The following example implements a new yellow subview on the current page:

@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor redColor];
  // subview
  UIView *view = [[UIView alloc] init];
  view.frame = CGRectMake(50.50.100.50);
  view.backgroundColor = [UIColor yellowColor];
  [self.view addSubview:view];
}
@end
Copy the code

UIViewController is a view controller that manages the hierarchy of views and contains a view by default. It can manage the life cycle of views, switching between views, etc., and UIViewController can manage other uiview controllers. Here is an example of switching between two UIViewControllers via UINavigationController:

UIViewController

The React Native page loading process is described

First, we create a new RN project with the following command:

npx react-native init demo
Copy the code

Let’s start with the entry file index.js:

import {AppRegistry} from 'react-native'
import App from './App'
import {name as appName} from './app.json'

AppRegistry.registerComponent(appName, () = > App)
Copy the code

Obviously, need to know about AppRegistry registerComponent is done, let’s take a look at:

/** * Registers an app's root component. * * See https://reactnative.dev/docs/appregistry.html#registercomponent */
  registerComponent(
    appKey: string,
    componentProvider: ComponentProvider, section? : boolean, ): string {let scopedPerformanceLogger = createPerformanceLogger();
    runnables[appKey] = {
      componentProvider,
      run: (appParameters, displayMode) = > {
        renderApplication(
          componentProviderInstrumentationHook(
            componentProvider,
            scopedPerformanceLogger,
          ),
          appParameters.initialProps,
          appParameters.rootTag,
          wrapperComponentProvider && wrapperComponentProvider(appParameters),
          appParameters.fabric,
          showArchitectureIndicator,
          scopedPerformanceLogger,
          appKey === 'LogBox', appKey, coerceDisplayMode(displayMode), appParameters.concurrentRoot, ); }};if (section) {
      sections[appKey] = runnables[appKey];
    }
    return appKey;
  },
Copy the code

From the above code, it just stores the components in runnables and doesn’t actually render them. So when do you render? We need to look at the native code, we open the ios directory in the appdelegate. m file, you can see:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"rnDemo"
                                            initialProperties:nil]; . }Copy the code

The first line of code initializes the bridge, etc., and then asynchronously loads and executes the JS file. Namely executes AppRegistry registerComponent.

The second line of code prepares a view container for rendering and listens for JS files to load. When loading the success, the AppRegistry invokes the JS code. RunApplication:

- (void)runApplication:(RCTBridge *)bridge
{
  NSString*moduleName = _moduleName ? :@ "";
  NSDictionary *appParameters = @{
    @"rootTag" : _contentView.reactTag,
    @"initialProps" : _appProperties ?: @{},
  };

  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  // Call the method in the JS code
  [bridge enqueueJSCall:@"AppRegistry" method:@"runApplication" args:@[ moduleName, appParameters ] completion:NULL];
}
Copy the code

The JS code AppRegistry. RunApplication executes runnables in corresponding to run method the final rendering of the page:

runApplication(
    appKey: string,
    appParameters: any, displayMode? : number, ):void{... runnables[appKey].run(appParameters, displayMode); },Copy the code

implementation

With all this preparation behind us, we’re finally ready to implement our on-demand loading, starting with the overall solution.

The project design

As shown in the figure, we initialize a MyRNViewController at application startup and manage it via UINavigationContoller. When the loading attempt in MyRNViewController is complete, base.bundle.js and home.bundle.js are loaded and executed via Bridge, and the runApplication method is executed to render the page upon success.

When the Go To Business1 button is clicked, a new MyRNViewController is pushed in using UINavigationContoller. When the attempted loading in the MyRNViewController is complete, Business1.bundle. js is executed using the same Bridge load, and on success the runApplication method is executed to render the page.

Let’s talk about it in detail.

Improvement of AppDelegate. M

As mentioned above, when the application starts, it initializes a MyRNViewController via UINavigationContoller, which is implemented in the Application method of AppDelegate.m:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  MyRNViewController *vc =  [[MyRNViewController alloc] initWithModuleName:@"home"];
  self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:vc];
  [self.window makeKeyAndVisible];
  return YES;
}
Copy the code

MyRNViewController

// MyRNViewController is automatically called when initialized
- (void)loadView {
  RCTRootView *rootView = [Helper createRootViewWithModuleName:_moduleName
                                                    initialProperties:@{}];
  self.view = rootView;
}
Copy the code

The loadView method is automatically called when MyRNViewController is initialized. This method creates a RCTRootView as the default view of the ViewController:

+ (RCTRootView *) createRootViewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties {
  // _sharedBridge RCTBridge of the global share
  // Initialize if not already initialized
  if(! _sharedBridge) { [self createBridgeWithURL:[NSURL URLWithString:baseUrl]];
  }

  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_sharedBridge
                                                   moduleName: moduleName
                                            initialProperties: initialProperties];
  return rootView;
}

Copy the code

The default view of MyRNViewController is loaded and the viewDidLoad method is executed:

// MyRNViewController attempts to load after completion
- (void)viewDidLoad {
  NSString *randStr = [Helper randomStr:2];
  NSURL *moduleUrl = [NSURL URLWithString:[NSString stringWithFormat:@ "http://127.0.0.1:8080/%@.bundle.js? v=%@", _moduleName, randStr]];
  [Helper loadBundle:moduleUrl runAppOnView:self.view];
}
Copy the code

This method loads the bundle and executes the runApplication method on the current view:

+ (void) loadBundle:(NSString *)bundleUrl runAppOnView:(RCTRootView*)view {
  // Ensure that the base bundle is loaded only once
  if (needLoadBaseBundle) {
    [Helper loadBundleWithURL:[NSURL URLWithString:baseUrl] onComplete:^{
      needLoadBaseBundle = false;
      [Helper loadBundleWithURL:bundleUrl onComplete:^{
        [Helper runApplicationOnView:view];
      }];
    }];
  } else{ [Helper loadBundleWithURL:bundleUrl onComplete:^{ [Helper runApplicationOnView:view]; }]; }} + (void) loadBundleWithURL:(NSURL *)bundleURL onComplete:(dispatch_block_t)onComplete {
  [_sharedBridge loadAndExecuteSplitBundleURL2:bundleURL onError:^(void){} onComplete:^{
    NSLog([NSString stringWithFormat: @ "% @", bundleURL]);
    onComplete();
  }];
}

+ (void) runApplicationOnView:(RCTRootView *)view {
  [view runApplication:_sharedBridge];
}
Copy the code

LoadAndExecuteSplitBundleURL2 here is in the react – native source new method, at the same time also put (void) runApplication (RCTBridge *) bridge; Declaration is a public method for external use. See here for details.

According to the need to load

How is loading on demand triggered when we click the Go To Business1 button? Let’s look at the code for the home page:

import {View, Text, Button, StyleSheet, NativeModules} from 'react-native'; . <Button title='Go To Business1'
  onPress={() = > {
    NativeModules.Navigator.push('business1')}} / >...Copy the code

Here we actually implement a Native Module Navigator:

// Navigator.h
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface Navigator : NSObject <RCTBridgeModule>

@end
// Navigator.m
#import <UIKit/UIKit.h>

#import "Navigator.h"
#import "Helper.h"
#import "MyRNViewController.h"

@implementation Navigator

RCT_EXPORT_MODULE(Navigator);

/** * We are doing navigation operations, so make sure they are all on the UI Thread. * Or you can wrap specific methods that require the main queue like this: */
- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

RCT_EXPORT_METHOD(push:(NSString *)moduleName) {
  MyRNViewController *newVc = [[MyRNViewController alloc] initWithModuleName:moduleName];
  [[Helper getNavigationController] showViewController:newVc sender:self];
}

@end
Copy the code

When the push method is called, a new MyRNViewController is pushed into the UINavigationContoller, and the logic is similar to that described above.

conclusion

Based on the research results of the previous article, an actual React Native example was unpacked in this paper. Then, by rewriting the native code, different service packages can be loaded on demand. Project complete code here.

However, this is just a simple implementation, demo process, there are actually a lot of optimization can be done:

  • Currently loaded bundles are not cached and are re-downloaded each time. With caching, you can also optimize for differential updates, where every time the latest bundle is released, the difference between the previous version and the latest one is calculated, and when the client loads the bundle, it only needs to apply the difference to the old bundle.
  • All bundles run in the same context, and there are problems such as global variable pollution and all services crash when a bundle crashes.

Welcome to pay attention to the public account “front-end tour”, let us travel in the front-end ocean together.