在本文开始之前,建议先把官方开发文档对于这部分内容了解一下,地址:developer.huawei.com/consumer/cn…
备注:在看文档的时候,一定要注意是 HarmonyOS 还是 OpenHarmony。因为两者使用的 SDK 是不同的。
这里不对此展开详细的说明,只对 HAR 和 HSP 说下自己的理解,相比较于 HAP,他们都不能单独运行到设备上。他们两个的区别主要有两方面,一个是 HAR 不支持声明 Pages 页面,另一个是 HSP 的发布需要确保版本号和宿主程序保持一致。
在日常使用场景中,我们要依据实际业务来创建使用 HAR 还是 HSP,对于单 HAP 来说,两者在选择上其实都可以,但对于多 HAP 的业务,因为在打包时,HAR 会被编译打包到所有依赖该文件的 HAP 和 HSP 包当中,这就需要平衡两者之间的选择。
针对第一个区别,我们不能够通过 pushUrl 的方式进行页面跳转,但鸿蒙还提供了另外的方式,通过路由命名跳转到指定页面。比如下面我们命名当前页面路由为 myPage
那么我们就可以通过下面的方式进行跳转:
router.pushNamedRoute({ name: 'myPage' })
如果在跳转的时候把当前组件卸载,我们可以用
router.replaceNamedRoute({ name: 'myPage' })
在跳转的同时我们可以进行数据传递:
router.pushNamedRoute({
name: 'myPage',
params: {
data1: 'message',
data2: {
data3: [123, 456, 789]
}
}
})
在 myPage 页面进行数据获取:
router.getParams();
上面所说这些,主要是针对公司内部自有业务开发场景来说的,如果你现在封装了一个功能,希望提供给所有可能使用到该三方库的开发者,应该选择哪种方式呢?上面所说的第二点区别其实已经给出了答案,最优解是选择 HAR 包,同时 HAR 包可以发布到 OHPM 私仓或者中心仓供其他应用使用,在发布到中心仓时最好开启代码混淆,以此来保证代码安全。
在 HAR 模块的 build-profile.json5 文件中的 ruleOptions 字段下的 enable 进行设置,配置如下所示:
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": [
"./obfuscation-rules.txt"
]
},
"consumerFiles": [
"./consumer-rules.txt"
]
}
},
},
]
这样在构建 HAR 的时候,就会对代码进行编译、混淆及压缩处理。
针对以上对于鸿蒙程序包的理解,假设现在要做一款三方 SDK,并不是说 Toast 弹窗、网络框架一类,而是说提供带有 UI 交互的形式,其实现方式可以是通过自定义组件嵌入到宿主程序,也可以是自定义弹窗或者是通过子窗口。下面先来介绍下子窗口的开发示例:
对于窗口的简单属性设置,一般我们会在 onWindowStageCreate 方法中获取主窗口然后设置相关属性,比如下面代码设置窗口可触状态。
// 获取应用主窗口。
let windowClass: window.Window | null = null;
windowStage.getMainWindow((err: BusinessError, data) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to obtain the main window. Cause: ' + JSON.stringify(err));
return;
}
// 设置主窗口属性。以设置"是否可触"属性为例。
let isTouchable: boolean = true;
windowClass.setWindowTouchable(isTouchable, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to set the window to be touchable. Cause:' + JSON.stringify(err));
return;
}
})
})
备注:对于主窗口的属性设置,我们也可以在 module.json5 配置文件中设置,比如最大窗口宽度 maxWindowWidth 等。
有些时候,我们不一定要在 onWindowStageCreate 中拿到窗口来使用,比如我们可能在某个界面中使用,这个时候我们可以使用 AppStorage。AppStorage 是应用全局的 UI 状态存储,是和应用的进程绑定的,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。
下面我们在 onWindowStageCreate 方法中存储 windowStage 方便之后调用。
windowStage.loadContent("pages/Index", (err: BusinessError) => {
let errCode: number = err.code
if (errCode) {
console.error('loadContent Error: ' + JSON.stringify(err))
return
}
console.log('loadContent Success')
// 存储 windowStage
AppStorage.setOrCreate('windowStage', windowStage)
})
在 Index.ets 中获取:
let windowStage_: window.WindowStage | undefined = undefined
// 获取windowStage
windowStage_ = AppStorage.get('windowStage')
这个时候我们就可以在 Index 界面进行相关的操作,比如创建一个子窗口。一般流程是:创建子窗口 ---> 设置窗口属性 ---> 加载界面 ---> 展示子窗口。示例代码如下:
// 1.创建应用子窗口。
if (windowStage_ == null) {
console.error('Failed to create the subwindow. Cause: windowStage_ is null');
} else {
windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
return;
}
sub_windowClass = data;
console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
// 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in moving the window.');
});
sub_windowClass.resize(500, 500, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in changing the window size.');
});
// 3.为子窗口加载对应的目标页面。
sub_windowClass.setUIContent("pages/Index2", (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
// 3.显示子窗口。
(sub_windowClass as window.Window).showWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in showing the window.');
});
});
})
}
这里需要注意一点,setUIContent 加载的界面必须要在 main_pages 中进行配置。
问题来了,HAR 包不允许界面配置,那么如果是在 HAR 包中加载界面应该怎么实现呢?这里我们可以使用命名路由的形式,然后通过 loadContentByName 来加载。示例代码如下:
// MainPage.ets
export const myMainPage : string = 'myMainPage';
@Entry({ routeName: myMainPage })
// 加载
import * as MainPage from './components/MainPage'; // 导入命名路由页面
(sub_windowClass as window.Window).loadContentByName(MainPage.myMainPage, (err: BusinessError) => {}
这样我们就成功实现了在 HAR 包中加载界面的需求。
当需要销毁子窗口的时候,通过 destroyWindow 方法来实现:
// 销毁子窗口
(sub_windowClass as window.Window).destroyWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
关于子窗口的生命周期,是跟随主窗口的。使用场景一般是实现弹窗、悬浮球等。
另外在实践中发现,除了路由跳转页面的形式从一个页面到另一个页面,也可以通过窗口加载界面的形式来实现。比如现在主窗口展示的是界面 Index,有另外一个 Index2 界面,我们如果在 Index 界面有个事件触发机制需要直接把 Index2 作为当前窗口的界面,可以使用下面示例代码:
// 加载 Index2
let windowStage: window.WindowStage | undefined = AppStorage.get('windowStage')
windowStage?.loadContent('pages/Index2', (err: BusinessError) => {
let errCode: number = err.code
if (errCode) {
console.error('loadContent Error: ' + JSON.stringify(err))
return
}
console.log('loadContent Success')
})
我们通过打印日志可以发现,此时 Index 界面是销毁的。
以上就是对于窗口的一些开发实战。再结合前面所述程序包,我们就可以实现三方带有 UI 交互的 SDK 的研发需求了。