本篇文章由我們團(tuán)隊(duì)的小馬童鞋翻譯完成,原文地址:From Swift to Javascript and Back
我承認(rèn),我喜歡用 JavaScript 來開發(fā) web 程序。有很多庫是用 JavaScript 來寫的,坦白的說,我認(rèn)為,在其他語言上在實(shí)現(xiàn)一遍的話然爆,非常的2∈蛲迹可能你要猜到了曾雕,我要說的就是,把 JavaScript 庫整合到你的 Swift 代碼中助被,并在 Swift 和 JavaScript 之間實(shí)現(xiàn)數(shù)據(jù)的共享和方法的相互調(diào)用剖张。
為了保持文章不會太長,我將簡明的介紹幾個(gè)基礎(chǔ)元素然后把主要把精力放在示例上揩环。
JSVirtualMachine
JSVirtualMachine 是一個(gè)執(zhí)行 JavaScript 代碼的封閉環(huán)境搔弄。它主要有兩個(gè)目的
- 允許你在獨(dú)立的線程中并行的執(zhí)行代碼
- 允許你清理所分配的 JavaScript 和 OC/Swift 橋接的內(nèi)存
在 JavaScript 中做的任何事情,最終都會在一個(gè) JSVirtualMachine 中執(zhí)行丰滑。當(dāng)你需要創(chuàng)建一個(gè)獨(dú)立的線程時(shí)顾犹,你需要為每一個(gè) 線程單獨(dú)創(chuàng)建一個(gè)JSVirtualMachine,因?yàn)? JavaScriptCore Api 是線程安全的褒墨。直到初始化線程結(jié)束炫刷,你才可以在單獨(dú)的線程中訪問在 JSVirtualMachine 中的代碼,如果你在后臺線程中這么做的話郁妈,那么浑玛,你將無法得到預(yù)期的結(jié)果。
對于內(nèi)存問題還有一個(gè)問題噩咪,當(dāng)你使用完一個(gè)對象時(shí)锄奢,JSVirtualMachine ?提供了釋放這個(gè)對象的機(jī)制。有一個(gè)壞消息就是剧腻,任何時(shí)候,你將一個(gè) OC/Swift 對象?暴露給 JavaScript 并且存儲的時(shí)候涂屁,將會產(chǎn)生一個(gè)循環(huán)引用书在。同樣,你將一個(gè) JavaScript 值存儲為 OC/Swift 對象的時(shí)候拆又,也將產(chǎn)生循環(huán)引用儒旬。循環(huán)引用的產(chǎn)生栏账,是因?yàn)?JSValue 為了在 native 中訪問底層的 JavaScript 值所持有了一個(gè)強(qiáng)引用,同樣的栈源, 一個(gè) context 持有你所傳到 JavaScript 中的 任何 OC/Swift 對象的強(qiáng)引用挡爵。當(dāng)你用你的對象做完某些事情之后,你應(yīng)當(dāng)用 JSVirtualMachine 的 removeManagedReference:withOwner
方法去清理內(nèi)存甚垦。特別的茶鹃,你應(yīng)當(dāng)用 JSManagedValue 在 native 中有條件的存儲 JSValue 。JavaScript 的垃圾回收器將回收那些你移除強(qiáng)引用的對象艰亮。
JSContext
一個(gè) JSContext 對象就是 JavaScript 代碼的運(yùn)行環(huán)境闭翩,這和瀏覽器的 window 對象很相似。在同一個(gè) context 中迄埃,被加到這個(gè) context 中的任何對象疗韵,都會可以被其他的對象無障礙的訪問。實(shí)際上侄非,是沙盒控制范圍內(nèi)的 JavaScript 的變量蕉汪,方法,對象逞怨。你可以執(zhí)行 JavaScript 和 OC/Swift 代碼者疤,訪問 JavaScript 中的值,可以通過 JSContext 把一些值和對象骇钦,從 OC/Swift 傳遞到 JavaScript宛渐。
JSValue
JSValue 是 JavaScript 值的一個(gè)封裝。它像一個(gè)橋梁一樣可以使你在 JavaScript 和 OC/Swift 之間傳遞和共享數(shù)據(jù)眯搭。一個(gè) JSValue 持有一個(gè)在他所屬的 JSContext 的一個(gè)強(qiáng)引用窥翩。作為警告,你應(yīng)該記住鳞仙,如果你想在 OC/Swift 中存儲 JSValue 的時(shí)候寇蚊,將會產(chǎn)生循環(huán)引用。另外棍好,為了訪問底層的 JavaScript 對象仗岸,你可以使用 JSValue 創(chuàng)建一個(gè)被封裝為 OC/Swift 對象的JavaScript 對象。同樣借笙,你也可以創(chuàng)建用 OC/Swift 寫的 JavaScript 方法扒怖。
JSManagedValue
JSManagedValue 是一個(gè) JSValue 附加了一些額外的邏輯,使你在本地存儲一些底層的 JavaScript 對象的時(shí)候不會引起循環(huán)引用业稼。它和 ARC 有點(diǎn)類似盗痒,它的內(nèi)存管理可以在離開作用域的時(shí)候自動釋放對象。JSManagedValue 存在于以下兩種情況下低散。
- 這個(gè)值仍然是底層的 JavaScript 對象俯邓。
- 被用
addManagedReference:withOwner
添加到 JSManagedValue 并且沒有被removeManagedReference:withOwner
方法移除骡楼。
否則的話 JSManagedValue 會被置為 nil ,釋放 JSValue, 在 JavaScript 那邊可以被垃圾回收稽鞭。
JSExport
JSValue 可以把 JavaScript 內(nèi)置的類型轉(zhuǎn)為 OC/Swift 對象鸟整,同樣也可以吧 OC/Swift 對象轉(zhuǎn)為 JavaScript 類型。但是朦蕴,在沒有其他幫助下篮条,JSValue 不能把 OC/Swift 的類轉(zhuǎn)為 JavaScript 對象。JSExport 這個(gè) protocol
提供了一種方式來把 OC/Swift 的實(shí)例方法梦重,類方法兑燥,屬性,轉(zhuǎn)為 JavaScript 對象琴拧。
默認(rèn)使用 JSValue 的時(shí)候降瞳,JavascriptCore 將把 OC/Swift 類轉(zhuǎn)為 JavaScript 對象,但不會暴露其是實(shí)例方法蚓胸,類方法挣饥,屬性。你可以在 JSExport protocol
中定義那些方法想要暴露給 JavaScript
JavaScriptCore 實(shí)戰(zhàn)
我們已經(jīng)介紹了一些 JavaScriptCore 中的類和協(xié)議沛膳,時(shí)候在我們的例子中使用它們了扔枫。
初始化
第一步,我們先創(chuàng)建一個(gè) JSVirtualMachine 和 JSContext 實(shí)例锹安,如果你不打算通過線程來執(zhí)行并發(fā)操作短荐,你可以用一個(gè)空的構(gòu)造函數(shù)來創(chuàng)建一個(gè) JSContext 這樣也會默認(rèn)創(chuàng)建一個(gè) JSVirtualMachine,否則的話叹哭,應(yīng)該創(chuàng)建一個(gè) JSVirtualMachine 實(shí)例忍宋,然后把它作為參數(shù)傳遞到 JSContext 的構(gòu)造函數(shù)中
方法調(diào)用執(zhí)行
有一個(gè)很棒的事情就是你擁有 WebKit
的 JavaScript 引擎,是時(shí)候加強(qiáng)一下了风罩,我喜歡iOS開發(fā)糠排,同樣也花費(fèi)大量的時(shí)間在 Node.js
和 web 開發(fā)。我經(jīng)常使用 JavaScript 庫來減少我代碼量超升,因?yàn)橛泻芏嗪苈斆鞒绦騿T相當(dāng)漂亮的完成了一些很復(fù)雜的工作入宦,現(xiàn)在我們要做的就是在我們的代碼中使用已經(jīng)存在的 JavaScript 庫。
在我們的例子中室琢,我們用 Moment.js
來做一些時(shí)間操作乾闰。我用 Playground
來完成這些。你可以建一個(gè) Playground
來跟著我一起做盈滴⌒谥遥可以使用 Bower 來安裝 Moment.js
,然后拷貝 moment 目錄到工程目錄的資源文件下。如果你有疑問宽菜,可以參考我的另一篇關(guān)于 Playground的文章。
我們接下來所做的竿报,就是找到 moment.js 文件铅乡,把內(nèi)容轉(zhuǎn)為字符串,然后再注入到 JSContext 中 ;

