助你快速搭建一个健壮可控的WebApp

  笔者因公司需求,从0打造一款WebApp,一直维护到现在。整个接口算是从混乱到现在的有序。笔者也从一个WebView+H5的小菜鸟,磨炼成了中等生。   WebApp简单来讲,就是利用原生的WebView承载H5的html页面,并且实现JS和原生之间的通信。   WebApp的好处是显而易见的。业务页面来源于H5,原生作为一个承载壳提供流畅性支持,能够低成本的实现跨平台的实施以及快速嵌入微信小程序、钉钉、OA等APP中。与纯H5的App相比较,它能够更轻易的使用原生底层库,并且更加流畅;而与纯原生的相比较,它实现了跨平台,能够通过H5的特性快速嵌套进其他APP中。

核心类:

image.png

WebViewActivity:

public class WebViewActivity extends Activity {
    private final String TAG = "WebViewActivity";
    private final String PRE = "protocol://android";
    private WebView webView;
    private String url = "http://222.211.90.120:6071/td_mobile/mingtu/login/html/login.html";
    private CustomWebChromeClient webChromeClient;
    private NullPageControll nullPageControll;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        webView = findViewById(R.id.webview);
        nullPageControll=new NullPageControll(this,webView);
        WebSettingUtil.getInstance(webView,this).initWebSetting();
        webView.loadUrl(url);
        webView.setWebViewClient(new CustomWebViewClient(PRE,this,nullPageControll));
        webChromeClient=new CustomWebChromeClient(this,nullPageControll);
        webView.setWebChromeClient(webChromeClient);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        WebViewH5Order.parseOnActivityForResult(webView,this,requestCode,resultCode,intent);
        webChromeClient.forActivityResult(requestCode,resultCode,intent);
    }
}

  我们的WebView页面,这里是所有拓展设置的入口,为了尽量减少这个类的代码量,让其清晰,所以分离出了拓展设置,形成了核心类中的其他内容。 &emsp: PRE:该常量可以理解为一个规则,在JS和原生通信的时候作为唯一标识

WebSettingUtil:

public class WebSettingUtil {
    private static WebSettingUtil webSettingUtil;
    private static WebView webView;
    private static Context context;
    private WebSettings webSettings;
    private WebSettingUtil(){
    }
    public static synchronized WebSettingUtil getInstance(WebView webView, Context context){
        if(webSettingUtil==null) webSettingUtil=new WebSettingUtil();
        WebSettingUtil.context=context;
        WebSettingUtil.webView=webView;
        return webSettingUtil;
    }
    public void initWebSetting(){
        webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);//设置支持javascrip
        webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//设置缓存模式
        webSettings.setDomStorageEnabled(true);//是否支持持久化存储,保存到本地
        webSettings.setAppCacheMaxSize(1024 * 1024 * 8);
        String appCachePath = context.getCacheDir().getAbsolutePath();
        webSettings.setAppCachePath(appCachePath);//设置数据库缓存路径
        webSettings.setAllowFileAccess(true);
        webSettings.setDatabaseEnabled(true);//开启database storage API功能
        webSettings.setAppCacheEnabled(true);//设置开启Application H5 Caches功能
        if (Build.VERSION.SDK_INT >= 19) {
            webSettings.setLoadsImagesAutomatically(true);
        } else {
            webSettings.setLoadsImagesAutomatically(false);
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            webSettings.setDatabasePath("/data/data/" + webView.getContext().getPackageName() + "/databases/");
        }
        // 设置可以支持缩放
        webSettings.setSupportZoom(false);//设置是否支持缩放
        webSettings.setBuiltInZoomControls(false);//设置是否出现缩放工具
        webSettings.setDisplayZoomControls(true); // 隐藏webview缩放按钮
        // 自适应屏幕
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);

        webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
        webSettings.setSavePassword(true);
        webSettings.setSaveFormData(true);
    }
}

  WebView的基本设置,笔者已经做了好了常用设置,在WebViewActivity调用 void initWebSetting()即可。   在WebSettings的设置中可以对浏览器的常用设置进行配置。例如:对javascrip的支持、缓存模式以及本地持久化保存相关设置、浏览器缩放设置等等。

CustomWebViewClient

public class CustomWebViewClient extends WebViewClient {
    private final String TAG = "CustomWebViewClient";
    private String pre = "";//约定的字段,用于拦截浏览器跳转,然后自定义操作
    private Activity activity;
    private NullPageControll nullPageControll;
    private boolean isError=false;

