HarmonyOS:WebView 控制及 H5 原生交互实现

发布于:2025-04-06 ⋅ 阅读:(9) ⋅ 点赞:(0)

一、效果展示               

                                                

二、技术栈
        

  1. 技术栈:
    • 编程语言:使用 TypeScript 进行开发,借助其类型系统提升代码的可读性与稳定性。

    • 框架与库:基于鸿蒙系统相关框架(如@kit.ArkWeb@hadss/hmrouter)进行开发,同时结合自定义的Index模块(其中包含authcameraPlugin等)来实现特定业务功能。

  2. 核心点:
    • 组件化开发:通过@Component装饰器定义MKWeb组件,组件内封装了各种状态(如srctitle等)和方法(如webBackwebClose等),使得代码结构清晰,易于维护。

    • WebView 集成:利用webview.WebviewController控制 WebView,实现页面加载、刷新、导航等功能,并通过一系列on事件(如onPageBeginonProgressChange等)监听 WebView 的状态变化,实时更新组件状态,为用户提供良好的交互体验。

    • H5 与原生交互:通过controller.registerJavaScriptProxy方法注册 JavaScript 代理,实现 H5 调用原生的用户信息查询、移除、更新以及相机相册调用等功能,打通了 H5 与原生应用的通信桥梁。

    • UI 构建:运用@Builder装饰器构建菜单和整体 UI 布局,使用ColumnRow等布局组件实现页面的合理排版,同时通过ImageText等组件展示界面元素,并且对Image组件进行扩展定义通用的图标样式。

三、详细源码及注解
                

// 导入所需的模块和类
import { auth, cameraPlugin, MkUser, SafeConstants } from "../../../../Index"
import { webview } from "@kit.ArkWeb"
import { HMRouterMgr } from "@hadss/hmrouter"

// 使用 @Component 装饰器定义一个组件
@Component
export struct MKWeb {
  // 定义加载的页面地址,初始值为空字符串
  src: ResourceStr = ''  // 加载的页面地址
  // 定义当前网页的标题,初始值为 '美寇商城'
  @State title: string = 'XX商城'
  // 从本地存储中获取顶部安全距离,初始值为 0
  @StorageProp(SafeConstants.TOP_HEIGHT) safeTop: number = 0
  // 定义是否正在加载的状态,初始值为 true
  @State isLoading: boolean = true
  // 定义加载进度,初始值为 0
  @State Progress:number = 0
  // 定义当前页面在历史记录中的索引,初始值为 0
  @State historyCurrIndex: number = 0
  // 定义当前页面在历史记录中的总长度,初始值为 0
  @State historySize: number = 0

  // 创建一个 WebviewController 实例,用于控制 WebView
  controller = new webview.WebviewController()

  /**
   * 回到web容器的上一个页面
   */
  webBack(){
    // 如果当前页面在历史记录中有前一个页面,则返回上一个页面
    if(this.historyCurrIndex > 0){
      this.controller.backward()
    }else{
      // 否则,关闭当前页面
      HMRouterMgr.pop()
    }
  }

  /**
   * 回到上一个页面
   */
  webClose(){
    // 关闭当前页面
    HMRouterMgr.pop()
  }

/*
 *
 * h5调用原生程序功能
 * */

  webInit(){
    // 注册 JavaScript 代理,允许 H5 调用原生功能
    this.controller.registerJavaScriptProxy({
      // 查询当前用户信息
      queryUser:():MkUser =>auth.getUser(),
      // 移除当前用户信息
      removeUser:():void =>auth.removeUser(),
      // 更新当前用户信息
      updateUser:(u:MkUser):void => auth.saveUser(u),
      // 调用相机拍照并返回照片路径
      pickerCamera:():Promise<string>=>cameraPlugin.pickerCamera(),
      // 调用相册选择照片并返回照片路径
      pickerPhoto:():Promise<string>=>cameraPlugin.pickerCamera()
    },'mk',[
      'queryUser',
      'removeUser',
      'updateUser',
      'pickerCamera',
      'pickerPhoto'
    ])
  }

