Neo 构建鸿蒙应用【二】:技术路线全解

Neo 构建鸿蒙应用【二】:技术路线全解

Neo 框架连载 · AI 辅助撰写 · GitHub

上一篇建立了四层架构的宏观视图。这一篇把 Neo 的全部技术机制讲完:Service、NeoModule、ServiceManager、Phase、Observable、StateBinder、Scope。

核心思路借鉴了 JavaBean 和 IoC------Java Boy 编程梦开始的地方。

Service:最小功能单元

所有领域建模被命名为 XXXService,继承 Service 基类。一个 Service 的完整生命周期:

kotlin 复制代码
constructor → register → init → loginCallback → load → (WORKING)
                                                  ↑
                                    reload ← unload ← logoutCallback

基类定义

typescript 复制代码
export abstract class Service implements AppPropagation {
  tag: string = this.constructor.name    // 默认用类名作标识
  context: Context | undefined
  depServices: AppPropagation[] = []     // 依赖的服务列表

  constructor(services: Service[]) {
    this.depServices = services
  }

  register = (cxt: Context) => {
    this.context = cxt
    this.init()
    // lifecycle → INITIALIZED
  }

  loginCallback = async () => {
    this.load().then(res => {
      if (res) {
        // lifecycle → WORKING
        serviceManager.refreshNode(this, this.lifecycle)
      }
    })
  }

  logoutCallback = async () => { /* unload → INITIALIZED */ }

  init() {}                                        // 初始化成员属性,不能做网络 IO
  async load(): Promise<boolean> { return true }   // 加载数据
  async unload(): Promise<boolean> { return true } // 卸载清理
  reload = () => { this.unload().then(() => this.load()) }
}

设计要点:

  • register 是箭头函数属性,不是方法。ArkTS 中子类不能 override 父类的箭头函数属性,保证基类的生命周期管理不被绕过
  • constructor(services: Service[]) 构造器注入,Spring 的思路
  • init() 不能做网络 IO------仅用于初始化成员属性
  • load() 返回 Promise<boolean>------true 表示成功,状态转为 WORKING

实际例子

typescript 复制代码
// AuthService.ets
export class AuthService extends Service {
  private currentUser: UserInfo | null = null
  private token: string = ''

  constructor() { super([]) }

  init() { this.currentUser = null; this.token = '' }

  async load(): Promise<boolean> {
    const saved = await this.loadFromStorage()
    if (saved) { this.token = saved.token; this.currentUser = saved.user }
    return true
  }

  async unload(): Promise<boolean> {
    this.currentUser = null; this.token = ''
    return true
  }
}

NeoModule:Koin 式模块声明

Service 定义好了,如何组织起来?借鉴 Koin 的 module DSL 风格:

typescript 复制代码
import { NeoModule, GLOBAL_PHASE, BUSINESS_PHASE } from 'neo'

const networkModule = new NeoModule('Network', [
  { tag: 'ApiService', phase: GLOBAL_PHASE, factory: () => new ApiService([]) },
  { tag: 'AuthService', phase: GLOBAL_PHASE, factory: () => new AuthService([]) },
])

const userModule = new NeoModule('User', [
  { tag: 'UserService', phase: BUSINESS_PHASE,
    factory: () => new UserService([]),
    dependencies: ['AuthService'] },
])

每个声明包含:

字段 说明
tag 服务唯一标识
phase 所属加载阶段
factory 延迟创建工厂
dependencies 依赖的其他服务 tag

模块组合:

typescript 复制代码
const appModule = networkModule.merge(userModule)
// 同名声明以当前模块优先

校验(load() 时自动调用):

typescript 复制代码
const errors = appModule.validate()
// 检测缺失依赖 + 循环依赖(DFS)

ServiceManager:IoC 容器

中枢管理者,处理注册、依赖解析和生命周期。

核心流程

typescript 复制代码
// EntryAbility.onCreate
serviceManager.register(this.context)          // 注入上下文
serviceManager.loadModule(appModule)            // 加载模块(校验、分组、注册)
await serviceManager.loginCallback()            // 触发多阶段启动

依赖解析

内部维护两张映射:serviceMap(tag → 实例)和 dependentMap(被谁依赖)。loginCallback 触发时递归解析依赖链------加载 Service A 之前,确保其所有依赖已 WORKING:

typescript 复制代码
// ServiceManager 内部(简化)
private ensureNodeWorking(node) {
  await Promise.all(node.depServices.map(dep => this.ensureNodeWorking(dep)))
  this.invokeLogin(node)
  await this.waitForLifecycle(node.tag, ServiceLifeCycle.WORKING)
}

