第 9 章 ArkWeb适配与开发

本章围绕ArkWeb适配与开发展开深入探讨。ArkWeb作为方舟Web,为开发者提供了强大的Web组件,在 HarmonyOS 的框架层中发挥着重要作用,其统一Web内核设计有效提升了开发者体验。

Web组件具备丰富的功能,涵盖页面加载、交互、调试等多个方面。开发者可利用其加载网络页面、本地页面及 HTML格式文本数据,通过多种接口实现页面跳转、历史记录导航等操作。同时,应用侧与前端页面交互方式多样,如应用侧调用前端函数、前端调用应用侧函数,还可建立数据通道进行通信。

此外,还涉及自定义页面请求响应、Cookie管理、缓存与存储管理、隐私模式设置等内容。调试与调优方面,详细介绍了使用DevTools工具调试前端页面的步骤,以及预加载、预连接等优化手段。最后通过基于应用拉起相关能力实现Web跳转、基于Web组件实现随机抽奖、实现应用免密登录三个案例,结合实际场景,全面展示了 ArkWeb在开发中的具体应用及关键知识点。

9.1 ArkWeb简介

ArkWeb(方舟Web)提供了Web组件,用于在应用程序中显示Web页面内容。Web组件为开发者提供页面加载、页面交互、页面调试等能力,可以用于实现移动端的混合式开发(Hybrid App):

  • 页面加载: Web组件提供基础的前端页面加载的能力,包括加载网络页面、本地页面、html格式文本数据。
  • 页面交互: Web组件提供丰富的页面交互的方式,包括:设置前端页面深色模式,新窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。
  • 页面调试: Web组件支持使用Devtools工具调试前端页面。

@ohos.web.webview提供web控制能力,通过如下方式导入模块:

typescript 复制代码
import { webview } from '@kit.ArkWeb'

9.1.1 Web生态在HarmonyOS中的位置

HarmonyOS的整体架构分为四层:应用层、框架层、系统服务层和内核层。其中,ArkWeb作为 ArkUI 框架层中的一个重要组件,为开发者提供了强大的Web能力支持。Web应用生态由Web 子系统承载,该子系统属于增强软件服务子系统集,其实现深度依赖于HarmonyOS的其他子系统以及内核层提供的基础能力。通过这种分层设计和模块化协作,HarmonyOS能够高效支持Web应用的开发与运行,同时确保系统的稳定性和性能。

9.1.2 ArkWeb总体架构

为了构建统一的Web生态,HarmonyOS采用了 统一Web内核 的设计策略,确保其接口能够跟随系统版本迭代而保持稳定,从而有效防止生态碎片化,提升开发者的使用体验。同时,Web内核支持以hap形式独立构建,能够实现独立发布和升级,这不仅提高了系统的灵活性,还确保了Chromium内核漏洞的快速修复,进一步增强了系统的安全性和稳定性。

总体架构如下图所示:

  • Web组件

ArkU组件之一,提供web组件页面加载和操作属性、事件等API。

  • NWeb

负责Web组件的Native引擎,使用web嵌入框架和web内容的抽象接口,来管理Web组件的功能。

  • CEF(Chromium Embedded Framework)

web嵌入框架,为NWeb引擎提供高阶接口能力。

  • Chromium

在开源Chromium基础上进行了适配,提供包括H5页面解析加载、渲染和执行引擎等Web核心能力。

  • 平台适配层

支撑渲染模块和执行模块所有基础能力,主要包括网络协议、图形/窗口能力。

9.2 Web组件生命周期

开发者可以使用Web组件加载本地或者在线网页。

Web组件提供了丰富的组件生命周期回调接口,通过这些回调接口,开发者可以感知Web组件的生命周期状态变化,进行相关的业务处理。

Web组件的状态主要包括:Controller绑定到Web组件、网页加载开始、网页加载进度、网页加载结束、页面即将可见等。

  • onControllerAttached事件:当Controller成功绑定到Web组件时触发该回调。
  • onLoadIntercept事件:当Web组件加载url之前触发该回调,用于判断是否阻止此次访问。默认允许加载。
  • onInterceptRequest事件:当Web组件加载url之前触发该回调,用于拦截url并返回响应数据。
  • onPageBegin事件:网页开始加载时触发该回调
  • onProgressChange事件:告知开发者当前页面加载的进度。
  • onPageEnd事件:网页加载完成时触发该回调。

9.3 页面加载、显示及导航

本节主要介绍Web组件加载网络页面、本地页面和HTML格式文本数据的方法,以及历史记录导航、页面跳转、跨应用跳转的实现方式。通过loadUrl()、loadData()等接口操作,结合示例代码展示不同场景下页面加载与导航功能的实现细节。

9.3.1 加载网络页面

开发者可以在网络组件创建时,指定默认加载的网络页面。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定的网页。

页面加载过程中,若涉及网络资源获取,请在module.json5中配置网络访问权限:

json 复制代码
"requestPermissions": [
  {
    "name" : "ohos.permission.INTERNET"
  }
]

在下面的示例中,在Web组件加载完"developer.huawei.com/consumer/cn...

typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('loadUrl')
        .onClick(() => {
          try {
            // 点击按钮时,通过loadUrl,跳转到www.example1.com
            this.controller.loadUrl(
              'https://developer.huawei.com/consumer/cn/doc/'
            )
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      // 组件创建时,加载www.example.com
      Web({
        src: 'https://developer.huawei.com/consumer/cn/',
        controller: this.controller
      })
    }
  }
}

9.3.2 加载本地页面

将本地页面文件放在应用的rawfile目录下,可以在Web组件创建的时候指定默认加载的本地页面,并且加载完成后可通过调用loadUrl()接口变更当前Web组件的页面。

  • 将资源文件放置在应用的resources/rawfile目录下。
  • 应用侧代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('loadUrl')
        .onClick(() => {
          try {
            // 点击按钮时,通过loadUrl,跳转到local1.html
            this.controller.loadUrl($rawfile("local1.html"))
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      // 组件创建时,通过$rawfile加载本地文件local.html
      Web({ src: $rawfile("local.html"), controller: this.controller })
    }
  }
}
  • local.html页面代码。
html 复制代码
<!-- local.html -->
<!DOCTYPE html>
<html>
  <body>
    <p>Hello World</p>
  </body>
</html>
  • local1.html页面代码。
html 复制代码
<!-- local1.html -->
<!DOCTYPE html>
<html>
  <body>
    <p>This is local1 page</p>
  </body>
</html>

9.3.3 加载HTML格式的文本数据

Web组件可以通过loadData()接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面。

typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('loadData')
        .onClick(() => {
          try {
            // 点击按钮时,通过loadData,加载HTML格式的文本数据
            this.controller.loadData(
              "<html><body>Source:<pre>source</pre></body></html>",
              "text/html",
              "UTF-8"
            )
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      // 组件创建时,加载www.example.com
      Web({
        src: 'https://developer.huawei.com/consumer/cn/',
        controller: this.controller
      })
    }
  }
}

9.3.4 历史记录导航

在前端页面点击网页中的链接时,Web组件默认会自动打开并加载目标网址。当前端页面替换为新的加载链接时,会自动记录已经访问的网页地址。可以通过forward()和backward()接口向前/向后浏览上一个/下一个历史记录。

在下面的示例中,点击应用的按钮来触发前端页面的后退操作。

typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController =
    new webview.WebviewController()

  build() {
    Column() {
      Button('backward')
        .onClick(() => {
          if (this.webviewController.accessBackward()) {
            this.webviewController.backward()
          }
        })
      Web({
        src: 'https://developer.huawei.com/consumer/cn/',
        controller: this.webviewController
      })
    }
  }
}

如果存在历史记录,accessBackward()接口会返回true。同样,你可以使用accessForward()接口检查是否存在前进的历史记录。如果您不执行检查,那么当用户浏览到历史记录的末尾时,调用forward()和backward()接口时将不执行任何操作。

9.3.5 页面跳转

当点击网页中的链接需要跳转到应用内其他页面时,可以通过使用Web组件的onLoadIntercept()接口来实现。

在下面的示例中,应用首页Index.ets加载前端页面route.html,在前端route.html页面点击超链接,可跳转到应用的ProfilePage.ets页面。

  • 应用首页Index.ets页面代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { router } from '@kit.ArkUI'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController =
    new webview.WebviewController()

  build() {
    Column() {
      // 资源文件route.html存放路径src/main/resources/rawfile
      Web({
        src: $rawfile('route.html'),
        controller: this.webviewController
      })
        .onLoadIntercept((event) => {
          if (event) {
            let url: string = event.data.getRequestUrl()
            if (url.indexOf('native://') === 0) {
              // 跳转其他界面
              router.pushUrl({ url: url.substring(9) })
              return true
            }
          }
          return false
        })
    }
  }
}
  • route.html前端页面代码。
