autox.js结合JsBridge及weui+实现web界面的交互,应用WebView与 HTML开发脚本界面

前言

在autox中,结合webView使用原生js实现web界面,在交互方面非常繁琐。
来看官方的例子:

"ui";
ui.layout(
    <vertical>
        <horizontal bg="#c7edcc" gravity="center" h="auto">
            <button text="网络冲浪" id="surfInternetBtn" style="Widget.AppCompat.Button.Colored" w="auto" />
            <button text="记忆翻牌" id="loadLocalHtmlBtn" style="Widget.AppCompat.Button.Colored" w="auto" />
            <button text="控制台" id="consoleBtn" style="Widget.AppCompat.Button.Colored" w="auto" />
        </horizontal>
        <vertical h="*" w="*">
            <webview id="webView" layout_below="title" w="*" h="*" />
        </vertical>
    </vertical>
);

function callJavaScript(webViewWidget, script, callback) {
    try {
        console.assert(webViewWidget != null, "webView控件为空");
        //console.log(script.toString())
        webViewWidget.evaluateJavascript("javascript:" + script, new JavaAdapter(android.webkit.ValueCallback, {
            onReceiveValue: (val) => {
                if (callback) {
                    callback(val);
                }
            }
        }));
    } catch (e) {
        console.error("执行JavaScript失败");
        console.trace(e);
    }
}

function AutoX() {
    let getAutoXFrame = () => {
        let bridgeFrame = document.getElementById("AutoXFrame");
        if (!bridgeFrame) {
            bridgeFrame = document.createElement('iframe');
            bridgeFrame.id = "AutoXFrame";
            bridgeFrame.style = "display: none";
            document.body.append(bridgeFrame);
        }
        return bridgeFrame;
    };
    const h5Callbackers = {};
    let h5CallbackIndex = 1;
    let setCallback = (callback) => {
        let callId = h5CallbackIndex++;
        h5Callbackers[callId] = {
            "callback": callback
        };
        return callId;
    };
    let getCallback = (callId) => {
        let callback = h5Callbackers[callId];
        if (callback) {
            delete h5Callbackers[callId];
        }
        return callback;
    };

    function invoke(cmd, params, callback) {
        let callId = null;
        try {
            let paramsStr = JSON.stringify(params);
            let AutoXFrame = getAutoXFrame();
            callId = setCallback(callback);
            AutoXFrame.src = "jsbridge://" + cmd + "/" + callId + "/" + encodeURIComponent(paramsStr);
        } catch (e) {
            if (callId) {
                getCallback(callId);
            }
            console.trace(e);
        }
    };
    let callback = (data) => {
        let callId = data.callId;
        let params = data.params;
        let callbackFun = getCallback(callId);
        if (callbackFun) {
            callbackFun.callback(params);
        }
    };
    return {
        invoke: invoke,
        callback: callback
    };
};
function bridgeHandler_handle(cmd, params) {
    console.log('bridgeHandler处理 cmd=%s, params=%s', cmd, JSON.stringify(params));
    let fun = this[cmd];
    if (!fun) {
        throw new Error("cmd= " + cmd + " 没有定义实现");
    }
    let ret = fun(params)
    return ret;
}
function mFunction(params) {
    toastLog(params.toString());
    device.vibrate(120);
    return files.isDir('/storage/emulated/0/Download')//'toast提示成功';
}
function webViewExpand_init(webViewWidget) {
    webViewWidget.webViewClient = new JavaAdapter(android.webkit.WebViewClient, {
        onPageFinished: (webView, curUrl) => {
            try {
                // 注入 AutoX
                callJavaScript(webView, AutoX.toString() + ";var auto0 = AutoX();auto0.invoke('mFunction','This is AutoX!',(data) => {console.log('接收到callback1:' + JSON.stringify(data));});", null);
            } catch (e) {
                console.trace(e)
            }
        },
        shouldOverrideUrlLoading: (webView, request) => {
            let url = '';
            try {
                url = (request.a && request.a.a) || (request.url);
                if (url instanceof android.net.Uri) {
                    url = url.toString();
                }
                if (url.indexOf("jsbridge://") == 0) {
                    let uris = url.split("/");
                    let cmd = uris[2];
                    let callId = uris[3];
                    let params = java.net.URLDecoder.decode(uris[4], "UTF-8");
                    console.log('AutoX处理JavaScript调用请求: callId=%s, cmd=%s, params=%s', callId, cmd, params);
                    let result = null;
                    try {
                        result = bridgeHandler_handle(cmd, JSON.parse(params));
                    } catch (e) {
                        console.trace(e);
                        result = {
                            message: e.message
                        };
                    }
                    result = result || {};
                    webView.loadUrl("javascript:auto0.callback({'callId':" + callId + ", 'params': " + JSON.stringify(result) + "});");
                } else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://") || url.startsWith("ws://") || url.startsWith("wss://")) {
                    webView.loadUrl(url);
                } else {
                }
                return true;
            } catch (e) {
                if (e.javaException instanceof android.content.ActivityNotFoundException) {
                    webView.loadUrl(url);
                } else {
                    toastLog('无法打开URL: ' + url);
                }
                console.trace(e);
            }
        },
        onReceivedError: (webView, webResourceRequest, webResourceError) => {
            let url = webResourceRequest.getUrl();
            let errorCode = webResourceError.getErrorCode();
            let description = webResourceError.getDescription();
            console.trace(errorCode + " " + description + " " + url);
        }
    });
    webViewWidget.webChromeClient = new JavaAdapter(android.webkit.WebChromeClient, {
        onConsoleMessage: (msg) => {
            console.log("[%s:%s]: %s", msg.sourceId(), msg.lineNumber(), msg.message());
        }
    });
}
webViewExpand_init(ui.webView)
ui.webView.loadUrl("https://wht.im");