    public CustomWebViewClient(String pre, Activity activity, NullPageControll nullPageControll) {
        this.pre = pre;
        this.activity = activity;
        this.nullPageControll = nullPageControll;
    }

    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.i(TAG, "拦截到的url----" + url);
        if (url.contains(pre)) {
            Map<String, String> map = getParamsMap(url, pre);
            String code = map.get("code");
            String data = map.get("data");
            WebViewH5Order.parseCodeOrder(view, activity, null, code, data);
            return true;
        } else {
            //。。。这里执行浏览器的正常跳转
            return false;
        }
    }

    //开始载入页面调用的,我们可以设定一个loading的页面,告诉用户程序在等待网络响应。
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        Log.i(TAG, "onPageStarted");
         nullPageControll.beginload();
    }

    //在页面加载结束时调用。我们可以关闭loading条,切换程序动作
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        Log.i(TAG, "onPageFinished");
        if (!isError){
            nullPageControll.initNullPage();
        }else{
            nullPageControll.errorload();
            isError=false;
        }
    }

    //在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
    @Override
    public void onLoadResource(WebView view, String url) {
        super.onLoadResource(view, url);
//        Log.i(TAG,"onLoadResource");
    }

    //加载页面的服务器没有网络或者超时,触发
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
        Log.i(TAG, "onReceivedError");
        isError=true;
        nullPageControll.changeErrorImage(R.mipmap.notinternet);
    }

    @TargetApi(android.os.Build.VERSION_CODES.M)
    @Override
    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
        super.onReceivedHttpError(view, request, errorResponse);
        int statusCode = errorResponse.getStatusCode();
        if (!request.getUrl().toString().contains(".html")) return;
        Log.i(TAG, "onReceivedHttpError——" + statusCode + "\turl——" + request.getUrl());
        if (404 == statusCode || 500 == statusCode) {
            isError=true;
            nullPageControll.changeErrorImage(R.mipmap.notpage);
        }
    }

    //处理https请求
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        super.onReceivedSslError(view, handler, error);
        Log.i(TAG, "onReceivedSslError");
    }

    /**
     * 通过拦截到的URL和约定的标识,获取HTML传递过来的信息
     *
     * @param url
     * @param pre
     * @return
     */
    private Map<String, String> getParamsMap(String url, String pre) {
        ArrayMap<String, String> queryStringMap = new ArrayMap<>();
        if (url.contains(pre)) {
            int index = url.indexOf(pre);
            int end = index + pre.length();
            String queryString = url.substring(end + 1);

            String[] queryStringSplit = queryString.split("&");

            String[] queryStringParam;
            for (String qs : queryStringSplit) {
                if (qs.toLowerCase().startsWith("data=")) {
                    //单独处理data项,避免data内部的&被拆分
                    int dataIndex = queryString.indexOf("data=");
                    String dataValue = queryString.substring(dataIndex + 5);
                    queryStringMap.put("data", dataValue);
                } else {
                    queryStringParam = qs.split("=");

                    String value = "";
                    if (queryStringParam.length > 1) {
                        //避免后台有时候不传值,如“key=”这种
                        value = queryStringParam[1];
                    }
                    queryStringMap.put(queryStringParam[0].toLowerCase(), value);
                }
            }
        }
        return queryStringMap;
    }
}

  CustomWebViewClient是WebViewClient的子类,在WebView中通过 webView.setWebViewClient(new CustomWebViewClient(PRE,this,nullPageControll)); Override shouldOverrideUrlLoading():重写该方法,拦截浏览器打开以及跳转时的url,可以通过拦截到的url与pre比对,实现js和原生的通信。值得一提的是这列也可以用来处理因为HTTP劫持导致打开H5页面出现广告的问题。 Override onPageStarted():页面开始加载时回调 Override onPageFinished():页面加载结束后回调,在改方法执行前,原生是无法与js通信的 Override onReceivedError():当没有网络,或者链接超时是触发 Override onReceivedHttpError():当加载页面发生报错的时候回调,例如404/500等

CustomWebChromeClient:

public class CustomWebChromeClient extends WebChromeClient {
    private final String TAG="CustomWebChromeClient";
    private ValueCallback<Uri> mUploadMessage;
    public ValueCallback<Uri[]> uploadMessage;
    public static final int REQUEST_SELECT_FILE = 100;
    private final static int FILECHOOSER_RESULTCODE = 2;
    private NullPageControll nullPageControll;
    private Activity activity;
    public CustomWebChromeClient(Activity activity,NullPageControll nullPageControll){
        this.activity=activity;
        this.nullPageControll=nullPageControll;
    }
    //获取网页的加载进度并显示
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
        Log.i(TAG,"onProgressChanged——"+newProgress);
    }
    //获取Web网页中的标题
    @Override
    public void onReceivedTitle(WebView view, String title) {
        Log.i(TAG,"onReceivedTitle——"+title);
        if(isChinese(title)) nullPageControll.setTitle(title);
        // android 6.0 以下通过title获取加载错误的信息
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            if (title.contains("404") || title.contains("500") || title.contains("Error")) {
                nullPageControll.changeErrorImage(R.mipmap.notpage);
                nullPageControll.errorload();
            }
        }
    }

    // For Lollipop 5.0+ Devices
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public boolean onShowFileChooser(WebView mWebView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (uploadMessage != null) {
            uploadMessage.onReceiveValue(null);
            uploadMessage = null;
        }
        uploadMessage = filePathCallback;
        Intent intent = fileChooserParams.createIntent();
        try {
            activity.startActivityForResult(intent, REQUEST_SELECT_FILE);
        } catch (ActivityNotFoundException e) {
            uploadMessage = null;
            Log.i(TAG, "Cannot Open File Chooser");
            return false;
        }
        return true;
    }
    private boolean isChinese(String str){
        String regEx="^[\\u0391-\\uFFE5]+$ ";
        return Pattern.compile(regEx).matcher(str).matches();
    }

    /**
     * Activity回调调用
     * @param requestCode
     * @param resultCode
     * @param intent
     */
    public void forActivityResult(int requestCode, int resultCode, Intent intent){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (requestCode == REQUEST_SELECT_FILE) {
                if (uploadMessage == null)
                    return;
                uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
                uploadMessage = null;
            }
        } else if (requestCode == FILECHOOSER_RESULTCODE) {
            if (null == mUploadMessage)
                return;
            // Use MainActivity.RESULT_OK if you're implementing WebView inside Fragment
            // Use RESULT_OK only if you're implementing WebView inside an Activity
            Uri result = intent == null || resultCode != WebViewActivity.RESULT_OK ? null : intent.getData();
            mUploadMessage.onReceiveValue(result);
            mUploadMessage = null;
        } else{
            Log.e(TAG,"Failed to Upload Image");
        }
    }
    // For 3.0+ Devices (Start)
    // onActivityResult attached before constructor
    protected void openFileChooser(ValueCallback uploadMsg, String acceptType) {
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        activity.startActivityForResult(Intent.createChooser(i, "File Browser"), FILECHOOSER_RESULTCODE);
    }

    //For Android 4.1 only
    protected void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        mUploadMessage = uploadMsg;
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        activity.startActivityForResult(Intent.createChooser(intent, "File Browser"), FILECHOOSER_RESULTCODE);
    }

    protected void openFileChooser(ValueCallback<Uri> uploadMsg) {
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
    }
}

CustomWebChromeClient是WebChromeClient的子类,在WebView中通过 webChromeClient=new CustomWebChromeClient(this,nullPageControll); webView.setWebChromeClient(webChromeClient);引用。它可以辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。

NullPageControll