html 复制代码
<!-- route.html -->
<!DOCTYPE html>
<html>
  <body>
    <div>
      <a href="native://pages/case1/ProfilePage">个人中心</a>
    </div>
  </body>
</html>

注意:pages/case1/ProfilePage要根据真实的路径设置,并在main_pages.json5里进行设置。

  • 跳转页面ProfilePage.ets代码。
html 复制代码
@Entry
@ComponentV2
struct ProfilePage {
  @Local message: string = 'Hello World'

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
    }
  }
}

9.3.6 跨应用跳转

Web组件可以实现点击前端页面超链接跳转到其他应用。

在下面的示例中,点击call.html前端页面中的超链接,跳转到电话应用的拨号界面。

  • 应用侧代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { call } from '@kit.TelephonyKit'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController =
    new webview.WebviewController()

  build() {
    Column() {
      Web({
        src: $rawfile('call.html'),
        controller: this.webviewController
      })
        .onLoadIntercept((event) => {
          if (event) {
            let url: string = event.data.getRequestUrl()
            // 判断链接是否为拨号链接
            if (url.indexOf('tel://') === 0) {
              // 跳转拨号界面
              call.makeCall(url.substring(6), (err) => {
                if (!err) {
                  console.info('make call succeeded.')
                } else {
                  console.info('make call fail, err is:' + JSON.stringify(err))
                }
              })
              return true
            }
          }
          return false
        })
    }
  }
}
  • 前端页面call.html代码。
html 复制代码
<!-- call.html -->
<!DOCTYPE html>
<html>
  <body>
    <div>
      <a href="tel://xxx xxxx xxx">拨打电话</a>
    </div>
  </body>
</html>

9.4 应用侧与前端页面交互

本节阐述应用侧与前端页面的交互技术,包括应用侧调用前端页面函数、前端页面调用应用侧函数,以及建立数据通道通信。借助 JSBridge和端口通信技术,实现两端灵活交互。

9.4.1 应用侧调用前端页面函数

应用侧可以通过runJavaScript()方法调用前端页面的JavaScript相关函数。JSBridge(JavaScript Bridge)就是用于前端页面和应用侧之间进行通信的桥梁。

在下面的示例中,点击应用侧的"runJavaScript"按钮时,来触发前端页面的htmlTest()方法。

  • 前端页面代码。
html 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<button type="button" onclick="callArkTS()">Click Me!</button>
<h1 id="text">
    这是一个测试信息,默认字体为黑色,调用runJavaScript方法后字体为绿色,
    调用runJavaScriptCodePassed方法后字体为红色
</h1>
<script>
    // 调用有参函数时实现。
    var param = "param: JavaScript Hello World!"
    function htmlTest(param) {
        document.getElementById('text').style.color = 'green'
        console.log(param)
    }
    // 调用无参函数时实现。
    function htmlTest() {
        document.getElementById('text').style.color = 'green'
    }
    // Click Me!触发前端页面callArkTS()函数执行JavaScript传递的代码。
    function callArkTS() {
        changeColor()
    }
</script>
</body>
</html>
  • 应用侧代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController =
    new webview.WebviewController()

  aboutToAppear() {
    // 配置Web开启调试模式
    webview.WebviewController.setWebDebuggingAccess(true)
  }

  build() {
    Column({ space: 10 }) {
      Button('runJavaScript')
        .onClick(() => {
          // 前端页面函数无参时,将param删除。
          this.webviewController.runJavaScript('htmlTest(param)')
        })
      Button('runJavaScriptCodePassed')
        .onClick(() => {
          // 传递runJavaScript侧代码方法。
          this.webviewController.runJavaScript(`
            function changeColor(){
              document.getElementById('text').style.color = 'red'
            }
          `)
        })
      Web({
        src: $rawfile('index.html'),
        controller: this.webviewController
      })
    }
  }
}

9.4.2 前端页面调用应用侧函数

开发者使用Web组件将应用侧代码注册到前端页面中,注册完成之后,前端页面中使用注册的对象名称就可以调用应用侧的函数,实现在前端页面中调用应用侧方法。

注册应用侧代码有两种方式,一种在Web组件初始化调用,使用javaScriptProxy()接口。另外一种在Web组件初始化完成后调用,使用registerJavaScriptProxy()接口,需要和deleteJavaScriptRegister接口配合使用,防止内存泄漏。

在下面的示例中,将test()方法注册在前端页面中, 该函数可以在前端页面触发运行。

  • javaScriptProxy()接口使用示例如下。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

class testClass {
  constructor() {
  }

