JavaScriptCore iOS教程

前言

本文翻译自JavaScriptCore Tutorial for iOS: Getting Started
翻译的不对的地方还请多多包涵指正,谢谢~

JavaScriptCore iOS教程

自从2014年引入Swift,它的人气就飙升:2016年2月它在TIOBE(语言使用度排名网站)的排名已在16位。语言排名的前九位空间很少,你会找到一种看起来似乎跟Swift完全相反的语言:JavaScript。Swift在编译时期的安全性上下了很多功夫,但是JavaScript是一个弱类型且动态化的。

Swift和JavaScript看起来似乎不一样,但有个东西将他们绑在一起:你可以使用他们开发一个轻量级的iOS应用!

在这篇JavaScriptCore教程,你将搭建一个hybird应用(iOS原生代码与Web网页并存并有交互),使用部分JavaScript代码。最重要的是,你将学习到:

  • JavaScriptCore框架中的一些组件;
  • 怎样在iOS原生代码中调用JavaScript代码;
  • 怎样在JavaScript中调用Native代码;

注意:你不需要具备丰富的JavaScript知识来学习这篇教程。如果教程让你对JavaScript感兴趣,Mozilla Developer Network对初学者来说是一个不错的学习资料,或者你也可以直接去买JavaScript书看。

开始吧

下载教学工程并且解压它。工程目录如下:

  • Web:包含Web应用需要的HTML和CSS,我们将会把Web应用转换成iOS;
  • Native:iOS工程。这里就是本教程做的所有任务的地方;
  • JS:包含iOS工程所需要的所有JavaScript代码;

这个应用名叫ShowTime,你可以用它通过价格字段来在iTunes上查找影片。为了实际看到它,可以打开Web文件夹下的index.html,输入价格按下Enter建:

JavaScriptCore iOS教程_第1张图片
Movie night is ON...

为了在iOS上测试ShowTime工程,打开在Native/ShowTime下的xcodeproj工程,编译并且运行它,效果如下:

JavaScriptCore iOS教程_第2张图片
...Or Not?

正如你所见,手机的显示效果并不是很好,但是你可以很快修正它。这个工程已经包含了一些代码;自由放松的浏览去了解工程的意图。这个应用的目的是提供跟Web应用一样的体验;它将在UICollectionView展现搜索结果。

什么是JavaScriptCore

JavaScriptCore框架提供使用Web套件中JavaScript引擎的能力。以前这个框架只有在Mac开发才有,而且只有C函数的接口,但是随着iOS7和OS X10.9系统推出后,出现了更好的基于Objective-C的封装接口。该框架提供Swift/Objective-C和JaveScript之间的沟通协作能力。

注意:React Native就是证明JavaScriptCore能力的优秀例子。如果你对使用JavaScript构建原生App感到好奇,那么强烈你去看看React Native教程

在这个章节,你将更深入地了解JavaScriptCore的接口。JavaScriptCore由一些关键组件构成:JSVirtualMachine,JSContext 和 JSValue。我们来看看他们是如何协同工作的。

JSVirtualMachine

JavaScript代码是在一个JSVirtualMachine类的虚拟机上执行的。你一般不会直接接触到这个类,但有一个重要的场景会用到它:异步执行JavaScript代码。在单个JSVirtualMachine内,不可能在同一时间执行多个线程。为了支持并行,你必须使用多个虚拟机。

每一个JSVirtualMachine实例都有它自己的堆栈和垃圾回收器,这意味着不能在虚拟机之间传递对象。虚拟机的垃圾回收器不知道如何处理来自不同堆栈的值。

JSContext

一个JSContext对象代表一个JavaScript代码的执行环境。它对应一个全局对象,它的网页开发相当于一个window对象。和虚拟机不同,你可以自由地在Context对象中传递对象(前提是他们都在一个虚拟机内)。

JSValue

JSValue是你将工作的最基本的数据。它代表任何可能的JavaScript的值。一个JSValue的实例被绑定到它所属的JSContxt对象。任何来自于JSContext的对象都是JSValue类型。

下面这个图标展示了每个对象之间的工作关系:

JavaScriptCore iOS教程_第3张图片

现在你应该对JavaScriptCore框架中可能的类型有了更好的理解,终于我们可以开始写代码了。

JavaScriptCore iOS教程_第4张图片
Enough theory, let's get to work!

调用JavaScript方法

