Optimize code and UI modules (Android)

Next we need to optimize the code

Divide the code into multiple files

Create the following structure file:

JSBridgeUI code move

Move the jsbridgeui-related code to jsbridgeui.kt.

package com.example.tobebigfe

import android.webkit.WebView
import android.widget.Toast
import com.example.tobebigfe.jsbridge.WebActivity
import org.json.JSONObject


class JSBridgeUI(val activity: WebActivity, val webView: WebView) : BridgeModule {

    override fun callFunc(func: String, arg: JSONObject) {
        when (func) {
            "toast" -> toast(arg)
        }
    }

    private fun toast(arg: JSONObject) {
        val message = arg.getString("message")
        Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
    }
}
Copy the code

Bridge related code movement

And then:

  • BridgeModule
  • BridgeObject

The associated code is moved to webviewbridge.kt

Code minor changes to:

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

// Add parameters
class BridgeObject(val activity: WebActivity, val webView: WebView) {

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

  init {
      bridgeModuleMap["UI"] = JSBridgeUI(activity, webView)
  }

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

      valmodule = bridgeModuleMap[moduleName] module? .callFunc(funcName, jsonArg) } }Copy the code

WebActivity code

abstract class WebActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        WebView.setWebContentsDebuggingEnabled(true)
        webView.settings.javaScriptEnabled = true
        webView.settings.cacheMode = LOAD_NO_CACHE
        webView.webViewClient = WebViewClient()

        // Add our js object before loading the page
        webView.addJavascriptInterface(BridgeObject(this, webView), "androidBridge")

        // Load the web page in assets
        webView.loadUrl(getLoadUrl())
    }

    // Propose an abstract method that lets subclasses implement the loaded URL
    abstract fun getLoadUrl(a): String


}
Copy the code

Implement the UI. Alert

Jsbridge.js is appended

JSBridge.UI.alert = function(params) {
    callNative('UI.alert', params)
}
Copy the code

index.html

<script type="text/javascript">
    function onClickButton(button) {
    switch (button) {
        case "UI.toast":
            JSBridge.UI.toast("This is toast!")
            break
        case "UI.alert":
            JSBridge.UI.alert({
                title: 'notice',
                message: 'This is an Alert',
                button: "Good"
            })
            break}}</script>
<button onclick="onClickButton(this)">UI.toast</button>
<button onclick="onClickButton(this)">UI.alert</button>
Copy the code

JSBridgeUI

override fun callFunc(func: String, arg: JSONObject) {
    when (func) {
        "toast" -> toast(arg)
        "alert" -> alert(arg)
    }
}

private fun alert(arg: JSONObject) {
    activity.runOnUiThread {
        AlertDialog.Builder(activity)
            .setTitle(arg.get("title") asString? ? :"Tip")
            .setMessage(arg.get("message") asString? ? :"")
            .setItems(arrayOf(arg.get("button") asString? ? :"Sure")) { _, _ -> }
            .create()
            .show()
    }
}
Copy the code

Running effect

UI. Confirm and callback

Ui. confirm is a bridge that requires callback to JS.

Native

Native relatively requires some modification of method parameters, step by step.

BridgeModule adds the callbackId parameter because the callback is implemented using:

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

Add a new class BridgeModuleBase to implement callback code:

abstract class BridgeModuleBase(val webView: WebView) : BridgeModule {

    fun callback(callbackId: String, value: Int) {
        execJS("window.$callbackId($value)")}fun callback(callbackId: String, value: Boolean) {
        execJS("window.$callbackId($value)")}fun callback(callbackId: String, value: String?). {
        if (value == null) {
            execJS("window.$callbackId(null)")}else {
            execJS("window.$callbackId('$value')")}}fun callback(callbackId: String, json: JSONObject) {
        execJS("window.$callbackId($json)")}fun execJS(script: String) {
        Log.e("WebView"."exec $script")
        webView.post {
            webView.evaluateJavascript(script, null)}}}Copy the code