當(dāng)你調(diào)用 JSContext 的 evaluateScript() 方法的時(shí)候烈菌,你的 JavaScript 代碼將會執(zhí)行 阵幸,在我們的例子中,我們把整個(gè) moment.js 庫作為全局對象加載到當(dāng)前的 JSContext 中芽世, 任何后來添加的腳本或者 JavaScript 對象將可以使用 moment.js 的方法挚赊。
到目前為止,這聽起來還不錯济瓢,事實(shí)上是荠割,這取決你注入到 JSContext 中的內(nèi)容,為了使 Moment.js 庫可以工作旺矾,我們應(yīng)該通過調(diào)用 evaluateScript 來調(diào)用它的構(gòu)造方法和格式化方法蔑鹦。一旦我們這么做,我們通過調(diào)用 JSContext 的 objectForKeyedSubscript 將會得到一個(gè) moment.js 的初始化對象箕宙。上邊的例子展示了怎么執(zhí)行一個(gè)方法嚎朽,調(diào)用構(gòu)造函數(shù)。這都是在背后運(yùn)行的柬帕。
例子擴(kuò)展
其實(shí)我想提供一些更深刻的例子哟忍,來讓你來思考在你代碼中使用的這些類的可能性。我們將創(chuàng)建一個(gè)小的 APP 來展示聯(lián)系人陷寝。不像真實(shí)的聯(lián)系人锅很,每次運(yùn)行的時(shí)候,我們將使用JavaScript庫 Faker 來為我們提供聯(lián)系人信息盼铁。在我們的例子中粗蔚,我們將會定義 Swift 模型,構(gòu)建可以在 JavaScript 中執(zhí)行的 Swift 方法饶火。我們也要從 JavaScript 中獲取偽造的聯(lián)系人數(shù)據(jù)鹏控。為了更加直觀,我們將使用 WKWebView 來顯示我們的聯(lián)系人肤寝。
第一步当辐,我們創(chuàng)建一個(gè)聯(lián)系人對象,列出我們想要暴露給 JavaScript 方法的 JSExports protocol 鲤看,我們用 JSContext 的 setObject 方法來確保我們的聯(lián)系人對象可以訪問 JavaScript。

