1. 程式人生 > >JS 的靜態程式碼檢查工具 Flow

JS 的靜態程式碼檢查工具 Flow

無意中看到了VUE的原始碼,沒看幾行就被看蒙了。

Vue: Class<Component>
const vm: Component = this

 

想了一下,查了查相關資料,是不是ES 6的新語法。 確實不是。 是Facebook開源的JS靜態檢查工具用法。

這篇文章寫的不錯,https://segmentfault.com/a/1190000008088489      轉載一下:

 

本章的目標是提供一些Flow工具的介紹與使用建議。Flow本質上也只是個檢查工具,它並不會自動修正程式碼中的錯誤,也不會強制說你沒按照它的警告訊息修正,就不會讓你執行程式。當然,並沒有要求什麼時候一定要用這類的工具,只是這種作法可以讓你的程式碼更具強健性與提高閱讀性,也可以直接避去很多不必要的資料型別使用上的問題,這種開發方式目前在許多框架與函式庫專案,或是以JavaScript應用為主的開發團隊中都已經都是必用工具。

注: 本文內容大部份參考自Flow官網,是之前我個人部落格文章 - "Flow靜態資料型別的檢查工具,10分鐘快捷入門"的增修版本。

注: 本文內容字數過萬,去除程式碼也有數千字,筆誤在所難免,有錯再回饋留言吧。

注意

"奇異博士"說過「使用警語應該要加註在書的最前面」。所以我把注意專案先加在這裡。

  • 由於Flow還是個年輕的專案,問題仍然很多,功能也沒你想像中完整,用起來有時候會卡頓是正常的,效能仍須改善。以後使用者愈來愈多就會愈作愈好。

  • Windows平臺的支援也是幾個月前(2016.8)時的事,Flow只支援64位元的作業系統,32位元就不能用了。

  • 如果你是要學或用React或Vue.js等等,Flow是必學的。不管你要用不用,庫原始碼裡面都用了。

Flow介紹

Flow是個JavaScript的靜態型別檢查工具,由Facebook出品的開原始碼專案,問世只有一年多,是個相當年輕的專案。簡單來說,它是對比TypeScript語言的解決方式。

會有這類解決方案,起因是JavaScript是一種弱(動態)資料型別的語言,弱(動態)資料型別代表在程式碼中,變數或常量會自動依照賦值變更資料型別,而且型別種類也很少,這是直譯式指令碼語言的常見特性,但有可能是優點也是很大的缺點。優點是容易學習與使用,缺點是像開發者經常會因為賦值或傳值的型別錯誤,造成不如預期的結果。有些時候在使用框架或函式庫時,如果沒有仔細看檔案,亦或是檔案寫得不清不楚,也容易造成誤用的情況。

這個缺點在應用規模化時,會顯得更加嚴重。我們在開發團隊的協同時,一般都是用詳盡的文字說明,來降低這個問題的發生,但JS語言本身無法有效阻止這些問題。而且說明檔案也需要花時間額外編寫,其他的開發者閱讀也需要花時間。在現今預先編譯器流行的年代,像TypeScript這樣的強(靜態)類的JavaScript超集語言就開始流行,用嚴格的角度,以JavaScript語言為基底,來重新打造另一套具有強(靜態)型別特性的語言,就如同Java或C#這些語言一樣,這也是為什麼TypeScript稱自己是企業級的開發JavaScript解決方案。

注: 強(靜態)型別語言,意思是可以讓變數或常量在宣告(定義)時,就限制好只能使用哪種型別,之後在使用時如果發生型別不相符時,就會發出錯誤警告而不能編譯。但不只這些,語言本身也會拓展了更多的型別與語法。

