从刚开始接触鸿蒙开发时,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 = {
x: 1,
y: 2
}
但是在 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(10, 20)
这样处理之后,我们就可以不需要把类型和值分开写。这里需要注意的是,并不是我们需要全部放弃 {} 的写法,而是在某些时候,需要限制 {} 用法的灵活性,从而提高底层引擎的解析性能。
这个思路的转变对于部分前端开发来说可能比较困难。例如在嵌套数据时,我们需要单独为子数据声明一个 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 直接不支持 any,unknown 这种的类型,在声明时,我们必须明确给出具体的类型。
这样的话,对于前端开发来说,门槛就上来了一点,因为还是有很大部分同学对 TS 的使用比较依赖 any,这就比较难受了。
三、许多常用能力遭到限制
例如:
不支持展开运算符展开对象。
const p0: PointPM = {x: 1, y: 2}
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 的使用难度和推广难度。