【譯】JavaScriptCore Tutorial for iOS: Getting Started

本片文章翻譯自raywenderlich

雖然看起來(lái) SwiftJavaScript 看起來(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ū)

這本書(shū)

0X0 開(kāi)始

點(diǎn)擊這里下載本篇教程所用到的代碼,解壓后,你將得到以下目錄

  • Web目錄,包含 HTMLCSS 文件
  • 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,但在 iOS7OS X 10.9 上實(shí)現(xiàn)了更好的OC的封裝,這個(gè)框架使得 OC SwiftJavaScript 有很強(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)代碼了

Enough theory, let’s get to work!

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 屬性

  1. 加載了 common.js, 這里邊包含了你想要訪問(wèn)的 JavaScript 代碼
  2. 加載 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)一步一步的看一下

  1. 首先,確保 context 屬性被正確的初始化,如果在初始化的時(shí)候發(fā)生了錯(cuò)誤,也就沒(méi)有必要在執(zhí)行下去了,比如說(shuō) common.js 不在 bundle 中。
  2. 你詢問(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ù)組。
  3. filterByLimit() 返回那些適合給定價(jià)格的電影列表。
  4. 現(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í)行以下兩步

  1. 使用 @convention(block)Swift 閉包轉(zhuǎn)為 OCblock。
  2. 在你映射到 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.swiftparseResponse(_: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
  1. 使用 SwiftunsafeBitCast(_:_:) 方法把一個(gè) block 轉(zhuǎn)為一個(gè) AnyObject
  2. 調(diào)用 contextsetObject(_:forKeyedSubscript:) 方法把 block 加載到 JavaScript 的運(yùn)行時(shí),然后,使用 evaluateScript() 得到 blockJavaScript 中的引用。
  3. 最后一步是通過(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 ProtocolProtocol,然后聲明那些你想要暴露給 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ī)則

  1. 暴露實(shí)例方法, JavaScriptCore 將創(chuàng)建一個(gè)對(duì)應(yīng)的方法作為原型對(duì)象的屬性。
  2. 暴露的屬性,將作為原型的訪問(wèn)屬性。
  3. 對(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)在,試用 JavaScriptmapToNative() 方法來(lái)創(chuàng)建 Movie 數(shù)組。如果你編譯運(yùn)行,你會(huì)看到你的 APP 和用它應(yīng)有的樣子是一樣的。

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

這就是無(wú)縫用戶體驗(yàn)

你可以在這里下載本教程完整的代碼。

如果你想學(xué)習(xí)更多的關(guān)于 JavaScriptCore 的內(nèi)容, 請(qǐng)參看 WWDC 2013 Session 615

如有翻譯不足的地方,還望多多指正,謝謝?。。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容