TypeScript自然有它的市場,但它有一些明顯的問題,首先是JavaScript開發者需要再進一步學習,內容不少,也有一定陡峭的學習曲線,不過這還算小事情。重大的事情是需要把已經在使用的應用程式碼,都要整個改用TypeScript程式碼語法,才能發揮完整的功用。這對很多已經有內部程式碼庫的大型應用開發團隊而言,將會是個重大的決定,因為如果不往全面重構的路走,將無法發揮強(靜態)型別語言的最大效用。

所以許多現行的開原始碼函式庫或框架,並不會直接使用TypeScript作為程式碼的語言,另一方面當然因為是TypeScript並非普及到一定程度的語言,社群上有熱愛的粉絲也有不是那麼支援的反對者。當然,TypeScript也有它的優勢,自從TypeScript提出了DefinitelyTyped的解決方式之後,讓現有的函式庫能額外再定義出裡面使用的型別,這也是另一個可以與現有框架與庫相整合的方案,這讓許多函式庫與框架都提交定義檔案,提供了另一種選擇。另一個優勢是,TypeScript也是個活躍的開原始碼專案,發展到現在也有一段時間,算是逐漸成熟的專案。它的背後有微軟公司的支援,在最近釋出的知名的、全新打造過的Angular2框架中(由Google主導),也採用了TypeScript作為基礎的開發語言。

現在,Flow提供了另一個新的選項,它是一種強(靜態)型別的輔助檢查工具。Flow的功能是讓現有的JavaScript語法可以事先作型別的宣告(定義),在開發過程中進行自動檢查,當然在最後編譯時,一樣可以用babel工具來移除這些標記。

相較於TypeScript是另外重新制定一套語言,最後再經過編譯為JavaScript程式碼來執行。Flow走的則是非強制與非侵入性的路線。Flow的優點是易學易用,它的學習曲線沒有TypeScript來得高,雖然內容也很多,但大概一天之內學個大概,就可以漸進式地開始使用。而且因為Flow從頭到尾只是個檢查工具,並不是新的程式語言或超集語言,所以它可以與各種現有的JavaScript程式碼相容,如果你哪天不想用了,就去除掉標記就是回到原來的程式碼,沒什麼負擔。當然,Flow的功用可能無法像TypeScript這麼全面性,也不可能改變要作某些事情的語法結構。

總結來說,這兩種方式的目的是有些相似的,各自有優點也有不足之處,青菜蘿蔔各有所愛,要選擇哪一種方式就看你的選擇。

從一個小例子演示

這種型別不符的情況在程式碼中非常容易發生,例如以下的例子:

function foo(x) {
  return x + 10
}

foo('Hello!')

x這個傳參,我們在函式宣告時希望它是個數字型別,但最後使用呼叫函式時則用了字串型別。最後的結果會是什麼嗎? "Hello!10",這是因為加號(+)在JavaScript語言中,除了作為數字的加運算外,也可以當作字串的連線運算。想當然這並不是我們想要的結果。

聰明如你應該會想要用型別來當傳參的識別名,容易一眼看出傳參要的是什麼型別,像下面這樣:

function foo(number) {
  return number + 10
}

但如果在複合型別的情況,例如這個傳參的型別可以是數字型別也可以是布林型別,你又要如何寫得清楚?更不用說如果是個複雜的物件型別時,結構又該如何先確定好?另外還有函式的返回型別又該如何來寫?

利用Flow型別的定義方式,來解決這個小案例的問題,可以改寫為像下面的程式碼:

// @flow

function foo(x: number): number {
  return x + 10
}

foo('hi')

你有看到在函式的傳參,以及函式的圓括號(())後面的兩個地方,加了: number標記,這代表這個傳參會限定為數字型別,而返回值也只允許是數字型別。

當使用非數字型別的值作為傳入值時,就會出現由Flow工具發出的警告訊息,像下面這樣:

message: '[flow] string (This type is incompatible with number See also: function call)'

這訊息是說,你這函式的傳參是string(字串)型別,與你宣告的number(數字)不相符合。

如果是要允許多種型別也是很容易可以加標記的,假使這個函式可以使用布林與數字型別,但返回可以是數字或字串,就像下面這樣修改過:

