ReactFlow程式碼靜態檢查

溜達向日葵發表於2018-07-20

Flow

Flow是Facebook開源的靜態程式碼檢查工具,他的作用是在執行程式碼之前對React元件以及Jsx語法進行靜態程式碼的檢查以發現一些可能存在的問題。Flow可以用於所有前端開發的專案而不僅僅侷限於React,碼友們可以到 官網仔細瞭解(友情提示:可能需要VPN,非常不穩定),本文只介紹如何配合React開發使用。

Flow僅僅是一個用於檢查的工具,安裝使用都很方便,使用時注意以下3點即可:

  1. 將Flow增加到我們的專案中。
  2. 確保編譯之後的程式碼移除了Flow相關的語法。
  3. 在需要檢查的地方增加了Flow相關的型別註解。(類似與Java的Annotation機制)

接下來我們來一一說明以上三點的具體內容。碼友們邊閱讀邊操作即可。

將Flow增加到我們的專案中

安裝最新版本的Flow:

Npm:

npm install --save-dev flow-bin

安裝完成之後在package.json檔案中增加執行指令碼:

{
  // ...
  "scripts": {
    "your-script-name": "flow",
    // ...
  },
  // ...
}

然後初始化Flow:

npm run flow init

執行完成後,Flow會在終端輸出一下內容:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow "init"

然後在根目錄下生成一個名為 .flowconfig 的檔案,開啟之後是這樣的:

[ignore]

[include]

[libs]

[lints]

[options]

[strict]

基本上,配置檔案沒有什麼特殊需求是不用去配置的,Flow預設涵蓋了當前目錄之後的所有檔案。[include]用於引入專案之外的檔案。例如:

[include]

../otherProject/a.js

[libs]

他會將和當前專案平級的otherProject/a.js 檔案納入進來。關於配置檔案請看這裡

編譯之後的程式碼移除Flow相關的語法

Flow在JavaScript語法的基礎上增加了一些 註解(annotation)進行了擴充套件。因此瀏覽器無法正確的解讀這些Flow相關的語法,我們必須在編譯之後的程式碼中(最終釋出的程式碼)將增加的Flow註解移除掉。具體方法需要看我們使用了什麼樣的編譯工具。下面將說明一些React開發常用的編譯工具

Create React App

如果你的專案是使用Create React App直接建立的。那麼移除Flow語法的事項就不用操心了,Create React App已經幫你搞定了這個事,直接跳過這一小節吧。

Babel

在15.x版本之前入坑React的碼友應該絕大部分都用的Babel作為語法糖編譯器,那個時候畢竟Create React App完全沒有成熟。如果使用Babel我們還需要安裝一個Babel對於Flow的preset:

npm install --save-dev babel-preset-flow

然後,我們需要在專案根目錄Babel的配置檔案 .babelrc 中新增一個Flow相關的preset:

{
  "presets": [
    "flow",
    //other config
  ]
}

其他方式

如果你既沒有使用Create React App也沒使用Babel作為語法糖編譯器,那麼可以使用 flow-remove-types 這個工具在釋出之前移除Flow程式碼。

執行Flow

完成上述步驟之後,就可以開始執行flow了:

npm run flow

然後會輸類似一下的內容:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow

Launching Flow server for /yourProjectPath
Spawned flow server (pid=10705)
Logs will go to /tmp/flow/zSworkzSchkuizSone-big-website.log
Monitor logs will go to /tmp/flow/zSworkzSchkuizSone-big-website.monitor_log
No errors!

第一次執行會生成很多臨時檔案比較慢,之後會快許多。

增加Flow註解

如果你瞭解C++/C#的超程式設計或者Java的Annotation,那麼理解Flow的Annotation就會非常輕鬆。大概就是在檔案、方法、程式碼塊之前增加一個註解(Annotation)用來告知Flow的執行行為。

首先,Flow只檢查包含 // @flow 註解的檔案。所以如果需要檢查,我們需要這樣編寫我們的檔案:

// @flow
import React from `react`

class MyComponent extends React.Component {
    render(){
        return (<div>MyComponent</div>)
    }
}

export default MyComponent

然後我們再執行Flow就變成這樣的風格了:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ dev/src/home/test.js:5:21

