本案例基于ArkTS的声明式开发范式的样例,主要介绍了Web组件如何加载本地、云端的H5和Vue页面。样例主要包含以下功能:

  1. Web组件加载H5页面。
  2. Web组件加载Vue页面。

1. 案例效果截图

2. 案例运用到的知识点

2.1. 核心知识点

  • Web:提供具有网页显示能力的Web组件。
  • runJavaScript:异步执行JavaScript脚本,并通过回调方式返回脚本执行的结果。
  • 在配置文件module.json5文件里添加网络权限:ohos.permission.INTERNET。
  • 搭建服务端环境。

2.2. 其他知识点

  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Local/@Provider/@Consumer
  • 渲染控制:if/ForEach
  • 自定义组件和组件生命周期
  • 自定义构建函数@Builder
  • @Extend:定义扩展样式
  • Navigation:导航组件
  • 内置组件:Stack/Progress/Image/Column/Row/Text/Button
  • 常量与资源分类的访问
  • MVVM模式

3. 代码结构

├──entry/src/main/ets        // 代码区
│  ├──common
│  │  └──Constant.ets        // 常量类
│  ├──entryability            
│  │  └──EntryAbility.ets    // 程序入口类
│  ├──pages
│  │  ├──MainPage.ets        // 主页入口文件
│  │  ├──VuePage.ets         // 抽奖页入口文件(Vue页面)
│  │  └──WebPage.ets         // 抽奖页入口文件(H5页面)
│  └──viewmodel                          
│     └──NavigatorBean.ets   // 导航model
├──entry/src/main/resources  
│  ├──base
│  │  ├──element             // 尺寸、颜色、文字等资源文件存放位置
│  │  ├──media               // 媒体资源存放位置
│  │  └──profile             // 页面配置文件存放位置
│  ├──rawfile                // 本地html代码存放位置 
├──HttpServerOfWeb           // H5服务端代码
└──HttpServerOfWeb           // Vue服务端代码

4. 公共文件与资源

本案例涉及到的常量类和工具类代码如下:

4.1. 通用常量类

