使用Flow編輯和發布JavaScript模塊
Flow 是一個JavaScript的靜態類型檢查器,它提供了使用額外信息如期待的變量值的類型、函數功能和返回值等信息去注釋JavaScript代碼的能力。最近,在Elm這個JavaScript語言的超集做了很多類似的工作之后,我開始去探索流行的JavaScript動態類型添加問題。而且連 TypeScript 這個非常流行的并且被廣泛應用于 Angular 2社區的語言也開始使用Flow了。
我開始使用Flow是因為它被大量應用于React社區(鑒于它是一個非死book的項目也就不足為奇)并且它是基于React的和它是一個類型檢查器。雖然現在我沒有在使用React時應用Flow,但不久的將來我們將會看到大量關于它的博客文章出現,因為它很容易使用。寫這篇文章并不是我對Flow是我喜愛超過了TypeScript或者聲明Flow比TypeScript更好。我僅僅是分享一些關于Flow的經驗——我一直對這種事情很積極。
JavaScript類型檢查
首先,我選擇了 util-fns 來作為例子演示. util-fns 是我自己寫的很小的一個工具庫(有點類似Lodash和Underscore,但是跟小更簡潔)。它主要是一個很簡單的學習和實驗Flow的工程。 我選擇它是因為它是我發布的npm模塊,并且可以就此探索在不丟失類型的情況下如何去發布一個模塊的方法。這意味著任何一個開發者運行 npm install util-fns 都能訪問這類信息并且可以及時的收到更新消息。
安裝 Flow
為了使用Flow,我首先安裝對 flow-bin 這個npm模塊的本地依賴:
npm install --save-dev flow-bin
你也可以選擇全局安裝, 但我喜歡把我所有的依賴安裝到當前工程里邊。這樣,當你想要在不同的工程里邊使用不同版本的Flow,你可以覆蓋當前的環境。
然后運行命令 ./node_modules/.bin/flow init 。
注意:在 $PATH 里邊我有一個 ./node_modules/.bin 目錄,你能在我的 dotfiles 里找到它。這樣子做會有一些風險,因為我能運行在這個目錄下的任何可執行文件,但是,我愿意去冒這個風險,因為我知道我在本地都安裝了些什么,并且這是一個高效的做法!
運行 flow init 你會創建一個看起來像這樣的 .flowconfig 文件:
[ignore]
[include]
[libs]
[options]
不要擔心這種奇怪的語法,實際上,它基本上是空的。這個配置現在已經足夠了,我沒必要再編輯其它的配置了,但如果你需要其它配置的話,到Flow的站點 。
創建這個文件之后,我們就可以運行Flow檢查我們的代碼了。你可以運行 flow 命令來看看都有些什么輸出:
Launching Flow server for /Users/jackfranklin/git/flow-test
Spawned flow server (pid=30624)
Logs will go to /private/tmp/flow/zSUserszSjackfranklinzSgitzSflow-test.log
No errors!
你看到的第一個信息就是Flow啟動了一個服務。此服務器是在后臺運行,并在你工作時增量的檢查Flow代碼。 運行服務后,Flow能緩存文件的狀態并且當內容更改時候重新檢查它。這使得檢查你的文件的時候Flow運行的非常快。當你想檢查整個工程時候你需要運行 flow check 命令,但開發模式下你僅僅運行 flow 即可。這樣可以使用Flow服務(如果沒有則啟動一個),并且可以只高效的檢查更改過的文件。
當你運行Flow的時候沒有看到任何錯誤,實際上,那是因為你并沒有用Flow檢查任何代碼。Flow被設計成可以注入到一個已有的JavaScript工程并且不會造成任何錯誤,因此,Flow僅僅檢查頂部有以下注釋的文件:
// @flow
這意味著你可以增量的使用Flow,這是一個非常棒的優點。我正在考慮在大的JS代碼庫中使用Flow,如果不能增量的部署它我將不會考慮使用它了。
搭配Babel使用
重要的一點是:Flow只是一個類型檢查器,它并不能從你的代碼剝離類型和生成JS代碼。為了實現這一點,我推薦這個Babel插件 transform-flow-strip-types ,它可以告訴Babel當你在編譯的時候移除相應的類型。讓我們來看看如何部署它。
寫一些Flow代碼!
現在我們準備去寫一些代碼,就讓我們從 sum 函數開始吧。它接受一個數組作為參數,返回數組每一項的累加和。以下是實現代碼:
const sum = input => {
return input.reduce((a, b) => a + b)
}
export default sum
這沒什么好奇怪的,使用 reduce 方法可以很容易的實現這個功能。接著讓我們使用Flow來注釋這個函數。首先我們注釋它需要攜帶的參數,即聲明需要一個 Array 類型的 number 。意思是 input 將是一個數組并且每一項的值都是 number 類型的,Flow的聲明數組的語法如下:
// @flow
const sum = (input: Array<number>) => {
return input.reduce((a, b) => a + b)
}
export default sum
注意,我還添加了注釋 // @flow 以便Flow檢查我的代碼。接下來聲明函數返回值的類型也是 number :
// @flow
const sum = (input: Array<number>): number => {
return input.reduce((a, b) => a + b)
}
export default sum
如果你再次運行 flow ,你會發現仍然不會報錯。這意味著Flow已經確認我的代碼已經符合我的預期。
讓我們犯一個錯誤(是個很明顯的錯誤,但是想象一下這個出現在真實場景中效果):
// @flow
const sum = (input: Array<number>): number => {
return input.reduce((a, b) => a + 'b')
}
然后運行 flow ,你會看到一些錯誤(你可能需要滾動查看完整的錯誤):
3: return input.reduce((a, b) => a + 'b')
^^^^^^^ string.
This type is incompatible with the expected param type of
2: const sum = (input: Array<number>): number => {
^^^^^^ number
Flow已經正確的標識出了錯誤信息,即我們在使用 reduce 方法的時候使用了字符串類型 b 和一個數值類型的 a 相加這樣做是無效的。Flow知道a的值是一個 number 類型,因為我們明確規定了 input 必須是一個 Array ,因此它可以發現問題所在。
Flow很擅長檢查一些愚蠢的錯誤,一旦你習慣了使用它,就可以大量避免這些愚蠢的錯誤,并且你可以提前發現它們,而不用等到放到頁面里邊刷新頁面的時候才發現。
使用Flow還有個好處是在你的代碼庫中一旦你注釋了某個函數的類型,Flow就能在你使用這個函數的時候標識出一些錯誤信息。
比如說,我在6個月后使用了 sum 方法,但是我忘記了我需要傳一個數組類型的參數,你可能會使用 sum(1,2,3) 來調用而不是 sum([1,2,3]) 。這是一個很常見的錯誤,但你卻需要到瀏覽器里邊運行,且查看源代碼之后才能知道具體哪里出錯。使用了Flow之后,我們久可他更快的定位錯誤信息:
8: sum(1, 2, 3)
^ number. This type is incompatible with the expected param type of
2: const sum = (input: Array<number>): number => {
^^^^^^^^^^^^^ array type
這節省了大量的時間和精力去追蹤錯誤,并且一旦出現錯誤,就立馬被標識了出來。Flow還有配套的插件和編輯器讓你能查看代碼出現錯誤時的一些信息。
目前為止,我們都還停留在對Flow的表面上,并且知道了它能干什么,接下來,我們需要繼續探索如何去發布使用Flow注釋的npm代碼。
發布帶有類型定義信息的JavaScript模塊
現在,我的小模塊 util-fns 已經準備好發布到npm讓所有人都能下載和使用它。我的代碼中有許多的類型定義,且我所有的代碼都是使用的ES2015來編碼的。發布的時候,我將使用babel去把我的代碼編譯成ES5代碼,讓它能在更多的瀏覽器上運行。然而,在代碼中花費大量時間和精力添加的類型檢查是愚蠢的,一旦我把它們從發布的模塊中刪除,其它人的代碼也隨之刪除,這對其它人來說可沒有太多的好處。
相反,我希望使用Flow的開發人員能夠看到模塊提供的函數的類型信息,如果他們犯錯了,Flow也能告訴他們錯在哪。我也希望使用我模塊的開發者使用Flow,這樣就無需太多額外的編譯步驟。
我的解決方式是在一個模塊中使用兩個版本的代碼。一個版本是完全用Babel編譯且沒有使用類型檢查器。另外一個版本則是包含類型定義的原始代碼。當我探索發布帶有類型定義的代碼到npm的方法時,我發現,當一個文件被引用的時候,Flow會查看文件是否帶有后綴 .flow ,沒有后綴的則不進行檢查。也就是說,如果我有以下類似代碼:
import foo from './my-module'
Flow將在引用 my-module.js 之前檢查文件 my-module.js.flow 是否存在,如果存在,則檢查之。當然,其它工具則會忽略 .flow 后綴的文件而直接引用 my-module.js 。
我們需要做的是在我們的項目中發布每個文件的兩個版本。因此,對于 sum.js ,我們應該這樣發布:
-
lib/sum.js , 使用babel編譯,沒有使用類型檢查器。
-
lib/sum.js.flow , 使用類型檢查的原始代碼。
Babel配置
在Babel中配置使用Flow需要創建一個配置文件 .babelrc ,并配置插件 transform-flow-strip-types ,讓其它人也可能使用它:
"presets": ["es2015"],
"plugins": [
"transform-flow-strip-types",
]
}
然后你可以告訴Babel你的輸入文件夾 src 和輸出文件夾 lib :
babel src/ -d lib
通常,你會將 lib 目錄添加到你的 .gitignore 中,因為我們不想提交我們編譯過后的代碼到Git上。
告訴npm使用 lib 目錄
當我們發布這個package的時候還需要告訴npm應該發布 lib 目錄下的文件。如果你已經在你的 .gitignore 文件中忽略了 lib 目錄,npm發布時默認會不包含 lib 目錄。但是, lib 實際上是我們希望用戶運行的代碼,所以在我們的例子中,我們需要發布它。
我的首選方法時在 package.json 中配置 files 入口:
"files": [
"lib"
]
最后,我們需要更新配置里的 main 屬性。這是用戶使用我們的模塊時候的入口(例如 import utils from 'util-fns' )。在這個例子中,我想要使用 lib/index.js 作為入口文件,所以我得這樣更新 package.json :
"main": "lib/index.js"
生成 .flow 文件
雖然現在我們已經有一個完全經過編譯的JavaScript文件目錄 lib ,但時我還想保留我得原始文件,且給它加上 .flow 后綴。幸運的是,我不是第一個想要這樣的人,我在Github上找到了我所需要得模塊 flow-copy-source 。我可以把它安裝到開發者依賴中:
npm install --save-dev flow-copy-source
然后簡單得運行它:
flow-copy-source src lib
一旦我運行它,他就會把 src 目錄下得文件拷貝到 lib 目錄下,同時為每一個文件加上 .flow 后綴。現在我的 lib 目錄看起來像這樣:
lib
├── index.js
├── index.js.flow
├── ...and so on
├── sum.js
└── sum.js.flow
發布時候構建
我們現在幾乎準備好去發布npm模塊了,但最后一步是確保發布時,我們不會忘記任何上述步驟。我可以在 package.json 里邊配置腳本命令 prepublish 讓npm在運行 npm publish 時自動的先執行它。通過這樣做,我將確保我的項目是最新的,構建完成時,我就發布了一個新版本的倉庫。通常我會把npm腳本命令分成幾塊,這樣我就可以使一個腳本命令運行Babel,另外一個運行flow-copy-source,并且可以在它們之前運行腳本命令 prepublish :
"prepublish": "npm run babel-prepublish && npm run flow-prepublish",
"babel-prepublish": "babel src/ -d lib",
"flow-prepublish": "flow-copy-source src lib",
最后,我們已經準備好去發布模塊了!我可以運行 npm publish 去發布模塊到倉庫里邊,并且在我運行之前運行 prepublish 命令,生成被編譯過的文件和 .flow 的文件:
> npm run babel-prepublish && npm run flow-prepublish
> util-fns@0.1.3 babel-prepublish /Users/jackfranklin/git/util-fns
> babel src/ -d lib
src/index.js -> lib/index.js
...and so on
src/sum.js -> lib/sum.js
> util-fns@0.1.3 flow-prepublish /Users/jackfranklin/git/util-fns
> flow-copy-source src lib
使用我們的新模塊
為了檢查我們發布的代碼類型是否正常工作,我們可以在另外的工程中安裝新發布的使用Flow配置過的模塊 util-fns :
npm install --save util-fns
因為我們已經混淆了我的API,所以我們再次運行想要去運行的同一個方法時不存在的:
// @flow
import utils from 'util-fns'
utils.getSum([1, 2, 3])
Flow能夠發現 getSum 方法在模塊里邊不存在:
4: console.log(utils.getSum([1, 2, 3]))
^^^^^^ property getSum. Property not found in
4: console.log(utils.getSum([1, 2, 3]))
^^^^^ object literal
現在想象我記得有個函數叫做 sum ,但是我忘了我必須傳遞一個數組作為參數:
// @flow
import utils from 'util-fns'
console.log(utils.sum(1, 2, 3))
Flow也可發現問題所在,但僅僅限于包里那些有 .flow 后綴的文件。請注意,如果我們想去找出匹配類型的 sum 函數的源碼,Flow會告訴我們要哪個文件找,并查找出對應位置:
4: console.log(utils.sum(1, 2, 3))
^ number. This type is incompatible with the expected param type of
2: const sum = (input: Array<number>): number => {
^^^^^^^^^^^^^ array type.
See: node_modules/util-fns/lib/sum.js.flow:2
這個非常明智的,因為一個開發者工作時經常會忘記一些之前寫的一些API。這意味著,我很快意識到錯誤,且在我編碼時給我一些暗示和幫助,告訴我函數接受什么樣的參數和它們是什么類型的。你可看到我的額外的成果 util-fns 包,可以引導其它人在使用Flow環境的時候有一個更好的體驗。
在沒定義過類型檢查的庫里使用
雖然在這這個例子中,我們發布的 util-fns 中函數類型已經定義過了,但并不是所有的代碼庫里都配置了這些。還有很多很多的代碼庫沒有使用Flow,但是對于普通的JavaScript來說,沒有任何類型的檢查這是一種非常不好的行為。
幸運的是, flow-typed 可以幫助到你。對于許多很多流行的庫來說,這是一個很好的類型庫,包括了NodeJS和客戶端JavaScript、Express、Lodash、Enzyme, Jest, Moment, Redux等等。
你可以通過npm安裝 flow-typed ,且可以在你的項目里簡單的運行 flow-typed install 就可以。接著你瀏覽 package.json 里的每一個依賴,試著去安裝這個庫里相應的類型定義。這意味著你仍然可以直接使用類型信息,即使是像Lodash這樣沒使用Flow的庫也一樣。
總結
我希望我這篇博客可以幫助你使用Flow走進JavaScript類型檢查的世界。我的文章僅僅是描述了Flow的作用,還有更多的東西需要我去研究和學習。如果你是一個庫的開發者,我鼓勵你去嘗試使用Flow,這能幫助你在開發過程中防止錯誤的發生。在發布庫時包含這些類型定義也是很好的;您的用戶將能夠從中受益但它們使用錯誤的時候,這也意味著Flow可以發現API的變化,并及時通知用戶類型的變化。
來自:http://www.zcfy.cc/article/authoring-and-publishing-javascript-modules-with-flow-2503.html