鸿蒙混合开发与Web调用原生鸿蒙的能力

634 阅读5分钟

1.前言

在鸿蒙开发中,将 Web 页面与原生鸿蒙能力相结合可以极大地丰富应用的功能和用户体验。本笔记旨在记录在 Web 页面中调用原生鸿蒙的相册服务、拍照功能以及传感器的过程和要点。

本文承接前面的一篇文章继续对权限调用进行补充 本文具体讲的是在鸿蒙框架里使用了web组件,里面的代码内容全部是用前端语言框架来实现的,然后我们怎么在鸿蒙里组合两者之间的优势,一个是性能更好,权限更好管理,一个是代码更容易开发和直接拿来服用,所以这篇文章就是两者的结合,通过鸿蒙一个大的框架,里面包裹着前端html的内容,然后html还能使用鸿蒙相关的原生能力,极大的提高了开发成本和效率,接下来一起看一下一些简单的示例吧😊

2.注册JavaScript代理

通过代理实现原生和网页之间交互

首先新建一个html页面进行测试使用

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>权限测试</title>
</head>

<body>
  <h2>测试用网页</h2>
  <button>测试事件</button>
  <button id="pickerPhoto">相册服务</button>
  <button id="pickerCamera">拍照服务</button>
  <button id="vibrate">马达震动</button>
  <img width="200" />
  <p></p>
  
  
  <script>
  //逻辑...
   
  </script>
 
</body>
</html>

用浏览器打开这个html效果演示:

image.png

注入JavaScript对象到window对象中

aboutToAppear(): void {
//其他略...
//xx.xx.xx.xx 换成你的本地网络IP地址
拍照和传感器测试需要用真机,且手机和电脑要连接同一个wifi才可以
  this.src = 'http://xx.xx.xx.xx:5500/day01/test.html'
}
  1. vscode 通过 open with liveserver 运行
  2. 终端输入 ipconfig 查看 ip 地址

image.png 在src替换掉原来的ip地址

image.png 然后把代码再虚拟机或者真机运行起来,得到如图效果

recording.gif

在 Chrome 浏览器中使用什么进行调试?

  • 控制面板,开发者工具,F12

在 手机端 浏览器使用什么进行调试呢?

<!-- 引入调试控制台 -->
  <script src="https://cdn.bootcdn.net/ajax/libs/eruda/3.3.0/eruda.min.js"></script>
  <script>eruda.init();</script>

这个是在线的链接如果觉得加载太慢就把js文件建个文件夹放在前端框架里

h5-调试工具插件

image.png

3.弹窗测试

先来个简单的弹窗简单测试一下 在 DevEco Studio 里写:初始结构代码:

import { promptAction } from '@kit.ArkUI'
import { emitter } from '@kit.BasicServicesKit'
import { auth, cameraPlugin, EmitterKey, locationPlugin, MkUser, photoPlugin, sensorPlugin } from 'basic'
import { webview } from '@kit.ArkWeb'
import { AreaColumns } from 'basic/src/main/ets/utils/LocationPlugin'


@Builder
function WebViewBuilder() {
  WebView()
}

@Component
struct WebView {
  src: ResourceStr = ''
  @Consume
  pageStack: NavPathStack
  // 当前网页的标题
  @State title: string = '默认标题'
  // 从本地存储中获取顶部安全距离
  @StorageProp('safeTop') safeTop: number = 0
  // 网页是否在加载中
  @State loading: boolean = true
  // 网页加载的进度
  @State progress: number = 0
  // 页面视图控制器
  controller = new webview.WebviewController()

  aboutToAppear(): void {
    const params = this.pageStack.getParamByName('WebView')[0] as string
    promptAction.showToast({
      message: params
    })
    //this.src = 'http://192.168.34.111:5174/'
    //this.src = ' http://192.168.34.111:5500/day01/test.html'
    //this.src = 'http://192.168.254.12:5174/'
    this.src = 'http://192.168.254.12:5500/day01/test.html'

  }

  /**
   * 回到web容器的上一个页面
   */
  webBack() {
    if (this.controller.accessBackward()) {
      this.controller.backward()
    } else {
      this.pageStack.pop()
    }
  }

  /**
   * 回到上一个页面
   */
  webClose() {
    this.pageStack.pop()
  }

  toIndex(index: number) {
    this.pageStack.clear()
    emitter.emit(EmitterKey.CHANGE_TAB, {
      data: {
        index
      }
    })
  }

