本片文章翻譯自raywenderlich
雖然看起來(lái) Swift 和 JavaScript 看起來(lái)有很大的不同,但你可以使用二者創(chuàng)建一個(gè)靈活的 iOSAPP
在這篇 JavaScriptCore 教程中,你將建立一個(gè)網(wǎng)頁(yè)配套的 iOSAPP 復(fù)用存在的 JavaScript 代碼,通過(guò)本教程,你將學(xué)到
- JavaScriptCore framework
- 在
iOS中怎樣調(diào)用JavaScript代碼 - 在
JavaScript中怎樣調(diào)用iOS的代碼
你不必對(duì)JavaScript編程很有經(jīng)驗(yàn),如果這篇教程激起你學(xué)習(xí)JavaScript的興趣,你可以看 Mozilla Developer Network的入門(mén)教程 不想看英文的話,
注 :廖雪峰的博客作為入門(mén)也是一個(gè)不錯(cuò)的選擇,或者買(mǎi)這本書(shū)

0X0 開(kāi)始
點(diǎn)擊這里下載本篇教程所用到的代碼,解壓后,你將得到以下目錄
- Web目錄,包含
HTML和CSS文件 - Native目錄,iOS工程目錄,本篇教程主要在這個(gè)目錄下進(jìn)行
- js目錄,這篇教程所用到的JavaScript代碼
這個(gè) APP 叫做 ShowTime 你可以輸入價(jià)格,從 iTuns 中搜索電影,你可以打開(kāi)在瀏覽器中打開(kāi) Web/index.html,然后輸入價(jià)格,回車(chē)后,你將看到這個(gè)頁(yè)面所呈現(xiàn)的內(nèi)容;

在 iOS 端,打開(kāi)工程,編譯后,你將看到如下界面
你可以看到,在iOS端功能還沒(méi)有就緒,我們將一步一步的完成它,這個(gè)工程已經(jīng)包含了一些代碼,那我們接下來(lái)要怎么做呢。這個(gè) APP 主要提供和網(wǎng)頁(yè)相似的瀏覽體驗(yàn),在 CollectionView 中顯示搜索到的結(jié)果
0X1 JavaScriptCore
JavaScriptCore.framework 提供了訪問(wèn) WebKit 的 JavaScript 引擎 ,通常的來(lái)說(shuō),這個(gè)是在Mac上的C API,但在 iOS7 和 OS X 10.9 上實(shí)現(xiàn)了更好的OC的封裝,這個(gè)框架使得 OC Swift 與 JavaScript 有很強(qiáng)的互通性。
React Native就是JavaScriptCore一個(gè)超級(jí)好的例子, 如果你好奇怎么使用JavaScript來(lái)構(gòu)建Native APP你可以點(diǎn)這里去查看教程。
在這一部分,你將看到里邊的API,以及 JavaScriptCore 的重要組成部分,JSVirtualMachine JSContext JSValue
JSVirtualMachine
JavaScript 代碼在 JSVirtualMachine 所實(shí)現(xiàn)的虛擬機(jī)中執(zhí)行,一般來(lái)說(shuō),你不用直接與這個(gè)類(lèi)打交道,但有一個(gè)重要的使用就是,他不能并發(fā)執(zhí)行,如果想要并發(fā)執(zhí)行的話,就需要多個(gè) JSVirtualMachine。
每一個(gè) JSVirtualMachine 實(shí)例,有自己的堆和垃圾回收器,這也就意味著,你不能在兩個(gè)虛擬你之間傳遞對(duì)象,一個(gè)虛擬機(jī)的垃圾回收器不知道怎么處理其他堆中的值。
JSContext
一個(gè) JSContext 對(duì)象代表了一個(gè)執(zhí)行JavaScript代碼的上下文環(huán)境,它與一個(gè)全局對(duì)象相對(duì)應(yīng),在 web 開(kāi)發(fā)中,相當(dāng)于 window 對(duì)象。不像虛擬機(jī),你可以在兩個(gè)context之間傳遞值(因?yàn)樗麄冊(cè)谕粋€(gè)虛擬機(jī)中)。
JSValue
JSValue 是你主要處理的數(shù)據(jù)類(lèi)型,它能代表所有可能的JavaScript值,一個(gè)實(shí)例 JSValue 被綁定到一個(gè) JSContext 上,任何從 context 來(lái)的值都將是 JSValue 類(lèi)型
這張圖解釋了,JSContext 和 JSVirtualMachine 是怎么協(xié)作的
現(xiàn)在你已經(jīng)理解了一些JavaScriptCore的一些類(lèi)型,是時(shí)候?qū)扅c(diǎn)代碼了