// entry/src/main/ets/common/Constants.ets
export class CommonConstant {
  static readonly WEB_PAGE_URI: string = 'pages/WebPage'
  static readonly VUE_PAGE_URI: string = 'pages/VuePage'
  static readonly LOCAL_PATH: Resource = $rawfile('local/index.html')
  static readonly VUE_PATH: string = 'resource://rawfile/vue3/index.html'
  static readonly VUE_CLOUD_PATH:string = 'http://192.168.31.150:5173/index.html'
  static readonly CLOUD_PATH: string = 'http://192.168.31.150:3000/index.html'
  static readonly WEB_ALERT_DIALOG_TEXT_VALUE: string = '恭喜您抽中:'
  static readonly WEB_CONSTANT_FULL_WIDTH: string = '100%'
  static readonly WEB_CONSTANT_FULL_HEIGHT: string = '100%'
  static readonly WEB_CONSTANT_WIDTH: string = '93.3%'
  static readonly WEB_CONSTANT_HEIGHT: string = '55.9%'
  static readonly WEB_CONSTANT_MARGIN_TOP: string = '7.1%'
  static readonly WEB_CONSTANT_MARGIN_LEFT: string = '3.3%'
  static readonly WEB_CONSTANT_MARGIN_RIGHT: string = '3.3%'
  static readonly WEB_CONSTANT_TOP_ROW_HEIGHT: string = '7.2%'
  static readonly WEB_CONSTANT_IMAGE_WIDTH: string = '5.6%'
  static readonly WEB_CONSTANT_IMAGE_HEIGHT: string = '32%'
  static readonly WEB_CONSTANT_IMAGE_MARGIN_LEFT: string = '7.2%'
  static readonly WEB_CONSTANT_TEXT_VALUE_WIDTH: string = '35.6%'
  static readonly WEB_CONSTANT_TEXT_VALUE_HEIGHT: string = '3.1%'
  static readonly WEB_CONSTANT_TEXT_VALUE_MARGIN_TOP: string = '3.1%'
  static readonly WEB_CONSTANT_TEXT_VALUE_FONT_SIZE: number = 18
  static readonly WEB_CONSTANT_TEXT_VALUE_FONT_WEIGHT: number = 500
  static readonly WEB_CONSTANT_TIP_TEXT_VALUE_WIDTH: string = '15.6%'
  static readonly WEB_CONSTANT_TIP_TEXT_VALUE_HEIGHT: string = '2.4%'
  static readonly WEB_CONSTANT_TIP_TEXT_VALUE_FONT_SIZE: number = 14
  static readonly WEB_CONSTANT_TIP_TEXT_VALUE_MARGIN_TOP: string = '0.9%'
  static readonly WEB_CONSTANT_TIP_TEXT_VALUE_OPACITY: number = 0.6
  static readonly WEB_CONSTANT_TOP_TEXT_WIDTH: string = '82.2%'
  static readonly WEB_CONSTANT_TOP_TEXT_HEIGHT: string = '50%'
  static readonly WEB_CONSTANT_TOP_TEXT_MARGIN_LEFT: string = '5%'
  static readonly WEB_CONSTANT_TOP_TEXT_FONT_SIZE: number = 20
  static readonly WEB_CONSTANT_BUTTON_WIDTH: string = '86.7%'
  static readonly WEB_CONSTANT_BUTTON_HEIGHT: string = '5.1%'
  static readonly WEB_CONSTANT_BUTTON_MARGIN_TOP: string = '10%'
  static readonly WEB_CONSTANT_BUTTON_BORDER_RADIUS: string = '20'
  static readonly WEB_CONSTANT_BUTTON_FONT_SIZE: number = 16
  static readonly WEB_CONSTANT_DURATION: number = 3000
  static readonly WEB_CONSTANT_PROGRESS_MIN: number = 0
  static readonly WEB_CONSTANT_PROGRESS_MAX: number = 100
  static readonly WEB_CONSTANT_PROGRESS_STEP: number = 10
  static readonly WEB_CONSTANT_MILLI_SECONDS: number = 100
  static readonly WEB_CONSTANT_PROGRESS_STROKE_WIDTH: number = 15
  static readonly WEB_CONSTANT_PROGRESS_SCALE_COUNT: number = 15
  static readonly WEB_CONSTANT_PROGRESS_SCALE_WIDTH: number = 5
  static readonly WEB_CONSTANT_PROGRESS_WIDTH: number = 80
  static readonly WEB_CONSTANT_PROGRESS_POSITION_X: string = '40%'
  static readonly WEB_CONSTANT_PROGRESS_POSITION_Y: string = '30%'
  static readonly MAIN_CONSTANT_FULL_HEIGHT: string = '100%'
  static readonly MAIN_CONSTANT_IMAGE_HEIGHT: string = '38.7%'
  static readonly MAIN_CONSTANT_BUTTON_MARGIN_TOP_BUTTON: string = '3.1%'
  static readonly MAIN_CONSTANT_BUTTON_HEIGHT: string = '5.1%'
  static readonly MAIN_CONSTANT_BUTTON_MARGIN_TOP: string = '4.6%'
  static readonly MAIN_CONSTANT_BUTTON_WIDTH: string = '86.7%'
  static readonly MAIN_CONSTANT_BUTTON_BORDER_RADIUS: number = 20
  static readonly MAIN_CONSTANT_BUTTON_FONT_SIZE: number = 16
}

本案例涉及到的资源文件如下:

4.2. string.json

