前言
本文翻译自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建:
为了在iOS上测试ShowTime工程,打开在Native/ShowTime下的xcodeproj工程,编译并且运行它,效果如下:
正如你所见,手机的显示效果并不是很好,但是你可以很快修正它。这个工程已经包含了一些代码;自由放松的浏览去了解工程的意图。这个应用的目的是提供跟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框架中可能的类型有了更好的理解,终于我们可以开始写代码了。
调用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的属性:
- 首先从应用的bundle加载包含你想用的JavaScript代码的叫common.js的文件
- 在加载完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 []
}
让我们一步一步来看看这个过程:
- 首先,你的保证你的context对象合理初始化了。如果在设置的过程中有任何的错误(例如:common.js文件不在bundle内),那就没有继续下去的意义了;
- 你向context对象询问
parseJSON()
方法。就像之前提到过的,询问的结果被包裹在JSValue对象内。下一步,你使用callWithArguments(_:)
方法调用方法,该方法的参数是一个数组。最后你把JavaScript值转换成一个数组; -
filterByLimit()
返回一个符合给定价格限制的影片列表; - 你得到了影片列表,但还有一步工作要做:列表是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
- 使用
unsafeBitCast(_:_:)
函数把block转换成AnyObject; - context调用
setObject(_:forKeyedSubscript:)
把代码块嵌入到JavaScript运行时。之后在JavaScript代码内你可以通过evaluateScript()
方法得到代码块的引用; - 最后一步在JavaScript调用
callWithArguments(_:)
执行刚定义的代码块,将JSValue数组作为参数。返回值是Movie的数组;
最后是见证代码结果的时候了~ 编译并运行,输入一个价格你应该就能看到下面的页面:
仅仅几行代码,就生成了一个原生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数组。如果是现在编译运行,你应该可以看到应用是这样:
恭喜~ 你不仅创建了一个浏览影片的非常棒的应用,而且你是通过使用现有代码及不同语言实现的。