An overview of the

In addition to the H5 technology, Hybrid also developed FaceBook’s ReactNative, Alibaba’s Weex, and later even the Flutter.

This chapter focuses on how to integrate ReactNative into the project, and will use many of the JSBridge functions written before to provide Native interface calls to RN.

Create a ReactNative project

Current directory structure:

android/
ios/
web/
Copy the code

Let’s create a react-native directory

android/
ios/
web/
react-native/
Copy the code

Create an IntegrationProject under react-native

Create a package.json file with the following contents:

{
  "name": "integration-project"."version": "0.0.1"."private": true."scripts": {
    "start": "react-native start"
  },
  "dependencies": {
    "react": "16.9.0"."react-native": "0.61.5"}}Copy the code

That is, we will use [email protected] and [email protected] for integration, and the version declaration is not good

cd react-native/IntegrationProject
npm install
Copy the code

Create index under the react – native/IntegrationProject. Js

import React from "react";
import { AppRegistry, StyleSheet, Text, View } from "react-native";

class HelloWorld extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>Hello, World 1</Text>
      </View>); }}var styles = StyleSheet.create({
  container: {
    flex: 1.justifyContent: "center"
  },
  hello: {
    fontSize: 20.textAlign: "center".margin: 10}}); AppRegistry.registerComponent("IntegrationProject", () => HelloWorld);
Copy the code

Note that registerComponent uses the name IntegrationProject, which is a key point that will be mentioned later

npm run start
Copy the code

The project is created if you start normal

Integrate into Android

Configuring adding a Dependency

In build.gradle (Project: ToBeBigFE), add:

allprojects {
    repositories {
        // point to our RN Android source code
        maven { url "$rootDir/.. /react-native/IntegrationProject/node_modules/react-native/android"}... }}Copy the code

In build. Gradle (Project: app), add dependencies:

implementation "com.facebook.react:react-native:+"
def hermesPath = "$rootDir/.. /react-native/IntegrationProject/node_modules/hermes-engine/android/"
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
Copy the code

Note that in actual projects, the preceding paths may not be like this. Configure the paths according to the relative paths of your Android project and react project

Then click Sync Now in the upper right corner

MyApplication initializes SoLoader

Add a line of code to MyApplication:

class MyApplication : Application() {

    override fun onCreate(a) {
        super.onCreate()
        SoLoader.init(this.false) // This line represents loading the react so library
        WebManager.init(this)}}Copy the code

ReactNativeActivity

Add reactnativeActivity.kt to react package and add reactnativeActivity.kt:

package com.example.tobebigfe.react

import android.app.Activity
import com.facebook.react.common.LifecycleState
import com.facebook.react.shell.MainReactPackage
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactRootView
import android.os.Bundle
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
import android.view.KeyEvent
import com.example.tobebigfe.BuildConfig
import android.view.KeyEvent.KEYCODE_MENU
import android.content.Intent


class ReactNativeActivity : Activity(), DefaultHardwareBackBtnHandler {

    private var mReactRootView: ReactRootView? = null
    private var mReactInstanceManager: ReactInstanceManager? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        mReactRootView = ReactRootView(this)
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setCurrentActivity(this)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()
        // The string here (e.g. "MyReactNativeApp") has to match
        // the string in AppRegistry.registerComponent() in index.jsmReactRootView!! .startReactApplication(mReactInstanceManager,"IntegrationProject".null)

        setContentView(mReactRootView)
    }

    override fun invokeDefaultOnBackPressed(a) {
        super.onBackPressed()
    }

    override fun onPause(a) {
        super.onPause() mReactInstanceManager? .onHostPause(this)}override fun onResume(a) {
        super.onResume() mReactInstanceManager? .onHostResume(this.this)}override fun onDestroy(a) {
        super.onDestroy() mReactInstanceManager? .onHostDestroy(this) mReactRootView? .unmountReactApplication() }override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        if(keyCode == KEYCODE_MENU && mReactInstanceManager ! =null) { mReactInstanceManager!! .showDevOptionsDialog()return true
        }
        return super.onKeyUp(keyCode, event)
    }

    override fun onBackPressed(a) {
        if(mReactInstanceManager ! =null) { mReactInstanceManager!! .onBackPressed() }else {
            super.onBackPressed()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent){ mReactInstanceManager? .onActivityResult(this, requestCode, resultCode, data)}}Copy the code

Note this line of code:

mReactRootView!! .startReactApplication(mReactInstanceManager,"IntegrationProject".null)
Copy the code

We used an IntegrationProject, the same as before

Configuration AndroidManifest. XML

<?xml version="1.0" encoding="utf-8"? >
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.tobebigfe">.<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />.<application .>
        <! React debug activity -->
        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
        <! -- Add ReactNativeActivity -->
        <activity
            android:name=".react.ReactNativeActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>

    </application>
</manifest>
Copy the code

MainActivity

For testing purposes, we add code to the last line of MainActivity’s onCreate

override fun onCreate(savedInstanceState: Bundle?).{... startActivity(Intent(this, ReactNativeActivity::class.java))
}
Copy the code

So as soon as the App opens it will go to RN’s interface

Run the App

If it is an emulator, this problem will not occur:

This is because in a development environment, the URL that ReactNativeActivity opens is http://localhost:8081

We need ADB to configure, do an IP mapping, it doesn’t matter, don’t understand

Adb configuration and IP mapping

If you run this from the command line:

adb
Copy the code

If you can use this command, you can skip the following step and configure ADB:

vim ~/.lbash_profile
Copy the code

Add a line:

export PATH="$PATH:/Users/mingo/Library/Android/sdk/platform-tools"
Copy the code

Connect to the front, exit after:

source ~/.bash_profile
Copy the code

Note that/Users/mingo/Library/Android/SDK/platform – the path and you install the Android SDK tools, if your AndroidStudio it’s not a bug, you can find from here:

After the front is done:

$ adb devices
List of devices attached
2KE0219B23007696	device

$ adb reverse tcp:8081 tcp:8081
8081
mingodeiMac:~ mingo$
Copy the code

Run again:

So that’s Android integration

Integrate into iOS

Use CocoaPods to manage dependencies

After this chapter, we will need CocoaPods to manage our dependencies. Of course, the previous dependencies managed by Swift Package Manager need not be changed, so we can mix the two dependencies.

Install CocoaPods. There are many ways to install CocoaPods

sudo gem install cocoapods
Copy the code

Create a podfile in ios with the following contents:

source 'https://github.com/CocoaPods/Specs.git'

# Required for Swift apps
platform :ios.'9.0'
use_frameworks!

# The target name is most likely the name of your project.
target 'ToBeBigFE' do

    react_native_path = ".. /react-native/IntegrationProject/node_modules/react-native"
    pod 'FBLazyVector'.:path= >"#{react_native_path}/Libraries/FBLazyVector"
    pod 'FBReactNativeSpec'.:path= >"#{react_native_path}/Libraries/FBReactNativeSpec"
    pod 'RCTRequired'.:path= >"#{react_native_path}/Libraries/RCTRequired"
    pod 'RCTTypeSafety'.:path= >"#{react_native_path}/Libraries/TypeSafety"
    pod 'React'.:path= >"#{react_native_path}"
    pod 'React-Core'.:path= >"#{react_native_path}"
    pod 'React-Core/DevSupport'.:path= >"#{react_native_path}"
    pod 'React-CoreModules'.:path= >"#{react_native_path}/React/CoreModules"
    pod 'React-RCTActionSheet'.:path= >"#{react_native_path}/Libraries/ActionSheetIOS"
    pod 'React-RCTAnimation'.:path= >"#{react_native_path}/Libraries/NativeAnimation"
    pod 'React-RCTBlob'.:path= >"#{react_native_path}/Libraries/Blob"
    pod 'React-RCTImage'.:path= >"#{react_native_path}/Libraries/Image"
    pod 'React-RCTLinking'.:path= >"#{react_native_path}/Libraries/LinkingIOS"
    pod 'React-RCTNetwork'.:path= >"#{react_native_path}/Libraries/Network"
    pod 'React-RCTSettings'.:path= >"#{react_native_path}/Libraries/Settings"
    pod 'React-RCTText'.:path= >"#{react_native_path}/Libraries/Text"
    pod 'React-RCTVibration'.:path= >"#{react_native_path}/Libraries/Vibration"
    pod 'React-Core/RCTWebSocket'.:path= >"#{react_native_path}"

    pod 'React-cxxreact'.:path= >"#{react_native_path}/ReactCommon/cxxreact"
    pod 'React-jsi'.:path= >"#{react_native_path}/ReactCommon/jsi"
    pod 'React-jsiexecutor'.:path= >"#{react_native_path}/ReactCommon/jsiexecutor"
    pod 'React-jsinspector'.:path= >"#{react_native_path}/ReactCommon/jsinspector"
    pod 'ReactCommon/jscallinvoker'.:path= >"#{react_native_path}/ReactCommon"
    pod 'ReactCommon/turbomodule/core'.:path= >"#{react_native_path}/ReactCommon"

    pod 'Yoga'.:path= >"#{react_native_path}/ReactCommon/yoga"

    pod 'DoubleConversion'.:podspec= >"#{react_native_path}/third-party-podspecs/DoubleConversion.podspec"
    pod 'glog'.:podspec= >"#{react_native_path}/third-party-podspecs/glog.podspec"
    pod 'Folly'.:podspec= >"#{react_native_path}/third-party-podspecs/Folly.podspec"