// entry/src/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "模块描述"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "Web"
    },
    {
      "name": "promptsLocal",
      "value": "本地H5小程序"
    },
    {
      "name": "promptsCloud",
      "value": "云端H5小程序"
    },
    {
      "name": "loadLocalH5",
      "value": "加载本地H5"
    },
    {
      "name": "loadCloudH5",
      "value": "加载云端H5"
    },
    {
      "name": "loadVue",
      "value": "加载本地Vue页面"
    },
    {
      "name": "loadCloudVue",
      "value": "加载云端Vue页面"
    },
    {
      "name": "loadInterceptVue",
      "value": "加载拦截请求页面"
    },
    {
      "name": "textValue",
      "value": "以上为Web组件"
    },
    {
      "name": "local",
      "value": "(本地)"
    },
    {
      "name": "online",
      "value": "(在线)"
    },
    {
      "name": "btnValue",
      "value": "点击抽奖"
    },
    {
      "name": "prompts",
      "value": "抽奖应用"
    },
    {
      "name": "return_home",
      "value": "返回"
    },
    {
      "name": "web_alert_dialog_button_value",
      "value": "确定"
    },
    {
      "name": "web_width",
      "value": "93.3%"
    },
    {
      "name": "internet_err",
      "value": "网络加载失败!"
    },
    {
      "name": "reason",
      "value": "Used to initiate network data requests."
    }
  ]
}

4.3. color.json

// entry/src/main/resources/base/element/color.json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "blue",
      "value": "#007DFF"
    },
    {
      "name": "black",
      "value": "#000000"
    },
    {
      "name": "navy_blue",
      "value": "#F1F3F5"
    },
    {
      "name": "text_value_font_color",
      "value": "#182431"
    }
  ]
}

其他资源请到源码中获取。

5. 前端页面

本项目的前端页面分为两类:一类采用纯原生Web技术实现,另一类基于Vue框架开发。每种类型的前端页面又分别包含本地版本和云端版本。如下图所示,rawfile目录下的local和vue3目录存放的是本地静态前端文件,而项目根目录下的HttpServerOfWeb和HttpServerOfVue目录则用于模拟云端环境,包含前端构建工具,并可在Node.js环境下启动服务。

5.1. 原生Web抽奖代码

// HttpServerOfWeb/public/js/index.js

// 奖品名称数组
let prizesArr = ["啤酒", "奶茶", "汉堡", "咖啡", "西瓜", "鸡腿", "柠檬", "蛋糕"]
// 图片数组
let arrBigImg = ["./img/1-beer.png", "./img/2-milk.png", "./img/3-hamburg.png",
  "./img/4-coffee.png", "./img/5-watermelon.png", "./img/6-drumstick.png",
  "./img/7-lemon.png", "./img/8-cake.png", "./img/9-prizes.png"]

// 获取所有奖品单元
let allPrizesLi = document.querySelectorAll('.prizes-li')
// 获取奖品图片
let prizesImg = document.querySelectorAll('.pic')

// 旋转的初始值
let count = 0
let isClick = true
let index = 3
// 旋转到哪个位置
let prizesPosition = 0

// 绑定图片
for (let j = 0; j < prizesImg.length; j++) {
  prizesImg[j].src = arrBigImg[j]
}
// 旋转速度,数值越大,速度越慢
let speed = 500

// 旋转函数
function roll() {
  // 速度递减
  speed -= 50
  if (speed <= 10) {
    speed = 10
  }

  // 每次调用时移除所有的激活类名
  for (let j = 0; j < allPrizesLi.length; j++) {
    allPrizesLi[j].classList.remove('active')
  }
  prizesPosition++

  // 计算圈数
  if (prizesPosition >= allPrizesLi.length - 1) {
    prizesPosition = 0
    count++
  }

  allPrizesLi[prizesPosition].classList.add('active')
  let initSpeed = 500
  let timer
  // 最少旋转的总圈数
  let totalCount = 5

  // 当达到指定圈数且达到指定位置时停止
  if (count >= totalCount && (prizesPosition + 1) === index) {
    clearTimeout(timer)
    isClick = true
    speed = initSpeed
    // 等待 1 秒后打开弹窗
    timer = setTimeout(openDialog, 1000)
  } else {
    // 等待 1 秒后继续旋转
    timer = setTimeout(roll, speed)
    // 最后一圈减速
    if (count >= totalCount - 1 || speed <= 50) {
      speed += 100
    }
  }
}

// 抽奖启动函数
function startDraw() {
  // 防止多次触发抽奖
  if (isClick) {
    count = 0
    // 随机生成中奖位置
    index = Math.floor(Math.random() * prizesArr.length + 1)
    roll()
    isClick = false
  }
}

// 打开弹窗
function openDialog() {
  linkObj.messageFromHtml(prizesArr[prizesPosition])
}

