前端转鸿蒙开发几个比较难受的地方

文摘   2024-10-22 08:51   广东  

从刚开始接触鸿蒙开发时,arkTS 版本还在 API 8,眨眼之间一年多时间过去了,现在已经更新了到 API 12,API 12 对应的是 harmonyOS Next 的 beta 版本。各方面的发展和之前的版本相比,都逐渐开始有了自己的特性,变得更加成熟。

虽然说,arkTS 是在 TypeScript 的基础之上进行的扩展和改变,因此很多人都认为,前端转鸿蒙开发的成本非常低,但是发展到 API 12,还是有一些开发习惯逐渐与纯粹的前端开发有了非常大的区别,上手难度也没有想象中的那么低了。

这篇文章,结合我这一年多以来的鸿蒙应用开发经验,给大家分享一下,鸿蒙开发与前端开发在编码习惯上,我个人认为几个比较重要的差异。

一、更多的使用 class 来定义数据

在前端开发中,大多数时候,我们更习惯于忽略 class 语法的存在,因为我们可以随意的使用 {} 来创建一个对象就可以开始随意使用了。如果需要类型,则额外使用 interface 来单独定义即可

interface Point {
  x: number,
  y: number
}

const p: Point = {
  x1,
  y2
}

但是在 arkTS 中,随意使用这种方式来创建对象,往往意味着不确定的类型风险。

例如,arkTS 严格禁止在运行的过程中删除对象中的某一个属性

delete p.x

因此,当我们习惯了在 TS 中使用 interface + {} 来定义一个对象时,在 arkTS 的应用中经常会遇到一些不支持的报错。例如使用字符串来访问属性值。

我们需要转变思路,重新以面向对象的思路去声明每一个对象。

class Point<T = number> {
  x: T;
  y: T;
  constructor(x: T, y: T) {
    this.x = x
    this.y = y
  }
}
const p = new Point(1020)

这样处理之后,我们就可以不需要把类型和值分开写。这里需要注意的是,并不是我们需要全部放弃 {} 的写法,而是在某些时候,需要限制 {} 用法的灵活性,从而提高底层引擎的解析性能。

这个思路的转变对于部分前端开发来说可能比较困难。例如在嵌套数据时,我们需要单独为子数据声明一个 class 并 new 一个实例出来。

例如在我们需要深度监听某一个数据时,就必须要明确声明 class

// 监听数据这一层
@State
private persons: Array<Person> = [
  new Person('TOM'20),
  new Person('Jake'22)
]
// 监听到数组项元素的变化
@Observed
class Person {
  name: string
  age: number

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

因此,总的来说,我们在 arkTS 中,会更加多的使用 class 来表达数据。如果你不喜欢它的话,可能会在开发中感觉到比较难受。

从我个人的角度来说,我也能接受这种方式,因为 class 自带类型。但是一个比较难受的点时,我们必须在构造函数中明确表示创建函数时的初始化方式。{} 的写法在 arkUI 中,更多的会应用于参数的传递这种场景。例如

interface PointPM<T = number> {
  x: T;
  y: T;
}

class Point<T = number> {
  x: T;
  y: T;
  constructor(params: PointPM<T>) {
    this.x = params.x
    this.y = params.y
  }
}

const p = new Point({x: 1, y: 2})

通常情况下,这里定义的 PointPM 不会有其他动态的操作,仅作为函数的入参

二、不支持 any、unknown

一个可能会让部分 TypeScript 基础不扎实的同学感觉到很难受的点,就是 arkTS 非常注重类型安全。因为和 TS 不同,arkTS 的类型会直接参与运行。因此,在这个前提之下,arkTS 直接不支持 anyunknown 这种的类型,在声明时,我们必须明确给出具体的类型。

这样的话,对于前端开发来说,门槛就上来了一点,因为还是有很大部分同学对 TS 的使用比较依赖 any,这就比较难受了。

三、许多常用能力遭到限制

例如,

不支持展开运算符展开对象

const p0: PointPM = {x1y2}

const p = new Point(...p0)

不支持结构赋值

const {x} = p0

说实话,用惯了解构,到这里不支持了,确实很难受。不过在面向对象中的设想中,也确实需要用到解构的地方非常少。

不支持映射类型

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean
}

四、arkTS 特性再解读

