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

1. 什麼是Flow?javascript

Flow 是javascript代碼的靜態類型檢查工具。它是Facebook的開源項目(https://github.com/facebook/flow),Vue.js(v2.6.10的源碼使用了Flow作了靜態類型檢查。所以咱們如今先來了解下Flow的基本知識,有助於咱們分析源碼。
2. 爲何要用Flow?java

javascript是弱類型語言,弱類型體如今代碼中的變量會根據上下文環境自動改變的數據類型。那麼這種弱類型有優勢也有缺點,優勢是咱們容易學習和使用,缺點是:開發者常常由於賦值或傳值致使類型錯誤。形成一些和預期不同的結果。在代碼編譯的時候可能不會報錯,可是在運行階段就可能會出現各類奇怪的bug。所以在大型項目中咱們有必要使用Flow來作代碼靜態類型檢查。node

下面咱們從一個簡單的demo提及。好比以下代碼:git

function add (x) {
  return x + 10;
}

var a = add('Hello!');
console.log(a); // 輸出:Hello!10

如上代碼,x這個參數,咱們在add函數聲明的時候,其實咱們但願該參數是一個數字類型,可是在咱們代碼調用的時候則使用了字符串類型。致使最後的結果爲 "Hello!10"; 爲何會出現這種結果呢?那是由於 加號(+)在javascript語言中,它既有做爲數字的加運算符外,還能夠做爲字符串的拼接操做。github

所以爲了解決類型檢查,咱們可使用Flow來解決。下面咱們來介紹下 如何在咱們項目中使用Flow。npm

3. 開始一個新的 Flow 項目編程

首先咱們建立一個名爲 v-project 項目:json

$ mkdir -p v-project  
$ cd v-project

 接着,添加Flow, 執行以下命令:bootstrap

$ 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一些基本的知識瞭解下便可。

相關文章
相關標籤/搜索