uni-app直接用webiew打开本地js资源

2020-11-17 09:56:02

以前我很不看好uni-app,现在又点后悔没有早一点使用了,他可以打包我们的web成js放入资源,供ios和android使用。


如果再结合activity不销毁,隐藏的方法,像里面传递参数,来改变页面,不销毁webview,我发现这样比原生的都要快。这样又能用于app端,又能生成小程序,何乐而不为


正式应为uni-app最基础的功能,可以帮我完成最流程的功能。嘻嘻。

下面看怎么使用吧。


参考地址 将uniapp打包成h5放在安卓webview中(解决uniapp引入第三方地图卡顿问题)

我还是不倾向与用uni-app直接开发原生app的,因为这样确实体验不好,也没有那么自由,所有我用这种混合式开发。

本来是使用uniapp进行开发,然后打包成安卓软件的,因为是用了地图模块(基于天地图),因为uniapp框架的限制,只能使用webview组件引入地图文件,然后出现一个问题,发现地图在浏览器中打开很流畅,打包成app之后非常卡顿,试了很多种办法,包括把地图放在vue文件中来渲染,发现依然卡,然后想到能不能直接把打包成H5,然后整个文件丢在安卓中,用安卓的webview去打开它,尝试之后发现效果还挺好的,下面列出代码(自己需要啥功能需要自己去添加,可以自行添加腾讯的X5内核)

具体步骤
  1. HbuilderX把uniapp项目打包成H5手机版

  2. 新建一个安卓项目

  3. 新建一个assets目录(src目录单击右键选择,弹出的提示框选择finish就可以自动生成assets目录了)
    在这里插入图片描述

  4. 把打包好的文件发到assets目录

  5. 编写程序

  6. 运行

uniapp打包注意点

将app打包成H5手机版注意别忘了把运行的基础路径改为./
在这里插入图片描述

开发工具

Android Studio版本 4.0.1

Java版本 Java1.8(使用到了lambda表达式,需要去设置为1.8,否者会报错)设置教程:https://blog.csdn.net/weixin_43373239/article/details/88741896

使用到的库

statusbar状态栏工具类(实现沉浸式状态栏/变色状态栏)

https://jaeger.itscoder.com/android/2016/03/27/statusbar-util.html

使用到的权限
<!--外部存储权限 这个目前没有使用到--><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!--获取wifi和网络信息--><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><!--网络权限--><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.READ_PHONE_STATE" /><!--定位--><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />1234567891011
目录结构

在这里插入图片描述

软件运行流程

初次安装软件后,打开app,会弹出申请定位权限提示框,如果用户没有授权,则会弹出“请开启定位权限”提示框,用户点击确定后会自动关闭软件,如果用户授权之后,会弹出申请获取手机信息权限(因为在华为手机上如果没有获取此项权限可能无法使用定位功能),如果用户没有授权会提示“请开启获取手机信息权限”,用户点击确定按钮后会自动关闭软件,当用户开启所有权限后,会进入到登录页面,因为网页是存放在安卓本地,所以不会存在用户断网之后出现404页面的情况。

授予权限

拒绝授权

授予权限

拒绝授权

进入湿地监测软件

申请定位权限

申请获取手机信息权限

弹出信息提示框`提示开启权限`

进入到登录页面

退出程序

布局文件(activity_mian.xml)
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none" /></androidx.constraintlayout.widget.ConstraintLayout>12345678910111213

android:scrollbars="none"不显示滚动条

