鸿蒙开发之封装webView组件(API12)

399 阅读5分钟
  • 本文主要分析如何封装一个鸿蒙web组件。包括顶部导航条(含返回、关闭、标题展示和路由菜单功能)、底部 webview、加载进度条等功能的实现。阐述了组件布局、事件处理(如加载进度、历史记录、标题更新),还有父组件调用子组件和菜单操作设置。

1. 组件设计展示

2. 组件功能包含

  • 顶部的导航条
    • 返回上一个网页,以及关闭网页
    • 网页的标题动态展示(网页document标题)
    • 右上角的路由菜单
  • 底部的webview
  • 加载时的进度条

3. 封装web组件实现

3.1. 完成组件静态布局

  • src/main/ets/utils/WebComponent.ets 新建组件
  • 完成顶部导航条 静态布局
@Component
export struct WebComponent {
  src: ResourceStr = '' // 加载的页面地址
  // 当前网页的标题
  @Prop title: string = '标题'
  // 从本地存储中获取顶部安全距离
  @StorageProp('safeTop') safeTop: number = 0

  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.MenuBuilderParam)
        }
        .width(100)
      }
      .height(50 + this.safeTop)
      .backgroundColor($r('app.color.white'))
      .padding({ top: this.safeTop })
  }
}


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

3.2. 添加web组件

  • 使用堆叠组件将loading容器 和 web容器都放置在一起,
// 堆叠组件
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)
  • 此时的UI结构

3.3. 添加进度条功能

  • 使用web中的 onProgressChange 事件,此时回调会返回了一个进度数字,我们将他保存下来
// 网页是否在加载中
@State loading: boolean = true
// 网页加载的进度
@State progress: number = 0
  
....

// 如果加载中, 则显示进度条插件
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 })
  .onProgressChange((data) => { // 网页加载进度变化时触发该回调
    // 1. 进度条 新的加载进度,取值范围为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 事件里也添加上
web() 
  ....
// 开始加载网页时触发
.onPageBegin(() => { 
  this.progress = 0
  this.loading = true
})

3.4. 记录这个网页点击的历史连接,方便后面返回以及退出

  • 设置web中的 onRefreshAccessedHistory
historyCurrIndex: number = 0
historySize: number = 0

.onRefreshAccessedHistory(() => { // 加载网页页面完成时触发该回调,用于应用更新其访问的历史链接
    const history = this.controller.getBackForwardEntries() // 获取当前Webview的历史信息列表
    this.historyCurrIndex = history.currentIndex // 当前在页面历史列表中的索引
    this.historySize = history.size // 历史列表中索引的数量,最多保存50条,超过时起始记录会被覆盖
    // AlertDialog.show({ message: this.historyCurrIndex + '-' + this.historySize })
  })

3.5. 网页的标题动态展示(网页document标题)

  • 设置web的 onTitleReceive 事件
@Prop title: string = '标题'
  
.onTitleReceive((data) => { // 控制title
  this.title = data?.title || ''
})

3.6. 点击返回 && 点击退出

  • 添加点击事件
// 左侧图标
Row() {
  Image($r("app.media.ic_public_left"))
    .iconStyle()
    .onClick(() => {
      this.webBack()
    })
  Image($r('app.media.ic_public_close'))
    .iconStyle()
    .onClick(() => {
      this.webClose()
    })
}
  • 添加相对应的逻辑
/**
 * 回到web容器的上一个页面
 */
webBack() {
  // 如果在web容器中, 当前页面之前还有页面, 则容器内返回上一页
  if (this.historyCurrIndex > 0) {
    this.controller.backward()
  } else {
    router.back()
  }
}

/**
 * 回到上一个页面
 */
webClose() {
  router.back()
}

3.7. 父组件调用子组件

  • index.ets
import { router } from '@kit.ArkUI'
import { WebComponent } from '../utils/WebComponent'

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

  aboutToAppear(): void {
    this.src = 'https://m.suning.com/'
  }

  build() {
    Column() {
      // 使用网页容器组件
      WebComponent({MenuBuilderParam: this.MenuBuilder,src:'https://m.suning.com' })
    }
  }
}

3.8. 设置右上角菜单操作

  • 我们可以构建一个builder去实现,不过由于使用组件是可能需要传入的操作不一致,所以我们又父组件传入到子组件中
// webComponent
@Builder
MenuBuilder() {}
@BuilderParam MenuBuilderParam: () => void = this.MenuBuilder;

// 右侧菜单
Row() {
  Blank()
  Image($r('app.media.ic_public_more'))
    .iconStyle()
    .bindMenu(this.MenuBuilderParam)
}

// 父组件传递
@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)
}

build() {
  Column() {
    // 使用网页容器组件
    WebComponent({MenuBuilderParam: this.MenuBuilder,src:'https://m.suning.com' })
  }
}

4. 完整代码

import { router } from '@kit.ArkUI'
import { webview } from '@kit.ArkWeb'

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

  // 右上角菜单项 使用自定义组件的自定义构建函数初始化@BuilderParam
  @Builder
  MenuBuilder() {
  }

  @BuilderParam MenuBuilderParam: () => void = this.MenuBuilder;

  /**
   * 回到web容器的上一个页面
   */
  webBack() {
    // 如果在web容器中, 当前页面之前还有页面, 则容器内返回上一页
    if (this.historyCurrIndex > 0) {
      this.controller.backward()
    } else {
      router.back()
    }
  }

  /**
   * 回到上一个页面
   */
  webClose() {
    router.back()
  }

  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.MenuBuilderParam)
        }
        .width(100)
      }
      .height(50 + this.safeTop)
      .backgroundColor($r('app.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('app.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
                })
              }
            }
          })
          .onPageBegin(() => { // 开始加载网页时触发
            this.progress = 0
            this.loading = true
            console.log('mk-logger', 'onPageBegin')
          })
          // .onPageEnd(() => { // 网页加载完成时触发
          //   console.log('mk-logger', 'onPageEnd')
          // })
          .onRefreshAccessedHistory(() => { // 加载网页页面完成时触发该回调,用于应用更新其访问的历史链接
            const history = this.controller.getBackForwardEntries() // 获取当前Webview的历史信息列表
            this.historyCurrIndex = history.currentIndex // 当前在页面历史列表中的索引
            this.historySize = history.size // 历史列表中索引的数量,最多保存50条,超过时起始记录会被覆盖
            // AlertDialog.show({ message: this.historyCurrIndex + '-' + this.historySize })
          })
          .onTitleReceive((data) => { // 控制title
            console.log('mk-logger', 'onTitleReceive')
            this.title = data?.title || ''
          })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.under'))
  }
}


@Extend(Image)
function iconStyle() {
  .width(24)
  .aspectRatio(1)
  .fillColor($r('app.color.text'))
  .margin(13)
}
  • index.ets
import { router } from '@kit.ArkUI'
import { WebComponent } from '../utils/WebComponent'

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

  aboutToAppear(): void {
    // 2. 测试使用
    this.src = 'https://m.suning.com/'
  }

  @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)
  }

  build() {
    Column() {
      // 使用网页容器组件
      WebComponent({MenuBuilderParam: this.MenuBuilder,src:'https://m.suning.com' })
    }
  }
}