public class NullPageControll {
    private Activity activity;
    private ViewGroup nullpage,reloadpage;
    private TextView tv_reload,title;
    private ImageView img_back,img_error;
    private ProgressBar progressBar;
    private WebView webView;
    private int progress=0;
    public NullPageControll(Activity activity, WebView webView){
        this.activity=activity;
        this.webView=webView;
        findView();
        init();
    }
    private void init(){
        tv_reload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                webView.reload();
            }
        });
        img_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(webView.canGoBack()){
                    webView.goBack();
                }else{
                    activity.finish();
                }
            }
        });
    }
    private void findView(){
        nullpage=activity.findViewById(R.id.nullpage);
        reloadpage=activity.findViewById(R.id.reloadpage);
        tv_reload=activity.findViewById(R.id.reload);
        img_back=activity.findViewById(R.id.back);
        title=activity.findViewById(R.id.title);
        progressBar=activity.findViewById(R.id.progressbar);
        img_error=activity.findViewById(R.id.errorimg);
    }
    public void setTitle(String str){
        title.setText(str);
    }

    /**
     * 开始加载时调用
     */
    public void  beginload(){
        isShowNullPage(true);
        isShowReloadPage(false);
        startProgress();
    }

    /**
     * 错误加载时调用
     */
    public void errorload(){
        isShowReloadPage(true);
        isShowReloadPage(true);
        finishProgress();
    }
    /**
     * 将各个部件还原初始状态
     */
    public void initNullPage(){
        progress=0;
        progressBar.setProgress(progress);
        progressBar.setVisibility(View.GONE);
        isShowNullPage(false);
        isShowReloadPage(false);
    }

    /**
     * 更换错误加载时显示的图片,默认显示页面不存在的提示
     * @param resource
     */
    public void changeErrorImage(int resource){
        img_error.setImageResource(resource);
    }
    /**
     * 是否显示nullpage页面
     * @param isShow
     */
    private void isShowNullPage(boolean isShow){
        nullpage.setVisibility(isShow?View.VISIBLE:View.GONE);
    }

    /**
     * 是否显示reloadpage页面
     * @param isShow
     */
    private void isShowReloadPage(boolean isShow){
        reloadpage.setVisibility(isShow?View.VISIBLE:View.GONE);
    }

    /**
     * 开始加载动画
     * 这是一个假的进度条动画,目的是有个更好的体验效果
     */
    private void startProgress(){
        progress=0;
        progressBar.setProgress(progress);
        progressBar.setVisibility(View.VISIBLE);
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(progress<95){
                    try {
                        Thread.sleep(20);
                        progress+=1;
                        progressBar.setProgress(progress);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    /**
     * 结束加载动画
     */
    private void finishProgress(){
        progress=100;
        progressBar.setProgress(progress);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                progressBar.setVisibility(View.GONE);
                setTitle("");
            }
        },100);
    }
}

  NullPageControll是对WebView用户体验的一个优化类。提供加载页和错误页的控制器。   H5有一个通病在于在网络不流畅的时候,点击页面跳转时,因为会先请求页面html导致卡顿,整个页面没有反应,跟卡死了一样。IOS稍微好一点,Android尤为明显。为了解决这个问题,我们需要利用原生自己绘制一个请求html页面时的加载动画页面和错误加载提示页。这样可以大幅度的提高用户的体验。而我们可以通过WebViewClient提供的回调去处理各个阶段的状态。

附件:

GitHub下载地址

笔者还在学习中,文章大多以笔记的风格为主。欢迎留言交流沟通,不喜勿喷。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

开源库BaseRecyclerViewAdapterHelper

相信大家RecyclerView应该不会陌生,大多数开发者应该都使用上它了,它也是google推荐替换ListView的控件,但是用过它的同学应该都知道它在某些...

33270
来自专栏项勇

笔记56 | 管理网络的使用

17460
来自专栏Phoenix的Android之旅

Toast在子线程调用的问题

如果在子线程调用那么让Toast能正常显示的方式是在它之前和之后调用Looper.prepare()和Looper.loop()

11630
来自专栏james大数据架构

实例演示Android异步加载图片

本文给大家演示异步加载图片的分析过程。让大家了解异步加载图片的好处,以及如何更新UI。 首先给出main.xml布局文件: 简单来说就是 LinearLayou...

22550
来自专栏Android知识点总结

2-AII--BroadcastReceiver有序广播

14740
来自专栏沃趣科技

Oracle 12c ASM专题|Flex Diskgroup相关概念

原文链接 https://martincarstenbach.wordpress.com/2017/07/11/12-2-new-feature-the-fle...

39170
来自专栏Android点滴积累

屏幕旋转时调用PopupWindow update方法更新位置失效的问题及解决方案

   接到一个博友的反馈,在屏幕旋转时调用 PopupWindow 的 update 方法失效。使用场景如下:在一个 Activity 中监听屏幕旋转事件,在A...

14600
来自专栏Android点滴积累

屏幕旋转时调用PopupWindow update方法更新位置失效的问题及解决方案

   接到一个博友的反馈,在屏幕旋转时调用 PopupWindow 的 update 方法失效。使用场景如下:在一个 Activity 中监听屏幕旋转事件,在A...

33690
来自专栏Android干货

安卓开发_startActivityForResult的详细用法

39660
来自专栏编程之路

羊皮书APP(Android版)开发系列(五)APP引导页实现

14150

扫码关注云+社区

领取腾讯云代金券