👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言:插件化不是“为了酷”,是为了“少崩、少发版、少互相甩锅”🙂

我见过最离谱的“插件化”项目:
主工程里什么都没有,全靠“插件”撑着,结果版本一升级,接口一改,插件集体爆炸——那场面就像你把家里承重墙拆了,还问为啥屋顶塌了🙃。

在鸿蒙里做插件化/动态化,我建议你先记住一句“祖训”:

动态化 ≠ 在线执行新代码;插件化 = 稳定契约 + 可控加载 + 可回退。

1) 插件化设计原则:别急着拆模块,先把“边界”和“契约”定死🧱

1.1 先定“插件是什么”:功能插件 vs UI插件 vs 能力插件

我通常把插件分三类(你选一种就够,别三种都要):

  • 功能插件(Feature Plugin):比如“会员中心”“扫一扫”“反馈页”
  • UI插件(UI Widget):一套可复用组件库(按钮、卡片、业务组件)
  • 能力插件(Capability Plugin):比如日志采集、埋点、配置中心(更偏底层)

大多数业务团队,最适合先做 功能插件:能看到收益,也最容易收敛边界。

1.2 插件契约必须“瘦”:只暴露稳定接口,不暴露内部细节

**契约(Contract)**怎么设计最抗揍?

  • 返回值尽量简单(基础类型/DTO)
  • 避免把页面组件结构当接口(UI 是最容易变的)
  • 版本号要有(不然升级就是赌命)

你可以用“接口 + 工厂 + 元信息”的形式固定下来:

// contracts/IPlugin.ets
export interface IPlugin {
  readonly id: string
  readonly version: string
  init?(ctx: PluginContext): void
  getRoutes(): Array<RouteDef>
}

export interface PluginContext {
  env: 'dev' | 'prod'
  log: (msg: string) => void
}

export interface RouteDef {
  path: string
  title: string
  // 注意:这里不直接暴露组件树,给一个“页面入口函数”更稳
  open: (params?: Record<string, Object>) => Promise<void>
}

1.3 插件必须可回退:加载失败要“体面”,不要“白屏”

插件化项目最怕“某个插件挂了,主应用跟着陪葬”。正确姿势:

  • 插件加载失败 → 主应用降级(隐藏入口/提示升级/走旧页)
  • 插件接口不兼容 → 走兼容层或禁用该插件
  • 插件初始化超时 → 别卡主线程,直接 fallback

2) 动态加载能力:鸿蒙的“动态化”,更像“按需导入模块”📦

2.1 ArkTS 动态 import:按条件/按需加载模块(官方正牌)

鸿蒙官方文档明确:动态 import 支持条件延迟加载,能提升加载速度、降低内存占用;并且支持加载 HSP/HAR/OHPM 包/Native 库等。

这意味着:你可以把插件做成模块(HAR/HSP),需要时再 import(),而不是启动时全塞进来。

2.1.1 常量动态 import(最稳)
// 常量路径:打包期可分析,风险小
const mod = await import('@my/plugins/payment')
mod.start()
2.1.2 变量动态 import(更灵活,但要配 runtimeOnly)

官方说明:通过变量动态 import 其他模块时,需要在 build-profile.json5buildOptionruntimeOnly(静态 import 和常量动态 import 不需要)。

示意(重点看结构):

// entry/build-profile.json5(示意)
{
  "buildOption": {
    "runtimeOnly": [
      "@my/plugins/payment",
      "@my/plugins/feedback"
    ]
  }
}

然后你就能这么玩:

// 变量动态 import:根据配置决定加载哪个插件
async function loadPlugin(name: string) {
  // name 例如:'@my/plugins/payment'
  return await import(name)
}

2.2 HAR vs HSP:别选错,不然你“插件”要么太重,要么不合规😅

官方 FAQ 对二者定位说得很清楚:

  • HAR:静态共享包(编译期复用),不支持在配置文件里声明 pages/abilities 等(更像“库”)
  • HSP:动态共享包(运行时共享)

所以一个很实用的选择建议是:

  • UI组件库/工具库:优先 HAR(稳定、纯库、少心智负担)
  • 带页面/带路由的“功能插件”:更偏 HSP(更贴合“模块化功能”)

3) 安全限制:你想“随时下发代码”,系统想“你别乱来”🛑

3.1 动态 import ≠ 在线执行新代码

官方动态 import 讲的是:加载你已打包进应用的 HSP/HAR/OHPM/Native 库等模块,实现按需导入与解耦。
它不是让你“从服务器下载一段代码然后当场执行”的那种玩法。

你要真做“远程代码执行”,风险非常直接:被篡改、被注入、被劫持……这类行为在移动平台安全体系里普遍被严控(Android 官方安全说明也明确指出远程动态代码加载的安全风险)。
鸿蒙这边更强调“确定性 + 安全”,所以工程上你更应该走“模块化 + 版本发布”的正路。

3.2 沙箱与路径限制:Native 动态加载也不是想 dlopen 哪儿就 dlopen

OpenHarmony 的沙箱机制会限制应用对系统资源/路径的访问,很多“绝对路径访问”在启用沙箱后会失效,需要通过 context 等方式访问应用数据目录。
这对“动态加载 native 库/资源文件”的影响就是:

  • 你必须把可加载内容放在合理位置、受控路径
  • 别在代码里硬编码奇怪路径(到真机上大概率翻车)

现实一点说:你想搞“偷偷摸摸的动态化”,沙箱第一个不同意🙂

