HarmonyOS开发--混合开发JSBridge

什么是混合开发?

混合开发是一种融合了原生开发和Web开发优势的移动应用开发方式。
具体来说,混合开发通常指的是利用一种框架或平台来创建应用程序,这种程序结合了原生应用的一些功能和特性(比如访问设备的摄像头、相册、GPS、蓝牙等),并且使用Web技术(HTML5、CSS和JavaScript)来编写大部分的应用代码。

1. Web容器

1.1克隆模板代码

模板仓库地址: https://gitee.com/yjh8866/meikou_mall_hybrid

1.2 创建Web容器组件

commons\basic\src\main\ets\components

MkWeb.ets

import { router } from '@kit.ArkUI'

@Component
export  struct MKWeb {
  src: ResourceStr = ''  // 加载的页面地址
  
  build() {
    Column(){
     Text('我是用于放web页面')
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.under'))
  }
}

Index.ets

export { MKWeb } from './src/main/ets/components/MkWeb'

WebPage.ets

import { router } from '@kit.ArkUI'
import { MKWeb } from '@mk/basic/Index'

@Entry
@Component
struct WebPage {
  src: ResourceStr = ''

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, string>
    // 1. 生产使用
    // this.src = 'resource://rawfile/index.html#' + params['src']
    // 2. 测试使用
    this.src = 'http://192.168.56.1:5173/#' + params['src']
  }

  build() {
    Column() {
      // 使用网页容器组件
      MKWeb({ src: this.src })
    }
  }
}

1.3创建页面导航栏

在这里插入图片描述

导航条: 宽度->100%, 高度->50+安全高度, 背景->白色, 上内边距: 安全高度
图标: 宽高->24, 外边距->13, 填充颜色 -> 文字颜色
文字: 字体大小->16, 加粗->500, 颜色->黑色, 居中, 最大行数1(超出则滚动)
右侧文字绑定一个下拉菜单

import { router } from '@kit.ArkUI'

@Component
export  struct MKWeb {
  // 当前网页的标题
  @Prop title: string = '美寇商城'
  // 从本地存储中获取顶部安全距离
  @StorageProp('safeTop') safeTop: number = 0

  /**
   * 回到web容器的上一个页面
   */
  webBack(){}

  /**
   * 回到上一个页面
   */
  webClose(){

  }

  @Builder
  MenuBuilder() {
    Menu() {
      MenuItem({ content: '首页' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 0 } }))
      MenuItem({ content: '分类' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 1 } }))
      MenuItem({ content: '购物袋' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 2 } }))
      MenuItem({ content: '我的' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 3 } }))
      MenuItem({ content: '刷新一下' })
        .onClick(() => {})
    }
    .width(100)
    .fontColor($r('app.color.text'))
    .font({ size: 14 })
    .radius(4)
  }

  build() {
    Column(){
      // 导航条
      Row() {
        Row() {
          Image($r("app.media.ic_public_left"))
            .iconStyle()
            .onClick(() => {
              this.webBack()
            })
            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 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.under'))
  }
}



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

1.4设置加载进度条

在这里插入图片描述

堆叠容器: 宽度->100%, 占满剩余空间, 顶部对齐
进度条: 线性/当前值/总值, 填充宽度 -> 2, 开启进度平滑动效, 颜色-> 红色, 层级->上,

 // 网页是否在加载中
 @State loading: boolean = true
 // 网页加载的进度
 @State progress: number = 0

// ....

// 堆叠组件
  Stack({ alignContent: Alignment.Top }){
    // 如果加载中, 则显示进度条插件
    if (this.loading) {
      Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
        .style({ strokeWidth: 2, enableSmoothEffect: true })
        .color($r('app.color.red'))
        .zIndex(1)
    }
  }
  .width('100%')
  .layoutWeight(1)

1.5 设置Web组件加载网页

在这里插入图片描述

  1. 配置文本容器组件
src: ResourceStr = 'https://m.suning.com'  // 加载的页面地址
// ...
// 网页加载的进度
@State progress: number = 0
// 页面视图控制器
controller = new webview.WebviewController()

// ...

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

  // web组件: 用于加载在线网页
  Web({ src: this.src, controller: this.controller })
}
.width('100%')
.layoutWeight(1)
  1. 设置进度条 显示/隐藏 和 加载进度
    onPageBegin: 开始加载网页时触发
    onPageEnd: 网页加载完成时触发
    onProgressChange: 网页加载进度变化时触发该回调
// 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
        })
      }
    }
  })
  .onPageBegin(() => { // 开始加载网页时触发
    this.progress = 0
    this.loading = true
    console.log('mk-logger', 'onPageBegin')
  })
  .onPageEnd(() => { // 网页加载完成时触发
    console.log('mk-logger', 'onPageEnd')
  })

