Swift和Javascript的神奇魔法

前言

今天在網上看到了一篇介紹Swift和Javascript交互的文章,感覺作者寫的很好,因此把作者文章中的主要知識點進行一個總結。

對於我個人而言,在項目中使用Javascript的原因有兩個:

  • 某些任務,很可能已經有現成的Javascript庫存在了,使用起來比原生實現更簡單

  • 在架構上的考慮

可以再這裏下載演示demo

demo中我們主要演示了3大塊Swift和Javascript交互的神奇魔法:

  • 在Swift中獲取和使用Javascript的屬性和函數,處理Javascript的異常,在Javascript中獲取和使用Swift的屬性和函數

  • 使用Javascript第三方庫Snowdown把Markdown文本轉換成HTML文本

  • 使用Javascript解析複雜的數據,然後用Swift展示

效果圖:

Model,Initial OS,Latest OS,Image URLiPhone (1st Generation),iPhone OS 1.0,iPhone OS 3.1.3,https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/IPhone_2G_PSD_Mock.png/81px-IPhone_2G_PSD_Mock.pngiPhone 3G,iPhone OS 2.0,iOS 4.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/IPhone_PSD_White_3G.png/81px-IPhone_PSD_White_3G.pngiPhone 3GS,iPhone OS 3.0,iOS 6.1.6,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/IPhone_PSD_White_3G.png/81px-IPhone_PSD_White_3G.pngiPhone 4,iOS 4.0,iOS 7.1.2,https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/IPhone_4_Mock_No_Shadow_PSD.png/81px-IPhone_4_Mock_No_Shadow_PSD.pngiPhone 4S,iOS 5.0,iOS 9.3.5,https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/IPhone_4S_No_shadow.png/99px-IPhone_4S_No_shadow.pngiPhone 5,iOS 6.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/IPhone_5.png/99px-IPhone_5.pngiPhone 5C,iOS 7.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/IPhone_5C_%28blue%29.svg/88px-IPhone_5C_%28blue%29.svg.pngiPhone 5S,iOS 7.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/IPhone_5s.png/88px-IPhone_5s.pngiPhone 6,iOS 8.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/IPhone6_silver_frontface.png/100px-IPhone6_silver_frontface.pngiPhone 6 Plus,iOS 8.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/IPhone_6_Plus_Space_Gray.svg/120px-IPhone_6_Plus_Space_Gray.svg.pngiPhone 6S,iOS 9.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/IPhone_6S_Rose_Gold.png/105px-IPhone_6S_Rose_Gold.pngiPhone 6S Plus,iOS 9.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/IPhone_6S_Rose_Gold.png/125px-IPhone_6S_Rose_Gold.pngiPhone SE,iOS 9.3,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/en/thumb/d/d0/IPhone_SE_%28rose_gold%29.png/95px-IPhone_SE_%28rose_gold%29.pngiPhone 7,iOS 10.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/IPhone_7_Jet_Black.svg/105px-IPhone_7_Jet_Black.svg.pngiPhone 7 Plus,iOS 10.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/IPhone_7_Plus_Jet_Black.svg/125px-IPhone_7_Plus_Jet_Black.svg.png

把上邊的數據解析後,展示爲:

Swift,Javascript的基本交互

JavaScriptCore 中最主要的角色就是 JSContext 類。一個 JSContext 對象是位於 JavaScript 環境和本地 Javascript 腳本之間的橋樑。

因此需要初始化一個JSContext對象:

var jsContext: JSContext!

我不會像原文那樣一步一步的演示功能,我只是記錄下使用JSContext的核心思想和用法。

我們看看JSContext的初始化方法:

func initializeJS() {        self.jsContext = JSContext()        
        /// Catch exception
        self.jsContext.exceptionHandler = { context, exception in
            if let ex = exception {                print("JS exception: " + ex.toString())
            }
        }        
        let jsPath = Bundle.main.path(forResource: "jssource", ofType: "js")        if let path = jsPath {            do {                let jsSourceContents = try String(contentsOfFile: path)
                jsContext.evaluateScript(jsSourceContents)
            } catch let ex {                print(ex.localizedDescription)
            }
        }        
        // Configurate log
        let consoleLogObject = unsafeBitCast(self.consoleLog, to: AnyObject.self)
        jsContext.setObject(consoleLogObject, forKeyedSubscript: "consoleLog" as (NSCopying & NSObjectProtocol))
        jsContext.evaluateScript("consoleLog")
    }

上邊的代碼中做了下邊這幾件事:

  • 使用JSContext()初始化JSContext對象

  • JSContext中有一個屬性exceptionHandler用來監聽Javascript的錯誤。這個屬性很有用,我們使用這個屬性來發現Javascript的錯誤

  • JSContext的evaluateScript方法可以把數據調入到JavaScriptCore的運行時環境中。該方法需要傳遞的參數是Javascript代碼。返回值爲Javascript代碼中的最後一個JSValue。

  • let consoleLogObject = unsafeBitCast(self.consoleLog, to: AnyObject.self) unsafeBitCast用作強制類型轉換,使用的時候需要明確的知道要轉換的類型

  • open func setObject(_ object: Any!, forKeyedSubscript key: (NSCopying & NSObjectProtocol)!)通過這種方式爲Javascript添加屬性或者函數

那麼,接下來,我們看一段Swift中獲取Javascript屬性的代碼:

func helloWorld() {        if let valiableHW = jsContext.objectForKeyedSubscript("helloWorld") {            print(valiableHW.toString())
        }
    }