回到XCode工程,在工程导航栏内展开Data组并打开MovieService.swift文件,这个类将获取并处理从iTunes来的数据。现在,它是空的,你的工作就是实现这里里面的方法。

MovieService大致的工作如下:

  • oadMoviesWithLimit(_:onComplete:)用于获取影片数据;
  • parseResponse(_:withLimit:)将使用共享的JavaScript代码来处理数据;

第一步获取影片列表。如果你熟悉JavaScript开发,你应该知道一般是使用XMLHttpRequest对象来获取网络数据。但这个对象并不是语言的一部分,因此你不能在iOS应用内使用,只能求助于iOS原生的网络代码。

在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代码前,你需要为响应提供一个可执行的上下文。首先在本类顶部UIKit引入代码行的下方添加下面一行代码引入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
}()

这块定义了叫context的懒加载的JSContext的属性:

  1. 首先从应用的bundle加载包含你想用的JavaScript代码的叫common.js的文件
  2. 在加载完JS文件后,context对象会通过context.evaluateScript()审查你的内容,参数是你的JS内容;

现在是时候调用JavaScript方法的时候了。仍在MovieService类中,找到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值转换成一个数组;
  3. filterByLimit()返回一个符合给定价格限制的影片列表;
  4. 你得到了影片列表,但还有一步工作要做:列表是JSValue列表,你需要把他们映射成swift类型的;

注意:你可能会觉得这里的objectForKeyedSubscript()使用有点奇怪。不幸的是,Swift只能访问这些原始标注的方法而不能访问一些名字更合理的方法。但Objective-C可以使用成对的中括号的标注语法。

解析原生代码

一种在JavaScript运行时执行原生代码的方法是定义代码块(block)。他们会被自动桥接到JavaScript方法里。但有一点要注意,这种方式只能是Objective-C代码块,而Swift的不行。为了让Swift也能执行,你不得不执行两个任务:

  • @convention(block)封装Swift闭包,桥接成Objective-C代码块;
  • 在你把代码块给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类,在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
  1. 使用unsafeBitCast(_:_:)函数把block转换成AnyObject;
  2. context调用setObject(_:forKeyedSubscript:)把代码块嵌入到JavaScript运行时。之后在JavaScript代码内你可以通过evaluateScript()方法得到代码块的引用;
  3. 最后一步在JavaScript调用callWithArguments(_:)执行刚定义的代码块,将JSValue数组作为参数。返回值是Movie的数组;

最后是见证代码结果的时候了~ 编译并运行,输入一个价格你应该就能看到下面的页面:

JavaScriptCore iOS教程_第5张图片
That's more like it

仅仅几行代码,就生成了一个原生App并且使用JavaScript来解析和过滤结果。

使用JSExport协议

在JavaScript使用自定义对象的另一种方式是使用JSExport协议。你必须创建一个符合JSExport的协议,并且定义你想暴露给JavaScript的属性和方法。

对于引入的每个原生类,JavaScriptCore会在适合的JSContext实例内创建一个原型。框架会选择性地做这件事,默认是没有任何方法和属性暴露给JavaScript。因此你必须选择暴露的方法或属性。JSExport的规则如下:

  • 对于引入的实例方法,JavaScriptCore框架创建一个对应的JavaScript函数,作为原型对象的属性;
  • 类的属性是作为原型的可访问的属性引入的;
  • 对于类方法,框架会在构造器对象内创建一个JavaScript的函数;

为了理解实际的过程是怎样的,我们切换到Movie.swift类并在已存在的类声明上定义以下新的协议:

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构造Movie对象。后面的是必要的,因为JavaScriptCore不会桥接初始化器。

是时候来改变Movie类以实现JSExport协议了。用以下的代码替换整个Movie类:

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

类方法仅仅是调用了Movie初始化的方法。

现在你的类已经可以在JavaScript中使用了。为了理解目前代码的实现,打开Resource组下的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
}()

这里其实跟之前没多大变化。加载addditions.js内容到上下文当中(context)。通过context调用setObject(_:forKeyedSubscript:)方法使Movie原型能在context中使用。

只剩下最后一件事情:在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
}

替换掉builder的使用,现在代码使用mapToNative()方法在JavaScript运行时创建Movie数组。如果是现在编译运行,你应该可以看到应用是这样:

JavaScriptCore iOS教程_第6张图片

恭喜~ 你不仅创建了一个浏览影片的非常棒的应用,而且你是通过使用现有代码及不同语言实现的。

你可能感兴趣的:(JavaScriptCore iOS教程)