关键代码说明:

  • openDialog函数会在应用端调用。
  • linkObj.messageFromHtml由应用端定义并调用,用于处理从网页端传递的消息。

5.2. Vue抽奖代码

// HttpServerOfVue/public/communication.js

function startDraw() {
  if (isClick) {
    count = 0;
    index = Math.floor(Math.random() * prizesArr.length + 1)
    roll();
    isClick = false;
  }
}

function openDialog() {
  linkObj.messageFromHtml(prizesArr[prizesPosition])
}
document._initMsg = 'hello vue'

function outWeb(){
  document._changeMsg('I am Web')
}
document._sendMsgToWeb = (val) =>{
  linkObj.messageFromHtml(val)
}

关键代码说明:

  • openDialog函数会在应用端调用。
  • linkObj.messageFromHtml由应用端定义并调用,用于处理从网页端传递的消息。

全部前端资源请到源码中获取。

6. Web组件

6.1. 首页面

// entry/src/main/ets/pages/MainPage.ets
// 导入必要的常量和组件
import { CommonConstant as Const } from '../common/Constant' // 通用常量
import { NavigatorBean } from '../viewmodel/NavigatorBean' // 导航参数对象
import { WebPage } from './WebPage' // Web 页面组件
import { VuePage } from './VuePage' // Vue 页面组件

// 扩展 Button 组件,定义统一的按钮样式
@Extend(Button)
function fancy(top: string) {
  .fontSize(Const.MAIN_CONSTANT_BUTTON_FONT_SIZE)
  .fontColor($r('app.color.start_window_background'))
  .width(Const.MAIN_CONSTANT_BUTTON_WIDTH)
  .height(Const.MAIN_CONSTANT_BUTTON_HEIGHT)
  .margin({ top: top })
  .backgroundColor($r('app.color.blue'))
  .borderRadius(Const.MAIN_CONSTANT_BUTTON_BORDER_RADIUS)
}

// 入口组件,应用主页面
@Entry
@ComponentV2
struct MainPage {
  // 提供导航路径栈
  @Provider('navPathStack') navPathStack: NavPathStack = new NavPathStack()

  // 页面映射函数,根据传入的 name 加载对应页面
  @Builder
  PageMap(name: string) {
    if (name === Const.WEB_PAGE_URI) {
      WebPage() // 加载 Web 页面
    } else if (name === Const.VUE_PAGE_URI) {
      VuePage() // 加载 Vue 页面
    }
  }

  // 构建 UI 结构
  build() {
    Navigation(this.navPathStack) { // 使用导航组件
      Stack({ alignContent: Alignment.Top }) { // 使用 Stack 布局,顶部对齐
        Image($r('app.media.background')) // 背景图片
          .width(Const.MAIN_CONSTANT_FULL_HEIGHT)
          .height(Const.MAIN_CONSTANT_IMAGE_HEIGHT)
        Column() { // 按钮区域,纵向排列
          // 加载本地 H5 页面
          Button($r('app.string.loadLocalH5'))
            .fancy(Const.MAIN_CONSTANT_BUTTON_MARGIN_TOP) // 应用按钮样式
            .onClick(() => {
              this.navPathStack.pushPath({
                name: Const.WEB_PAGE_URI, // 跳转到 Web 页面
                param: {
                  path: Const.LOCAL_PATH, // 传入本地路径
                  tips: $r('app.string.local') // 提示信息
                } as NavigatorBean
              })
            })

          // 加载云端 H5 页面
          Button($r('app.string.loadCloudH5'))
            .fancy(Const.MAIN_CONSTANT_BUTTON_MARGIN_TOP_BUTTON)
            .onClick(() => {
              this.navPathStack.pushPath({
                name: Const.WEB_PAGE_URI, // 跳转到 Web 页面
                param: {
                  path: Const.CLOUD_PATH, // 传入云端路径
                  tips: $r('app.string.online') // 提示信息
                } as NavigatorBean
              })
            })

          // 加载本地 Vue 页面
          Button($r('app.string.loadVue'))
            .fancy(Const.MAIN_CONSTANT_BUTTON_MARGIN_TOP_BUTTON)
            .onClick(() => {
              this.navPathStack.pushPath({
                name: Const.VUE_PAGE_URI, // 跳转到 Vue 页面
                param: {
                  path: Const.VUE_PATH, // 传入本地 Vue 路径
                  tips: $r('app.string.local') // 提示信息
                } as NavigatorBean
              })
            })

          // 加载云端 Vue 页面
          Button($r('app.string.loadCloudVue'))
            .fancy(Const.MAIN_CONSTANT_BUTTON_MARGIN_TOP_BUTTON)
            .onClick(() => {
              this.navPathStack.pushPath({
                name: Const.VUE_PAGE_URI, // 跳转到 Vue 页面
                param: {
                  path: Const.VUE_CLOUD_PATH, // 传入云端 Vue 路径
                  tips: $r('app.string.online') // 提示信息
                } as NavigatorBean
              })
            })
        }
        .height(Const.MAIN_CONSTANT_FULL_HEIGHT) // 设置列高度
        .justifyContent(FlexAlign.Center) // 居中对齐
      }
      .backgroundColor($r('app.color.navy_blue')) // 设置背景颜色
    }
    .hideTitleBar(true) // 隐藏标题栏
    .navDestination(this.PageMap) // 绑定页面映射
    .mode(NavigationMode.Stack) // 使用堆栈式导航
  }
}