总之,一年多的开发经验下来,遇到的之前很常用,但是在鸿蒙应用开发中却不支持的语法非常多。一篇文章肯定总结不完。但是我们可以总结出来一个非常明显的发展特性,那就是限制 TS 的类型灵活性

由于 TS 是基于 JavaScript 发展而来,虽然在类型上面做了很多努力,但是由于需要在很多场景兼容和支持 JS 的类型灵活性,因此 TS 到现在为止,已经发展成为了一个市面上,拥有最复杂类型系统的编程语言,它一方面拥有强大的类型推导,另外一方面又兼顾了 JS 的类型灵活。因此,随着经验的积累,我们很容易慢慢开始写出复杂的类型体操。

和 TypeScript 相比,arkTS 的发展目标完全不一样。在鸿蒙应用的开发泛式中,arkTS 拥有独立的编译引擎,因此他完全不需要顾及 JS 的任何历史包袱。因此,arkTS 可以轻装上阵,把自己发展成为一门真正的、类型可预测的、类型安全的强类型语言。

因此,在语法设计上,arkTS 在 TS 的基础之上做了非常多的减法,用以削弱类型灵活性。

基于这个判断,我们可以很容易判断出来哪些语法是不被支持的。例如,在普通函数中使用 this 就不会被支持。

在 js 的函数中,this 指向谁,是一个动态的属性,谁调用这个函数,那么在该函数上下文创建时,this 的指向才会明确。这种不确定性,明显违背了 arkTS 的发展目标。

arkTS 这样做的一个非常重要的好处,就是类型体操这个事情基本上不会有了。从另外一个角度来说,反而降低了复杂度。

五、总结

鸿蒙应用开发使用 arkTS 作为编程语言,他虽然是在 TypeScript 的基础之上发展而来,但是由于发展目标不一样,因此使用时,对于前端开发而言,实际上还是有一定的适应难度。因为强类型在开发体验上肯定是有所牺牲的,当数据类型特别复杂时,处理起来要比 TS 麻烦很多。

一个最主要的区别就是,TS 不需要编译通过,我们在开发环境中,依然会将 TS 打包成 JS 参与到程序的运行中去,因此,就算是你的代码存在大量的 TS 报错,但是你的程序有可能依然可以正常运行而且不会出现一点问题。

但是 arkTS 有自己的编译器,我们写的代码会直接参与运行,因此,任何语法报错都无法通过编译,程序也无法正常运行。

大家不要小看这个区别。这个区别的差异会导致在生态上面,arkTS 的发展会被 TS 要正常很多。因为 TS 程序是可以在报错的情况下依然正常执行的,于是,例如我封装一个函数

function add(p: number{
  return p + 1
}

此时,当我不按照类型约定传入 number,而是直接传入非法字符串。此时 TS 肯定会报错,但是在一些不规范不严谨的使用者看来,这种报错是可以被接受的,他可能就不会去在意这个报错。

也就是说,TS 报错失去了他应该具备的威慑力。

因此,这个时候就会发生一种很难受的事情,那就是作为封装者预知了这种风险:有的不规范的使用者无视 TS 的报错继续使用,就会导致潜在的 bug 出现。所以封装者就需要在封装 add 函数时,对其他的意外类型做一个兜底,从而支持更多的类型传入。让这个函数的封装平白变得更加复杂。

因此我们作为使用者有的时候会发现,一些开源库的类型入参为了支持更多的类型而变得特别复杂,很多都读不懂。从而又从另外一个角度加剧了 TS 的使用成本。普通开发者想要解决掉所有的 TS 类型报错可能是一件工作量非常大的事情,从而进一步加剧了他们接受项目代码中存在报错,陷入一个恶性循环。

从这个角度来说,arkTS 的生态发展会更加正常一些。相关的使用成本也会比 TS 要低很多。

arkTS 对前端开发的启示

实际上,在团队范围的可控范围以内,不管是作为个人还是作为项目 Leader,我们都可以借鉴 arkTS 的这个强类型的思路去制定我们的团队规范,从团队规范的角度,主动牺牲掉一些 TS 的能力,从而降低 TS 的使用难度和推广难度。

我的付费专栏

成为 React 高手,推荐 《React 哲学》

大量可演示案例,沉浸式学习,推荐《React 19 全解》

鸿蒙应用开发,推荐学习 《鸿蒙应用开发速成法》

这波能反杀
往者不可谏,来者犹可追
 最新文章