【译】JavaScriptCore Tutorial for iOS: Getting Started

本片文章翻译自raywenderlich

虽然看起来 SwiftJavaScript 看起来有很大的不同,但你可以使用二者创建一个灵活的 iOSAPP
在这篇 JavaScriptCore 教程中,你将建立一个网页配套的 iOSAPP 复用存在的 JavaScript 代码,通过本教程,你将学到

  • JavaScriptCore framework
  • iOS 中怎样调用 JavaScript 代码
  • JavaScript 中怎样调用 iOS 的代码

你不必对JavaScript编程很有经验,如果这篇教程激起你学习JavaScript的兴趣,你可以看 Mozilla Developer Network的入门教程 不想看英文的话,

注 :廖雪峰的博客作为入门也是一个不错的选择,或者买这本书

【译】JavaScriptCore Tutorial for iOS: Getting Started_第1张图片
这本书

0X0 开始

点击这里下载本篇教程所用到的代码,解压后,你将得到以下目录

  • Web目录,包含 HTMLCSS 文件
  • Native目录,iOS工程目录,本篇教程主要在这个目录下进行
  • js目录,这篇教程所用到的JavaScript代码

这个 APP 叫做 ShowTime 你可以输入价格,从 iTuns 中搜索电影,你可以打开在浏览器中打开 Web/index.html,然后输入价格,回车后,你将看到这个页面所呈现的内容;

【译】JavaScriptCore Tutorial for iOS: Getting Started_第2张图片

iOS 端,打开工程,编译后,你将看到如下界面

你可以看到,在iOS端功能还没有就绪,我们将一步一步的完成它,这个工程已经包含了一些代码,那我们接下来要怎么做呢。这个 APP 主要提供和网页相似的浏览体验,在 CollectionView 中显示搜索到的结果

0X1 JavaScriptCore

JavaScriptCore.framework 提供了访问 WebKit 的 JavaScript 引擎 ,通常的来说,这个是在Mac上的C API,但在 iOS7OS X 10.9 上实现了更好的OC的封装,这个框架使得 OC SwiftJavaScript 有很强的互通性。

React Native 就是 JavaScriptCore 一个超级好的例子, 如果你好奇怎么使用 JavaScript 来构建 Native APP 你可以点这里去查看教程。

在这一部分,你将看到里边的API,以及 JavaScriptCore 的重要组成部分,JSVirtualMachine JSContext JSValue

JSVirtualMachine

JavaScript 代码在 JSVirtualMachine 所实现的虚拟机中执行,一般来说,你不用直接与这个类打交道,但有一个重要的使用就是,他不能并发执行,如果想要并发执行的话,就需要多个 JSVirtualMachine

每一个 JSVirtualMachine 实例,有自己的堆和垃圾回收器,这也就意味着,你不能在两个虚拟你之间传递对象,一个虚拟机的垃圾回收器不知道怎么处理其他堆中的值。

JSContext

一个 JSContext 对象代表了一个执行JavaScript代码的上下文环境,它与一个全局对象相对应,在 web 开发中,相当于 window 对象。不像虚拟机,你可以在两个context之间传递值(因为他们在同一个虚拟机中)。

JSValue

JSValue 是你主要处理的数据类型,它能代表所有可能的JavaScript值,一个实例 JSValue 被绑定到一个 JSContext 上,任何从 context 来的值都将是 JSValue 类型

这张图解释了,JSContext 和 JSVirtualMachine 是怎么协作的

现在你已经理解了一些JavaScriptCore的一些类型,是时候写点代码了

【译】JavaScriptCore Tutorial for iOS: Getting Started_第3张图片
Enough theory, let’s get to work!

0X2 调用JavaScript方法

回到 Xcode ,展开 Data 文件夹,打开 MovieService.swift ,这个类将请求并处理从 iTunes 返回的数据,现在,他们大部分是空的,我们的工作就是把这些方法实现了。

通常,MovieService 的工作流将是这样的

  • loadMoviesWithLimit(_:onComplete:) 取得对应的电影数据
  • parseResponse(_:withLimit:) 将借助于 JavaScript 代码来处理请求回来的数据。

第一步是获取电影列表,如果你熟悉 JavaScript 编程的话,一般我们使用 XMLHttpRequest 对象来进行网络请求。由于这个对象并不是 JavaScript 语言本身的对象,如果使用了这个对象,那我们将无法再 iOS APP 中的上下文中使用,所以,我们还是要用 native 来进行网络请求的

MovieService 类中,找到 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 来获取电影列表,在把网络请求的响应信息传递个 JavaScript 代码前,你要有一个 JavaScript 可执行的上下文,首先,在 MovieService.swift 中加入下边的代码来导入 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
}()

这样就定义了一个懒加载的 JSContext 属性

  1. 加载了 common.js, 这里边包含了你想要访问的 JavaScript 代码
  2. 加载 js 之后, context 通过执行 context.evaluateScript() 来访问js的内容。传递的参数就是js文件的内容。

是时候来执行 JavaScript 方法了,还是在 MovieService.swift 这个类里边,找到 parseResponse(_:withLimit:) 函数,添加如下代码

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 []
}