0X2 調(diào)用JavaScript方法
回到 Xcode ,展開(kāi) Data 文件夾,打開(kāi) MovieService.swift ,這個(gè)類(lèi)將請(qǐng)求并處理從 iTunes 返回的數(shù)據(jù),現(xiàn)在,他們大部分是空的,我們的工作就是把這些方法實(shí)現(xiàn)了。
通常,MovieService 的工作流將是這樣的
-
loadMoviesWithLimit(_:onComplete:)取得對(duì)應(yīng)的電影數(shù)據(jù) -
parseResponse(_:withLimit:)將借助于JavaScript代碼來(lái)處理請(qǐng)求回來(lái)的數(shù)據(jù)。
第一步是獲取電影列表,如果你熟悉 JavaScript 編程的話,一般我們使用 XMLHttpRequest 對(duì)象來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求。由于這個(gè)對(duì)象并不是 JavaScript 語(yǔ)言本身的對(duì)象,如果使用了這個(gè)對(duì)象,那我們將無(wú)法再 iOS APP 中的上下文中使用,所以,我們還是要用 native 來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求的
在 MovieService 類(lèi)中,找到 loadMoviesWithLimit(_:onComplete:) 方法,改成下邊這樣
func loadMoviesWithLimit(limit: Double, onComplete complete: [Movie] -> ()) {
guard let url = NSURL(string: movieUrl) else {
print("Invalid url format: \(movieUrl)")
return
}
NSURLSession.sharedSession().dataTaskWithURL(url) { data, _, _ in
guard let data = data,
jsonString = String(data: data, encoding: NSUTF8StringEncoding) else {
print("Error while parsing the response data.")
return
}
let movies = self.parseResponse(jsonString, withLimit:limit)
complete(movies)
}.resume()
}
這一段是用 NSURLSession 來(lái)獲取電影列表,在把網(wǎng)絡(luò)請(qǐng)求的響應(yīng)信息傳遞個(gè) JavaScript 代碼前,你要有一個(gè) JavaScript 可執(zhí)行的上下文,首先,在 MovieService.swift 中加入下邊的代碼來(lái)導(dǎo)入 JavaScriptCore
import JavaScriptCore
然后在 MovieService 中定義如下屬性
lazy var context: JSContext? = {
let context = JSContext()
// 1
guard let
commonJSPath = NSBundle.mainBundle().pathForResource("common", ofType: "js") else {
print("Unable to read resource files.")
return nil
}
// 2
do {
let common = try String(contentsOfFile: commonJSPath, encoding: NSUTF8StringEncoding)
context.evaluateScript(common)
} catch (let error) {
print("Error while processing script file: \(error)")
}
return context
}()
這樣就定義了一個(gè)懶加載的 JSContext 屬性
- 加載了
common.js, 這里邊包含了你想要訪問(wèn)的JavaScript代碼 - 加載 js 之后,
context通過(guò)執(zhí)行context.evaluateScript()來(lái)訪問(wèn)js的內(nèi)容。傳遞的參數(shù)就是js文件的內(nèi)容。
是時(shí)候來(lái)執(zhí)行 JavaScript 方法了,還是在 MovieService.swift 這個(gè)類(lèi)里邊,找到 parseResponse(_:withLimit:) 函數(shù),添加如下代碼
func parseResponse(response: String, withLimit limit: Double) -> [Movie] {
// 1
guard let context = context else {
print("JSContext not found.")
return []
}
// 2
let parseFunction = context.objectForKeyedSubscript("parseJson")
let parsed = parseFunction.callWithArguments([response]).toArray()
// 3
let filterFunction = context.objectForKeyedSubscript("filterByLimit")
let filtered = filterFunction.callWithArguments([parsed, limit]).toArray()
// 4
return []
}
我們來(lái)一步一步的看一下
- 首先,確保
context屬性被正確的初始化,如果在初始化的時(shí)候發(fā)生了錯(cuò)誤,也就沒(méi)有必要在執(zhí)行下去了,比如說(shuō)common.js不在bundle中。 - 你詢問(wèn)
context對(duì)象來(lái)提供一個(gè)parseJson方法,就像先前提到的一樣,查詢的結(jié)果被包含到一個(gè)JSValue對(duì)象中,下邊你將通過(guò)調(diào)用callWithArguments(_:)來(lái)執(zhí)行JavaScript方法,傳遞一個(gè)數(shù)組過(guò)去,最后,再把JSValue對(duì)象轉(zhuǎn)為數(shù)組。 -
filterByLimit()返回那些適合給定價(jià)格的電影列表。 - 現(xiàn)在已經(jīng)獲得了電影列表。但是這兒還缺失了一塊代碼,
filtered持有一個(gè)數(shù)組,我們應(yīng)當(dāng)把他映射為本地的Movie類(lèi)型。
你可能發(fā)現(xiàn)在這里用
objectForKeyedSubscript()有點(diǎn)古怪,很不幸,Swift只能訪問(wèn)這些原始的方法,而不能把他們轉(zhuǎn)為適當(dāng)?shù)哪_本方法。但OC卻可以使用方括號(hào)語(yǔ)法來(lái)來(lái)使用下標(biāo)訪問(wèn)。
暴露 Native 代碼
在 JavaScript 中運(yùn)行 Native 代碼的方法就是定義 block,他們將會(huì)被自動(dòng)橋接到 JavaScript 方法中。 但有個(gè)小問(wèn)題,這種方式只對(duì) OC 有效,對(duì) Swift 的閉包無(wú)效。為了執(zhí)行閉包,你要執(zhí)行以下兩步
- 使用
@convention(block)把Swift閉包轉(zhuǎn)為OC的block。 - 在你映射到
JavaScript方法之前,應(yīng)該轉(zhuǎn)為AnyObject。
在 Movie.swift 添加 下邊的代碼
static let movieBuilder: @convention(block) [[String : String]] -> [Movie] = { object in
return object.map { dict in
guard let
title = dict["title"],
price = dict["price"],
imageUrl = dict["imageUrl"] else {
print("unable to parse Movie objects.")
fatalError()
}
return Movie(title: title, price: price, imageUrl: imageUrl)
}
}
這個(gè)閉包傳遞一個(gè) JavaScript 數(shù)組(用字典代替),并且用它來(lái)構(gòu)建 Movie 實(shí)例。
回到 MovieService.swift 在 parseResponse(_:withLimit:) 中,用一下代碼替換 return 這一段
// 1
let builderBlock = unsafeBitCast(Movie.movieBuilder, AnyObject.self)
// 2
context.setObject(builderBlock, forKeyedSubscript: "movieBuilder")
let builder = context.evaluateScript("movieBuilder")
// 3
guard let unwrappedFiltered = filtered,
let movies = builder.callWithArguments([unwrappedFiltered]).toArray() as? [Movie] else {
print("Error while processing movies.")
return []
}
return movies
- 使用
Swift的unsafeBitCast(_:_:)方法把一個(gè)block轉(zhuǎn)為一個(gè)AnyObject - 調(diào)用
context的setObject(_:forKeyedSubscript:)方法把block加載到JavaScript的運(yùn)行時(shí),然后,使用evaluateScript()得到block在JavaScript中的引用。 - 最后一步是通過(guò)
callWithArguments(_:)執(zhí)行JavaScript中的block,傳一個(gè) JSValue 的數(shù)組作為參數(shù)。返回的參數(shù)將是一個(gè)包含Movie對(duì)象的數(shù)組。
是時(shí)候看看你的代碼的效果了。編譯并運(yùn)行,輸入價(jià)格之后回車(chē),你將看到如下界面。
只有幾行代碼,你就從創(chuàng)建了一個(gè)用 JavaScript 來(lái)解析和過(guò)濾結(jié)果的 Native APP。
使用 JSExport Protocol
在 JavaScript 中使用自定義對(duì)象就是另外一種方式是使用 JSExport Protocol。你只需要?jiǎng)?chuàng)建一個(gè)繼承與 JSExport Protocol 的 Protocol,然后聲明那些你想要暴露給 JavaScript 的方法和屬性。
每一個(gè)想傳輸?shù)?JavaScript 中的 Native 類(lèi),JavaScriptCore 將在適當(dāng)?shù)?JSContext 實(shí)例中創(chuàng)建一個(gè)屬性。這個(gè) framework 默認(rèn)情況下,你的類(lèi)不會(huì)暴露任何屬性和方法給 JavaScript, 你必須選擇性暴露。JSExport 有幾條規(guī)則
- 暴露實(shí)例方法,
JavaScriptCore將創(chuàng)建一個(gè)對(duì)應(yīng)的方法作為原型對(duì)象的屬性。 - 暴露的屬性,將作為原型的訪問(wèn)屬性。
- 對(duì)于類(lèi)方法,
framework將會(huì)創(chuàng)建一個(gè)JavaScript對(duì)象的構(gòu)造函數(shù)。
為了看如何實(shí)際的處理這些,轉(zhuǎn)到 Movie.swift 在現(xiàn)有的類(lèi)中定義新的 protocol
import JavaScriptCore
@objc protocol MovieJSExports: JSExport {
var title: String { get set }
var price: String { get set }
var imageUrl: String { get set }
static func movieWithTitle(title: String, price: String, imageUrl: String) -> Movie
}
在這里,你定義了所有的你想暴露給 JavaScript 的屬性和一個(gè)類(lèi)方法,這個(gè)類(lèi)方法,將用作在 JavaScript 中構(gòu)造 Movie 對(duì)象。后者是重要的,因?yàn)?JavaScriptCore 還沒(méi)有初始化。
是 Movie 遵守 JSExport 用下邊的代碼來(lái)替換整個(gè)類(lèi)
class Movie: NSObject, MovieJSExports {
dynamic var title: String
dynamic var price: String
dynamic var imageUrl: String
init(title: String, price: String, imageUrl: String) {
self.title = title
self.price = price
self.imageUrl = imageUrl
}
class func movieWithTitle(title: String, price: String, imageUrl: String) -> Movie {
return Movie(title: title, price: price, imageUrl: imageUrl)
}
}
這個(gè)類(lèi)方法只是簡(jiǎn)單的調(diào)用了類(lèi)的初始化方法。
現(xiàn)在,你的類(lèi)已經(jīng)準(zhǔn)備好被 JavaScript 調(diào)用了。為了看我們是如何實(shí)現(xiàn)的,打開(kāi)資源文件下的 additions.js,已經(jīng)實(shí)現(xiàn)了如下代碼。
var mapToNative = function(movies) {
return movies.map(function (movie) {
return Movie.movieWithTitlePriceImageUrl(movie.title, movie.price, movie.imageUrl);
});
};
上邊的方法,使用數(shù)組中的每一個(gè)元素來(lái)創(chuàng)建 Movie 實(shí)例。值得注意的一點(diǎn)是,函數(shù)的簽名是怎么改變的。因?yàn)?JavaScript 并沒(méi)有定義參數(shù),這取決于額外的駝峰命名的函數(shù)名。
打開(kāi) MovieService.swift 用以下代碼代替懶加載的 context 屬性。
lazy var context: JSContext? = {
let context = JSContext()
guard let
commonJSPath = NSBundle.mainBundle().pathForResource("common", ofType: "js"),
additionsJSPath = NSBundle.mainBundle().pathForResource("additions", ofType: "js") else {
print("Unable to read resource files.")
return nil
}
do {
let common = try String(contentsOfFile: commonJSPath, encoding: NSUTF8StringEncoding)
let additions = try String(contentsOfFile: additionsJSPath, encoding: NSUTF8StringEncoding)
context.setObject(Movie.self, forKeyedSubscript: "Movie")
context.evaluateScript(common)
context.evaluateScript(additions)
} catch (let error) {
print("Error while processing script file: \(error)")
}
return context
}()
這兒并沒(méi)有什么大的改變。使用 setObject(_:forKeyedSubscript:) 把 additions.js 的內(nèi)容加載到 context 中。也是的 Movie 屬性在 JavaScript 屬性中可用。
只剩下一件事情可以做了,在 MovieService.swift 中,把 parseResponse(_:withLimit:) 的實(shí)現(xiàn)替換為以下代碼
func parseResponse(response: String, withLimit limit: Double) -> [Movie] {
guard let context = context else {
print("JSContext not found.")
return []
}
let parseFunction = context.objectForKeyedSubscript("parseJson")
let parsed = parseFunction.callWithArguments([response]).toArray()
let filterFunction = context.objectForKeyedSubscript("filterByLimit")
let filtered = filterFunction.callWithArguments([parsed, limit]).toArray()
let mapFunction = context.objectForKeyedSubscript("mapToNative")
guard let unwrappedFiltered = filtered,
movies = mapFunction.callWithArguments([unwrappedFiltered]).toArray() as? [Movie] else {
return []
}
return movies
}
與創(chuàng)建閉包相反,現(xiàn)在,試用 JavaScript 的 mapToNative() 方法來(lái)創(chuàng)建 Movie 數(shù)組。如果你編譯運(yùn)行,你會(huì)看到你的 APP 和用它應(yīng)有的樣子是一樣的。

恭喜你,現(xiàn)在已經(jīng)創(chuàng)建了一個(gè)可以瀏覽電影的超棒 APP,并且重用了用不同語(yǔ)言編寫(xiě)的已經(jīng)存在的代碼。

你可以在這里下載本教程完整的代碼。
如果你想學(xué)習(xí)更多的關(guān)于 JavaScriptCore 的內(nèi)容, 請(qǐng)參看 WWDC 2013 Session 615
如有翻譯不足的地方,還望多多指正,謝謝?。。?/p>