// @flow

function foo(x: number | boolean): number | string {
  if (typeof x === 'number') {
    return x + 10
  }
  return 'x is boolean'
}

foo(1)
foo(true)
foo(null)  // 這一行有型別錯誤訊息

由上面這個小例子你可以想見,如果在多人協同開發某個有規模的JavaScript應用時,這種型別的輸出輸入問題就會很常遇見。如果利用Flow工具的檢查,可以避免掉許多不必要的型別問題。

真實案例

可能你會認為Flow工具只能運用在小型程式碼中,但實際上Facebook會創造出Flow工具,有很大的原因是為了React與React Native。

舉一個我最近正在研究的的函式庫程式碼中NavigationExperimental(這網址位置有可能會變,因為是直接連到原始碼裡),這裡面就預先聲明瞭所有的物件結構,像下面這樣的程式碼:

export type NavigationGestureDirection = 'horizontal' | 'vertical';

export type NavigationRoute = {
  key: string,
  title?: string
};

export type NavigationState = {
  index: number,
  routes: Array<NavigationRoute>,
};

// ...

Flow具備有像TypeScript語言中,預先定義物件型別的作用。上面程式碼的都是這個元件中預先定義的型別,這些型別可以再套用到不同的程式碼文件之中。

export type NavigationGestureDirection = 'horizontal' | 'vertical';

上面這行類似於列舉(enum)的型別,意思是說要不就是'horizontal'(水平的),要不然就'vertical'(垂直的),就這兩種字串值可使用。

export type NavigationRoute = {
  key: string,
  title?: string
};

這行裡面用了一個問號(?)定義在title屬性的後面,這代表這屬性是可選的(Optional),不過你可能會有點搞混,因為問號(?)可以放在兩個位置,見下面的例子:

export type Test = {
  titleOne?: string,
  titleTwo: ?string
}

titleOne代表的是屬性為可自定義的(可有可無),但一定是字串型別。titleTwo代表的是型別可自定義,也就是值的部份除了定義的型別,也可以是null或undefined,不過這屬性是需要的,而且你一定要給它一個值。好的,這有些太細部了,如果有用到再查手冊文件就可以。

export type NavigationState = {
  index: number,
  routes: Array<NavigationRoute>,
};

上面的程式碼可以看到,只要是宣告過的型別(type),同樣可以拿來拿在其他型別中套用,像這裡的Array<NavigationRoute>,就是使用了上面已宣告的NavigationRoute型別。它是一個數組,裡面放的成員是NavigationRoute型別,是個物件的結構。

剛已經有說過Flow工具有很大的原因是為了React與React Native所設計,因為Flow本身就內建對PropTypes的檢查功能,也可以正確檢查JSX語法,在這篇官方文件中有說明,而這在之後介紹React的文件的例子中就可以看到。

安裝與使用

Flow目前可以支援macOS、Linux(64位元)、Windows(64位元),你可以從以下的四種安裝方式選擇其中一種:

  • 直接從Flow的釋出頁面下載可執行檔案,加到計算機中的PATH(路徑),讓flow指令可以在命令列視窗訪問即可。

  • 透過npm安裝即可,可以安裝在全域性(global)或是各別專案中。下面為安裝在專案中的指令:

npm install --save-dev flow-bin
  • macOS中可以使用homebrew安裝:

brew update
brew install flow
  • 透過OCaml OPAM套裝管理程式打包與安裝,請見Flow的Github頁面

Flow簡單使用三步驟

第1步: 初始化專案

在你的專案根目錄的用命令列工具輸入下面的指令,這將會建立一個.flowconfig文件,如果這文件已經存在就不需要再進行初始化,這個設定檔一樣是可以加入自定義的設定值,請參考Advanced Configuration這裡的說明,目前有很多專案裡面都已經內附這個設定檔,例如一些React的專案:

flow init

第2步: 在程式碼文件中加入要作型別檢查的註釋

一般都在程式碼檔案的最上面一行加入,沒加Flow工具是不會進行檢查的,有兩種格式都可以:

// @flow

/* @flow */

第3步: 進行檢查

目前支援Flow工具外掛的程式碼編輯工具很多,常見的Atom, Visual Studio Code(VSC), Sublime與WebStorm都有,當有安裝搭配程式碼編輯工具的外掛時,編輯工具會輔助顯示檢查的訊息。不過有時候會有點卡頓的要等一下,因為檢查速度還不是那麼快。

或是直接用下面的命令列指令來進行檢查:

flow check

在Visual Studio Code中因為它內建TypeScript與JavaScript的檢查功能,如果要使用Flow工具來作型別檢查,需要在使用者設定中,加上下面這行設定值以免衝突:

"javascript.validate.enable": false

轉換(編譯)有Flow標記的程式碼

注: 有些腳手架就已經裝好與設定好這個babel拓展外掛,你不用再多安裝了。

在開發的最後階段要將原本有使用Flow標記,或是有型別註釋的程式碼,進行清除或轉換。轉換的工作要使用babel編譯器,這也是目前較推薦的方式。

使用babel編譯器如果以命令列工具為主,可以使用下面的指令來安裝在全域性中:

npm install -g babel-cli

再來加裝額外移除Flow標記的npm套件babel-plugin-transform-flow-strip-types在你的專案中:

npm install --save-dev babel-plugin-transform-flow-strip-types

然後建立一個.babelrc設定檔案,檔案內容如下:

{
  "plugins": [
    "transform-flow-strip-types"
  ]
}

完成設定後,之後babel在編譯時就會一併轉換Flow標記。

下面的指令則是直接把src目錄的檔案編譯到dist目錄中:

babel src -d dist

當然,babel的使用方式不是隻有上面說的這種命令列指令,你可以視專案的使用情況來進行設定。

Flow支援的資料型別

Flow用起來是的確是簡單,但裡面的內容很多,主要原因是是要看實際不同的使用情況作搭配。JavaScript裡面的原始資料型別都有支援,而在函式、物件與一些新的ES6中的類,在搭配使用時就會比較複雜,詳細的情況就請到官網文件中觀看,以下只能提供一些簡單的介紹說明。

原始資料型別

Flow支援原始資料型別,如下面的列表:

  • boolean

  • number

  • string

  • null

  • void

其中的void型別,它就是JS中的undefined型別。

這裡可能要注意的是,在JS中undefinednull的值會相等但型別不同,意思是作值相等比較時,像(undefined == null)時會為true,有時候在一些執行期間的檢查時,可能會用值相等比較而不是嚴格的相等比較,來檢查這兩個型別的值。

所有的型別都可以使用垂直線符號(|)作為聯合使用(也就是 OR 的意思),例如string | number指的是兩種型別其中一種都可使用,這是一種聯合的型別,稱為"聯合(Union)型別"。

最特別的是可選的(Optional)型別的設計,可選型別代表這個變數或常量的值有可能不存在,也就是允許它除了是某個型別的值外,也可以是nullundefined值。要使用可選型別,就是在型別名稱定義前加上問號(?),例如?string這樣,下面是一個簡單的例子:

let bar: ?string = null

字面文字(literal)型別

字面文字型別指的是以真實值作為資料型別,可用的值有三種,即數字、字串或布林值。字面文字型別搭配聯合的型別可以作為列舉(enums)來使用,例如以下的一個撲克牌的型別例子:

type Suit =
  | "Diamonds"
  | "Clubs"
  | "Hearts"
  | "Spades";

type Rank =
  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
  | "Jack"
  | "Queen"
  | "King"
  | "Ace";

type Card = {
  suit: Suit,
  rank: Rank,
}