关键代码说明:

// 代码片段
this.navPathStack.pushPath({
  name: Const.WEB_PAGE_URI,
  param: {
    path: Const.CLOUD_PATH,
    tips: $r('app.string.online')
  } as NavigatorBean
})
  • Const.WEB_PAGE_URI和Const.VUE_PAGE_URI分别对应导航至Web页面和Vue页面。
  • path: Const.CLOUD_PATH和path: Const.VUE_CLOUD_PATH分别用于定义本地与云端前端页面的加载路径。

6.2. Web页面

// entry/src/main/ets/pages/WebPage.ets
import { webview } from '@kit.ArkWeb'
import { promptAction } from '@kit.ArkUI'
import { CommonConstant as Const } from '../common/Constant'

// 定义一个处理 Web 端消息的类
class LinkClass {
  messageFromHtml(value: string) {
    // 显示一个弹窗,展示 HTML 页面传递的消息
    AlertDialog.show({
      message: Const.WEB_ALERT_DIALOG_TEXT_VALUE + value,
      confirm: {
        value: $r('app.string.web_alert_dialog_button_value'),
        action: () => {}
      },
      cancel: () => {}
    })
  }
}

const TITLE: string = '以上为HTML页面'

@ComponentV2
export struct WebPage {
  @Consumer('navPathStack') navPathStack: NavPathStack = new NavPathStack()
  webController: webview.WebviewController = new webview.WebviewController()
  // 传递的参数,包括路径和提示信息
  @Local params: object = this.navPathStack.getParamByName(Const.WEB_PAGE_URI)
  @Local progressVal: number = 0 // 进度条初始值
  @Local isLoading: boolean = true // 是否正在加载
  @Local intervalLoading: number = -1 // 加载进度计时器
  @Local linkObj: LinkClass = new LinkClass() // 用于 JS 交互的对象

