📚往期笔录记录🔖:
🔖鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🔖嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🔖对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🔖鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🔖记录一场鸿蒙开发岗位面试经历~
🔖持续更新中……
一、混合开发的背景和好处
混合开发(Hybrid Development)是一种结合原生应用和Web应用的开发模式,旨在同时利用两者的优势。随着移动应用需求的多样化和复杂化,单一的开发方式往往难以满足所有需求。混合开发提供了一种灵活、高效的解决方案,特别是在以下方面具有显著的优势:
-
跨平台兼容:混合开发允许开发者编写一次代码,并在多个平台(如Android、iOS、HarmonyOS等)上运行。这大大减少了开发和维护成本。
-
快速迭代:Web技术(如HTML、CSS、JavaScript)的快速开发和部署能力,使得混合应用可以更快地进行迭代和更新。
-
丰富的Web生态:借助丰富的Web生态系统,开发者可以利用大量现有的Web库和框架,快速实现复杂功能。
-
原生性能:通过将关键功能部分使用原生代码实现,混合应用可以在保证性能的同时,享受Web开发的灵活性。
二、混合开发应用的结构
为了更好地理解混合开发的概念,以下是一张示意图,展示了混合开发架构中原生代码与Web代码的结合。
图中展示了混合开发应用的结构,其中包括:
- 原生层(Native Layer):包括操作系统(如HarmonyOS)、设备硬件和原生API,提供高性能和底层功能支持。
- Web层(Web Layer):包括HTML、CSS、JavaScript等Web技术,负责应用的界面和逻辑部分。
- 桥接层(Bridge Layer):连接原生层和Web层,允许两者之间的数据和功能交互。
通过这种架构,开发者可以在Web层快速构建界面和业务逻辑,同时利用原生层提供的高性能和丰富功能,实现混合开发的最佳效果。
在HarmonyOS NEXT Developer Beta1版本中,ArkTS 提供了强大的混合开发能力,允许开发者在应用中嵌入 Web 组件,利用 Web 技术构建应用的一部分。接下来,我将介绍如何在 ArkTS 中使用 Web 组件。
三、ArkTS Web 组件教程
一、语法说明
在 ArkTS 中,混合开发主要通过 Web
组件和 WebView
API 来实现。通过这些工具,可以加载和显示网页内容,进行页面控制和数据交互。
1. 基本语法
Web 组件用于在界面中嵌入一个网页浏览器。它的常用属性包括:
src
:指定要加载的网页资源地址。controller
:webview控制器。
src: ResourceStr = 'https://m.jd.com'
Web({ src: this.src, controller: this.controller })
注意:访问在线网页时需添加网络权限:ohos.permission.INTERNET
2. 事件处理
Web 组件支持多种事件,如:
onPageBegin
:页面开始加载时触发。onPageEnd
:页面加载完成时触发。onErrorReceive
:页面加载出错时触发。onTitleReceive
:网页document标题更改时触发该回调。onProgressChange
:网页加载进度变化时触发该回调。onRefreshAccessedHistory
:加载网页页面完成时触发该回调,用于应用更新其访问的历史链接。onAppear
:组件挂载显示后触发此回调(通用事件)。
Web({ src: this.src, controller: this.controller })
.onProgressChange((data) => {
})
.onPageBegin(() => {
})
.onPageEnd(() => {
})
.onErrorReceive((e) => {
})
.onTitleReceive((data) => {
})
.onRefreshAccessedHistory(() => {
})
.onAppear(() => {
})
3. WebView API
WebView
API 提供了一组方法和属性,用于更细粒度地控制 Web 组件,如加载URL、执行JavaScript代码等。
import { webview } from '@kit.ArkWeb'
// 1\. 创建 WebView 实例
controller = new webview.WebviewController()
// 2\. 调用WebView 实例 api 进行控制
run() {
// 动态加载页面
this.controller.loadUrl('url')
// 注入 js 对象
this.controller.registerJavaScriptProxy
// ...
}
Web({ src: this.src, controller: this.controller })
四、实战案例
下面是一个完整的小案例,展示如何在 ArkTS 中使用 Web 组件加载网页、处理页面事件,并与网页进行交互。
开发环境
DevEco Studio NEXT Developer Beta1
构建版本:5.0.3.403, built on June 20, 2024
Runtime version: 17.0.10+1-b1087.17 aarch64
1. 新建工程
主要目录结构:
|-- entry
| |-- src
| | |-- main
| | | |-- ets
| | | | |-- pages
| | | | | |-- Index.ets(案例代码)
|-- module.json5
2. 配置权限
在 module.json5
中配置网络权限:
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
// ...
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
// ...
],
"extensionAbilities": [
// ...
]
}
}
3. 加载页面
在 Index.ets
文件中,使用 Web 组件和 WebView
API,加载京东商城首页:
@Entry
@Component
struct WebPage {
// 1\. web 组件基本使用
src: ResourceStr = 'https://m.jd.com'
// webview控制器
controller = new webview.WebviewController()
build() {
Navigation() {
Web({ src: this.src, controller: this.controller })
}
.title('混合开发')
.titleMode(NavigationTitleMode.Mini)
}
}
效果如图:
4. 页面标题
获取当前加载网页标题,进行动态展示:
// ...
@State
// 当前网页的标题
title: string = ''
build() {
Navigation() {
Web({ src: this.src, controller: this.controller })
.onTitleReceive((data) => {
this.title = data?.title || '--'
})
}
.title(this.title)
.titleMode(NavigationTitleMode.Mini)
}
效果如图:
5. 页面进度
获取页面进度数据,添加进度条效果:
第一步:定义状态,存储加载中和进度数据
// 网页是否在加载中
@State loading: boolean = true
// 网页加载进度
@State progress: number = 0
第二步:添加进度条结构
build() {
Navigation() {
Stack({ alignContent: Alignment.Top }) {
// 加载中显示进度条
if (this.loading) {
Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
.style({ strokeWidth: 3, enableSmoothEffect: true })
.color('#fd1800')
.zIndex(1)
}
Web({ src: this.src, controller: this.controller })
.onTitleReceive((data) => {
this.title = data?.title || '--'
})
}
.width('100%')
.layoutWeight(1)
}
.title(this.title)
.titleMode(NavigationTitleMode.Mini)
}
第三步:添加事件获取进度数据
Web({ src: this.src, controller: this.controller })
.onProgressChange((data) => {
if (data) {
// 记录加载进度
this.progress = data.newProgress
if (data.newProgress === 100) {
// 加载完成进度条消失
animateTo({ duration: 300, delay: 200 }, () => {
this.loading = false
})
}
}
})
.onPageBegin(() => {
console.log('mgx', '开始加载网页')
this.progress = 0
this.loading = true
})
.onPageEnd(() => {
console.log('mgx', '网页加载完成')
})
ok,看效果:
6. 页面返回和刷新
下边咱们来获取 web 容器中页面的历史记录进行返回控制和页面刷新操作。 点击左上角返回按钮,如果容器中页面有历史记录,则返回历史页面;否则,返回原生记录页面。
第一步:定义状态,存储当前历史记录索引
// 当前访问页面历史记录索引
historyCurrIndex: number = 0
第二步:添加事件获取历史记录
// ...
.onRefreshAccessedHistory(() => {
// == 网页加载完成可访问页面历史记录 ==
// 获取当前Webview的页面历史记录列表
const history = this.controller.getBackForwardEntries()
// 当前页面历史记录索引
this.historyCurrIndex = history.currentIndex
// 历史记录索引的数量,最多保存50条,超过时起始记录会被覆盖
console.log('mgx', history.size)
})
第三步:在原生返回钩子函数中控制逻辑
onBackPress() {
// 在web容器中, 当前页面之前还有页面, 则容器内返回上一页
if (this.historyCurrIndex > 0) {
this.controller.backward()
} else {
// 返回原生页面
router.back()
}
// 自定义返回逻辑
return true
}
在页面标题中添加菜单图标,点击刷新页面。
第一步:给Navigation组件右侧添加菜单(图标随便)
build() {
Navigation() {
// ...
}
.title(this.title)
.titleMode(NavigationTitleMode.Mini)
.menus(this.titleMenus)
}
第二步:定义菜单 Builder 绑定事件控制刷新
@Builder
titleMenus() {
Row() {
Image($r('app.media.startIcon'))
.width(18)
.aspectRatio(1)
.margin({ right: 10 })
.onClick(() => {
// 刷新网页
this.controller.refresh()
})
}
.width(50)
.height('100%')
.justifyContent(FlexAlign.End)
.alignItems(VerticalAlign.Center)
}
7. JSBridge代理
接下来,进入重点环节,我们来学习实操下如何向 Web 容器的网页中注入 JS 对象,给网页提供原生的能力支持,例如: 选择相册、拍照、传感器等底层能力。
核心依赖webview 提供的registerJavaScriptProxy
api,提供了应用与Web组件加载的网页之间强大的交互能力。注入JavaScript对象到window对象中,并在window对象中调用该对象的方法。
第一步:定义注入 JS 的类型
示例注入了两个函数:
- test 方法,获取网页调用后传参
- select 方法,选择原生相册,获取选择图片结果显示到网页中
interface InjectJs {
// 测试方法
test: (a: string) => void
// 选择相册
select: () => Promise<string>
}
type InjectKeys = keyof InjectJs
第二步:定义注入的方法和逻辑
// 2\. JSBridge代理
webInject() {
this.controller.registerJavaScriptProxy({
// 参数 1:注入应用侧JavaScript对象
// 参数 2:注入对象的名称,与window中调用的对象名一致
// 参数 3:注入后window对象可以通过此名字访问应用侧JavaScript对象
test: (a) => {
AlertDialog.show({
message: `网页传参:${a}`
})
},
select: async () => {
// 1\. 打开相册选择图片
const photoSelectOptions = new picker.PhotoSelectOptions()
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
const photoPicker = new picker.PhotoViewPicker();
const res = await photoPicker.select(photoSelectOptions)
// 2\. 文件操作
// 2.1 获取照片的uri地址
const uri = res.photoUris[0]
// 2.2 根据uri同步打开文件
const file = fs.openSync(uri)
// 2.3 同步获取文件的详细信息
const stat = fs.statSync(file.fd)
// 2.4 创建缓冲区存储读取的文件流
const buffer = new ArrayBuffer(stat.size)
// 2.5 开始同步读取文件流到缓冲区
fs.readSync(file.fd, buffer)
// 2.6 关闭文件流
fs.closeSync(file)
// 3\. 转成base64编码的字符串
const helper = new util.Base64Helper()
const str = helper.encodeToStringSync(new Uint8Array(buffer))
return 'data:image/png;base64,' + str
}
} as InjectJs,
'mg',
[
'test',
'select',
] as InjectKeys[])
}
第三步:在通用事件onAppear
执行注入
Web({ src: this.src, controller: this.controller })
// ...
.onAppear(() => {
// JSBridge代理注入
this.webInject()
})
第四步:本地创建网页加载
这里使用的是原生 js 开发网页端,实际开发中可选择 vue 、 react 、 uni-app 等框架开发,效率更高。
目录:entry/src/main/resources/rawfile/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>混合开发</title>
<style>
* {
margin: 0;
padding: 0;
}
ul li {
padding: 10px;
}
</style>
</head>
<body>
<div>
<h1>H5页面</h1>
<div>
<p>
<!-- 显示选择相册的图片 -->
<img id="img"
src="">
</p>
</div>
<ul>
<li>
<button id="test">调用原生test</button>
</li>
<li>
<button id="sel">调用原生select选择相册</button>
</li>
</ul>
</div>
<script>
window.onload = () => {
document.querySelector('#test').addEventListener('click', () => {
mg.test('我是来自web数据啦')
})
document.querySelector('#sel').addEventListener('click', async () => {
const b64 = await mg.select()
document.querySelector('#img').setAttribute('src', b64)
})
}
</script>
</body>
</html>
页面效果:
第五步:加载本地网页和测试调用
重新运行项目,点击按钮调用注入的函数测试
// 1\. web 组件基本使用
// src: ResourceStr = 'https://m.jd.com'
src: ResourceStr = $rawfile('index.html')
效果如图:
第六步:使用eruda
插件进行真机调试
<script src="https://cdnjs.cloudflare.com/ajax/libs/eruda/2.4.1/eruda.min.js"></script>
<script>eruda.init()</script>
效果如图:
8. 执行网页JS
有时候,我们需要在原生端调用网页中 JS,完成一些业务需求。
第一步:网页中挂载 JS到 window
<script>
window.go = () => {
return '我是网页JS函数'
}
window.onload = () => {
// ...
}
</script>
第二步:原生端调用挂载的 JS
借助 webview 提供runJavaScript api 异步执行JavaScript脚本,并通过回调方式返回脚本执行的结果。
// 3. 执行网页 js
runJs() {
// this.controller.loadUrl($rawfile('index.html'))
this.controller.runJavaScript('go()', (error, res) => {
if (error) {
return AlertDialog.show({
message: error.message
})
}
// 获取执行返回值
AlertDialog.show({
message: res
})
})
}
build() {
Navigation() {
Button('执行网页 js')
.onClick(() => {
this.runJs()
})
// ...
}
}
五、总结
本文介绍了在 HarmonyOS NEXT Developer Beta1 版本中,使用 ArkTS 进行混合开发时 Web 组件的基本用法。通过 Web
组件和 WebView
API,可以轻松地在应用中嵌入和控制网页内容。以下是几个关键点的总结:
- 基本语法:通过
Web
组件可以加载和显示网页,处理页面事件,并与网页进行交互。 - 事件处理:支持多种事件处理,如页面开始加载、加载完成和加载出错、访问历史记录等。
- API 使用:
WebView
API 提供了丰富的方法,用于充当 Bridge 代理,更好和网页端进行通信。
通过本文的介绍和示例代码,你应该能够初步掌握在 ArkTS 中使用 Web 组件的基本方法,并应用于实际开发中。混合开发可以让你充分利用 Web 技术的优势,同时享受 ArkTS 的高效开发体验。