接下來我們創(chuàng)建一個(gè)可以在JavaScript 中執(zhí)行的方法。我們將使用 @convention(block)
語法來把一個(gè) Swift 閉包轉(zhuǎn)為一個(gè) block蹈垢,這個(gè) block 將會變成 JavaScript 的一個(gè)含有同樣參數(shù)和返回值的方法袖裕,我們在此調(diào)用 JSContext 的 setObject 方法 把我們的方法傳遞給 JavaScript 曹抬。但是這次我們需要使用 unsafeBitCast
方法來吧一個(gè) Swift 閉包轉(zhuǎn)為 JavaScript 能正確處理的 AnyObject
類型。

下一步我們將會介紹如何在 JavaScript 中調(diào)用和使用 contact 對象急鳄。你將創(chuàng)建一個(gè) contact.js
然后把它加到 Playground 的資源文件中,我們的腳本將有一個(gè)從 Faker.js
創(chuàng)建聯(lián)系人對象的一個(gè)方法张足。它的目的是用來把一個(gè)新的聯(lián)系人添加到 Playground 的 view 上來顯示,并把這個(gè)對象返回坎藐。

這些代碼創(chuàng)建了一個(gè) JSExports protocol 和一個(gè)我們想要暴露給 JavaScript 的類對象为牍。并且我們把這類添加到了 JSContext 中使得其可以訪問 JavaScript,我們還創(chuàng)建了一個(gè)把聯(lián)系人顯示在界面上的 Swift 方法顺饮。
接下來我們把 Faker.js
加載到我們的代碼中,就像我們加載 Moment.js
一樣吟逝。同樣我們也需要添加 contact.js
赦肋,這個(gè) js 在我們的全局空間內(nèi)添加了一個(gè) createFakeContact()
方法。然后我們在本地獲得并執(zhí)行它佃乘。這有很多來來回回的調(diào)用發(fā)生在 JavaScript 與 Swift 中,這正是我想要說明的趣避。 你可以很容易的在二者之前來回的調(diào)用和傳遞程帕。


這個(gè)文件在 ContactDrawable 類中用 WKWebView 來加載 page.html 文件

為了使我們的聯(lián)系人顯示,我創(chuàng)建了一個(gè) UIView 并把它賦值給 XCPlaygroundPage.currentPage.liveView讲逛, Playground 展示了隨機(jī)創(chuàng)建的假的聯(lián)系人數(shù)據(jù)岭埠。

總結(jié)
Playground 展示了一個(gè)每次運(yùn)行都會改變的聯(lián)系人詳情列表蔚鸥。在多想一點(diǎn)许赃,我們可以適配一個(gè)用 JavaScript 創(chuàng)建并控制的 native UI,或者從服務(wù)段下載一段腳本來執(zhí)行本地的任務(wù)启盛。