注: type是Flow中定義類型別名(Type Alias)的關鍵字,是一種預先宣告的型別,這些宣告的標記一樣只會在開發階段中使用,最後編譯去除。

類型別名

類型別名(Type Alias)提供了可以預先定義與集中程式碼中所需要的型別,一個簡單的例子如下:

type T = Array<string>
var x: T = []
x["Hi"] = 2 //有Flow警告

類型別名(Type Alias)也可以用於複雜的應用情況,詳見Flow官網提供的Type Aliases內容。

任何的資料型別

在某一些情況可能不需要定義的太過於嚴格,或是還在開發中正在除錯時,有一種作為漸進的改善程式碼的型別。

Flow提供了兩種特殊的型別可以作為鬆散的資料型別定義:

  • any: 相當於不檢查。既是所有型別的超集(supertype),也是所有型別的子集(subtype)

  • mixed: 類似於any是所有型別的超集(supertype),但不同於any的是,它不是所有型別的子集(subtype)

mixed是一個特別的型別,中文是混合的意思,mixed算是any的"囉嗦"進化型別。mixed用在函式的輸入(傳參)與輸出(返回)時,會有不一樣的狀態,例如以下的例子會出現警告:

function foo(x: mixed): string {
  return x + '10'
}

foo('Hello!')
foo(1)

會出現警告訊息如下:

[flow] mixed (Cannot be added to string)

這原因是雖然輸入時可以用mixed,但Flow會認為函式中x的值不見得可以與string型別作相加,所以會請求你要在函式中的程式碼,要加入檢查對傳入型別在執行期間的型別檢查程式碼,例如像下面修改過才能過關:

function foo(x: mixed): string {
  if (typeof x === 'number' || typeof x === 'string') {
    return x + '10'
  }
  throw new Error('Invalid x type')
}

foo('Hello!')
foo(1)

mixed雖然"囉嗦",但它是用來漸進替換any使用的,有時候往往開發者健忘或偷懶沒作傳入值在執行期間的型別檢查,結果後面要花更多的時間才能找出錯誤點,這個型別的設計大概是為了提早預防這樣的情況。

注: 從上面的例子可以看到Flow除了對型別會作檢查外,它也會請求對某些型別需要有動態的檢查。在官方的檔案可以參考Dynamic Type Tests這個章節。

複合式的資料型別

陣列(Array)

陣列型別使用的是Array<T>,例如Array<number>,會限定陣列中的值只能使用數字的資料型別。當然你也可以加入埀直線(|)來定義允許多種型別,例如Array<number|string>

物件(Object)

物件型別會比較麻煩,主要原因是在JavaScript中所有的資料型別大概都可以算是物件,就算是基礎資料型別也有對應的包裝物件,再加上有個異常的null型別的typeof返回值也是物件。

物件型別在Flow中的使用,基本上要分作兩大部份來說明。

第一種是單指Object這個型別,Flow會判斷所有的基礎資料類不是屬於這個型別的,以下的例子全部都會有警告:

// 以下都有Flow警告

(0: Object);
("": Object);
(true: Object);
(null: Object);
(undefined: Object);

其他的複合式資料型別,除了陣列之外,都會認為是物件型別。如下面的例子:

({foo: "foo"}: Object);
(function() {}: Object);
(class {}: Object);
([]: Object); // Flow不認為陣列是屬於物件

注意: 上面有兩個特例,typeof nulltypeof []都是返回'object'。也就是說在JS的標準定義中,null陣列用typeof檢測都會返回物件型別。所以,Flow工具的檢查會與JS預設並不相同,這一點要注意。

注: typeof在Flow中有一些另外的用途,詳見Typeof的說明。

第二種方式是要定義出完整的物件的字面文字結構,像{ x1: T1; x2: T2; x3: T3;}的語法,用這個結構來檢查,以下為例子:

let object: {foo: string, bar: number} = {foo: "foo", bar: 0};

object.foo = 111; //Flow警告
object.bar = '111'; //Flow警告

