JavaScriptCore iOS教程

前言

本文翻譯自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建:

Movie night is ON...

為了在iOS上測試ShowTime工程毅舆,打開在<mark>Native/ShowTime</mark>下的xcodeproj工程,編譯并且運行它愈腾,效果如下:

...Or Not?

正如你所見憋活,手機的顯示效果并不是很好,但是你可以很快修正它虱黄。這個工程已經(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框架中可能的類型有了更好的理解胶征,終于我們可以開始寫代碼了塞椎。

Enough theory, let's get to work!

調(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的屬性:

  1. 首先從應(yīng)用的bundle加載包含你想用的JavaScript代碼的叫common.js的文件
  2. 在加載完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 []
}

讓我們一步一步來看看這個過程:

  1. 首先押搪,你的保證你的context對象合理初始化了。如果在設(shè)置的過程中有任何的錯誤(例如:common.js文件不在bundle內(nèi))浅碾,那就沒有繼續(xù)下去的意義了大州;
  2. 你向context對象詢問parseJSON()方法。就像之前提到過的垂谢,詢問的結(jié)果被包裹在JSValue對象內(nèi)厦画。下一步,你使用callWithArguments(_:)方法調(diào)用方法埂陆,該方法的參數(shù)是一個數(shù)組苛白。最后你把JavaScript值轉(zhuǎn)換成一個數(shù)組;
  3. filterByLimit()返回一個符合給定價格限制的影片列表焚虱;
  4. 你得到了影片列表购裙,但還有一步工作要做:列表是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
  1. 使用unsafeBitCast(_:_:)函數(shù)把block轉(zhuǎn)換成AnyObject佣渴;
  2. context調(diào)用setObject(_:forKeyedSubscript:)把代碼塊嵌入到JavaScript運行時。之后在JavaScript代碼內(nèi)你可以通過evaluateScript()方法得到代碼塊的引用赫粥;
  3. 最后一步在JavaScript調(diào)用callWithArguments(_:)執(zhí)行剛定義的代碼塊观话,將JSValue數(shù)組作為參數(shù)。返回值是Movie的數(shù)組越平;

最后是見證代碼結(jié)果的時候了~ 編譯并運行频蛔,輸入一個價格你應(yīng)該就能看到下面的頁面:

That's more like it
That's more like it

僅僅幾行代碼,就生成了一個原生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)的钻注。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚂且,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子幅恋,更是在濱河造成了極大的恐慌杏死,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捆交,死亡現(xiàn)場離奇詭異淑翼,居然都是意外死亡,警方通過查閱死者的電腦和手機品追,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門玄括,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诵盼,你說我怎么就攤上這事惠豺。” “怎么了风宁?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵洁墙,是天一觀的道長。 經(jīng)常有香客問我戒财,道長热监,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任饮寞,我火速辦了婚禮孝扛,結(jié)果婚禮上列吼,老公的妹妹穿的比我還像新娘。我一直安慰自己苦始,他們只是感情好寞钥,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陌选,像睡著了一般理郑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咨油,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天您炉,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛看铆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冀膝,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼霎挟!你這毒婦竟也來了畸写?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氓扛,失蹤者是張志新(化名)和其女友劉穎枯芬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體采郎,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡千所,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蒜埋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淫痰。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖整份,靈堂內(nèi)的尸體忽然破棺而出待错,到底是詐尸還是另有隱情,我是刑警寧澤烈评,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布火俄,位于F島的核電站,受9級特大地震影響讲冠,放射性物質(zhì)發(fā)生泄漏瓜客。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谱仪。 院中可真熱鬧玻熙,春花似錦、人聲如沸疯攒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敬尺。三九已至称杨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間筷转,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工悬而, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呜舒,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓笨奠,卻偏偏與公主長得像袭蝗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子般婆,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容