  aboutToAppear() {
    // 启动定时器更新进度条
    this.intervalLoading = setInterval(() => {
      this.progressVal = this.progressVal >= Const.WEB_CONSTANT_PROGRESS_MAX 
        ? Const.WEB_CONSTANT_PROGRESS_MIN 
        : (this.progressVal + Const.WEB_CONSTANT_PROGRESS_STEP)
    }, Const.WEB_CONSTANT_MILLI_SECONDS)
  }

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.TopStart }) {
        Image($r('app.media.background'))
          .width(Const.MAIN_CONSTANT_FULL_HEIGHT)
          .height(Const.MAIN_CONSTANT_IMAGE_HEIGHT)

        Row() {
          Column() {
            // 加载 H5 页面
            Web({ src: this.params[0]['path'], controller: this.webController })
              .zoomAccess(false)
              .width(Const.WEB_CONSTANT_WIDTH)
              .aspectRatio(1)
              .margin({
                left: Const.WEB_CONSTANT_MARGIN_LEFT, 
                right: Const.WEB_CONSTANT_MARGIN_RIGHT,
                top: Const.WEB_CONSTANT_MARGIN_TOP
              })
              .onErrorReceive((event) => {
                // 处理网络错误
                if (event?.error.getErrorInfo() === 'ERR_INTERNET_DISCONNECTED' 
                    || event?.error.getErrorInfo() === 'ERR_CONNECTION_TIMED_OUT') {
                  promptAction.showToast({
                    message: $r('app.string.internet_err'),
                    duration: Const.WEB_CONSTANT_DURATION
                  })
                }
              })
              .onProgressChange((event) => {
                // 页面加载完成后停止进度条
                if (event?.newProgress === Const.WEB_CONSTANT_PROGRESS_MAX) {
                  this.isLoading = false
                  clearInterval(this.intervalLoading)
                  this.intervalLoading = -1
                }
              })
                // 配置 JavaScript 交互代理
              .javaScriptProxy({
                object: this.linkObj,
                name: 'linkObj',
                methodList: ['messageFromHtml'],
                controller: this.webController
              })

            Column() {
              // 显示提示文本
              Text(TITLE)
                .fontSize(Const.WEB_CONSTANT_TEXT_VALUE_FONT_SIZE)
                .textAlign(TextAlign.Center)
                .fontColor($r('app.color.text_value_font_color'))
                .height(Const.WEB_CONSTANT_TEXT_VALUE_HEIGHT)
                .fontWeight(Const.WEB_CONSTANT_TEXT_VALUE_FONT_WEIGHT)
                .margin({ top: Const.WEB_CONSTANT_TEXT_VALUE_MARGIN_TOP })
              Text(this.params[0]['tips'])
                .fontSize(Const.WEB_CONSTANT_TIP_TEXT_VALUE_FONT_SIZE)
                .textAlign(TextAlign.Center)
                .fontColor($r('app.color.text_value_font_color'))
                .width(Const.WEB_CONSTANT_TIP_TEXT_VALUE_WIDTH)
                .height(Const.WEB_CONSTANT_TIP_TEXT_VALUE_HEIGHT)
                .opacity(Const.WEB_CONSTANT_TIP_TEXT_VALUE_OPACITY)
                .margin({ top: Const.WEB_CONSTANT_TIP_TEXT_VALUE_MARGIN_TOP })
            }

            // 按钮触发 JavaScript 方法
            Button($r('app.string.btnValue'))
              .fontSize(Const.WEB_CONSTANT_BUTTON_FONT_SIZE)
              .fontColor($r('app.color.start_window_background'))
              .margin({ top: Const.WEB_CONSTANT_BUTTON_MARGIN_TOP })
              .width(Const.WEB_CONSTANT_BUTTON_WIDTH)
              .height(Const.WEB_CONSTANT_BUTTON_HEIGHT)
              .backgroundColor($r('app.color.blue'))
              .borderRadius(Const.WEB_CONSTANT_BUTTON_BORDER_RADIUS)
              .onClick(() => {
                this.webController.runJavaScript('startDraw()')
              })
          }
          .width(Const.WEB_CONSTANT_FULL_WIDTH)
          .height(Const.WEB_CONSTANT_FULL_HEIGHT)
        }

        // 加载时显示进度条
        if (this.isLoading) {
          Progress({
            value: Const.WEB_CONSTANT_PROGRESS_MIN,
            total: Const.WEB_CONSTANT_PROGRESS_MAX,
            type: ProgressType.ScaleRing
          })
            .color(Color.Grey)
            .value(this.progressVal)
            .width(Const.WEB_CONSTANT_PROGRESS_WIDTH)
            .style({
              strokeWidth: Const.WEB_CONSTANT_PROGRESS_STROKE_WIDTH,
              scaleCount: Const.WEB_CONSTANT_PROGRESS_SCALE_COUNT,
              scaleWidth: Const.WEB_CONSTANT_PROGRESS_SCALE_WIDTH
            })
            .zIndex(1)
            .position({
              x: Const.WEB_CONSTANT_PROGRESS_POSITION_X,
              y: Const.WEB_CONSTANT_PROGRESS_POSITION_Y
            })
        }
      }
      .backgroundColor($r('app.color.navy_blue'))
    }
    .title($r('app.string.return_home'))
  }
}