ui.surfInternetBtn.on("click", () => {
    webViewExpand_init(ui.webView);
    ui.webView.loadUrl("https://wht.im");
});
ui.consoleBtn.on("click", () => {
    app.startActivity("console");
});
ui.loadLocalHtmlBtn.on('click', () => {
    webViewExpand_init(ui.webView);
    let path = "file:" + files.path("game.html");
    ui.webView.loadUrl(path);
});

而在v6.3.9的版本开始,引入了JsBridge框架。实现web界面的交互就方便多了。
以下是前端代码:

<html>
  <body style="font: size 2em">
    <div style="font-size: 100px">原内容</div>
    <!-- 导入依赖包,也可以不加,不过需要监听AutoxJsBridgeReady事件后才能使用$autox -->
    <script src="autox://sdk.v1.js"></script>
    <script>
      function addText(text) {
        const div = document.createElement("div");
        div.innerHTML = text;
        document.body.appendChild(div);
      }
      //注册一个监听函数
      $autox.registerHandler("jsTest", (data, callBack) => {
        addText(`来自安卓调用,data=${data}`);
        setTimeout(() => {
          //回调安卓
          callBack("web回调数据");
        }, 1000);
      });
      //调用安卓端
      $autox.callHandler("test", "web调用数据", (data) => {
        addText("安卓回调, data:" + data);
      });

      document.addEventListener("AutoxJsBridgeReady", () => {
        //$autox.
      });
    </script>
  </body>
</html>

以下是脚本代码:

"ui";

ui.layout(`
    <vertical>
        <webview id="web" h="*"/>
    </vertical>`)

ui.web.loadUrl("file://" + files.path("./网页.html"))
/*
    注意:在web与安卓端传递的数据只能是字符串,其他数据需自行使用JSON序列化
    在调用callHandler时传入了回调函数,但web端没有调用则会造成内存泄露。
    jsBridge自动注入依赖于webViewClient,如设置了自定义webViewClient则需要在合适的时机(页面加载完成后)调用webview.injectionJsBridge()手动注入
*/
//注册一个监听函数
ui.web.jsBridge.registerHandler("test", (data, callBack) => {
    toastLog("web调用安卓,data:" + data)
    setTimeout(() => {
        //回调web
        callBack("1155")
    }, 2000)
})
//定时器中等待web加载完成
setTimeout(() => {
    ui.web.jsBridge.callHandler('jsTest', '数据', (data) => {
        toastLog('web回调,data:' + data)
    })
}, 1000)