  @Builder
  MenuBuilder() {
    Menu() {
      MenuItem({ content: '首页' })
        .onClick(() => {
          this.toIndex(0)

        })
      MenuItem({ content: '分类' })
        .onClick(() => {
          this.toIndex(1)
        })
      MenuItem({ content: '购物袋' })
        .onClick(() => {
          this.toIndex(2)
        })
      MenuItem({ content: '我的' })
        .onClick(() => {
          this.toIndex(3)
        })
      MenuItem({ content: '刷新一下' })
        .onClick(() => {
          this.controller.refresh() // 刷新
        })
    }
    .width(100)
    .fontColor($r('[basic].color.text'))
    .font({ size: 14 })
    .radius(4)
  }

  build() {
    NavDestination() {
      Column() {
        // 使用网页容器组件
        // 导航条
        Row() {
          Row() {
            // 返回(网页)
            Image($r("[basic].media.ic_public_left"))
              .iconStyle()
              .onClick(() => {
                this.webBack()
              })
            // 关闭(鸿蒙页面)
            Image($r('[basic].media.ic_public_close'))
              .iconStyle()
              .onClick(() => {
                this.webClose()
              })
          }
          .width(100)

          Text(this.title)
            .fontSize(16)
            .fontWeight(500)
            .fontColor($r('[basic].color.black'))
            .layoutWeight(1)
            .maxLines(1)
            .textAlign(TextAlign.Center)
            .textOverflow({ overflow: TextOverflow.MARQUEE })
          Row() {
            Blank()
            Image($r('[basic].media.ic_public_more'))
              .iconStyle()
              .bindMenu(this.MenuBuilder)
          }
          .width(100)
        }
        .height(50 + this.safeTop)
        .backgroundColor($r('[basic].color.white'))
        .padding({ top: this.safeTop })

        // 后续放置 进度条+页面
        // 堆叠组件
        Stack({ alignContent: Alignment.Top }) {
          // 如果加载中, 则显示进度条插件
          if (this.loading) {
            Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
              .style({ strokeWidth: 2, enableSmoothEffect: true })
              .color($r('[basic].color.red'))
              .zIndex(1)
          }

          // web组件: 用于加载在线网页
          Web({ src: this.src, controller: this.controller })
            .onProgressChange((data) => { // 网页加载进度变化时触发该回调
              // 1. 进度条
              console.log('mk-logger', JSON.stringify(data)) // 新的加载进度,取值范围为0到100的整数
              if (data) {
                // 1.1 记录加载进度
                this.progress = data.newProgress
                // 1.2 如果加载进度完成
                if (data.newProgress === 100) {
                  // 1.3 动画让进度条消失
                  animateTo({ duration: 300, delay: 300 }, () => {
                    this.loading = false
                  })
                }
              }
            })
            .onAppear(() => {
              // 组件挂载显示时触发此回调
              this.controller.registerJavaScriptProxy({
                // 参与注册的应用侧JavaScript对象。
                // 注册对象的名称,与window中调用的对象名一致。
                // 注册后window对象可以通过此名字访问应用侧JavaScript对象。
                // ...
                // 测试代码
                //参数一,鸿蒙提供的方法源码
                sayHi: (info: object) => {
                  AlertDialog.show({
                    message: JSON.stringify(info)
                      .slice(0, 800)
                  })
                },
            
              },
                //参数2 注入 window 对象全局变量 (名字自己命名)
                'mk', [
                // 参与注册的应用侧JavaScript对象的方法。
                // ...
                // 测试代码
                //参数3
                  'sayHi',
             
                ])
            })
            .onPageBegin(() => { // 开始加载网页时触发
              this.progress = 0
              this.loading = true
              console.log('mk-logger', 'onPageBegin')
            })
            .onPageEnd(() => { // 网页加载完成时触发
              console.log('mk-logger', 'onPageEnd')
            })// 网页document标题更改时触发该回调
            .onTitleReceive((data) => {
              console.log('mk-logger', 'onTitleReceive')
              this.title = data?.title || ''
            })
        }
        .width('100%')
        .layoutWeight(1)

      }
    }
    .hideTitleBar(true)
  }
}

@Extend(Image)
function iconStyle() {
  .width(24)
  .aspectRatio(1)
  .fillColor($r('[basic].color.text'))
  .margin(13)
}

关键代码

.onAppear(() => {
  // 组件挂载显示时触发此回调
  this.controller.registerJavaScriptProxy({
    //参数一,鸿蒙提供的方法源码
    sayHi: (info: object) => {
      AlertDialog.show({
        message: JSON.stringify(info)
          .slice(0, 800)
      })
    },
  },
    //参数2 注入 window 对象全局变量
    'mk', [
    //参数3
      'sayHi',
    ])
})