由上邊的代碼可以看出,通過函數open func objectForKeyedSubscript(_ key: Any!) -> JSValue!可以獲取JSValue,然後使用toString()獲取字符串。

除了獲取屬性外,下邊的代碼演示瞭如何使用Javascript中的函數:

 func jsDemo1() {        let firstName = "zhang"
        let lastName = "san"
        if let funcFullName = jsContext.objectForKeyedSubscript("getFullName") {            if let fullName = funcFullName.call(withArguments: [firstName, lastName]) {                print(fullName)
            }
        }
    }

通過函數open func objectForKeyedSubscript(_ key: Any!) -> JSValue!可以獲取JSValue,然後調用call函數,並傳遞參數過去就實現了這個功能。

我們在看看js代碼中是如何使用Swift屬性和函數的:

function generateLuckyNumbers() {
    
    consoleLog("打印東東啊");    
    var luckyNumbers = [];    while (luckyNumbers.length != 6) {        var randomNumber = Math.floor((Math.random() * 50) + 1);        if (!luckyNumbers.includes(randomNumber)) {
            luckyNumbers.push(randomNumber);
        }
    }
    
    handleLuckyNumbers(luckyNumbers);
}

上邊代碼中的handleLuckyNumbers函數就是Swift中的函數,大家可以去demo中查看。

Markdown文本轉換成HTML文本

這個文本轉換最核心的內容就是解析Markdown的語法,然後輸出HTML文本,如果我們自己手寫轉換代碼,那就太麻煩了。Javascript已經有一個很強大的第三方庫Snowdown。

在JSContext的初始化方法中添加下邊的代碼:

// Fetch and evaluate the Snowdown script.let snowdownScript = try String(contentsOf: URL(string: "https://cdn.rawgit.com/showdownjs/showdown/1.6.3/dist/showdown.min.js")!)self.jsContext.evaluateScript(snowdownScript)

上邊的代碼中把轉換腳本調入Javascript運行時,然後我們再通過下邊的代碼調用Javascript的代碼:

func convertMarkdownToHTML() {        if let funcConvertMarkdownToHTML = jsContext.objectForKeyedSubscript("convertMarkdownToHTML") {
            funcConvertMarkdownToHTML.call(withArguments: [self.tvEditor.text])
        }
    }

Javascript的代碼如下:

function convertMarkdownToHTML(source) {    var converter = new showdown.Converter();    var htmlResult = converter.makeHtml(source);
    consoleLog(htmlResult);
}

核心思想就是接受Javascript轉換後的結果。

自定義類和JavaScript

前面,我們學習瞭如何暴露 Swift 程序代碼給 JS,但 JavaScriptCore 的功能並不僅限於此。它還提供一種暴露自定義類的機制,並直接在 JS 中使用這些類的屬性和函式。這就是 JSExport,它是一個協議,通過它你能夠以更強大的方式來溝通 Swift 和 JS。

我們看看自定義類的代碼:

import UIKitimport JavaScriptCore@objc protocol DeviceInfoJSExport: JSExport {    var model: String! { get set}    var initialOS: String! { get set}    var latestOS: String! { get set}    var imageURL: String! { get set}    
    static func initializeDevice(withModel: String) -> DeviceInfo}class DeviceInfo: NSObject, DeviceInfoJSExport {    var model: String!    var initialOS: String!    var latestOS: String!    var imageURL: String!    
    init(withModel model: String) {        super.init()        
        self.model = model
    }    
    class func initializeDevice(withModel: String) -> DeviceInfo {        return DeviceInfo(withModel: withModel)
    }    
    func concatOS() -> String {        if let initial = initialOS {            if let latest = latestOS {                return initial + "-" + latest
            }
        }        return ""
    }
}

如果我們實現了JSExport協議,那麼 JavaScript 運行時就能捕獲該協議中的內容。對於這種設計,可以讓我們很靈活的使用它的功能。

再看看Javascript中關於這一段的核心代碼:

function parseiPhoneList(originalData) {    var results = Papa.parse(originalData, { header: true });    if (results.data) {        var deviceData = [];        
        for (var i=0; i < results.data.length; i++) {            var model = results.data[i]["Model"];            
            var deviceInfo = DeviceInfo.initializeDeviceWithModel(model);
            
            deviceInfo.initialOS = results.data[i]["Initial OS"];
            deviceInfo.latestOS = results.data[i]["Latest OS"];
            deviceInfo.imageURL = results.data[i]["Image URL"];
            
            deviceData.push(deviceInfo);
        }        
        return deviceData;
    }    
    return null;
}

上邊的代碼,調用了第三方解析庫的函數,把數據解析出來後,生成deviceInfo數組,然後我們在Swift中就獲取到了解析好的數據:

func parseDeviceData() {        if let path = Bundle.main.path(forResource: "iPhone_List", ofType: "csv") {            do {                let contents = try String(contentsOfFile: path)                
                if let functionParseiPhoneList = self.jsContext.objectForKeyedSubscript("parseiPhoneList") {                    if let parsedDeviceData = functionParseiPhoneList.call(withArguments: [contents]).toArray() as? [DeviceInfo] {                        self.deviceInfo = parsedDeviceData                        self.tblDeviceList.reloadData()
                    }
                }
                
            }            catch {                print(error.localizedDescription)
            }
        }
    }

實現這些功能的基礎就是Javascript的函數有返回值。

總結


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章