1.6 导航栏相关操作处理

  1. 导航左侧处理
    返回处理: 在webview中, 如果当前页面之前有页面则控制器内返回, 如果没有则原生侧路由返回
    关闭处理: 原生侧路由返回
// ...
// 加载网页页面完成时触发该回调,用于应用更新其访问的历史链接
.onRefreshAccessedHistory(() => { 
  const history = this.controller.getBackForwardEntries()  // 获取当前Webview的历史信息列表
  this.historyCurrIndex = history.currentIndex  // 当前在页面历史列表中的索引
  this.historySize = history.size  // 历史列表中索引的数量,最多保存50条,超过时起始记录会被覆盖
  // AlertDialog.show({message: this.historyCurrIndex + '-' + this.historySize })
 })
  webBack() {
    // 如果在web容器中, 当前页面之前还有页面, 则容器内返回上一页
    if (this.historyCurrIndex > 0) {
      this.controller.backward()
    } else {
      router.back()
    }
  }

  webClose() {
    router.back()
  }
  1. 导航下拉菜单-刷新
 @Builder
  MenuBuilder() {
    Menu() {
      MenuItem({ content: '首页' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 0 } }))
      MenuItem({ content: '分类' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 1 } }))
      MenuItem({ content: '购物袋' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 2 } }))
      MenuItem({ content: '我的' })
        .onClick(() => router.back({ url: 'pages/Index', params: { index: 3 } }))
      MenuItem({ content: '刷新一下' })
        .onClick(() => this.controller.refresh())
    }
    .width(100)
    .fontColor($r('app.color.text'))
    .font({ size: 14 })
    .radius(4)
  }
  1. 导航条标题设置
// 网页document标题更改时触发该回调
.onTitleReceive((data) => { 
  console.log('mk-logger', 'onTitleReceive')
  this.title = data?.title || ''
})

2. 应用侧-JSBridge

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过分析, 在加载的网页中, 需要调用原生能力, 主要包括:

  1. 认证信息
    ● 查询用户信息
    ● 更新用户信息
    ● 移除用户
  2. 拍照服务/相册服务
  3. 传感器
  4. 本地省市区数据

2.1 注册JavaScript代理

实现语法: registerJavaScriptProxy(object: object, name: string, methodList: Array): void

webInit() {
    this.controller.registerJavaScriptProxy({ 
      // 参与注册的应用侧JavaScript对象。
      // 注册对象的名称,与window中调用的对象名一致。
      // 注册后window对象可以通过此名字访问应用侧JavaScript对象。
      // ...
    }, 'mk', [  
      // 参与注册的应用侧JavaScript对象的方法。
      // ...
    ])
}

// ...

Web({ src: this.src, controller: this.controller })
.onAppear(() => {  // 组件挂载显示时触发此回调
   this.webInit()
 })

2.2 认证信息 mk.queryUser mk.removeUser mk.updateUser

import { auth, MkUser } from '../utils/auth'

webInit() {
    this.controller.registerJavaScriptProxy({ 
      queryUser: (): MkUser => auth.queryUser(), // 查询用户
      removeUser: (): void => auth.removeUser(), // 移除用户
      updateUser: (user: MkUser): void => auth.updateUser(user),  // 更新用户
    }, 'mk', [  
      'queryUser',
      'updateUser',
      'removeUser',
    ])
}