3.3 插件权限与能力边界:插件别拿“主应用权限”当免死金牌

插件模块(无论 HAR/HSP)最终都运行在应用的进程/权限模型里。你要做的不是“给插件更多权限”,而是:

  • 插件只做它该做的事
  • 敏感能力统一走主应用的授权与审计
  • 插件接口层做参数校验,避免“插件把主应用玩坏”

4) 实战案例:做一个“功能插件化”的支付/反馈模块(可加载、可回退)💥

4.1 目录结构(长得像真项目,不是 PPT)

app/
  entry/                     # 主HAP
    src/main/ets/
      contracts/
        IPlugin.ets
      plugin/
        PluginManager.ets
      pages/
        Home.ets
  plugins/
    payment_hsp/             # 支付插件(HSP)
      src/main/ets/
        index.ets
        PaymentEntry.ets
    feedback_hsp/            # 反馈插件(HSP)
      src/main/ets/
        index.ets
        FeedbackEntry.ets

4.2 插件入口:每个插件暴露一个 createPlugin()

// plugins/payment_hsp/src/main/ets/index.ets
import { IPlugin, PluginContext, RouteDef } from '../../entry/src/main/ets/contracts/IPlugin'

export function createPlugin(): IPlugin {
  return {
    id: 'payment',
    version: '1.0.0',
    init(ctx: PluginContext) {
      ctx.log(`[payment] init ok, env=${ctx.env}`)
    },
    getRoutes(): Array<RouteDef> {
      return [{
        path: '/payment',
        title: '支付',
        async open(params?: Record<string, Object>) {
          // 这里示意:实际你会用 router.pushUrl / Navigation 跳转
          console.info(`[payment] open with params=${JSON.stringify(params ?? {})}`)
        }
      }]
    }
  }
}

4.3 主应用 PluginManager:按需加载 + 缓存 + 降级

// entry/src/main/ets/plugin/PluginManager.ets
import { IPlugin, PluginContext } from '../contracts/IPlugin'

type PluginModule = { createPlugin: () => IPlugin }

export class PluginManager {
  private cache: Map<string, IPlugin> = new Map()

  constructor(private ctx: PluginContext) {}

  async get(id: string): Promise<IPlugin | null> {
    if (this.cache.has(id)) return this.cache.get(id) ?? null

    const moduleName = this.resolveModuleName(id)
    if (!moduleName) return null

    try {
      const mod = (await import(moduleName)) as unknown as PluginModule
      const plugin = mod.createPlugin()
      plugin.init?.(this.ctx)
      this.cache.set(id, plugin)
      return plugin
    } catch (e) {
      // 关键:别白屏,别崩,体面降级
      this.ctx.log(`[PluginManager] load failed id=${id}, err=${JSON.stringify(e)}`)
      return null
    }
  }

  private resolveModuleName(id: string): string | null {
    // 真实项目你会从配置中心/开关系统里拿
    const map: Record<string, string> = {
      payment: '@my/plugins/payment_hsp',
      feedback: '@my/plugins/feedback_hsp'
    }
    return map[id] ?? null
  }
}

注意:这里用了“变量动态 import”,你需要在 build-profile.json5 里配 runtimeOnly(前面讲过),官方也明确了这个要求。

4.4 首页入口:点击时加载插件并打开路由

// entry/src/main/ets/pages/Home.ets
import { PluginManager } from '../plugin/PluginManager'

@Entry
@Component
struct Home {
  private pm = new PluginManager({
    env: 'prod',
    log: (msg: string) => console.info(msg)
  })

  build() {
    Column({ space: 12 }) {
      Button('打开支付(插件)')
        .onClick(async () => {
          const plugin = await this.pm.get('payment')
          if (!plugin) {
            // 降级体验:给用户一个明确反馈
            console.info('支付模块暂不可用,请稍后重试🙂')
            return
          }
          const routes = plugin.getRoutes()
          const pay = routes.find(r => r.path === '/payment')
          await pay?.open({ orderId: 'NO.20251215' })
        })

      Button('打开反馈(插件)')
        .onClick(async () => {
          const plugin = await this.pm.get('feedback')
          if (!plugin) {
            console.info('反馈模块加载失败(我也很无奈😮‍💨)')
            return
          }
          await plugin.getRoutes()[0].open({ from: 'home' })
        })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

4.5 你真正得到的收益(不是“看起来高级”,是真能省事)

  • 首包更轻:非核心功能不必启动就加载
  • 首屏更快:动态 import 让冷启动减少不必要模块初始化
  • 团队协作更顺:插件模块独立开发、独立测试
  • 风险可控:加载失败可回退,功能开关可熔断

最容易踩的坑(我提前帮你骂一遍😅)

  1. 把插件接口做太肥:一改就全体联动,插件化瞬间失效
  2. 用变量动态 import 却不配 runtimeOnly:本地能跑、线上找不到模块,最气人
  3. 把插件当“万能扩展点”:最后主工程只剩一个壳,排查问题像破案
  4. 忽略沙箱路径限制:动态加载 native/文件路径硬编码,上真机直接崩

结语:鸿蒙的“动态化”,拼的是克制,不是放飞🙂

你要是把插件化当成“随时上线新代码”的捷径,那多半会走歪;但你要把它当成“模块化 + 按需加载 + 运行时共享”的工程能力,它真的能让大型应用变得更轻、更稳、更好协作。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:某个被流“治愈”过的 移动端 老兵
📅 日期:2025-11-05
🧵 本文原创,转载请注明出处。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