当前位置:   article > 正文

android 6.0 webview 加载js_Flutter下实现WebView拦截加载离线资源

flutter inappwebview 拦截js

c161b82a2ab8caf774e74e8a2a4a40f2.png

秦子帅 明确目标,每天进步一点点..... 777e9b13cfa4133873e991688a9ef789.png

作者 |  星星y

地址 |  https://www.jianshu.com/p/dae373ca9f0f

前言

哎,Flutter真香啊

在用移动设备加载H5页面时,一些公共资源如css,js,图片等如果比较大时,就需要通过拦截网络,改从本地资源加载。在Android原生WebView中,我们可以在WebViewClient中的shouldInterceptRequest方法来拦截替换资源。
然而在Flutter中的WebView插件,不管是官方的webview_flutter,还是flutter_webview_plugin都不支持加载本地资源。
庆幸的是webview_flutter底层实现是基于WebView(Android)和WKWebView(iOS)。只要在官方webview_flutter上稍作修改,就可以实现离线资源加载。

项目地址

github: https://github.com/iamyours/webview_flutter 

pub:https://pub.dev/packages/iwebview_flutter

Android端实现

首先我们从webview_flutter中下载最新Archive(当前使用0.3.15+1)。https://pub.dev/packages/webview_flutter/versions解压后,使用AndroidStudio打开,右键工程目录,使用Android模式打开

2f13026a8ef677e5cbaab9164322d175.png

如果要实现WebView请求拦截,就必须给webView设置WebViewCilent,全局搜索setWebViewClient找到只有一处实现:

//FlutterWebView.javaprivate void applySettings(Map<String, Object> settings) {for (String key : settings.keySet()) {switch (key) {
            ...case "hasNavigationDelegate":
                final boolean hasNavigationDelegate = (boolean) settings.get(key);
                final WebViewClient webViewClient =
                        flutterWebViewClient.createWebViewClient(hasNavigationDelegate);
                webView.setWebViewClient(webViewClient);break;
            ...
        }
    }
}

修改WebViewClient

通过以上代码我们知道具体逻辑在createWebViewClient方法中:

WebViewClient createWebViewClient(boolean hasNavigationDelegate) {this.hasNavigationDelegate = hasNavigationDelegate;if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {return internalCreateWebViewClient();
    }return internalCreateWebViewClientCompat();
}

然后在internalCreateWebViewClientinternalCreateWebViewClientCompat添加shouldInterceptRequest方法,然后参照onPageFinishedFlutterWebViewClient加入shouldInterceptRequest方法:

private WebViewClient internalCreateWebViewClient() {return new WebViewClient() {
        ...@Overridepublic void onPageFinished(WebView view, String url) {
            FlutterWebViewClient.this.onPageFinished(view, url);
        }
        ...//参照@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            WebResourceResponse response = FlutterWebViewClient.this.shouldInterceptRequest(view, url);if (response != null) return response;return super.shouldInterceptRequest(view, url);
        }
    };
}

异步变同步,MethodChannel接收Flutter层数据

我们在shouldInterceptRequest中接收来自Flutter世界的数据,如assets中的二进制数据。但是要注意的是通过MethodChannel接收数据是通过异步回调的形式,但是shouldInterceptRequest方法需要同步接收数据,因此需要一个异步变同步的执行器,同时MethodChannel调用必须在主线呈调用。方法有很多,我这里通过CountDownLatch实现。

public class SyncExecutor {private final CountDownLatch countDownLatch = new CountDownLatch(1);
    Handler mainHandler = new Handler(Looper.getMainLooper());
    WebResourceResponse res = null;public WebResourceResponse getResponse(final MethodChannel methodChannel, final String url) {
        res = null;
        mainHandler.post(new Runnable() {@Overridepublic void run() {
                methodChannel.invokeMethod("shouldInterceptRequest", url, new MethodChannel.Result() {@Overridepublic void success(Object o) {if (o instanceof Map) {
                            Map<String, Object> map = (Map<String, Object>) o;
                            byte[] bytes = (byte[]) map.get("data");String type = (String) map.get("mineType");String encode = (String) map.get("encoding");
                            res = new WebResourceResponse(type, encode, new ByteArrayInputStream(bytes));
                        }
                        countDownLatch.countDown();
                    }@Overridepublic void error(String s, String s1, Object o) {
                        res = null;
                        countDownLatch.countDown();
                    }@Overridepublic void notImplemented() {
                        res = null;
                        countDownLatch.countDown();
                    }
                });
            }
        });try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }return res;
    }
}