关键代码说明:

// 代码片段
  .javaScriptProxy({
    object: this.linkObj,
    name: 'linkObj',
    methodList: ['messageFromHtml'],
    controller: this.webController
  })
  • 应用侧代码通过javaScriptProxy方法定义代理供前端调用。
// 代码片段
.onClick(() => {
  this.webController.runJavaScript('startDraw()')
})
  • 应用侧通过runJavaScript函数调用前端函数。
  1. Vue页面
// entry/src/main/ets/pages/VuePage.ets
import { webview } from '@kit.ArkWeb'
import { CommonConstant as Const } from '../common/Constant'

// 定义一个类 LinkClass,用于处理从 HTML 页面传递过来的消息
class LinkClass {
  messageFromHtml(value: string) {
    console.log(value) 
    // 显示一个弹窗,展示 HTML 页面传递的消息
    AlertDialog.show({
      message: Const.WEB_ALERT_DIALOG_TEXT_VALUE + value,
      confirm: {
        value: $r('app.string.web_alert_dialog_button_value'),
        action: () => {}
      },
      cancel: () => {}
    })
  }
}

// 定义 Vue 页面标题
const TITLE: string = '以上为Vue页面'

@ComponentV2
export struct VuePage {
  @Consumer('navPathStack') navPathStack: NavPathStack = new NavPathStack() 
  webController: webview.WebviewController = new webview.WebviewController()
  @Local params: object = this.navPathStack.getParamByName(Const.VUE_PAGE_URI)
  // 创建 LinkClass 实例,用于 JavaScript 代理
  @Local linkObj: LinkClass = new LinkClass()

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.TopStart }) { // 顶部对齐的布局
        Image($r('app.media.background')) // 设置背景图片
          .width(Const.MAIN_CONSTANT_FULL_HEIGHT)
          .height(Const.MAIN_CONSTANT_IMAGE_HEIGHT)

        Row() {
          Column() {
            // 加载 Vue H5 页面
            Web({ src: this.params[0]['path'], controller: this.webController })
              .zoomAccess(false)
              .width(Const.WEB_CONSTANT_WIDTH)
              .aspectRatio(1)
              .margin({
                left: 0, right: Const.WEB_CONSTANT_MARGIN_RIGHT,
                top: Const.WEB_CONSTANT_MARGIN_TOP
              })
              .backgroundColor(Color.Transparent)
              .javaScriptProxy({
                object: this.linkObj, // 绑定 JavaScript 代理对象
                name: 'linkObj', // HTML 页面调用时使用的对象名
                methodList: ['messageFromHtml'], // 允许调用的方法列表
                controller: this.webController
              })

            // 文字说明部分
            Column() {
              Text(TITLE) // 显示标题文本
                .fontSize(Const.WEB_CONSTANT_TEXT_VALUE_FONT_SIZE)
                .textAlign(TextAlign.Center)
                .fontColor($r('app.color.text_value_font_color'))
                .height(Const.WEB_CONSTANT_TEXT_VALUE_HEIGHT)
                .fontWeight(Const.WEB_CONSTANT_TEXT_VALUE_FONT_WEIGHT)
                .margin({ top: Const.WEB_CONSTANT_TEXT_VALUE_MARGIN_TOP })
              Text(this.params[0]['tips']) // 显示提示信息
                .fontSize(Const.WEB_CONSTANT_TIP_TEXT_VALUE_FONT_SIZE)
                .textAlign(TextAlign.Center)
                .fontColor($r('app.color.text_value_font_color'))
                .width(Const.WEB_CONSTANT_TIP_TEXT_VALUE_WIDTH)
                .height(Const.WEB_CONSTANT_TIP_TEXT_VALUE_HEIGHT)
                .opacity(Const.WEB_CONSTANT_TIP_TEXT_VALUE_OPACITY)
                .margin({ top: Const.WEB_CONSTANT_TIP_TEXT_VALUE_MARGIN_TOP })
            }

            // 按钮,触发 WebView 内部 JavaScript 方法
            Button($r('app.string.btnValue'))
              .fontSize(Const.WEB_CONSTANT_BUTTON_FONT_SIZE)
              .fontColor($r('app.color.start_window_background'))
              .margin({ top: Const.WEB_CONSTANT_BUTTON_MARGIN_TOP })
              .width(Const.WEB_CONSTANT_BUTTON_WIDTH)
              .height(Const.WEB_CONSTANT_BUTTON_HEIGHT)
              .backgroundColor($r('app.color.blue'))
              .borderRadius(Const.WEB_CONSTANT_BUTTON_BORDER_RADIUS)
              
              // 运行 Web 页面内的 JavaScript 函数
              .onClick(() => {
                this.webController.runJavaScript('outWeb()')
              })
          }
          .width(Const.WEB_CONSTANT_FULL_WIDTH)
          .height(Const.WEB_CONSTANT_FULL_HEIGHT)
        }
      }
      .backgroundColor($r('app.color.navy_blue')) // 设置背景颜色
    }
    .title($r('app.string.return_home')) // 设置导航标题
  }
}