主要Activity(MainActivity)
package com.example.opeak;import android.Manifest;import android.annotation.SuppressLint;import android.app.Activity;import android.app.AlertDialog;import android.content.Context;import android.content.DialogInterface;import android.content.Intent;import android.content.pm.PackageManager;import android.graphics.Color;import android.location.LocationManager;import android.net.Uri;import android.os.Bundle;import android.view.KeyEvent;import android.webkit.GeolocationPermissions;import android.webkit.JsPromptResult;import android.webkit.JsResult;import android.webkit.ValueCallback;import android.webkit.WebChromeClient;import android.webkit.WebSettings;import android.webkit.WebView;import android.webkit.WebViewClient;import android.widget.Toast;import androidx.annotation.NonNull;import androidx.core.app.ActivityCompat;import androidx.core.content.ContextCompat;import com.jaeger.library.StatusBarUtil;public class MainActivity extends Activity {
    private WebView webView;
    private static final int LOCATION_CODE = 1;
    private static final int READ_PHONE_CODE = 2;
    private boolean isWebLocation = false;
    private ValueCallback<Uri[]> valueCallback;
    private boolean uploadImage = true;
    /**
     * 顶部背景色 蓝 灰
     */
    private String[] topColors = {"#FFFFFF", "#52A8F9", "#F8F8F8"};
    private String indexPageUrl = "file:///android_asset/h5/index.html#/pages/index/index";
    private String loginPageUrl = "file:///android_asset/h5/index.html#/";


    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getGPSPermission();
        String url = "file:///android_asset/h5/index.html";
        webView = findViewById(R.id.webView);
        WebSettings webSettings = webView.getSettings();
        //开启JavaScript
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDomStorageEnabled(true);
        //设置可以访问文件
        webSettings.setAllowFileAccess(true);
        webSettings.setLoadsImagesAutomatically(true);
        webView.setVerticalScrollBarEnabled(false);
        webView.setHorizontalScrollBarEnabled(false);
        webSettings.setAppCacheEnabled(true);
        webSettings.setDatabaseEnabled(true);
        String dir = getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
        webSettings.setGeolocationDatabasePath(dir);
        webSettings.supportMultipleWindows();
        webSettings.setAllowContentAccess(true);
        webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
        //允许地理位置可用
        webSettings.setGeolocationEnabled(true);
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
        //加载url
        webView.loadUrl(url);
        //不可复制 就是拦截长按事件(如果要改成可以复制  把下面的代码注释掉就可以了)
        webView.setOnLongClickListener(v -> true);
        webView.setWebChromeClient(new WebChromeClient() {
            //网页申请定位后回调
            @Override
            public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
                //判断是否有权限、是否开启了定位功能
                locationPermission();
                //invoke(申请定位网站的网址,是否同意定位,是否缓存授权)
                callback.invoke(origin, isWebLocation, false);
            }

            //选择文件
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {
                showToast("请选择头像!");
                MainActivity.this.valueCallback = valueCallback;
                showFileChooser();
                //如果filePathCallback被调用;返回false,如果忽略处理
                return uploadImage;
            }

            //js弹框  下面的几个分别对应js中的alert弹框、confirm弹框等等
            @Override
            public boolean onJsAlert(WebView webView, String url, String message, JsResult jsResult) {
                jsResult.confirm();
                showToast(message);
                return true;
            }

            @Override
            public boolean onJsConfirm(WebView webview, String url, String message, JsResult result) {
                //可以弹框或进行其它处理,但一定要回调result.confirm或者cancel
                showMeDialog(message, (dialogInterface, i) -> {
                            result.confirm();
                        },
                        (dialogInterface, i) -> {
                            result.cancel();
                        });
                return true;
            }

            @Override
            public boolean onJsBeforeUnload(WebView webview, String url, String message, JsResult result) {
                //可以弹框或进行其它处理,但一定要回调result.confirm或者cancel
                return true;
            }

            @Override
            public boolean onJsPrompt(WebView webview, String url, String message, String defaultValue, JsPromptResult result) {
                //可以弹框或进行其它处理,但一定要回调result.confirm或者cancel,confirm可以将用户输入作为参数
                return true;
            }
        });

        webView.setWebViewClient(new WebViewClient() {
            /**
             * 顶部任务栏颜色变化
             * @param webView
             * @param s
             * @param b
             */
            @Override
            public void doUpdateVisitedHistory(WebView webView, String s, boolean b) {
                if (s.equals(indexPageUrl)) {
                    StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[1]));
                } else if (s.equals(loginPageUrl)) {
                    StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[0]));
                } else {
                    StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[2]));
                }
            }
        });
    }

    /**
     * 判断是否拥有定位权限
     * PERMISSION_GRANTED:有权限
     */
    public void locationPermission() {
        LocationManager lm = (LocationManager) MainActivity.this.getSystemService(LOCATION_SERVICE);
        boolean ok = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
        if (ok) {
            getGPSPermission();
        } else {
            isWebLocation = false;
            showToast("未开启GPS定位服务,定位功能将受限!");
        }
    }

    /**
     * 申请定位权限
     */
    public void getGPSPermission() {
        //没有权限
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            // 没有权限,申请权限。
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, LOCATION_CODE);
        } else {
            isWebLocation = true;
            phoneInformationPermission();
        }
    }

    /**
     * 判断是否有获取手机信息权限(没有此权限,华为手机不能定位)
     */
    public void phoneInformationPermission() {
        //如果没有权限
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            // 没有权限,申请权限。
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_PHONE_STATE}, READ_PHONE_CODE);
        }
    }

    /**
     * 申请权限后会回调
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == LOCATION_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                isWebLocation = true;
                phoneInformationPermission();
            } else {
                isWebLocation = false;
                showMeDialog("请开启手机定位权限!",
                        (dialogInterface, i) -> {
                            closeApp();
                        },
                        null);
            }
        } else if (requestCode == READ_PHONE_CODE) {
            if (grantResults.length <= 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                showMeDialog("请开启获取手机信息权限!",
                        (dialogInterface, i) -> {
                            closeApp();
                        },
                        null);
            }
        }
    }


    /**
     * 关闭app
     */
    private void closeApp() {
        finish();
        System.exit(0);
    }

    /**
     * 显示文件选择器
     */
    private void showFileChooser() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");//设置类型,我这里是任意类型,任意后缀的可以这样写。
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, 1);
    }

    /**
     * 上传图片后回调
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {//是否选择,没选择就不会继续
            Uri uri = data.getData();
            Uri[] uris = new Uri[]{uri};
            valueCallback.onReceiveValue(uris);
            uploadImage = false;
            showToast("正在上传头像,请稍后!");
        } else {
            uploadImage = true;
            valueCallback.onReceiveValue(null);
            showToast("没有选择头像文件!");
        }
    }

    /**
     * 编写后退事件
     *
     * @param keyCode
     * @param event
     * @return
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            //时候可以后退
            boolean goBack = webView.canGoBack();
            if (goBack) {
                //后退
                webView.goBack();
            } else {
                showCoverDialog();
            }
            return true;
        }
        //继续执行父类的其他点击事件
        return false;
    }

    /**
     * 显示退出弹框
     */
    private void showCoverDialog() {
        showMeDialog("是否退出本程序", (dialogInterface, i) -> {
                    closeApp();
                },
                (dialogInterface, i) -> {
                });
    }

    /**
     * 显示弹框
     *
     * @param message 内容
     * @param ok      点击确定的事件
     * @param cancel  点击取消的事件
     */
    private void showMeDialog(String message, DialogInterface.OnClickListener ok, DialogInterface.OnClickListener cancel) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示");
        builder.setMessage(message);
        builder.setPositiveButton("是", ok);
        if (cancel != null) {
            builder.setNegativeButton("否", cancel);
        }
        builder.show();
    }

    @Override
    protected void onDestroy() {
        //删除地理位置授权,也可以删除某个域名的授权(参考接口类)
        GeolocationPermissions.getInstance().clearAll();
        webView.destroy();
        super.onDestroy();
    }

    /**
     * 显示提示(去除小米手机自带应用名)
     *
     * @param message 消息
     */
    private void showToast(String message) {
        Toast toast = Toast.makeText(this, null, Toast.LENGTH_SHORT);
        toast.setText(message);
        toast.show();
    }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.opeak">
    <!--外部存储权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--获取wifi和网络信息-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!--网络权限-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <!--定位-->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


    <application
        android:name=".MeApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true"
        tools:targetApi="m">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application></manifest>1234567891011121314151617181920212223242526272829303132333435