2.3 拍照服务 mk.pickerCamera

  1. 打开相机后置摄像头得到拍照结果集
  2. 根据结果集的URI属性同步打开文件
  3. 以同步方法获取文件详细属性信息
  4. 定义缓冲区用于保存读取的文件
  5. 开始同步读取内容到缓冲区
  6. 读取完毕后关闭文件流
  7. 借助util工具方法把读取的文件流转成base64编码的字符串

CameraPlugin.ets

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()
import { cameraPlugin } from '../plugins/CameraPlugin'

webInit() {
    this.controller.registerJavaScriptProxy({ 
      queryUser: (): MkUser => auth.queryUser(), // 查询用户
      removeUser: (): void => auth.removeUser(), // 移除用户
      updateUser: (user: MkUser): void => auth.updateUser(user),  // 更新用户
      pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
    }, 'mk', [  
      'queryUser',
      'updateUser',
      'removeUser',
      'pickerCamera',
    ])
}

2.4 相册服务 mk.pickerPhoto

相册选择器
新版API
PhotoPlugin.ets

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

class PhotoPlugin {
  async pickerPhoto(){
    // 1. 打开相册选择图片
    let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 5;
    let photoPicker = new photoAccessHelper.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()
import { photoPlugin } from '../plugins/PhotoPlugin'

webInit() {
    this.controller.registerJavaScriptProxy({ 
      queryUser: (): MkUser => auth.queryUser(), // 查询用户
      removeUser: (): void => auth.removeUser(), // 移除用户
      updateUser: (user: MkUser): void => auth.updateUser(user),  // 更新用户
      pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
      pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),  // 调用相册
    }, 'mk', [  
      'queryUser',
      'updateUser',
      'removeUser',
      'pickerCamera',
    ])
}

2.5 传感器 mk.vibrator

SensorPlugin.ets

import { vibrator } from '@kit.SensorServiceKit'

class SensorPlugin {
  vibrator() {
    vibrator.startVibration({ type: 'time', duration: 50 }, { usage: 'touch' })
  }
}

export const sensorPlugin = new SensorPlugin()
// ...
import { sensorPlugin } from '../plugins/SensorPlugin'

webInit() {
    this.controller.registerJavaScriptProxy({ 
      queryUser: (): MkUser => auth.queryUser(), // 查询用户
      removeUser: (): void => auth.removeUser(), // 移除用户
      updateUser: (user: MkUser): void => auth.updateUser(user),  // 更新用户
      pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
      pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),  // 调用相册
      vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器
    }, 'mk', [  
      'queryUser',
      'updateUser',
      'removeUser',
      'pickerCamera',
    ])
}

2.6 本地数据 mk.getAreaColumns

  1. 定义读取的本地数据的数据类型(AreaDataItem)
  2. 定义输出数据的数据类型(AreaColumns)
  3. 读取rawfile目录下的本地文件area.json
  4. 将读取的字节数组转码成字符串
  5. 将读取的Json字符串转成对象数据
  6. 遍历对象数据并处理返回
    util工具函数

LocationPlugin.ets

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.decodeWithStream(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. 返回数据
       return areaColumns
     } catch (e) {
       return areaColumns
     }
   }
}

export const locationPlugin = new LocationPlugin()
// ...
import { AreaColumns, locationPlugin } from '../plugins/LocationPlugin'

webInit() {
    this.controller.registerJavaScriptProxy({ 
      queryUser: (): MkUser => auth.queryUser(), // 查询用户
      removeUser: (): void => auth.removeUser(), // 移除用户
      updateUser: (user: MkUser): void => auth.updateUser(user),  // 更新用户
      pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
      pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),  // 调用相册
      vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器
      getAreaColumns: (): Promise<AreaColumns> => locationPlugin.getAreaColumns(), // 获取省市区
    }, 'mk', [  
      'queryUser',
      'updateUser',
      'removeUser',
      'pickerCamera',
    ])
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值