从例子可以看出,web前端和脚本进行交互,只需要相互调用注册好的函数就可以了。
大大简化了交互过程。
由于官方文档不够详细,缺乏有效的注解,本文将结合实际的例子,来对交互方式进行详细分析。

JSBridge 定义

JSBridge 在autox中,是一种 JS 实现的 Bridge,连接着桥两端的 脚本 和 H5。它在 APP 内方便地让 脚本 调用 前端JS,前端JS 调用 脚本 ,是前端和后端双向通信的通道。

JSBridge 主要提供了 JS 调用 脚本 代码的能力,在前端实现autox框架内api的调用。

通过JSBridge,Web端可以调用脚本端的api接口,同样脚本端也可以通过JSBridge调用Web端的JavaScript接口,实现彼此的双向调用。

JSBridge 交互原理

交互的主要原理是,通过 WebView 提供的接口,向JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 脚本 代码逻辑,达到 JavaScript 调用 脚本操作api 的目的。

前端

测试项目引用weui+ 6.08框架实现webui。
先来看autox项目的目录树:

layout                       
├─ css                       
│  ├─ weui.css               
│  └─ weuix.css              
├─ js                                     
│  ├─ zepto.min.js           
│  └─ zepto.weui.js          
├─ res                               
└─ index.html 
main.js
project.json

layout目录用于存放前端文件,脚本代码写在main.js文件中。

html界面上,主要有两个checkbox,用于同步显示服务状态、控制服务状态。

// layout/index.html

<div class="weui-cell weui-cell_switch">
    <div class="weui-cell__bd"><label class="weui-label">无障碍服务</label></div>
    <div class="weui-cell__ft">
        <input class="weui-switch" type="checkbox" id="autoService">
    </div>
</div>
<div class="weui-cell weui-cell_switch">
    <div class="weui-cell__bd"><label class="weui-label">悬浮窗权限</label></div>
    <div class="weui-cell__ft">
        <input class="weui-switch" type="checkbox" id="floatyService">
    </div>
</div>

UI界面渲染效果如下:
file

接下来实现javascript代码。

获取服务状态

在UI界面加载完毕,脚本初始化完成后,UI界面上的checkbox需要显示服务状态。
而服务状态,是从脚本里获取的,即:html文件要从main.js脚本文件里,获取到数据。
怎么获取呢?

// layout/index.html

getServiceState();   //刷新自动化服务状态

/*
    * 回调-获取服务状态
    * 主动从安卓里获取服务状态,并同步到checkbox
*/
function getServiceState() {
    // 注册一个监听函数
    $autox.registerHandler("getServiceState", (data) => {
        syncServiceCheckbox(data);
    });
}

/**
 * 同步显示服务checkbox选中状态
 */
function syncServiceCheckbox(data) {
    let uiData = JSON.parse(data);   // 字符串转对象

    let auto_service_state = uiData['auto_service_state'];    //获取到无障碍服务状态
    if (auto_service_state) {
        $("#autoService").attr("checked", 'checked');   //设置选中
    } else {
        $("#autoService").removeAttr("checked");       //设置不选中
    }
}

前端注册一个“getServiceState”方法,就可以调用脚本中的这个方法了。
需要注意的是,交互过程中传递的数据,必须是文本型。
如果有多个数据传递,可以使用json格式。
因为json是通用型的数据,在各种开发语言中都可以自由解析和传递。

//main.js

// 定时器中等待web加载完成
setTimeout(() => {

    /**
     * 前端获取无障碍状态
     */
    let uiData = {
        auto_service_state: auto.service != null ? true : false,
    }
    ui.web.jsBridge.callHandler('getServiceState', JSON.stringify(uiData));   

}, 1000)

checkbox点击事件

前端点击checkbox之后,获得checkbox的选中状态。
然后将该状态作为参数传递给脚本,脚本再根据状态,进行相应的处理。

