1.前言
在鸿蒙开发中,将 Web 页面与原生鸿蒙能力相结合可以极大地丰富应用的功能和用户体验。本笔记旨在记录在 Web 页面中调用原生鸿蒙的相册服务、拍照功能以及传感器的过程和要点。
本文承接前面的一篇文章继续对权限调用进行补充 本文具体讲的是在鸿蒙框架里使用了web组件,里面的代码内容全部是用前端语言框架来实现的,然后我们怎么在鸿蒙里组合两者之间的优势,一个是性能更好,权限更好管理,一个是代码更容易开发和直接拿来服用,所以这篇文章就是两者的结合,通过鸿蒙一个大的框架,里面包裹着前端html的内容,然后html还能使用鸿蒙相关的原生能力,极大的提高了开发成本和效率,接下来一起看一下一些简单的示例吧😊
2.注册JavaScript代理
首先新建一个html页面进行测试使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>权限测试</title>
</head>
<body>
<h2>测试用网页</h2>
<button>测试事件</button>
<button id="pickerPhoto">相册服务</button>
<button id="pickerCamera">拍照服务</button>
<button id="vibrate">马达震动</button>
<img width="200" />
<p></p>
<script>
//逻辑...
</script>
</body>
</html>
用浏览器打开这个html效果演示:
注入JavaScript对象到window对象中
aboutToAppear(): void {
//其他略...
//xx.xx.xx.xx 换成你的本地网络IP地址
拍照和传感器测试需要用真机,且手机和电脑要连接同一个wifi才可以
this.src = 'http://xx.xx.xx.xx:5500/day01/test.html'
}
- vscode 通过 open with liveserver 运行
- 终端输入 ipconfig 查看 ip 地址
在src替换掉原来的ip地址
然后把代码再虚拟机或者真机运行起来,得到如图效果
在 Chrome 浏览器中使用什么进行调试?
- 控制面板,开发者工具,F12
在 手机端 浏览器使用什么进行调试呢?
- Eruda 手机调试面板工具, 第三方库:github.com/liriliri/er… 代码实现
<!-- 引入调试控制台 -->
<script src="https://cdn.bootcdn.net/ajax/libs/eruda/3.3.0/eruda.min.js"></script>
<script>eruda.init();</script>
这个是在线的链接如果觉得加载太慢就把js文件建个文件夹放在前端框架里
h5-调试工具插件
3.弹窗测试
先来个简单的弹窗简单测试一下 在 DevEco Studio 里写:初始结构代码:
import { promptAction } from '@kit.ArkUI'
import { emitter } from '@kit.BasicServicesKit'
import { auth, cameraPlugin, EmitterKey, locationPlugin, MkUser, photoPlugin, sensorPlugin } from 'basic'
import { webview } from '@kit.ArkWeb'
import { AreaColumns } from 'basic/src/main/ets/utils/LocationPlugin'
@Builder
function WebViewBuilder() {
WebView()
}
@Component
struct WebView {
src: ResourceStr = ''
@Consume
pageStack: NavPathStack
// 当前网页的标题
@State title: string = '默认标题'
// 从本地存储中获取顶部安全距离
@StorageProp('safeTop') safeTop: number = 0
// 网页是否在加载中
@State loading: boolean = true
// 网页加载的进度
@State progress: number = 0
// 页面视图控制器
controller = new webview.WebviewController()
aboutToAppear(): void {
const params = this.pageStack.getParamByName('WebView')[0] as string
promptAction.showToast({
message: params
})
//this.src = 'http://192.168.34.111:5174/'
//this.src = ' http://192.168.34.111:5500/day01/test.html'
//this.src = 'http://192.168.254.12:5174/'
this.src = 'http://192.168.254.12:5500/day01/test.html'
}
/**
* 回到web容器的上一个页面
*/
webBack() {
if (this.controller.accessBackward()) {
this.controller.backward()
} else {
this.pageStack.pop()
}
}
/**
* 回到上一个页面
*/
webClose() {
this.pageStack.pop()
}
toIndex(index: number) {
this.pageStack.clear()
emitter.emit(EmitterKey.CHANGE_TAB, {
data: {
index
}
})
}
@Builder
MenuBuilder() {
Menu() {
MenuItem({ content: '首页' })
.onClick(() => {
this.toIndex(0)
})
MenuItem({ content: '分类' })
.onClick(() => {
this.toIndex(1)
})
MenuItem({ content: '购物袋' })
.onClick(() => {
this.toIndex(2)
})
MenuItem({ content: '我的' })
.onClick(() => {
this.toIndex(3)
})
MenuItem({ content: '刷新一下' })
.onClick(() => {
this.controller.refresh() // 刷新
})
}
.width(100)
.fontColor($r('[basic].color.text'))
.font({ size: 14 })
.radius(4)
}
build() {
NavDestination() {
Column() {
// 使用网页容器组件
// 导航条
Row() {
Row() {
// 返回(网页)
Image($r("[basic].media.ic_public_left"))
.iconStyle()
.onClick(() => {
this.webBack()
})
// 关闭(鸿蒙页面)
Image($r('[basic].media.ic_public_close'))
.iconStyle()
.onClick(() => {
this.webClose()
})
}
.width(100)
Text(this.title)
.fontSize(16)
.fontWeight(500)
.fontColor($r('[basic].color.black'))
.layoutWeight(1)
.maxLines(1)
.textAlign(TextAlign.Center)
.textOverflow({ overflow: TextOverflow.MARQUEE })
Row() {
Blank()
Image($r('[basic].media.ic_public_more'))
.iconStyle()
.bindMenu(this.MenuBuilder)
}
.width(100)
}
.height(50 + this.safeTop)
.backgroundColor($r('[basic].color.white'))
.padding({ top: this.safeTop })
// 后续放置 进度条+页面
// 堆叠组件
Stack({ alignContent: Alignment.Top }) {
// 如果加载中, 则显示进度条插件
if (this.loading) {
Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
.style({ strokeWidth: 2, enableSmoothEffect: true })
.color($r('[basic].color.red'))
.zIndex(1)
}
// web组件: 用于加载在线网页
Web({ src: this.src, controller: this.controller })
.onProgressChange((data) => { // 网页加载进度变化时触发该回调
// 1. 进度条
console.log('mk-logger', JSON.stringify(data)) // 新的加载进度,取值范围为0到100的整数
if (data) {
// 1.1 记录加载进度
this.progress = data.newProgress
// 1.2 如果加载进度完成
if (data.newProgress === 100) {
// 1.3 动画让进度条消失
animateTo({ duration: 300, delay: 300 }, () => {
this.loading = false
})
}
}
})
.onAppear(() => {
// 组件挂载显示时触发此回调
this.controller.registerJavaScriptProxy({
// 参与注册的应用侧JavaScript对象。
// 注册对象的名称,与window中调用的对象名一致。
// 注册后window对象可以通过此名字访问应用侧JavaScript对象。
// ...
// 测试代码
//参数一,鸿蒙提供的方法源码
sayHi: (info: object) => {
AlertDialog.show({
message: JSON.stringify(info)
.slice(0, 800)
})
},
},
//参数2 注入 window 对象全局变量 (名字自己命名)
'mk', [
// 参与注册的应用侧JavaScript对象的方法。
// ...
// 测试代码
//参数3
'sayHi',
])
})
.onPageBegin(() => { // 开始加载网页时触发
this.progress = 0
this.loading = true
console.log('mk-logger', 'onPageBegin')
})
.onPageEnd(() => { // 网页加载完成时触发
console.log('mk-logger', 'onPageEnd')
})// 网页document标题更改时触发该回调
.onTitleReceive((data) => {
console.log('mk-logger', 'onTitleReceive')
this.title = data?.title || ''
})
}
.width('100%')
.layoutWeight(1)
}
}
.hideTitleBar(true)
}
}
@Extend(Image)
function iconStyle() {
.width(24)
.aspectRatio(1)
.fillColor($r('[basic].color.text'))
.margin(13)
}
关键代码
.onAppear(() => {
// 组件挂载显示时触发此回调
this.controller.registerJavaScriptProxy({
//参数一,鸿蒙提供的方法源码
sayHi: (info: object) => {
AlertDialog.show({
message: JSON.stringify(info)
.slice(0, 800)
})
},
},
//参数2 注入 window 对象全局变量
'mk', [
//参数3
'sayHi',
])
})
在test.html里进行使用 关键代码
<script>
document.querySelector('button').addEventListener('click', async () => {
mk.sayHi('你好,可以弹窗吗?')
})
</script>
弹窗使用的是鸿蒙组件的原生能力,现在看一下在web里是否可以显示出来?
4.整合相册服务 mk.pickerPhoto
权限调用这些都需要调用一些工具类 这里提供一个PhotoPlugin.ets
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';
class PhotoPlugin {
async pickerPhoto(){
// 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)
console.log('mk-logger', 'photoPlugin', JSON.stringify(res))
// 2. 文件操作
// 2.1 获取照片的uri地址
const uri = res.photoUris[0]
// 2.2 根据uri同步打开文件
const file = fs.openSync(uri)
// 2.3 同步获取文件的详细信息
const stat = fs.statSync(file.fd)
// 2.4 创建缓冲区存储读取的文件流
const buffer = new ArrayBuffer(stat.size)
// 2.5 开始同步读取文件流到缓冲区
fs.readSync(file.fd, buffer)
// 2.6 关闭文件流
fs.closeSync(file)
// 3. 转成base64编码的字符串
const helper = new util.Base64Helper()
const str = helper.encodeToStringSync(new Uint8Array(buffer))
console.log('mk-logger', 'photoPlugin-str', str)
return str
}
}
export const photoPlugin = new PhotoPlugin()
放在你的项目工具类里边,使用前记得导出 在WebView里进行使用 关键代码
.onAppear(() => {
// 组件挂载显示时触发此回调
this.controller.registerJavaScriptProxy({
//参数一,鸿蒙提供的方法源码
sayHi: (info: object) => {
AlertDialog.show({
message: JSON.stringify(info)
.slice(0, 800)
})
},
},
//参数2 注入 window 对象全局变量
'mk', [
//参数3
'sayHi',//弹窗
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
])
})
关键代码
<script>
document.querySelector('button').addEventListener('click', async () => {
mk.sayHi('你好,可以弹窗吗?')
})
// 相册服务 mk.pickerPhoto
const pickerPhoto = document.querySelector("#pickerPhoto");
pickerPhoto.addEventListener("click", async () => {
const str = await mk.pickerPhoto();
//显示图片
document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
</script>
ps: img 标签使用 base64 显示图片。
- data:image/gif;base64,base64编码的gif图片数据
- data:image/png;base64,base64编码的png图片数据
- data:image/jpeg;base64,base64编码的jpeg图片数据
效果演示: 虚拟机没有图片可以用截屏功能截一张图片使用
5.拍照服务 mk.pickerCamera
需要通过真机测试,(真机和电脑需连接同一个 wifi)
- 真机和电脑连接 wifi
- 获取电脑连接 wifi 后的 ip 地址,替换掉原来的 ip 地址
- 电脑连接真机,编译到真机调试即可 这里提供一个拍照工具类创建并导出
import { camera, cameraPicker } from '@kit.CameraKit';
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';
class CameraPlugin {
async pickerCamera(){
// 1. 打开相机后置摄像头得到拍照结果集
const pickerProfile: cameraPicker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
const pickerResult: cameraPicker.PickerResult = await cameraPicker.pick(getContext(),
[cameraPicker.PickerMediaType.PHOTO], pickerProfile);
// 2. 根据结果集的URI属性同步打开文件
const file = fs.openSync(pickerResult.resultUri)
// 3. 同步读取文件的详情信息
const stat = fs.statSync(file.fd)
// 4. 定义缓冲区用于保存读取的文件
const buffer = new ArrayBuffer(stat.size)
// 5. 开始同步读取内容到缓冲区
fs.readSync(file.fd, buffer)
// 6. 读取完毕后关闭文件流
fs.closeSync(file)
// 7. 借助util工具方法把读取的文件流转成base64编码的字符串
const helper = new util.Base64Helper()
const str = helper.encodeToStringSync(new Uint8Array(buffer))
console.log('mk-logger', 'pickerCamera', str)
return str
}
}
export const cameraPlugin = new CameraPlugin()
放在你的项目工具类里边,使用前记得导出 在WebView里进行使用 关键代码
.onAppear(() => {
// 组件挂载显示时触发此回调
this.controller.registerJavaScriptProxy({
//参数一,鸿蒙提供的方法源码
sayHi: (info: object) => {
AlertDialog.show({
message: JSON.stringify(info)
.slice(0, 800)
})
},
},
//参数2 注入 window 对象全局变量
'mk', [
//参数3
'sayHi',//弹窗
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机拍照
])
})
关键代码
<script>
document.querySelector('button').addEventListener('click', async () => {
mk.sayHi('你好,可以弹窗吗?')
})
// 相册服务 mk.pickerPhoto
const pickerPhoto = document.querySelector("#pickerPhoto");
pickerPhoto.addEventListener("click", async () => {
const str = await mk.pickerPhoto();
//显示图片
document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
/ 拍照服务 mk.pickerCamera
const pickerCamera = document.querySelector("#pickerCamera");
pickerCamera.addEventListener("click", async () => {
const str = await mk.pickerCamera();
console.log(str);
document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
});
</script>
拍照功能必须使用真机才能测试 投屏演示
6.传感器 mk.vibrator
创建并导出工具类SensorPlugin.ets
import { vibrator } from '@kit.SensorServiceKit'
class SensorPlugin {
vibrator() {
vibrator.startVibration({ type: 'time', duration: 50 }, { usage: 'touch' }) //duration: 50 震动时间(毫秒)
}
}
export const sensorPlugin = new SensorPlugin()
在WebView里进行使用 关键代码
.onAppear(() => {
// 组件挂载显示时触发此回调
this.controller.registerJavaScriptProxy({
//参数一,鸿蒙提供的方法源码
sayHi: (info: object) => {
AlertDialog.show({
message: JSON.stringify(info)
.slice(0, 800)
})
},
},
//参数2 注入 window 对象全局变量
'mk', [
//参数3
'sayHi',//弹窗
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(),//相册服务
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机拍照
vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器
])
})
关键代码
<script>
document.querySelector('button').addEventListener('click', async () => {
mk.sayHi('你好,可以弹窗吗?')
})
// 相册服务 mk.pickerPhoto
const pickerPhoto = document.querySelector("#pickerPhoto");
pickerPhoto.addEventListener("click", async () => {
const str = await mk.pickerPhoto();
//显示图片
document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
/ 拍照服务 mk.pickerCamera
const pickerCamera = document.querySelector("#pickerCamera");
pickerCamera.addEventListener("click", async () => {
const str = await mk.pickerCamera();
console.log(str);
document.querySelector("img").src = `data:image/jpeg;base64,${str}`;
});
// 传感器 马达震动 mk.vibrate
const vibrate = document.querySelector("#vibrate");
vibrate.addEventListener("click", async () => {
mk.vibrator();
});
</script>
另外震动还需要获取传感器权限: module.json5里进行配置
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.VIBRATE"
}
],
"name": "basic",
"type": "shared",
"description": "$string:shared_desc",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"pages": "$profile:main_pages"
}
}
震动效果展示不了,可以用真机去测试一下
7.结语
在鸿蒙开发中,探索将 Web 页面与原生鸿蒙能力相结合的过程充满挑战与惊喜。通过注册 JavaScript 代理、实现弹窗测试、整合相册服务、拍照服务以及调用传感器,我们逐步拓展了应用的功能边界,为用户带来更加丰富和个性化的体验。
尽管在这个过程中可能会遇到各种技术难题,如权限配置、真机调试的复杂性等,但每一次的突破都让我们更加深入地理解了鸿蒙生态的强大潜力。随着技术的不断进步和鸿蒙开发社区的日益壮大,相信未来我们能够在 Web 页面与原生能力融合的道路上创造出更多令人瞩目的应用场景,为用户提供更加便捷、高效和创新的交互体验。
让我们继续在鸿蒙开发的征程上砥砺前行,不断探索新的可能性,为打造更加出色的鸿蒙应用而努力。