#星光不负 码向未来#从Vue到鸿蒙小记 原创

盐焗西兰花
发布于 2025-10-27 23:22
浏览
0收藏

#星光不负 码向未来#从Vue到鸿蒙小记

前言:Vue开发者的鸿蒙转型踩坑记录

作为一名深耕Vue多年的前端开发者,接触鸿蒙生态后,我就一头扎进了前端转鸿蒙的学习里。起初我想得特简单:不就是换套语法嘛——Vue的组件化、响应式思想,跟鸿蒙ArkUI看着差不多。编写uI界面确实差不多,但是找上手写第一个稍微复杂一点的状态管理模块就栽了o(╥﹏╥)o:V1版本的装饰器规则绕得我头大,嵌套对象改完视图不刷新的问题,处理起来也比较难受,而且不仅仅是我一个难受,在后续逛鸿蒙开发者社区,以及参加一些线下沙龙和其他转型的开发者一聊才发现,这块处理起来都挺难受的【狗头】!

这篇文章就结合我的转型经历,聊聊鸿蒙状态管理从V1到V2的变化,把各阶段的坑和解决办法讲清楚,再顺带说说跨平台框架鸿蒙化的进展,给想转型的同学做个参考。

鸿蒙状态管理V1:探索与困境

(一)V1状态管理小试牛刀

#星光不负 码向未来#从Vue到鸿蒙小记-鸿蒙开发者社区

刚接触V1时,我特意对比了Vue和鸿蒙的响应式机制:Vue2用Object.defineProperty、Vue3用Proxy,我们只需要定义状态变量即可实现数据的响应式;但鸿蒙V1得靠@State、@Prop、@Link等装饰器来实现,每个的作用域和传递规则都不一样,为了方便记忆,官方有一张完整的说明图示,如上图不同场景的装饰器是不同的。在业务不复杂不考虑嵌套数据的更新时,写起来和前端确实没啥区别。

先上个基础示例,看看V1怎么用装饰器实现父子组件状态传递,大家一看就懂:

// 父组件代码
@Entry
@Component
struct ParentComponent {
  // 定义父组件内部状态,@State装饰器实现响应式
  @State parentCount: number = 0;

  build() {
    Column() {
      Text(`父组件计数:${this.parentCount}`)
        .fontSize(18)
      // 点击按钮修改状态
      Button('父组件计数+1')
        .onClick(() => {
          this.parentCount++;
        })
        .margin(10)
      // 向子组件传值,子组件用@Prop接收
      ChildComponent({
        childCount: this.parentCount,
        changeCount: (newCount) => {
          this.parentCount = newCount;
        },
      })

    }
    .padding(20)
  }
}

// 子组件代码
@Component
struct ChildComponent {
  // 通过@Prop接收传递的父组件状态
  @Prop childCount: number = 0;
  changeCount: (newCount: number) => void = (newCount) => {
  }

  build() {
    Column() {
      Text(`子组件接收的计数:${this.childCount}`)
        .fontSize(16)
        .fontColor('#0094ff')
        .onClick(() => {
          this.changeCount(Math.floor(Math.random() * 100))
        })
    }
    .padding(10)
    .border({ width: 1 })
  }
}

这段代码核心就2点:

  1. 父-子:父组件用@State定义parentCount,子组件用@Prop接收。父组件的count一改,子组件就同步更新
  2. 子-父:点击子组件,通过回调函数传递随机数给父组件,父组件parentCount改变,重新传递子组件@Prop

这是一个非常简单的父子联动demo,咱们可以发现,虽然语法和Vue略有不同,但是步骤是一致的,当时的我,天真的认为其他的状态管理也和这个父子通讯类似,自己前端的经验可以全盘照搬。。。

对应官方文档:状态管理概述

#星光不负 码向未来#从Vue到鸿蒙小记-鸿蒙开发者社区

(二)较为繁琐的深度监听

然后我碰到了深层数据监听的问题o(╥﹏╥)o,当时做用户信息编辑模块,要改嵌套的地址信息。按Vue的习惯直接写this.userInfo.city.name,页面纹丝不动!仔细查阅官方文档才知道:“上文所述的装饰器(包括@State、@Prop、@Link、@Provide和@Consume装饰器)仅能观察到第一层的变化,嵌套属性默认是监听不到的。”

我写了个demo复现这个问题,大家一看就明白:

import { data } from '@kit.TelephonyKit'

class UserInfo {
  name: string
  age: number
  city: City

  constructor(
    name: string,
    age: number,
    city: City
  ) {
    this.name = name
    this.age = age
    this.city = city
  }
}

class City {
  name: string

  constructor(
    name: string
  ) {
    this.name = name
  }
}

