前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >助你快速搭建一个健壮可控的WebApp

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

作者头像
饮水思源为名
发布2018-12-13 16:23:00
1K0
发布2018-12-13 16:23:00
举报
文章被收录于专栏:Android小菜鸡Android小菜鸡

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

核心类:

image.png

WebViewActivity:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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

代码语言:javascript
复制
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:

代码语言:javascript
复制
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

代码语言:javascript
复制
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下载地址

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.11.15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 核心类:
  • WebViewActivity:
  • WebSettingUtil:
  • CustomWebViewClient
  • CustomWebChromeClient:
  • NullPageControll
  • 附件:
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档