注意到这里success中接收到Map数据,我们会在接下来Flutter层传过来。


Flutter层传递数据

webview_method_channel.dart中我们找到了onPageFinished接收来自Android或iOS的调用。参照onPageFinished方法,我们加入shouldInterceptRequest方法,同样的在_platformCallbacksHandler对应的类中加入shouldInterceptRequest方法。依次向上层类推。

Future<dynamic> _onMethodCall(MethodCall call) async {switch (call.method) {case 'onPageFinished':
        _platformCallbacksHandler.onPageFinished(call.arguments['url']);return null;case 'shouldInterceptRequest':
        String url = call.arguments;var response = await _platformCallbacksHandler.shouldInterceptRequest(url);if (response != null) {return {"data": response.data, "mineType": response.mineType, "encoding": response.encoding};
        }return null;
    }

//webview_method_channel.dartabstract class WebViewPlatformCallbacksHandler {
  ...void onPageFinished(String url);/// iamyours:Invoked by [WebViewPlatformController] when a request url intercepted.FutureshouldInterceptRequest(String url);
}

//webview_method_channel.dartclass Response {final String mineType;final String encoding;final Uint8List data;
  Response(this.mineType, this.encoding, this.data);
}typedef void PageFinishedCallback(String url);/// iamyours Signature for when a [WebView] interceptRequest .typedef FutureShouldInterceptRequestCallback(String url);class WebView extends StatefulWidget {
  ...const WebView({
    ...this.onPageFinished,this.shouldInterceptRequest,
    ...,
  })
class _WebViewState extends State{
    ...@overridevoid onPageFinished(String url) {if (_widget.onPageFinished != null) {
      _widget.onPageFinished(url);
    }
  }
    ...@overrideFutureshouldInterceptRequest(String url) async{if (_widget.shouldInterceptRequest != null) {return _widget.shouldInterceptRequest(url);
    }return null;
  }
}

然后我们在example中实现一个简单的logo替换效果

WebView(
  initialUrl: "https://wap.sogou.com/",
  javascriptMode: JavascriptMode.unrestricted,
  debuggingEnabled: true,
  onProgressChanged: (int p){
    setState(() {
      progress = p/100.0;
    });
  },
  backgroundColor: Colors.red,
  shouldInterceptRequest: (String url) async {//替换搜狗搜索logo为baiduvar googleLogo = "https://wap.sogou.com/resource/static/index/images/logo_new.6f31942.png";
    print("============url:$url");if (url == googleLogo) {
      ByteData data = await rootBundle.load("assets/baidu.png");
      Uint8List bytes = Uint8List.view(data.buffer);return Response("image/png", null, bytes);
    }return null;
  },
),

最终效果

bce26720522639c372b2835014f4ed80.png


iOS端实现

NSURLProtocol拦截请求

webview_flutteriOS端是基于WKWebview实现的,拦截请求通过NSURLProtocol实现,可以参照iOS WKWebView (NSURLProtocol)拦截js、css,图片资源一文。此法拦截是全局拦截的,所以需要一个全局变量存储所有的FlutterMethodChannel,这里定义一个单例存储这些数据。

//FlutterInstance.h#import #import NS_ASSUME_NONNULL_BEGIN@interface FlutterInstance : NSObject@property(nonatomic,retain)NSMutableDictionary *channels;
+(FlutterInstance*)get;
+(FlutterMethodChannel*)channelWithAgent:(NSString*)agent;
+(void)removeChannel:(int64_t)viewId;@endNS_ASSUME_NONNULL_END

