如何在WebKit中使用JavaScriptCore
這裡先要道個歉。其實有點標題黨了
眾所周知,WKWebView由於採用了非同步處理js的方式,間接砍掉了UIWebView的documentView.webView.mainFrame.javaScriptContext
屬性,也就不能很方便的使用javaScriptCore讓js呼叫原生方法,最近我在負責這類工作,其中一個要求就是要能實現web端直接使用jsBridge.getData(),jsBridge.openNative()的形式進行呼叫。
那怎麼辦呢?
總不能說放棄WebKit用回被蘋果拋棄的UIWebView吧?
總不能跟他們說:對不起我做不了吧(雖然我真的很想這樣說?
在不算特別難的情況下,查詢了一下目前iOS主流的jsBrideg方案(這裡不客氣的說一句在座的各位都是垃圾),沒有一個是符合邏輯學的,像什麼WebViewJavascriptBridge,dsBridge等等都是同一類東西,即需要web註冊啦,呼叫只能用bridge.call(“方法名”)啦等等等等
雖說如此但我還是從dsBridge中找到了比較好的處理回撥的方式:利用輸入框來回撥,除此之外真的沒什麼有用的了,真心不建議使用這些第三方,太麻煩了根本不像是有夢想的人寫出來的東西,都2018年還得註冊才能用。。。自己寫一個方便的又不難
我是怎麼做的呢
首先我們要確定一下目標:
- web端可以直接呼叫bridge的方法
- 安卓那邊可以很容易就實現,所以不能依賴前端有額外的注入,不然他們就得增加額外的維護工作,越多的維護內容意味著更容易的出錯,這是我們應該避免的
- 基於上面那一條,這個額外的工作應該是自動生成的
- 我寫程式碼的必要要求:低侵入性
綜上所訴:
- JavaScriptCore可以很方便的完成,只要能解決怎麼注入
- 避免前端差別對待只要iOS本地進行注入就行
- 自動完成可以交給runtime生成注入的js程式碼
- 這個儘量,必要時用黑魔法也是能接受的(記得寫好測試程式碼)
*以下程式碼均使用swift
首先我們按照UIWebView時代的需求,準備一個繼承自JSExport協議的協議:
final class JSResult: NSObject, HandyJSON {
var status: Int = 0
var msg: String?
var data: [String: Any] = [:]
func isNotAFunction() -> JSResult{
status = -1
msg = "無對應方法"
return self
}
var asyncCallback: ((JSResult)->Void)?
}
@objc protocol JSBridgeCallFunction: JSExport {
///從 APP 獲取資料
func get(_ type: String, Data extraParams: NSDictionary) -> JSResult
}
這裡有幾點用過JSExport都知道的坑:
- 如果js呼叫的方法叫getData,那麼原生對應的方法名得叫[get:Data:],如果有三個引數就可以是[get:Da:ta:],swift的話可以給變數取別名是沒問題的
- 這裡字典最好用NSDictionary,其實感覺用[AnyHash: AnyHash]應該也是能行的,但我嫌不好看
- 識別不了非JavaScriptCore支援的型別
- 雖然傳block(閉包)也是可以的,但實際上我這種做法傳這個就沒什麼意義了。因為不是WebKit在呼叫JavaScriptCore,具體會在下面流程看到
- 基於上一點,這個方法都需要一個返回值,這個沒任何要求只要是NSObject的子類都行,因為下面的協議需要是@objc的
- 返回型別需要能轉字典和轉JSON,這裡為了方便使用了HandyJSON實現
- JSResult的內容是根據需求來的,這個只是作為例子,isNotAFunction和asyncCallback是用來做額外處理的,會在後面解釋為什麼有這兩個東西
然後是實現了JSBridgeCallFunction的類
class JSBridge: NSObject, JSBridgeCallFunction {
func get(_ type: String, Data extraParams: NSDictionary) -> JSResult {
let result = JSResult()
guard let type = GetDataType(rawValue: type) else { return result.isNotAFunction() }
switch type {
case .USERINFO:
if let data = User.current.toJSON() {
result.data = data
}
}
return result
}
}
extension JSBridge {
enum GetDataType: String {
///獲取使用者資訊
case USERINFO
}
}
這裡為了方便js得知客戶端沒有實現某些type,所以返回了isNotAFunction(這個名字是從JSContext的exceptionHandler裡面學來的?)
User也是實現了HandlyJSON所以可以拿簡單轉字典
前面說了是用輸入框進行回撥,那麼就要去WKWebView處理輸入框的WKUIDelegate方法裡進行處理
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
if let context = JSContext() {
context.setObject(JSBridge(), forKeyedSubscript: "JSBridge" as NSString)
context.exceptionHandler = { context, value in
if let valueStr = value?.toString(), valueStr.contains("is not a function") {//這個是沒用的,留著方便除錯
completionHandler("{ status: -1, msg: '無對應方法' }")
}
}
if let result = context.evaluateScript(prompt)?.toObject() as? JSResult {
if result.asyncCallback != nil {
result.asyncCallback = { result in
completionHandler(result.toJSONString())
}
} else {
completionHandler(result.toJSONString())
}
return
}
}
completionHandler("")
}
感覺蘋果也是基本放棄這個庫了。好多地方都不是很方便接入swift(包括初始化居然是optional的。。。)
這裡我解釋一下,prompt傳進來的是類似於JsBridge.getData("USERINFO")
的東西,然後直接交給JSContext去對映原生方法
asyncCallback是用來處理非同步的,上面這個處理的邏輯其實是很微妙的,如果js那邊呼叫的時候其實是用一個非同步回撥的話,那麼到了上面這段程式碼的時候其實是把非同步轉成了同步,那麼真正遇到原生裡面需要非同步處理的時候就會出問題(比如要登陸,登陸結束才能回撥js)所以我設計就是如果需要處理原生非同步的話,返回的result物件的asyncCallback就不會為空,上面程式碼判斷不為空就重新賦值這個閉包,然後在真正處理結束的地方才會呼叫result.asyncCallback?()
那麼重點來了,為了實現傳進來的prompt是類似於JsBridge.getData("USERINFO")
的東西,要怎麼生成這個注入的js呢,對此我請來了前端的負責人寫了一段js:
!(function () {
function _objToJson (obj) {
var str = '';
try {
str = JSON.stringify(obj);
} catch (e) {}
return str;
}
function _jsonToObj (str) {
var obj = {};
try {
obj = JSON.parse(str);
} catch (e) {}
return obj;
}
function _toQuery (method, type, params) {
var str = params
? 'JSBridge.' + method + '("' + type + '",' + _objToJson(params) + ')'
: 'JSBridge.' + method + '("' + type + '")';
return str;
}
function _getData(type, extraParams, callback) {
var query = _toQuery('getData', type, extraParams);
var result = prompt(query);
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
}
var JSBridge = window.JSBridge = {
getData: _getData
};
var doc = document;
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('JSBridgeReady');
readyEvent.bridge = JSBridge;
doc.dispatchEvent(readyEvent);
})();
然後我把這段js分割成兩段:
static private let jsPrefix =
"""
!(function () {
function _objToJson (obj) {
var str = '';
try {
str = JSON.stringify(obj);
} catch (e) {}
return str;
}
function _jsonToObj (str) {
var obj = {};
try {
obj = JSON.parse(str);
} catch (e) {}
return obj;
}
function _toQuery (method, type, params) {
var str = params
? 'JSBridge.' + method + '("' + type + '",' + _objToJson(params) + ')'
: 'JSBridge.' + method + '("' + type + '")';
return str;
}
var doc = document;
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('JSBridgeReady');
readyEvent.bridge = JSBridge;
doc.dispatchEvent(readyEvent);
"""
static private let jsSufix = "})();"
中間的部分就用runtime來生成了,最終的生成函式:
static func generateJSBridgeJs() -> String {
var result = "var JSBridge = window.JSBridge = {"
var functions = ""
var count: UInt32 = 0
let methodList = protocol_copyMethodDescriptionList(JSBridgeCallFunction.self, true, true, &count)
for index in 0..<Int(count) {
if let method = methodList?[index], let selector = method.name {
let methodName = NSStringFromSelector(selector).replacingOccurrences(of: ":", with: "")
result += "\(methodName): _\(methodName),"
functions +=
"""
function _\(methodName) (paraA, paraB, callback) {
var query = _toQuery('\(methodName)', paraA, paraB);
var result = prompt(query);
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
}
"""
}
}
result += "};"
return jsPrefix + result + functions + jsSufix
}
在頁面載入完呼叫:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(JSBridge.generateJSBridgeJs()) { (result, error) in
guard let result = result as? Bool, result, error == nil else {
fatalError("注入失敗,請檢查JSBridge.generateJSBridgeJs()")
}
}
}
江江!搞定,至此不管後端怎麼加方法,只要這邊JSBridgeCallFunction裡新增新的方法就行了,完全不需要修改任何地方
But,其實這個自動化生成有一些限制:
首先我這裡根據專案需求,把js呼叫的函式寫死為:
function _\(methodName) (paraA, paraB, callback)
這樣就需要和前端協商好引數的順序了,如果有回撥就需要放到最後一位,像有時候callback是必選的,paraB是可選的話,他們一般的習慣都是把paraB放到最後一位去,反過來這種對他們來說就有點反人類了,但無傷大雅,反正不是我在寫嘿嘿嘿
實際情況下可能會有更多的引數,但這個其實也很有辦法解決:假設只有一個非同步回撥,那麼在前面獲取的方法有多少個引數,生成多少個para就行,然後_toQuery改成傳陣列
但還有可能js傳了多個function作為引數,那這個就GG啦,目前我沒遇到這種情況所以沒動力深入研究解決辦法?,或許可以拆分成多個函式去進行不同的回撥?但判斷太多了不好寫了
又或者是,前端負責維護一張方法名錶,動態獲取這張方法名錶後去解析動態生成,但這樣又跟註冊有點像了我又不是很喜歡。。。。
總之目前用在我負責的專案的話這樣說足夠的,但通用性不強,說不定哪天心血來潮會根據這個思路寫一個通用的庫
相關文章
- 簡述Chromium, CEF, Webkit, JavaScriptCore, V8, BlinkWebKitJavaScript
- JavaScriptCore全面解析JavaScript
- 深入理解JavaScriptCoreJavaScript
- Webkit 核心初探WebKit
- 如何在 Laravel 中靈活的使用 TraitLaravelAI
- 如何在 JavaScript 中更好地使用陣列JavaScript陣列
- 如何在 React 中優雅的使用 addEventListenerReactdev
- 如何在 Blazor WebAssembly中 使用 功能開關BlazorWeb
- iOS與JS互動之UIWebView-JavaScriptCore框架iOSJSUIWebViewJavaScript框架
- WebKit Insie: Active 樣式表WebKit
- 如何在Windows 11系統中將任意檔案(如bat/log等)固定在開始選單?WindowsBAT
- 如何在 Vue 中優雅地使用 CSS Modules?VueCSS
- 如何在DebianStretch中安裝使用PHP5PHP
- 如何在 Linux 終端中連線使用 WiFi?LinuxWiFi
- 如何在AE2022中使用 Keylight 效果?
- 如何在 Linux 中安裝和使用 duf 命令Linux
- WebKit Inside: DOM樹的構建WebKitIDE
- reCAPTCHA是Google使用方式如下如所示APTGo
- -webkit-text-size-adjust使用匯總, 小於12px字型時WebKit
- 如何在 Linux 中安裝、配置和使用 Fish Shell?Linux
- 如何在vue專案中優雅的使用SVGVueSVG
- [譯文] 如何在 JavaScript 中更好地使用陣列JavaScript陣列
- 如何在 Linux 中建立非登入使用者?Linux
- 如何在React中優雅的使用Interval(輪詢)React
- 如何在 Kubernetes 環境中搭建 MySQL(四):使用 StMySql
- #Webkit 翻譯# Web 檢查器中的圖層視覺化工具WebKit視覺化
- iframe可以使用父頁面中的資源嗎(如:css、js等)?CSSJS
- WWDC 2018:Safari與WebKit的新特性WebKit
- 如何在 Ubuntu 中管理和使用邏輯卷管理 LVMUbuntuLVM
- 如何在 Linux 中配置使用 SSD (固態驅動器)Linux
- 如何在Nuxt3.0中使用MongoDB資料庫UXMongoDB資料庫
- 如何在CentOS 7中使用nmcli工具管理網路CentOS
- PS使用教程:如何在Photoshop中給灰色圖片上色?
- PremierePro使用教程:如何在PremierePro中製作出翻頁效果?REM
- 技術乾貨| 如何在MongoDB中輕鬆使用GridFS?MongoDB
- 在Linux中,如何在Linux中使用LXD進行容器管理?Linux
- 如何在網頁中做出炫酷的動畫(使用Spine)網頁動畫
- 如何在Linux中鎖定和解鎖多個使用者Linux