什么是混合开发?
混合开发是一种融合了原生开发和Web开发优势的移动应用开发方式。
具体来说,混合开发通常指的是利用一种框架或平台来创建应用程序,这种程序结合了原生应用的一些功能和特性(比如访问设备的摄像头、相册、GPS、蓝牙等),并且使用Web技术(HTML5、CSS和JavaScript)来编写大部分的应用代码。
1. Web容器
1.1克隆模板代码
模板仓库地址: https://gitee.com/yjh8866/meikou_mall_hybrid
1.2 创建Web容器组件
commons\basic\src\main\ets\components
MkWeb.ets
import { router } from '@kit.ArkUI'
@Component
export struct MKWeb {
src: ResourceStr = '' // 加载的页面地址
build() {
Column(){
Text('我是用于放web页面')
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.under'))
}
}
Index.ets
export { MKWeb } from './src/main/ets/components/MkWeb'
WebPage.ets
import { router } from '@kit.ArkUI'
import { MKWeb } from '@mk/basic/Index'
@Entry
@Component
struct WebPage {
src: ResourceStr = ''
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>
// 1. 生产使用
// this.src = 'resource://rawfile/index.html#' + params['src']
// 2. 测试使用
this.src = 'http://192.168.56.1:5173/#' + params['src']
}
build() {
Column() {
// 使用网页容器组件
MKWeb({ src: this.src })
}
}
}
1.3创建页面导航栏
导航条: 宽度->100%, 高度->50+安全高度, 背景->白色, 上内边距: 安全高度
图标: 宽高->24, 外边距->13, 填充颜色 -> 文字颜色
文字: 字体大小->16, 加粗->500, 颜色->黑色, 居中, 最大行数1(超出则滚动)
右侧文字绑定一个下拉菜单
import { router } from '@kit.ArkUI'
@Component
export struct MKWeb {
// 当前网页的标题
@Prop title: string = '美寇商城'
// 从本地存储中获取顶部安全距离
@StorageProp('safeTop') safeTop: number = 0
/**
* 回到web容器的上一个页面
*/
webBack(){}
/**
* 回到上一个页面
*/
webClose(){
}
@Builder
MenuBuilder() {
Menu() {
MenuItem({ content: '首页' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 0 } }))
MenuItem({ content: '分类' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 1 } }))
MenuItem({ content: '购物袋' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 2 } }))
MenuItem({ content: '我的' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 3 } }))
MenuItem({ content: '刷新一下' })
.onClick(() => {})
}
.width(100)
.fontColor($r('app.color.text'))
.font({ size: 14 })
.radius(4)
}
build() {
Column(){
// 导航条
Row() {
Row() {
Image($r("app.media.ic_public_left"))
.iconStyle()
.onClick(() => {
this.webBack()
})
Image($r('app.media.ic_public_close'))
.iconStyle()
.onClick(() => {
this.webClose()
})
}
.width(100)
Text(this.title)
.fontSize(16)
.fontWeight(500)
.fontColor($r('app.color.black'))
.layoutWeight(1)
.maxLines(1)
.textAlign(TextAlign.Center)
.textOverflow({ overflow: TextOverflow.MARQUEE })
Row() {
Blank()
Image($r('app.media.ic_public_more'))
.iconStyle()
.bindMenu(this.MenuBuilder)
}
.width(100)
}
.height(50 + this.safeTop)
.backgroundColor($r('app.color.white'))
.padding({ top: this.safeTop })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.under'))
}
}
@Extend(Image)
function iconStyle() {
.width(24)
.aspectRatio(1)
.fillColor($r('app.color.text'))
.margin(13)
}
1.4设置加载进度条
堆叠容器: 宽度->100%, 占满剩余空间, 顶部对齐
进度条: 线性/当前值/总值, 填充宽度 -> 2, 开启进度平滑动效, 颜色-> 红色, 层级->上,
// 网页是否在加载中
@State loading: boolean = true
// 网页加载的进度
@State progress: number = 0
// ....
// 堆叠组件
Stack({ alignContent: Alignment.Top }){
// 如果加载中, 则显示进度条插件
if (this.loading) {
Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
.style({ strokeWidth: 2, enableSmoothEffect: true })
.color($r('app.color.red'))
.zIndex(1)
}
}
.width('100%')
.layoutWeight(1)
1.5 设置Web组件加载网页
- 配置文本容器组件
src: ResourceStr = 'https://m.suning.com' // 加载的页面地址
// ...
// 网页加载的进度
@State progress: number = 0
// 页面视图控制器
controller = new webview.WebviewController()
// ...
// 堆叠组件
Stack({ alignContent: Alignment.Top }){
// 如果加载中, 则显示进度条插件
if (this.loading) {
Progress({ type: ProgressType.Linear, value: this.progress, total: 100 })
.style({ strokeWidth: 2, enableSmoothEffect: true })
.color($r('app.color.red'))
.zIndex(1)
}
// web组件: 用于加载在线网页
Web({ src: this.src, controller: this.controller })
}
.width('100%')
.layoutWeight(1)
- 设置进度条 显示/隐藏 和 加载进度
onPageBegin: 开始加载网页时触发
onPageEnd: 网页加载完成时触发
onProgressChange: 网页加载进度变化时触发该回调
// web组件: 用于加载在线网页
Web({ src: this.src, controller: this.controller })
.onProgressChange((data) => { // 网页加载进度变化时触发该回调
// 1. 进度条
console.log('mk-logger', JSON.stringify(data)) // 新的加载进度,取值范围为0到100的整数
if (data) {
// 1.1 记录加载进度
this.progress = data.newProgress
// 1.2 如果加载进度完成
if (data.newProgress === 100) {
// 1.3 动画让进度条消失
animateTo({ duration: 300, delay: 300 }, () => {
this.loading = false
})
}
}
})
.onPageBegin(() => { // 开始加载网页时触发
this.progress = 0
this.loading = true
console.log('mk-logger', 'onPageBegin')
})
.onPageEnd(() => { // 网页加载完成时触发
console.log('mk-logger', 'onPageEnd')
})
1.6 导航栏相关操作处理
- 导航左侧处理
返回处理: 在webview中, 如果当前页面之前有页面则控制器内返回, 如果没有则原生侧路由返回
关闭处理: 原生侧路由返回
// ...
// 加载网页页面完成时触发该回调,用于应用更新其访问的历史链接
.onRefreshAccessedHistory(() => {
const history = this.controller.getBackForwardEntries() // 获取当前Webview的历史信息列表
this.historyCurrIndex = history.currentIndex // 当前在页面历史列表中的索引
this.historySize = history.size // 历史列表中索引的数量,最多保存50条,超过时起始记录会被覆盖
// AlertDialog.show({message: this.historyCurrIndex + '-' + this.historySize })
})
webBack() {
// 如果在web容器中, 当前页面之前还有页面, 则容器内返回上一页
if (this.historyCurrIndex > 0) {
this.controller.backward()
} else {
router.back()
}
}
webClose() {
router.back()
}
- 导航下拉菜单-刷新
@Builder
MenuBuilder() {
Menu() {
MenuItem({ content: '首页' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 0 } }))
MenuItem({ content: '分类' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 1 } }))
MenuItem({ content: '购物袋' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 2 } }))
MenuItem({ content: '我的' })
.onClick(() => router.back({ url: 'pages/Index', params: { index: 3 } }))
MenuItem({ content: '刷新一下' })
.onClick(() => this.controller.refresh())
}
.width(100)
.fontColor($r('app.color.text'))
.font({ size: 14 })
.radius(4)
}
- 导航条标题设置
// 网页document标题更改时触发该回调
.onTitleReceive((data) => {
console.log('mk-logger', 'onTitleReceive')
this.title = data?.title || ''
})
2. 应用侧-JSBridge
通过分析, 在加载的网页中, 需要调用原生能力, 主要包括:
- 认证信息
● 查询用户信息
● 更新用户信息
● 移除用户 - 拍照服务/相册服务
- 传感器
- 本地省市区数据
2.1 注册JavaScript代理
实现语法: registerJavaScriptProxy(object: object, name: string, methodList: Array): void
webInit() {
this.controller.registerJavaScriptProxy({
// 参与注册的应用侧JavaScript对象。
// 注册对象的名称,与window中调用的对象名一致。
// 注册后window对象可以通过此名字访问应用侧JavaScript对象。
// ...
}, 'mk', [
// 参与注册的应用侧JavaScript对象的方法。
// ...
])
}
// ...
Web({ src: this.src, controller: this.controller })
.onAppear(() => { // 组件挂载显示时触发此回调
this.webInit()
})
2.2 认证信息 mk.queryUser mk.removeUser mk.updateUser
import { auth, MkUser } from '../utils/auth'
webInit() {
this.controller.registerJavaScriptProxy({
queryUser: (): MkUser => auth.queryUser(), // 查询用户
removeUser: (): void => auth.removeUser(), // 移除用户
updateUser: (user: MkUser): void => auth.updateUser(user), // 更新用户
}, 'mk', [
'queryUser',
'updateUser',
'removeUser',
])
}
2.3 拍照服务 mk.pickerCamera
- 打开相机后置摄像头得到拍照结果集
- 根据结果集的URI属性同步打开文件
- 以同步方法获取文件详细属性信息
- 定义缓冲区用于保存读取的文件
- 开始同步读取内容到缓冲区
- 读取完毕后关闭文件流
- 借助util工具方法把读取的文件流转成base64编码的字符串
CameraPlugin.ets
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()
import { cameraPlugin } from '../plugins/CameraPlugin'
webInit() {
this.controller.registerJavaScriptProxy({
queryUser: (): MkUser => auth.queryUser(), // 查询用户
removeUser: (): void => auth.removeUser(), // 移除用户
updateUser: (user: MkUser): void => auth.updateUser(user), // 更新用户
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
}, 'mk', [
'queryUser',
'updateUser',
'removeUser',
'pickerCamera',
])
}
2.4 相册服务 mk.pickerPhoto
import { picker } from '@kit.CoreFileKit'
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';
class PhotoPlugin {
async pickerPhoto(){
// 1. 打开相册选择图片
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 5;
let photoPicker = new photoAccessHelper.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()
import { photoPlugin } from '../plugins/PhotoPlugin'
webInit() {
this.controller.registerJavaScriptProxy({
queryUser: (): MkUser => auth.queryUser(), // 查询用户
removeUser: (): void => auth.removeUser(), // 移除用户
updateUser: (user: MkUser): void => auth.updateUser(user), // 更新用户
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(), // 调用相册
}, 'mk', [
'queryUser',
'updateUser',
'removeUser',
'pickerCamera',
])
}
2.5 传感器 mk.vibrator
SensorPlugin.ets
import { vibrator } from '@kit.SensorServiceKit'
class SensorPlugin {
vibrator() {
vibrator.startVibration({ type: 'time', duration: 50 }, { usage: 'touch' })
}
}
export const sensorPlugin = new SensorPlugin()
// ...
import { sensorPlugin } from '../plugins/SensorPlugin'
webInit() {
this.controller.registerJavaScriptProxy({
queryUser: (): MkUser => auth.queryUser(), // 查询用户
removeUser: (): void => auth.removeUser(), // 移除用户
updateUser: (user: MkUser): void => auth.updateUser(user), // 更新用户
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(), // 调用相册
vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器
}, 'mk', [
'queryUser',
'updateUser',
'removeUser',
'pickerCamera',
])
}
2.6 本地数据 mk.getAreaColumns
- 定义读取的本地数据的数据类型(AreaDataItem)
- 定义输出数据的数据类型(AreaColumns)
- 读取rawfile目录下的本地文件area.json
- 将读取的字节数组转码成字符串
- 将读取的Json字符串转成对象数据
- 遍历对象数据并处理返回
util工具函数
LocationPlugin.ets
import { util } from '@kit.ArkTS'
// 1. 定义读取的本地数据的数据类型(AreaDataItem)
export interface AreaDataItem {
code: string
name: string
areaList: AreaDataItem[]
}
// 2. 定义输出数据的数据类型(AreaColumns)
export interface AreaColumns {
province_list: Record<number, string>
city_list: Record<number, string>
county_list: Record<number, string>
}
class LocationPlugin {
async getAreaColumns(){
// 1. 定义对象用于存储转换后的数据
const areaColumns: AreaColumns = {
province_list: {},
city_list: {},
county_list: {}
}
try {
// 2. 读取rawfile目录下的本地文件
const unit8Array = getContext().resourceManager.getRawFileContentSync('area.json')
// 3. 将读取的字节数组转成字符串
const decoder = new util.TextDecoder()
const resStr = decoder.decodeWithStream(unit8Array)
// 4. 将读取的Json字符串转成对象数组
const areaData = JSON.parse(resStr) as AreaDataItem[]
// 5. 遍历处理数据
// 5.1 省转换
areaData.forEach((province)=>{
areaColumns.province_list[Number(province.code)] = province.name
// 5.2 市转换
province.areaList.forEach((city)=>{
areaColumns.city_list[Number(city.code)] = province.name
// 5.3 区转换
city.areaList.forEach((county)=>{
areaColumns.county_list[Number(county.code)] = county.name
})
})
})
// 6. 返回数据
return areaColumns
} catch (e) {
return areaColumns
}
}
}
export const locationPlugin = new LocationPlugin()
// ...
import { AreaColumns, locationPlugin } from '../plugins/LocationPlugin'
webInit() {
this.controller.registerJavaScriptProxy({
queryUser: (): MkUser => auth.queryUser(), // 查询用户
removeUser: (): void => auth.removeUser(), // 移除用户
updateUser: (user: MkUser): void => auth.updateUser(user), // 更新用户
pickerCamera: (): Promise<string> => cameraPlugin.pickerCamera(), // 调用相机
pickerPhoto: (): Promise<string> => photoPlugin.pickerPhoto(), // 调用相册
vibrator: (): void => sensorPlugin.vibrator(), // 调用传感器
getAreaColumns: (): Promise<AreaColumns> => locationPlugin.getAreaColumns(), // 获取省市区
}, 'mk', [
'queryUser',
'updateUser',
'removeUser',
'pickerCamera',
])
}