《成为大前端》系列 5.2 JSBridge模块化 - 优化代码和完善UI模块

658 阅读5分钟

优化代码和完善UI模块(Android)

接下来我们需要优化一下代码

将代码分成多个文件

创建如下结构文件:

JSBridgeUI代码移动

JSBridgeUI相关代码移动到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()
    }
}

Bridge相关代码移动

然后将:

  • BridgeModule
  • BridgeObject

相关代码移动到到WebViewBridge.kt

代码小修改为:

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

// 增加参数
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]

      val module = bridgeModuleMap[moduleName]
      module?.callFunc(funcName, jsonArg)
  }
}

WebActivity代码

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()

        // 在加载网页前添加我们的js对象
        webView.addJavascriptInterface(BridgeObject(this, webView), "androidBridge")

        // 加载assets中的网页
        webView.loadUrl(getLoadUrl())
    }

    // 提出一个抽象方法,让子类实现加载的url
    abstract fun getLoadUrl(): String


}

实现UI.alert

jsbridge.js后面加上

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

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: '通知',
                message: '这是一条Alert',
                button: "好"
            })
            break
    }
    }
</script>
<button onclick="onClickButton(this)">UI.toast</button>
<button onclick="onClickButton(this)">UI.alert</button>

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") as String? ?: "提示")
            .setMessage(arg.get("message") as String? ?: "")
            .setItems(arrayOf(arg.get("button") as String? ?: "确定")) { _, _ -> }
            .create()
            .show()
    }
}

运行效果

UI.confirm和callback

UI.confirm是一个需要callback给js的bridge,下面是实现过程:

Native

Native相对来说要改动一些方法参数,一步步来。

BridgeModule增加callbackId参数,因为实现callback需要用到:

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

新增加一个类BridgeModuleBase实现callback的代码:

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)
        }
    }

}

BridgeObject里调用module.callFunc修改:

val module = bridgeModuleMap[moduleName]
module?.callFunc(funcName, callbackId, jsonArg)

JSBridgeUI修改:

// 1. 继承BridgeModuleBase,为完成callback做准备
class JSBridgeUI(val activity: WebActivity, webView: WebView) : BridgeModuleBase(webView) {
    // 2. 增加callbackId参数
    override fun callFunc(func: String, callbackId: String, arg: JSONObject) {

JSBridgeUI的confirm实现:


override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
    when (func) {
        "toast" -> toast(arg)
        "alert" -> alert(arg)
        // 需要传递callbackId
        "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("取消")
        buttons.add("确定")
    }

    val message = arg.get("message") as String?
    
    activity.runOnUiThread {
        AlertDialog.Builder(activity)
            .setTitle(message)
            .setItems(buttons.toTypedArray()) { _, index ->
                // 调用父类callback,返回选中的index
                callback(callbackId, index)
            }
            .create()
            .show()
    }
}

有个细节是,android版我们不支持js传递title属性,因为 setMessage会使setItems会无效,因此,上面代码使用setTitle 来传递message参数给AlertDialog

运行效果

选中一个按钮会有toast出来button的index,即js得到了原生返回的选中结果,这里就不截toast的图了

优化代码和完善UI模块(iOS)

接下来我们需要优化一下代码,修复一些问题,代码还存在一些iOS相关的编程问题

将代码分成多个文件

ToBeBigFE/JSBridge下,新建WebViewBridge.swift

将:

  • BridgeModule
  • BridgeHandler

剪切到WebViewBridge.swift

ToBeBigFE/JSBridge下,新建JSBridgeUI.swift

JSBridgeUI类的代码剪切到JSBridgeUI.swift

完成后,结构如下:

修复内存泄漏和条件判断

JSBridgeUI.swift修改:

class JSBridgeUI : BridgeModule {
    
    // 1. 使用weak解除循环引用问题
    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. 使用guard,防止js传过来的message是空指针
        guard let message = arg["message"] as? String else {
            return
        }
        // 3. 使用问号
        viewController?.view.makeToast(message)
    }
}

BridgeHandler类修改:

class BridgeHandler : NSObject, WKScriptMessageHandler {
    
    // 1. 使用weak解除循环引用问题
    weak var webView: WKWebView?
    // 2. 使用weak解除循环引用问题
    weak var viewController: WebViewController?
    var moduleDict = [String:BridgeModule]()
    
    func initModules() {
        // 加!
        moduleDict["UI"] = JSBridgeUI(viewController: viewController!)
    }
    
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage)
    {
        // 3. guard防止不合法调用和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,防止解析data出错
        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 优化
        guard let module = moduleDict[moduleName] else {
            return
        }
        // 6. 默认arg为空Dictionary
        module.callFunc(funcName, arg: arg ?? [String:Any?]())
    }
}

实现UI.alert

jsbridge.js后面加上

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

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: '通知',
                message: '这是一条Alert',
                button: "好"
            })
            break
    }
    }
</script>
<button onclick="onClickButton(this)">UI.toast</button>
<button onclick="onClickButton(this)">UI.alert</button>

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 ?? "提示"
    let message = arg["message"] as? String ?? ""
    let button = arg["button"] as? String ?? "确定"
    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)
}
...

实现UI.confirm和callback

UI.confirm是一个需要callback给js的bridge,下面是实现过程:

Native

Native相对来说要改动一些方法参数,一步步来。

BridgeModule改动:

protocol BridgeModule : class {
    // 新增callbackId参数,callback时会使用到
    func callFunc(_ funcName: String, callbackId: String, arg: [String: Any?])
}

增加一个类BridgeModuleBase,这个类完成一些callback逻辑:

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?]) {}
}

JSBridgeUI改为继承BridgeModuleBase:

class JSBridgeUI : BridgeModuelBase {

BridgeHandler的改动:

// 在callFunc之前赋值webView,因为callback需要用到
module.webView = webView
// 增加callbackId参数
module.callFunc(funcName, callbackId: callbackId, arg: arg ?? [String:Any?]())

JSBridgeUI改动:

// 增加callbackId参数
override func callFunc(_ funcName: String, callbackId: String, arg: [String : Any?]) {
    switch funcName {
    case "toast":
        toast(arg)
    case "alert":
        alert(arg)
    case "confirm":
        // 和toast、alert不同的是confirm需要传递callbackId
        confirm(callbackId: callbackId, arg)
    default: break
    }
}

func confirm(callbackId: String, _ arg: [String : Any?]) {
    let title = arg["title"] as? String ?? "提示"
    let message = arg["message"] as? String ?? ""
    let buttons = arg["buttons"] as? [String] ?? ["取消", "确定"]
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
    
    buttons.forEach { button in
        let action = UIAlertAction(title: button, style: .default) { action in
            // callback用户选中的action的index
            self.callback(callbackId: callbackId, value: buttons.firstIndex(of: button)!)
        }
        alertController.addAction(action)
    }
    
    viewController?.present(alertController, animated: true, completion: nil)
}

jsbridge.js后面加上

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

index.html

<button onclick="onClickButton(this)">UI.confirm</button>
case "UI.confirm":
    JSBridge.UI.alert({
        title: '请确认',
        message: '你认识mingo吗?',
        buttons: [
        "不确定",
        "不认识",
        "认识"
        ]
    }, (button) => {
        JSBridge.UI.toast("选择了:" + button)
    })
    break

运行效果

选中一个按钮会有toast出来button的index,即js得到了原生返回的选中结果,这里就不截toast的图了