关键代码说明:

// 代码片段
.javaScriptProxy({
  object: this.linkObj, // 绑定 JavaScript 代理对象
  name: 'linkObj', // HTML 页面调用时使用的对象名
  methodList: ['messageFromHtml'], // 允许调用的方法列表
  controller: this.webController
})
  • 应用侧代码通过javaScriptProxy方法定义代理供前端调用。
// 代码片段
.onClick(() => {
  this.webController.runJavaScript('outWeb()')
})
  • 应用侧通过runJavaScript函数调用前端函数。

7. 运行前端云端工程

  1. 搭建nodejs环境:本案例服务端是基于nodejs实现的,需要安装nodejs,如果您本地已有nodejs环境可以跳过此步骤。
    1. 检查本地是否安装nodejs:打开命令行工具(如Windows系统的cmd和Mac电脑的Terminal,这里以Windows为例),输入node -v,如果可以看到版本信息,说明已经安装nodejs。
    2. 如果本地没有nodejs环境,您可以去nodejs官网上下载所需版本进行安装配置。
    3. 配置完环境变量后,重新打开命令行工具,输入node -v,如果可以看到版本信息,说明已安装成功。
  1. 运行服务端代码:
    1. 运行H5服务端代码。在本项目的HttpServerOfWeb目录下打开命令行工具,输入npm install 安装服务端依赖包,安装成功后输入npm start点击回车。看到“服务器启动成功!"则表示服务端已经在正常运行。
    2. 运行Vue服务端代码。在本项目的HttpServerOfVue目录下打开命令行工具,输入npm install 安装服务端依赖包,安装成功后输入npm run dev点击回车。看到输出链接,则表示服务端已经可以正常启动了。

  1. 构建局域网环境:测试本案例时要确保运行服务端代码的电脑和测试机连接的是同一局域网下的网络,您可以用您的手机开一个个人热点,然后将测试机和运行服务端代码的电脑都连接您的手机热点进行测试。
  2. 连接服务器地址:打开命令行工具,输入ipconfig命令查看本地ip,将本地ip地址复制到entry/src/main/ets/common/constants/Constants.ets文件中,如果运行H5服务端,则修改CLOUD_PATH变量,如果运行Vue服务端,则修改VUE_CLOUD_PATH变量。注意只替换ip地址部分,不要修改端口号,保存好ip之后即可运行Codelab进行测试。
// main/ets/common/utils/Constants.ets
export class CommonConstant {
  // ...
  static readonly VUE_CLOUD_PATH: string = 'http://192.168.31.150:5173/index.html'
  static readonly CLOUD_PATH: string = 'http://192.168.31.150:3000/index.html'
  //...
}

8. 代码与视频教程

完整案例代码与视频教程请参见:

代码:Code-09-02.zip。

视频:《基于Web组件实现随机抽奖》。

需要参加鸿蒙认证的请点击 鸿蒙认证链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