获取服务:

typescript 复制代码
const userService = serviceManager.get<UserService>('UserService')!
const isReady = serviceManager.ready(userService)

Phase:渐进式启动

27 个 Service 不需要同时加载。

四个预定义阶段

阶段 优先级 策略 用途
GLOBAL_PHASE 10 串行等待 配置、数据库、网络、安全
BUSINESS_PHASE 20 串行等待 认证、用户、消息、通话
FEATURE_PHASE 30 并行触发 搜索、统计、主题、国际化
LAZY_PHASE 40 并行触发 小游戏、故事

策略含义:

  • 串行等待waitForComplete: true):该阶段全部完成后才进入下一阶段
  • 并行触发waitForComplete: false):触发后立即进入下一阶段,不等待完成
scss 复制代码
时间轴 ─────────────────────────────────────────────→

GLOBAL (串行)  ████████████████
BUSINESS (串行)                 ████████████████
FEATURE (并行)                                   ████ ← 不阻塞
LAZY (并行)                                           ██ ← 不阻塞

                     ↑ UI 可交互

启动耗时可控的核心:GLOBAL + BUSINESS 同步确保核心就绪,FEATURE + LAZY 异步不阻塞。优化手段就是把非必要 Service 从 BUSINESS 降到 FEATURE。

实战中的启动优化

我曾在实际项目中用这套机制做过启动优化(详见原文),当时 Neo 还不存在,但核心代码已经具备了 Service + Phase 的雏形。

优化成果固然可喜,但更重要的是整个结构变得可控了。因为每个 Service 的依赖关系、加载阶段、生命周期都是声明式的,我可以保证:

  • 把某个 Service 从 BUSINESS 降到 FEATURE,不会导致依赖它的模块出问题------依赖解析是自动的
  • 新增一个 Service 不需要改任何已有代码------在 AppModule 加一行声明即可
  • 变更的影响范围是可预期的------约束由框架兜底

这不是黑盒优化,而是白盒优化。以后遇到启动性能问题,打开 AppModule 调整 Phase 归属即可------不需要重新分析依赖链,不需要画调用图,套路是固定的。

自定义阶段:

typescript 复制代码
const CACHE_PHASE = createPhase({
  name: 'CACHE', priority: 25, waitForComplete: false, description: '缓存预热'
})

Observable:Service 端的数据源

Service 加载了数据,如何让页面感知变化?

朴素做法是页面直接调 Service 方法拿返回值赋给 @State------但只在初始化时有效,其他页面的数据变更不会自动刷新。

Neo 的方案是 Observable------泛型观察者容器:

typescript 复制代码
export class Observable<T> {
  private value_: T
  private listeners: Set<(value: T) => void> = new Set()

  constructor(initialValue: T) { this.value_ = initialValue }
  getValue(): T { return this.value_ }

  setValue(newValue: T): void {
    if (this.value_ === newValue) return
    this.value_ = newValue
    this.notify()
  }

  onChange(listener: (value: T) => void): () => void {
    this.listeners.add(listener)
    return () => { this.listeners.delete(listener) }
  }
}

没有 Subject、没有 Operator、没有调度器。ArkTS 不支持 RxJS 那套管道,也没有必要引入。

Service 中使用

typescript 复制代码
export class IMService extends Service {
  private conversationsObs = new Observable<Conversation[]>([])
  private messagesObs = new Observable<ChatMessage[]>([])

  getConversationsObservable() { return this.conversationsObs }
  getMessagesObservable() { return this.messagesObs }

  async sendMessage(conversationId: string, content: string): Promise<ChatMessage> {
    const msg = await this.doSend(conversationId, content)
    const msgs = this.messagesObs.getValue()
    this.messagesObs.setValue([...msgs, msg])  // 自动通知 UI
    return msg
  }

  async load(): Promise<boolean> {
    this.conversationsObs.setValue(await this.fetchConversations())
    return true
  }
}

模式:内部持有 Observable → getter 暴露 → setValue 触发变更。外部只能订阅,不能直接修改。

StateBinder:Service 到 @State 的桥

Observable 解决了通知问题,StateBinder 解决了写入 @State 的问题:

typescript 复制代码
export class StateBinder {
  static bind<T>(
    observable: Observable<T>,
    component: Object,      // 页面组件实例
    stateKey: string        // @State 属性名
  ): () => void {
    const record = component as Record<string, Object>
    record[stateKey] = observable.getValue() as Object    // 初始化
    const unlisten = observable.onChange((newValue: T) => {
      record[stateKey] = newValue as Object               // 变更时自动写入
    })
    return unlisten
  }