在test.html里进行使用 关键代码

<script>
    document.querySelector('button').addEventListener('click', async () => {
      mk.sayHi('你好,可以弹窗吗?')
    })
  </script>

弹窗使用的是鸿蒙组件的原生能力,现在看一下在web里是否可以显示出来?

recording.gif

4.整合相册服务 mk.pickerPhoto

权限调用这些都需要调用一些工具类 这里提供一个PhotoPlugin.ets

import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';

class PhotoPlugin {
  async pickerPhoto(){
    // 1. 打开相册选择图片
    const photoSelectOptions = new picker.PhotoSelectOptions()
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;
    const  photoPicker = new picker.PhotoViewPicker();
    const res = await photoPicker.select(photoSelectOptions)
    console.log('mk-logger', 'photoPlugin', JSON.stringify(res))

    // 2. 文件操作
    // 2.1 获取照片的uri地址
    const uri = res.photoUris[0]
    // 2.2 根据uri同步打开文件
    const file = fs.openSync(uri)
    // 2.3 同步获取文件的详细信息
    const stat = fs.statSync(file.fd)
    // 2.4 创建缓冲区存储读取的文件流
    const buffer = new ArrayBuffer(stat.size)
    // 2.5 开始同步读取文件流到缓冲区
    fs.readSync(file.fd, buffer)
    // 2.6 关闭文件流
    fs.closeSync(file)

    // 3. 转成base64编码的字符串
    const helper = new util.Base64Helper()
    const str = helper.encodeToStringSync(new Uint8Array(buffer))
    console.log('mk-logger', 'photoPlugin-str', str)
    return str
  }
}
export const photoPlugin = new PhotoPlugin()

放在你的项目工具类里边,使用前记得导出 在WebView里进行使用 关键代码

.onAppear(() => {
  // 组件挂载显示时触发此回调
  this.controller.registerJavaScriptProxy({
    //参数一,鸿蒙提供的方法源码
    sayHi: (info: object) => {
      AlertDialog.show({
        message: JSON.stringify(info)
          .slice(0, 800)
      })
    },
  },
    //参数2 注入 window 对象全局变量
    'mk', [
    //参数3
    'sayHi',//弹窗
    pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
    ])
})

关键代码

