前端轉(zhuǎn)鴻蒙開(kāi)發(fā)幾個(gè)比較難受的地方
從剛開(kāi)始接觸鴻蒙開(kāi)發(fā)時(shí),arkTS 版本還在 API 8,眨眼之間一年多時(shí)間過(guò)去了,現(xiàn)在已經(jīng)更新了到 API 12,API 12 對(duì)應(yīng)的是 harmonyOS Next 的 beta 版本。各方面的發(fā)展和之前的版本相比,都逐漸開(kāi)始有了自己的特性,變得更加成熟。
雖然說(shuō),arkTS 是在 TypeScript 的基礎(chǔ)之上進(jìn)行的擴(kuò)展和改變,因此很多人都認(rèn)為,前端轉(zhuǎn)鴻蒙開(kāi)發(fā)的成本非常低,但是發(fā)展到 API 12,還是有一些開(kāi)發(fā)習(xí)慣逐漸與純粹的前端開(kāi)發(fā)有了非常大的區(qū)別,上手難度也沒(méi)有想象中的那么低了。
這篇文章,結(jié)合我這一年多以來(lái)的鴻蒙應(yīng)用開(kāi)發(fā)經(jīng)驗(yàn),給大家分享一下,鴻蒙開(kāi)發(fā)與前端開(kāi)發(fā)在編碼習(xí)慣上,我個(gè)人認(rèn)為幾個(gè)比較重要的差異。
一、更多的使用 class 來(lái)定義數(shù)據(jù)
在前端開(kāi)發(fā)中,大多數(shù)時(shí)候,我們更習(xí)慣于忽略 class 語(yǔ)法的存在,因?yàn)槲覀兛梢噪S意的使用 {} 來(lái)創(chuàng)建一個(gè)對(duì)象就可以開(kāi)始隨意使用了。如果需要類型,則額外使用 interface 來(lái)單獨(dú)定義即可。
interface Point {
x: number,
y: number
}
const p: Point = {
x: 1,
y: 2
}
但是在 arkTS 中,隨意使用這種方式來(lái)創(chuàng)建對(duì)象,往往意味著不確定的類型風(fēng)險(xiǎn)。
例如,arkTS 嚴(yán)格禁止在運(yùn)行的過(guò)程中刪除對(duì)象中的某一個(gè)屬性。
delete p.x
因此,當(dāng)我們習(xí)慣了在 TS 中使用 interface + {} 來(lái)定義一個(gè)對(duì)象時(shí),在 arkTS 的應(yīng)用中經(jīng)常會(huì)遇到一些不支持的報(bào)錯(cuò)。例如使用字符串來(lái)訪問(wèn)屬性值。
我們需要轉(zhuǎn)變思路,重新以面向?qū)ο蟮乃悸啡ヂ暶髅恳粋€(gè)對(duì)象。
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)
這樣處理之后,我們就可以不需要把類型和值分開(kāi)寫(xiě)。這里需要注意的是,并不是我們需要全部放棄 {} 的寫(xiě)法,而是在某些時(shí)候,需要限制 {} 用法的靈活性,從而提高底層引擎的解析性能。
這個(gè)思路的轉(zhuǎn)變對(duì)于部分前端開(kāi)發(fā)來(lái)說(shuō)可能比較困難。例如在嵌套數(shù)據(jù)時(shí),我們需要單獨(dú)為子數(shù)據(jù)聲明一個(gè) class 并 new 一個(gè)實(shí)例出來(lái)。
例如在我們需要深度監(jiān)聽(tīng)某一個(gè)數(shù)據(jù)時(shí),就必須要明確聲明 class。
// 監(jiān)聽(tīng)數(shù)據(jù)這一層
@State
private persons: Array<Person> = [
new Person('TOM', 20),
new Person('Jake', 22)
]
// 監(jiān)聽(tīng)到數(shù)組項(xiàng)元素的變化
@Observed
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
因此,總的來(lái)說(shuō),我們?cè)?arkTS 中,會(huì)更加多的使用 class 來(lái)表達(dá)數(shù)據(jù)。如果你不喜歡它的話,可能會(huì)在開(kāi)發(fā)中感覺(jué)到比較難受。
從我個(gè)人的角度來(lái)說(shuō),我也能接受這種方式,因?yàn)?class 自帶類型。但是一個(gè)比較難受的點(diǎn)時(shí),我們必須在構(gòu)造函數(shù)中明確表示創(chuàng)建函數(shù)時(shí)的初始化方式。{} 的寫(xiě)法在 arkUI 中,更多的會(huì)應(yīng)用于參數(shù)的傳遞這種場(chǎng)景。例如:
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 不會(huì)有其他動(dòng)態(tài)的操作,僅作為函數(shù)的入?yún)ⅰ?/p>
二、不支持 any、unknown
一個(gè)可能會(huì)讓部分 TypeScript 基礎(chǔ)不扎實(shí)的同學(xué)感覺(jué)到很難受的點(diǎn),就是 arkTS 非常注重類型安全。因?yàn)楹?TS 不同,arkTS 的類型會(huì)直接參與運(yùn)行。因此,在這個(gè)前提之下,arkTS 直接不支持 any,unknown 這種的類型,在聲明時(shí),我們必須明確給出具體的類型。
這樣的話,對(duì)于前端開(kāi)發(fā)來(lái)說(shuō),門檻就上來(lái)了一點(diǎn),因?yàn)檫€是有很大部分同學(xué)對(duì) TS 的使用比較依賴 any,這就比較難受了。
三、許多常用能力遭到限制
例如:
不支持展開(kāi)運(yùn)算符展開(kāi)對(duì)象。
const p0: PointPM = {x: 1, y: 2}
const p = new Point(...p0)
不支持結(jié)構(gòu)賦值。
const {x} = p0
說(shuō)實(shí)話,用慣了解構(gòu),到這里不支持了,確實(shí)很難受。不過(guò)在面向?qū)ο笾械脑O(shè)想中,也確實(shí)需要用到解構(gòu)的地方非常少。
不支持映射類型。
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean
}
四、arkTS 特性再解讀
總之,一年多的開(kāi)發(fā)經(jīng)驗(yàn)下來(lái),遇到的之前很常用,但是在鴻蒙應(yīng)用開(kāi)發(fā)中卻不支持的語(yǔ)法非常多。一篇文章肯定總結(jié)不完。但是我們可以總結(jié)出來(lái)一個(gè)非常明顯的發(fā)展特性,那就是限制 TS 的類型靈活性
由于 TS 是基于 JavaScript 發(fā)展而來(lái),雖然在類型上面做了很多努力,但是由于需要在很多場(chǎng)景兼容和支持 JS 的類型靈活性,因此 TS 到現(xiàn)在為止,已經(jīng)發(fā)展成為了一個(gè)市面上,擁有最復(fù)雜類型系統(tǒng)的編程語(yǔ)言,它一方面擁有強(qiáng)大的類型推導(dǎo),另外一方面又兼顧了 JS 的類型靈活。因此,隨著經(jīng)驗(yàn)的積累,我們很容易慢慢開(kāi)始寫(xiě)出復(fù)雜的類型體操。
和 TypeScript 相比,arkTS 的發(fā)展目標(biāo)完全不一樣。在鴻蒙應(yīng)用的開(kāi)發(fā)泛式中,arkTS 擁有獨(dú)立的編譯引擎,因此他完全不需要顧及 JS 的任何歷史包袱。因此,arkTS 可以輕裝上陣,把自己發(fā)展成為一門真正的、類型可預(yù)測(cè)的、類型安全的強(qiáng)類型語(yǔ)言。
因此,在語(yǔ)法設(shè)計(jì)上,arkTS 在 TS 的基礎(chǔ)之上做了非常多的減法,用以削弱類型靈活性。
基于這個(gè)判斷,我們可以很容易判斷出來(lái)哪些語(yǔ)法是不被支持的。例如,在普通函數(shù)中使用 this 就不會(huì)被支持。
在 js 的函數(shù)中,this 指向誰(shuí),是一個(gè)動(dòng)態(tài)的屬性,誰(shuí)調(diào)用這個(gè)函數(shù),那么在該函數(shù)上下文創(chuàng)建時(shí),this
的指向才會(huì)明確。這種不確定性,明顯違背了 arkTS 的發(fā)展目標(biāo)。
arkTS 這樣做的一個(gè)非常重要的好處,就是類型體操這個(gè)事情基本上不會(huì)有了。從另外一個(gè)角度來(lái)說(shuō),反而降低了復(fù)雜度。
五、總結(jié)
鴻蒙應(yīng)用開(kāi)發(fā)使用 arkTS 作為編程語(yǔ)言,他雖然是在 TypeScript 的基礎(chǔ)之上發(fā)展而來(lái),但是由于發(fā)展目標(biāo)不一樣,因此使用時(shí),對(duì)于前端開(kāi)發(fā)而言,實(shí)際上還是有一定的適應(yīng)難度。因?yàn)閺?qiáng)類型在開(kāi)發(fā)體驗(yàn)上肯定是有所犧牲的,當(dāng)數(shù)據(jù)類型特別復(fù)雜時(shí),處理起來(lái)要比 TS 麻煩很多。
一個(gè)最主要的區(qū)別就是,TS 不需要編譯通過(guò),我們?cè)陂_(kāi)發(fā)環(huán)境中,依然會(huì)將 TS 打包成 JS 參與到程序的運(yùn)行中去,因此,就算是你的代碼存在大量的 TS 報(bào)錯(cuò),但是你的程序有可能依然可以正常運(yùn)行而且不會(huì)出現(xiàn)一點(diǎn)問(wèn)題。
但是 arkTS 有自己的編譯器,我們寫(xiě)的代碼會(huì)直接參與運(yùn)行,因此,任何語(yǔ)法報(bào)錯(cuò)都無(wú)法通過(guò)編譯,程序也無(wú)法正常運(yùn)行。
大家不要小看這個(gè)區(qū)別。這個(gè)區(qū)別的差異會(huì)導(dǎo)致在生態(tài)上面,arkTS 的發(fā)展會(huì)被 TS 要正常很多。因?yàn)?TS 程序是可以在報(bào)錯(cuò)的情況下依然正常執(zhí)行的,于是,例如我封裝一個(gè)函數(shù)
function add(p: number) {
return p + 1
}
此時(shí),當(dāng)我不按照類型約定傳入 number,而是直接傳入非法字符串。此時(shí) TS 肯定會(huì)報(bào)錯(cuò),但是在一些不規(guī)范不嚴(yán)謹(jǐn)?shù)氖褂谜呖磥?lái),這種報(bào)錯(cuò)是可以被接受的,他可能就不會(huì)去在意這個(gè)報(bào)錯(cuò)。
也就是說(shuō),TS 報(bào)錯(cuò)失去了他應(yīng)該具備的威懾力。
因此,這個(gè)時(shí)候就會(huì)發(fā)生一種很難受的事情,那就是作為封裝者預(yù)知了這種風(fēng)險(xiǎn):有的不規(guī)范的使用者無(wú)視 TS 的報(bào)錯(cuò)繼續(xù)使用,就會(huì)導(dǎo)致潛在的 bug 出現(xiàn)。所以封裝者就需要在封裝 add 函數(shù)時(shí),對(duì)其他的意外類型做一個(gè)兜底,從而支持更多的類型傳入。讓這個(gè)函數(shù)的封裝平白變得更加復(fù)雜。
因此我們作為使用者有的時(shí)候會(huì)發(fā)現(xiàn),一些開(kāi)源庫(kù)的類型入?yún)榱酥С指嗟念愋投兊锰貏e復(fù)雜,很多都讀不懂。從而又從另外一個(gè)角度加劇了 TS 的使用成本。普通開(kāi)發(fā)者想要解決掉所有的 TS 類型報(bào)錯(cuò)可能是一件工作量非常大的事情,從而進(jìn)一步加劇了他們接受項(xiàng)目代碼中存在報(bào)錯(cuò),陷入一個(gè)惡性循環(huán)。
從這個(gè)角度來(lái)說(shuō),arkTS 的生態(tài)發(fā)展會(huì)更加正常一些。相關(guān)的使用成本也會(huì)比 TS 要低很多。
arkTS 對(duì)前端開(kāi)發(fā)的啟示
實(shí)際上,在團(tuán)隊(duì)范圍的可控范圍以內(nèi),不管是作為個(gè)人還是作為項(xiàng)目 Leader,我們都可以借鑒 arkTS 的這個(gè)強(qiáng)類型的思路去制定我們的團(tuán)隊(duì)規(guī)范,從團(tuán)隊(duì)規(guī)范的角度,主動(dòng)犧牲掉一些 TS 的能力,從而降低 TS 的使用難度和推廣難度。