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

一、效果展示

二、技术栈

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

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

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

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

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

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

三、详细源码及注解

javascript 复制代码
// 导入所需的模块和类
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未放上啦,需要可以自己手动添加一下

javascript 复制代码
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()
javascript 复制代码
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()
javascript 复制代码
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 相关应用功能。有需要的可自行改造,这里这里只封装了原生的功能,但是调用相机需要真机,有条件可以自行尝试,这里的源码仅供参考,有其他的需求可以参考改造。

相关推荐
一只栖枝4 小时前
华为 HCIE 大数据认证中 Linux 命令行的运用及价值
大数据·linux·运维·华为·华为认证·hcie·it
zhanshuo8 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
harmonyos
zhanshuo8 小时前
在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
harmonyos
whysqwhw13 小时前
鸿蒙分布式投屏
harmonyos
whysqwhw15 小时前
鸿蒙AVSession Kit
harmonyos
whysqwhw16 小时前
鸿蒙各种生命周期
harmonyos
whysqwhw17 小时前
鸿蒙音频编码
harmonyos
whysqwhw17 小时前
鸿蒙音频解码
harmonyos
whysqwhw18 小时前
鸿蒙视频解码
harmonyos
whysqwhw18 小时前
鸿蒙视频编码
harmonyos