优化代码和完善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的图了