@Entry
@Component
struct Index {
  // 定义嵌套对象状态
  @State userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))

  build() {
    Column({ space: 10 }) {
      Text(`姓名:${this.userInfo.name}`)
        .fontSize(18)
      Text(`年龄:${this.userInfo.age}`)
        .fontSize(18)
      Text(`城市:${this.userInfo.city.name}`)
        .fontSize(18)
      // 修改第一层属性,正常更新
      Button('修改姓名')
        .onClick(() => {
          this.userInfo.name = '李四'; // 视图会刷新
        })
        .margin(5)
      // 修改嵌套属性,不触发更新(坑点!)
      Button('修改城市')
        .onClick(() => {
          this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
        })
        .margin(5)
    }
  }
}

@Component
struct cityCom {
  @ObjectLink city: City

  build() {
    Text(this.city.name)
  }
}

实际测试就是这样:改userInfo.name这种第一层属性,视图正常更新;但改city.name这种嵌套属性,页面完全没反应。

如何解决呢?既然只能监听到第一层,那我直接对第一层属性进行修改:

// 其他略
Button('修改城市')
  .onClick(() => {
    // this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
    this.userInfo.city = new City("北京")
  })

使用上面的写法可以触发视图更新,但是这么做的话每次都需要创建新的实例化对象,性能上略有浪费,就没有更好的方法了吗?然后我就看到了:@Observed装饰器和@ObjectLink

  1. @Observed添加给Class
  2. 添加子组件,子组件通过@ObjectLink接收传递过来的被@Observed装饰的状态变量

具体代码如下

import { data } from '@kit.TelephonyKit'

class UserInfo {
  name: string
  age: number
  city: City

  constructor(
    name: string,
    age: number,
    city: City
  ) {
    this.name = name
    this.age = age
    this.city = city
  }
}

@Observed
class City {
  name: string

  constructor(
    name: string
  ) {
    this.name = name
  }
}

@Entry
@Component
struct Index {
  // 定义嵌套对象状态
  @State userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))

  build() {
    Column({ space: 10 }) {
      Text(`姓名:${this.userInfo.name}`)
        .fontSize(18)
      Text(`年龄:${this.userInfo.age}`)
        .fontSize(18)
      // 这里依旧不会刷新
      Text(`城市:${this.userInfo.city.name}`)
        .fontSize(18)
      // 添加子组件 传递 city
      cityCom({ city: this.userInfo.city })
      // 修改第一层属性,正常更新
      Button('修改姓名')
        .onClick(() => {
          this.userInfo.name = '李四'; // 视图会刷新
        })
        .margin(5)
      // 修改嵌套属性,不触发更新(坑点!)
      Button('修改城市')
        .onClick(() => {
          this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
        })
        .margin(5)
    }
  }
}

@Component
struct cityCom {
  // 使用ObjectLInk接收状态变量
  @ObjectLink city: City

  build() {
    Text(this.city.name)
  }
}

这么写完之后,当我们点击修改城市,城市就可以更新了。但为了实现这个效果,我们需要

  1. 使用新的装饰器
  2. 拆分子组件

但是我如果继续在父组件中,去访问那个嵌套属性,因为是@State装饰,依旧是无法监听到改变(42行代码),需要在子组件中,配合@ObjectLInk才可以监听到。

#星光不负 码向未来#从Vue到鸿蒙小记-鸿蒙开发者社区

(三)组件输入输出不明确

接下来这个问题,主要是因为自己粗心,但是也侧面反映了V1中组件通信装饰器的职能不明确的问题,直接上代码,子组件用错装饰器导致状态不同步:

// 错误示例:子组件用@State接收父组件状态,导致后续的修改UI不刷新
@Component
struct WrongDecoratorDemo {
  // 错误!子组件接父组件值该用@Prop,我误用了@State
  @State wrongProp: string = '';

  build() {
    Text(`子组件错误接收:${this.wrongProp}`)
  }
}

@Entry
@Component
struct Index {
  @State parentMsg: string = '父组件消息';

  build() {
    Column() {
      WrongDecoratorDemo({ wrongProp: this.parentMsg })
      Button('修改父组件消息')
        .onClick(() => {
          this.parentMsg = '修改后的父组件消息';
          // 子组件用了@State,这里修改同步不过去!
        })
    }
  }
}


子组件中错误的选用了@State来接收父组件传递的数据,导致后续父组件修改状态变量页面不更新,首先主要原因在我,但是这么写在传递数据的时候不会有任何的错误提示,让我在编码阶段一度认为自己是没错的。

不好好在随着熟练度的替身,这些小问题倒也容易规避,只是依旧会期待,期待有更好的写法。

鸿蒙状态管理V2:突破与展望

(一)痛点击破-深度监听

鸿蒙状态管理V2发布时,我第一时间就打开文档进行确认,看到了这段介绍之后,立马打开编辑器进行测试,因为之前我遇到的痛点全部都已经解决了。

  1. 深度监听,支持属性级更新
  2. <font style=“color:rgb(36, 39, 40);”>在组件中明确输入与输出</font>
  3. <font style=“color:rgb(36, 39, 40);”>新增计算属性支持</font>
  4. <font style=“color:rgb(36, 39, 40);”>…</font>

咱们逐个上代码演示

直接上代码,看看V2怎么解决深度监听问题,跟V1对比差距很明显:

@ObservedV2
class UserInfo {
  @Trace
  name: string
  age: number
  city: City

  constructor(
    name: string,
    age: number,
    city: City
  ) {
    this.name = name
    this.age = age
    this.city = city
  }
}

@ObservedV2
  class City {
    @Trace
    name: string

    constructor(
      name: string
    ) {
      this.name = name
    }
  }

  @Entry
  @ComponentV2
  struct Index {
    // 定义嵌套对象状态
     userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))

    build() {
      Column({ space: 10 }) {
        Text(`姓名:${this.userInfo.name}`)
          .fontSize(18)
        Text(`年龄:${this.userInfo.age}`)
          .fontSize(18)
        Text(`城市:${this.userInfo.city.name}`)
          .fontSize(18)
        // 修改第一层属性,正常更新
        Button('修改姓名')
          .onClick(() => {
            this.userInfo.name = '李四'; // 视图会刷新
          })
          .margin(5)
        // 修改嵌套属性,不触发更新(坑点!)
        Button('修改城市')
          .onClick(() => {
            this.userInfo.city.name = '上海'; // 页面更新,深度监
          })
          .margin(5)
      }
    }
  }


需要监听的class用@ObservedV2修饰,需要监听的属性用@Trace修饰,首层属性,或者深沉次属性都可以被监听到,相比于V1的写法更加简单便捷。

(二)痛点击破-明确输入输出

新增的装饰器在语义上更加明确,比如用@Local代替V1的@State,一看就知道是组件本地状态;父向子传值用@Param,依旧直接上代码

@ComponentV2
  struct WrongDecoratorDemo {
    // 正确写法,子组件用@Param接收父组件状态
    @Param wrongProp: string = '';

    // 直接报错,子组件用@Local定义状态变量,外部无法传递属性
    // @Local wrongProp: string = '';

    // 直接报错,子组件定义普通属性,无法从外部传递属性值
    // wrongProp: string = '';

    build() {
      Text(`子组件错误接收:${this.wrongProp}`)
    }
  }

@Entry
  @ComponentV2
  struct Index {
    @Local parentMsg: string = '父组件消息';

    build() {
      Column() {
        WrongDecoratorDemo({ wrongProp: this.parentMsg })
        Button('修改父组件消息')
          .onClick(() => {
            this.parentMsg = '修改后的父组件消息';
            // 子组件用了@State,这里修改同步不过去!
          })
      }
    }
  }


用@Local,或者不写装饰器无法从外部传递数据,做到了明确的输入输出,这个改变我很喜欢。

(三)性能优化-计算属性

<font style=“color:rgba(0, 0, 0, 0.9);”>“当开发者使用相同的计算逻辑重复绑定在UI上时,为了防止重复计算,可以使用@Computed计算属性。”这是官网解释,Vue中也是一样的作用呢。</font>

@Computed
get sum() {
  return this.count1 + this.count2 + this.count3;
}
Text(`${this.count1 + this.count2 + this.count3}`) // 计算this.count1 + this.count2 + this.count3
Text(`${this.count1 + this.count2 + this.count3}`) // 重复计算this.count1 + this.count2 + this.count3
Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算
Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算

重复取值会读取缓存值,性能消耗会更低一些V1这个可视没有的呢。

除了我罗列的这些以外,还有更多的新特性,稍后大伙可以自行探索,相信大伙可能还有疑惑:

  1. V2是从api12推出,现在已经非常稳定了,可以放心使用
  2. 新项目无脑直接V2即可
  3. V2和V1可以混用

如果你之前用的是V1,正好试试V2,如果还没有接触鸿蒙,直接从V2开始就是啦~~

跨平台框架的鸿蒙化进程

转型过程中,我还试了不同跨平台框架的鸿蒙化方案,也让我更了解鸿蒙生态的包容性。对Vue开发者来说,转型不一定非得硬啃原生,用熟悉的跨平台框架也是条好路。现在uni-app、Flutter、RN这几个主流框架都在搞鸿蒙化,进度还不慢,给我们不少选择。

总结与展望

鸿蒙从V1到V2的进化,不仅解决了我们开发者的痛点,更让我看到了国产操作系统生态的快速成长。在版本的更迭中,类似的优化层出不穷。

在学习过程中,鸿蒙开发者社区的Codelabs案例、线下沙龙的专家答疑,还有其他转型开发者的经验分享,都给了我很大帮助。大家都在踊跃的分享自己的经验心得,就像当初别人帮我那样,把经验传递下去。

更值得高兴的是,uni-app、Flutter、RN这些跨平台框架的鸿蒙化也在稳步推进,给我们提供了多样化的转型路径,如果你有对应的经验,同样可以快速切入

最后想对刚入门鸿蒙的前端同行说:转型路上难免踩坑,但每解决一个问题都是成长。跟着官方文档学基础,在社区里找同伴,多动手做实战项目,你会发现鸿蒙开发并没有那么难。星光不负,我们都能在鸿蒙生态里实现自己的价值。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
收藏
回复
举报
回复
    相关推荐