API 26 新增的 @CustomEnv 为 ArkUI 提供了自定义环境变量能力:通过 CustomEnvKey.create() 创建类型化 Key,祖先容器注入值,后代组件用装饰器读取;未设置时使用本地默认值。它很像轻量依赖注入,但如果把所有全局状态都塞进去,仍会形成隐式依赖、刷新范围失控和测试困难。

本文不只演示语法,而是从主题、布局密度、实验开关、服务适配器和测试替身等实际场景,说明如何设计 Key、注入边界、默认值、可观察更新与迁移策略。

HarmonyOS 7 新特性(四十三)封面

一、@CustomEnv 解决什么问题

过去,深层组件常通过多层参数透传获得主题、格式化器或页面策略。参数本身不属于中间组件,却必须逐层声明。

interface ReadingEnvironment {
  density: 'COMPACT' | 'COMFORTABLE'
  imagePolicy: 'AUTO' | 'LOW_DATA'
  experiment: string
}

自定义环境适合表达“在一棵组件子树中普遍成立、由祖先提供、后代只读消费”的上下文。普通业务实体、输入表单和高频列表数据仍应使用显式状态或 ViewModel。

二、用类型化 Key 隔离语义

两个值即使都是字符串,也不代表可以共用 Key。Key 要按语义创建,并集中导出。

export const DensityEnv = CustomEnvKey.create<'COMPACT' | 'COMFORTABLE'>()
export const LocaleEnv = CustomEnvKey.create<string>()
export const ExperimentEnv = CustomEnvKey.create<ExperimentFlags>()

export interface ExperimentFlags {
  newReaderToolbar: boolean
  smartSummary: boolean
}

集中注册可以防止不同模块重复创建同名但不相同的 Key,也便于做依赖台账。

三、默认值必须安全可用

官方语义指出:如果环境没有设置,使用变量本地声明的默认值。因此默认值不能是随手填的占位符。

@ComponentV2
struct ReaderToolbar {
  @CustomEnv(DensityEnv) density: 'COMPACT' | 'COMFORTABLE' = 'COMFORTABLE'
  @CustomEnv(ExperimentEnv) flags: ExperimentFlags = {
    newReaderToolbar: false,
    smartSummary: false
  }

  build() {
    Row({ space: this.density === 'COMPACT' ? 8 : 16 }) {
      Text(this.flags.smartSummary ? '智能摘要' : '目录')
    }
  }
}

默认值应对应功能关闭、权限最小、布局稳定的保守路径。未注入时组件仍能预览、测试和降级。

四、注入边界选择页面或功能域

不要一律在应用根节点注入。越靠根,影响范围越大,依赖越隐蔽。优先在页面、弹窗、工作区或可独立测试的功能域创建边界。

@Entry
@ComponentV2
struct ReaderPage {
  private env: ReadingEnvironment = {
    density: 'COMFORTABLE',
    imagePolicy: 'AUTO',
    experiment: 'reader-v3'
  }

  build() {
    WithEnv({ /* 以目标 API 26 实际签名为准 */ }) {
      ReaderContent()
    }
  }
}

这里强调架构位置;WithEnv 的具体构造和设置方式应按当前 API 26 SDK 文档编写,不要从旧版本示例猜测签名。

HarmonyOS 7 新特性(四十三)核心链路

五、区分环境与领域状态

可进入环境变量的值通常具有低频变化、横切语义和只读消费特征。

type Candidate = {
  name: string
  frequency: 'LOW' | 'HIGH'
  ownedByPage: boolean
  crossCutting: boolean
}

function shouldUseCustomEnv(c: Candidate): boolean {
  return c.frequency === 'LOW' && c.crossCutting && !c.ownedByPage
}

主题、字号级别、度量单位、实验开关、只读服务接口通常合适;购物车商品、聊天消息、输入框文本和分页结果不合适。

六、把服务注入成窄接口

环境中可以放服务抽象,但不要暴露巨大容器或全局单例。组件只依赖自己需要的最小接口。

export interface PriceFormatter {
  format(cents: number, currency: string): string
}

export const PriceFormatterEnv = CustomEnvKey.create<PriceFormatter>()

const fallbackFormatter: PriceFormatter = {
  format: (cents, currency) => `${currency} ${(cents / 100).toFixed(2)}`
}

这样组件没有直接依赖网络、数据库或应用级 ServiceLocator,测试时也能注入确定性替身。

七、组件只读,不反向修改环境

