iOS 與 JS 交互手冊 - JavaScriptCore

在前端快速發展的今天,JavaScript 在移動端的應用也愈來愈普遍,做爲 iOS 開發者來講,與 JavaScript 進行交互也是不大不小的一個方面,尤爲是一些 web 內嵌頁,UIWebViewWKWebView 對 JavaScript 語言來講就像一個黑盒,出現問題使用 Objective-C 和 Swift 很難進行調試。前端

我最近就遇到一個這方面的問題,在各類瀏覽器和 Android 上都沒問題,恰恰在 UIWebView 裏面有一個 Vue 的組件數據綁定異常,和前端同事搞了一上午,排查過程就很少說了,處理結果就是換了一種 Vue 數據綁定的寫法,這個 Bug 的造成緣由和最終解決方案對我而言簡直就像玄學了,因此本文也不是要討論這個,固然有了解的朋友但願能夠留言給我講解一下。web

這個 Bug 是這樣的,一個頂部 Tabs,好比說 5 個,默認打開第一個頁面,點擊 Tabs 狀態刷新實現切換。swift

第一個頁面有一個 Vue 組件,稱爲 title,正確的值是 138XXX,初始頁面正常,切換頁面後第 2 -5 的 title 顯示的所有是 138XXX+正確的值。數組

而後切換回 1 也就是初始頁面,顯示的值成了 138XXX138XXX。瀏覽器

最終解決的辦法是將 <label>{{title}}<label> 改成 <label v-model = title><label>,嗯,代碼不必定對,大概是這麼個意思。閉包

查了一下區別就是單向綁定和雙向綁定,可是 title 這個值沒有被修改過。ide

在排查過程當中,前端同事不能經過 Chrome 之類的工具調試,我這邊也不能爲所欲爲的拿到 JavaScript 的方法和變量幫他調試,因此只能猜想緣由一點一點改內嵌頁的代碼,很是麻煩。因此我整理了一下 JavaScriptCore 使用方法,水了這篇文章。工具

經 @lsvih 同窗指點,safari > develop > xxx iPhone > inspect 能夠很方便的調試 WebView,粗略看了一下,基本和 Chrome 差很少,對前端開發者很是友好,是我孤陋寡聞了,在此感謝!學習

固然,做爲一個有追求的 iOSer,學習和 JavaScript 交互天然不會單單是爲了成爲前端控制檯,因此下面的內容仍是有些用處的。ui

本文涉及講解的地方很少,註釋挺詳細的,直接上代碼,適合做爲手冊使用。

屬性的交互

// 建立 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)")
    }
}
複製代碼
相關文章
相關標籤/搜索