參與工作時(shí)間比較長(zhǎng)了,隨著Web前端行業(yè)的發(fā)展(大家都懂得..)罗标,客戶端與Web端的交互也越來(lái)越頻繁庸队。其實(shí)本人不太喜歡依賴第三方,那種看不到摸不著的東西用起來(lái)總感覺(jué)不是很安心闯割,同時(shí)也是為了保證雙方都能夠高效完成交互的途中不出現(xiàn)一些意料不到的異常彻消,對(duì)此,研究了一下JavaScriptCore
這個(gè)庫(kù)還是很有必要的宙拉,并分別結(jié)合UIWebView
以及WKWebView
做了一下交互總結(jié)宾尚。
寫(xiě)的比較多,如果是第一次接觸這個(gè)庫(kù)谢澈,建議還是看一看煌贴;如果時(shí)間比較緊,想直接知道結(jié)果的锥忿,送你一個(gè)捷徑??傳送門(mén)牛郑,有幫助可以Star一下,十分感謝
假設(shè)一個(gè)簡(jiǎn)單的場(chǎng)景
- Web通過(guò)一個(gè)
<input/>
輸入一個(gè)字符串敬鬓,通過(guò)點(diǎn)擊按鈕設(shè)置成導(dǎo)航標(biāo)題 - 原生設(shè)置完導(dǎo)航標(biāo)題后淹朋,告知Web
"以將<#字符串#>"設(shè)置成導(dǎo)航Title
笙各,并在網(wǎng)頁(yè)最底下的label顯示出來(lái)。
分別使用UIWebView
以及WKWebView
實(shí)現(xiàn)效果如下:
JavaScriptCore
類庫(kù)里面有12個(gè)類(還有兩個(gè)是負(fù)責(zé)導(dǎo)入相關(guān)類的頭文件以及一個(gè)關(guān)于WebKit的宏定義)础芍;在基本的交互過(guò)程中杈抢,其實(shí)最常使用的有三個(gè):JSContext、JSValue者甲、JSExport
JSContext
簡(jiǎn)單的理解為執(zhí)行JavaScript的一個(gè)環(huán)境春感,就好像我們?cè)诶L制View時(shí)候需要獲取的CGContext
一樣,JS的執(zhí)行需要在此環(huán)境之下虏缸。
JSValue
可以理解成 一種供iOS數(shù)據(jù)結(jié)構(gòu)與JS數(shù)據(jù)結(jié)構(gòu)相互轉(zhuǎn)換的包裝鲫懒,也可以看成一種橋接關(guān)系,我們執(zhí)行JS獲取的結(jié)果就是通過(guò)JSValue對(duì)象進(jìn)行包裝傳給客戶端進(jìn)行處理的刽辙,類型轉(zhuǎn)換官方文檔描述如下:
Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)
JavaScriptType
返回的JSValue
數(shù)據(jù)可通過(guò)JSValue.toXXX()
轉(zhuǎn)成客戶端相應(yīng)的數(shù)據(jù)結(jié)構(gòu)窥岩;反之,客戶端對(duì)象也可以通過(guò)JSValue()
的構(gòu)造方法將相應(yīng)的數(shù)據(jù)結(jié)構(gòu)封裝成JSValue宰缤。
JSExport
這是一個(gè)協(xié)議颂翼,官方文檔沒(méi)有暴露出任何的open協(xié)議方法,可以理解為一個(gè)空協(xié)議慨灭。
通常用法是自定義一個(gè)CustomExport : JSExport
朦乏,里面將JS可以調(diào)用的屬性或者方法進(jìn)行暴露,JS就可以直接使用暴露的屬性與方法了氧骤。
ObjC方法定義樣式是非常特殊的呻疹,但官方文檔給出了轉(zhuǎn)換后JS調(diào)用的樣式:
//Objective-C
- (void)doFoo:(id)foo withBar:(id)bar;
//JS
doFooWithBar(foo,bar)
但這樣會(huì)有一個(gè)缺點(diǎn),萬(wàn)一筹陵,方法有很多個(gè)參刽锤,拼接起來(lái)的JS方法名簡(jiǎn)直就是日了X;不過(guò)這點(diǎn)Apple已經(jīng)幫我們想到了朦佩,使用JSExportAs
宏并思,可以將方法名簡(jiǎn)化,就像Swift
中的typealias
以及ObjC
中的typedef
语稠。
//這樣在JS中直接調(diào)用doFoo(foo,bar)即可
JSExportAs(doFoo,
- (void)doFoo:(id)foo withBar:(id)bar
);
以上三個(gè)文件就算理解完了宋彼,下面來(lái)一段小應(yīng)用??。
客戶端調(diào)用JavaScript
執(zhí)行簡(jiǎn)單的JavaScript
let context = JSContext()
//方法函數(shù)定義采用的是ES6語(yǔ)法仙畦,因?yàn)樽罱趯W(xué)習(xí)RN输涕,習(xí)慣這么寫(xiě)了呢??
let _ = context?.evaluateScript("var textnumber = 1")
let _ = context?.evaluateScript("var names = ['Yue','Xiao','Wen']")
let _ = context?.evaluateScript("var triple = (value) => value + 3")
let returnValue = context?.evaluateScript("triple(3)") //因?yàn)橛蟹祷刂担枰邮找幌?
//打印結(jié)果:returnValue = Optional(6)
print("__testValueInContext --- returnValue = \(returnValue?.toNumber())")
獲取定義的JavaScript變量
//通過(guò)變量名字獲取對(duì)象
let names = context?.objectForKeyedSubscript("names")
//通過(guò)定義順序的下標(biāo)獲取對(duì)象议泵,就是取['Yue','Xiao','Wen']的第0個(gè)元素
let firstName = names?.objectAtIndexedSubscript(0) //Yue
//打印結(jié)果:names = Optional([Yue, Xiao, Wen]) firstName = Optional(Yue)
print("__testValueInContext --- names = \(names?.toArray())\nfirstName = \(firstName)")
/// 獲得context創(chuàng)建的函數(shù)變量
let function = context?.objectForKeyedSubscript("triple")
//運(yùn)行
let result = function?.call(withArguments: [3])
//打印結(jié)果:context-function's result = Optional(6)
print("__testValueInContext --- context-function's result = \(result?.toNumber())")
捕獲執(zhí)行異常
/// 捕獲JS運(yùn)行錯(cuò)誤
context?.exceptionHandler = {(context,exception) in
print("__testValueInContext --- JS error = \(exception)\n")//打印錯(cuò)誤
}
/**
執(zhí)行一個(gè)錯(cuò)誤的js,因?yàn)闆](méi)有函數(shù)Triple(上面的方法名第一字母是小寫(xiě)的),會(huì)調(diào)用上面的exceptionHandler
打印結(jié)果: JS error = Optional(ReferenceError: Can't find variable: Triple)
*/
let _ = context?.evaluateScript("Triple(3)")
JavaScript 調(diào)用客戶端
仔細(xì)看看JSValue
的類型轉(zhuǎn)換占贫,就可以知道桃熄,JS中方法就是客戶端中的閉包先口,不過(guò)這里樓主采用了Swift和ObjC混編模式型奥,至于原因下面會(huì)說(shuō)一下:(用法相似,但是真正的結(jié)構(gòu)并不一樣)
//獲得處理完畢的數(shù)據(jù)
let result = RITLJSCoreObject.textJavaScriptUseiOS(inObjC: "Hello")
//結(jié)果 I am Objc, result = Optional("Hello I am append String")
print("I am Objc, result = \(result?.toString())\n")
實(shí)現(xiàn)方法:
+(JSValue *)textJavaScriptUseiOSInObjC:(NSString *)value
{
JSContext * context = [JSContext new];
//設(shè)置block
context[@"stringHandler"] = ^(NSString * oldValue){
NSMutableString * valueHandler = [[NSMutableString alloc]initWithString:oldValue];
[valueHandler appendString:@" I am append String"];
return valueHandler;
};
NSString * js = [NSString stringWithFormat:@"stringHandler('%@')",value];
//注入
return [context evaluateScript:js];
}
Swift
版本如下碉京,功能實(shí)現(xiàn)在本人看來(lái)應(yīng)該是一樣的厢汹,但在進(jìn)行注入的時(shí)候出現(xiàn)了問(wèn)題,導(dǎo)致執(zhí)行方法出現(xiàn)了 undefined
谐宙。多謝評(píng)論區(qū)
我只是個(gè)仙的提示
可能是Swift
的一個(gè)bug烫葬,也可能是我使用不當(dāng)
如果是我使用錯(cuò)了嚼锄,還請(qǐng)知道原因的小伙伴私信一下命黔,十分感謝。
let context = JSContext()
//初始化一個(gè)閉包
//由于OC中block與Swift中的closure結(jié)構(gòu)并不一樣站辉,需要使用`@convention(block) `關(guān)鍵詞聲明一下
let stringHandler : @convention(block) (String) -> String = { (value) in
var value = value
value.append(" I am appending word with closure!")
return value
}
//封裝成JSValue
let handerValue = JSValue(object: stringHandler, in: context)
// ~~問(wèn)題語(yǔ)句$$$$划栓,我懷疑是注入失敗..見(jiàn)鬼了~~
context?.setObject(handerValue, forKeyedSubscript: "stringHandler" as NSString)
let result = context?.evaluateScript("stringHandler('Hello')")
// ~~結(jié)果:I am Swift ,result = Optional("undefined") - - 很無(wú)解有沒(méi)有6医怼!V臆瘛蒋歌!(之前)~~
// 結(jié)果: I am Swift ,result = Optional("Hello I am appending word with closure!")
print("I am Swift ,result = \(result?.toString())\n")
實(shí)現(xiàn)場(chǎng)景
終于可以運(yùn)用上面的一些方法來(lái)實(shí)現(xiàn)功能啦。
JavaScript中的邏輯如下:
- 確認(rèn)當(dāng)前使用的是UIWebView還是WKWebView,并通過(guò)變量ritl_type確定
- 點(diǎn)擊按鈕委煤,根據(jù)類型執(zhí)行不同的操作
- 客戶端通過(guò)執(zhí)行iosTellSomething方法告知Web堂油,修改當(dāng)前l(fā)abel的值
// 默認(rèn)為WKWebView
var ritl_tyle = "WKWebView";
// 確定是webView還是WKWebView
function sureType(value){
ritl_tyle = value;
};
// 按鈕點(diǎn)擊
function buttonDidTap (){
var inputValue = $('#input').val()
if (ritl_tyle == "UIWebView"){//如果是UIWebView
RITLExportObject.say(inputValue)//通過(guò)注入的對(duì)象進(jìn)行通知客戶端
}
else if (ritl_tyle == "WKWebView"){//如果是WKWebView
alert("WKWebView");
window.webkit.messageHandlers.ChangedMessage.postMessage(inputValue);
}
};
function iosTellSomething(value){
//document.getElementById("label").value = "收到啦";//設(shè)置給label
$('#label').text(value);
}
UIWebView
JSExport
定義一個(gè)自定義的協(xié)議RITLJSExport
,這里仍然采用混編模式,因?yàn)槲疫€是Swfit
注入失敗了...
Objective
@protocol RITLJSExport <NSObject,JSExport>
// 類似typedef 將saySomething定義為say,便于JS調(diào)用
JSExportAs(say,
- (void)saySomething:(NSString *)thing
);
@end
@interface RITLExportObject : NSObject
/// 進(jìn)行的回調(diào)
@property (nonatomic, copy) void(^dosomething)(NSString *);
/// 將自己注冊(cè)到JSContext
- (void)registerSelfToContext:(JSContext *)context;
@end
@interface RITLExportObject (RITLJSExport)<RITLJSExport>
@end
Swift
import UIKit
/// 必須追加@objc
@objc protocol RITLJSSwiftExport: JSExport {
/// 方法的標(biāo)簽一定記得去掉
func say(_ something: String)
}
/// 必須追加@objc
@objc class RITLExportSwiftObject: NSObject {
var doSomething: ((String?) -> Void)?
override init() {
super.init()
}
}
extension RITLExportSwiftObject : RITLJSSwiftExport {
func say(_ something: String) {
doSomething?(something)
}
}
UIWebViewDelegate
在UIWebViewDelegate
中的webViewDidFinishLoad()
方法中對(duì)JSContext
進(jìn)行截取碧绞,并執(zhí)行操作:
// MARK: UIWebView-Delegate 系列
extension RITLJSWebViewController : UIWebViewDelegate {
func webViewDidFinishLoad(_ webView: UIWebView) {
//獲得JSContent對(duì)象
guard let context : JSContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext? else {
return
}
//告訴web府框,這里是UIWebView
webView.stringByEvaluatingJavaScript(from: "sureType('UIWebView')")
/* 使用的ObjC的Export對(duì)象 */
let exportObject = RITLExportObject()
exportObject.dosomething = { [weak self](value) in
guard let value = value else { return }
self?.navigationItem.title = value //設(shè)置導(dǎo)航欄
//執(zhí)行js告知,修改導(dǎo)航欄完畢
webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已將\(value)設(shè)置成導(dǎo)航Title')")//回應(yīng)
}
//進(jìn)行注入
exportObject.registerSelf(to: context)
// 使用Swift的Export對(duì)象
let exportObject = RITLExportSwiftObject()
exportObject.doSomething = { [weak self](value) in
guard let value = value else { return }
DispatchQueue.main.async {
//設(shè)置導(dǎo)航欄
self?.navigationItem.title = value
//執(zhí)行js告知头遭,修改導(dǎo)航欄完畢
webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已將\(value)設(shè)置成導(dǎo)航Title')")//回應(yīng)
}
}
context.setObject(exportObject, forKeyedSubscript: "RITLExportObject" as NSString)
}
}
WKWebView
首先有一點(diǎn)寓免,WKWebView
是獲取不到JSContext
的,那咋辦计维?沒(méi)關(guān)系袜香,WKWebView
提供給了我們非常便利的交互,不詳細(xì)說(shuō)了鲫惶,之前寫(xiě)的一篇博文已經(jīng)介紹了蜈首,有興趣可以看看iOS開(kāi)發(fā)-------基于WKWebView的原生與JavaScript數(shù)據(jù)交互
添加JavaScript
交互
// 使用WkWebView
lazy var wkWebView : WKWebView = {
let webView: WKWebView = WKWebView(frame: self.view.bounds)
webView.navigationDelegate = self
webView.uiDelegate = self
webView.configuration.userContentController.add(RITLSciptMessageHandler(self), name: "ChangedMessage")// 添加處理
return webView
}()
在WKNavigationDelegate中告知web當(dāng)前使用webView的類型:
// 是為了使用JS確認(rèn)一下類型,實(shí)際開(kāi)發(fā)不需要在這個(gè)代理下進(jìn)行如下操作
extension RITLJSWebViewController : WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
//確認(rèn)類型
webView.evaluateJavaScript("sureType('WKWebView')", completionHandler: nil)
}
}
履行WKScriptMessageHandler
協(xié)議欠母,完成交互操作即可
// MARK: WKWebView-Delegate 系列
extension RITLJSWebViewController : WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
{
//如果body體是約定好的字符串欢策,并且通過(guò)標(biāo)志ChangedMessage傳遞并且存在body體
guard message.body is String,message.name == "ChangedMessage",let body:String = message.body as? String else { return }
navigationItem.title = body//設(shè)置導(dǎo)航
//執(zhí)行通知HTML
wkWebView.evaluateJavaScript("iosTellSomething('已將\(body)設(shè)置成導(dǎo)航Title')") { (_, error) in
print("error = \(error?.localizedDescription)")
}
}
}
最后記得移除哦
deinit {
print("\(type(of: self)) deinit")
if ritl_useWkWebView {
wkWebView.configuration.userContentController.removeAllUserScripts()
}
}
這樣子,基于JavaScriptCore的UIWebView以及WKWebView交互就算圓滿完成啦赏淌,歡迎前去Start