寫一個易於維護使用方便效能可靠的Hybrid框架(一)—— 思路構建
寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛
前言
繼上一篇之後,我反覆思來想去,我下一篇該怎麼寫,那麼想法有了,我應該怎麼去落實,框架在程式碼層面我要怎麼設計,怎麼樣才能使用起來儘可能的方便,那麼好吧,我深深的覺得,上一篇我給自己挖了個大坑,最近的思想一直依託於Cordova框架的設計模式,說實話想跳出來很難,我真的很難很難想出一個比它外掛化部分更好設計,所以外掛化這一塊,我依舊延續Cordova思想,精簡掉平時工作用不到的部分,偏向於更方便的方向設計,所以,今天我寫了個簡短的demo,基本實現了js和native端的通訊,下面依託demo來寫我的Hybrid框架第二篇,先聊聊native端外掛化部分。
正題
在框架使用層面和js-bridge方面,依舊延續上篇提到的兩篇博文,也就是框架內不提供webView,webView由使用者自己實現,webView的一切我不關心,我只負責給你的webView提供Hybrid能力。第二點是通訊上基於WKWebView的addScriptMessageHandler
方式,棄用UIWebView的URL攔截方式。
還是簡單說一下WKWebView的addScriptMessageHandler
的通訊問題,只是簡單介紹下就不詳細講解它的通訊過程了,WKWebView初始化時,有一個引數叫configuratio
n,它是WKWebViewConfiguration
型別的引數,而WKWebViewConfiguration
有一個屬性叫userContentController
,它又是WKUserContentController
型別的引數。WKUserContentController
物件有一個方法-addScriptMessageHandler:name:
,第一個引數是userContentController
的代理物件,第二個引數對應著js端postMessage
的物件(本篇看完你就明白了沒啥說的)。既然設定了代理物件,當然還要實現它的代理方法,也就是WKScriptMessageHandler
協議提供的userContentController:didReceiveScriptMessage:
函式,有了他倆,我們就可以進行通訊了,WKWebView通訊就是這麼簡單。
先看一下我的Hybrid框架是怎麼使用的,當然思想還是基於上篇提到的大佬,直接看程式碼吧:
#import "ViewController.h"
#import <WebKit/WebKit.h>
#import "SHRMWebViewEngine.h"
@interface ViewController ()<WKNavigationDelegate>
@property (strong, nonatomic) WKWebView *webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
self.webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
/***/
SHRMWebViewEngine *jsBridge = [[SHRMWebViewEngine alloc] init];
jsBridge.delegate = self;
[jsBridge bindBridgeWithWebView:self.webView];
/***/
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"index.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
if (@available(iOS 9.0, *)) {
[self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
} else {
// Fallback on earlier versions
}
[self.view addSubview:self.webView];
}
@end
複製程式碼
這其實應該是框架使用者要編寫的程式碼,看上去很常規,中間多了三行程式碼,SHRMWebViewEngine *jsBridge = [[SHRMWebViewEngine alloc] init];jsBridge.delegate = self;[jsBridge bindBridgeWithWebView:self.webView];
,實際上這三行程式碼就讓你的webView具有了Hybird的能力了,用起來是不是很方便,其實這沒啥說的,這個思想也是之前大佬提過的了,我就當是做了個總結吧。那再通過程式碼看一下我在裡面都做了什麼吧。
#import "SHRMWebViewEngine.h"
#import "SHRMWebViewDelegate.h"
#import "SHRMWebViewHandleFactory.h"
@interface SHRMWebViewEngine ()
@property (nonatomic, strong) SHRMWebViewDelegate *webViewDelegate;
@property (nonatomic, strong) SHRMWebViewHandleFactory *webViewhandleFactory;
@end
@implementation SHRMWebViewEngine
- (instancetype)init {
if (self = [super init]) {
_webViewhandleFactory = [[SHRMWebViewHandleFactory alloc] initWithWebViewEngine:self];
_webViewDelegate = [[SHRMWebViewDelegate alloc] initWithWebViewEngine:self];
}
return self;
}
- (void)bindBridgeWithWebView:(WKWebView *)webView {
self.webView = webView;
if (![_delegate conformsToProtocol:@protocol(WKUIDelegate)]) {
self.webView.UIDelegate = _webViewDelegate;
}
if (![_delegate conformsToProtocol:@protocol(WKNavigationDelegate)]) {
self.webView.navigationDelegate = _webViewDelegate;
}
webView.configuration.userContentController = [[WKUserContentController alloc] init];
[webView.configuration.userContentController addScriptMessageHandler:self name:@"SHRMWKJSBridge"];
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.body isKindOfClass:[NSArray class]]) {
[_webViewhandleFactory handleMsgCommand:message.body];
}
}
#pragma mark - SHRMWebViewProtocol
- (void)sendPluginResult:(NSString *)result callbackId:(NSString*)callbackId {
NSString *jsStr = [NSString stringWithFormat:@"fetchComplete('(%@)','%@')",callbackId,result];
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@",result, error);
}];
}
- (void)runInBackground:(void (^)(void))block {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
#pragma mark - dealloc
- (void)dealloc {
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"SHRMWKJSBridge"];
}
@end
複製程式碼
1.
SHRMWebViewHandleFactory
物件,負責執行native端外掛的呼叫過程,內部將來會做成工廠。
2.
SHRMWebViewDelegate
物件,負責WKWebView的代理實現,我的思路是如果開發者沒有自己實現WKWebView的代理,那麼預設我會走框架內提供的代理方法,包括進度條等。
3.
bindBridgeWithWebView:
函式做了webView的繫結和代理的繫結,主要還是為了拿到想要獲得Hybrid能力的webView。拿到這個webView,我們就可以做接下來的通訊了。SHRMWKJSBridge
為自定義的js端postMessage的物件(和上面提到的匹配上了),是jsBridge的統一入口,這個SHRMWKJSBridge
一會下面還會說到,再對比下就更清楚了。
4.
userContentController:didReceiveScriptMessage:
拿到js傳遞過來的引數,引數怎麼傳遞過來的一會會粘js端的程式碼。
5.
SHRMWebViewProtocol:
提供了兩個介面,一個是native回撥js介面,一個是開闢子執行緒執行耗時操作介面。通過程式碼可以看到native回撥js使用的是evaluateJavaScript:completionHandler:
函式,這是WKWebView之後提供的,天然非同步執行,不需要像UIWebView那樣要開發者手動處理js端回撥native端的非同步問題。
那麼實際上SHRMWebViewEngine
核心類目前只做了這幾件事情,當然這只是我今天寫的demo,後續會對程式碼進行優化和對通訊調優,我們先一步一步來。
@protocol SHRMWebViewProtocol <NSObject>
/**
native call back js
@param result simulate data
@param callbackId callbackId
*/
- (void)sendPluginResult:(NSString *)result callbackId:(NSString*)callbackId;
/**
background
@param block long running
*/
- (void)runInBackground:(void (^)(void))block;
@end
複製程式碼
這個就是我們剛才看到的介面定義的地方,今天一直在想,Cordova在外掛處理的時候,我們自定義外掛都是需要繼承自CDVPlugin
基類的(沒用過沒關係,就理解為一個提供了js回撥native介面的基類就行了),它的回撥介面實現類CDVPlugin基類裡面有提供,所以它可以通過CDVPlugin基類來進行native端對js的回撥。關於這一塊我做了簡單改造,移除了CDVPlugin基類,因為我們的外掛完全可以不用繼承任何其他的類,只需要繼承自NSObject就好,還是看程式碼:
@class SHRMMsgCommand;
@interface SHRMFetchPlugin : NSObject
- (void)nativeFentch:(SHRMMsgCommand *)command;
@end
@implementation SHRMFetchPlugin
- (void)nativeFentch:(SHRMMsgCommand *)command {
NSString *method = [command argumentAtIndex:0];
NSString *url = [command argumentAtIndex:1];
NSString *param = [command argumentAtIndex:2];
NSLog(@"(%@):%@,%@,%@",command.callbackId, method, url, param);
[command.delegate sendPluginResult:@"fetch success" callbackId:command.callbackId];
}
@end
複製程式碼
不可避免,我還是需要SHRMMsgCommand
這個物件,不然外掛無法和框架構成聯絡。SHRMMsgCommand
上面沒有說幹嘛的,它實際上只是js傳遞過來的引數接受者,儲存著引數資訊供外掛使用。command.delegate
實際是id <SHRMWebViewProtocol> delegate
型別的物件,因為我目前是把回撥介面是現在了SHRMWebViewEngine
裡面,實際上這個delegate就是它。這麼做的目的主要是為了解耦合,這樣SHRMMsgCommand
完全不必引用SHRMWebViewEngine
的標頭檔案了。這個就是模擬網路請求native端提供的外掛,什麼是外掛,顧名思義,就是不跟框架產生耦合,實際上框架內是不需要引入SHRMFetchPlugin的,如果說哪天這個外掛不用了,直接把這個類刪了就可以了。
到這裡我們在native端的簡單外掛化基本實現了,js與native間基於WKWebView的通訊也實現了。這樣我在js端直接呼叫
window.webkit.messageHandlers.SHRMWKJSBridge.postMessage(['13383445','SHRMFetchPlugin','nativeFentch',['post','https:www.baidu.com','user']]);
複製程式碼
就可以實現js call native了,SHRMWKJSBridge
(這個到這裡就很明白了吧)就是我上面定義的傳送postMessage
的物件,當然native回撥js是需要js提供一個全域性函式供給native來呼叫的,我在demo裡面定義了一個fetchComplete
函式:
function fetchComplete(id,result) {
asyncAlert(result);
document.getElementById("returnValue").value = (id) + result;
}
function asyncAlert(content) {
setTimeout(function(){
alert(content);
},1);
}
複製程式碼
再來看下上面回撥js的程式碼:
- (void)sendPluginResult:(NSString *)result callbackId:(NSString*)callbackId {
NSString *jsStr = [NSString stringWithFormat:@"fetchComplete('(%@)','%@')",callbackId,result];
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@",result, error);
}];
}
複製程式碼
一目瞭然了吧,這樣整個呼叫過程就結束了,當然window.webkit.messageHandlers.SHRMWKJSBridge.postMessage
和fetchComplete
後期我們會把他們單獨封裝起來,對於前端開發者來說只會給他們統一的呼叫介面和回撥介面,這個後面再說。如果說只是為了簡單實現功能其實到這裡就可以了,但是畢竟我們是在構建一個框架,所以這樣是遠遠不行的,二篇就先到這吧(PS:主要是demo就寫到了這,另外時候不早了得睡了)。
總結
總結一下,本篇簡單實現了native端的外掛化,基於WKWebView通訊的構建,框架內不提供webView的實現三個功能這與我們上一篇的預期也是相符合的,但是遠遠不夠,那麼我們後續第三篇再繼續。那麼下篇會著重介紹native端外掛的可配置和js端介面的封裝(這點真是難為了我這個未入門的前端了)。