<script>
    document.querySelector('button').addEventListener('click', async () => {
      mk.sayHi('你好,可以弹窗吗?')
    })
    // 相册服务 mk.pickerPhoto
    const pickerPhoto = document.querySelector("#pickerPhoto");
    pickerPhoto.addEventListener("click", async () => {
    const str = await mk.pickerPhoto();
     //显示图片
    document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
  </script>

ps: img 标签使用 base64 显示图片。

  • data:image/gif;base64,base64编码的gif图片数据
  • data:image/png;base64,base64编码的png图片数据
  • data:image/jpeg;base64,base64编码的jpeg图片数据

效果演示: 虚拟机没有图片可以用截屏功能截一张图片使用

recording.gif

5.拍照服务 mk.pickerCamera

相机选择器

文件管理

Util工具函数

需要通过真机测试,(真机和电脑需连接同一个 wifi)

  1. 真机和电脑连接 wifi
  2. 获取电脑连接 wifi 后的 ip 地址,替换掉原来的 ip 地址
  3. 电脑连接真机,编译到真机调试即可 这里提供一个拍照工具类创建并导出
import { camera, cameraPicker } from '@kit.CameraKit';
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';

class CameraPlugin {
   async pickerCamera(){
      // 1. 打开相机后置摄像头得到拍照结果集
      const pickerProfile: cameraPicker.PickerProfile = {
         cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
      };
      const pickerResult: cameraPicker.PickerResult = await cameraPicker.pick(getContext(),
         [cameraPicker.PickerMediaType.PHOTO], pickerProfile);


      // 2. 根据结果集的URI属性同步打开文件
      const file = fs.openSync(pickerResult.resultUri)
      // 3. 同步读取文件的详情信息
      const stat = fs.statSync(file.fd)
      // 4. 定义缓冲区用于保存读取的文件
      const buffer = new ArrayBuffer(stat.size)
      // 5. 开始同步读取内容到缓冲区
      fs.readSync(file.fd, buffer)
      // 6. 读取完毕后关闭文件流
      fs.closeSync(file)


      // 7. 借助util工具方法把读取的文件流转成base64编码的字符串
      const helper = new util.Base64Helper()
      const str = helper.encodeToStringSync(new Uint8Array(buffer))
      console.log('mk-logger', 'pickerCamera', str)
      return str
   }
}


export const cameraPlugin = new CameraPlugin()

放在你的项目工具类里边,使用前记得导出 在WebView里进行使用 关键代码

.onAppear(() => {
  // 组件挂载显示时触发此回调
  this.controller.registerJavaScriptProxy({
    //参数一,鸿蒙提供的方法源码
    sayHi: (info: object) => {
      AlertDialog.show({
        message: JSON.stringify(info)
          .slice(0, 800)
      })
    },
  },
    //参数2 注入 window 对象全局变量
    'mk', [
    //参数3
    'sayHi',//弹窗
    pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
    pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机拍照

    ])
})

关键代码

<script>
    document.querySelector('button').addEventListener('click', async () => {
      mk.sayHi('你好,可以弹窗吗?')
    })
    // 相册服务 mk.pickerPhoto
    const pickerPhoto = document.querySelector("#pickerPhoto");
    pickerPhoto.addEventListener("click", async () => {
    const str = await mk.pickerPhoto();
     //显示图片
    document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
    / 拍照服务 mk.pickerCamera
    const pickerCamera = document.querySelector("#pickerCamera");
    pickerCamera.addEventListener("click", async () => {
      const str = await mk.pickerCamera();
      console.log(str);
      document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
    });
  </script>

拍照功能必须使用真机才能测试 投屏演示

recording.gif

6.传感器 mk.vibrator

创建并导出工具类SensorPlugin.ets

import { vibrator } from '@kit.SensorServiceKit'

class SensorPlugin {
  vibrator() {
    vibrator.startVibration({ type: 'time', duration: 50 }, { usage: 'touch' })                  //duration: 50 震动时间(毫秒)
  }
}

export const sensorPlugin = new SensorPlugin()

在WebView里进行使用 关键代码

.onAppear(() => {
  // 组件挂载显示时触发此回调
  this.controller.registerJavaScriptProxy({
    //参数一,鸿蒙提供的方法源码
    sayHi: (info: object) => {
      AlertDialog.show({
        message: JSON.stringify(info)
          .slice(0, 800)
      })
    },
  },
    //参数2 注入 window 对象全局变量
    'mk', [
    //参数3
    'sayHi',//弹窗
    pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
    pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机拍照
    vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器

    ])
})

关键代码

<script>
    document.querySelector('button').addEventListener('click', async () => {
      mk.sayHi('你好,可以弹窗吗?')
    })
    // 相册服务 mk.pickerPhoto
    const pickerPhoto = document.querySelector("#pickerPhoto");
    pickerPhoto.addEventListener("click", async () => {
    const str = await mk.pickerPhoto();
     //显示图片
    document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
    / 拍照服务 mk.pickerCamera
    const pickerCamera = document.querySelector("#pickerCamera");
    pickerCamera.addEventListener("click", async () => {
      const str = await mk.pickerCamera();
      console.log(str);
      document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
    });
     // 传感器  马达震动 mk.vibrate
    const vibrate = document.querySelector("#vibrate");
    vibrate.addEventListener("click", async () => {
      mk.vibrator();
    });
  </script>

另外震动还需要获取传感器权限: module.json5里进行配置


{

  "module": {

    "requestPermissions": [

      {

        "name": "ohos.permission.VIBRATE"

      }

    ],

    "name": "basic",

    "type": "shared",

    "description": "$string:shared_desc",

    "deviceTypes": [

      "phone",

      "tablet",

      "2in1"

    ],

    "deliveryWithInstall": true,

    "pages": "$profile:main_pages"

  }

}

震动效果展示不了,可以用真机去测试一下

7.结语

在鸿蒙开发中,探索将 Web 页面与原生鸿蒙能力相结合的过程充满挑战与惊喜。通过注册 JavaScript 代理、实现弹窗测试、整合相册服务、拍照服务以及调用传感器,我们逐步拓展了应用的功能边界,为用户带来更加丰富和个性化的体验。

尽管在这个过程中可能会遇到各种技术难题,如权限配置、真机调试的复杂性等,但每一次的突破都让我们更加深入地理解了鸿蒙生态的强大潜力。随着技术的不断进步和鸿蒙开发社区的日益壮大,相信未来我们能够在 Web 页面与原生能力融合的道路上创造出更多令人瞩目的应用场景,为用户提供更加便捷、高效和创新的交互体验。

让我们继续在鸿蒙开发的征程上砥砺前行,不断探索新的可能性,为打造更加出色的鸿蒙应用而努力。