零、什么是动态导入
动态导入并不是鸿蒙系统特有的,而是 TypeScript 语言具有的特性。但是鸿蒙系统增加了一些特有规则,因为鸿蒙系统存在 HAR、HSP、HAP 模块,所以动态导入会有一些系统上的约束。这篇文章会详细的分享鸿蒙系统中 "怎么使用动态导入" 和 "为什么要使用动态导入" 。
进入正题前,先解决一个问题:什么是动态导入?
既然有动态导入,很正常就会联想到静态导入。日常中,在代码文件开头使用 import 导入其他代码的方式就是静态导入 ,例如以下代码导入一个 User 类用于承载用户信息。
typescript
import { User } from "./bean/Bean"
const user = new User()
相比之下,动态导入 是在需要使用时才进行 import,通过传入文件路径来加载对应模块,以下代码实现了与上面相同的逻辑:
typescript
const module = await import("./bean/Bean")
const user = new module.User()
这篇文章并不是讲解黑科技在鸿蒙上的使用,只是分享日常工作中 "动态导入" 的使用场景,是完全遵循着官方文档进行操作。
一、动态导入有什么作用
既然静态导入可以把代码文件导入,达到 A 代码文件可以使用 B 代码文件中定义的类、方法、常量等,为什么还需要动态导入呢?
一个东西的产生肯定是为了解决某些问题,它可以解决以下的问题:
- 解决 HAR 循环依赖
- 解决 ts 无法依赖 ets
- 解决 Worker 内存隔离
- 让 HAR 加载外部逻辑
- 延时加载模块
- ......
接下来便一一分享每个问题是怎么处理的。
二、解决 HAR 循环依赖
在较为复杂的项目中,一般都会对业务代码进行拆解放到不同的模块中,只要拆分就会有不同层级的模块依赖,高层模块对低层模块的依赖是单向的,则不会有问题。但是同层级的模块如果要相互调用,此时就会有模块的循环依赖问题,而动态导入可以解决这一问题。

上图 HarA 和 HarB 是同层级的两个 HAR 模块,被上层模块 HAP/HSP 依赖。 HarA 和 HarB 有着下面各自的业务逻辑:
HarA 中的 HarALogic.ets,提供了一个 sayHello 方法:
typescript
class HarALogic {
sayHello(name: string): string {
Log.i("HarALogic", `Hello, ${name}!`)
return `HarALogic 已经执行完 Hello, ${name}!`
}
}
export const harALogic = new HarALogic()
HarB 中的 HarBLogic.ets,提供了一个 sayHi 方法:
typescript
class HarBLogic {
sayHi(name: string): string {
Log.i("HarBLogic", `Hi, ${name}!`)
return `HarBLogic 已经执行完 Hi, ${name}!`
}
}
export const harBLogic = new HarBLogic()
现在有一个场景:HarA 模块需要调用 HarBLogic 的逻辑,HarB 模块需要调用 HarALogic 的逻辑。如果静态导入,则两个模块需要互相依赖,然后在需要使用的地方导入文件,但这会导致循环依赖的编译错误,如下图所示。