  // 使用 @Builder 装饰器定义一个菜单构建器
  @Builder
  MenuBuilder() {
    Menu() {
      // 添加一个菜单项,点击时刷新页面
      MenuItem({ content: '刷新一下' })
        .onClick(() => {
          this.controller.refresh()
        })
    }
    .width(100)
    .fontColor($r('app.color.text'))
    .font({ size: 14 })
    .radius(4)
  }

  // 构建组件的 UI
  build() {
    Column(){
      /*----------------------------------导航条--------------------------------------*/
      Row() {
        Row() {
          // 添加返回按钮,点击时调用 webBack 方法
          Image($r("app.media.ic_public_left"))
            .iconStyle()
            .onClick(() => {
              this.webBack()
            })
          // 添加关闭按钮,点击时调用 webClose 方法
          Image($r('app.media.ic_public_close'))
            .iconStyle()
            .onClick(() => {
              this.webClose()
            })
        }
        .width(100)

        // 显示当前网页的标题
        Text(this.title)
          .fontSize(16)
          .fontWeight(500)
          .fontColor($r('app.color.black'))
          .layoutWeight(1)
          .maxLines(1)
          .textAlign(TextAlign.Center)
          .textOverflow({ overflow: TextOverflow.MARQUEE })
        Row() {
          Blank()
          // 添加更多操作按钮,绑定菜单
          Image($r('app.media.ic_public_more'))
            .iconStyle()
            .bindMenu(this.MenuBuilder)
        }
        .width(100)
      }
      .height(50 + this.safeTop)
      .backgroundColor($r('app.color.white'))
      .padding({ top: this.safeTop })
      /*---------------------------------堆叠布局-------------------------------------*/
      Stack({alignContent: Alignment.Top}) {
       // 如果正在加载,显示进度条
       if(this.isLoading){
         Progress({total: 100, value:this.Progress, type: ProgressType.Linear})
           .style({strokeWidth: 2,enableSmoothEffect: true})
           .color($r('app.color.red'))
           .zIndex(1)
       }
       // 添加 WebView 组件,加载指定页面
       Web({ src:this.src, controller:this.controller })
         // 页面开始加载时,设置 isLoading 为 true
         .onPageBegin(()=>{
           this.isLoading = true
         })
         // 页面加载进度变化时,更新 Progress 状态
         .onProgressChange((res)=>{
           this.Progress = res.newProgress
           // 如果加载完成,延迟 300 毫秒后设置 isLoading 为 false
           if(res.newProgress == 100){
             animateTo({duration: 300,delay:100}, ()=>{
               this.isLoading = false
             })
           }
         })
         // 页面加载完成时,不执行任何操作
         .onPageEnd(()=>{})
         // 刷新历史记录时,更新当前页面的历史记录索引和总长度
         .onRefreshAccessedHistory(()=>{
           const history = this.controller.getBackForwardEntries()
           this.historyCurrIndex = history.currentIndex
           this.historySize = history.size
         })
         // 接收到页面标题时,更新 title 状态
         .onTitleReceive((res)=>{
           this.title = res.title
         })
         // 页面显示时,初始化 WebView
         .onAppear(()=>{
           this.webInit()
         })

      }
        .width('100%')
        .layoutWeight(1)

    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.under'))
  }
}

// 扩展 Image 组件,定义图标样式
@Extend(Image)
function iconStyle() {
  .width(24)
  .aspectRatio(1)
  .fillColor($r('app.color.text'))
  .margin(13)
}

Web组件主要完成混合开发单页面业务,在其中集成了Loading加载动画,以及注入了鸿蒙原生的功能(图库、调用相机,省市区,注:省市区是一个JSON文件通常存放在Rawfile文件下,我这里的JSON未放上啦,需要可以自己手动添加一下

import { camera, cameraPicker } from "@kit.CameraKit";
import { fileIo } from "@kit.CoreFileKit";
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 = fileIo.openSync(pickerResult.resultUri)
    // 3. 同步读取文件的详情信息
    const stat = fileIo.statSync(file.fd)
    // 4. 定义缓冲区用于保存读取的文件
    const buffer = new ArrayBuffer(stat.size)
    // 5. 开始同步读取内容到缓冲区
    fileIo.readSync(file.fd, buffer)
    // 6. 读取完毕后关闭文件流
    fileIo.closeSync(file)


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

export const cameraPlugin = new CameraPlugin()
import { photoAccessHelper } from "@kit.MediaLibraryKit"
import { fileIo } from "@kit.CoreFileKit"
import { util } from "@kit.ArkTS"

class PhotoPlugin{
 async  pickPhoto(){
   // 1. 打开相册选择图片
    let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions()
   // 设置图片类型
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
   // 设置最多可选图片数量
    PhotoSelectOptions.maxSelectNumber = 1
   // 设置是否显示原图
    let photoPicker = new photoAccessHelper.PhotoViewPicker()
   // 调用相册选择图片
    const res = await photoPicker.select(PhotoSelectOptions)


   // 2. 文件操作
   // 2.1 获取照片的uri地址
   const uri = res.photoUris[0]
   // 2.2 根据uri同步打开文件
   const file = fileIo.openSync(uri)
   // 2.3 同步获取文件的详细信息
   const stat = fileIo.statSync(file.fd)
   // 2.4 创建缓冲区存储读取的文件流
   const buffer = new ArrayBuffer(stat.size)
   // 2.5 开始同步读取文件流到缓冲区
   fileIo.readSync(file.fd, buffer)
   // 2.6 关闭文件流
   fileIo.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()
import { util } from '@kit.ArkTS'

// 1. 定义读取的本地数据的数据类型(AreaDataItem)
export interface AreaDataItem {
  code: string
  name: string
  areaList: AreaDataItem[]
}

// 2. 定义输出数据的数据类型(AreaColumns)
export interface AreaColumns {
  province_list: Record<number, string>
  city_list: Record<number, string>
  county_list: Record<number, string>
}

class  LocationPlugin {
  async getAreaColumns(){
    // 1. 定义对象用于存储转换后的数据
    const areaColumns: AreaColumns = {
      province_list: {},
      city_list: {},
      county_list: {}
    }

    try {
      // 2. 读取rawfile目录下的本地文件
      const unit8Array = getContext().resourceManager.getRawFileContentSync('area.json')
      // 3. 将读取的字节数组转成字符串
      const decoder = new util.TextDecoder()
      const resStr = decoder.decodeToString(unit8Array)
      // 4. 将读取的Json字符串转成对象数组
      const areaData = JSON.parse(resStr) as AreaDataItem[]
      // 5. 遍历处理数据
      // 5.1 省转换
      areaData.forEach((province)=>{
        areaColumns.province_list[Number(province.code)] = province.name
        // 5.2 市转换
        province.areaList.forEach((city)=>{
          areaColumns.city_list[Number(city.code)] = province.name
          // 5.3 区转换
          city.areaList.forEach((county)=>{
            areaColumns.county_list[Number(county.code)] = county.name
          })
        })
      })
      // 6. 返回数据
      AlertDialog.show({message:JSON.stringify(areaColumns,null,4)})
      return areaColumns
    } catch (e) {
      return areaColumns
    }
  }
}

export const locationPlugin = new LocationPlugin()

四、总结

本篇代码主要围绕MKWeb组件展开,综合运用多种技术实现了 Web 相关的丰富功能。从技术栈的选型到各功能模块的核心实现,都展示了在鸿蒙系统下开发 Web 交互界面的思路。然而,代码在一些细节上存在不足,如代码重复、错误处理缺失以及配置灵活性问题。通过对这些要点的分析,开发者可以在类似项目中优化代码结构,增强应用的健壮性和可维护性,从而打造出更优质的 Web 相关应用功能。有需要的可自行改造,这里这里只封装了原生的功能,但是调用相机需要真机,有条件可以自行尝试,这里的源码仅供参考,有其他的需求可以参考改造。