ios 开发傲视苍穹iOS《Swift》VIP专题iOS技能

JavaScriptCore iOS教程

2016-09-05  本文已影响1492人  桃红宿雨

前言

本文翻译自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代码。最重要的是,你将学习到:

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

开始吧

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

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

Movie night is ON...

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

...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框架中可能的类型有了更好的理解,终于我们可以开始写代码了。

Enough theory, let's get to work!

调用JavaScript方法

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

MovieService大致的工作如下:

第一步获取影片列表。如果你熟悉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也能执行,你不得不执行两个任务:

让我们切换到<mark>Movie.swift</mark>文件,并在类中添加以下代码:

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的数组;

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

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

上一篇下一篇

猜你喜欢

热点阅读