寫一個易於維護使用方便性能可靠的Hybrid框架(二)—— 插件化

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

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

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

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

前言

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

正題

在框架使用層面和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通訊就是這麼簡單。java

先看一下個人Hybrid框架是怎麼使用的,固然思想仍是基於上篇提到的大佬,直接看代碼吧:web

#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端插件的調用過程,內部未來會作成工廠。bash

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就寫到了這,另外時候不早了得睡了)。

總結

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