SHRMJavaScriptBridge 0.0.4

SHRMJavaScriptBridge 0.0.4

Maintained by GitWangKai.



  • By
  • 王凯

SHRMJavaScriptBridge

Version Pod License iOS 8.0+ ARC

Link

  • Blog :

《写一个易于维护使用方便性能可靠的Hybrid框架(四)—— 框架构建》

《写一个易于维护使用方便性能可靠的Hybrid框架(三)—— 配置插件》

《写一个易于维护使用方便性能可靠的Hybrid框架(二)—— 插件化》

《写一个易于维护使用方便性能可靠的Hybrid框架(一)—— 思路构建》

  • 注:为解决UIWebView使用JavaScriptCore在最佳时机获取JSContext对象,使用了《UIWebView-TS_JavaScriptContext》分类进行处理。
  • 注:为了解决《UIWebView-TS_JavaScriptContext》上架可能被拒绝的情况,用插件化的方式为UIWebView额外构建了对URL进行拦截方式的通信。

介绍

iOS JS-Native交互框架(支持WKWebView/UIWebView),功能强大,轻松集成,两行代码即可,业务框架分离,易于拓展。 关于通信方案说明:

  • WKWebView使用addScritMessageHandler构建通信,苹果提供的bridge,可以理解为亲儿子,好处自然不用多说。
  • UIWebView使用JavaScriptCore框架构建通信,JavaScriptCore的黑魔法,连RN都没能逃过,功能可见一斑。

架构图

image text

框架类图

image text

特性

  • 支持WKWebView和UIWebView,两行代码即可让WebView能力无限。
  • 针对WKWebView进行了Cookie丢失处理。
  • 针对WKWebView白框架屏问题进行了处理。
  • 针对WKWebView所带来的一些Crash问题进行了容错处理。
  • 插件化JS-Native业务逻辑,业务完全分离,解耦。
  • 基于__attribute( )函数进行插件注册,业务模块的注册只需要在自己内部注册即可,摆脱plist等传统注册方式。目前已知阿里BeeHive/美团Kylin组件皆使用此方式进行注册。目前注册功能为插件是否提前预加载提供。
  • 业务模块回调参框架数给JS侧进行了统一回调处理:业务模块完全不关心是WK or UI。

安装

1.手动导入

下载SHRMJavaScriptBridge文件夹,将SHRMJavaScriptBridge文件夹拖入到你的工程中。

2.CocoaPods

  1. 在 Podfile 中添加 pod 'SHRMJavaScriptBridge', '~> 0.0.3'
  2. 执行 pod installpod update
  3. 导入 <SHRMWebViewEngine.h>

用法

SHRMWebViewEngine是框架的主体类,对外提供两个函数:bindBridgeWithWebView:传入你的webView,setWebViewDelegate:传入你的controller,用来拦截webView的代理函数。具体参照demo。

1.使用框架

// WKWebView
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
webView.navigationDelegate = self;
/***/
SHRMWebViewEngine* bridge = [SHRMWebViewEngine bindBridgeWithWebView:webView];
[bridge setWebViewDelegate:self];
/***/
[self.view addSubview:webView];
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"index.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
if (@available(iOS 9.0, *)) {
[webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
} else {
// Fallback on earlier versions
}
// UIWebView
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.frame];
/***/
SHRMWebViewEngine* bridge = [SHRMWebViewEngine bindBridgeWithWebView:webView];
[bridge setWebViewDelegate:self];
/***/
[self.view addSubview:webView];
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"index1.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
[webView loadRequest:[NSURLRequest requestWithURL:fileURL]];

两种WebView在使用上并无差别,完全兼容。

2.一些针对WKWebView带来的Crash问题处理

2.1 由于WKWebView在请求过程中用户可能退出界面销毁对象,当请求回调时由于接收处理对象不存在,造成Bad Access crash,所以可将WKProcessPool设为单例

static WKProcessPool *_sharedWKProcessPoolInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    _sharedWKProcessPoolInstance = [[WKProcessPool alloc] init];
});
self.processPool = _sharedWKProcessPoolInstance;

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.processPool = self.processPool;

2.2 解决window.alert() 时 completionHandler 没有被调用导致崩溃问题

@property (nonatomic, assign, getter=loadFinished) BOOL isLoadFinished;
- (void)viewDidLoad {
    [super viewDidLoad];
    self.isLoadFinished = NO;
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.isLoadFinished = YES;
}
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {

    if (!self.isLoadFinished) {
        completionHandler();
        return;
    }
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
    if (self)
        [self presentViewController:alertController animated:YES completion:^{}];
    else
        completionHandler();
}

2.3 其他处理参考demo。

3.为了解决UIWebView使用《UIWebView-TS_JavaScriptContext》可能上架被拒绝的问题,又不想丢弃JSContext的通信方式,基于此目的,对UIWebView的通信方式构建了两套,将原有的JSContext通信方式单独拆分为SHRMJSCoreBrdige对象进行处理。另外新增了SHRMUIBaseBridge对象处理URL拦截的通信。并且基于面向协议的方式,将两种通信方式进行了插件化分离,如果不想使用其中的一种,直接将对应的类直接删除即可!不需要额外的操作。例如上架发现《UIWebView-TS_JavaScriptContext》被拒绝,可以直接删除SHRMJSCoreBrdige,不会影响框架使用,直接build即可。然后在SHRMUIWebViewJavaScriptBridge中对通信进行变更,打开注释即可,打开相对应的注视,即代表使用对应的通信方式,其他什么都不需要改变!!!