end
Copy the code

How did we introduce ReactNative

Install dependencies

cd ios
pod install
Copy the code

After successful installation, the structure looks like this:

At this point, we close XCode and double-click tobebigFe. xcworkspace to open the project.

ReactNativeController

Add a Group React and increased ReactNativeController inside swift document, the content is as follows:

import Foundation
import React

class ReactNativeController : UIViewController {

    override func loadView(a) {
        let jsCodeURL = URL(string: "http://localhost:8081/index.bundle? platform=ios")!
        let rootView = RCTRootView(
            bundleURL: jsCodeURL,
            moduleName: "IntegrationProject",
            initialProperties: [:],
            launchOptions: nil
        )
        self.view = rootView
    }

}
Copy the code

You can set localhost to your computer’s IP address by skipping the path to jsCodeURL for real computers

To make it easier to see the results of the integration, we add a line of code to the viewDidLoad of the ViewController:

func viewDidLoad(a){...self.present(ReactNativeController(), animated: true, completion: nil)}Copy the code

In this way, as soon as you open the App, you’ll jump to the ReactNative interface

The results


So we’re done integrating here

Integration with the Development Environment (Android)

In the previous section we started ReactNative with the setUseDeveloperSupport method

mReactInstanceManager = ReactInstanceManager.builder()
    ...
    .setUseDeveloperSupport(BuildConfig.DEBUG)
    ...
    .build()
Copy the code

But this method does not control the host port

This section focuses on integrating our previous development environment Settings page to provide configurable meeting environment Settings

The development environment setup supports ReactNative

res/xml/dev_preferences.xml

Add a Preference item:

<ListPreference
    app:key="webDev.type"
    app:title="Project Type"
    app:entries="@array/web_dev_types"
    app:entryValues="@array/web_dev_types"
    app:useSimpleSummaryProvider="true"
    />
Copy the code

@array/web_dev_types reports red, which needs to be defined

res/values/arrays.xml

Add file res/values/arrays.xml with the following contents:

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <string-array name="web_dev_types">
        <item>WebView</item>
        <item>ReactNative</item>
    </string-array>
</resources>
Copy the code

MainActivity. Kt changes

Added a menu item at the top of the ActionBar. Click to go to the React screen

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)

    if (BuildConfig.DEBUG) {
        shakeSensor = ShakeSensor(this)
        shakeSensor.shakeListener = this
        shakeSensor.register()
    }
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menu.add("Open the React")
    return super.onCreateOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.title == "Open the React") {
        val intent = Intent(this, ReactNativeActivity::class.java)
        intent.putExtra("project"."IntegrationProject")
        startActivity(intent)
    }
    return super.onOptionsItemSelected(item)
}
Copy the code

Run a look at the development environment

Go to the home page and go to the development environment Settings

Next, how to use the development environment Settings into ReactNative

WebManager changes

Add two methods for ReactNativeActivity

object WebManager {

    ...


    fun isDebugReactProject(id: String): Boolean {
        return isDebugProject(id) && preferences.isReactNativeProject()
    }

    fun getWebDevServer(a): String {
        returnpreferences.webDevServer() ? :"localhost"}... }...// It is added to determine whether the development environment is ReactNative
private fun SharedPreferences.isReactNativeProject(a): Boolean {
    return getString("webDev.type"."WebView") = ="ReactNative"
}
Copy the code

ReactNativeActivity changes

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)

    mReactRootView = ReactRootView(this)
    val builder = ReactInstanceManager.builder()
        .setApplication(application)
        .setCurrentActivity(this)
        .setBundleAssetName("index.android.bundle")
        .setJSMainModulePath("index")
        .addPackage(MainReactPackage())
        .setInitialLifecycleState(LifecycleState.RESUMED)

    val project = getProject()
    // For ReactNative
    if (WebManager.isDebugReactProject(project)) {
        / / change debug_http_host
        val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
        preferences.edit().putString("debug_http_host", WebManager.getWebDevServer()).apply()

        // Enable dev support
        builder.setUseDeveloperSupport(true)
        mReactInstanceManager = builder.build()
    } else {

        // Disable dev support
        builder.setUseDeveloperSupport(false)
        mReactInstanceManager = builder.build()
    }

    // The string here (e.g. "MyReactNativeApp") has to match
    // the string in AppRegistry.registerComponent() in index.jsmReactRootView!! .startReactApplication(mReactInstanceManager, getProject(),null)

    setContentView(mReactRootView)
}

// Add a method to get the intent parameter project
fun getProject(a): String {
    return intent.getStringExtra("project") ?: "IntegrationProject"
}
Copy the code

Debug_http_host is a key obtained by DefaultSharedPreferences in ReactNative’s DevSupport module.

Running effect

This completes the development environment integration

Integration with the development environment (iOS)

In the previous section we wrote down the URL of ReactNative

let jsCodeURL = URL(string: "http://localhost:8081/index.bundle? platform=ios")!
Copy the code

This section focuses on integrating our previous development environment Settings page to provide configurable meeting environment Settings

Web/DevSettingsController changes

Add a way to select the project type and store it with Type as the key

class DevSettingsController.{

    lazy var typePickerField: OptionPickerFormItem = {
        let instance = OptionPickerFormItem()
        instance.title("Project Type").placeholder("required")
        instance.append([
            "WebView"."ReactNative"
        ])
        instance.selectOptionWithTitle(self.settings.string(forKey: "type")??"WebView")
        instance.valueDidChange = { (selected: OptionRowModel?).in
            letprojectType = selected? .title ??"WebView"
            self.settings.setValue(projectType, forKey: "type")}return instance
    }()
    