我们来一步一步的看一下

  1. 首先,确保 context 属性被正确的初始化,如果在初始化的时候发生了错误,也就没有必要在执行下去了,比如说 common.js 不在 bundle 中。
  2. 你询问 context 对象来提供一个 parseJson 方法,就像先前提到的一样,查询的结果被包含到一个 JSValue 对象中,下边你将通过调用 callWithArguments(_:) 来执行 JavaScript 方法,传递一个数组过去,最后,再把 JSValue 对象转为数组。
  3. filterByLimit() 返回那些适合给定价格的电影列表。
  4. 现在已经获得了电影列表。但是这儿还缺失了一块代码,filtered 持有一个数组,我们应当把他映射为本地的 Movie 类型。

你可能发现在这里用 objectForKeyedSubscript() 有点古怪,很不幸,Swift 只能访问这些原始的方法,而不能把他们转为适当的脚本方法。但 OC 却可以使用方括号语法来来使用下标访问。

暴露 Native 代码

JavaScript 中运行 Native 代码的方法就是定义 block,他们将会被自动桥接到 JavaScript 方法中。 但有个小问题,这种方式只对 OC 有效,对 Swift 的闭包无效。为了执行闭包,你要执行以下两步

  1. 使用 @convention(block)Swift 闭包转为 OCblock
  2. 在你映射到 JavaScript 方法之前,应该转为 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)
  }
}

这个闭包传递一个 JavaScript 数组(用字典代替),并且用它来构建 Movie 实例。

回到 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(_:_:) 方法把一个 block 转为一个 AnyObject
  2. 调用 contextsetObject(_:forKeyedSubscript:) 方法把 block 加载到 JavaScript 的运行时,然后,使用 evaluateScript() 得到 blockJavaScript 中的引用。
  3. 最后一步是通过 callWithArguments(_:) 执行 JavaScript 中的 block,传一个 JSValue 的数组作为参数。返回的参数将是一个包含 Movie 对象的数组。

是时候看看你的代码的效果了。编译并运行,输入价格之后回车,你将看到如下界面。

只有几行代码,你就从创建了一个用 JavaScript 来解析和过滤结果的 Native APP

使用 JSExport Protocol

JavaScript 中使用自定义对象就是另外一种方式是使用 JSExport Protocol。你只需要创建一个继承与 JSExport ProtocolProtocol,然后声明那些你想要暴露给 JavaScript 的方法和属性。

每一个想传输到 JavaScript 中的 Native 类,JavaScriptCore 将在适当的 JSContext 实例中创建一个属性。这个 framework 默认情况下,你的类不会暴露任何属性和方法给 JavaScript, 你必须选择性暴露。JSExport 有几条规则

  1. 暴露实例方法, JavaScriptCore 将创建一个对应的方法作为原型对象的属性。
  2. 暴露的属性,将作为原型的访问属性。
  3. 对于类方法,framework 将会创建一个 JavaScript 对象的构造函数。

为了看如何实际的处理这些,转到 Movie.swift 在现有的类中定义新的 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 的属性和一个类方法,这个类方法,将用作在 JavaScript 中构造 Movie 对象。后者是重要的,因为 JavaScriptCore 还没有初始化。

Movie 遵守 JSExport 用下边的代码来替换整个类

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)
  }
}

这个类方法只是简单的调用了类的初始化方法。

现在,你的类已经准备好被 JavaScript 调用了。为了看我们是如何实现的,打开资源文件下的 additions.js,已经实现了如下代码。

var mapToNative = function(movies) {
  return movies.map(function (movie) {
    return Movie.movieWithTitlePriceImageUrl(movie.title, movie.price, movie.imageUrl);
  });
};

上边的方法,使用数组中的每一个元素来创建 Movie 实例。值得注意的一点是,函数的签名是怎么改变的。因为 JavaScript 并没有定义参数,这取决于额外的驼峰命名的函数名。

打开 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
}()

这儿并没有什么大的改变。使用 setObject(_:forKeyedSubscript:)additions.js 的内容加载到 context 中。也是的 Movie 属性在 JavaScript 属性中可用。

只剩下一件事情可以做了,在 MovieService.swift 中,把 parseResponse(_:withLimit:) 的实现替换为以下代码

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
}

与创建闭包相反,现在,试用 JavaScriptmapToNative() 方法来创建 Movie 数组。如果你编译运行,你会看到你的 APP 和用它应有的样子是一样的。

【译】JavaScriptCore Tutorial for iOS: Getting Started_第4张图片

恭喜你,现在已经创建了一个可以浏览电影的超棒 APP,并且重用了用不同语言编写的已经存在的代码。

【译】JavaScriptCore Tutorial for iOS: Getting Started_第5张图片
这就是无缝用户体验

你可以在这里下载本教程完整的代码。

如果你想学习更多的关于 JavaScriptCore 的内容, 请参看 WWDC 2013 Session 615

如有翻译不足的地方,还望多多指正,谢谢!!!

你可能感兴趣的:(【译】JavaScriptCore Tutorial for iOS: Getting Started)