美文网首页HarmonyOS
前端 Web 与原生应用端 WebView 通信交互 - Har

前端 Web 与原生应用端 WebView 通信交互 - Har

作者: survivorsfyh | 来源:发表于2024-06-04 17:54 被阅读0次

    基于鸿蒙 HarmonyOS Next 与前端 Vue 通信交互相关小结;
    DevEco Studio NEXT Developer Preview2
    Vue js

    两端相互拟定好协议后,通过前端页面的点击事件,将所需的数据传输给原生移动端组件方法中,处理后将消息回传至前端.

    根据官方文档的案例尝试,但没成功 ... 后经过几经尝试后两端握手成功 ... (官方文档略显粗糙,好一番折腾)


    一.应用端

    基于 import web_webview from '@ohos.web.webview' 并初始化 Web 组件;
    调用 javaScriptProxy 方法,定义配置协议、参数和方法相关,具体可参考如下 code 片段;
    name: 交互协议
    object: 定义交互对象
    methodList: 交互对象中所涵盖的方法,支持多个,也可以通过一个对象方法再细化多个子方法
    controller: 组件

    import web_webview from '@ohos.web.webview'
    
    @Entry
    @Component
    export struct HomePage {
      controller: web_webview.WebviewController = new web_webview.WebviewController()
      ports: web_webview.WebMessagePort[] = [];
      nativePort: web_webview.WebMessagePort | null = null;
      message: web_webview.WebMessageExt = new web_webview.WebMessageExt();
    
      // 声明注册对象
      @State WebCallAppMethod: WebCallAppClass = new WebCallAppClass()
    
      aboutToAppear(): void {
        try {
          // 启用网页调试功能
          web_webview.WebviewController.setWebDebuggingAccess(true);
        } catch (error) {
          let e: business_error.BusinessError = error as business_error.BusinessError;
          console.log(`Error Code: ${e.code}, Message: ${e.message}`);
          this.controller.refresh(); // 页面异常,刷新
        }
      }
    
    
      build() {
        Row() {
          Column({ space: 20 }) {
            Web({ src: 'http://192.168.12.108:8080', controller: this.controller })
              .width('100%')
              .height('100%')
              .backgroundColor(Color.White)
              .multiWindowAccess(true)
              .javaScriptAccess(true)
              .geolocationAccess(true)
              .imageAccess(true)
              .onlineImageAccess(true)
              .domStorageAccess(true)
              .fileAccess(true)
              .mediaPlayGestureAccess(true)
              .mixedMode(MixedMode.All)
              .layoutMode(WebLayoutMode.FIT_CONTENT) // 自适应布局
              .verticalScrollBarAccess(true)
              .horizontalScrollBarAccess(false)
              .cacheMode(CacheMode.Default)
              .zoomAccess(false)// 禁止手势缩放
              .onConsole((event) => {
                console.log('[交互] - onConsole')
                LogUtils.info(event?.message.getMessage())
                return false
              })
              .onPageBegin(() => { // 页面加载中
                // this.registerWebJavaScript()
              })
              .onPageEnd(() => { // 页面加载完成
                console.log('[Web] - 页面加载完成')
                // this.registerWebJavaScript()
              })
              .onErrorReceive((event) => { // 异常: 无网络,页面加载错误时
                if (event) {
                  console.info('getErrorInfo:' + event.error.getErrorInfo());
                  console.info('getErrorCode:' + event.error.getErrorCode());
                  console.info('url:' + event.request.getRequestUrl());
                  console.info('isMainFrame:' + event.request.isMainFrame());
                  console.info('isRedirect:' + event.request.isRedirect());
                  console.info('isRequestGesture:' + event.request.isRequestGesture());
                  console.info('getRequestHeader_headerKey:' + event.request.getRequestHeader().toString());
                  let result = event.request.getRequestHeader();
                  console.info('The request header result size is ' + result.length);
                  for (let i of result) {
                    console.info('The request header key is : ' + i.headerKey + ', value is : ' + i.headerValue);
                  }
                }
              })
              .onHttpErrorReceive((event) => { // 异常: 网页加载资源 Http code >= 400 时
                if (event) {
                  console.info('url:' + event.request.getRequestUrl());
                  console.info('isMainFrame:' + event.request.isMainFrame());
                  console.info('isRedirect:' + event.request.isRedirect());
                  console.info('isRequestGesture:' + event.request.isRequestGesture());
                  console.info('getResponseData:' + event.response.getResponseData());
                  console.info('getResponseEncoding:' + event.response.getResponseEncoding());
                  console.info('getResponseMimeType:' + event.response.getResponseMimeType());
                  console.info('getResponseCode:' + event.response.getResponseCode());
                  console.info('getReasonMessage:' + event.response.getReasonMessage());
                  let result = event.request.getRequestHeader();
                  console.info('The request header result size is ' + result.length);
                  for (let i of result) {
                    console.info('The request header key is : ' + i.headerKey + ' , value is : ' + i.headerValue);
                  }
                  let resph = event.response.getResponseHeader();
                  console.info('The response header result size is ' + resph.length);
                  for (let i of resph) {
                    console.info('The response header key is : ' + i.headerKey + ' , value is : ' + i.headerValue);
                  }
                }
              })
              .onConfirm((event) => { // 提示框处理相关
                AlertDialog.show({
                  title: '温馨提示',
                  message: event?.message,
                  confirm: {
                    value: 'onAlert',
                    action: () => {
                      event?.result.handleConfirm()
                    }
                  },
                  cancel: () => {
                    event?.result.handleCancel()
                  }
                })
                return true;
              })
              .onShowFileSelector((event) => { // 文件上传处理相关
                console.log('MyFileUploader onShowFileSelector invoked');
                const documentSelectOptions = new picker.PhotoSelectOptions();
                let uri: string | null = null;
                const documentViewPicker = new picker.PhotoViewPicker();
                documentViewPicker.select(documentSelectOptions).then((documentSelectResult) => {
                  uri = documentSelectResult[0];
                  console.info('documentViewPicker.select to file succeed and uri is:' + uri);
                  if (event) {
                    event.result.handleFileList([uri]);
                  }
                }).catch((err: BusinessError) => {
                  console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
                })
                return true;
              })
              .javaScriptProxy({ // web call app
                // 对象注入 web
                object: this.WebCallAppMethod,
                name: 'WebCallApp', // AppCallWeb WebCallAppSsss  WebCallApp
                methodList: ['WebCallApp', 'CmdTest', 'CmdOpenUrl'],
                controller: this.controller
              })
          }.width('100%').height('100%')
        }
      }
    }
    
    interface CommandModel { // 泛型: 交互
      sn: string,
      args: Object,
      command: string,
    }
    
    class WebCallAppClass {
      constructor() {
      }
    
      WebCallApp(value: string): string { // 采用该 Json 对象模式,通过解析对象中的 type 后细化对应不同的子方法
        console.log('[交互] --- WebCallApp - 测试')
        console.log(value)
        let params:CommandModel = JSON.parse(value)
        console.info(params.sn)
        console.info(params.command)
        console.info(JSON.stringify(params.args))
        return '[交互] --- WebCallApp - 测试 - 回调123123123'
      }
    
      CmdOpenUrl(): Object {
        console.log('[交互] --- WebCallApp - CmdOpenUrl');
        new Object({
          'command': '111',
        })
        return Object;
      }
    
      CmdTest(value: string): string {
        console.log('[交互] --- WebCallApp - test');
        console.log(value);
        return '[交互] --- WebCallApp - test';
      }
    }
    

    二.前端

    前端配置有两种方式,可以通过 index.html 配置 js 后调用,也可以单独另起一个 js 类方法去适配,具体可参考如下 code 按需尝试;

    方式一.通过 index.html

    在项目根目录的 index.html 文件中添加如下 script 段落后,在业务所需的地方调用即可

    // index.html
    <script>
      // eruda.init()
      (function () {
        if (!window.applicationCache && typeof(Worker)=='undefined') {
          alert("E001-检测到您的环境不支持HTML5,程序终止运行!");//不支持HTML5
          return;
        }
    
        var global = window; // create a pointer to the window root
        if (typeof WebCallAppSsss === 'undefined') {
          global.WebCallApp = { // 此处的 WebCallApp 与原生端 javaScriptProxy 中的 name 相互匹配
            // 如下方法对应的是 javaScriptProxy 中 object 对象中的方法
            WebCallApp: (arg) => {},
            CmdOpenUrl: (arg) => {},
            CmdTest: (arg) => {},
          }; // create elf root if not existed
    
        }
    
        window.WebCallAppSsss.global = global; // add a pointer to window
      })();
    </script>
    
    // 业务所需的点击事件方法中调用
    methods : {
        onClickGoBack() {
            let str = WebCallAppSsss.CmdTest('[交互] - 测试'); // 方式一
            // this.webApp.WebCallApp('CmdTest', '111111'); // 方式二
            Toast.success('abc');
        },
    }
    

    方式二.通过自定义 js

    自定义 webCallApp 类,通过引入类方法调用

    // WebCallApp.js
    import {
      AppCallBacks,
      AppCommendBackHandlers,
    } from './AppMsgHandlers'
     
    export default {
      WebCallApp(command,args,callback,context) {
        /**
         * 交互
         *
         * 协议:command
         * 入参:args
         * 回调:callback
         * 参数:context
         * */
        if (typeof Elf.AppCallWeb != "undefined") {
          context = context || window;//默认为window对象
          args = args || {};
          let sn = null;
          //IOS调用相机--sn特殊处理
          if (command == "callCamera") {
            sn = "examCamera";
          } else {
            sn = this.getSerialNumber();//请求App统一加水单号
          }
          let params = {
            args : args,
            command : command,
            sn : sn,
          };
          //绑定回调函数
          if (callback) {
            AppCallBacks[ sn ] = {
              callback : callback,
              context : context
            };
          }
          if (window.webkit && window.webkit.messageHandlers) { // iOS
            params.sn = sn;
            window.webkit.messageHandlers[ "WebCallApp" ].postMessage(JSON.stringify(params));
          } else if (Elf.WebCallApp) { // Android
            params.sn = sn;
            Elf.WebCallApp(JSON.stringify(params));
          } else if (this.isInCef()) { // PC
            params.sn = sn;
            Elf.WebCallCef(JSON.stringify(params));
          } else if (this.isInHarmonyOS()) { // harmonyOS Next
            console.log('[鸿蒙] - 入参');
            console.log(params);
            let result = WebCallApp[ 'WebCallApp' ](JSON.stringify(params));
            console.log('[鸿蒙] - 回调');
            console.log(result);
            // callback(注:具体回调格式可自行定义,此处是对数据进行转JSon并Encode处理)
            if (result && typeof result == "string") {
              result = decodeURIComponent(result.replace(/\+/g, '%20'));
              try {
                result = JSON.parse(result);//解决空格变成+的问题
              } catch (error) {
                AppCallBacks[ sn ].callback.call(AppCallBacks[ sn ].context, result);
                return;
              }
              if (result.sn) {
                AppCallBacks[ sn ].callback.call(AppCallBacks[ sn ].context, result);
                return;
              }
            }
          } else {
            // ...
          }
        }
      },
      isInApp() {
        if (typeof Elf.AppCallWeb != "undefined") {
          return !!((window.webkit && window.webkit.messageHandlers) || typeof Elf.WebCallApp == "function" || typeof Elf.WebCallCef == "function");
        }
      },
      isInIOS() {
        return window.webkit && window.webkit.messageHandlers;
      },
      isInAndroid() {
        if (typeof Elf.AppCallWeb != "undefined") {
          return typeof Elf.WebCallApp == "function";
        }
      },
      isInHarmonyOS() {
        if (navigator.userAgent.toLowerCase().indexOf('openharmony') !== -1) {
          return true;
        } else {
          return false;
        }
      },
      getSerialNumber() {
        var uuid = this.UUID(3,8);
        return new Date().format("yyyyMMddhhmmssS") + uuid;
      },
      UUID(len,radix) {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
        var uuid = [],
          i;
        radix = radix || chars.length;
        if (len) {
          for (i = 0; i < len; i++) {
            uuid[i] = chars[0 | Math.random() * radix];
          }
        } else {
          var r;
          uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
          uuid[14] = '4';
          for (i = 0; i < 36; i++) {
            if (!uuid[i]) {
              r = 0 | Math.random() * 16;
              uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
            }
          }
        }
        return uuid.join('');
      },
    }
    
    /* eslint-disable */
    // AppMsgHandlers
    import webApp from './index';
    
    /*
     *
     * app对接
     * 移动端种植Elf对象
     * window => Elf
     * 
     */
    (function () {
      if (!window.applicationCache&&typeof(Worker)=='undefined') {
        alert("E001-检测到您的环境不支持HTML5,程序终止运行!");//不支持HTML5
        return;
      }
      // iOS & Android
      var global = window;//create a pointer to the window root
      if (typeof Elf === 'undefined') {
          global.Elf = {};//create elf root if not existed
      }
      Elf.global = global;//add a pointer to window
      // Harmony
      if (typeof WebCallApp === 'undefined') { // var global = window;
        global.WebCallApp = {
          WebCallApp: (args) => {}
        };
      }
      WebCallApp.global = global;
    })();
    
    var AppCallBacks = {},//动态数据流水列表
        AppCommendBackHandlers = [],//APP后退监听事件列表
        APPCommendBookStateHandlers = [],//下载状态监听事件列表
        AppCommendRefreshHandlers = [],//刷新监听事件列表
        APPCommendAddToBookShelfHandlers = [],//添加到书架监听事件列表
        APPCommendAddToObtainedBookHandlers = [],//添加到已获得图书列表监听
        APPCommendReBackHandlers = [],//监听重新回到页面通知
        AppCommendNetworkHandlers = [],//监听网络链接状态
        AppCommendAppStartingHandlers = [],//监听APP进入后台运行
        AppCommendAppReactivateHandlers = [],//监听APP重新进入前台运行
        AppCommendScreenShotss = [],//监听手机截屏
        AppCommendKeyboardBounceUp = [];//监听移动端软键盘事件
    
    if (typeof Elf != "undefined") {
      Elf.AppCallWeb = (sn,result) => {
        if (result && typeof result == "string") {
          result = decodeURIComponent(result.replace(/\+/g,'%20'));
          try {
            result = JSON.parse(result);//解决空格变成+的问题
          } catch (error) {
            AppCallBacks[sn].callback.call(AppCallBacks[sn].context,result);
            return;
          }
          if (result.sn) {
            AppCallBacks[sn].callback.call(AppCallBacks[sn].context,result.QrCode);
            return;
          }
        }
        if (AppCallBacks[sn]) {
          if (JSON.parse(result.opFlag)) {
            //执行对应回调
            AppCallBacks[sn].callback.call(AppCallBacks[sn].context,(typeof result.serviceResult == "string") ? JSON.parse(result.serviceResult) : result.serviceResult);
          } else {
            //接口调用返回失败信息,统一处理错误消息
            Toast(result.errorMessage ? result.errorMessage : "服务器异常!");
          }
          //调用完成删除对象
          delete AppCallBacks[sn];
        } else if (AppMsgHandlers[sn] && typeof AppMsgHandlers[sn] == "function") {
          //处理消息通知
          AppMsgHandlers[sn].call(window,result);
        }
      };
    }
    
    // if (typeof WebCallApp === 'undefined') { // var global = window;
    //   Elf.WebCallApp = {
    //     WebCallApp: (args) => {}
    //   };
    // }
    // window.WebCallApp.Elf = Elf;
    export {
      AppCallBacks,
      AppCommendBackHandlers
    }
    
    

    业务方法调用
    前端 Vue 部分

    // 业务所需的点击事件方法中调用
    methods : {
        onClickGoBack() {
            // let str = WebCallAppSsss.CmdTest('[交互] - 测试'); // 方式一
            // 方式二
            this.webApp.WebCallApp('CmdTest', {
              'JSBridge' : '交互测试'
            }, function (callback) {
              console.log('[AppCallWeb] - 回调');
              console.log(callback);
            });
            Toast.success('abc');
        },
    }
    

    鸿蒙端部分

    WebCallApp(value: string): string {
      console.log('[交互] --- WebCallApp - 测试')
      console.log(value)
      let params:CommandModel = JSON.parse(value)
      let sn = params.sn
      let command = params.command
      let args: Object = params.args
      console.info(sn)
      console.info(command)
      console.info(JSON.stringify(args))
     
      //let appCallWeb = AppCallWeb.shareInstance()
      //let callback = appCallWeb.callbackBasic(sn, args, null)
     
      return encodeURI(JSON.stringify(params))
      // return '[交互] --- WebCallApp - 测试 - 回调123123123'
    }
    

    以上便是此次分享的全部内容,希望能对大家有所帮助!

    相关文章

      网友评论

        本文标题:前端 Web 与原生应用端 WebView 通信交互 - Har

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