Cannot use property Component [1] with less than 1 type argument.

     dev/src/home/test.js
      23import React from `react`
      45class MyComponent extends React.Component {
      6render(){
      7return (<div>MyComponent</div>)
      8│     }

     /tmp/flow/flowlib_cc1898a/react.js
 [1] 26declare class React$Component<Props, State = void> {

到這裡,Flow已經算是安裝成功了,接下來的事是要增加各種註解以加強型別限定或者引數檢測。之後的內容將簡要介紹flow的相關語法規則。

React元件引數檢查

React元件引數檢查介紹了React通過PropType機制限定使用者使用元件傳遞的引數型別以及範圍,但是PropType是一種執行檢測機制,在程式跑起來之後獲取到具體資料才會執行檢查。而Flow是靜態檢查,是在程式碼編譯執行之前進行一次檢查,兩者相輔相成互不干擾。

Props引數檢查

承接上面 MyComponent 的例子,我們引入Flow的註解對程式碼進行檢查:

// @flow
// flow的例子,可以看看和PropType的差異在哪
import React from `react`

type Props = {
    num : number,
    text : ?string
}

//通過<>引入Flow型別檢查
//可以直接寫成 React.Component<{num : number, text ?: string}>這樣的形式
class MyComponent extends React.Component<Props> {
    render(){
        return (<div>{this.props.num}{this.props.text}</div>)
    }
}

export default MyComponent

然後在執行Flow,輸出了No Error。

然後我們使用這個元件:

// @flow
// flow的例子,可以看看和PropType的差異在哪
import React from `react`

type Props = {
    num : number,
    text : ?string
}

class MyComponent extends React.Component<Props> {
    render(){
        this.props.myValue;
        return (<div>{this.props.num}{this.props.text}</div>)
    }
}

//void 表示 undefined 不傳遞引數
//這裡傳遞型別發生錯誤
const UseComponent = (props : void) =>(<MyComponent num="2" text={2}/>)

export default UseComponent

執行flow之後輸出:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ dev/src/home/test.js:12:20

Cannot get this.props.myValue because property myValue is missing in Props [1].

      9│
 [1] 10class MyComponent extends React.Component<Props> {
     11render(){
     12this.props.myValue;
     13return (<div>{this.props.num}{this.props.text}</div>)
     14│     }
     15│ }


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ dev/src/home/test.js:17:40

Cannot create MyComponent element because:
 • string [1] is incompatible with number [2] in property num.
 • number [3] is incompatible with string [4] in property text.

    [2]  6│     num : number,
    [4]  7│     text : ?string
          :
        14│     }
        15│ }
        16│
 [1][3] 17const UseComponent = (props : void) =>(<MyComponent num="2" text={2}/>)
        1819export default UseComponent



Found 3 errors

輸出內容可以看出一共有2個錯誤欄輸出:

  • 第一欄表示myValue並沒有宣告。
  • 第二欄[1]違反了[2]的限定,[3]違反了[4]的限定。我們將元件變更為<MyComponent num={2} text=”2″/>即可檢查通過。

增加對State的檢查

React的資料通過兩處控制——props 和 state。Flow也提供了state資料的檢查,我們在例子中增加state檢查:

// @flow
// flow的例子,可以看看和PropType的差異在哪
import React from `react`

type Props = {
    num : number,
    text : ?string
}

type State = {
    count: number,
};

class MyComponent extends React.Component<Props, State> {
    constructor(...props){
        super(...props)
        this.state = {count:`1`}
    }

    render(){
        return (<div>{this.props.num}{this.props.text}</div>)
    }
}

const UseComponent = (props : void) =>(<MyComponent num={2} text="2"/>)

export default UseComponent

此時執行Flow會輸出:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ dev/src/home/test.js:17:29