android:usesCleartextTraffic=true别忘了加(如果你的网页都是https的请求那就可以不用加了)。

摘取网络上的一段解释:

android:usesCleartextTraffic 指示应用程序是否打算使用明文网络流量,例如明文HTTP。目标API级别为27或更低的应用程序的默认值为“ true”。面向API级别28或更高级别的应用默认为“ false”。

当属性设置为“ false”时,平台组件(例如,HTTP和FTP堆栈,DownloadManager和MediaPlayer)将拒绝应用程序使用明文流量的请求。强烈建议第三方库也采用此设置。避免明文通信的主要原因是缺乏机密性,真实性和防篡改保护;网络攻击者可以窃听所传输的数据,并且还可以对其进行修改而不会被检测到。

build.gradle文件
apply plugin: 'com.android.application'android {
    compileSdkVersion 29
    buildToolsVersion "30.0.1"

    defaultConfig {
        applicationId "com.example.opeak"
        minSdkVersion defaultMinSdkVersion
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8    }}dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation 'com.jaeger.statusbarutil:library:1.4.0'}12345678910111213141516171819202122232425262728293031323334353637

com.jaeger.statusbarutil:library:1.4.0就是前面说的statusbar

打包教程

https://blog.csdn.net/weixin_43373239/article/details/107833035

