美文网首页
助你快速搭建一个健壮可控的WebApp

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

作者: 饮水思源为名 | 来源:发表于2018-11-15 14:27 被阅读21次

      笔者因公司需求,从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下载地址

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

    相关文章

      网友评论

          本文标题:助你快速搭建一个健壮可控的WebApp

          本文链接:https://www.haomeiwen.com/subject/irvefqtx.html