// layout/index.html

/*
    * 事件-checkbox-无障碍服务
*/
$(document).on("click", "#autoService", function () {
    let checked = $(this).prop("checked");
    clickCheckBox('AUTO_SERVICE', checked);
})

/**
 * 点击checkbox
 * type:点击类型/标识。用于区分点击的是哪个按钮
 */
function clickCheckBox(type, checked) {
    let htmlData = {
        type: type,
        checked: checked,
    }

    // 调用安卓端'ui.js'中方法
    $autox.callHandler('e_checkbox', JSON.stringify(htmlData));
}

脚本端注册前端按钮点击事件的监听函数:

// main.js

/**
 * 注册一个监听函数:checkbox点击事件
 * 当用户在前端点击checkbox时,会执行此区域代码
 */
ui.web.jsBridge.registerHandler('e_checkbox', (data) => {
    // toastLog("checkbox选中数据:" + data)
    let htmlData = JSON.parse(data);
    let checked = htmlData['checked'];

    switch (htmlData['type']) {
        case 'AUTO_SERVICE':
            // 用户勾选无障碍服务的选项时,跳转到页面让用户去开启
            if (checked && auto.service == null) {
                console.verbose('跳转到无障碍设置页面');
                //!!!如果应用自身的无障碍假死或者出现故障,下面这个跳转会失效。遇到这种情况,需要手动重启无障碍服务。
                app.startActivity({
                    action: "android.settings.ACCESSIBILITY_SETTINGS"
                });
            } else if (!checked && auto.service != null) {
                console.verbose('关闭无障碍服务');
                auto.service.disableSelf();  //关闭自己service的方法,在设置界面可以看到辅助功能状态被关闭
            }
            break;
        default:
            break;
    }

})

resume事件

这是另外一种情况,即脚本端主动向前端发送数据,调用前端的函数,在前端执行相应的处理。

脚本代码:

/**
 * 当用户回到本界面时,resume事件会被触发
 * 此时根据服务的开启情况,同步开关的状态
 * 设置前端checked是否选中
 */
ui.emitter.on("resume", function () {
    let uiData = {
        auto_service_state: auto.service != null ? true : false,
    }
    ui.web.jsBridge.callHandler('setServiceState', JSON.stringify(uiData));   
});

前端代码:

/**
 * 安卓的resume事件触发此方法
 * 当回到应用界面时,前端获取服务状态并显示到checkbox
 */
$autox.registerHandler("setServiceState", (data) => {
    syncServiceCheckbox(data);
});

避坑

目前6.51版本autox的webview还不太完善。经过实测,调用MUI、WEUI等前端框架测试正常,打包后就出现一些组件无法点击的问题。

不知道开发者后续能否修复。眼下不建议使用这些前端框架做UI界面。

补充

现在是2023-12-01

autox的webview打包后,部分组件无法使用的问题,经过反馈,在新的版本中已经得到了解决。

根本的原因,是打包的时候,默认将js文件都加密了。导致运行的时候无法解密,从而出现部分js组件失效。

只需要在打包的时候设置不加密js文件,就可以正常使用webview。

1. 官方交流QQ群,添加多个不批。建议使用安卓手机或电脑申请。
飞云脚本圈: 586333520飞云脚本圈
Auto.js学习交流③群:286635606
Auto.js学习交流②群:712194666(满员)
IOS免越狱自动化测试群:691997586
2. 盗版,破解有损他人权益和违法作为,请各位会员支持正版。
3. 本站部分资源来源于用户上传和网络搜集,如有侵权请提供版权证明并联系站长删除。
4.如未特别申明,本站的技术性文章均为原创,未经授权,禁止转载/搬运等侵权行为。
5.全站所有付费服务均为虚拟商品,购买后自动发货。售出后概不接受任何理由的退、换。注册即为接受此条款。
6.如果站内内容侵犯了您的权益,请联系站长删除。
飞云脚本 » autox.js结合JsBridge及weui+实现web界面的交互,应用WebView与 HTML开发脚本界面

企业级大数据智能营销管理系统

了解详情