用Flow編寫更好的js程式碼
關於本文:
本文發表於前端早讀課【第897期】
你是否經常在debug那些簡單可避免的bug?可能你給函式傳參的時候搞錯了引數的順序,或者本來應該傳個Number型別的引數,你傳了一個String型別?JavaScript的弱型別是這一類bug的罪魁禍首,靜態型別語言中不存在此類bug。
Flow就是JavaScript的靜態型別檢查工具,由Facebook團隊於2014年的Scale Conference上首次提出。該庫的目標在於檢查JavaScript中的型別錯誤,開發者通常不需要修改程式碼即可使用,故使用成本很低。同時,它也提供額外語法支援,使得開發者能更大程度地發揮Flow的作用。
本文將介紹Flow及其主特性。下面分別從如何安裝設定,如何新增型別註釋,如何在執行時自動去掉註釋等方面來介紹。
安裝
目前Flow相容的作業系統有Mac OS X,Linux(64位),Windows(64位)。最簡單的安裝方式是通過npm:
npm install --save-dev flow-bin
然後在package.json
檔案中的scripts
項新增:
"scripts": {
"flow": "flow"
}
完成之後,就可以開始探索它的特性了。
啟動
首先需要在專案的根目錄下建立一個.flowconfig
配置檔案。我們可以通過以下命令建立一個空配置檔案:
npm run flow init
完成設定之後,在終端輸入以下命令可以在你的專案根目錄以及任何子目錄資料夾下進行專門的型別檢查:
npm run flow check
但是,這並不是最高效的使用方式,因為每次Flow都會重新檢查整個專案的所有檔案。開發過程中,推薦啟動Flow服務。
Flow服務的工作方式是增量檢查,也就是說它只檢查變化的部分。在終端輸入以下命令來啟動Flow服務:
npm run flow
首次執行該命令時,服務啟動並且顯示最初型別檢查結果。這保證了Flow更高效的增量式工作流。然後接下來每次想要知道檢測結果,只要輸入flow
命令即可。開發結束之後,輸入npm run flow stop
Flow的型別檢查是可選的,並不需要一次性檢查所有程式碼。你可以選擇你想要檢查的檔案,只要在對應的JavaScript檔案最前面加上帶有@flow
標識的註釋即可:
/*@flow*/
當你想在已有專案中加入Flow的時候,該特性特別有幫助。因為你可以一一選擇並檢測你要的檔案,然後修正錯誤。
型別推斷
通常,型別檢查分為以下兩種方式:
- 通過註釋:事先註釋好我們期待的型別,Flow就會基於這些註釋來評估
- 通過程式碼推斷:通過變數的使用上下文來推斷出變數型別,然後根據這些推斷來檢查型別
第一種方式,我們需要額外編寫只在開發階段起作用的程式碼,最後在程式碼編譯打包的階段被剔除。顯然,這種額外新增型別註釋的方式增加了工作量。
第二種方式,不需要任何程式碼修改即可進行型別檢查,最小化開發者的工作量。它不會強制你改變開發習慣,因為它會自動推斷出變數的型別。這就是所謂的型別推斷,Flow最重要的特性之一。
我們來通過一個例子來說明這個特性:
/*@flow*/
function foo(x) {
return x.split(' ');
}
foo(34);
當你在終端執行npm run flow
命令的時候,上述程式碼會報錯,因為函式foo()
的期待引數是字串,而我們輸入了數字。
錯誤資訊類似如下:
index.js:4
4: return x.split(' ');
^^^^^ property `split`. Property not found in
4: return x.split(' ');
^ Number
上述資訊清楚地指出了出錯位置和錯誤原因。我們只要將引數變成字串,即可修正錯誤,如下所示:
/*@flow*/
function foo(x) {
return x.split(' ');
};
foo('Hello World!');
如上所述,以上程式碼不會報任何錯。該例想說明的是,因為split()
方法只適用於string
型別的變數,所以x
應該是string
,這就是型別推斷。
空型別
Flow處理null
的方式與其他型別庫不同。它不會忽略null
,這樣可以防止了因給變數傳了null
而導致程式崩潰的錯誤。
思考以下程式碼:
/*@flow*/
function stringLength (str) {
return str.length;
}
var length = stringLength(null);
Flow會報錯。為了防止出錯,我們需要單獨處理null
。
/*@flow*/
function stringLength (str) {
if (str !== null) {
return str.length;
}
return 0;
}
var length = stringLength(null);
程式碼中我們引入對null
的檢查,確保程式碼能在任何情況下都正常且正確執行。上述程式碼可以通過Flow的型別檢查。
型別註釋
如上所述,型別推斷是Flow最有用的特性之一,不需要編寫型別註釋就能獲取有用的反饋。但在某些特定的場景下,新增型別註釋可以提供更好更明確的檢查依據。
考慮以下程式碼:
/*@flow*/
function foo(x, y){
return x + y;
}
foo('Hello', 42);
Flow檢查上述程式碼時檢查不出任何錯誤,因為+
即可以用在字串上,也可以用在數字上,我們並沒有明確指出add()
的引數必須為數字。
在這種情況下,我們可以藉助型別註釋來指明期望的型別。型別註釋是以冒號:
開頭,可以在函式引數,返回值,變數宣告中使用。
如果我們在上段程式碼中新增型別註釋,就會變成如下:
/*@flow*/
function foo(x : number, y : number) : number {
return x + y;
}
foo('Hello', 42);
現在Flow就能檢查出錯誤,因為函式引數的期待型別為數字,而我們提供了字串。
Flow報錯資訊類似如下:
index.js:7
7: foo('Hello', 42);
^^^^^^^ string. This type is incompatible with the expected param type of
3: function foo(x : number, y : number) : number{
^^^^^^ number
如果傳入的引數是數字,就不會有錯誤。型別註釋在大型複雜的JavaScript檔案中也很有用,它能保證程式碼按照預期進行。
記住上一個例子,我們來看看Flow能支援的其他更多型別註釋。
函式
/*@flow*/
/*--------- Type annotating a function --------*/
function add(x : number, y : number) : number {
return x + y;
}
add(3, 4);
上述程式碼展示了變數型別註釋以及函式型別註釋。函式add()
的引數,以及函式的返回值,期待型別為數字。如果傳入其他型別引數,Flow就會檢測到錯誤。
陣列
/*-------- Type annotating an array ----------*/
var foo : Array<number> = [1,2,3];
陣列型別註釋的格式是Array<T>
,T
表示陣列中每項的資料型別。在上述程式碼中,foo
是每項均為數字的陣列。
類
下面展示了類和物件的型別註釋模型。唯一需要注意的是,可以在兩個型別之間使用或邏輯,用|
來間隔。變數bar1
添加了必須為Bar
類的型別註釋。
/*-------- Type annotating a Class ---------*/
class Bar{
x:string; // x should be string
y:string | number; // y can be either a string or a number
constructor(x,y){
this.x=x;
this.y=y;
}
}
var bar1 : Bar = new Bar("hello",4);
物件字面量
物件的型別註釋類似於類,指定物件屬性的型別。
/*--------- Type annonating an object ---------*/
var obj : {a : string, b : number, c: Array<string>, d : Bar} = {
a : "hello",
b : 42,
c : ["hello", "world"],
d : new Bar("hello",3)
}
Null
若想任意型別T
可以為null
或者undefined
,只需類似如下寫成?T
的格式即可。
/*@flow*/
var foo : ?string = null;
此時,foo
可以為字串,也可以為null
。
目前我們只對Flow的型別註釋做了很淺的探索。一旦你習慣了使用這些基本型別,建議在Flow官網上的型別文件深入瞭解所有的型別。
庫定義
我們經常需要引入第三方庫,Flow檢查時就會丟擲錯誤。但這並不是我們期待的錯誤。
慶幸的是,我們不需要修改庫原始碼去防止這些報錯。我們只需建立一個庫定義(libdef)。libdef是包含第三方庫宣告的JS檔案簡稱。
觀察下面的例子:
/* @flow */
var users = [
{ name: 'John', designation: 'developer' },
{ name: 'Doe', designation: 'designer' }
];
function getDeveloper() {
return _.findWhere(users, {designation: 'developer'});
}
Flow會檢查出以下錯誤:
interfaces/app.js:9
9: return _.findWhere(users, {designation: 'developer'});
^ identifier `_`. Could not resolve name
由於Flow並不認識_
,所以會報錯。要解決這個問題,我們需要引入Underscore的庫定義。
使用flow-typed
flow-typed倉庫包含了眾多流行的第三方庫的libdef。只需在專案根目錄下建立一個名為flow-typed
的資料夾,並且下載相關的定義檔案即可。
為了進一步簡化,可以用npm的命令列方式一鍵獲取和安裝libdef檔案:
npm install -g flow-typed
安裝成功之後, 執行flow-typed install
來檢查package.json
檔案,並且下載所有專案中用到的第三方庫的libdef。
自定義libdef
如果你用的庫並不在flow-typed倉庫,你可以建立你自己的libdef。本文不會細談自定義libdef,因為很少會有人遇到,感興趣可以檢視此文件。
剔除型別註釋
由於額外新增的型別註釋不是正確的JavaScript語法,打包編譯的時候需要在原始碼中剔除。可以通過flow-remove-types來剔除,或者如果你已經用Babel來轉譯JS,你可以使用Babel preset來移除。我們只討論第一種方法。
首先需要安裝flow-remove-types作為專案依賴庫:
npm install --save-dev flow-remove-types
然後在package.json
檔案中新增另一個script
入口:
"scripts": {
"flow": "flow",
"build": "flow-remove-types src/ -D dest/",
}
上述命令將剔除src
資料夾下的所有型別註釋,在dist
資料夾中儲存編譯後的版本。編譯後的檔案就是普通的能運行於瀏覽器的JavaScript檔案。
結語
本文討論了Flow各種各樣的型別檢查特性,展示了Flow如何幫助我們捕獲錯誤提高程式碼質量。我們也看到了如何用可選的方式去逐個檢查JS檔案,如何做型別推斷。
你覺得Flow這種對JavaScript進行靜態型別檢查的方式如何?頗有用處,還是畫蛇添足?看了本文後,是不是躍躍欲試了?歡迎分享你的想法,說說你的疑問。