1. 什么是Flow?
Flow 是javascript代碼的靜態類型檢查工具。它是Facebook的開源項目(https://github.com/facebook/flow),Vue.js(v2.6.10的源碼使用了Flow做了靜態類型檢查。因此我們現在先來了解下Flow的基本知識,有助於我們分析源碼。
2. 為什么要用Flow?
javascript是弱類型語言,弱類型體現在代碼中的變量會根據上下文環境自動改變的數據類型。那么這種弱類型有優點也有缺點,優點是我們容易學習和使用,缺點是:開發者經常因為賦值或傳值導致類型錯誤。造成一些和預期不一樣的結果。在代碼編譯的時候可能不會報錯,但是在運行階段就可能會出現各種奇怪的bug。因此在大型項目中我們有必要使用Flow來做代碼靜態類型檢查。
下面我們從一個簡單的demo說起。比如如下代碼:
function add (x) { return x + 10; } var a = add('Hello!'); console.log(a); // 輸出:Hello!10
如上代碼,x這個參數,我們在add函數聲明的時候,其實我們希望該參數是一個數字類型,但是在我們代碼調用的時候則使用了字符串類型。導致最后的結果為 "Hello!10"; 為什么會出現這種結果呢?那是因為 加號(+)在javascript語言中,它既有作為數字的加運算符外,還可以作為字符串的拼接操作。
因此為了解決類型檢查,我們可以使用Flow來解決。下面我們來介紹下 如何在我們項目中使用Flow。
3. 開始一個新的 Flow 項目
首先我們創建一個名為 v-project 項目:
$ mkdir -p v-project
$ cd v-project
接着,添加Flow, 執行如下命令:
$ npm install --save-dev flow-bin
如上安裝完成后,我們需要在要執行靜態檢查文件的根目錄下 執行一下命令:flow init.執行完成后,我們會發現我們根目錄下多了一個 .flowconfig 文件。該文件的作用是: 告訴Flow在這個目錄下文件開始檢測。我們可以在該 .flowconfig 配置文件內可以進行一些高級配置,比如說僅包含一些目錄, 或 忽略一些目錄進行檢測等操作。
現在在我們的項目下會有如下目錄結構:
|--- v-project | |--- node_modules | |--- .flowconfig | |--- package.json
package.json 文件基本代碼如下:
{ "name": "v-project", "devDependencies": { "flow-bin": "^0.106.3" } }
現在我們在 v-project根目錄下新建 index.js 文件,代碼如下:
// @flow var str = "hello world!"; console.log(str);
接着我們在項目的根目錄下運行如下命令,如果一切正常的話,會提示如下信息:
$ flow check
Found 0 errors
但是如果我們把代碼改成如下所示; 它就會報錯了,index.js 代碼改成如下:
// @flow var str != "hello world!"; console.log(str);
執行結果如下圖所示:
注意第一行,我們添加了 // @flow, 是用來告訴 Flow,你需要檢查我這個文件。如果不加這個注釋,Flow就不會檢查該文件了。
當然,我們可以強制 Flow 來檢測所有的文件,不管文件有沒有 @flow 注釋,我們只需要在命令行中帶上 --all 參數就行了,如下所示:
$ flow check --all
但是這個命令,我們一般情況下還是需要慎用的,當我們在一個大型項目中,該項目假如引入了很多第三方庫,那么檢測器可能會找到很多我們不想要的錯誤。
注意:flow check 這個命令雖然是可行,但不是最高效的用法,該命令會讓flow每次都在項目下檢查所有文件一遍。
4. 理解類型注釋
Javascript是一種弱類型語言,在語法上沒有規定明確的表示類型,比如如下JS代碼運行是正常的。
function add(num1, num2) { return num1 + num2; } var result = add(1, '2'); console.log(result); // 輸出:12
如上代碼,輸出的 result 的值為 '12'; 但是有可能這並不是我們想要的,我們有可能想要兩個數字相加得出結果,但是編寫代碼的時候,一不小心把參數寫成字符串去了。導致預期的結果不一樣。
Flow 可以通過靜態分析和類型注釋,來幫我們解決類似的問題,讓我們的代碼更加符合預期。
類型注釋一般都是以 : 開頭的,可以使用在方法參數中、變量聲明及返回值中,比如使用類型注釋更改上面的代碼如下:
// @flow function add(num1:number, num2:number) :number { return num1 + num2; } var result = add(1, '2');
執行命令后結果如下所示:
$ flow check Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ index.js:8:21 Cannot call add with '2' bound to num2 because string [1] is incompatible with number [2]. [2] 4│ function add(num1:number, num2:number) :number { 5│ return num1 + num2; 6│ } 7│ [1] 8│ var result = add(1, '2'); 9│ console.log(result); Found 1 error
如上代碼,num1:number 和 num2:number 的含義是:num1 和 num2 傳遞的參數都為數字類型的,:number {} 中的 :number的含義是:希望返回結果也是數字類型。上面如果我們把 '2' 改成 數字 2 就正常了。
類型注釋在大型復雜的項目文件中很有用,它能保證代碼按照預期進行。
下面我們來看看Flow能支持的其他更多類型注釋,分別為如下:
函數
// @flow function add(num1:number, num2:number) :number { return num1 + num2; } add(1, 2);
數組
// @flow var foo : Array<number> = [1, 2, 3];
如上數組類型注釋的格式是 Array<number>,number的含義表示數組中的每項數據類型都為 number(數字) 類型。
類
下面是類和對象的注釋模型,在兩個類型之前我們可以使用 或(|) 邏輯,變量foo添加了必須為Foo類的類型注釋。
// @flow class Foo { x: string; // x 必須為字符串類型 y: string | number; // y 可以為字符串或數字類型 constructor(x, y) { this.x = x; this.y = y; } } // 類實例化 var foo : Foo = new Foo("hello", 112);
對象字面量
對象字面量需要指定對象屬性的類型即可。如下演示:
// @flow class Foo { x: string; // x 必須為字符串類型 y: string | number; // y 可以為字符串或數字類型 constructor(x, y) { this.x = x; this.y = y; } } var obj : {a : string, b : number, c : Array<string>, d : Foo} = { a : "kongzhi", b : 1, c : ["kongzhi111", "kongzhi222"], d : new Foo("hello", 1) }
Null
假如我們想任意類型 T 可以為null或undefined的話,我們只需要類似如下寫成 ?T 的格式的即可。
// @flow var foo : ?string = null;
如上代碼,foo 可以為字符串,也可以為null。
5. 理解模塊界限
在跨模塊使用的時候,Flow需要明確注釋,為了保證Flow在各自模塊內的獨立檢測,提高性能,因此我們需要在每個模塊中有自己的Flow.
在我們的 v-project 項目目錄中新建一個 module.js, 整個目錄結構假如變為如下:
|--- v-project | |--- node_modules | |--- index.js | |--- module.js | |--- .flowconfig | |--- package.json
module.js 代碼如下:
/* * module.js * @flow */ function module(str: string) : number { return str.length; } module.exports = module;
index.js 代碼如下:
/* * index.js * @flow */ var module = require('./module'); var result = module(1122);
在命令行中運行發現報錯,如下提示:
$ flow check Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ index.js:8:21 Cannot call module with 1122 bound to str because number [1] is incompatible with string [2]. index.js 5│ */ 6│ 7│ var module = require('./module'); [1] 8│ var result = module(1122); 9│ module.js [2] 7│ function module(str: string) : number { Found 1 error
如果我們把 index.js 代碼中的 module(1122); 改成 module('1122'); 字符串這樣的,再運行下就不會報錯了。
6. 使用Flow檢測第三方庫模塊
大多數javascript應用程序都依賴於第三方庫。如果在我們的項目代碼中引用外部資源時,我們要如何使用Flow呢?慶幸的是,我們不需要修改第三方庫源碼,我們只需要創建一個庫定義 (libdef). libdef是包含第三方庫聲明的JS文件的簡稱。
下面我們來演示下這個過程,假如我們選擇了 lodash 庫。下面我們的 index.js 代碼中使用了該庫。如下代碼所示:
// @flow import _ from 'lodash';
然后我們在命令行運行的時候 會報錯如下信息:
$ flow check Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ index.js:4:15 Cannot resolve module lodash. 1│ 2│ // @flow 3│ 4│ import _ from 'lodash'; 5│ 6│ 7│ Found 1 error
這是因為flow 找不到 lodash 模塊,因此我們這個時候需要去下載 lodash 的模塊文件,我們可以使用 flow-typed 來管理這些第三方庫的定義文件。
1. flow-typed
flow-typed 倉庫包含了很多流行的第三方庫的定義文件。 flow-typed 可以看github代碼(https://github.com/flow-typed/flow-typed)
我們使用npm命令行方式全局安裝下 flow-typed, 如下命令:
npm install -g flow-typed
安裝成功后,我們需要查找該庫,是否存在我們的 flow-typed 倉庫中,如下命令查找下:
flow-typed search lodash;
運行命令完成后,我們就可以看到有如下版本的了。
Found definitions: ╔═══════════╤═════════════════╤══════════════════════╗ ║ Name │ Package Version │ Flow Version ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash-es │ v4.x.x │ >=v0.104.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash-es │ v4.x.x │ >=v0.63.x <=v0.103.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.47.x <=v0.54.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.38.x <=v0.46.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.55.x <=v0.62.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.63.x <=v0.103.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.104.x ║ ╟───────────┼─────────────────┼──────────────────────╢ ║ lodash │ v4.x.x │ >=v0.28.x <=v0.37.x ║ ╚═══════════╧═════════════════╧══════════════════════╝
現在我們可以選擇一個版本進行安裝,我們需要在我們的項目根目錄下運行如下命令:
flow-typed install lodash@4.x.x;
文件下載完成后,會自動在我們的項目根目錄下 新建一個 flow-typed/npm 文件夾,在該文件夾下有一個 lodash_v4.x.x.js文件。
那么這個時候,我們再運行 flow check; 命令就不會報錯了。
2. 自定義libdef
如果我們用的庫在flow-typed倉庫搜索不到怎么辦?比如我引入了一個在flow-typed管理庫中找不到的庫,比如該庫叫 "kongzhi" 庫(但是在npm包中確實有該庫),該庫下有對外暴露的方法,比如叫 findWhere 這樣的方法,我們在 index.js 中調用了該方法,並且該庫的假如別名對外叫_; 如下代碼:
// @flow var obj = [ { title: 'kongzhi1111', flag: true }, { title: 'kongzhi2222', flag: false } ]; function test() { return _.findWhere(obj, {flag: true}); }
因此 運行 flow check; 命令后,會報如下錯誤:
$ flow check Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ index.js:10:10 Cannot resolve name _. 7│ ]; 8│ 9│ function test() { 10│ return _.findWhere(obj, {flag: true}); 11│ } 12│ 13│ Found 1 error
如上代碼報錯,那是因為Flow不認識全局變量 _. ,要解決這個問題,我們需要為我們的 kongzhi庫創建一個接口文件。
因此我們需要在我們項目中根目錄下新建一個叫 "interfaces" 文件夾,在該文件夾下新建一個 'kongzhi.js' 文件,在該文件下代碼如下所示:
declare class Kongzhi { findWhere<T>(list: Array<T>, properties: {}) : T; } declare var _: Kongzhi;
然后我們需要在我們的 根目錄中的 .flowconfig 文件中配置 [libs] 為 interfaces/ 了, 如下所示:
[ignore] [include] [libs] interfaces/ [lints] [options] [strict]
如上,在 .flowconfig 中默認有如上配置項。如上配置后,Flow就會查找 interfaces/目錄下的所有 .js 文件作為接口定義。
有了該接口文件,我們在命令中再次運行下 就不會報錯了。如下運行結果:
$ flow check
Found 0 errors
現在整個項目的目錄結構變為如下:
|--- v-project | |--- flow-typed | | |--- npm | | | |--- lodash_v4.x.x.js | |--- interfaces | | |--- kongzhi.js | |--- node_modules | |--- .flowconfig | |--- index.js | |--- module.js | |--- package.json
更多的自定義 libdef,請查看(https://flow.org/en/docs/libdefs/creation/)。
7. 剔除類型注釋
類型注釋不是我們JS規范的一部分,因此我們需要移除它,這里我們使用Babel來移除它。
比如我們的 module.js 中的代碼,如下代碼:
function module(str: string) : number { return str.length; }
str: string 和 : number 它不是我們的JS規范中的一部分,因此不管我們在瀏覽器端還是在nodeJS中運行都會報錯的。
為了簡單的來測試下,我們在node.js中運行測試下,如下:
$ node module.js function module(str: string) : number { ^ SyntaxError: Unexpected token : at new Script (vm.js:80:7) at createScript (vm.js:264:10) at Object.runInThisContext (vm.js:316:10) at Module._compile (internal/modules/cjs/loader.js:670:28) at Object.Module._extensions..js (internal/modules/cjs/loader.js:718:10) at Module.load (internal/modules/cjs/loader.js:605:32) at tryModuleLoad (internal/modules/cjs/loader.js:544:12) at Function.Module._load (internal/modules/cjs/loader.js:536:3) at Function.Module.runMain (internal/modules/cjs/loader.js:760:12) at startup (internal/bootstrap/node.js:303:19)
如上可以看到,會報錯,在編程時,我們希望使用Flow對類型檢查,但是在代碼運行的時候,我們需要把所有的類型約束要去掉。因此我們需要使用Babel這個工具來幫我們去掉。
因此首先我們要安裝babel相關的庫,安裝命令如下:
npm install --save-dev babel-cli babel-preset-flow
babel-cli: 只要我們要安裝babel的話,那么babel-cli庫都需要安裝的。
babel-preset-flow: 該庫目的是去除類型。
安裝完成后,我們的 package.json 變成如下:
{ "name": "v-project", "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-flow": "^6.23.0", "flow-bin": "^0.106.3" }, "scripts": { "flow": "flow check" } }
為了項目結構合理及方便,我們把上面的 index.js 和 module.js 放到 src/js 目錄里面去,因此目錄結構變成如下:
|--- v-project | |--- flow-typed | | |--- npm | | | |--- lodash_v4.x.x.js | |--- interfaces | | |--- kongzhi.js | |--- node_modules | |--- .flowconfig | |--- src | | |--- js | | | |--- index.js | | | |--- module.js | |--- package.json | |--- .babelrc
src/js/module.js 在剔除之前代碼如下:
/* * module.js * @flow */ function module(str: string) : number { return str.length; } module.exports = module;
如上安裝完成相應的庫之后,我們需要在項目的根目錄下新建 .babelrc 文件,添加如下配置:
{ "presets": ["flow"] }
我們現在在項目的根目錄下運行如下命令,就可以在項目的根目錄生成 dist/js 文件夾,該文件夾下有index.js和module.js文件,如下命令:
./node_modules/.bin/babel src -d dist
然后我們查看 dist/js/module.js 代碼變成如下:
/* * module.js * */ function module(str) { return str.length; } module.exports = module;
我們也可以在node命令行中測試:$ node dist/js/module.js 后也不會報錯了。
8. Flow類型自動檢測
如上我們雖然使用了Babel去除了Flow的類型注釋,但是我們並沒有對Flow的靜態類型檢測。因此如果我們想讓babel進行Flow的靜態類型校驗的話,那么我們需要手動集成另外一個插件--- babel-plugin-typecheck。
想了解更多相關的知識, 可以看npm庫(https://www.npmjs.com/package/babel-plugin-typecheck)。
接下來我們需要進行具體的babel集成步驟,因此我們需要安裝 babel-plugin-typecheck 插件。如下命令:
npm install --save-dev babel-plugin-typecheck
當然我們要全局安裝下 babel-cli; 如下命令:
npm install -g babel-cli
接下來,我們需要在我們的 .babelrc 中添加 typecheck 插件進去,如下所示:
{ "presets": ["flow"], "plugins": ["typecheck"] }
現在我們就可以使用babel來編譯我們的Flow代碼了。如下命令所示:
$ babel src/ -d dist/ --watch src/js/index.js -> dist/js/index.js src/js/module.js -> dist/js/module.js
現在我們把 src/js/module.js 中的代碼改成如下:
/* * module.js * @flow */ function module(str: string) : number { return str.length; } var str != "hello world!"; console.log(str); module.exports = module;
然后我們保存后,在命令行中會看到如下報錯信息:
可以看到,我們使用babel就可以完成了校驗和編譯的兩項工作。再也不用使用 flow check; 這樣的對全局文件進行搜索並檢測了。
我們使用了 babel 的 --watch 功能解決了之前 Flow命令不能同時監聽,提示的缺憾了。
babel的缺陷:
但是使用 babel 也有缺陷的,比如我們現在把 src/js/module.js 代碼改成如下:
/* * module.js * @flow */ function module(str: string) : number { return str.length; } function foo(x : number) :number { return x * 12; } foo('a'); module.exports = module;
如上代碼,我們添加了一個foo函數,有一個參數x,我們希望傳遞的參數為 number 類型,並且希望返回的值也是 number 類型,但是我們在 foo('a'); 函數調用的時候,傳遞了一個字符串 'a' 進去,babel 在檢測的時候並沒有報錯,這或許是它的缺陷。
但是我們在 flow check; 就會報錯如下:
9. 了解 .flowconfig 配置項
我們在項目中的根目錄運行命令:flow init; 會創建 .flowconfig文件,該文件的作用是告訴Flow在這個目錄下開始檢測。不過 .flowconfig配置項也提供了一些配置選項,告訴Flow哪些文件需要檢測,哪些文件不需要檢測。
.flowconfig 默認有如下配置(我們講解前面三個比較常用的配置項):
[ignore]
[include]
[libs]
[lints]
[options]
[strict]
1. [ignore] 該配置是用來告訴flow哪些文件不需要檢測,默認為空,所有的文件需要檢測。我們也可以使用正則去匹配路徑,哪些路徑不需要進行檢測。
[ignore]
.*/src/*
如上表示的含義是:在src目錄下的所有文件不需要檢測,因此如果src下有某個js文件是不對的類型,也不會報錯的。
2. [include] 該配置是用來告訴Flow還要檢測哪些文件或者目錄。該配置的每一行表示一個待檢測的路徑,我們可以使用相對於根目錄下的路徑,或者絕對路徑,或支持一個或多個星號通配符。比如如下:
[include] ../xx.js ../xxxDir/ ../xxxDir/*.js
注意:如果 [ignore] 和 [include] 同時存在,並且同時匹配同個路徑,那就看那個配置在后面,那個優先級就更高。
3. [libs]
該配置下一般存放第三方接口文件,當我們引用第三方庫文件后,我們需要聲明一個接口文件,我們可以放在該目錄下。比如:
[libs]
interfaces/
Flow就會查找 interfaces/目錄下的所有 .js 文件作為接口文件的定義。
如上就是Flow一些基本的知識了解下即可。