注意事项
  1. 正式打包生成apk的时候,如果使用的不是同一个签名证书,更新app的时候会出现签名不同,无法安装,只要把原app卸载即可。


  • 2020-04-03 10:20:20

    Vue 项目性能优化

    Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。本文是作者通过实际项目的优化实践进行总结而来,希望读者读完本文,有一定的启发思考,从而对自己的项目进行优化起到帮助。本文内容分为以下三部分组成:

  • 2020-04-03 13:07:46

    flex布局与position:absolute/fixed的冲突问题

    导航栏内,平均分为四块,为了适配各种移动设备,使用了flex布局。 与此同时,产品经理要求:页面上滚越过封面图时,导航栏变为固定定位,浮在页面顶部。 拿到需求之后,思路就是先搞好布局,然后监听window.onscroll,当页面滚的距离大于封面图的时候,给ul加入position:fixed。

  • 2020-04-03 16:56:59

    Inkscape教程

    本教程演示了Inkscape基础使用。这是常规Inkscape文档,你可以预览、编辑、复制、保存。 本教程包括画布导航、管理文档、形状工具基础、选择技术、使用选择转换对象、分组、设置填充和画笔、对齐和Z顺序。有关更高级的主题请查看帮助菜单中的其它教程。

  • 2020-04-03 17:04:35

    Inkscape/SVG附中文教程PDF

    Inkscape中的终极工具是XML编辑器(Shift+Ctrl+X),可以实时显示整个文档的XML树形图。修改绘图时,你可以注意一下XML树形图中的变化。也可以在XML编辑器中修改文本、元素或者节点属性,然后在画图上查看效果。这是一个非常形象化的学习SVG格式的交互式工具。并且可以实现一些通常的编辑工具无法完成的功能。

  • 2020-04-03 19:09:31

    CryptoJS.enc.UTF8 中文乱码

    ret = CryptoJS.AES.encrypt(data,'secret key 123') content = ret.toString() result = CryptoJS.AES.decrypt(content,'secret key 123') print(result.toString(CryptoJS.enc.Utf8))

  • 2020-04-03 19:10:56

    nodejs与javascript中的aes加密

    aes加密简单来说,在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。高级加密标准已然成为对称密钥加密中最流行的算法之一。

  • 2020-04-03 19:13:05

    Express-session的使用

    当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie), 找到对应的 session(value)。 客户的信息都保存在 session 中

  • 2020-04-08 22:46:28

    Element的操作方法

    Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。这个接口描述了所有相同种类的元素所普遍具有的方法和属性。一些接口继承自 Element 并且增加了一些额外功能的接口描述了具体的行为。例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基础。大多数功能是在这个类的更深层级(hierarchy)的接口中被进一步制定的。