所以需要改为动态导入,但在进行代码编写之前,需要如下配置:
1、HAP 的 build-profile.json5 配置
动态导入 HAR 模块时,需要在 HAP 的 build-profile.json5 中配置 runtimeOnly.packages,声明哪些 HAR 包会被动态导入:
json
{
"apiType": "stageMode",
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"packages": [
"hara",
"harb"
]
}
}
}
}
同时需要移除 HarA 和 HarB 在 oh-package.json5 中对彼此的静态依赖配置。
如果不配置
runtimeOnly.packages,编译不会有任何的错误提示,直到运行动态导入才会报找不到模块。
2、模块导出逻辑
typescript
// HarA 模块的 Index.ets
export { harALogic } from './src/main/ets/HarALogic'
// HarB 模块的 Index.ets
export { harBLogic } from './src/main/ets/HarBLogic'
3、代码调用
typescript
// HarA 调用 HarB
class HarACallHarBLogic {
async call(): Promise<string> {
const harName = "harb"
const ns: ESObject = await import(harName)
return ns.harBLogic.sayHi("江澎涌")
}
}
// 调用上面的代码会输出:
// 54367 A01000/om.jian...ort/HarBLogic I Hi, 江澎涌!
// 54367 A01000/om.jian...enceComponent I 【HarA 调用 HarB】msg=HarBLogic 已经执行完 Hi, 江澎涌!
// HarB 调用 HarA
class HarBCallHarALogic {
async call():Promise<string> {
const harName = "hara"
const ns: ESObject = await import(harName)
return ns.harALogic.sayHello("Jiang Peng Yong")
}
}
// 调用上面的代码会输出:
// 54367 A01000/om.jian...ort/HarALogic I Hello, Jiang Peng Yong!
// 54367 A01000/om.jian...enceComponent I 【HarB 调用 HarA】msg=HarALogic 已经执行完 Hello, Jiang Peng Yong!
三、解决 ts 无法依赖 ets
在鸿蒙开发中,如果你在 .ts 文件中使用 import { xxx } from "./xxx.ets",编译器会直接报错。但是,通过动态导入 HAR 模块名,.ts 可以在运行时访问该 HAR 入口文件(Index.ets)导出的内容,某种程度上也算是一种 "解决 ts 无法依赖 ets" 的方案吧。
接下来演示如何在 ts 文件中导入 HarA 模块的 ets 文件,并使用它。
和平常的 HAR 模块一样,在 HarA 模块的 Index.ets 导出了 harALogic 单例和 User 类,两者均为 ets 文件:
typescript
export { harALogic } from './src/main/ets/HarALogic'
export { User } from './src/main/ets/User'
然后在 HAP 中的 ts 文件,例如示例中的 TsFile.ts,通过动态导入来使用 HarA 的 ets 代码:
typescript
export class TsFile {
async callHarEtsFile() {
const path = "hara"
const ns: ESObject = await import(path)
const msg = ns.harALogic.sayHello(`小朋友`)
Log.i(`TsFile`, `【callEtsFile】msg=${msg}`)
}
// 调用后输出
// 47860 A01000/om.jian...ort/HarALogic I Hello, 小朋友!
// 47860 A01000/om.jian...import/TsFile I 【callEtsFile】msg=HarALogic 已经执行完 Hello, 小朋友!
async createUserInfo() {
const path = "hara"
const ns: ESObject = await import(path)
const user = new ns.User(`小朋友`, 18)
const info = user.info()
Log.i(`TsFile`, `【createUserInfo】info=${info}`)
}
// 调用后输出
// 47860 A01000/om.jian...import/TsFile I 【createUserInfo】info=你好,我是小朋友。我今年18岁。
}
四、解决 Worker 内存隔离
Worker 是鸿蒙中实现多线程的方式,每个 Worker 拥有独立的内存空间。这意味着主线程中 import 加载的模块和创建的实例,不会自动传递到 Worker 中。 所以如果要在 Worker 内部使用某个模块,必须在 Worker 内部自行加载。
动态导入是 Worker 内部按照外部需求加载模块的核心手段:主线程通过消息告诉 Worker 需要加载哪个模块,Worker 内部通过 await import(path) 完成加载。
这里借助 JWorker 库,展示一下 Worker 加载外部逻辑的场景:
JWorker 是一套简单易用的基于鸿蒙 Worker 的双向 RPC 通讯机制。 OHPM 的传送门:ohpm.openharmony.cn/#/cn/detail...
1、先编写 Worker 文件,并通过 JWorker 建立通讯 Channel
typescript
// Worker 文件
import { initJWorker, JWorkerChannel } from 'jworker'
import { EntryServerChannel } from './channel/EntryServerChannel'
const subWorker = initJWorker()
JWorkerChannel("EntryChannel", new EntryServerChannel())
通过 JWorker 的通讯 Channel 接收主 Worker 发送的信息,进行动态加载模块和调用逻辑:
importUserBean方法是动态导入一个类并创建一个实例,子 Worker 才能持有真正的 User 实例。在主 Worker 创建的实例,子 Worker 因为内存隔离是无法获取的。importIdCreator方法是动态导入一个单例,同样该单例和主 Worker 的单例也是内存隔离的。
typescript
// ========== EntryServerChannel ==========
export class EntryServerChannel extends Channel {
async handleMessage(methodName: string, data: Any) {
switch (methodName) {
case "importUserBean": {
try {
const loadInfo = data as LoadInfo
const module: Any = await import(loadInfo.path)
const user = new module["User"]()
Log.i(TAG, `【importUserBean】user=${JSON.stringify(user)} info=${user.info()}`)
} catch (e) {
Log.e(TAG, `【importUserBean】e=${e}`)
} finally {
break
}
}
case "importIdCreator": {
try {
const loadInfo = data as LoadInfo
const module: Any = await import(loadInfo.path)
const id = module["idCreator"].obtain()
Log.i(TAG, `【importIdCreator】id=${id}`)
} catch (e) {
Log.e(TAG, `【importIdCreator】e=${e}`)
} finally {
break
}
}
}
}
}
// ========== Bean.ts ==========
export class User {
name: string = "江澎涌"
height: number = 170
weight: number = 122
info(): string {
return `你好,我是${this.name},身高${this.height}厘米,体重${this.weight}斤。`
}
}
// ========== IdCreator.ts ==========
class IdCreator {
private id = 0
obtain(): number {
return this.id++
}
}
export const idCreator = new IdCreator()
2、在主 Worker 编写调用逻辑
typescript
// 创建 Worker 并且构建通讯 Channel
this.jworker = createJWorker(new worker.ThreadWorker("entry/ets/workerDynamicImport/EntryWorker.ets"))
this.entryClientChannel = new EntryClientChannel()
this.jworker.addChannel("EntryChannel", this.entryClientChannel)
// 发送 importUserBean 方法,会让子 Worker 构建 User
this.entryClientChannel.send("importUserBean", { path: "../bean/Bean" } as LoadInfo)
// 会输出以下内容
// 47860 A01000/om.jiang...erverChannel I 【importUserBean】loadInfo={"path":"../bean/Bean"} user={"name":"江澎涌","height":170,"weight":122} info=你好,我是江澎涌,身高170厘米,体重122斤。
// 发送 importIdCreator 方法,会让子 Worker 加载单例
this.entryClientChannel.send("importIdCreator", { path: "../utils/IdCreator" } as LoadInfo)
// 会输出以下内容
// 47860 A01000/om.jiang...erverChannel I 【importIdCreator】loadInfo={"path":"../utils/IdCreator"} id=0
值得注意:
在这个场景中,因为 IdCreator.ts 是完全通过动态导入的,编译器无法分析到,不会将其编译,因此需要在 HAP 的 build-profile.json5 文件中进行配置,在 runtimeOnly.sources 中增加该文件,具体如下:
json
{
"apiType": "stageMode",
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/workerDynamicImport/EntryWorker.ets"
]
},
"arkOptions": {
"runtimeOnly": {
"sources": [
// 一定要添加这个,否则会加载失败
"./src/main/ets/workerDynamicImport/utils/IdCreator.ts"
]
}
}
}
}
如果不增加,则会报以下错误:
javascript
7756 A01000/om.jiang...erverChannel E 【importIdCreator】e=ReferenceError: Cannot find module '../utils/IdCreator' imported from '&entry/src/main/ets/workerDynamicImport/channel/EntryServerChannel&'.
五、让 HAR 加载外部逻辑
基于上一小节 "解决 Worker 内存隔离" ,我们进一步分享一个场景------相机拍摄模块加载外部滤镜。
我们封装了一个 Camera HAR 模块,内部有一个 Worker 用于处理相机帧,相机模块需要提供一个给外部添加滤镜的 API ,业务才能根据自身需要开发不同效果。所以这里就涉及了两个问题:
- 相机的 Worker 和主线程内存隔离
- 相机模块是基础模块,被上层业务依赖,所以无法通过
oh-package.json5进行设置依赖
而动态导入可以解决这些问题。动态加载无法加载 HAP 的代码 ,所以为了能让 Camera HAR 可以动态加载到滤镜的代码,首先需要建立一个 Filter HAR 模块,同时让它依赖 Camera HAR 模块,才能让业务滤镜实现 Filter 接口。 然后在 HAP 模块的 build-profile.json5 中配置 filter。
json
{
"apiType": "stageMode",
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"packages": [
"filter"
]
}
}
}
}
Camera 模块定义了 Filter 接口,同时需要定义一个描述滤镜加载信息的接口 FilterInfo 。
typescript
export interface FilterInfo {
module: string // HAR 模块名
clazz: string // 类名
}
export interface Filter {
setId(id: string)
render()
}
在 Filter 模块中编写业务的滤镜,需要实现 Filter 接口,遵循 Camera 滤镜规范。
typescript
import { Filter } from "camera";
export class MonoFilter implements Filter {
private id: string | undefined = undefined
private count = 1
setId(id: string): void {
this.id = id
}
render(): void {
Log.i("MonoFilter", `【render】id=${this.id} count=${this.count++}`)
}
}
然后在 Filter 模块的 Index.ets 中将滤镜导出即可。最后业务只需要像以下方式 addFilter 添加滤镜信息即可。
typescript
const camera = new Camera()
camera.start()
// 添加滤镜:指定模块名和类名
camera.addFilter({ module: "filter", clazz: "MonoFilter" })
相机模块内部,则需要通过 JWorker 发送信息到子 Worker 进行创建和添加至滤镜链。
typescript
export class CameraServerChannel extends Channel {
private filterChain: FilterChain = new FilterChain()
async handleMessage(methodName: string, data: Any) {
switch (methodName) {
case CameraMessageType.AddFilter: {
const filterInfo = data as FilterInfo
try {
const ns = await import(filterInfo.module)
const filter = new ns[filterInfo.clazz]()
filter.setId(`${CameraServerChannel.filterId++}`)
this.filterChain.addFilter(filter)
} catch (e) {
Log.e(TAG, `【AddFilter】添加失败 e=${e}`)
}
break
}
case CameraMessageType.FrameAvailable: {
this.filterChain.render()
break
}
}
}
}
等到下次相机帧可用的时候,调用滤镜链,则会看到以下输出:
bash
24494 A01000/om.jian...ServerChannel I 【AddFilter】模拟添加滤镜 filterInfo={"clazz":"MonoFilter","module":"filter"}
24494 A01000/om.jian...ServerChannel I 【FrameAvailable】模拟相机帧驱动
24494 A01000/om.jian...t/FilterChain I 【FrameAvailable】使用滤镜 filters 个数=1
24494 A01000/om.jian...rt/MonoFilter I 【render】id=0 count=1
六、延时加载模块
除了解决上面提到的各种限制问题,动态导入还有一个常见用途:延迟加载 / 按需加载。
静态导入会在应用启动时就加载所有导入的模块,即使某些模块当前页面并不需要。动态导入则是在运行时按需加载,可以减少首屏加载时间,提升应用启动性能。
以下代码展示了静态导入和动态导入的对比:
typescript
// 静态导入:应用启动时就会加载 Bean 模块,后续可以直接使用
import { User } from '../workerDynamicImport/bean/Bean'
const user = new User()
Log.i(TAG, `【静态导入】user=${JSON.stringify(user)}`)
// 动态导入:运行到对应逻辑,才会加载模块
const module = await import("../workerDynamicImport/bean/Bean")
const user = new module.User()
Log.i(TAG, `【动态导入】user=${JSON.stringify(user)}`)
两种方式的区别:
- 静态导入 :
import { User } from '...'在文件顶部声明,应用启动时即加载该模块,无论用户是否会用到。 - 动态导入 :
await import("...")在需要时才执行,模块在用户真正需要时才加载。
对于功能模块较多的应用,将非首屏必需的模块改为动态导入,可以有效减少启动时的模块加载数量,提升启动速度。
七、写在最后
这篇文章更多的是分享鸿蒙动态加载的具体使用场景,算是对官方文档的一个真实场景扩充吧。如果你觉得文章对你有所帮助,请给我一个赞并关注我吧。如果发现有哪些欠妥的地方,请在留言区与我讨论,我们共同进步。
项目地址:github.com/zincPower/D...
个人博客
csdn:blog.csdn.net/weixin_3762...
公众号:微信搜索 "江澎涌"