HarmonyOS

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

2024-06-04  本文已影响0人  survivorsfyh

基于鸿蒙 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'
}

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

上一篇下一篇

猜你喜欢

热点阅读