Cannot assign object literal to this.state because string [1] is incompatible
with number [2] in property count.

 [2] 11│     count: number,
     12│ };
     1314class MyComponent extends React.Component<Props, State> {
     15│     constructor(...props){
     16super(...props)
 [1] 17this.state = {count:`1`}
     18│     }
     1920render(){

檢測出state.count在建構函式中賦值的型別錯誤。

元件預設值

使用Flow後一樣可以使用預設值,但是必須要注意預設值的型別要和註解宣告的一致:

import * as React from `react`;

type Props = {
  foo: number, 
};

class MyComponent extends React.Component<Props> {
  static defaultProps = {
    foo: 42, 
  };
}

函式型別的元件

除了使用Class關鍵字,使用函式同樣可以構造一個React元件,配合Flow使用:

import React from `react`;

type Props = {//引數檢查
  foo: number,
  bar?: string,
};

function MyComponent(props: Props) {
  return <div>{props.bar}</div>;
}

MyComponent.defaultProps = {
  foo: 42 //指定預設值
};

React事件、子元件、高階元件檢查擴充套件

除了對單個元件基本的檢查,Flow還提供了對React事件、refs、子元件、高階元件、Redux。本文就不一一介紹了,有需要的碼友可以按照下面的資源清單去了解相關的內容:

型別檢查擴充套件

Flow會檢查所有的JavaScript基礎型別——Boolean、String、Number、null、undefined(在Flow中用void代替)。除此之外還提供了一些操作符號,例如例子中的 text : ?string,他表示引數存在“沒有值”的情況,除了傳遞string型別之外,還可以是null或undefined。需要特別注意的是,這裡的沒有值和JavaScript的表示式的“非”是兩個概念,Flow的“沒有值”只有null、void(undefined),而JavaScript表示式的“非”包含:null、undefined、0、false。

除了前面的例子中給出的各種型別引數,Flow還有更豐富的檢查功能,檢視 這裡 以瞭解更多內容。

React資料型別參考

對於Flow來說,除了常規的JavaScript資料型別之外,React也有自己特有的資料型別。比如React.Node、React.Key、React.Ref<>等。需要詳細瞭解的,可以檢視官閘道器於React型別的說明

需要特別說明的是,如果所要使用React的型別,在通過ES6引入React物件時需要使用這樣的方式:

import * as React from `react`
//替換 import React from `react`

//或者單獨引入一個型別
//import type {Node} from `react

兩者的差異在於ES6的星號import的特性,使用*號會將一個檔案中的所有 export 內容組合成一個物件返回,而不使用星號僅僅能獲取到exprot default 那個原型。而引入Flow後不會修改React的預設匯出型別,因為預設匯出不一定是一個物件,他會通過export為React擴充套件更多的型別。

比如我們用React.Node限制render方法的返回型別:

import * as React from `react`
class MyComponent extends React.Component<{}> {
  render(): React.Node {
    // ...
  }
}

遇到的一些問題

我在使用的過程中目前遇到的問題之一是import 樣式資源 或  圖片時報 “./xxx.scss. Required module not found” 的異常,檢視官方文件瞭解Flow只支援.js、.jsx、.mjs、.json的檔案,如果需要匯入其他檔案需要並支援需要擴充套件options。在.flowconfig新增options:

[ignore]
[include]
[libs]
[lints]
[options]
module.file_ext=.scss
[strict]

此外,某些IDE對Flow的支援不是很好。我目前所使用的webstorm 2017.3.5相對還不錯,不過切記要到File->Setting->Languages&Frameworks->Javascript中將version設定為Flow。

寫在最後的使用心得

引入並按照Flow的規範去約束每一個元件會導致開發量增加不少(當然你引入不用是另外一回事,但是不用引入他做什麼?)。搭建好Flow的框架僅僅是開始,之後除了團隊成員要去了解flow的使用方法,早期還會遇到各種坑需要去解決。而且Flow也要比React的 PropTypes ”重“許多。

JavaScript本來是一個型別推導的原型語言,弄個Flow進來搞得越來越像Java這種強型別語言,也不知道是好是壞,而Java10又學JavaScript等加入了val這種可以型別推導的關鍵字….。

總的來說引入規範是有成本的,具體要看團隊規模以及專案大小,不是引入越多的技術棧就越有逼格。如果你獨立專案的前端開發人數並不多,或者程式碼膨脹(程式碼腐爛)速度也沒有讓你措手不及,建議慎重引入Flow。個人覺得Flow除了開發人員自檢還要整合到整個測試框架中,在整合測試或某個版本的程式碼釋出之前進行集中檢查。需要思考它在專案的開發、測試、模擬、上線迭代週期中扮演的角色,甚至整合到類似與CMMI之類的管理流程去反向量化考核程式碼質量。


相關文章