市面上的App大致分为3类::Native App、Web App和Hybrid App,Hybrid App兼具"Native App"良好的用户交互体验和"Web App"跨平台开发的优势
Webview解析
解决webview加载劣势——资源预加载:
资源加载缓慢: H5页面是从服务器上下发的,客户端的页面在内存里面,加载网页的时间更长,而且受限于网络情况,但是这种问题在某种程度上市可以弥补的,比如我们可以做一些资源预加载方案,下面列举资源预加载:
一、使用webview自身的缓存机制: 如果我们在App里面访问一个页面,短时间内再次访问这个页面,就会感觉第二次打开的时候顺畅很多,加载速度比第一次的时间要短,这个就是因为webview自身内部会做一些缓存,只要打开过得资源,他都会试着缓存到本地,第二次需要访问的时候它直接从本地读取,但这个读取其实是不太稳定的东西,关掉之后,或者说这种缓存失效之后,系统会自动把他清楚。我们在应用启动的时候开一个像素的webview,事先访问以下我们常用的资源,后续打开页面的时候如果再用到这些资源他就可以从本地读取,页面加载的时间会短一些。
二、自己去构建,自己管理缓存: 把这些需要预加载的资源放在App里面,他可能是预先放进去的,也可能是后续下载的,问题在于前端这些页面怎么去缓存,两个方案,第一种是前端可以在H5打包的时候把里面的资源URL进行替换,这样可以直接访问本地地址;第二种是客户端可以拦截这些网页发出的所有请求做替换:美团就是使用的的第二种预加载方案: 详情看美团大众点评Hybrid化建设,实现原理:每当webview发起资源请求的时候,我们会拦截这些资源的请求,去本地检查一下我们这些静态资源本地离线包有没有。针对本地的缓存文件我们有些策略能够及时的去更新它,为了安全考虑,也需要同时做一些预加载和安全包的加密工作。预加载有以下几点优势:
1、我们拦截了webview里面发出的所有的请求,但是并没有替换里面的前端应用的任何代码,前端这套页面代码可以在App内,或者其他的App里面都可以直接访问,他不需要为我们App做定制化的东西
2、这些URL请求,他会直接带上先前用户操作留下的Cookie,因为我们没有更改资源原始URL地址;
3、整个前端在用离线包和缓存文件的时候是完全无感知的,前端只用管写一个自己的页面,客户端会帮他处理好这样一些静态资源预加载的问题,有这个离线包的话加载速度会变快很多,特别是弱网情况下,没有这些离线包加载速度会慢一些。而且如果本地离线包的版本不能跟H5匹配的话,H5页面也不会发生什么问题。
WebView的常见设置:
WebSetting webSettings=webView.getSettings();
//设置这个属性为true允许webview和js代码进行交互,这个本身会有漏洞
webSettings.setJavaScriptEnabled(true);
//设置WebView是否可以打开WebView新窗口
webSettings.setJavaScriptCanOpenWindowAutomatically(true);
//webview是否支持多窗口,如果设置未true,需要重写
//WebChromeClient#onCreateWindow(WebView,boolean,boolean,Message)函数,默认为false
webSettings.setSuppportMutipleWindows(true);
//这个属性用来设置webview是否能够加载图片资源,包括哪些使用data uri协议嵌入的图片。使用setBlockNetworkImage(boolean)方法来控制仅仅加载使用网络URI协议的图片,需要提到的一点是如果这个设置从false变为true之后,所有被内容引用的正在显示的webview图片资源都会被自动加载,该标识默认值为true。
webSettings.setLoadsImagesAutomatically(false);
//标识是否加载网络上的图片(使用http或者https域名的资源),需要注意的是如果getLoadsImageAutomatically不返回true,这个标识将没有作用
webSettings.setBlockNetworkImage(boolean)
//显示webView提供的缩放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true)
//设置是否启动WebView API,默认值为false
webSettings.setDatabaseEnabled(true);
//打开webview的storage功能,这样JS的localStorage,sessionStorage对象才可以使用(比如一张网页的拼图js页面)
webSettings.setDomStorageEnabled(true);
//打开WebView的LBS功能,这样JS的geolocation对象才可以使用
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");
//设置是否打开webview表单数据的保存功能
webSettings.setSaveFormData(true);
//设置webview的默认的userAgent字符串
webSettings.setUserAgentString("");
//设置是否WebView支持"viewport"的HTML meta tag,这个标识用来屏幕自适应的,当这个标识设置为false时,页面布局的宽度被一直设置为css中控制的webview的宽度;如果设置为true并且页面含有viewport meta tag,那么被这个tag声明的宽度将会被使用。
//webSettings.setUseWideViewPort(false);
//设置webview的字体,可以通过这个函数,改变webview的字体,默认字体为"sans-serif"
webSettings.setStandardFontFamily("");
//设置webview字体的大小,默认大小为16
webSettings.setDefaultFontSize(20);
//设置webview支持的最小字体大小,默认为8
webSettings.setMinimumFontSize(12);
//设置页面是否支持缩放
webSettings.setSupportZoom(true);
//设置文本的缩放倍数,默认为100
webSettings.setTextZoom(2);
然后还有最常用的 WebViewClient 和 WebChromeClient,WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:
-
onLoadResource
-
onPageStart
-
onPageFinish
-
onReceiveError
-
onReceivedHttpAuthRequest
-
shouldOverrideUrlLoading
WebChromeClient 主要辅助 WebView 处理J avaScript 的对话框、网站 Logo、网站 title、load 进度等处理:
-
onCloseWindow(关闭WebView)
-
onCreateWindow
-
onJsAlert
-
onJsPrompt
-
onJsConfirm
-
onProgressChanged
-
onReceivedIcon
-
onReceivedTitle
-
onShowCustomView
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
}
...
//特定版本下会存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "myObj");
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}
第二种方式: 利用WebViewClient接口回调方法拦截url: 这种方式使用频次也很高,上面介绍的WebViewClient,其中有个回调接口shouldOverrideUrlLoading(WebView view,String url),我们利用这个拦截url,然后解析这个url的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑,
注意:这个方法在API24版本已经废弃了,需要使用shouleOverrideUrlLoading(WebView view,WebResourceRequest request)
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议
if (scheme.equals("js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//代表应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
js中的代码指定location地址:
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
webview通过shouldOverrideUrlLoading拦截js相关数据并做处理,处理完成后如果web端想要得到方法的返回值,只能通过webview的loadUrl方法去执行js方法把返回值传递回去,相关代码如下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");//javascript
function returnResult(result){
alert("result is" + result);
}
备注:这种方式打开Native页面还是很合适的,制定好相应的协议,就能够让web端具有打开所有本地页面的能力了。
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(message);
String scheme = uri.getScheme();
if (scheme.equals("js")) {
if (uri.getAuthority().equals("openActivity")) {
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
//代表应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
通过WebChromeClient接口,拦截JS中的几个提示方法,也就是几种样式的对话框,在JS中有三个常用的对话框方法:
function clickprompt(){
var result=prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
//java
mWebView.loadUrl("javascript:show(" + result + ")");//javascript
<script type="text/javascript">
function show(result){
alert("result"=result);
return "success";
}
</script>
注意,调用的名字一定要对应上,要不然调用不成功,而且js的调用一定要在onPageFinished函数回调之后才能调用,要不然会失败。
final int version = Build.VERSION.SDK_INT;
if (version < 18) {
mWebView.loadUrl(jsStr);
} else {
/*jsStr是javascript执行脚本,ValueCallBack是执行完脚本的返回值,也可能是null,在没有返回的情况*/
mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}
参考自微信公众号——App架构师,Android WebView详解