Mmzer

使用Flow编辑和发布JavaScript模块

Mmzer · 2017-02-19翻译 · 454阅读 原文链接

Flow是一个JavaScript的静态类型检查器,它提供了使用额外信息如期待的变量值的类型、函数功能和返回值等信息去注释JavaScript代码的能力。最近,在Elm这个JavaScript语言的超集做了很多类似的工作之后,我开始去探索流行的JavaScript动态类型添加问题。而且连TypeScript这个非常流行的并且被广泛应用于 Angular 2社区的语言也开始使用Flow了。

我开始使用Flow是因为它被大量应用于React社区(鉴于它是一个Facebook的项目也就不足为奇)并且它是基于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代码。Flow相关文档里有更多关于Flow能做什么的信息,并且可以通过这个对Flow进行持续的关注。

发布带有类型定义信息的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的变化,并及时通知用户类型的变化。

译者Mmzer尚未开通打赏功能

相关文章