    override func populate(_ builder: FormBuilder){... builder += typePickerField// }}Copy the code

WebManager changes

func getWebUrl(id: String, page: String) -> String {
    if isDebugProject(id),
        let server = settings.string(forKey: "server"),
        let project = settings.string(forKey: "project")
    {
        // If React is used, return the React development link
        if settings.string(forKey: "type") = ="ReactNative" {
            return "http://\(server)/\(page)? platform=ios"
        }
        return "http://\(server)/\(project)/dev/\(page)"}... }Copy the code

ViewController changes

Use the React button in the upper right corner to open the React screen so that you can click on the first page to pop up the development environment page

class ViewController : WebViewController {
    
    override func viewDidLoad(a) {
        project = "home"
        super.viewDidLoad()
        
        // self.present(ReactNativeController(), animated: true, completion: nil)
        let reactBtn = UIBarButtonItem(title: "Open the React", style: .plain, target: self, action: #selector(onClickReact))
        navigationItem.rightBarButtonItem = reactBtn
    }
    
    @objc private func onClickReact(a) {
        self.present(ReactNativeController(project: "IntegrationProject"), animated: true, completion: nil)}}Copy the code

Shake it and set it up

Simulator: Hardware -> Shake Gesture

Click on the project type and select ReactNative

Go back to the home page and click on the upper right corner to open React

This completes the development environment integration

Project “Todolist” developed

This time we will use ReactNative to develop a simple project and cooperate with WebView project

Project Home provides todolist entry

We added the entry and provided it to version 2 to release the Home project

package.json

  "deploy": {
    "version": "2"
  }
Copy the code

App.vue

Add an entry:

<div class="item todolist" @click="onClickTodoList">TodoList</div>
Copy the code

Jump to TodoList

onClickTodoList() {
    JSBridge.Navigation.open({
        id: 'todolist'.type: 'ReactNative'.page: 'index.bundle'})}Copy the code

Here we add the type parameter for Navigation. Open, instead of saying WebView, we say ReactNative, so Native needs to support jumping to ReactNative, which we’ll talk about later

release

Here I changed the source directory structure of the book, using the build_and_copy.sh and generateManifest. /

target=".. /.. /.. /book-to-be-big-fe-deploy/"Copy the code
let manifestDeployPath = path.join(__dirname, ".. /.. /.. /book-to-be-big-fe-deploy/manifest.json")
Copy the code

Run, build and copy to book-to-be-big-fe-deploy

cd web/deploy
sh build_and_copy.sh
Copy the code

!!!!!!!!! Note: After you commit Git, be sure to go to Gitee to publish

run

After running and opening the homepage 2 to N times, it will be updated to the ZIP package and the page will become:

This is what iOS looks like, and Android looks the same. And clicking will crash because we don’t have a Todolist business project yet

Create the TodoList project

Make a copy of the IntegrationProject, rename it todolist, and rename the name in packages. Json to todolist

Index. Js changes

Change to TodoList functionality

import React, { Component } from "react";
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  FlatList,
  AsyncStorage,
  Button,
  TextInput,
  Keyboard,
  Platform
} from "react-native";

const isAndroid = Platform.OS == "android";
const viewPadding = 10;

export default class TodoList extends Component {
  state = {
    tasks: [].text: ""
  };

  changeTextHandler = text= > {
    this.setState({ text: text });
  };

  addTask = (a)= > {
    let notEmpty = this.state.text.trim().length > 0;

    if (notEmpty) {
      this.setState(
        prevState= > {
          let { tasks, text } = prevState;
          return {
            tasks: tasks.concat({ key: tasks.length, text: text }),
            text: ""
          };
        },
        () => Tasks.save(this.state.tasks) ); }}; deleteTask =i= > {
    this.setState(
      prevState= > {
        let tasks = prevState.tasks.slice();

        tasks.splice(i, 1);

        return { tasks: tasks };
      },
      () => Tasks.save(this.state.tasks)
    );
  };

  componentDidMount() {
    Keyboard.addListener(
      isAndroid ? "keyboardDidShow" : "keyboardWillShow",
      e => this.setState({ viewPadding: e.endCoordinates.height + viewPadding })
    );

    Keyboard.addListener(
      isAndroid ? "keyboardDidHide" : "keyboardWillHide", () = >this.setState({ viewPadding: viewPadding })
    );

    Tasks.all(tasks= > this.setState({ tasks: tasks || [] }));
  }

  render() {
    return( <View style={[styles.container, { paddingBottom: this.state.viewPadding }]} > <TextInput style={styles.textInput} onChangeText={this.changeTextHandler} onSubmitEditing={this.addTask} value={this.state.text} placeholder="Add Tasks" returnKeyType="done" returnKeyLabel="done" /> <FlatList style={styles.list} data={this.state.tasks} renderItem={({ item, index }) => <View> <View style={styles.listItemCont}> <Text style={styles.listItem}> {item.text} </Text> <Button title="X" onPress={() => this.deleteTask(index)} /> </View> <View style={styles.hr} /> </View>} /> </View> ); } } let Tasks = { convertToArrayOfObject(tasks, callback) { return callback( tasks ? tasks.split("||").map((task, i) => ({ key: i, text: task })) : [] ); }, convertToStringWithSeparators(tasks) { return tasks.map(task => task.text).join("||"); }, all(callback) { return AsyncStorage.getItem("TASKS", (err, tasks) => this.convertToArrayOfObject(tasks, callback) ); }, save(tasks) { AsyncStorage.setItem("TASKS", this.convertToStringWithSeparators(tasks)); }}; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#F5FCFF", padding: viewPadding, paddingTop: 20 }, list: { width: "100%" }, listItem: { paddingTop: 2, paddingBottom: 2, fontSize: 18 }, hr: { height: 1, backgroundColor: "gray" }, listItemCont: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, textInput: { height: 40, paddingRight: 10, paddingLeft: 10, borderColor: "gray", borderWidth: isAndroid ? 0 : 1, width: "100%" } }); AppRegistry.registerComponent("todolist", () => TodoList);Copy the code

Running the development server

npm run start
Copy the code

iOS

JSBridgeNavigation changes

Support to open type=ReactNative projects

private func open(callbackId: String, arg: [String : Any?] ) {
    guard let vc = self.viewController else { return }
    let type = arg["type"] as? String ?? "WebView"
    if type == "WebView" {
        guard let url = parseWebUrl(arg) else { return }
        let newVC = WebViewController()
        newVC.url = url
        newVC.project = arg["id"] as? String
        if let params = arg["params"] as? [String:Any? ]  { newVC.params = params } vc.present(newVC, animated:true, completion: nil)}else if (type == "ReactNative") {
        let project = arg["id"] as! String
        let rectVC = ReactNativeController(project: project)
        vc.present(rectVC, animated: true, completion: nil)}else {
        vc.view.makeToast("Invalid type: \(type)")}}Copy the code

Does this run or crash, crash in webManager.getweburl, because TodoList is not in our manifest.json yet, and the business is not officially live yet

Configure as a project in development


The final result


Android

JSBridgeNavigation changes

private fun open(callbackId: String, arg: JSONObject) {
    val type = if (arg.has("type")) {
        arg.getString("type")}else {
        "WebView"
    }

    if (type == "WebView") {
        val intent = Intent(activity, WebActivity::class.java)
        // If the id parameter is passed in the front end, the item name jump mode is used
        if (arg.has("id")) {
            val id = arg.getString("id")
            val page = arg.getString("page")
            val url = WebManager.getWebUrl(id, page)
            intent.putExtra("url", url)
            intent.putExtra("project", id)
        }
        // Otherwise, there are URL arguments
        else if (arg.has("url")) {
            intent.putExtra("url", arg.getString("url"))}if (arg.has("params")) {
            val params = arg.get("params") asJSONObject? params? .let { intent.putExtra("params", it.toString())
            }
        }

        activity.startActivity(intent)
    }

    else if (type == "ReactNative") {
        val intent = Intent(activity, ReactNativeActivity::class.java)
        val id = arg.getString("id")
        intent.putExtra("project", id)
        activity.startActivity(intent)
    }
}
Copy the code

Does this run or crash, crash in webManager.getweburl, because Todolist is not in our manifest.json yet, and the business is not officially live yet

Configure as a project in development


The final result


conclusion

We developed todolist project and jumped to ReactNative project using Navigation module. We’ll tackle two more important questions later:

  • ReactNative uses the existing JSBridge
  • ReactNative project offline package

Offline Package Publishing

Next up is how to publish ReactNative as an offline package.

With our previous foundation, we only need to publish the ReactNative project as a standard ZIP package.

Take Todolist as an example

Build the ReactNative project

Take a look at the official build commands, using Android as an example

react-native bundle --platform android --dev false --entry-file index.js --bundle-output dist/index.android.js
Copy the code

As you can see, the difference in offline packages will be different due to different systems and different end products, so we need to resolve this situation by generating different ZIP packages and deploying them to different paths

Let’s start with the goals. We want the structure after build and pack:

dist/todolist/[version]/
  android/
    index.js
    todolist.zip
  ios/
    index.js
    todolist.zp
Copy the code

This structure also applies, so we need to modify our build and deployment processes to accommodate ReactNative

todolist/package.json

. "scripts": { "start": "react-native start", "build": "npm run build-android && npm run build-ios", "build-android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output dist/android/index.js --assets-dest dist/android", "build-ios": "react-native bundle --platform ios --dev false --entry-file index.js --bundle-output dist/ios/index.js --assets-dest dist/ios" }, ...Copy the code

NPM run build builds the following directory:

dist/
  android/
    index.js
  ios/
    index.js
Copy the code

Once the build is complete, the next step is to package the desired directory

deploy

Before completing the packaging, we will adjust the location of deploy directory. Since DEPLOY will serve both Web and React-Native, not just Web, we will remove deploy from Web and place it next to Web and React-Native:

android/
ios/
web/
react-native/
deploy/
    ...
Copy the code

Then step by step write code compatible with the React – Native package

build_and_copy.sh

#! /bin/shtarget=".. /.. /.. /book-to-be-big-fe-deploy/"
## web projectfor name in "home" "news" "weather" do if [ "$1" == "$name" ] || [ "$1" == "" ] ; then echo building project $name cd .. /web/$name rm -R dist npm run build node .. /.. /deploy/pack.js cp -r -f ./dist/ $target cd .. /.. /deploy fi done
## the react - native projectfor name in 'todolist' do if [ "$1" == "$name" ] || [ "$1" == "" ] ; Then echo building project $name # /react-native/$name ## clean rm -r dist ## Mkdir dist mkdir dist/ Android mkdir dist/ios ## build NPM run ## pack /.. ## copy cp -r -f./dist/ $target ## /.. /deploy fi done node generateManifest.jsCopy the code

pack.js

const path = require("path");
const { execSync } = require("child_process");

// Get the type parameter
const type = process.argv[2] | |"WebView"
const prjPath = process.cwd();
const packageJSON = require(path.join(prjPath, "package.json"));
const name = packageJSON.name;
const version = packageJSON.deploy.version;

/ / the web project
if (type === "WebView") {
  execSync('zip -q -r ' + name + '.zip *', {
      cwd: path.join(prjPath, "dist", name, version),
  })
} 
/ / the react - native project
else if (type === "ReactNative") {
  const distCwd = path.join(prjPath, 'dist')

  // Build the directory structure for deployment
  execSync('mkdir ' + name, { cwd: distCwd })
  execSync('mkdir ' + name + '/' + version, { cwd: distCwd })

  // Move the build to the deployment directory
  execSync('mv android ' + name + '/' + version, { cwd: distCwd })
  execSync('mv ios ' + name + '/' + version, { cwd: distCwd })

  / / zip
  execSync('zip -q -r ' + name + '.zip *', { 
      cwd: path.join(prjPath, 'dist/' + name + '/' + version + '/android')
  })
  execSync('zip -q -r ' + name + '.zip *', {
    cwd: path.join(prjPath, 'dist/' + name + '/' + version + '/ios')})}console.log('pack ok: ' + name)
Copy the code

generateManifest.js

Support for React-Native projects, and add a type parameter for each project, the value is WebView or ReactNative, for native code to determine the project type

const path = require("path")
const fs = require('fs')

const webPrjList = ['home'.'news'.'weather']
const reactPrjList = ['todolist']<i>
const manifest = {
    projects: []
}

webPrjList.forEach(prj= > {
    let version = require(path.join(__dirname, '.. /web', prj, 'package.json')).deploy.version
    manifest.projects.push({
        name: prj,
        type: 'WebView'.version: version
    })
})

reactPrjList.forEach(prj= > {
    let version = require(path.join(__dirname, '.. /react-native', prj, 'package.json')).deploy.version
    manifest.projects.push({
        name: prj,
        type: 'ReactNative'.version: version
    })
})

let manifestDeployPath = path.join(__dirname, ".. /.. /book-to-be-big-fe-deploy/manifest.json")
fs.writeFileSync(manifestDeployPath, JSON.stringify(manifest, null.2))
Copy the code

Running the deployment script

sh build_and_copy.sh
Copy the code

Contents to deploy to book-to-big-fe-deploy:

Manifest.json contents:

{
  "projects": [{"name": "home"."type": "WebView"."version": "2"
    },
    {
      "name": "news"."type": "WebView"."version": "1"
    },
    {
      "name": "weather"."type": "WebView"."version": "1"
    },
    {
      "name": "todolist"."type": "ReactNative"."version": "1"}}]Copy the code

At this point, the build package deployment is complete and, of course, you still commit Git manually to deploy Gitee Pages

Support for ReactNative Offline package (Android)

After deploying the ReactNative offline package in this article, we need to support Native as well

Re-copy the latest offline package into Assets

Delete the contents of Assets and drag the new contents into Assets without copying the ios Todolist

VersionManager

First we need a type that supports manifest.json

class VersionManager {...// Add a map
    private val typeMap = mutableMapOf<String, String>()

    ...

    // Add the getType method
    fun getType(id: String): String {
        returntypeMap[id] ? :"WebView"}...private fun parseManifest(content: String, output: MutableMap<String, String>) {
        val manifest = JSONObject(content)
        val projects = manifest.getJSONArray("projects")
        for (i in 0 until projects.length()) {
            val prj = projects.getJSONObject(i)

            // 以下增加 type 到 map 
            val name = prj.getString("name")
            output[name] = prj.getString("version")
            // Compatible with the following past cases where there was no type in preferences
            typeMap[name] = if (prj.has("type")) {
                prj.getString("type")}else {
                "WebView"}}}}Copy the code

ZipManager

The first step is to add the constructor, which is passed in to the versionManager

class ZipManager(... .val versionManager: VersionManager) {
    ...
}
Copy the code

UpdateOfflineZip supports downloading of the android ReactNative offline package. The download link is different from that of the web

/ / the original code
val zipUrl = "${WebConst.WEB_BASE_URL}$id/$version/$id.zip"

/ / modern yards
val type = versionManager.getType(id)
val zipUrl = if (type == "ReactNative") {
    "${WebConst.WEB_BASE_URL}$id/$version/android/$id.zip" // Point to the android location
} else {
    "${WebConst.WEB_BASE_URL}$id/$version/$id.zip"
}
Copy the code

WebManager

The versionManager is passed when the ZipManager is created

fun init(context: Context){... zipManager = ZipManager(context, httpClient, versionManager) }Copy the code

Then getWebUrl supports ReactNative, retrieving native JS files because you don’t need to return HTTP links

// The last line, the original code
return "${WebConst.WEB_BASE_URL}$id/$version/$page"

/ / modern yards
val type = versionManager.getType(id)
return if (type == "ReactNative") {
    val jsFileName = page.replace(".bundle".".js")
    val jsFile = getOfflineFile(id, version, jsFileName)
    jsFile.absolutePath
} else {
    "${WebConst.WEB_BASE_URL}$id/$version/$page"
}
Copy the code

This completes the Android support offline package.

You can shut down your development environment and run your tests, and ReactNative loads faster than the Web.

Support for ReactNative Offline pack (iOS)

After deploying the ReactNative offline package in this article, we need to support Native as well

Re-copy the latest offline package into Assets

Delete the contents of Assets and drag the new contents into Assets without copying android’s Todolist

VersionManager

First we need a type that supports manifest.json

class VersionManager {...// Add a dict
    private var typeDict = [String:String(of).../ / getType increase
    func getType(id: String) -> String {
        return typeDict[id] ?? "WebView"}...private func parseManifest(_ data: Data, output: inout [String:String]) throws {
        let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any? ]let projects = json["projects"] as! [[String:Any? ] ] projects.forEach { itemin
            let name = item["name"] as! String
            output[name] = item["version"] as? String
            typeDict[name] = item["type"] as? String ?? "WebView"  // Add this line here}}}Copy the code

ZipManager

The first step is to add the constructor, which is passed in to the versionManager

class ZipManager {
    
    private let versionManager: VersionManager.init(versionManager: VersionManager) {
        self.versionManager = versionManager ... }}Copy the code

UpdateOfflineZip supports download of the ReactNative offline package for ios. The download link is different from that for the web

/ / the original code
let zipUrl = "\(WebConst.WEB_BASE_URL)\(id)/\(version)/\(id).zip"

/ / modern yards
var zipUrl = ""
let type = versionManager.getType(id: id)
if type == "ReactNative" {
    zipUrl = "\(WebConst.WEB_BASE_URL)\(id)/\(version)/ios/\(id).zip"  // Point to the location of ios
} else {
    zipUrl = "\(WebConst.WEB_BASE_URL)\(id)/\(version)/\(id).zip"
}
Copy the code

WebManager

The versionManager is passed when the ZipManager is created

class WebManager {...private let zipManager: ZipManager
    
    init() {...self.zipManager = ZipManager(versionManager: self.versionManager)
    }

    ...
}
Copy the code

Then getWebUrl supports ReactNative, retrieving native JS files because you don’t need to return HTTP links

// The last line, the original code
return "\(WebConst.WEB_BASE_URL)\(id)/\(version!)/\(page)"

/ / modern yards
let type = versionManager.getType(id: id)
if type == "ReactNative" {
    // index.bundle -> index.js
    // index.bundle: from the development environment
    // index.js: publish environment
    // In fact, there can be unified, personal error, but it is not a problem
    let jsFile = page.replacingOccurrences(of: ".bundle", with: ".js")
    return "file://\(getOfflineFile(id: id, version: version! , filePath: page))"
} else {
    return "\(WebConst.WEB_BASE_URL)\(id)/\(version!)/\(page)"
}
Copy the code

This completes the ios support offline package.

You can shut down your development environment and run your tests, and ReactNative loads faster than the Web.

Modules that support calling JSBridge

We have written a lot of JSBridge modules before, and we hope to provide these modules to ReactNative for invocation. The actual project of the company is also like this, because it has accumulated a lot of Native codes. Therefore, after integrating ReactNative, One of the biggest problems is providing reuse of existing functionality.

It just so happens that we had such a good structure that it was easy to do that

JS end change

Adjust the common directory

First, we need to modify some of the code in Jsbridge.js to be compatible with the ReactNative call

Before we change it, I want us to clean up the position of Web/Common and align it with Web React-Native, because jsbridge-js is no longer just for the Web

The directory structure is very clear:

android/
ios/
web/
react-native/
deploy/
common/
  jsbridge.js
  package.json
Copy the code

Jsbridge. Js modified

We split jsbridge.js into three files:

Common/jsbridge-webview.js // module defines jsbridge-webview.js // Web project reference entry, Jsbridge-react-native. Js // react-native project reference entry, ReactNative codeCopy the code

jsbridge.js:

/* eslint-disable */

// Pass in JSBridge to mount the module
module.exports = function(JSBridge) {
  JSBridge.UI = {};
  JSBridge.UI.toast = function(message) {
    callNative("UI.toast", { message: message });
  };
  JSBridge.UI.alert = function(params) {
    callNative("UI.alert", params);
  };
  JSBridge.UI.confirm = function(params, callback) {
    callNative("UI.confirm", params, callback);
  };

  JSBridge.KVDB = {};
  JSBridge.KVDB.getInt = function(key, callback) {
    callNative("KVDB.getInt", { key: key }, callback);
  };
  JSBridge.KVDB.setInt = function(key, value) {
    callNative("KVDB.setInt", { key: key, value: value });
  };
  JSBridge.KVDB.getBool = function(key, callback) {
    callNative("KVDB.getBool", { key: key }, callback);
  };
  JSBridge.KVDB.setBool = function(key, value) {
    callNative("KVDB.setBool", { key: key, value: value });
  };
  JSBridge.KVDB.getString = function(key, callback) {
    callNative("KVDB.getString", { key: key }, callback);
  };
  JSBridge.KVDB.setString = function(key, value) {
    callNative("KVDB.setString", { key: key, value: value });
  };
  JSBridge.KVDB.getJSON = function(key, callback) {
    callNative("KVDB.getString", { key: key }, function(result) {
      callback(JSON.parse(result));
    });
  };
  JSBridge.KVDB.setJSON = function(key, value) {
    callNative("KVDB.setString", { key: key, value: JSON.stringify(value) });
  };

  JSBridge.Camera = {};
  JSBridge.Camera.takePicture = function(callback) {
    callNative("Camera.takePicture", {}, callback);
  };
  JSBridge.Camera.takeVideo = function(callback) {
    callNative("Camera.takeVideo", {}, callback);
  };
  JSBridge.Image = {};
  JSBridge.Image.pickPhotos = function(callback) {
    callNative("Image.pickPhotos", {}, callback);
  };

  function resolveNavParams(params) {
    params = JSON.parse(JSON.stringify(params));
    if(! params.url) {let path = location.pathname;
      if(! path) { params.url = location.protocol +"/ /" + location.host + "/" + params.page;
      } else {
        params.url =
          location.protocol +
          "/ /" +
          location.host +
          path.substr(0, path.lastIndexOf("/") + 1) + params.page; }}return params;
  }

  JSBridge.Navigation = {};
  JSBridge.Navigation.open = function(params, callback) {
    callNative("Navigation.open", resolveNavParams(params), callback);
  };
  JSBridge.Navigation.close = function(callback) {
    callNative("Navigation.close", {}, callback);
  };
  JSBridge.Navigation.push = function(params, callback) {
    if (window.androidBridge) {
      JSBridge.Navigation.open(params, callback);
    } else {
      callNative("Navigation.push", resolveNavParams(params), callback); }}; JSBridge.Navigation.getParams =function(callback) {
    callNative("Navigation.getParams", {}, callback);
  };
};

Copy the code

jsbridge-webview.js

const initJSBridgeModules = require("./jsbridge");

module.exports = (function(JSBridge) {
  var currentCallbackId = 0;

  function callNative(method, data, callback) {
    // Generate a unique callbackId
    var callbackId = "nativeCallback_" + currentCallbackId++;

    if (callback) {
      // Add callback to window
      window[callbackId] = function(result) {
        delete window[callbackId];
        callback(result);
      };
    }

    var stringData = JSON.stringify(data);
    if (window.androidBridge) {
      // Android passes three parameters
      window.androidBridge.callNative(callbackId, method, stringData);
    } else {
      // iOS does not support multiple parameters, we pass JSON objects
      window.webkit.messageHandlers.iOSBridge.postMessage({
        callbackId: callbackId,
        method: method,
        data: stringData }); }}// Hang the method under window.jsbridge
  var JSBridge = (window.JSBridge = {});

  JSBridge.callNative = callNative;
  initJSBridgeModules(JSBridge);

  returnJSBridge; }) ();Copy the code

Jsbridge-react-native. Js, very simple, and ReactNative supports Promise

/ / introduce NativeModules
const { NativeModules } = require('react-native')
const initJSBridgeModules = require("./jsbridge");

module.exports = (function() {
  const JSBridge = {}
  JSBridge.callNative = (method, data, callback) = > {
    var stringData = JSON.stringify(data);
    // Native defines JSBridge. CallNative, which we'll talk about later
    NativeModules.JSBridge.callNative(method, stringData).then(callback)
  }
  initJSBridgeModules(JSBridge);
  returnJSBridge; }) ();Copy the code

Fix references in web projects

Fixed three project import issues in the Web, fixing references and paths in code where three projects reference JsBridge

-   import JSBridge from '.. /.. /common/jsbridge'
+   import JSBridge from '.. /.. /.. /common/jsbridge-webview'
Copy the code

Go to the JS side here to complete the modification, and then the Native side

Modules that support calling JSBridge (Android)

How does this analysis support ReactNative’s module calling JSBridge

Understand ReactNative calls Native

First of all, let’s understand how ReactNative JS calls Native. We talked about it in the last section:

const { NativeModules } = require("react-native");

NativeModules.JSBridge.callNative(method, stringData).then(callback);
Copy the code

Jsbridge-react-native. Js

  • NativeModuels is where ReactNative provides us to mount local objects
  • JSBridge is an object that we Native injected into NativeModuels
  • CallNative is a Native method provided by JSBridge

So how does Native add the JSBridge object

::: tip Note that the following code is not the final result, just understand the process, the final code has some differences, will be explained later :::

/ / declare a class, inheritance ReactContextBaseJavaModule
class ReactNativeBridge(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
    // override getName, which is the name we mounted JSBridge,
    // You can change as you like, we use JSBridge here
    override fun getName(a): String {
        return "JSBridge"
    }

    // declare JSBridge. CallNative using @reactMethod.
    // This is a promise method
    @ReactMethod
    fun callNative(method: String, arg: String, promise: Promise) {
        Log.e("JSBridge"."%s %s".format(method, arg))
    }
}

// Declare a class that inherits ReactPackage
class JSBridgePackage() : ReactPackage {

    // In createNativeModules, create the objects above to mount,
    JSBridge = JSBridge = JSBridge
    override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
        val bridge = ReactNativeBridge(reactContext)
        return mutableListOf(bridge)
    }
    override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
        return mutableListOf()
    }
}

// In the previous ReactNativeActivity
class ReactNativeActivity.{
     override fun onCreate(savedInstanceState: Bundle?).{...val builder = ReactInstanceManager.builder()
            .setApplication(application)
            .setCurrentActivity(this)
            .addPackage(MainReactPackage())
            // Add this line
            .addPackage(JSBridgePackage())
            .setInitialLifecycleState(LifecycleState.RESUMED)
     }
}
Copy the code

Problem analysis of callNative

Previously we registered these modules in the BridgeObject of the WebViewBridge

bridgeModuleMap["UI"] = JSBridgeUI(activity, webView)
bridgeModuleMap["KVDB"] = JSBridgeKVDB(activity, webView)
bridgeModuleMap["Camera"] = JSBridgeCamera(activity, webView)
bridgeModuleMap["Image"] = JSBridgeImage(activity, webView)
bridgeModuleMap["Navigation"] = JSBridgeNavigation(activity, webView)
Copy the code

We can also register these modules in the ReactNativeBridge, but there is a problem. The parameters of these modules are WebActivity and WebView

For ReactNative, the absence of these two objects indicates that these modules depend on specific communication environments, which are webView-related classes

So, we need to refactor this part

The JsBridge package differentiates into JsBridge and WebView

We split jsBridge into two parts:

com.example.tobebigfe
    jsbridge
        JSBridgeCamera
        JSBridgeImage
        JSBridgeKVDB
        JSBridgeNavigation
        JSBridgeUI
    webivew
        CustomWebViewClient
        WebActivity
        WebViewBridge
Copy the code

So jsBridge is code that is independent of the specific communication environment, and WebView is code that is relevant to webView communication

::: tip Create a new webView package in Android Studio and drag the file into the webView.

Resolve WebActivity dependencies

Create a BaseActivity from WebActivity to raise JSBridgeXXX these modules need to use:

BaseActivity.kt

Put BaseActivity next to MainActivity

Extract part of the WebActivity code into BaseActivity as follows:

package com.example.tobebigfe

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity

typealias ActivityResultCallback = (requestCode: Int, resultCode: Int, result: Intent?) -> Boolean

abstract class BaseActivity : AppCompatActivity() {

    private var activityResultCallback: ActivityResultCallback? = null

    fun startActivityForCallback(intent: Intent, requestCode: Int, callback: ActivityResultCallback) {
        activityResultCallback = callback
        startActivityForResult(intent, requestCode)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data) activityResultCallback? .let {if (it.invoke(requestCode, resultCode, data)) {
                activityResultCallback = null}}}}Copy the code

::: tip WebActivity removed code is not mentioned here :::

Refactor the original code

Let’s start by refactoring from the top interface

The original code is as follows:

interface BridgeModule {
    fun callFunc(func: String, callbackId: String, arg: JSONObject)
}

abstract class BridgeModuleBase(val webView: WebView) : BridgeModule {
    ...
}
Copy the code

The new code:

interface BridgeCall {
    val moduleName: String
    val funcName: String
    val arg: JSONObject
    fun callback(value: Int)
    fun callback(value: Boolean)
    fun callback(value: String?).
    fun callback(json: JSONObject)
}

interface BridgeModule {
    fun callFunc(call: BridgeCall)
}

open class BridgeRegistry(activity: BaseActivity) {

    protected val bridgeModuleMap = mutableMapOf<String, BridgeModule>()

    init {
        bridgeModuleMap["UI"] = JSBridgeUI(activity)
        bridgeModuleMap["KVDB"] = JSBridgeKVDB(activity)
        bridgeModuleMap["Camera"] = JSBridgeCamera(activity)
        bridgeModuleMap["Image"] = JSBridgeImage(activity)
        bridgeModuleMap["Navigation"] = JSBridgeNavigation(activity)
    }

    fun getModule(id: String): BridgeModule? {
        return bridgeModuleMap[id]
    }
}
Copy the code

We removed the BridgeModuleBase class, defined BridgeCall, and modified the parameters of the BridgeModule callFunc method

The design principle here is:

WebView      callNative - \
                          |  -> new BridgeCall -> BridgeModule.callFunc(call)
ReactNative  callNative - /
Copy the code
  • BridgeCall represents a call parameter and return interface that is passed to the specific BrigdeModule
  • BridgeRegistry is the management class for the registry module

And we put this part of the code into jsbridge.kt, as shown in the figure below:

In webviewBridge.kt, it looks like this:

// WebViewJSBridgeCall inherits BridgeCall
// WebViewJSBridgeCall defines the communication environment for WebView calls
// * callbackId: WebView unique communication mode
// * webView parameters
EvaluateJavascript is used in callback mode
class WebViewJSBridgeCall(
    override val moduleName: String,
    override val funcName: String,
    override val arg: JSONObject,
    val webView: WebView,
    private val callbackId: String) : BridgeCall {

    override fun callback(value: Int) {
        execJS("window.$callbackId($value)")}override fun callback(value: Boolean) {
        execJS("window.$callbackId($value)")}override fun callback(value: String?). {
        if (value == null) {
            execJS("window.$callbackId(null)")}else {
            execJS("window.$callbackId('$value')")}}override fun callback(json: JSONObject) {
        execJS("window.$callbackId($json)")}private fun execJS(script: String) {
        Log.e("WebView"."exec $script")
        webView.post {
            webView.evaluateJavascript(script, null)}}}// BridgeObject inherits BridgeRegistry
class BridgeObject(val activity: WebActivity, val webView: WebView) : BridgeRegistry(activity) {

    @JavascriptInterface
    fun callNative(callbackId: String, method: String, arg: String) {
        Log.e("WebView"."callNative $method args is $arg")
        val jsonArg = JSONObject(arg)
        val split = method.split(".")
        val moduleName = split[0]
        val funcName = split[1]

        val module = bridgeModuleMap[moduleName]
        / / create WebViewJSBridgeCall
        val call = WebViewJSBridgeCall(moduleName, funcName, jsonArg, webView, callbackId)
        / / callFunc incomingmodule? .callFunc(call) } }Copy the code

Modify several modules of JSBridgeXXX

Here, JSBridgeCamera is used as an example, and others can follow this method

First, inherit BridgeBaseModule to inherit BridgeModule directly, with a single parameter and type of BaseActivity

class JSBridgeCamera(val activity: BaseActivity) : BridgeModule { ... }
Copy the code

CallFunc is changed to a call parameter of type BridgeCall

override fun callFunc(call: BridgeCall) {
    when (call.funcName) {
        "takePicture" -> takePicture(call)
        "takeVideo" -> takeVideo(call)
    }
}
Copy the code

TakePicture and takeVideo accept the Call parameter instead

private fun takePicture(call: BridgeCall){... }private fun takeVideo(call: BridgeCall){... }Copy the code

Call. Arg (jsbridgeui.alert)

private fun alert(call: BridgeCall) {
    val arg = call.arg
    ...
}
Copy the code

Now that we’re done refactoring, we can run the following to see if there are any errors

ReactNativeBridge

Next we need to provide ReactNative with the ability to call JSBridge

Under the React package, create reactnativeBridge.kt

As follows:

class JSBridgePackage(val activity: ReactNativeActivity) : ReactPackage {

    override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
        return mutableListOf(ReactNativeBridge(activity, reactContext))
    }

    override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
        return mutableListOf()
    }

}

