iOS 与 JS 交互手册 - JavaScriptCore

3,601 阅读4分钟

在前端快速发展的今天,JavaScript 在移动端的应用也越来越广泛,作为 iOS 开发者来说,与 JavaScript 进行交互也是不大不小的一个方面,尤其是一些 web 内嵌页,UIWebViewWKWebView 对 JavaScript 语言来说就像一个黑盒,出现问题使用 Objective-C 和 Swift 很难进行调试。

我最近就遇到一个这方面的问题,在各种浏览器和 Android 上都没问题,偏偏在 UIWebView 里面有一个 Vue 的组件数据绑定异常,和前端同事搞了一上午,排查过程就不多说了,处理结果就是换了一种 Vue 数据绑定的写法,这个 Bug 的形成原因和最终解决方案对我而言简直就像玄学了,所以本文也不是要讨论这个,当然有了解的朋友希望可以留言给我讲解一下。

这个 Bug 是这样的,一个顶部 Tabs,比如说 5 个,默认打开第一个页面,点击 Tabs 状态刷新实现切换。

第一个页面有一个 Vue 组件,称为 title,正确的值是 138XXX,初始页面正常,切换页面后第 2 -5 的 title 显示的全部是 138XXX+正确的值。

然后切换回 1 也就是初始页面,显示的值成了 138XXX138XXX。

最终解决的办法是将 <label>{{title}}<label> 改为 <label v-model = title><label>,嗯,代码不一定对,大概是这么个意思。

查了一下区别就是单向绑定和双向绑定,但是 title 这个值没有被修改过。

在排查过程中,前端同事不能通过 Chrome 之类的工具调试,我这边也不能随心所欲的拿到 JavaScript 的方法和变量帮他调试,所以只能猜测原因一点一点改内嵌页的代码,非常麻烦。所以我整理了一下 JavaScriptCore 使用方法,水了这篇文章。

经 @lsvih 同学指点,safari > develop > xxx iPhone > inspect 可以很方便的调试 WebView,粗略看了一下,基本和 Chrome 差不多,对前端开发者非常友好,是我孤陋寡闻了,在此感谢!

当然,作为一个有追求的 iOSer,学习和 JavaScript 交互自然不会单单是为了成为前端控制台,所以下面的内容还是有些用处的。

本文涉及讲解的地方不多,注释挺详细的,直接上代码,适合作为手册使用。

属性的交互

// 创建 js 运行环境
let context = JSContext()!

// 捕获运行异常
context.exceptionHandler = { (js, exception) in
    print(exception!.toObject())
}

// 执行 js 代码
let value = context.evaluateScript("2 + 3")!
print(value.toObject()) // 5

// 定义 js 变量
context.evaluateScript("var array = [1, 2 ,3]")

// 获取 js 变量
let array = context.objectForKeyedSubscript("array")!
print(array) // 1,2,3

// 判断 jsvalue 类型
if array.isArray {
    print("array 是数组") // array 是数组
}
else if array.isObject {
    print("array 是对象")
}
else if array.isString {
    print("array 是字符串")
}
else if array.isUndefined {
    print("array 未定义")
}

// 获取变量属性
let arrayLenght = array.objectForKeyedSubscript("length")!
print(arrayLenght.toInt32()) // 3

let arrayLenght2 = array.forProperty("length")
print(arrayLenght.toInt32()) // 3

// 获取 js 数组中的元素
// js 数组存取越界时会自动扩容数组长度,不会崩溃,但同时获取的元素有可能是 undefined 或者是 null
print(array.atIndex(1)) // 2
print(array.objectAtIndexedSubscript(2)) // 3
print(array.objectAtIndexedSubscript(3)) // undefined

// 向 js 数组插入元素
array.setObject("this", atIndexedSubscript: 4) // 1,2,3,,this
array.setValue("js", at: 6) // 1,2,3,,this,,js
array.setObject("is", atIndexedSubscript: 5) // 1,2,3,,this,is,js