Call module.callFunc in BridgeObject

valmodule = bridgeModuleMap[moduleName] module? .callFunc(funcName, callbackId, jsonArg)Copy the code

JSBridgeUI changes:

// 1. Inherit BridgeModuleBase to prepare for completing callback
class JSBridgeUI(val activity: WebActivity, webView: WebView) : BridgeModuleBase(webView) {
    // 2. Add callbackId
    override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
Copy the code

JSBridgeUI implements confirm:


override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
    when (func) {
        "toast" -> toast(arg)
        "alert" -> alert(arg)
        // The callbackId needs to be passed
        "confirm" -> confirm(callbackId, arg)
    }
}

private fun confirm(callbackId: String, arg: JSONObject) {
    val buttons = mutableListOf<String>()
    if (arg.has("buttons")) {
        val buttonArr = arg.getJSONArray("buttons")
        for (i in 0 until buttonArr.length()) {
            buttons.add(buttonArr.getString(i))
        }
    } else {
        buttons.add("Cancel")
        buttons.add("Sure")}val message = arg.get("message") as String?
    
    activity.runOnUiThread {
        AlertDialog.Builder(activity)
            .setTitle(message)
            .setItems(buttons.toTypedArray()) { _, index ->
                // Call the parent class callback to return the selected index
                callback(callbackId, index)
            }
            .create()
            .show()
    }
}
Copy the code

One detail is that we don’t support js passing the title attribute on Android because setMessage invalidates setItems, so the code above uses setTitle to pass the message argument to AlertDialog

Running effect

If you select a button, toast will appear in the index of the button. That is, js will get the selected result of the original return

Optimize code and UI modules (iOS)

Next we need to optimize the code and fix some issues, and there are some ios-related programming issues in the code

Divide the code into multiple files

Under ToBeBigFE/JSBridge, create webViewBridge.swift

Will:

  • BridgeModule
  • BridgeHandler

Shear to WebViewBridge. The swift

Under ToBeBigFE/JSBridge, create jsBridgeui.swift

Cut the code for the JSBridgeUI class to jsbridgeui.swift

After completion, the structure is as follows:

Fix memory leaks and condition judgments

JSBridgeUI. Swift changes:

class JSBridgeUI : BridgeModule {
    
    // 1. Use weak to remove cyclic references
    weak var viewController: WebViewController?
    
    init(viewController: WebViewController) {
        self.viewController = viewController
    }
    
    func callFunc(_funcName: String, arg: [String : Any?] ) {
        switch funcName {
        case "toast":
            toast(arg)
        default: break}}func toast(_arg: [String : Any?] ) {
        // 2. Use guard to prevent message from being null
        guard let message = arg["message"] as? String else {
            return
        }
        // 3. Use question marksviewController? .view.makeToast(message) } }Copy the code

BridgeHandler class changes:

class BridgeHandler : NSObject.WKScriptMessageHandler {
    
    // 1. Use weak to remove cyclic references
    weak var webView: WKWebView?
    // 2. Use weak to remove cyclic references
    weak var viewController: WebViewController?
    var moduleDict = [String:BridgeModule] ()func initModules(a) {
        / / add!
        moduleDict["UI"] = JSBridgeUI(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)")
        
        // 4. Do catch to prevent data parsing errors
        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])
        
        // 5. Guard optimization
        guard let module = moduleDict[moduleName] else {
            return
        }
        // 6. Arg is an empty Dictionary by default
        module.callFunc(funcName, arg: arg ?? [String:Any? ] ()}}Copy the code

Implement the UI. Alert

Jsbridge.js is appended

JSBridge.UI.alert = function(params) {
    callNative('UI.alert', params)
}
Copy the code

index.html

<script type="text/javascript">
    function onClickButton(button) {
    switch (button) {
        case "UI.toast":
            JSBridge.UI.toast("This is toast!")
            break
        case "UI.alert":
            JSBridge.UI.alert({
                title: 'notice',
                message: 'This is an Alert',
                button: "Good"
            })
            break}}</script>
<button onclick="onClickButton(this)">UI.toast</button>
<button onclick="onClickButton(this)">UI.alert</button>
Copy the code

JSBridgeUI

.func callFunc(_funcName: String, arg: [String : Any?] ) {
    switch funcName {
    case "toast":
        toast(arg)
    case "alert":
        alert(arg)
    default: break}}...func alert(_arg: [String : Any?] ) {
    let title = arg["title"] as? String ?? "Tip"
    let message = arg["message"] as? String ?? ""
    let button = arg["button"] as? String ?? "Sure"
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
    let okAction = UIAlertAction(title: button, style: .default, handler: {
        action in}) alertController.addAction(okAction) viewController? .present(alertController, animated:true, completion: nil)}...Copy the code

Implement UI.confirm and callback

Ui. confirm is a bridge that requires callback to JS.

Native

Native relatively requires some modification of method parameters, step by step.

BridgeModule changes:

protocol BridgeModule : class {
    // Add the callbackId parameter that callback will use
    func callFunc(_funcName: String, callbackId: String, arg: [String: Any?] )
}
Copy the code

Add a BridgeModuleBase class that does some callback logic:

class BridgeModuelBase : BridgeModule {
    weak var webView: WKWebView?
    