class ReactNativeBridge(activity: ReactNativeActivity, val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {

    private val bridgeRegistry = BridgeRegistry(activity)

    override fun getName(a): String {
        return "JSBridge"
    }

    @ReactMethod
    fun callNative(method: String, arg: String, promise: Promise) {
        Log.e("JSBridge"."%s %s".format(method, arg))

        val jsonArg = JSONObject(arg)
        val split = method.split(".")
        val moduleName = split[0]
        val funcName = split[1]

        val module = bridgeRegistry.getModule(moduleName)
        valcall = ReactNativeJSBridgeCall(moduleName, funcName, jsonArg, context, promise) module? .callFunc(call) } }class ReactNativeJSBridgeCall(
    override val moduleName: String,
    override val funcName: String,
    override val arg: JSONObject,
    val context: ReactApplicationContext,
    val promise: Promise
) : BridgeCall {

    override fun callback(value: Int) {
        promise.resolve(value)
    }

    override fun callback(value: Boolean) {
        promise.resolve(value)
    }

    override fun callback(value: String?). {
        promise.resolve(value)
    }

    override fun callback(json: JSONObject) {
        promise.resolve(json)
    }

}
Copy the code

ReactNativeJSBridgeCall defines the ReactNative BridgeCall, which uses the promise.resolve method as a callback

This completes the ReactNative call to JSBridge

Try out problem

We tried JSBridge at Todolist by introducing:

import JSBridge from ".. /.. /common/jsbridge-react-native.js";
Copy the code

Add trial code in addTask success is toast once:

addTask = (a)= > {
  let notEmpty = this.state.text.trim().length > 0;

  if (notEmpty) {
    this.setState(
      prevState= > {
        let { tasks, text } = prevState;
        return {
          tasks: tasks.concat({ key: tasks.length, text: text }),
          text: ""
        };
      },
      () => Tasks.save(this.state.tasks)
    );

    JSBridge.UI.toast("Add task success"); }};Copy the code

The jsbridge-react-native. Js file will not be found because ReactNative uses Metro as its packaging tool, which does not support references to code outside the project

Therefore, we will copy jsbridge-react-native. Js and jsbridge-react-native. Js to the project:

import JSBridge from "./jsbridge-react-native.js";
Copy the code

Running effect


Modules that support calling JSBridge (iOS)

How does this analysis support ReactNative’s module calling JSBridge

Understand ReactNative calls Native

First of all, let’s understand how ReactNative JS calls Native. We talked about it in the last section:

const { NativeModules } = require("react-native");

NativeModules.JSBridge.callNative(method, stringData).then(callback);
Copy the code

Jsbridge-react-native. Js

  • NativeModuels is where ReactNative provides us to mount local objects
  • JSBridge is an object that we Native injected into NativeModuels
  • CallNative is a Native method provided by JSBridge

So how does Native add the JSBridge object

::: tip Note that the following code is not the final result, just understand the process, the final code has some differences, will be explained later :::

First, we need to export our module using objc:

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(JSBridge.NSObject)

RCT_EXTERN_METHOD(callNative:(NSString *)method argString:(NSString *)argString resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)

@end
Copy the code

Then, declare the swift implementation

@objc(JSBridge)
class ReactNativeBridge: NSObject.RCTBridgeModule {

    @objc(callNative:argString:resolve:reject:)
    func callNative(method: String, argString: String, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        print("JSBridge \(method) \(argString)")}}Copy the code

This implements the NativeModules. JSBridge. CallNative can call through, the underlying principles do not speak here

But later implementations will be a little bit different, because we need to access the specific ViewController environment, which we’ll talk about later

Analyze the problems of callNative

Previously we registered these modules in the BridgeObject of the WebViewBridge

moduleDict["UI"] = JSBridgeUI(viewController: viewController)
moduleDict["KVDB"] = JSBridgeKVDB(viewController: viewController)
moduleDict["Camera"] = JSBridgeCamera(viewController: viewController)
moduleDict["Image"] = JSBridgeImage(viewController: viewController)
moduleDict["Navigation"] = JSBridgeNavigation(viewController: viewController)
Copy the code

We can also register these modules in the ReactNativeBridge, but there is a problem. The argument to these modules is a WebViewController

For ReactNative, the absence of these two objects indicates that these modules depend on specific communication environments, which are webView-related classes

So, we need to refactor this part

JSBridge splits into JSBridge and WebView

We split JSBridge into two parts:

ToBeBigFE JSBridge JSBridgeCamera.swift JSBridgeImage.swift JSBridgeKVDB.swift JSBridgeNavigation.swift JSBridgeUI.swift  WebView WebViewController.swift WebViewBridge.swiftCopy the code

In this way, JSBridge is the code independent of the specific communication environment, and the code under WebView is the code related to WebView communication

Resolve WebViewController dependencies

Create a BaseViewController that extracts JSBridgeXXX from WebViewController. These modules need to use:

BaseViewController.swift

So BaseViewController is going to be right next to ViewController

Extract part of the WebViewController code into BaseViewController

class BaseViewController : UIViewController {
    
    var project: String? = nil
    var params: [String:Any?] = [:]
    
}
Copy the code

::: tip WebViewController removed code is not mentioned here :::

Refactor the original code

Let’s start by refactoring from the top interface

The original code is as follows:

protocol BridgeModule : class {
    func callFunc(_funcName: String, callbackId: String, arg: [String: Any?] )
}

class BridgeModuelBase : BridgeModule {...// Some callback methods
}
Copy the code

The new code:

protocol BridgeCall {
    var moduleName: String { get }
    var funcName: String { get }
    var arg: [String : Any? ] {get }
    func callback(value: Int)
    func callback(value: Bool)
    func callback(value: String?)
    func callback(json: [String:Any?] )
}

protocol BridgeModule : class {
    func callFunc(_ call: BridgeCall)
}

class BridgeRegistry {
    
    var moduleDict = [String:BridgeModule] ()func initModules(viewController: BaseViewController) {
        moduleDict["UI"] = JSBridgeUI(viewController: viewController)
        moduleDict["KVDB"] = JSBridgeKVDB(viewController: viewController)
        moduleDict["Camera"] = JSBridgeCamera(viewController: viewController)
        moduleDict["Image"] = JSBridgeImage(viewController: viewController)
        moduleDict["Navigation"] = JSBridgeNavigation(viewController: viewController)
    }
    
    func getModule(id: String) -> BridgeModule? {
        return moduleDict[id]
    }
}

Copy the code

We removed the BridgeModuleBase class, defined BridgeCall, and modified the parameters of the BridgeModule callFunc method

The design principle here is:

WebView      callNative - \
                          |  -> new BridgeCall -> BridgeModule.callFunc(call)
ReactNative  callNative - /
Copy the code
  • BridgeCall represents a call parameter and return interface that is passed to the specific BrigdeModule
  • BridgeRegistry is the management class for the registry module

And we put this code in jsbridge. swift, as shown:

In WebViewBridge.swift, it looks like this:

// WebViewJSBridgeCall inherits BridgeCall
// WebViewJSBridgeCall defines the communication environment for WebView calls
// * callbackId: WebView unique communication mode
// * webView parameters
EvaluateJavascript is used in callback mode
class WebViewBridgeCall : BridgeCall {
    
    let moduleName: String
    let funcName: String
    let arg: [String:Any? ]let callbackId: String
    weak var webView: WKWebView?
    
    init(moduleName: String, funcName: String, arg: [String:Any? ] , callbackId:String, webView: WKWebView?). {self.moduleName = moduleName
        self.funcName = funcName
        self.arg = arg
        self.callbackId = callbackId
        self.webView = webView
    }
    
    func callback(value: Int) {
        execJS("window.\(callbackId)(\(value))")}func callback(value: Bool) {
        execJS("window.\(callbackId)(\(value))")}func callback(value: String?) {
        if value == nil {
            execJS("window.\(callbackId)(null)")}else {
            execJS("window.\(callbackId)('\(value!)')")}}func callback(json: [String:Any?] ) {
        guard let jsonData = try? JSONSerialization.data(withJSONObject: json, options: []) else {
            return
        }
        guard let jsonString = String(data: jsonData, encoding: .utf8) else {
            return
        }
        execJS("window.\(callbackId)(\(jsonString))")}func execJS(_ script: String) {
        print("WebView execJS: \(script)") webView? .evaluateJavaScript(script) } }// BridgeHandler extends BridgeRegistry
class BridgeHandler : NSObject.WKScriptMessageHandler {
    
    weak var webView: WKWebView?
    weak var viewController: BaseViewController?
    
    let bridgeRegistry = BridgeRegistry(a)override init() {
        super.init()}func initModules(a) {
        bridgeRegistry.initModules(viewController: viewController!)
    }
    
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage)
    {
        // 3. Guard prevents illegal calls and calls after deinit
        guard
            let body = message.body as? [String: Any].let webView = self.webView,
            let viewController = self.viewController,
            let callbackId = body["callbackId"] as? String.let method = body["method"] as? String.let data = body["data"] as? String.let utf8Data = data.data(using: .utf8)
        else {
            return
        }
        print("WebView callNative ok. body is \(body)")
        
        var arg: [String:Any? ] ?do {
            arg = try JSONSerialization.jsonObject(with: utf8Data, options: []) as? [String:Any? ] }catch (let error) {
            print(error)
            return
        }
        
        let split = method.split(separator: ".")
        let moduleName = String(split[0])
        let funcName = String(split[1])
        
        guard let module = bridgeRegistry.getModule(id: moduleName) else {
            return
        }
        / / create WebViewJSBridgeCall
        let call = WebViewBridgeCall(
            moduleName: moduleName,
            funcName: funcName,
            arg: arg ?? [String:Any? ] (), callbackId: callbackId, webView: webView)// callFunc is passed in
        module.callFunc(call)
    }
}
Copy the code

