前言
公司以前已经对 WKWebView 进行了封装,但当时的需求很少,主要是展示网页和右上角分享等几个功能。当时主要是通过拦截 url的方式实现。
但现在的功能需求增多,比如选择图片,上次传图片,发布文章等。。。
一般我们用一个类比如 WebBrowerController 开打开所有的网页,本地的或服务器。 这里我用 swift 写代码。 详细代码见 用代码一步一步实现自己的 ios 架构 Hybird 目录
以前的方式缺点
- 不好扩展新功能
- URL 解析复杂,编码易出错
本文侧重封装,解决上面的缺点,所以不详细介绍 js ios 互调的知识点,网络上已经很多。我会以后在代码里将互调的功能补充。我使用Wkwebview 新的JS、 iOS互调的方式,和 新的架构依据 开闭原则 代码容易扩展。
目标
封装一个易于维护和扩展的 Wkwebview。
不易扩展的要代码
//注册 js
web_.configuration.userContentController.add(self, name: "func1")
web_.configuration.userContentController.add(self, name: "func2")
web_.configuration.userContentController.add(self, name: "func3")
// js 回调处理
if message.name == func1 {
} else if message.name == func2 {
} else if message.name == func3
...
...
...
这样当功能增多时,这个文件变大 功能混乱, 并且要不停修改无法封装成底层库。
分析
分析耦合,主要是 注册JS和处理JS,将她们分离出去即可。
我们可以给 WebBrowerController 添加代理,由外部不同的代理去实现,不同的代理实现不同方法,由服务器告知需要调用哪些代理
服务器告知需要哪些代理
两种方案:
- 解析url
在 调用 WebBrowerController 时 在url 中约定 详细代码见具体代码。
主要代码:
init(url: String) {
super.init(nibName:nil, bundle:nil)
guard let components = URLComponents(string: url) else {
assertionFailure("malformed url: \(url)")
return
}
self.url = URL(string: url)
// DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
// self.config(self.parseConfig(components.queryItems))
// }
config(parseConfig(components.queryItems))
}
func parseConfig(_ queryPairs: [URLQueryItem]?) -> Dictionary<String,Array<String>>? {
var dic: Dictionary<String,Array<String>>? = nil
_ = queryPairs?.map {
if $0.name == webconfig, let value = $0.value {
if let data = Data(base64Encoded: value) {
dic = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? Dictionary<String,Array<String>>
}
}
}
return dic
}
- url 的方式请按照标准的url规则处理url。不然很容易解析失败!(url encode 、base64 编码等)
- WkWebview js 注册
主要代码:
lazy var web: WKWebView = {
let web_ = WKWebView()
web_.configuration.userContentController.add(self, name: webconfig) //url注册和这个注册 二选一
return web_
}()
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == webconfig {
config(message.body as? Dictionary<String,Array<String>>)
}
}
ios 调用对应的 代理
主要代码
func config(_ serverDic: Dictionary<String,Array<String>>?) {
if let modules = serverDic?["module"] {
_ = modules.map { module in
let namespace = Bundle.main.infoDictionary!["CFBundleExecutable"]as! String
let clsName = namespace + "." + module
let cls = NSClassFromString(clsName) as! FeakModule.Type
let instance = cls.init()
instance.web = web
instance.configWebView(web.configuration)
}
}
}
这里需要说明一下:let cls = NSClassFromString(clsName) as! FeakModule.Type
我发现 swift 只能根据类名反射到类,无法根据 协议名 反射到 遵守协议的 类。 这里我使用了一个 FeakModule
其实相对于一个 抽象基类。
protocol WebBrowerDelegate {
func configWebView(_ config: WKWebViewConfiguration);
}
class FeakModule: UIViewController, WebBrowerDelegate {
var web: WKWebView!
func configWebView(_ config: WKWebViewConfiguration) {
}
}
再定义具体的代理子类如:
class Module1: FeakModule, WKScriptMessageHandler
class Module2: FeakModule, WKScriptMessageHandler
这样 就可以编译成功,当运行时,会生成具体的 Module1 或者 Module2
具体的子类 去做 js 注册 和 处理回调
class Module2: FeakModule, WKScriptMessageHandler {
override func configWebView(_ config: WKWebViewConfiguration) {
print("module2 config webview")
config.userContentController.add(self, name: "module2_func1")
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "module2_func1" {
print("js call ios module2_func1")
}
}
}
每增加一个新需求,就新建一个 子类。 代码清晰简洁了!
结果
如此处理后,WebBrowerController 就说一个基础组件,可以在里面做一些基础的公共的工作。以后不管增加什么功能,WebBrowerController 都不需要改变。做到了 开闭原则
网友评论