    func callback(callbackId: String, value: Int) {
        execJS("window.\(callbackId)(\(value))")}func callback(callbackId: String, value: Bool) {
        execJS("window.\(callbackId)(\(value))")}func callback(callbackId: String, value: String?) {
        if value == nil {
            execJS("window.\(callbackId)(null)")}else {
            execJS("window.\(callbackId)('\(value!)')")}}func callback(callbackId: String, 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) }func callFunc(_funcName: String, callbackId: String, arg: [String: Any?] ){}}Copy the code

JSBridgeUI inherits BridgeModuleBase:

class JSBridgeUI : BridgeModuelBase {
Copy the code

BridgeHandler changes:

// Assign webView before callFunc because callback needs it
module.webView = webView
// Add the callbackId parameter
module.callFunc(funcName, callbackId: callbackId, arg: arg ?? [String:Any? ] ())Copy the code

JSBridgeUI changes:

// Add the callbackId parameter
override func callFunc(_funcName: String, callbackId: String, arg: [String : Any?] ) {
    switch funcName {
    case "toast":
        toast(arg)
    case "alert":
        alert(arg)
    case "confirm":
        // Unlike toast and Alert, confirm requires a callbackId
        confirm(callbackId: callbackId, arg)
    default: break}}func confirm(callbackId: String, _arg: [String : Any?] ) {
    let title = arg["title"] as? String ?? "Tip"
    let message = arg["message"] as? String ?? ""
    let buttons = arg["buttons"] as? [String]???? ["Cancel"."Sure"]
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
    
    buttons.forEach { button in
        let action = UIAlertAction(title: button, style: .default) { action in
            // callback Specifies the index of the action selected by the user
            self.callback(callbackId: callbackId, value: buttons.firstIndex(of: button)!) } alertController.addAction(action) } viewController? .present(alertController, animated:true, completion: nil)}Copy the code

Jsbridge.js is appended

JSBridge.UI.confirm = function(params, callback) {
    callNative('UI.confirm', params, callback)
}
Copy the code

index.html

<button onclick="onClickButton(this)">UI.confirm</button>
Copy the code
case "UI.confirm":
    JSBridge.UI.alert({
        title: 'Please confirm'.message: 'Do you know mingo? '.buttons: [
        "Not sure"."Don't know"."Know"
        ]
    }, (button) => {
        JSBridge.UI.toast("Chose:" + button)
    })
    break
Copy the code

Running effect

If you select a button, toast will appear in the index of the button. That is, js will get the selected result of the original return