// jsvalue 转数组
var arr = array.toArray()!
arr[3] = "swift"

// swift 数组转 jsvalue
let jsArr = JSValue(object: arr, in: context)!

// swift 数组传入 js
context.setObject(jsArr, forKeyedSubscript: "arr" as (NSCopying & NSObjectProtocol)!)
let element = context.evaluateScript("arr[2]")!
print(element) // 3

方法调用和传参

// 创建 js 运行环境
let context = JSContext()!

/************************* swift 调用 js 方法 *****************************/

// 定义 js run 方法
let jsRunCode = """
var run = function (animal) {
  var str = animal + '正在跑'
  console.log(str)
  return str
}
"""

// 在运行环境中插入 js 方法
context.evaluateScript(jsRunCode)

// 获取 run 方法
let runFunc = context.objectForKeyedSubscript("run")!

// swift 中执行 run 方法
let result1 = runFunc.call(withArguments: ["猪"])!
print(result1) //猪正在跑

// js中执行 run 方法
let result2 = context.evaluateScript("run('牛')")!
print(result2) //牛正在跑


/************************* js 调用 swift block *****************************/

// 闭包创建 js 对象
let person = {(name: String, age: Int) -> JSValue in
    let obj = JSValue(newObjectIn: context)!
    obj.setObject(name, forKeyedSubscript: "name" as NSCopying & NSObjectProtocol)
    obj.setValue(age, forProperty: "age")
    return obj
}

// 写入 js 对象
context.setObject(person("max", 18), forKeyedSubscript: "person" as NSCopying & NSObjectProtocol)

// 验证写入成功
let blockCode = """
var growUp = function(person) {
    person.age += 1
    return person
}
var type = typeof person
var result = growUp(person)
"""
context.evaluateScript(blockCode)

let type = context.evaluateScript("type")!
let result = context.evaluateScript("result")!
print(type) // object
print("name = \(result.objectForKeyedSubscript("name")) and age =  \(result.forProperty("age"))" ) // name = Optional(max) and age =  Optional(19)


/************************* js 调用 swift 方法 *****************************/

// 定义 swift 要响应的方法
func swiftResponser(animal: String) -> String {
    return animal + "正在飞"
}

// 定义要执行的 js 方法,及被调起的 swift 方法
let jsFlyCode = """
function fly(animal){
return swiftResponser(animal)
}
"""
context.evaluateScript(jsFlyCode)

// 向 js 传入 swift 方法
let block: @convention(block) (JSValue) -> String = { animal in
    return swiftResponser(animal: animal.toString()!)
}
let funcName = "swiftResponser" as (NSCopying & NSObjectProtocol)!
context.setObject(unsafeBitCast(block, to: AnyObject.self), forKeyedSubscript: funcName)

// 在 js 中执行 swift 方法
let result3 = context.evaluateScript("fly('鸟')")!
print(result3) //鸟正在飞

类的交互

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 创建 js 运行环境
        let context = JSContext()!
        
        let language = Language(name: "Swift", age: 4)
        context.setObject(language, forKeyedSubscript: "language" as NSCopying & NSObjectProtocol);
        context.evaluateScript("language.name = 'JavaScript'")
        context.evaluateScript("language.age = 25")
        context.evaluateScript("language.descriptions()") // // name = JavaScript and age = 25
        
        print(language.name)
        print(language.age)
    }
}

// 实现 JSExport 协议,务必用 @objc 修饰
// 协议中的属性和方法才具有和 js 交互的能力
@objc protocol JSLanguageModelProtocol: JSExport {

    var name: String { get set }
    var age: Int { get set }

    func descriptions()
}

// 声明需要和 js 交互的类
// 必须继承自 NSObject
class Language: NSObject, JSLanguageModelProtocol {

    var name: String
    var age: Int

    init(name: String, age: Int){
        self.name = name
        self.age = age
    }

    func descriptions() {
        print("name = \(self.name) and age = \(self.age)")
    }
}