HarmonyOS开发实战:Web前端通信总结

背景

由于原有React Native项目中有提供一些app的方法给Web前端调用,现鸿蒙版本App也要提供一样的方法供Web前端调用(下文的ArkTs代码例子参考Developer Beta1文档和Api 12)

通信方式

  • UserAgent - 可以自定义一些简单数据拼接在原有UserAgent后,Web前端截取解析拼接的内容,例:OS环境标识(IOS、安卓、鸿蒙),App版本号等
  • Header - 鸿蒙中使用Web组件去请求url,可通过标准http协议请求因此可以在http请求上添加header来传递简单数据,但要求Web前端部署服务器有处理header能力
  • Cookie - 可传递登录状态等信息,App主动设置信息到Cookie,Web前端使用Web标准Api查询Cookie
  • JSBridge
  • 端口通信

鸿蒙提供了这几种数据传递方式,其中前3种方式适合传递简单数据,复杂的数据传输和交互需通过JSBridge和端口通信,下面重点介绍

JSBridge

JSBridge通信方式就是App和Web前端各自提供JavaScript方法供另外一端调用

App调用Web前端方法

把下面html代码保存成index.html文件,放入鸿蒙App项目代码的resources/rawfile文件夹下(如果resources下没有rawfile就新增一个rawfile文件夹)

<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<h1 id="text">标题</h1>
<script>
// 定义一个htmlTest方法
function htmlTest() {
  document.getElementById('text').style.color = 'green';
}
</script>
</body>
</html>

index.html文件内定义了htmlTest方法

// xxx.ets
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Button('调用Web方法')
        .onClick(() => {
          // 传递runJavaScript侧代码方法。
          this.webviewController.runJavaScript(`htmlTest()`);
        })
      Web({ src: $rawfile('index.html'), controller: this.webviewController })
    }
  }
}

WebComponet组件内有一个button组件,点击button会通过webviewController.runJavaScript()方法会执行Web前端定义的htmlTest方法

总结:App调用Web前端方法比较简单,App通过webviewController.runJavaScript()方法去执行Web前端提供的方法

Web前端调用App方法

// xxx.ets
import { webview } from '@kit.ArkWeb';

class testClass {
  constructor() {
  }

  test(): string {
    return 'ArkTS Hello World!';
  }
}

@Entry
@Component
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();
  // 声明需要注册的对象
  @State testObj: testClass = new testClass();

  build() {
    Column() {
      // Web组件加载本地index.html页面
      Web({ src: $rawfile('index.html'), controller: this.webviewController})
        // 将对象注入到web端
        .javaScriptProxy({
          object: this.testObj,
          name: "testObjName",
          methodList: ["test"],
          controller: this.webviewController
        })
    }
  }
}

WebComponent组件通过javaScriptProxy属性注入了testClass对象,前端可以通过testObjName对象调用开放的test方法,通过WebComponent组件加载的url前都会注入testClass对象

<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<button type="button" onclick="callArkTS()">Click Me!</button>
<p id="demo"></p>
<script>
    function callArkTS() {
        let str = testObjName.test();
        document.getElementById("demo").innerHTML = str;
    }
</script>
</body>
</html>

Web前端通过testObjName对象(该对象由App注入)调用test方法

App在初始化Web组件时会注入testClass对象供前端调用,后面通过Web组件加载的url前端页面都可以调用由App注入的testObjName对象,且在前端页面加载完成前就可以调用testObjName对象

端口通信

<!--port.html-->
<!DOCTYPE html>
<html>
<body>
<h1>WebView Message Port Demo</h1>
<button type="button" onclick="callArkTS()">Web Send msg!</button>
<p id="demo"></p>
</body>
<script>
    var h5Port;
    window.addEventListener('message', function (event) {
        if (event.data === '__init_port__') {
            if (event.ports[0] !== null) {
                h5Port = event.ports[0]; // 1. 保存从应用侧发送过来的端口。
                h5Port.onmessage = function (event) {
                  // 2. 接收ets侧发送过来的消息。
                  document.getElementById("demo").innerHTML = event.data;
                }
            }
        }
    })
    // 3. 使用h5Port向应用侧发送消息。
    function PostMsgToEts(data) {
        if (h5Port) {
          h5Port.postMessage(data);
        }
    }

    function callArkTS() {
        PostMsgToEts('Msg from web');
    }
</script>

前端实现步骤:

  1. Web前端通过window.addEventListener('message', function (event) {})方法监听__init_port__消息
  2. 通过__init_port__消息获取到端口对象h5Port
  3. 通过h5Port对象监听后续的消息发送和监听
import { webview } from '@kit.ArkWeb';

@Component
export struct PortWeb {
  webController: webview.WebviewController = new webview.WebviewController();
  ports: webview.WebMessagePort[] = [];

  aboutToAppear(): void {
    webview.WebviewController.setWebDebuggingAccess(true);
  }

  // 4、使用应用侧的端口给另一个已经发送到html的端口发送消息。
  sendMessage(msg: string) {
    if (this.ports && this.ports[1]) {
      this.ports[1].postMessageEvent(msg);
    }
  }

  build() {
    Column() {
      Button('ArkTS send msg!')
        .onClick(() => {
          this.sendMessage('Msg from ArkTS');
        })
      Web({ src: $rawfile('port.html'), controller: this.webController })
        .width('100%')
        .onPageEnd(() => {
          // 1、创建两个消息端口。
          this.ports = this.webController.createWebMessagePorts();
          // 2、在应用侧的消息端口(如端口1)上注册回调事件。
          this.ports[1].onMessageEvent((result: webview.WebMessage) => {
            console.info(`received message : ${result}`);
          })
          // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。
          this.webController.postMessage('__init_port__', [this.ports[0]], '*');
        })
    }
  }
}