这里为了区分对应的请求是在哪个channel下的,我们在给相应的WKWebviewagent最后加入#_viewId

//// FlutterInstance.m#import "FlutterInstance.h"@implementation FlutterInstancestatic FlutterInstance *instance = nil;
+(FlutterInstance *)get
{@synchronized(self)
    {if(instance==nil)
        {
            instance= [FlutterInstance new];
            instance.channels = [NSMutableDictionary dictionary];
        }
    }return instance;
}
+(FlutterMethodChannel*)channelWithAgent:(NSString*)agent{NSRange range = [agent rangeOfString:@"#" options:NSBackwardsSearch];NSLog(@"range:%d,%d",range.length,range.location);NSString *key = [agent substringFromIndex:range.location+1];NSDictionary *channels = [self get].channels;
    FlutterMethodChannel *channel = (FlutterMethodChannel*)[channels objectForKey:key];return channel;
}
+(void)removeChannel:(int64_t)viewId{NSMutableDictionary *channels = [self get].channels;NSString *key = [NSString stringWithFormat:@"%lld",viewId];
    [channels removeObjectForKey:key];
}@end

userAgent区分MethodChannel

我们在WKWebviewloadUrl中修改userAgent区分各个WebView对应的viewId

//FlutterWebView.m
- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {NSURL* nsUrl = [NSURL URLWithString:url];if (!nsUrl) {return false;
  }NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];NSString *vid = [NSString stringWithFormat:@"%lld",_viewId];
    [_webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {NSString *fixAgent = [NSString stringWithFormat:@"%@#%d",result,_viewId];
        [_webView setCustomUserAgent:fixAgent];
    }];
  [_webView loadRequest:request];return true;
}

NSURLProtocol实现请求拦截

然后在NSURLProtocol协议中的startLoading方法实现请求拦截

// FlutterNSURLProtocol.h#import #import NS_ASSUME_NONNULL_BEGIN@interface FlutterNSURLProtocol : NSURLProtocol@endNS_ASSUME_NONNULL_END

// FlutterNSURLProtocol.m
- (void)startLoading
{NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];//给我们处理过的请求设置一个标识符, 防止无限循环,
    [NSURLProtocol setProperty:@YES forKey:KFlutterNSURLProtocolKey inRequest:mutableReqeust];NSString *agent = [mutableReqeust valueForHTTPHeaderField:@"User-Agent"];
    FlutterMethodChannel *channel = [FlutterInstance channelWithAgent:agent];if(channel==nil){NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];self.task = [session dataTaskWithRequest:self.request];
        [self.task resume];return;
    }
    [channel invokeMethod:@"shouldInterceptRequest" arguments:url result:^(id  _Nullable result) {if(result!=nil){NSDictionary *dic = (NSDictionary *)result;
            FlutterStandardTypedData *fData = (FlutterStandardTypedData *)[dic valueForKey:@"data"];NSString *mineType = dic[@"mineType"];NSString *encoding = dic[@"encoding"];if([encoding isEqual:[NSNull null]])encoding = nil;NSData *data = [fData data];NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:mineType expectedContentLength:data.length textEncodingName:encoding];
            [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
            [self.client URLProtocol:self didLoadData:data];
            [self.client URLProtocolDidFinishLoading:self];
        }else{NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];self.task = [session dataTaskWithRequest:self.request];
            [self.task resume];
        }
    }];
}

通过之前设置的userAgent获取相应的FlutterMethodChannel,调用shouldInterceptRequest方法获取Flutter数据,通过Xcode调试,我们知道相应的byte数据类型为FlutterStandardTypedData。或者参照下图:

e6405976cf2441f1028d638ddae6327f.png

具体效果

实现google搜索logo替换

4d9460da0a6c991164c6425633220762.png

---END---

15e8bffb4d96ed85ba5d88741079713a.png

 创作不易,点个“ 在看 c60aeb8b91a766fdaf313fd0fab43cc9.gif
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/106024
推荐阅读
相关标签
  

闽ICP备14008679号