前言
本文翻譯自JavaScriptCore Tutorial for iOS: Getting Started
翻譯的不對的地方還請多多包涵指正险毁,謝謝~
JavaScriptCore iOS教程
自從2014年引入Swift喇辽,它的人氣就飆升:2016年2月它在TIOBE(語言使用度排名網(wǎng)站)的排名已在16位驴一。語言排名的前九位空間很少攒霹,你會找到一種看起來似乎跟Swift完全相反的語言:JavaScript抓谴。Swift在編譯時期的安全性上下了很多功夫魂挂,但是JavaScript是一個弱類型且動態(tài)化的链患。
Swift和JavaScript看起來似乎不一樣,但有個東西將他們綁在一起:你可以使用他們開發(fā)一個輕量級的iOS應(yīng)用晴叨!
在這篇JavaScriptCore教程凿宾,你將搭建一個hybird應(yīng)用(iOS原生代碼與Web網(wǎng)頁并存并有交互),使用部分JavaScript代碼兼蕊。最重要的是初厚,你將學(xué)習(xí)到:
- JavaScriptCore框架中的一些組件;
- 怎樣在iOS原生代碼中調(diào)用JavaScript代碼孙技;
- 怎樣在JavaScript中調(diào)用Native代碼产禾;
注意:你不需要具備豐富的JavaScript知識來學(xué)習(xí)這篇教程。如果教程讓你對JavaScript感興趣牵啦,Mozilla Developer Network對初學(xué)者來說是一個不錯的學(xué)習(xí)資料亚情,或者你也可以直接去買JavaScript書看。
開始吧
下載教學(xué)工程并且解壓它哈雏。工程目錄如下:
- Web:包含Web應(yīng)用需要的HTML和CSS楞件,我們將會把Web應(yīng)用轉(zhuǎn)換成iOS;
- Native:iOS工程僧著。這里就是本教程做的所有任務(wù)的地方履因;
- JS:包含iOS工程所需要的所有JavaScript代碼;
這個應(yīng)用名叫<strong>ShowTime</strong>盹愚,你可以用它通過價格字段來在iTunes上查找影片栅迄。為了實際看到它,可以打開Web文件夾下的index.html皆怕,輸入價格按下Enter建:
為了在iOS上測試ShowTime工程毅舆,打開在<mark>Native/ShowTime</mark>下的xcodeproj工程,編譯并且運行它愈腾,效果如下:
正如你所見憋活,手機的顯示效果并不是很好,但是你可以很快修正它虱黄。這個工程已經(jīng)包含了一些代碼悦即;自由放松的瀏覽去了解工程的意圖。這個應(yīng)用的目的是提供跟Web應(yīng)用一樣的體驗橱乱;它將在UICollectionView
展現(xiàn)搜索結(jié)果辜梳。
什么是JavaScriptCore
JavaScriptCore框架提供使用Web套件中JavaScript引擎的能力。以前這個框架只有在Mac開發(fā)才有泳叠,而且只有C函數(shù)的接口作瞄,但是隨著iOS7和OS X10.9系統(tǒng)推出后,出現(xiàn)了更好的基于Objective-C
的封裝接口危纫。該框架提供Swift/Objective-C和JaveScript之間的溝通協(xié)作能力宗挥。
注意:React Native就是證明JavaScriptCore能力的優(yōu)秀例子乌庶。如果你對使用JavaScript構(gòu)建原生App感到好奇,那么強烈你去看看React Native教程
在這個章節(jié)契耿,你將更深入地了解JavaScriptCore的接口瞒大。JavaScriptCore由一些關(guān)鍵組件構(gòu)成:JSVirtualMachine,JSContext 和 JSValue宵喂。我們來看看他們是如何協(xié)同工作的糠赦。
JSVirtualMachine
JavaScript代碼是在一個JSVirtualMachine類的虛擬機上執(zhí)行的。你一般不會直接接觸到這個類锅棕,但有一個重要的場景會用到它:異步執(zhí)行JavaScript代碼拙泽。在單個JSVirtualMachine內(nèi),不可能在同一時間執(zhí)行多個線程裸燎。為了支持并行顾瞻,你必須使用多個虛擬機。
每一個JSVirtualMachine實例都有它自己的堆棧和垃圾回收器德绿,這意味著不能在虛擬機之間傳遞對象荷荤。虛擬機的垃圾回收器不知道如何處理來自不同堆棧的值。
JSContext
一個JSContext對象代表一個JavaScript代碼的執(zhí)行環(huán)境移稳。它對應(yīng)一個全局對象蕴纳,它的網(wǎng)頁開發(fā)相當于一個window對象。和虛擬機不同个粱,你可以自由地在Context對象中傳遞對象(前提是他們都在一個虛擬機內(nèi))古毛。
JSValue
JSValue是你將工作的最基本的數(shù)據(jù)。它代表任何可能的JavaScript的值都许。一個JSValue的實例被綁定到它所屬的JSContxt對象稻薇。任何來自于JSContext的對象都是JSValue類型。
下面這個圖標展示了每個對象之間的工作關(guān)系:
現(xiàn)在你應(yīng)該對JavaScriptCore框架中可能的類型有了更好的理解胶征,終于我們可以開始寫代碼了塞椎。
調(diào)用JavaScript方法
回到XCode工程,在工程導(dǎo)航欄內(nèi)展開Data組并打開MovieService.swift文件睛低,這個類將獲取并處理從iTunes來的數(shù)據(jù)“负荩現(xiàn)在,它是空的钱雷,你的工作就是實現(xiàn)這里里面的方法骂铁。
MovieService大致的工作如下:
-
oadMoviesWithLimit(_:onComplete:)
用于獲取影片數(shù)據(jù); -
parseResponse(_:withLimit:)
將使用共享的JavaScript代碼來處理數(shù)據(jù)急波;
第一步獲取影片列表。如果你熟悉JavaScript開發(fā)瘪校,你應(yīng)該知道一般是使用XMLHttpRequest
對象來獲取網(wǎng)絡(luò)數(shù)據(jù)澄暮。但這個對象并不是語言的一部分名段,因此你不能在iOS應(yīng)用內(nèi)使用,只能求助于iOS原生的網(wǎng)絡(luò)代碼泣懊。
在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
獲取影片列表馍刮。在你傳遞響應(yīng)數(shù)據(jù)給JavaScript代碼前信夫,你需要為響應(yīng)提供一個可執(zhí)行的上下文。首先在本類頂部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的屬性:
- 首先從應(yīng)用的bundle加載包含你想用的JavaScript代碼的叫common.js的文件
- 在加載完JS文件后静稻,context對象會通過
context.evaluateScript()
審查你的內(nèi)容,參數(shù)是你的JS內(nèi)容匈辱;
現(xiàn)在是時候調(diào)用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對象合理初始化了。如果在設(shè)置的過程中有任何的錯誤(例如:common.js文件不在bundle內(nèi))浅碾,那就沒有繼續(xù)下去的意義了大州;
- 你向context對象詢問
parseJSON()
方法。就像之前提到過的垂谢,詢問的結(jié)果被包裹在JSValue對象內(nèi)厦画。下一步,你使用callWithArguments(_:)
方法調(diào)用方法埂陆,該方法的參數(shù)是一個數(shù)組苛白。最后你把JavaScript值轉(zhuǎn)換成一個數(shù)組; -
filterByLimit()
返回一個符合給定價格限制的影片列表焚虱; - 你得到了影片列表购裙,但還有一步工作要做:列表是JSValue列表,你需要把他們映射成swift類型的鹃栽;
注意:你可能會覺得這里的
objectForKeyedSubscript()
使用有點奇怪躏率。不幸的是,Swift只能訪問這些原始標注的方法而不能訪問一些名字更合理的方法民鼓。但Objective-C可以使用成對的中括號的標注語法薇芝。
解析原生代碼
一種在JavaScript運行時執(zhí)行原生代碼的方法是定義代碼塊(block)。他們會被自動橋接到JavaScript方法里丰嘉。但有一點要注意夯到,這種方式只能是Objective-C代碼塊,而Swift的不行饮亏。為了讓Swift也能執(zhí)行耍贾,你不得不執(zhí)行兩個任務(wù):
- 用
@convention(block)
封裝Swift閉包阅爽,橋接成Objective-C代碼塊; - 在你把代碼塊給JavaScript方法執(zhí)行之前荐开,你需要將它強轉(zhuǎn)成
AnyObject
對象付翁;
讓我們切換到<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對象數(shù)組晃听,并用它們構(gòu)造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(_:_:)
函數(shù)把block轉(zhuǎn)換成AnyObject佣渴; - context調(diào)用
setObject(_:forKeyedSubscript:)
把代碼塊嵌入到JavaScript運行時。之后在JavaScript代碼內(nèi)你可以通過evaluateScript()
方法得到代碼塊的引用赫粥; - 最后一步在JavaScript調(diào)用
callWithArguments(_:)
執(zhí)行剛定義的代碼塊观话,將JSValue數(shù)組作為參數(shù)。返回值是Movie的數(shù)組越平;
最后是見證代碼結(jié)果的時候了~ 編譯并運行频蛔,輸入一個價格你應(yīng)該就能看到下面的頁面:

僅僅幾行代碼,就生成了一個原生App并且使用JavaScript來解析和過濾結(jié)果秦叛。
使用JSExport協(xié)議
在JavaScript使用自定義對象的另一種方式是使用JSExport協(xié)議晦溪。你必須創(chuàng)建一個符合JSExport的協(xié)議,并且定義你想暴露給JavaScript的屬性和方法挣跋。
對于引入的每個原生類三圆,JavaScriptCore會在適合的JSContext實例內(nèi)創(chuàng)建一個原型”芘兀框架會選擇性地做這件事舟肉,默認是沒有任何方法和屬性暴露給JavaScript。因此你必須選擇暴露的方法或?qū)傩圆榭狻SExport的規(guī)則如下:
- 對于引入的實例方法路媚,JavaScriptCore框架創(chuàng)建一個對應(yīng)的JavaScript函數(shù),作為原型對象的屬性樊销;
- 類的屬性是作為原型的可訪問的屬性引入的整慎;
- 對于類方法,框架會在構(gòu)造器對象內(nèi)創(chuàng)建一個JavaScript的函數(shù)围苫;
為了理解實際的過程是怎樣的裤园,我們切換到Movie.swift類并在已存在的類聲明上定義以下新的協(xié)議:
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構(gòu)造Movie對象剂府。后面的是必要的拧揽,因為JavaScriptCore不會橋接初始化器。
是時候來改變Movie類以實現(xiàn)JSExport協(xié)議了。用以下的代碼替換整個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)
}
}
類方法僅僅是調(diào)用了Movie初始化的方法淤袜。
現(xiàn)在你的類已經(jīng)可以在JavaScript中使用了万俗。為了理解目前代碼的實現(xiàn),打開Resource組下的additions.js饮怯。它已經(jīng)包含了以下代碼:
var mapToNative = function(movies) {
return movies.map(function (movie) {
return Movie.movieWithTitlePriceImageUrl(movie.title, movie.price, movie.imageUrl);
});
};
上述方法會處理來自輸入數(shù)組中的每個元素,并使用該元素生成Movie對象嚎研。值得一提的一點是方法是如何簽名的:因為JavaScript沒有已命名的參數(shù)蓖墅,它是使用駝峰命名的方式將額外的參數(shù)拼接到方法名后面。
打開MovieService.swift類临扮,并替換context屬性的實現(xiàn)方式:
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內(nèi)容到上下文當中(context)。通過context調(diào)用setObject(_:forKeyedSubscript:)
方法使Movie原型能在context中使用杆勇。
只剩下最后一件事情:在MovieService.swift類中贪壳,用以下代碼替換目前parseResponse(_:withLimit:)
的實現(xiàn):
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的使用,現(xiàn)在代碼使用mapToNative()
方法在JavaScript運行時創(chuàng)建Movie數(shù)組蚜退。如果是現(xiàn)在編譯運行闰靴,你應(yīng)該可以看到應(yīng)用是這樣:
恭喜~ 你不僅創(chuàng)建了一個瀏覽影片的非常棒的應(yīng)用,而且你是通過使用現(xiàn)有代碼及不同語言實現(xiàn)的钻注。