App Web组件实现步骤:

  1. 在onPageEnd(webController.postMessage方法需要在onPageEnd后才能发送消息)创建2个消息端口
  2. 端口0经webController.postMessage方法发送的__init_port__消息发送到前端
  3. App持有端口1,后续端口1可以发送消息到前端持有的端口0

总结:App通过创建2个端口对象,自己持有一个,另一个发送给前端,后续通过这两个端口对象进行消息的收发

细心的小伙伴已经发现App Web组件是可以通过webController.postMessage方式给前端发送消息的,并且安卓Webview和React Native Webview也是可以直接用webview.postMessage方式发送消息到前端,但是鸿蒙在这里封装了一下,webController.postMessage方法的第二个参数只能是port端口对象,那么其他不是port类型的消息只能通过port.postMessage来发送

延伸:如果采用端口通信方式,前端封装的收发方式除了给鸿蒙版本App使用还有可能有RN、安卓、IOS等使用,在前端不变的情况下那么这几个客户端就得像鸿蒙一样封装一下,先通过webview.postMessage方法发送__init_port__消息把端口对象发送给前端,例如在RN Webview初始化时注入以下JS,后续通信通过channel.port对象,官方Web Api

const INJECTED_JAVASCRIPT = `
    const channel = new MessageChannel();
    function onMessage(e) {
      console.log('cong', e);
    };
    channel.port1.onmessage = onMessage;
    channel.port1.start();
    (function () {
      window.parent.postMessage("__init_port__", "*", [channel.port2]);
    })();
    true;
`;

JSBridge和端口通信总结

这2种方式都可以实现App和前端的通信,在鸿蒙中JSBridge方式是在App Web组件初始化时就注入的,前端页面不用等待页面加载完成就可以进行通信,而端口通信必须等待前端页面加载完成,选型可以参考鸿蒙的FAQ回答

我们团队目前没有大文件等场景,因此选用JSBridge方式,但上面JSBridge方案还有明显缺点,因此需要再优化封装一下

JSBridge进一步封装优化

App调用Web前端方法还是比较简单无须再封装,重点是封装Web前端调用App方法,

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="./css/main.css">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>testApp</title>
</head>
<body>
<button type="button" onclick="callArkTS()">Web Get Phone!</button>
<p id="demo"></p>
</body>
<script>
    function callArkTS() {
        window.ohosCallNative.callNative('getPhoneNum', {}, (data) => {
            document.getElementById("demo").innerHTML = data;
        })
    }
</script>
</html>
import { webview } from '@kit.ArkWeb';

class testClass {
  constructor() {
  }

  test(): string {
    return 'ArkTS Hello World!';
  }
}

@Entry
@Component
export struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();
  // 异步执行脚本
  injectCode = `
    const JSBridgeMap = {};
    let callID = 0;

    function JSBridgeCallback(id, params) {
      JSBridgeMap[id](params);
      JSBridgeMap[id] = null;
      delete JSBridgeMap[id];
    }

    window.ohosCallNative = {
      callNative(method, params, callback) {
        const id = callID++;
        const paramsObj = {
            msgId: id,
            data: params || null
        }
        JSBridgeMap[id] = callback || (() => {});
        JSBridgeHandle.call(method, JSON.stringify(paramsObj));
      }
    }
`;
  // 处理前端调用app方法
  call = (func: string, params: string): void => {
    const paramsObject: ParamsItem = JSON.parse(params);
    let result: Promise<string> = new Promise((resolve) => resolve(''));
    switch (func) {
      case 'getPhoneNum':
        result = this.getPhoneNum();
        break;
      default:
        break;
    }
    result.then((data: string) => {
      console.log('test ====', paramsObject.msgId);
      this.webviewController.runJavaScript(`JSBridgeCallback('${paramsObject?.msgId}', ${JSON.stringify(data)})`);
    })
  }
  getPhoneNum = (): Promise<string> => {
    let phone = '135*****123';
    return new Promise((resolve) => {
      resolve(phone);
    });
  }

  build() {
    Column() {
      // Web组件加载本地index.html页面
      Web({ src: $rawfile('index3.html'), controller: this.webviewController })
        .javaScriptProxy({
          object: {
            call: this.call
          },
          name: "JSBridgeHandle",
          methodList: ["call"],
          controller: this.webviewController
        })
        .javaScriptOnDocumentEnd([
          { script: this.injectCode, scriptRules: ['*'] },
        ])
    }
  }
}

interface ParamsItem {
  msgId: number,
  data: string,
}

App web组件在javaScriptOnDocumentEnd内注入了injectCode,用来处理异步操作,如果能说服前端把injectCode放在Web前端也是可以的

总结:当App有新方法提供需要在call方法内新增一个case,支持异步和同步方法,Web前端调用通过 window.ohosCallNative.callNative(method, params, callback)方法调用App提供的方法,然后在callback接受结果回调

最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?但是又不知道从哪里下手,而且学习时频繁踩坑,最终浪费大量时间。所以本人整理了一些比较合适的鸿蒙(HarmonyOS NEXT)学习路径和一些资料的整理供小伙伴学习

点击领取→纯血鸿蒙Next全套最新学习资料希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取~~

一、鸿蒙(HarmonyOS NEXT)最新学习路线

有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)…等技术知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料

二、HarmonyOS Next 最新全套视频教程

三、《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

四、大厂面试必问面试题

五、鸿蒙南向开发技术

六、鸿蒙APP开发必备


完整鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS学习资料

总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。 

                        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值