  static bindAll(factories: Array<() => () => void>): () => void { /* 批量绑定 */ }
}

页面中使用

typescript 复制代码
@Entry
@Component
struct ChatListPage {
  @State conversations: Conversation[] = []
  private unbind: (() => void) | undefined

  aboutToAppear() {
    const imSvc = serviceManager.get<IMService>('IMService')!
    this.unbind = StateBinder.bind<Conversation[]>(
      imSvc.getConversationsObservable(),
      this as Object,       // ArkTS 要求显式转换
      'conversations'
    )
  }

  aboutToDisappear() { this.unbind?.() }

  build() {
    List() {
      ForEach(this.conversations, (conv: Conversation) => {
        ListItem() { /* 渲染会话项 */ }
      })
    }
  }
}

注意两点 ArkTS 限制:

  • this as Object 不能省略 ------组件的 this 类型不允许直接传给 Object 参数
  • 显式泛型 <T> 不能省略 ------ArkTS 不支持自动推断泛型给 Record<string, Object> 赋值

批量绑定:

typescript 复制代码
aboutToAppear() {
  this.unbind = StateBinder.bindAll([
    () => StateBinder.bind<Conversation[]>(imSvc.getConversationsObservable(), this as Object, 'conversations'),
    () => StateBinder.bind<ChatMessage[]>(imSvc.getMessagesObservable(), this as Object, 'messages'),
    () => StateBinder.bind<number>(matchSvc.getCountObservable(), this as Object, 'matchCount'),
  ])
}

aboutToDisappear() { this.unbind?.() }  // 一行解绑所有

Scope:页面级作用域

有些 Service 是页面专属的,页面离开时应该卸载:

typescript 复制代码
import { scopeManager } from 'neo'

aboutToAppear() {
  this.scope = scopeManager.createScope('ChatPage')
  this.scope.bindTo(this)  // aboutToDisappear 时自动 close

  const chatRoomSvc = new ChatRoomService([])
  this.scope.register(chatRoomSvc)
}
// 页面销毁 → scope.close() → unload 所有注册的服务

Scope 查找机制:优先从作用域内找,找不到回退到全局 ServiceManager。scope.get<T>('ChatRoomService') 获取页面级服务,scope.getGlobal<T>('AuthService') 获取全局服务。

小结

Neo 的完整技术路线:

组件 职责
Service 功能单元,声明式生命周期
NeoModule Koin 式模块声明,组合与校验
ServiceManager IoC 容器,依赖解析
Phase 多阶段加载,串行/并行策略
Observable Service 端可观察数据源
StateBinder Service 数据 → @State 响应式桥
Scope 页面级作用域,自动卸载

下一篇是最后一篇,用 SoulApp 示例串起全链路,并分享这个框架在工程实践中的一些感悟。


系列文章

相关推荐
ai安歌11 小时前
鸿蒙PC:Qt适配OpenHarmony实战【取色间】:RGB 滑动调整、HEX 展示和颜色预览
qt·华为·harmonyos
lqj_本人11 小时前
鸿蒙electron跨端框架PC想法卡片实战:把零散灵感做成能继续展开的卡片流
华为·electron·harmonyos
lqj_本人14 小时前
鸿蒙electron跨端框架PC浮签实战:做一面轻巧但能回找的桌面便签墙
华为·harmonyos
ai安歌15 小时前
鸿蒙PC:Qt适配OpenHarmony实战【人名录】:单机联系人卡片,不读系统通讯录也能演示详情联动
数据库·qt·harmonyos
lqj_本人16 小时前
鸿蒙electron跨端框架PC简序实战:把轻任务、优先级和截止时间塞进一张桌面清单
华为·harmonyos
想你依然心痛16 小时前
HarmonyOS 6 悬浮导航 + 沉浸光感:打造鸿蒙智能体驱动的沉浸式智能家居控制中枢
华为·ar·智能家居·harmonyos·智能体
lqj_本人16 小时前
鸿蒙PC:Qt适配OpenHarmony实战【花账】:从一笔支出开始,做一个本地记账小应用
数据库·qt·harmonyos
递归40417 小时前
ofdkit-harmony 0.2.0 发布:鸿蒙原生 OFD 阅读库,已上架 ohpm
开源·harmonyos·arkts·ofd·ohpm
nashane18 小时前
HarmonyOS 6学习:SoundPool音频防抖与Web长截图时序重构
学习·音视频·harmonyos·harmonyos 5
Exploring18 小时前
鸿蒙App开发,华为手机里装这一个就够了——「Hola万能计算器」到底有多万能?
harmonyos