函式(Function)

上面已經有看到,函式也屬於物件(Object)型別,當然也有自己的Function型別,函式的型別也可以從兩大部份來看。

第一是單指Function這個型別,可以用來定義變數或常量的型別。如下面的程式碼例子:

var anyFunction: Function = () => {};

第二指的是函式中的用法,上面已經有看到函式的輸出(返回值)與輸入(傳參)的用法例子。例如以下的例子:

function foo(x: number): number {
  return x + 10;
}

因為函式有很多種不同的使用情況,實際上可能會複雜很多,Flow工具可以支援目前最新的arrow functions、async functions與generator functions,詳見官方的這篇Functions的說明。

類(Class)

類是ES6(ES2015)中新式的特性,類目前仍然只是原型的語法糖,類本身也屬於一種物件(Object)型別。類的使用情況也可能會複雜,尤其是涉及多型與例項的情況,詳見Flow網站提供的Classes內容。

Flow的現在與未來的發展

Flow在最近的部落格中說明引入了flow-typed的函式庫定義檔("libdefs"),在這個Github儲存庫中將統一存放所有來自社群提供的函式庫定義檔案。這是一種可以讓現有的函式庫與框架,預先寫出裡面使用的型別定義。讓專案裡面有使用Flow工具與這些函式庫,就可以直接使用這些定義檔,以此結合現有的函式庫與框架來使用。這個作法是參考TypeScript的DefinitelyTyped方式。因為這還是很新的訊息(2016.10),目前加入的函式庫還沒有太多,不過React周邊的一些函式庫或元件都已經開始加入,其他常用的像underscore、backbone或lodash也已經有人在提交或維護。

Flow另一個發展會是在開發工具的自動完成功能的改進,因為如果已經能在撰寫程式碼時,就知道變數或常量的型別(靜態型別),那麼在自動完成功能中就可以更準確地給出可用的屬性或方法。這一個功能在Facebook自家的Nuclide開發工具的Flow說明頁中就有看到。Nuclide是基於Atom開發工具之上的工具,計算機硬體如果不夠力是跑不動的,而且它穩定性與執行速度都還需要再努力。這大概是未來可見到的一些新趨向。

結論

本文簡單的說明了Flow工具的功能介紹,以及其中的一些簡要的內容等等。相信看過後你已經對這個Flow工具有一些認識,以我個人學過TypeScript的經驗,相較於TypeScript的學習曲線,Flow大概是等於不用學。Flow雖然是一個很新的工具,但相當的有用,建議每個JavaScript開發者都可以試試,一開始不用學太多,大概這篇文件看完就可以開始用了。複雜的地方就再查詢官方的檔案即可。

對於每個正在使用JS開發稍具規模化的應用,或是開發開原始碼的函式庫或框架的團隊來說,讓JS具有靜態型別特性,是一個很重要而且必要的決定。以我的觀察,在網路上一直有很多的超集語言(例如TypeScript)的愛好者,會提出要全面改用TypeScript(或其他超集語言)的聲音,例如Vue.js在很早之前就有討論是不是要全面採用TypeScript的聲音。後來Vue.js只有提交TypeScript的DefinitelyTyped文件,但在2.0中則採行了Flow工具。在這篇Vue作者於知乎上發表的: Vue 2.0 為什麼選用 Flow 進行靜態程式碼檢查而不是直接使用 TypeScript?的內容中,你可以看到為何選擇Flow的理由,這可能也是整個開發團隊所認同的最後結果。作者回答的文中可以總結下面這句話:

全部換 TS(TypeScript) 成本過高,短期內並不現實。 相比之下 Flow 對於已有的 ES2015 程式碼的遷入/遷出成本都非常低 … 萬一哪天不想用 Flow 了,轉一下,就得到符合規範的 ES。

總之,Flow提供了另一個選擇,要用什麼工具就看聰明的你如何選擇了。