  test(): string {
    return 'ArkTS Hello World!'
  }
}

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController()
  // 声明需要注册的对象
  @Local testObj: testClass = new testClass()

  build() {
    Column() {
      Button('deleteJavaScriptRegister')
        .onClick(() => {
          try {
            this.webviewController.deleteJavaScriptRegister("testObjName")
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      // Web组件加载本地index.html页面
      Web({
        src: $rawfile('index.html'),
        controller: this.webviewController
      })// 将对象注入到web端
        .javaScriptProxy({
          object: this.testObj,
          name: "testObjName",
          methodList: ["test"],
          controller: this.webviewController,
          // 可选参数
          asyncMethodList: [],
          permission: '{}'
        })
    }
  }
}
  • 应用侧使用registerJavaScriptProxy()接口注册。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

class testClass {
  constructor() {
  }

  test(): string {
    return "ArkUI Web Component"
  }

  toString(): void {
    console.log('Web Component toString')
  }
}

@Entry
@ComponentV2
struct Index {
  webviewController: webview.WebviewController =
    new webview.WebviewController()
  @Local testObj: testClass = new testClass()

  build() {
    Column({ space: 10 }) {
      Button('refresh')
        .onClick(() => {
          try {
            this.webviewController.refresh()
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Button('Register JavaScript To Window')
        .onClick(() => {
          try {
            this.webviewController.registerJavaScriptProxy(
              this.testObj,
              "testObjName",
              ["test", "toString"],
              // 可选参数, asyncMethodList
              [],
              // 可选参数, permission
              '{}'
            )
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Button('deleteJavaScriptRegister')
        .onClick(() => {
          try {
            this.webviewController.deleteJavaScriptRegister("testObjName")
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Web({
        src: $rawfile('index2.html'),
        controller: this.webviewController
      })
    }
  }
}

****使用registerJavaScriptProxy()接口注册方法时,注册后需调用refresh()接口生效。

  • 可选参数permission是一个json字符串,示例如下:
json 复制代码
{
  "javascriptProxyPermission": {
    "urlPermissionList": [       // Object级权限,如果匹配,所有Method都授权
      {
        "scheme": "resource",    // 精确匹配,不能为空
        "host": "rawfile",       // 精确匹配,不能为空
        "port": "",              // 精确匹配,为空不检查
        "path": ""               // 前缀匹配,为空不检查
      },
      {
        "scheme": "https",       // 精确匹配,不能为空
        "host": "xxx.com",       // 精确匹配,不能为空
        "port": "8080",          // 精确匹配,为空不检查
        "path": "a/b/c"          // 前缀匹配,为空不检查
      }
    ],
    "methodList": [
      {
        "methodName": "test",
        "urlPermissionList": [   // Method级权限
          {
            "scheme": "https",   // 精确匹配,不能为空
            "host": "xxx.com",   // 精确匹配,不能为空
            "port": "",          // 精确匹配,为空不检查
            "path": ""           // 前缀匹配,为空不检查
          },
          {
            "scheme": "resource",// 精确匹配,不能为空
            "host": "rawfile",   // 精确匹配,不能为空
            "port": "",          // 精确匹配,为空不检查
            "path": ""           // 前缀匹配,为空不检查
          }
        ]
      },
      {
        "methodName": "test11",
        "urlPermissionList": [   // Method级权限
          {
            "scheme": "q",       // 精确匹配,不能为空
            "host": "r",         // 精确匹配,不能为空
            "port": "",          // 精确匹配,为空不检查
            "path": "t"          // 前缀匹配,为空不检查
          },
          {
            "scheme": "u",       // 精确匹配,不能为空
            "host": "v",         // 精确匹配,不能为空
            "port": "",          // 精确匹配,为空不检查
            "path": ""           // 前缀匹配,为空不检查
          }
        ]
      }
    ]
  }
}

可以将此json字符串转化为一行代码,放置到"可选参数,permission"处,替换掉'{}'即可。

  • index.html前端页面触发应用侧代码。
html 复制代码
<!-- index2.html -->
<!DOCTYPE html>
<html>
<body>
<button type="button" onclick="callArkTS()">Click Me!</button>
<p id="demo"></p>
<script>
    function callArkTS() {
        let str = testObjName.test()
        document.getElementById("demo").innerHTML = str
        console.info('ArkTS Hello World! :' + str)
    }
</script>
</body>
</html>

9.4.3 建立应用侧与前端页面数据通道

前端页面和应用侧之间可以用createWebMessagePorts()接口创建消息端口来实现两端的通信。

在下面的示例中,应用侧页面中通过createWebMessagePorts方法创建消息端口,再把其中一个端口通过postMessage()接口发送到前端页面,便可以在前端页面和应用侧之间互相发送消息。

  • 应用侧代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()
  ports: webview.WebMessagePort[] = []
  @Local sendFromEts: string =
    'Send this message from ets to HTML'
  @Local receivedFromHtml: string =
    'Display received message send from HTML'

  build() {
    Column({ space: 20 }) {
      // 展示接收到的来自HTML的内容
      Text(this.receivedFromHtml)
      // 输入框的内容发送到HTML
      TextInput({ placeholder: 'Send this message from ets to HTML' })
        .onChange((value: string) => {
          this.sendFromEts = value
        })

      // 该内容可以放在onPageEnd生命周期中调用。
      Button('postMessage')
        .onClick(() => {
          try {
            // 1、创建两个消息端口。
            this.ports = this.controller.createWebMessagePorts()
            // 2、在应用侧的消息端口(如端口1)上注册回调事件。
            this.ports[1].onMessageEvent((result: webview.WebMessage) => {
              let msg = 'Got msg from HTML:'
              if (typeof (result) === 'string') {
                console.info(`received string message from html5,
                  string is: ${result}`)
                msg = msg + result
              } else if (typeof (result) === 'object') {
                if (result instanceof ArrayBuffer) {
                  console.info(`received arraybuffer from html5,
                    length is: ${result.byteLength}`)
                  msg = msg + 'length is ' + result.byteLength
                } else {
                  console.info('not support')
                }
              } else {
                console.info('not support')
              }
              this.receivedFromHtml = msg
            })
            // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。
            this.controller.postMessage('__init_port__', [this.ports[0]], '*')
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })

      // 4、使用应用侧的端口给另一个已经发送到html的端口发送消息。
      Button('SendDataToHTML')
        .onClick(() => {
          try {
            if (this.ports && this.ports[1]) {
              this.ports[1].postMessageEvent(this.sendFromEts)
            } else {
              console.error(`ports is null, Please initialize first`)
            }
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Web({
        src: $rawfile('index3.html'),
        controller: this.controller
      })
    }
  }
}
  • 前端页面代码。
html 复制代码
<!--index3.html-->
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebView Message Port Demo</title>
</head>
<body>
<h1>WebView Message Port Demo</h1>
<div>
  <input type="button" value="SendToEts" onclick="PostMsgToEts(msgFromJS.value)">
  <br/>
  <input id="msgFromJS" type="text" value="send this message from HTML to ets">
  <br/>
</div>
<p class="output">display received message send from ets</p>
</body>
<script>
  var h5Port
  var output = document.querySelector('.output')
  window.addEventListener('message', function (event) {
    if (event.data === '__init_port__') {
      if (event.ports[0] !== null) {
        h5Port = event.ports[0]; // 1. 保存从应用侧发送过来的端口。
        h5Port.onmessage = function (event) {
          // 2. 接收ets侧发送过来的消息。
          var msg = 'Got message from ets:'
          var result = event.data
          if (typeof(result) === 'string') {
            console.info(`received string message from html5, 
              string is: ${result}`)
            msg = msg + result
          } else if (typeof(result) === 'object') {
            if (result instanceof ArrayBuffer) {
              console.info(`received arraybuffer from html5, 
                length is: ${result.byteLength}`)
              msg = msg + 'length is ' + result.byteLength
            } else {
              console.info('not support')
            }
          } else {
            console.info('not support')
          }
          output.innerHTML = msg
        }
      }
    }
  })
  
  // 3. 使用h5Port向应用侧发送消息。
  function PostMsgToEts(data) {
    if (h5Port) {
      h5Port.postMessage(data)
    } else {
      console.error('h5Port is null, Please initialize first')
    }
  }
</script>
</html>

9.4.4 JSBridge技术与端口通信技术的区别

通过JSBridge,可以在WebView中通过JavaScript代码调用客户端提供的功能,也可以从客户端中调用JS函数。JSBridge可能无法满足复杂的通信需求,这时可以使用端口通信技术来实现更灵活的跨平台通信。

使用runJavaScript与registerJavaScriptProxy的JSBridge技术:

  • 特点:函数的相互调用,不需要一直保持通道。
  • 使用场景:对于函数调用的场景,可以选择runJavaScript或者registerJavaScriptProxy实现。

使用WebMessagePort建立端口通信:

  • 特点:建立通道,实时监听,持续消耗资源。
  • 使用场景:对于有双向通信需求的场景,对于数据实时更新,聊天应用,大文件如图片等场景建议选择端口通信技术,更加稳定。

9.5 自定义页面请求响应

Web组件支持在应用拦截到页面请求后自定义响应请求能力。开发者通过onInterceptRequest()接口来实现自定义资源请求响应 。自定义请求能力可以用于开发者自定义Web页面响应、自定义文件资源响应等场景。

Web网页上发起资源加载请求,应用层收到资源请求消息。应用层构造本地资源响应消息发送给Web内核。Web内核解析应用层响应信息,根据此响应信息进行页面资源加载。

在下面的示例中,Web组件通过拦截页面请求"developer.huawei.com/consumer/cn... 在应用侧代码构建响应资源,实现自定义页面响应场景。

  • 前端页面index.html代码。
html 复制代码
<!--index4.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <!-- 页面资源请求 -->
    <a href="https://developer.huawei.com/consumer/cn/">intercept test!</a>
  </body>
</html>
  • 应用侧代码。
typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()
  responseResource: WebResourceResponse = new WebResourceResponse()
  // 开发者自定义响应数据
  @Local webData: string = '<!DOCTYPE html>\n' +
    '<html>\n' +
    '<head>\n' +
    '<title>intercept test</title>\n' +
    '</head>\n' +
    '<body>\n' +
    '<h1>intercept ok</h1>\n' +
    '</body>\n' +
    '</html>'

  build() {
    Column() {
      Web({ src: $rawfile('index4.html'), controller: this.controller })
        .onInterceptRequest((event) => {
          if (event) {
            console.info('url:' + event.request.getRequestUrl())
            // 拦截页面请求
            if (event.request.getRequestUrl() !== 
                'https://developer.huawei.com/consumer/cn/') {
              return null
            }
          }
          // 构造响应数据
          this.responseResource.setResponseData(this.webData)
          this.responseResource.setResponseEncoding('utf-8')
          this.responseResource.setResponseMimeType('text/html')
          this.responseResource.setResponseCode(200)
          this.responseResource.setReasonMessage('OK')
          return this.responseResource
        })
    }
  }
}

9.6 管理Cookie、数据存储及隐私模式

本节讲述Web组件对Cookie的管理,可通过WebCookieManager类操作。介绍缓存与存储管理,包括Cache和Dom Storage的使用,以及开启隐私模式的方法和效果,保障数据管理与用户隐私。

9.6.1 Cookie管理

Cookie是网络访问过程中,由服务端发送给客户端的一小段数据。客户端可持有该数据,并在后续访问该服务端时,方便服务端快速对客户端身份、状态等进行识别。

当Cookie SameSite属性未指定时,默认值为SameSite=Lax,只在用户导航到cookie的源站点时发送cookie,不会在跨站请求中被发送。

Web组件提供了WebCookieManager类,用于管理Web组件的Cookie信息。Cookie信息保存在应用沙箱路径下/proc/{pid}/root/data/storage/el2/base/cache/web/Cookiesd的文件中。

下面以configCookieSync()接口举例,为"www.example.com"设置单个Cookie的值"value=test"。

typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('configCookieSync')
        .onClick(() => {
          try {
            webview.WebCookieManager.configCookieSync(
              'https://www.example.com',
              'value=test'
            )
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Web({
        src: 'www.example.com',
        controller: this.controller
      })
    }
  }
}

9.6.2 缓存与存储管理

在访问网站时,网络资源请求是相对比较耗时的。开发者可以通过Cache、Dom Storage等手段将资源保存到本地,以提升访问同一网站的速度。

9.6.2.1 Cache

使用cacheMode()配置页面资源的缓存模式,Web组件为开发者提供四种缓存模式,分别为:

  • Default : 优先使用未过期的缓存,如果缓存不存在,则从网络获取。
  • None : 加载资源使用cache,如果cache中无该资源则从网络中获取。
  • Online : 加载资源不使用cache,全部从网络中获取。
  • Only :只从cache中加载资源。

在下面的示例中,选用缓存设置为None模式。

typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  @Local mode: CacheMode = CacheMode.None
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .cacheMode(this.mode)
    }
  }
}

同时,为了获取最新资源,开发者可以通过removeCache()接口清除已经缓存的资源,示例代码如下:

typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  @Local mode: CacheMode = CacheMode.None
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('removeCache')
        .onClick(() => {
          try {
            // 设置为true时同时清除rom和ram中的缓存,设置为false时只清除ram中的缓存
            this.controller.removeCache(true)
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Web({ src: 'www.example.com', controller: this.controller })
        .cacheMode(this.mode)
    }
  }
}

9.6.2.2 Dom Storage

Dom Storage包含了Session Storage和Local Storage两类。前者为临时数据,其存储与释放跟随会话生命周期;后者为可持久化数据,落盘在应用目录下。两者的数据均通过Key-Value的形式存储,通常在访问需要客户端存储的页面时使用。开发者可以通过Web组件的属性接口domStorageAccess()进行使能配置,示例如下:

typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .domStorageAccess(true)
    }
  }
}

9.6.3 使用隐私模式

开发者在创建Web组件时,可以将可选参数incognitoMode设置为true,来开启Web组件的隐私模式。 当使用隐私模式时,浏览网页时的Cookie、 Cache Data等数据不会保存在本地的持久化文件,当隐私模式的Web组件被销毁时,Cookie、 Cache Data等数据将不被记录下来。

  • 创建隐私模式的Web组件。
typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({
        src: 'www.example.com',
        controller: this.controller,
        incognitoMode: true
      })
    }
  }
}
  • 通过isIncogntoMode判断当前Web组件是否是隐私模式。
typescript 复制代码
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('isIncognitoMode')
        .onClick(() => {
          try {
            let result = this.controller.isIncognitoMode()
            console.log('isIncognitoMode' + result)
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code}`)
            console.error(`Message: ${(error as BusinessError).message}`)
          }
        })
      Web({ src: 'www.example.com', controller: this.controller })
    }
  }
}

9.7 调试与调优

本节介绍Web组件调试与调优的方法。使用DevTools工具调试前端页面,需开启调试开关、连接设备、进行端口转发等步骤。还可通过预加载、预连接等技术提升性能,如prepareForPageLoad()、initializeBrowserEngine()等接口的应用。

9.7.1 使用DevTools工具调试前端页面

Web组件支持使用DevTools工具调试前端页面。DevTools是一个 Web前端开发调试工具,提供了电脑上调试移动设备前端页面的能力。开发者通过setWebDebuggingAccess()接口开启Web组件前端页面调试能力,利用DevTools工具可以在电脑上调试移动设备上的前端网页,设备需为4.1.0及以上版本。也可以使用模拟器来调试,具体步骤与真机调试相同。以下操作均在模拟器环境中进行调试。

9.7.1.1 应用代码开启Web调试开关

调试网页前,需要应用侧代码调用setWebDebuggingAccess()接口开启Web调试开关。

如果没有开启Web调试开关,则DevTools无法发现被调试的网页。

  1. 在应用代码中开启Web调试开关,具体如下:
typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  aboutToAppear() {
    // 配置Web开启调试模式
    webview.WebviewController.setWebDebuggingAccess(true)
  }

  build() {
    Column() {
      Web({
        src: 'https://developer.huawei.com/consumer/cn/',
        controller: this.controller
      })
    }
  }
}
  1. 开启调试功能需要在DevEco Studio应用工程hap模块的module.json5文件中增加如下权限。
json 复制代码
"requestPermissions":[
  {
    "name" : "ohos.permission.INTERNET"
  }
]

9.7.1.2 将设备连接到电脑

请将设备连接至电脑,随后开启开发者模式,为后续的端口转发操作做好准备。

  1. 请开启设备上的开发者模式,并启用USB调试功能。

(1) 终端系统查看"设置 > 系统"中是否有"开发者选项",如果不存在,可在"设置 > 关于本机"连续七次单击"版本号",直到提示"开启开发者模式",点击"确认开启"后输入PIN码(如果已设置),设备将自动重启。

(2) USB数据线连接终端和电脑,在"设置 > 系统 > 开发者选项"中,打开"USB调试"开关,弹出的"允许USB调试"的弹框,点击"允许"。

  1. 使用hdc命令连接上设备。

打开命令行执行如下命令,查看hdc能否发现设备。

复制代码
hdc list targets
    • 如果命令有返回设备的ID,则说明hdc已连接上设备。
    • 如果命令返回 [Empty],则说明hdc还没有发现设备。
  1. 进入hdc shell。

当hdc命令连接上设备后,执行如下命令,进入hdc shell。

复制代码
hdc shell

9.7.1.3 端口转发

当应用代码调用setWebDebuggingAccess接口开启Web调试开关后,ArkWeb内核将启动一个domain socket的监听,以此实现DevTools对网页的调试功能。

但是Chrome浏览器无法直接访问到设备上的domain socket, 所以需要将设备上的domain socket转发到电脑上。

  1. 先在hdc shell里执行如下命令,查询ArkWeb在设备里创建的domain socket。
bash 复制代码
cat /proc/net/unix | grep devtools
  • 如果前几步操作无误,该命令的执行结果将显示用于查询的domain socket端口。
  • 如果没有查询到结果, 请再次确认。
    • 应用开启了Web调试开关。
    • 应用使用Web组件加载了网页。
  1. 将查询到的domain socket转发至电脑的TCP 9222端口。

执行exit退出hdc shell。

bash 复制代码
exit

在命令行里执行如下命令转发端口。

复制代码
hdc fport tcp:9222 localabstract:webview_devtools_remote_3671

"webview_devtools_remote_" 后面的数字,代表ArkWeb所在应用的进程号, 该数字不是固定的。请将数字改为自己查询到的值。

如果应用的进程号发生变化(例如,应用重新启动),则需要重新进行端口转发。

命令执行成功示意图:

  1. 在命令行里执行如下命令,检查端口是否转发成功。
bash 复制代码
hdc fport ls
    • 如果有返回端口转发的任务,则说明端口转发成功。
    • 如果返回 [Empty], 则说明端口转发失败。

9.7.1.4 在Chrome浏览器上打开调试工具页面

  1. 在电脑端Chrome浏览器地址栏中输入调试工具地址 chrome://inspect/#devices 并打开该页面。
  2. 修改Chrome调试工具的配置。

需要从本地的TCP 9222端口发现被调试网页,所以请确保已勾选 "Discover network targets"。然后再进行网络配置。

(1) 点击 "Configure" 按钮。

(2) 在 "Target discovery settings" 中添加要监听的本地端口localhost:9222。

  1. 为了同时调试多个应用,请在Chrome浏览器的调试工具网页内,于"Devices"选项中的"configure"部分添加多个端口号。

9.7.1.5 等待发现被调试页面

如果前面的步骤执行成功,稍后,Chrome的调试页面将显示待调试的网页。

9.7.1.6 开始网页调试

点击"inspect"打开调试界面。

9.7.2 预加载和预连接

可以通过prepareForPageLoad()来预解析或者预连接将要加载的页面。

在下面的示例中,在Web组件的onAppear中对要加载的页面进行预连接。

typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Button('loadData')
        .onClick(() => {
          if (this.webviewController.accessBackward()) {
            this.webviewController.backward()
          }
        })
      Web({
        src: 'https://www.example.com/',
        controller: this.webviewController
      })
        .onAppear(() => {
          // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址做dns预解析
          // 第三个参数为要预连接socket的个数。最多允许6个。
          webview.WebviewController.prepareForPageLoad(
            'https://www.example.com/',
            true,
            2
          )
        })
    }
  }
}

也可以通过initializeBrowserEngine()来提前初始化内核,然后在初始化内核后调用prepareForPageLoad()对即将要加载的页面进行预解析、预连接。这种方式适合提前对首页进行预解析、预连接。

在下面的示例中,Ability的onCreate中提前初始化Web内核并对首页进行预连接。

scala 复制代码
import { webview } from '@kit.ArkWeb'
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.log("EntryAbility onCreate")
    webview.WebviewController.initializeWebEngine()
    // 预连接时,需要將'https://www.example.com'替换成真实要访问的网站地址。
    webview.WebviewController.prepareForPageLoad(
      "https://www.example.com/", 
      true, 
      2
    )
    AppStorage.setOrCreate("abilityWant", want)
    console.log("EntryAbility onCreate done")
  }
}

9.7.3 预加载

如果能够预测到Web组件将要加载的页面或者即将要跳转的页面。可以通过prefetchPage()来预加载即将要加载页面。

预加载会提前下载页面所需的资源,包括主资源子资源,但不会执行网页JavaScript代码。预加载是WebviewController的实例方法,需要一个已经关联好Web组件的WebviewController实例。

在下面的示例中,在onPageEnd的时候触发下一个要访问的页面的预加载。

typescript 复制代码
import { webview } from '@kit.ArkWeb'

@Entry
@ComponentV2
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({
        src: 'https://www.example.com/',
        controller: this.webviewController
      })
        .onPageEnd(() => {
          // 预加载https://www.iana.org/help/example-domains。
          this.webviewController.prefetchPage(
            'https://www.iana.org/help/example-domains'
          )
        })
    }
  }
}

9.8 案例实战

本节通过三个案例展示ArkWeb的实际应用。基于应用拉起实现Web跳转,展示页面相互拉起及拉起系统应用场景;基于Web组件实现随机抽奖,介绍加载不同页面和抽奖功能;实现应用免密登录,展示Cookie管理操作,每个案例都涵盖相关知识点、代码结构及关键代码说明。

9.8.1 基于应用拉起相关能力实现Web跳转

本示例基于应用拉起相关能力,实现了Web页面与ArkTS页面的相互拉起、从Web页面拉起系统应用等场景。

9.8.1.1 案例效果截图

9.8.1.2 案例运用到的知识点

  1. 核心知识点
  • Web组件:onLoadIntercept拦截器拦截页面加载。
  • Navigation:组件导航。
  • UIAbility:为对应系统应用配置Want参数,使用startAbility进行拉起。
  • 环境变量获取:调用Ability接口直接获取系统环境变量
  1. 其他知识点
  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Provider/@Consumer
  • 自定义组件和组件生命周期
  • 内置组件:Column/Button
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

9.8.1.3 代码结构

less 复制代码
├──entry/src/main/ets                                   // 代码区
│  ├──common
│  |  ├──Logger.ets                                     // 日志工具类
│  |  └──Constants.ets                                  // 常量
│  ├──entryability
│  |  └──EntryAbility.ets
│  ├──entrybackupability
│  |  └──EntryBackupAbility.ets
│  └──pages
│     ├──Index.ets                                      // 入口界面
│     └──OriginPage.ets                                 // 原生页面
└──entry/src/main/resources                             // 应用资源目录

9.8.1.4 公共文件与资源

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

  1. 通用常量类
typescript 复制代码
// entry/src/main/ets/common/utils/CommonConstants.ets
export class Constants {
  static readonly ORIGIN_PAGE: string = 'OriginPage'
  static readonly FULL_SCREEN: string = '100%'
  static readonly WEB_PAGE: string = 'WebPage'
  static readonly ORIGIN_PAGE_CHINESE: string = '原生页面'
  static readonly WEB_PAGE_CHINESE: string = 'Web页面'
  static readonly ORIGIN_WEB_PAGE_LINK: string = 'resource://rawfile/index.html'
  static readonly WEB_PAGE_ADDRESS: string = 'resource://rawfile/index1.html'
  static readonly SYSTEM_LANGUAGE_KEY: string = 'systemLanguage'
  static readonly CHINESE_LANGUAGE: string = 'zh-Hans-CN'
  static readonly ENGLISH_LANGUAGE: string = 'en-Latn-CN'
}
  1. 日志类
typescript 复制代码
// main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'

export class Logger {
  private static domain: number = 0x0000
  private static prefix: string = 'WebPullOtherApplication'
  private static format: string = '%{public}s, %{public}s'

  static debug(...args: string[]): void {
    hilog.debug(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static info(...args: string[]): void {
    hilog.info(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static warn(...args: string[]): void {
    hilog.warn(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static error(...args: string[]): void {
    hilog.error(Logger.domain, Logger.prefix, Logger.format, args)
  }
}

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

  1. string.json
json 复制代码
// main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "模块描述"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "Web应用拉起"
    },
    {
      "name": "back_to_web_page",
      "value": "跳转回Web页面"
    },
    {
      "name": "original_page",
      "value": "原生页面"
    }
  ]
}
  1. float.json
json 复制代码
// main/resources/base/element/float.json
{
  "float": [
    {
      "name": "page_text_font_size",
      "value": "50fp"
    },
    {
      "name": "space_24",
      "value": "24vp"
    },
    {
      "name": "space_bottom",
      "value": "16vp"
    },
    {
      "name": "button_height",
      "value": "40vp"
    }
  ]
}

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

9.8.1.5 应用侧首页面

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
import { webview } from '@kit.ArkWeb'
import { common, Want } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { OriginPage } from './OriginPage'
import { Constants } from '../common/Constants'
import { Logger } from '../common/Logger'
import { env } from '../common/Env'

// 正则表达式用于检测资源URL前缀
const regex = /^resource:///

@Entry
@ComponentV2
struct Index {
  // 导航路径堆栈,管理页面导航
  @Provider('navPathStack') navPathStack: NavPathStack = new NavPathStack()

  // WebView 控制器
  private controller: WebviewController = new webview.WebviewController()

  // UIAbility 上下文对象
  private context = getContext(this) as common.UIAbilityContext

  // 用于存储不同 URL 对应的处理函数
  private functionsMap: Map<string, () => void> = new Map()

  // 存储当前应用的包名
  private bundleName: string = ''
  
  aboutToAppear(): void {
    this.bundleName = this.context.abilityInfo.bundleName  // 获取当前应用包名
    this.initFunctionsMap()  // 初始化 URL 处理函数映射
  }

  // 处理返回按键事件
  onBackPress(): boolean | void {
    if (this.controller.accessBackward()) { // 如果 WebView 可以后退
      this.controller.backward() // 让 WebView 执行后退操作
      return true
    }
  }

  // 页面映射方法,用于导航到不同页面
  @Builder
  PageMap(name: string) {
    if (name === Constants.ORIGIN_PAGE) {
      OriginPage() // 跳转到 OriginPage 页面
    }
  }

  // 初始化 URL 处理函数映射
  initFunctionsMap() {
    // 处理跳转到 OriginPage 的请求
    this.functionsMap.set('arkts://pages/toOriginPage',
      () => this.navPathStack.pushPath({ name: Constants.ORIGIN_PAGE }))

    // 处理跳转到系统 WiFi 设置页的请求
    this.functionsMap.set('network://pages/toSystemApp', () => {
      const want: Want = {
        bundleName: 'com.huawei.hmos.settings',
        abilityName: 'com.huawei.hmos.settings.MainAbility',
        uri: 'wifi_entry',
      }

      // 启动系统 WiFi 设置界面
      this.context.startAbility(want).then(() => {
        Logger.info(`Successfully started Ability.`)
      }).catch((err: BusinessError) => {
        Logger.error(`Failed to start Ability. Code: ${err.code},`)
        Logger.error(`Message: ${err.message}`)
      })
    })
  }

  // 组件构建 UI
  build() {
    Navigation(this.navPathStack) {
      Column() {
        Web({
          src: $rawfile(env.language == Constants.ENGLISH_LANGUAGE
            ? 'index_en.html' : 'index_cn.html'), // 根据语言加载不同 HTML 文件
          controller: this.controller
        })
          .zoomAccess(false) // 禁用缩放
          .onLoadIntercept((event) => { // 拦截页面加载事件
            const url: string = event.data.getRequestUrl() // 获取请求的 URL
            
            // 查找对应的处理函数
            const callFunc = this.functionsMap.get(url) as () => void
            
            callFunc && callFunc() // 如果存在处理函数,则执行
            return !regex.test(url) // 如果 URL 不是资源路径,则拦截
          })
      }
    }
    .hideTitleBar(true) // 隐藏标题栏
    .navDestination(this.PageMap) // 设置页面导航映射
    .mode(NavigationMode.Stack) // 使用堆栈导航模式
  }
}

关键代码说明:

  • Web组件的onLoadIntercept方法,执行url拦截工作,如果不是以resource://开头的URL地址,则拦截。
  • functionsMap对象里,根据不同的请求源,注册相应的功能函数。
  • navPathStack.pushPath实现路由跳转,context.startAbility实现Ability跳转。
  • env.language根据环境变量的系统语言的值,来载入不同的前端页面。

9.8.1.6 环境变量

  1. 定义环境变量类
typescript 复制代码
// entry/src/main/ets/common/utils/Env.ets
import { ConfigurationConstant } from '@kit.AbilityKit'

export class Env {
  language: string | undefined
  colorMode: ConfigurationConstant.ColorMode | undefined
  fontSizeScale: number | undefined
  fontWeightScale: number | undefined
}

export let env: Env = new Env()
  1. 在EntryAbility里获取并设置环境变量
typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets
// ...
import { env } from '../common/Env'

// ...

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // ...
    env.language = this.context.config.language
    env.colorMode = this.context.config.colorMode
    env.fontSizeScale = this.context.config.fontSizeScale
    env.fontWeightScale = this.context.config.fontWeightScale
  }

  // ...
}

9.8.1.7 web前端页面

  1. index_cn
html 复制代码
<!-- entry/src/main/resource/rawfile/index_cn.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Document</title>
    <link rel="stylesheet" href="./css/styles.css">
  </head>

  <body>
    <div class="web_page_demo">
      <div class="title">Web和应用的跳转与拉起</div>
      <ul>
        <li>
          <a class="function_item" href="arkts://pages/toOriginPage">
            跳转到原生页面
          </a>
        </li>
        <li>
          <a class="function_item" href="./index1_cn.html">
            跳转到Web页面
          </a>
        </li>
        <li>
          <a class="function_item" href="network://pages/toSystemApp">
            拉起系统应用
          </a>
        </li>
      </ul>
    </div>
  </body>
</html>
<script></script>
  1. index1_cn
html 复制代码
<!-- entry/src/main/resource/rawfile/index1_cn.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Document</title>
    <link rel="stylesheet" href="./css/styles1.css">
</head>
<body>
<div class="web_page_demo">
    <div class="header">
        <div class="back">
            <img onclick="backup()" src="./statics/ic_back.png" alt=""/>
        </div>

        <span>Web 页面</span>
    </div>
    <div class="title">Hello Web</div>
    <a class="function_item" href="javascript:void(0);" onclick="backup()"
    >跳转回Web页面</a
    >
</div>
</body>
</html>
<script>
    function backup(event) {
      window.history.back()
    }
</script>

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

9.8.1.8 应用侧原始页面

typescript 复制代码
// main/ets/pages/OriginPage.ets
import { Constants } from '../common/Constants'
import { env } from '../common/Env'

@ComponentV2
export struct OriginPage {
  @Consumer('navPathStack') navPathStack: NavPathStack = new NavPathStack()

  build() {
    NavDestination() {
      Column() {
        Button($r('app.string.back_to_web_page'))
          .width(Constants.FULL_SCREEN)
          .height($r('app.float.button_height'))
          .onClick(() => {
            this.navPathStack.pop()
          })
      }
      .width(Constants.FULL_SCREEN)
      .height(Constants.FULL_SCREEN)
      .justifyContent(FlexAlign.End)
      .padding({
        left: $r('app.float.space_24'),
        right: $r('app.float.space_24'),
        bottom: $r('app.float.space_bottom')
      })
    }
    .title(env.language == Constants.ENGLISH_LANGUAGE 
           ? Constants.ORIGIN_PAGE : Constants.ORIGIN_PAGE_CHINESE)
  }
}

关键代码说明:

  • @Consumer('navPathStack')通过消费父组件的navPathStack,获得路由栈对象。
  • env.language == Constants.ENGLISH_LANGUAGE根据系统语言显示title。

9.8.1.9 代码与视频教程

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

代码:Code-09-01.zip。

视频:《基于应用拉起相关能力实现Web跳转》。

9.8.2 基于Web组件实现随机抽奖

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

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

9.8.2.1 案例效果截图

9.8.2.2 案例运用到的知识点

  1. 核心知识点
  • Web:提供具有网页显示能力的Web组件。
  • runJavaScript:异步执行JavaScript脚本,并通过回调方式返回脚本执行的结果。
  • 在配置文件module.json5文件里添加网络权限:ohos.permission.INTERNET。
  • 搭建服务端环境。
  1. 其他知识点
  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Local/@Provider/@Consumer
  • 渲染控制:if/ForEach
  • 自定义组件和组件生命周期
  • 自定义构建函数@Builder
  • @Extend:定义扩展样式
  • Navigation:导航组件
  • 内置组件:Stack/Progress/Image/Column/Row/Text/Button
  • 常量与资源分类的访问
  • MVVM模式

9.8.2.3 代码结构

less 复制代码
├──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服务端代码

9.8.2.4 公共文件与资源

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

  1. 通用常量类
typescript 复制代码
// entry/src/main/ets/common/utils/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
}

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

  1. string.json
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."
    }
  ]
}
  1. color.json
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"
    }
  ]
}

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

9.8.2.5 前端页面

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

  1. 原生Web抽奖代码
typescript 复制代码
// 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由应用端定义并调用,用于处理从网页端传递的消息。
  1. Vue抽奖代码
javascript 复制代码
// 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由应用端定义并调用,用于处理从网页端传递的消息。

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

9.8.2.6 Web组件

  1. 首页面
typescript 复制代码
// 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) // 使用堆栈式导航
  }
}

关键代码说明:

typescript 复制代码
// 代码片段
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分别用于定义本地与云端前端页面的加载路径。
  1. Web页面
html 复制代码
// 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'))
  }
}

关键代码说明:

typescript 复制代码
// 代码片段
  .javaScriptProxy({
    object: this.linkObj,
    name: 'linkObj',
    methodList: ['messageFromHtml'],
    controller: this.webController
  })
  • 应用侧代码通过javaScriptProxy方法定义代理供前端调用。
typescript 复制代码
// 代码片段
.onClick(() => {
  this.webController.runJavaScript('startDraw()')
})
  • 应用侧通过runJavaScript函数调用前端函数。
  1. Vue页面
typescript 复制代码
// 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')) // 设置导航标题
  }
}

关键代码说明:

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

9.8.2.7 运行前端云端工程

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

9.8.2.8 代码与视频教程

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

代码:Code-09-02.zip。

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

9.8.3 实现应用免密登录

本案例使用ArkTS语言实现一个简单的免登录过程,展示基本的cookie管理操作。主要包含以下功能:

  1. 获取指定url对应的cookie的值。
  2. 设置cookie。
  3. 清除所有cookie。
  4. 免登录访问账户中心。

本案例使用的是在线网页,需添加网络权限:ohos.permission.INTERNET。

9.8.3.1 案例效果截图

9.8.3.2 原理说明

本应用旨在说明Web组件中cookie的管理操作。结合应用弹框和界面跳转两种方式进行讲解。

  • 应用弹框验证cookie读写

若用户已通过"设置cookie"完成cookie写操作,点击应用首页的"获取cookie"按钮,则应用弹窗中会带有"info=测试cookie写入"的信息。若用户未进行写操作,则弹窗信中无对应信息。

  • 界面跳转验证cookie存储

若用户在应用首页完成登录操作,则点击"验证cookies"按钮,界面会跳转至"关于"界面;若用户此前未完成登录操作,则会跳转至登录界面。这里借助真实的登录过程,体现了Web组件自动存储登录后的会话cookie,并在整个应用中生效的能力。

9.8.3.3 代码结构

less 复制代码
├──entry/src/main/ets               // 代码区
│  ├──common                         
│  │  ├──constants                   
│  │  │  └──CommonConstant.ets      // 常量类
│  │  └──utils                       
│  │     ├──DialogUtil.ets          // 弹框工具类 
│  │     └──Logger.ets              // 日志工具类
│  ├──entryability                    
│  │  └──EntryAbility.ets            // 程序入口类
│  ├──pages                          
│  │  ├──Verify.ets                 // 免登录验证界面
│  │  └──WebIndex.ets               // 应用首页
│  └──view                                
│     └──LinkButton.ets             // 链接按钮组件
└──entry/src/main/resources         // 应用资源目录

9.8.3.4 公共文件与资源

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

  1. 通用常量类
typescript 复制代码
// entry/src/main/ets/common/utils/CommonConstants.ets
export class CommonConstants {
  static readonly USER_CENTER_URL = 'https://id1.cloud.huawei.com/AMW/portal/userCenter/index.html?' +
    'service=http://developer.huawei.com/consumer/cn/#/accountCenter/userCenter'
  static readonly USER_ABOUT_URL = 'https://id1.cloud.huawei.com/AMW/portal/userCenter/index.html?' +
    'service=http://developer.huawei.com/consumer/cn/#/accountCenter/about'
  static readonly NAVIGATOR_SIZE = '20fp'
  static readonly TITLE_SIZE = '18fp'
  static readonly BUTTON_SIZE = '14fp'
  static readonly DIVIDER_MARGIN = 8
  static readonly DIVIDER_HEIGHT = '2.2%'
  static readonly PAGE_VERIFY = 'pages/Verify'
  static readonly PAGE_INDEX = 'pages/WebIndex'
  static readonly FULL_WIDTH = '100%'
  static readonly FULL_HEIGHT = '100%'
  static readonly BACK_WIDTH = '6.7%'
  static readonly BACK_HEIGHT = '3.1%'
  static readonly WEB_HEIGHT = '70%'
  static readonly WEB_WIDTH = '90%'
  static readonly PAGE_TITLE_HEIGHT = '3.1%'
  static readonly PAGE_TITLE_WIDTH = '25.6%'
  static readonly NAVIGATOR_MARGIN_TOP = '1.7%'
  static readonly NAVIGATOR_MARGIN_LEFT = '10%'
  static readonly FONT_WEIGHT_DEEPER = 500
  static readonly APP_TITLE_MARGIN_TOP = '15vp'
  static readonly APP_TITLE_MARGIN_LEFT = '26vp'
  static readonly WEB_MARGIN_BOTTOM = '30vp'
  static readonly SUB_LENGTH: number = 300
}

export enum CookieOperation {
  GET_COOKIE = '读取cookie',
  SET_COOKIE = '设置cookie',
  DELETE_COOKIE = '删除cookie',
  VERIFY_COOKIE = '验证cookie'
}
  1. 日志类
typescript 复制代码
// entry/src/main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'

class Logger {
  private domain: number
  private prefix: string
  private format: string = '%{public}s, %{public}s'

  constructor(prefix: string = 'MyApp', domain: number = 0xFF00) {
    this.prefix = prefix
    this.domain = domain
  }

  debug(...args: string[]): void {
    hilog.debug(this.domain, this.prefix, this.format, args)
  }

  info(...args: string[]): void {
    hilog.info(this.domain, this.prefix, this.format, args)
  }

  warn(...args: string[]): void {
    hilog.warn(this.domain, this.prefix, this.format, args)
  }

  error(...args: string[]): void {
    hilog.error(this.domain, this.prefix, this.format, args)
  }
}

export default new Logger('WebCookie', 0xFF00)
  1. 弹框函数
typescript 复制代码
// entry/src/main/ets/common/utils/DialogUtil.ets
import Logger from '../utils/Logger'
import { CommonConstants } from '../constants/CommonConstant'

export function showDialog(message: ResourceStr): void {
  let newMessage = message.toString()
  if (newMessage.length > CommonConstants.SUB_LENGTH) {
    message = newMessage.substring(0, CommonConstants.SUB_LENGTH)
  }
  AlertDialog.show(
    {
      title: $r('app.string.dialog_message'),
      message: message,
      confirm: {
        value: $r('app.string.button_confirm'),
        action: () => {
          Logger.info('Button-clicking callback')
        }
      },
      cancel: () => {
        Logger.info('Closed callbacks')
      }
    }
  )
}

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

  1. string.json
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": "button_confirm",
      "value": "确定"
    },
    {
      "name": "dialog_message",
      "value": "信息"
    },
    {
      "name": "navigator_name",
      "value": "Web组件"
    },
    {
      "name": "title_name",
      "value": "Web组件内"
    },
    {
      "name": "write_success",
      "value": "写入cookie成功"
    },
    {
      "name": "write_fail",
      "value": "写入cookie失败"
    },
    {
      "name": "delete_success",
      "value": "删除cookie成功"
    },
    {
      "name": "test_content",
      "value": "info=测试cookie写入"
    },
    {
      "name": "load_error",
      "value": "加载页面失败:请检查网络连接。"
    },
    {
      "name": "reason",
      "value": "Used to initiate network data requests."
    }
  ]
}
  1. float.json
json 复制代码
// entry/src/main/resources/base/element/float.json
{
  "float": [
    {
      "name": "page_title_margin_top",
      "value": "48vp"
    },
    {
      "name": "page_title_margin_bottom",
      "value": "28vp"
    }
  ]
}
  1. color.json
json 复制代码
// entry/src/main/resources/base/element/color.json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "link_blue",
      "value": "#007DFF"
    },
    {
      "name": "navigator_black",
      "value": "#000000"
    },
    {
      "name": "title_black",
      "value": "#182431"
    },
    {
      "name": "page_background_grey",
      "value": "#F1F3F5"
    }
  ]
}

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

9.8.3.5 Cookie读写操作

  1. 首页

首次打开应用时,首页的Web组件内呈现的是登录界面。用户完成登录操作后,会跳转至账号中心界面。首页包含"读取cookie"、"设置cookie"和"删除cookie"等多个按钮,可对cookie进行读取、设置和删除等操作。

typescript 复制代码
// entry/src/main/ets/pages/WebIndex.ets
import { webview } from '@kit.ArkWeb'
import { CommonConstants, CookieOperation } from '../common/constants/CommonConstant'
import { LinkButton } from '../view/LinkButton'
import { showDialog } from '../common/utils/DialogUtil'

@Entry
@ComponentV2
struct WebIndex {
  // 创建 WebView 控制器,用于管理 Web 视图
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      // 应用导航标题
      Text($r('app.string.navigator_name'))
        .fontSize(CommonConstants.NAVIGATOR_SIZE)
        .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
        .fontColor($r('app.color.navigator_black'))
        .width(CommonConstants.FULL_WIDTH)
        .margin({
          top: CommonConstants.APP_TITLE_MARGIN_TOP,
          left: CommonConstants.APP_TITLE_MARGIN_LEFT 
        })

      // 页面主标题
      Text($r('app.string.title_name'))
        .fontSize(CommonConstants.TITLE_SIZE)
        .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
        .fontColor($r('app.color.title_black'))
        .textAlign(TextAlign.Center)
        .width(CommonConstants.WEB_WIDTH)
        .height(CommonConstants.PAGE_TITLE_HEIGHT)
        .margin({
          top: $r('app.float.page_title_margin_top'),
          bottom: $r('app.float.page_title_margin_bottom')
        })

      // 加载 WebView,嵌入用户中心页面
      Web({
        src: CommonConstants.USER_CENTER_URL, // WebView 加载的 URL
        controller: this.controller // 绑定 WebView 控制器
      })
        .domStorageAccess(true) // 允许访问 DOM 存储
        .javaScriptAccess(true) // 允许执行 JavaScript
        .height(CommonConstants.WEB_HEIGHT)
        .width(CommonConstants.WEB_WIDTH)
        .margin({ bottom: CommonConstants.WEB_MARGIN_BOTTOM }) 
        .onErrorReceive((event) => { // 监听 WebView 加载错误事件
          if (event?.request.isMainFrame()) { // 检查是否为主框架加载失败
            let message = $r('app.string.load_error') // 加载失败提示信息
            showDialog(message) // 显示错误对话框
          }
        })

      // 操作按钮区域(获取、设置、删除和验证 Cookie)
      Row() {
        LinkButton({ buttonType: CookieOperation.GET_COOKIE, isNeedDivider: true }) // 获取 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.SET_COOKIE, isNeedDivider: true }) // 设置 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.DELETE_COOKIE, isNeedDivider: true }) // 删除 Cookie 按钮
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width(CommonConstants.WEB_WIDTH)
    }
    .backgroundColor($r('app.color.page_background_grey'))
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}

关键代码说明:

  • LinkButton({...})自定义组件,用于实现获取、设置、删除和验证 Cookie。
  1. LinkButton组件
typescript 复制代码
// entry/src/main/ets/view/LinkButton.ets
import { webview } from '@kit.ArkWeb'
import { router } from '@kit.ArkUI'
import { showDialog } from '../common/utils/DialogUtil'
import { CommonConstants, CookieOperation } from '../common/constants/CommonConstant'

/**
 * LinkButton 组件
 * 用于执行 Cookie 操作(获取、设置、删除)或导航到验证页面
 */
@ComponentV2
export struct LinkButton {
  @Param buttonType?: string = '' // 按钮类型(表示不同的 Cookie 操作)
  @Param isNeedDivider?: boolean = true // 是否需要分隔线(用于 UI 视觉分割)

  /**
   * 组件构建方法
   */
  build() {
    Row() { // 水平排列按钮和分隔线
      // 按钮文本
      Text(this.buttonType)
        .fontColor($r('app.color.link_blue')) // 设置字体颜色
        .fontSize(CommonConstants.BUTTON_SIZE) // 设置字体大小
        .textAlign(TextAlign.Center) // 文本居中
        .fontWeight(FontWeight.Normal) // 设置字体粗细
        .onClick(() => {
          this.operationMethod() // 绑定点击事件,执行相应操作
        })

      // 是否需要分隔线
      if (this.isNeedDivider) {
        Divider()
          .vertical(true) // 竖直分隔线
          .margin(CommonConstants.DIVIDER_MARGIN) // 设置分隔线边距
          .height(CommonConstants.DIVIDER_HEIGHT) // 设置分隔线高度
      }
    }
  }

  /**
   * 执行不同类型的 Cookie 操作或页面跳转
   */
  operationMethod(): void {
    try {
      if (this.buttonType === CookieOperation.GET_COOKIE) {
        // 获取指定 URL 的 Cookie
        let originCookie = webview.WebCookieManager.fetchCookieSync(CommonConstants.USER_CENTER_URL)
        showDialog(originCookie) // 显示获取到的 Cookie
      } else if (this.buttonType === CookieOperation.SET_COOKIE) {
        // 设置 Cookie
        webview.WebCookieManager.configCookieSync(CommonConstants.USER_ABOUT_URL, 'info=测试cookie写入')
        showDialog($r('app.string.write_success')) // 显示成功写入提示
      } else if (this.buttonType === CookieOperation.DELETE_COOKIE) {
        // 清除所有 Cookie
        webview.WebCookieManager.clearAllCookiesSync()
        let deleteMessage = $r('app.string.delete_success')
        showDialog(deleteMessage) // 显示删除成功提示
      } else {
        // 其他情况,执行页面跳转到 Cookie 验证页面
        // TODO
      }
    } catch (error) {
      // 捕获异常并显示错误信息
      showDialog('Operation failed: ' + JSON.stringify(error))
    }
  }
}

关键代码说明:

  • WebCookieManager.fetchCookieSync:获取指定 URL 的 Cookie。
  • WebCookieManager.configCookieSync:设置 Cookie
  • WebCookieManager.clearAllCookiesSync:清除所有 Cookie

9.8.3.6 cookie存储验证

一个应用中的所有Web组件共享一个WebCookie,因此一个应用中Web组件存储的cookie信息,也是可以共享的。当用户在应用内完成登录操作时,Web组件会自动存储登录的会话cookie。应用内其他页面可共享当前会话cookie信息,免去多余的登录操作。

  1. 首页入口
typescript 复制代码
// entry/src/main/ets/pages/WebIndex.ets
// ...

@Entry
@ComponentV2
struct WebIndex {
  // ...
  
  build() {
    Column() {
      // ...

      // 操作按钮区域
      Row() {
        // ...
        
        // 验证 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.VERIFY_COOKIE, isNeedDivider: false })
      }
      // ...
    }
    // ...
  }
}
  1. LinkButton组件路由跳转
typescript 复制代码
// entry/src/main/ets/view/LinkButton.ets
// ...

@ComponentV2
export struct LinkButton {
  // ...
  
  /**
   * 执行不同类型的 Cookie 操作或页面跳转
   */
  operationMethod(): void {
    try {
      if (this.buttonType === CookieOperation.GET_COOKIE) {
        // ...
      } else {
        // 其他情况,执行页面跳转到 Cookie 验证页面
        router.pushUrl({
          url: CommonConstants.PAGE_VERIFY
        })
      }
    }
    // ...
  }
}
  1. cookie验证
typescript 复制代码
// entry/src/main/ets/pages/Verify.ets
import { webview } from '@kit.ArkWeb'
import { showDialog } from '../common/utils/DialogUtil'
import { CommonConstants } from '../common/constants/CommonConstant'

/**
 * Verify 组件
 * 用于加载 `USER_ABOUT_URL` 页面,并在页面加载完成时尝试获取 Cookie。
 */
@Entry
@ComponentV2
struct Verify {
  @Local navPathStack: NavPathStack = new NavPathStack() // 导航路径管理
  fileAccess: boolean = true // 是否允许文件访问
  controller: webview.WebviewController = new webview.WebviewController() // WebView 控制器
  isRedirect: boolean = false // 标记是否已经进行跳转,防止重复操作

  /**
   * 页面显示时触发
   * 重新设置 `isRedirect` 为 false,确保每次进入页面时可以正确获取 Cookie
   */
  onPageShow(): void {
    this.isRedirect = false
  }

  /**
   * 组件构建方法
   */
  build() {
    Navigation(this.navPathStack) {
      Column() {
        Text($r('app.string.title_name'))
          .fontSize(CommonConstants.TITLE_SIZE)
          .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
          .fontColor($r('app.color.title_black'))
          .width(CommonConstants.PAGE_TITLE_WIDTH) 
          .height(CommonConstants.PAGE_TITLE_HEIGHT)
          .margin({
            top: $r('app.float.page_title_margin_top'),
            bottom: $r('app.float.page_title_margin_bottom')
          })

        // WebView 组件
        Web({
          src: CommonConstants.USER_ABOUT_URL, // 目标网页 URL
          controller: this.controller // 绑定 WebView 控制器
        })
          .height(CommonConstants.WEB_HEIGHT)
          .width(CommonConstants.WEB_WIDTH)
          .fileAccess(this.fileAccess) // 允许文件访问
          .javaScriptAccess(true) // 允许 JavaScript 交互
          .onPageEnd(() => {
            try {
              // 在页面加载完成后尝试获取 Cookie
              let originCookie = webview.WebCookieManager.fetchCookieSync(CommonConstants.USER_ABOUT_URL)
              if (originCookie === '' || this.isRedirect) {
                return // 如果 Cookie 为空或者已经重定向过,则不执行操作
              }
              this.isRedirect = true // 设置跳转标志,避免重复执行
              showDialog(originCookie) // 显示 Cookie 信息
            } catch (error) {
              // 捕获错误并显示弹窗提示
              showDialog('Failed to load the web page. ' + JSON.stringify(error))
            }
          })
      }
      .backgroundColor($r('app.color.page_background_grey'))
      .width(CommonConstants.FULL_WIDTH) 
      .height(CommonConstants.FULL_HEIGHT)
    }
    .titleMode(NavigationTitleMode.Mini)
    .title($r('app.string.navigator_name'))
  }
}

关键代码解读:

  • 在WebView页面加载完成后,同步获取指定URL(USER_ABOUT_URL) 的 Cookie。
  • 如果Cookie为空或已经执行过跳转 (isRedirect为true),则不进行任何操作,防止重复执行。否则,将isRedirect置为true。
  • 通过showDialog显示获取到的Cookie信息。

9.8.3.7 代码与视频教程

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

代码:Code-09-03.zip。

视频:《实现应用免密登录》。

9.9 本章小结

本章全面阐述了 ArkWeb适配与开发相关知识。先介绍ArkWeb架构及Web组件功能,如页面加载、交互和调试等。接着讲述组件生命周期、多种页面操作及应用与前端交互方式。还涉及Cookie管理、调试优化手段,最后通过多个案例展示实际应用,助力开发者掌握ArkWeb开发要点。

相关推荐
脑极体3 小时前
开源鸿蒙,给机器人带来了什么?
华为·机器人·开源·harmonyos
HarmonyOS_SDK5 小时前
意图框架事件推荐方案,精准匹配用户需求
harmonyos
Tolitres5 小时前
「鸿蒙 NEXT」基于 taskpool 实现自定义 Timer 工具类
harmonyos
城中的雾6 小时前
一键多环境构建——用 Hvigor 玩转 HarmonyOS Next
harmonyos
别说我什么都不会6 小时前
【仓颉三方库】分布式——config-client
harmonyos
咸鱼过江6 小时前
openharmony5.0.0中C++公共基础类测试-线程相关(一)
c++·harmonyos
我爱鸿蒙开发7 小时前
🥇聊聊鸿蒙的一端开发,多端部署。
前端·开源·harmonyos
智驾10 小时前
HarmonyOS 是 Android 套壳嘛?
android·harmonyos·替代·套壳
悬空八只脚11 小时前
React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part2
harmonyos
鸿蒙开发工程师—阿辉12 小时前
HarmonyOS Next 编译之如何使用多目标产物不同包名应用
华为·harmonyos