vue系列--- 認識Flow(一)


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│ */
      67│ 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.

 12│ // @flow
 34│ import _ from 'lodash'; 
 567│

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│ ];
  89│ function test() {
 10│   return _.findWhere(obj, {flag: true});
 11│ }
 1213│

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一些基本的知識了解下即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM