Swift:用enum处理JS传递给Native侧的事件消息

5,445 阅读5分钟

前言

其实这两年的更新频次相比21年是降低很多的,主要自22年之后,手上的项目就接连不断,被客户项目整的死去活来,当然在目前这个形式下,有活就说明能继续工作,我应该感到欣慰。

其实,就我自己的认知而言,我觉得自己很难写出非常深奥的技术文章,我也一直为此而感到苦恼与不安,毕竟谁都有一个大神梦,不过我可能只能在此停驻吧。

今天给大家分享一例用enum处理JS传递给Native侧的事件消息,算是在工作中总结的一点小实践,如果可以给大家一些启发就好了。

JS事件在Native侧的监听

在App开发过程中,我们会使用到H5页面,而加载H5页面,可能就不可避免需要和Web进行交互。

在iOS侧,我们一般都会使用到WebKit中,WebView相关的API进行设置与交互,其核心代码如下:


let config = WKWebViewConfiguration()

config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name:"showNativeAlert")


let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
config.preferences = preferences


let webView = WKWebView(frame: view.bounds, configuration: config)

在初始化WebView的时候,我们对WebView的配置项里面config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name:"showNativeAlert")我们增加了一个叫"showNativeAlert"的监听事件,也就是说当JS那边调用下面这个方法的时候,我们可以在代理的回调中监听到:

JS侧触发事件

    window.webkit.messageHandlers.showNativeAlert.postMessage('来个原生弹窗');

Native侧监听事件

    extension MyJueJinController: WKScriptMessageHandler {

        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

            print("方法名:\(message.name)")
            print("参数:\(message.body)")

             
            if message.name = "showNativeAlert" {
                print("监听了JS给Nativ的showNativeAlert方法,触发对应的逻辑方法")
            }

        }

    }

同时,我们在Controller析构的时候要记得移除"showNativeAlert"这个句柄:

deinit {

    webView.configuration.userContentController.removeScriptMessageHandler(forName: "showNativeAlert")

}

大家可以看到在整个Native侧,我们一直都在使用"showNativeAlert"这个硬编码字符串,如果我们WebView监听的事件不多,这样写好像也没什么问题,但是如果监听的事件多了,这样写既不优雅,同时持续维护的成本也会特别高。同时,硬编码的字符串也是隐患。

你不得不在func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)写更多的if else if或者switch:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

    print("方法名:\(message.name)")
    print("参数:\(message.body)")


    if message.name = "showNativeAlert" {
        print("监听了JS给Nativ的showNativeAlert方法,触发对应的逻辑方法")
    } else if message.name = "goToNext" {
    
    } else {
    
    }
    
    switch message.name {
    case "showNativeAlert":
        break
    case "goToNext":
        break
    default:
        break

    }
}

难道就没有什么好的办法去处理这些JS传递给Native侧的事件消息的句柄吗?

如果你看过Swift:通过Protocol封装统和入参这篇文章,你一定会有想法的。

这里,我先说结论,通过enum来处理JS传递给Native侧的事件消息的句柄。

你们可能会有疑惑,为什么又又又又是enum,且听我娓娓道来。

使用enum的优势

  • enum可以很好的处理硬编码字符串
enum ScriptMessageHandlerType: String {

    case showNativeAlert

    case goToNext
    
    case helloWorld = "hello_world"

}

enum ScriptMessageHandlerType这种”看似“继承String的方式,实际上是给每个枚举值定义了一个rawValue,并且它直接映射的就是其字符串编码,比如这样打印:

print(ScriptMessageHandlerType.showNativeAlert.rawValue)

"showNativeAlert"

同时我们对于枚举值的rawValue也可以自己进行定义与映射,比如上面代码的case helloWorld = "hello_world",我们对其打印:

print(ScriptMessageHandlerType.helloWorld.rawValue)

"hello_world"

同时我们也可以轻易的将一个匹配的字符串转成枚举值,比如下面:

let type = ScriptMessageHandlerType(rawValue: "showNativeAlert")

type此时是一个可选类型,因为ScriptMessageHandlerType(rawValue: "")如果是一个匹配不上的字符串,那么此时type就nil,在实战运用的时候我们需要注意。

  • enum可以遵守CaseIterable协议,简单轻易的将所有的枚举事件转为数组
extension ScriptMessageHandlerType: CaseIterable {}

看见上面这个理由,你可能会觉得,转成数组和监听JS事件有什么关系?

记得这段代码吗:

/// 添加监听句柄
config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name:"showNativeAlert")

/// 移除监听句柄
webView.configuration.userContentController.removeScriptMessageHandler(forName: "showNativeAlert")

如果我们有更多的句柄需求,这段代码你可能就多写几次了,😂😂😂

不过如果此时我们使用的是enum,那么写起来就不费吹灰之力了:

/// 使用for循序添加句柄
for type in ScriptMessageHandlerType.allCases {

    config.userContentController.add(WeakScriptMessageHandlerDelegate(delegate: self), name: type.rawValue)

}

/// 使用for循序移除句柄
for type in ScriptMessageHandlerType.allCases {

    webView.configuration.userContentController.removeScriptMessageHandler(forName: type.rawValue)

}

是不是这样看起来,非常简洁了呢?

  • enum与switch配合使用,不会遗漏逻辑

最后让我来看看如何处理监听的JS事件,我们重构了func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)里面代码:

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

        print("方法名:\(message.name)")
        print("参数:\(message.body)")

        guard let type = ScriptMessageHandlerType(rawValue: message.name) else {
            return
        }

        switch type {
        case .showNativeAlert:
            break
        case .goToNext:
            break
        case .helloWorld:
            break
        }
    }

因为此时ScriptMessageHandlerType类型是有穷的,所以不必写default这个判断。当你在ScriptMessageHandlerType枚举类型中新增枚举值的时候,如果在switch type中忘记增加新增枚举值的代码判断,会直接编译报错,从而降低因为新增句柄而忘记处理新增句柄逻辑的情况发生。

所以,基于以上几点原因,我用enum处理JS传递给Native侧的事件消息,各位觉得怎么样?

总结

这篇文章,写了这么多,结果到头来,最终还是在说enum的用法。

讲真的,Swift的enum真的非常强大,强大到有的时候我自己在使用的时候也会感叹居然可以这样?

如果持续做Swift开发,大家不妨这样来反推一下自己代码,这里可不可以试试enum?也许一个好点子就来了。

参考文档

Swift:通过Protocol封装统和入参

自己写的项目,欢迎大家star⭐️

RxStudy:RxSwift/RxCocoa框架,MVVM模式编写wanandroid客户端。

GetXStudy:使用GetX,重构了Flutter wanandroid客户端。

附上WeakScriptMessageDelegate的相关代码


    /// 专用WKScriptMessageHandler的代理层

    class WeakScriptMessageDelegate: NSObject {

        //MARK: - 属性设置 之前这个属性没有用weak修饰,所以一直持有,无法释放
        
        private weak var scriptDelegate: WKScriptMessageHandler!

        //MARK: - 初始化

        convenience init(scriptDelegate: WKScriptMessageHandler) {
            self.init()
            self.scriptDelegate = scriptDelegate
        }

    }

    extension WeakScriptMessageDelegate: WKScriptMessageHandler {

        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

            scriptDelegate.userContentController(userContentController, didReceive: message)

        }
    }