Modify several modules of JSBridgeXXX

Here, JSBridgeCamera is used as an example, and others can follow this method

First, inherit BridgeBaseModule to inherit BridgeModule directly, with only one parameter and type BaseViewController

class JSBridgeCamera : BridgeModule {

    weak var viewController: BaseViewController? .init(viewController: BaseViewController) {
        self.viewController = viewController
    }

    ...
}
Copy the code

CallFunc is changed to a call parameter of type BridgeCall

func callFunc(_ call: BridgeCall) {
    switch call.funcName {
    case "takePicture": takePicture(call)
    case "takeVideo": takeVideo(call)
    default: break}}Copy the code

TakePicture and takeVideo accept the Call parameter instead

private func takePicture(_ call: BridgeCall){{... }private func takeVideo(call: BridgeCall){... }Copy the code

Call. Arg (jsbridgeui.alert)

func alert(_ call: BridgeCall) {
    let arg = call.arg
    ...
}
Copy the code

Also, some methods have threading problems, such as Toast

::: warning Main Thread Checker: UI API called on a background thread: -[UIView init] PID: 31536, TID: 7502538, Thread name: (none), Queue name: com.facebook.react.JSBridgeQueue, QoS: 0 :::

Enter the main thread using DispatchQueu:

func toast(_ call: BridgeCall) {
    let arg = call.arg
    guard let message = arg["message"] as? String else {
        return
    }
    // Go to the main UI thread
    DispatchQueue.main.async {
        self.viewController? .view.makeToast(message) } }Copy the code

There are many other places you might encounter mainthread problems, and I won’t cover them all here

Now that we’re done refactoring, we can run the following to see if there are any errors

ReactNativeBridge

Next we need to provide ReactNative with the ability to call JSBridge

Under React, create a few files:

ReactNativeBridge.m

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(JSBridge.NSObject)

RCT_EXTERN_METHOD(callNative:(NSString *)method argString:(NSString *)argString resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)

@end
Copy the code

ReactNativeBridge.swift

ReactNativeBridge. Swift the ReactNativeBridgeCall that we declare, unlike WebView, we use RCTPromiseResolveBlock

We use @objc(JSBridge) and static func moduleName() to ensure the name of the ReactNativeBridge class is JSBridge because we are using a custom export module

The custom export module needs to modify the ReactViewController code, see the source code below

class ReactNativeBridgeCall : BridgeCall {
    let moduleName: String
    let funcName: String
    let arg: [String:Any? ]let resolve: RCTPromiseResolveBlock
    
    init(moduleName: String, funcName: String, arg: [String:Any? ] , resolve: @escapingRCTPromiseResolveBlock) {
        self.moduleName = moduleName
        self.funcName = funcName
        self.arg = arg
        self.resolve = resolve
    }
    
    func callback(value: Int) {
        resolve(value)
    }
    
    func callback(value: Bool) {
        resolve(value)
    }
    
    func callback(value: String?) {
        resolve(value)
    }
    
    func callback(json: [String:Any?] ) {
        resolve(json)
    }
}

@objc(JSBridge)
class ReactNativeBridge: NSObject.RCTBridgeModule {
    
    static func moduleName(a) -> String! {
        return "JSBridge"
    }
    
    weak var viewController: ReactNativeController?
    
    let bridgeRegistry: BridgeRegistry
    
    init(viewController: ReactNativeController) {
        self.bridgeRegistry = BridgeRegistry(a)super.init(a)self.viewController = viewController
        self.bridgeRegistry.initModules(viewController: viewController)
    }

    @objc(callNative:argString:resolve:reject:)
    func callNative(method: String, argString: String, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        print("JSBridge \(method) \(argString)")
        
        guard
            let viewController = self.viewController,
            let utf8Data = argString.data(using: .utf8)
        else {
            return
        }
        
        var arg: [String:Any? ] ?do {
            arg = try JSONSerialization.jsonObject(with: utf8Data, options: []) as? [String:Any? ] }catch (let error) {
            print(error)
            return
        }
        
        let split = method.split(separator: ".")
        let moduleName = String(split[0])
        let funcName = String(split[1])
        
        guard let module = bridgeRegistry.getModule(id: moduleName) else {
            return
        }
        let call = ReactNativeBridgeCall(
            moduleName: moduleName,
            funcName: funcName,
            arg: arg ?? [String:Any? ] (), resolve: resolve) module.callFunc(call) } }Copy the code

ReactNativeController

Using the custom export module requires us to manually create an RCTBridge and implement an RCTBridgeDelegate, which is implemented directly using ReactNativeController

Implement two methods:

  • SourceURL: Provides the path to the JS code file
  • ExtraModules: Provides custom exported modules for our own creation of the ReactNativeBridge
@objc
class ReactNativeController : BaseViewController {
    
    var jsCodeURL: URL!
    
    init(project: String) {
        super.init(nibName: nil, bundle: nil)
        self.project = project
        let urlStr = WebManager.shared.getWebUrl(id: project, page: "index.bundle")
        self.jsCodeURL = URL(string: urlStr)!
    }
    
    required init? (coder:NSCoder) {
        fatalError("init(coder:) has not been implemented")}override func loadView(a) {
        let bridge = RCTBridge(delegate: self, launchOptions: nil)!
        let rootView = RCTRootView(bridge: bridge, moduleName: project! , initialProperties: [:])self.view = rootView
    }
    
}

extension ReactNativeController : RCTBridgeDelegate {
    func sourceURL(for bridge: RCTBridge!) -> URL! {
        return jsCodeURL
    }
    
    func extraModules(for bridge: RCTBridge!)- > [RCTBridgeModule]! {
        return [
            ReactNativeBridge(viewController: self)]}}Copy the code

This completes the ReactNative call to JSBridge

Try out problem

We tried JSBridge at Todolist by introducing:

import JSBridge from ".. /.. /common/jsbridge-react-native.js";
Copy the code

Add trial code in addTask success is toast once:

addTask = (a)= > {
  let notEmpty = this.state.text.trim().length > 0;

  if (notEmpty) {
    this.setState(
      prevState= > {
        let { tasks, text } = prevState;
        return {
          tasks: tasks.concat({ key: tasks.length, text: text }),
          text: ""
        };
      },
      () => Tasks.save(this.state.tasks)
    );

    JSBridge.UI.toast("Add task success"); }};Copy the code

The jsbridge-react-native. Js file will not be found because ReactNative uses Metro as its packaging tool, which does not support references to code outside the project

Therefore, we will copy jsbridge-react-native. Js and jsbridge-react-native. Js to the project:

import JSBridge from "./jsbridge-react-native.js";
Copy the code

Running effect