本片文章翻译自raywenderlich
虽然看起来 Swift
和 JavaScript
看起来有很大的不同,但你可以使用二者创建一个灵活的 iOSAPP
在这篇 JavaScriptCore
教程中,你将建立一个网页配套的 iOSAPP
复用存在的 JavaScript
代码,通过本教程,你将学到
- JavaScriptCore framework
- 在
iOS
中怎样调用JavaScript
代码 - 在
JavaScript
中怎样调用iOS
的代码
你不必对JavaScript编程很有经验,如果这篇教程激起你学习JavaScript的兴趣,你可以看 Mozilla Developer Network的入门教程 不想看英文的话,
注 :廖雪峰的博客作为入门也是一个不错的选择,或者买这本书
0X0 开始
点击这里下载本篇教程所用到的代码,解压后,你将得到以下目录
- Web目录,包含
HTML
和CSS
文件 - Native目录,iOS工程目录,本篇教程主要在这个目录下进行
- js目录,这篇教程所用到的JavaScript代码
这个 APP
叫做 ShowTime
你可以输入价格,从 iTuns
中搜索电影,你可以打开在浏览器中打开 Web/index.html,然后输入价格,回车后,你将看到这个页面所呈现的内容;
在 iOS
端,打开工程,编译后,你将看到如下界面
你可以看到,在iOS端功能还没有就绪,我们将一步一步的完成它,这个工程已经包含了一些代码,那我们接下来要怎么做呢。这个 APP 主要提供和网页相似的浏览体验,在 CollectionView
中显示搜索到的结果
0X1 JavaScriptCore
JavaScriptCore.framework 提供了访问 WebKit 的 JavaScript 引擎 ,通常的来说,这个是在Mac上的C API,但在 iOS7
和 OS X 10.9
上实现了更好的OC的封装,这个框架使得 OC
Swift
与 JavaScript
有很强的互通性。
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的一些类型,是时候写点代码了
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
属性
- 加载了
common.js
, 这里边包含了你想要访问的JavaScript
代码 - 加载 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 []
}
我们来一步一步的看一下
- 首先,确保
context
属性被正确的初始化,如果在初始化的时候发生了错误,也就没有必要在执行下去了,比如说common.js
不在bundle
中。 - 你询问
context
对象来提供一个parseJson
方法,就像先前提到的一样,查询的结果被包含到一个JSValue
对象中,下边你将通过调用callWithArguments(_:)
来执行JavaScript
方法,传递一个数组过去,最后,再把JSValue
对象转为数组。 -
filterByLimit()
返回那些适合给定价格的电影列表。 - 现在已经获得了电影列表。但是这儿还缺失了一块代码,
filtered
持有一个数组,我们应当把他映射为本地的Movie
类型。
你可能发现在这里用
objectForKeyedSubscript()
有点古怪,很不幸,Swift
只能访问这些原始的方法,而不能把他们转为适当的脚本方法。但OC
却可以使用方括号语法来来使用下标访问。
暴露 Native 代码
在 JavaScript
中运行 Native
代码的方法就是定义 block
,他们将会被自动桥接到 JavaScript
方法中。 但有个小问题,这种方式只对 OC
有效,对 Swift
的闭包无效。为了执行闭包,你要执行以下两步
- 使用
@convention(block)
把Swift
闭包转为OC
的block
。 - 在你映射到
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.swift
在 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
- 使用
Swift
的unsafeBitCast(_:_:)
方法把一个block
转为一个AnyObject
- 调用
context
的setObject(_:forKeyedSubscript:)
方法把block
加载到JavaScript
的运行时,然后,使用evaluateScript()
得到block
在JavaScript
中的引用。 - 最后一步是通过
callWithArguments(_:)
执行JavaScript
中的block
,传一个 JSValue 的数组作为参数。返回的参数将是一个包含Movie
对象的数组。
是时候看看你的代码的效果了。编译并运行,输入价格之后回车,你将看到如下界面。
只有几行代码,你就从创建了一个用 JavaScript
来解析和过滤结果的 Native APP
。
使用 JSExport Protocol
在 JavaScript
中使用自定义对象就是另外一种方式是使用 JSExport Protocol
。你只需要创建一个继承与 JSExport Protocol
的 Protocol
,然后声明那些你想要暴露给 JavaScript
的方法和属性。
每一个想传输到 JavaScript
中的 Native
类,JavaScriptCore
将在适当的 JSContext
实例中创建一个属性。这个 framework
默认情况下,你的类不会暴露任何属性和方法给 JavaScript
, 你必须选择性暴露。JSExport
有几条规则
- 暴露实例方法,
JavaScriptCore
将创建一个对应的方法作为原型对象的属性。 - 暴露的属性,将作为原型的访问属性。
- 对于类方法,
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
}
与创建闭包相反,现在,试用 JavaScript
的 mapToNative()
方法来创建 Movie
数组。如果你编译运行,你会看到你的 APP
和用它应有的样子是一样的。
恭喜你,现在已经创建了一个可以浏览电影的超棒 APP
,并且重用了用不同语言编写的已经存在的代码。
你可以在这里下载本教程完整的代码。
如果你想学习更多的关于 JavaScriptCore
的内容, 请参看 WWDC 2013 Session 615
如有翻译不足的地方,还望多多指正,谢谢!!!