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:
- usingPrevious articleThe React Native app is packed into three
bundle
:
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
- When entering the application, load and run it first
base.bundle.js
(including,react
及react-native
And so on base library), and then load runhome.bundle.js
, the page displays home-related content. - Click on the Home page
Go To Business1
Jump to business1 page, which will load and runbusiness1.bundle.js
The 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.