寫一個易於維護使用方便效能可靠的Hybrid框架(二)—— 外掛化

Sevin發表於2018-12-07

《寫一個易於維護使用方便效能可靠的Hybrid框架(一)—— 思路構建》

《寫一個易於維護使用方便效能可靠的Hybrid框架(二)—— 外掛化》

《寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛》

《寫一個易於維護使用方便效能可靠的Hybrid框架(四)—— 框架構建》

前言

繼上一篇之後,我反覆思來想去,我下一篇該怎麼寫,那麼想法有了,我應該怎麼去落實,框架在程式碼層面我要怎麼設計,怎麼樣才能使用起來儘可能的方便,那麼好吧,我深深的覺得,上一篇我給自己挖了個大坑,最近的思想一直依託於Cordova框架的設計模式,說實話想跳出來很難,我真的很難很難想出一個比它外掛化部分更好設計,所以外掛化這一塊,我依舊延續Cordova思想,精簡掉平時工作用不到的部分,偏向於更方便的方向設計,所以,今天我寫了個簡短的demo,基本實現了js和native端的通訊,下面依託demo來寫我的Hybrid框架第二篇,先聊聊native端外掛化部分。

正題

在框架使用層面和js-bridge方面,依舊延續上篇提到的兩篇博文,也就是框架內不提供webView,webView由使用者自己實現,webView的一切我不關心,我只負責給你的webView提供Hybrid能力。第二點是通訊上基於WKWebView的addScriptMessageHandler方式,棄用UIWebView的URL攔截方式。

還是簡單說一下WKWebView的addScriptMessageHandler的通訊問題,只是簡單介紹下就不詳細講解它的通訊過程了,WKWebView初始化時,有一個引數叫configuration,它是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.postMessagefetchComplete後期我們會把他們單獨封裝起來,對於前端開發者來說只會給他們統一的呼叫介面和回撥介面,這個後面再說。如果說只是為了簡單實現功能其實到這裡就可以了,但是畢竟我們是在構建一個框架,所以這樣是遠遠不行的,二篇就先到這吧(PS:主要是demo就寫到了這,另外時候不早了得睡了)。

寫一個易於維護使用方便效能可靠的Hybrid框架(二)—— 外掛化

總結

總結一下,本篇簡單實現了native端的外掛化,基於WKWebView通訊的構建,框架內不提供webView的實現三個功能這與我們上一篇的預期也是相符合的,但是遠遠不夠,那麼我們後續第三篇再繼續。那麼下篇會著重介紹native端外掛的可配置和js端介面的封裝(這點真是難為了我這個未入門的前端了)。

相關文章