【译】JavaScriptCore Tutorial for iOS: Getting Started

本片文章翻译自raywenderlich

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

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

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

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

这本书

0X0 开始

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

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

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

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的一些类型,是时候写点代码了

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 和用它应有的样子是一样的。

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

这就是无缝用户体验

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容