环境表达上下文,不应成为任意后代写全局状态的通道。修改动作通过显式回调或命令接口上抛。

interface ThemeCommand {
  requestTheme(theme: 'LIGHT' | 'DARK'): Promise<void>
}

@ComponentV2
struct ThemeButton {
  @Param onRequestTheme: (theme: 'LIGHT' | 'DARK') => void

  build() {
    Button('切换主题').onClick(() => this.onRequestTheme('DARK'))
  }
}

所有权清晰后,审计“是谁改变了主题”才有路径可追踪。

八、嵌套覆盖用于局部策略

同一应用里,阅读页可使用舒适密度,桌面侧栏可覆盖为紧凑密度。局部覆盖应尽量窄,并在命名上体现目的。

@ComponentV2
struct CompactInspectorPanel {
  build() {
    WithEnv({ /* DensityEnv -> COMPACT */ }) {
      InspectorContent()
    }
  }
}

避免在多层树中反复覆盖同一 Key,否则实际取值难以判断。调试日志可在开发构建中输出注入边界和版本。

九、更新频率决定刷新成本

环境值变化会影响消费它的后代。高频动画进度、滚动位置或每帧传感器数据放入环境,会扩大无效刷新。

interface EnvMetric {
  key: string
  updatePerMinute: number
  consumerCount: number
}

function riskScore(m: EnvMetric): number {
  return m.updatePerMinute * m.consumerCount
}

对变化频繁的数据,使用局部状态、绘制属性或专用数据通道。环境值优先使用不可变对象,更新时整体替换,避免深层原地修改导致观察语义不明确。

十、预览与单元测试注入替身

默认值保证组件能独立运行,局部环境覆盖则可以测试多种上下文。

const fakeFormatter: PriceFormatter = {
  format: (cents) => `TEST:${cents}`
}

interface ReaderCase {
  density: 'COMPACT' | 'COMFORTABLE'
  smartSummary: boolean
  expectedToolbarHeight: number
}

至少覆盖默认未注入、父级注入、子树覆盖、动态切换和销毁重建。截图测试要同时检查深色模式、放大字体和大屏分栏。

十一、从参数透传渐进迁移

不要一次性改完整棵组件树。先选择一个低频、只读、横切属性建立 Key,再逐层删除中转参数。

// 迁移前:Page -> Section -> Card -> PriceText
// 迁移后:Page 注入 PriceFormatterEnv,PriceText 直接消费
interface MigrationRecord {
  key: string
  provider: string
  consumers: string[]
  fallbackDefined: boolean
  tests: string[]
}

迁移期间允许旧参数与环境并存一小段时间,但要规定唯一优先级并尽快删除兼容层。

十二、错误使用模式

第一,把可变业务实体放进环境,导致任何组件都能隐式读取;第二,默认值依赖网络或抛异常,使预览无法运行;第三,用一个 AppContext 包含几十项服务;第四,组件反向修改共享对象;第五,嵌套覆盖太深;第六,没有对 API 26 支持范围做版本保护。

interface EnvRegistryItem {
  owner: string
  key: string
  purpose: string
  defaultSafe: boolean
  minApi: number
}

维护简单注册表,就能在评审时识别全局依赖膨胀。

HarmonyOS 7 新特性(四十三)检查清单

十三、上线检查清单

  • 每个 Key 只表达一种业务语义;
  • Key 集中注册并标注所有者;
  • 默认值安全、稳定且可用于预览;
  • 注入边界优先落在页面或功能子树;
  • 高频领域状态未放入环境;
  • 服务以最小只读接口注入;
  • 修改动作通过显式命令或回调;
  • 覆盖默认、父级注入和局部覆盖测试;
  • 已核对目标 API 26 SDK 的具体签名与设备范围;
  • 旧参数透传有明确删除计划。

结语

@CustomEnv 的价值不是少写几个参数,而是给组件树建立清晰、类型化、可覆盖的上下文边界。用安全默认值保证独立运行,用窄接口约束依赖,用页面级注入控制刷新范围,再用替身测试验证不同环境,才能把它变成可维护的架构工具,而不是新的全局变量入口。

官方参考

  • @CustomEnv API 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-custom-env-property
  • HarmonyOS 7 版本说明:https://developer.huawei.com/consumer/cn/doc/harmonyos-releases/changelogs-600
  • HarmonyOS 7 新能力一览:https://developer.huawei.com/consumer/cn/features/
Logo

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

更多推荐