- (void)setWebViewEngine:(SHRMWebViewEngine *)webViewEngine {
    
//    NSString *defaultUIWebViewBridgeClass = @"SHRMJSCoreBrdige";
    NSString *baseUIWebViewBridgeClass = @"SHRMUIBaseBridge";
    self.UIWebViewBridge = [[NSClassFromString(baseUIWebViewBridgeClass) alloc] init];
    self->_webView.delegate = self.UIWebViewBridge;
    [self.UIWebViewBridge setWebViewEngine:webViewEngine];
    self.UIWebViewBridge.UIWebViewDelegateCalss = self;
}

如果使用了URL拦截的方式进行通信,那么前端的调用方式也需要变更,由原来的:

postUIWebViewParamer(['13383446','SHRMTestUIWebViewPlguin','nativeTestUIWebView',['post','openFile','user']])

变更为:

var command = ['13383446','SHRMTestUIWebViewPlguin','nativeTestUIWebView',['post','openFile','user']];
var json = JSON.stringify(command);
window.location.href = "protocol://#" + json;

即可。

拦截部分的源码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:@"protocol"]) {
        NSString *decodedURL = request.URL.absoluteString.stringByRemovingPercentEncoding;
        NSArray *paramArray = [decodedURL componentsSeparatedByString:@"#"];
        NSString * command = [paramArray lastObject];
        
        NSError* error = nil;
        id object = [NSJSONSerialization JSONObjectWithData:[command dataUsingEncoding:NSUTF8StringEncoding]
                                                    options:NSJSONReadingMutableContainers
                                                      error:&error];
        if (error != nil) {
            NSLog(@"NSString JSONObject error: %@, Malformed Data: %@", [error localizedDescription], self);
        }
        
        if (self.webViewEngine) {
            [self.webViewEngine.webViewhandleFactory handleMsgCommand:(NSArray *)object];
        }
        
        return NO;
    }
    
    if (self.UIWebViewDelegateCalss && [self.UIWebViewDelegateCalss respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [self.UIWebViewDelegateCalss webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    }else {
        return YES;
    }
}

业务插件,不需要改变。

4.自定义业务插件(原生侧)

  1. 创建插件类,继承自SHRMBasePlugin
  2. 插件类里面添加@SHRMRegisterWebPlugin宏,暂时用于插件是否需要提前初始化,加快第一次调用速度。也可以扩充一些其他功能。
  3. 插件里构建业务逻辑,通过sendPluginResult:callbackId:函数把结果回传给JS侧。
#import "SHRMBasePlugin.h"
@interface SHRMTestUIWebViewPlguin : SHRMBasePlugin
- (void)nativeTestUIWebView:(SHRMMsgCommand *)command;
@end
@SHRMRegisterWebPlugin(SHRMTestUIWebViewPlguin, 1)

@implementation SHRMTestUIWebViewPlguin
- (void)nativeTestUIWebView:(SHRMMsgCommand *)command {
    NSString *method = [command argumentAtIndex:0];
    NSString *url = [command argumentAtIndex:1];
    NSString *param = [command argumentAtIndex:2];
    NSLog(@"(%@):%@,%@,%@",command.callbackId, method, url, param);

    SHRMPluginResult *result = [SHRMPluginResult resultWithStatus:SHRMCommandStatus_OK messageAsString:@"uiwebview test success!"];
    [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
@end

这样一个插件就定义完毕了:插件的意思就是独立的,与其他功能模块无耦合的业务模块,用来处理一类JS-Native的交互。例如JS想要获取地图信息、wifi信息、文件处理等都可以定义为一个插件。插件的好处就是无耦合!!!拖入项目可以直接使用,删除后项目也不需要做任何修改,直接build!以后再有新的交互需求你只需要按照上面的步骤创建插件完成功能并把结果返回给JS就可以了!不需要动框架!!

5.自定义业务插件(JS侧)

JS侧目前还没有开放插件化功能,只是说还不够完善,但它不影响功能使用:

  1. 如果JS加载在WKWebView,JS调用Native通过
window.webkit.messageHandlers.SHRMWKJSBridge.postMessage(['13383445','SHRMFetchPlugin','nativeFentch',['post','https:www.baidu.com','user']])

即可。 ['post','https:www.baidu.com','user']为想要传递的参数。 13383445为此次通信ID,ID可不传,为后续JS侧插件化预留。 SHRMFetchPlugin为Native侧插件类名。 nativeFentch为插件方法名。

  1. 如果JS加载在UIWebView,JS调用Native通过
postUIWebViewParamer(['13383446','SHRMTestUIWebViewPlguin','nativeTestUIWebView',['post','openFile','user']])

即可,其中['post','openFile','user'] 依旧为你想要传递的参数,另外三个参数含义同上。

  1. 详细使用参照Demo。

后续功能延伸:

  1. JS侧插件化。
  2. 基于此引入离线包。
  3. 引入flutter。
  4. 构建小程序。
  5. other ...

License

SHRMJavaScriptBridge is available under the Apache License 2.0. See the LICENSE file for more info.