2014 facebook 推出的一款弥补 JavaScript 弱类型弊端的工具。在 React/Vue 中我们都能够看到 Flow 的使用,足以见得 Flow 是一个非常成熟的技术方案。
它的原理就是让我们在代码当中通过添加一些类型注解的方式来去标记代码中每个变量或参数是什么类型的,Flow 就可以根据这些注解来检查代码当中是否存在类型使用上的异常,从而实现在开发阶段对类型的检查。
相比TypeScript,Flow只是一个小工具,而TypeScript是一门全新的语言,所以相比较而言,Flow几乎没有什么学习成本。
flow快速上手:
因为Flow是以npm模块去工作的,所以先构建一个新项目
yarn init --yes
再安装Flow的类型检查工具 flow-bin, 将它作为项目的开发依赖去安装:体积可以有些大,耐心等待
yarn add flow-bin --dev
安装完成过后可以在node_modules目录的bin目录下找到flow的可执行文件,就表明我们可以再命令行去执行flow,执行flow的作用就是检测我们当前项目当中对应代码的类型异常。
当然还需要添加flow的配置文件,通过flow init命令去初始化 .flowconfig文件,执行:
yarn flow init
完成后项目的根目录下就会多出这样一个配置文件,里面有一些初始的配置选项,用到的时候再说,先放一边。
接下来创建一个01-getting-started.js文件,我们尝试使用一下flow。还是以之前的sum函数为例:
//@flow
function sum(a:number , b:number){
return a + b;
}
sum(100,100);
sum("100","100");
正常来说我们会遇到因为参数类型不明确所产生的问题。这里我们使用类型注解去标记一下这两个参数的类型。就是在参数名后面加:number,表示参数必须是number类型的数据。如果我们传入其他类型的参数,flow在执行检查的时候就会报出错误。
我在在使用vscode编辑器的时候,没有等到flow执行工作,编辑器就已经对:number报出了错误,因为它不是标准JavaScript语法,而编辑器会自动校验JavaScript语法从而报出错误,我们使用了flow,也就不需要编辑器提供的校验了,在设置里面把它关掉。(学习后记得要重新开启呀)
注意:在代码中使用类型注解有一个前提,就是必须在当前文件一开始的位置通过注释的方式添加一个@flow的标记,这样flow执行检查时才会检查这个文件。
好了,接下来就使用安装好的flow工具模块去检查代码中的类型问题。(第一次执行总是会慢一点点,因为第一次执行flow会开启一个后台服务去监视文件,而后执行就会快很多)
yarn flow
完成编码工作后可以执行flow stop去结束这个服务
yarn flow stop
执行完之后可以看到flow帮我们找到了两个错误,而且每个错误都有详细的描述信息。
总结一下使用flow去进行类型检查的步骤:
- 安装flow的类型检查工具(flow-bin)
- 在需要flow进行类型检查的文件的开头添加//@flow的一个注释标记
- 最后再代码当中为我们代码的成员去添加类型注释(:类型)
- 完成以后使用flow-bin提供的flow命令去检测代码的问题了
Flow编译解除注解
flow命令可以检测js文件中的类型问题。它的工作原理是根据代码中的 (':类型' ) 的类型注解去找到所有类型使用上的异常。但是添加类型注解的代码是不符合JavaScript语法规范的。这样的代码是无法正常运行的。
要解决这个问题也很简单,就是在执行完类型检查后,使用工具再进行编译,自动移除类型注解。
目前有两种主流的方案:
1.使用flow官方提供的remove type模块,这也是最快捷最简单的方案。
先安装:安装好后就可以使用这个模块所提供的命令行工具去自动移除类型注解
yarn add flow-remove-types --dev
运行:这个命令的第一个参数是源代码所在的目录,然后通过-d参数去指定转换过后的输出目录,在这里把它指定到dist文件夹下面
yarn flow-remove-types . -d dist
然后在项目根目录下面可以看到多出来一个dist目录,在dist文件夹里面就看到了转换过后的结果。
之前添加的类型注解不见了,这个文件就可以直接运行了。
到这里应该就明白folw是如何去解决Javascript弱类型所带来的弊端了。无外乎就是把我们编写的代码和实际生产环境运行的代码分开,中间加入了编译的环节。这样我们在开发阶段就能使用一些扩展语言是类型检测变得可能。
说到编译,最常见的JavaScript编译工具就是babel,babel配合插件也可以实现这里自动移除代码中的类型注解。
2.使用babel
这里先安装一下@babel/core(Babel的核心模块),然后安装@babel/cli(Babel的cli工具,它可以让我们在命令行中使用babel命令完成编译),最后安装@babel/preset-flow(包含了转换flow类型注解的插件)
yarn add @babel/core @babel/cli @babel/preset-flow --dev
接下来需要在项目中添加一个babel的配置文件,.babelrc文件里面配置一下presets,就是刚才我们安装的flow的preset。
接下来就可以使用babel来自动编译我们的js代码了。帮我们移除类型注解。
这里把我们编写的js文件放入src文件目录下
回到命令行终端,运行一下babel这个命令:
它的运行参数和flow的remove-types模块类似,第一个参数是源代码所在的目录,然后通过-d参数去指定转换过后的输出目录,在这里把它指定到dist文件夹下面。
yarn babel src -d dist
在dist文件夹里面就看到了转换过后的结果:
类型注解也被移除掉了。
总结:
这里介绍了两种方案去移除我们源代码中的类型注解,第一种是flow官方提供的flow-remove-types,第二种是使用babel去配合flow转换的插件,如果在项目中已经使用了babel,那就选择第二种。反之,就选择第一种更简单,更快捷的remove-types。
flow开发工具插件
有了以上两个环节,就可以在项目当中去使用flow了,但是目前flow所检测到代码当中的问题都被输出到了控制台,而且需要我们去命令行终端去运行命令才能执行一次检测,这种体验很不直观,最好的方式是开发中在编辑器里直接显示出来对类型使用上的问题,所以一般会选择安装一个开发工具的插件,让开发工具只管的去体现问题。
打开vscode的插件面板,搜索flow,在搜索结果里找到Flow Language Support,然后安装。
回到代码中就可以看到了,类型异常就会被红色波浪线标记到了。
如果使用的是其他的开发工具(编辑器),也都会有类似的插件,可以在flow的官网中找到。
https://flow.org/en/docs/editors/
Flow类型推断
Flow除了可以使用类型注解的方式标记类型,还可以很聪明帮我们推断它应该是什么类型。
例如:这个求平方的函数。
/**
*类型推断
*@flow
*/
function square(n:number){
return n * n;
};
square("100");
很明显它应该只允许接收number类型的参数,所以正常我们应该在函数的参数位置给它添加: number类型注解,然后调用函数的时候传入的参数不是number类型的时候就会报错。
但这里即便我们不添加类型注解,然后直接传入非数字参数。可以看到flow它仍然可以帮我们发现代码类型使用中的错误。
flow会根据调用时传入的参数是字符串,推断出函数的n参数是字符串类型,而字符串不可以进行乘法运算的,所以它也会提示错误。
这种根据我们代码中的使用情况,去推断出来变量的类型这个特征叫做类型推断。
不过大多数情况下还是建议为我们代码中的成员添加类型注解,这样可以让我们的代码有更好的可读性。
Flow类型注解
类型注解不仅可以添加在函数的参数 上,也可以标记变量的类型和函数的返回值的类型。
1. 标记变量的类型
let num:number = 100 ;
num = "100" //这里会标记出错误
2.标记函数返回值类型
function foo():number{
return "888" //这里会标记出错误
}
注意:没有返回值的时候,默认返回undefined,所以他也会报语法错误。所以没有返回值的函数可以将它的返回值标记为:void。
Flow支持的类型
在用法上,flow几乎没有什么难度,无外乎就是根据我们代码中的类型注解,使用flow命令去检测代码中的类型异常。接下来值得了解的也就是:
- flow当中具体还支持那些类型。
- 以及在类型注解当中有没有什么更高级的用法。
类型:
1.js中的原始数据类型:string,number,boolean,null,undefined,symbol,
//字符串类型
const a : string = "string"
//number类型的变量可以用来存放数字,NaN,infinity(无穷大)
const b : number = Infinity //NaN //100
//boolean类型只能存放true,false
const c : boolean = true //false
//null本身
const d : null = null
//注意:想要给一个变量存放undefined,要把它的类型标记为void
const e : void = undefined;
//symbol 类型
const f : symbol = Symbol()
注意:想要给一个变量存放undefined,要把它的类型标记为void
除了对普通的数值做类型限制的原始类型,flow还支持对有结构的数据做类型限制(对象,数组)
2.数组类型
(1)使用Array类型 加泛型参数(表示数组当中每个元素的类型)
const arr1 : Array<number> = [1,2,3,4,5]
(2)元素类型后面跟数组的[] 去 表示全部由 某类型 组成的数组
const arr2 : number[] = [100,200,300]
(3)固定长度的数组(元组):使用类似于数组字面量的语法去表示
const arr3 : [string,number] = ["wjp",312]
这样arr3变量就只能存放一个包含两个元素的数组,且第一个元素是字符串类型,第二个是数字类型。
* 一个函数当中,同时要去返回多个返回值的时候,就可以使用元组的数据类型
3.对象类型
在flow中去描述对象类型的方式和对象字面量语法类似。
例1:表示当前变量当中所存放的对象必须具有foo和bar属性且类型必须为字符串和数字
const obj1:{foo:string,bar:number} = {
foo:"100",
bar:100
}
例2:可选属性:属性名后面添加一个?表示可选属性
const obj1:{foo:string,bar?:number} = {
foo:"100"
}
例3: 索引器语法:{ [string]:string } 表示当前对象允许添加任意个数的键值,但必须遵守类型
const obj1:{ [string]:string } = {
aaa:"100",
bbb:"200",
ccc:"300",
}
4.函数类型
对于函数的类型限制,一般指对函数参数类型和返回值的类型进行约束。
函数参数类型和返回值的类型在类型注解小结有介绍过。
除此之外,函数也可以作为一种类型用来存放变量。例如:类似于箭头函数的函数签名的类型。
//@flow
function foo( callBack:(string,number)=>void ){
callBack( 'wjp' , 100 )
};
foo(function(str,n){
console.log(str,n)
// 这里实参函数的类型必须符合 形参函数类型的限制
// 所以不允许有返回值 或者返回undefined
})
5.特殊类型
(1)字面量类型:限制变量必须是某一个值。
变量a的值必须等于'foo',等于其他字符串就会报错,更不能等于其他类型
const a:'foo' = 'foo'
一般不会单独使用,一般是配合联合类型的用法去组合几个值。
(2)联合类型(或类型):
type变量只能存放这三个字面量类型的值 其中之一。其他任何值都会报错。
const type: 'success'|'warnning'|'danger' = 'success';
也可以用普通类型来组合联合类型:
const b:string|number = 'wjp' //或者等于一个数字
(3)类型别名:(用type声明一个类型)
还可以用 type 关键词做一个单独的声明,声明一个类型,用来表示联合过后的一个结果。
type StringOrNumber = string|number;
const b:StringOrNumber = 'wjp' //或者等于一个数字
这里的StringOrNumber 就相当于一个类型别名,可以在多个地方使用了。
(4)maby类型(有可能):允许变量为空或者未定义
//变量gender必须是一个数字,不允许为空,所以会报错
const gender:number = null;
//在number类型前面添加了问号,表示它除了可以存放数字也可以存放null或undefined
const gender:?number = null;
//它等同于联合类型:
const gender:number|null|undefined= null;
maby类型就是在具体类型基础之上扩展了 null 和undefined两个值。
(5)mixed类型 和Any类型
相同点:Any类型和mixed类型都可以接收任意类型的值
//@flow
// mixed类型可以接收任意类型的值
//mixed类型其实就是所有类型的联合类型 string|number|boolean...
function passMixed(value:mixed){
}
passMixed('aaa')
passMixed(100)
// Any类型可以接收任意类型的值
function passAny(value:mixed){
}
passAny('aaa')
passAny(100)
差异:any是弱类型,mixed是强类型
在passAny内部,可以把参数value当作任意类型来使用:无论把它当作什么类型的数据来使用在语法上都不会报错-------也就是说any仍然是弱类型的。
// Any类型可以接收任意类型的值
function passAny(value:mixed){
//把value当作字符串来用
value.substr(1);
//把value当作数字来用
value*value
}
passAny('aaa')
passAny(100)
而mixed完全不一样,如果直接把它当作任意类型数据,直接调用各自数据类型的方法就直接会报语法错误
因为mixed类型是一个具体的类型,如果没有明确value是字符串,就不可以把它当作字符串使用,对于数字来讲也是一样的。
想要去明确mixed类型的数据是不是string,可以通过判断typeof的方式去明确,也就是以前类型判断的方式,就这样就不会报错了。想要使用数字的方式,就去判断一下是不是数字:
//@flow
// mixed类型可以接收任意类型的值
//mixed类型其实就是所有类型的联合类型 string|number|boolean...
function passMixed(value:mixed){
if( typeof value == 'string' ){
value.substr(1);
};
if( typeof value == 'number' ){
value*value
};
}
passMixed('aaa')
passMixed(100)
这样就不会报错了。
很明显,使用mixed类型仍然是类型安全的,因为如果在passMixed函数里随意使用value是有类型安全隐患的,仍然会报语法错误。需要通过判断来解决安全隐患。
相比较而言any就是不安全的。尽量不要使用any类型,any类型存在的意义是去兼容以前的老代码,因为曾经的老代码可能还会借助于js的弱类型或者动态类型去做一些特殊的情况,那些情况想要被兼容就需要any类型。
flow官方描述各个类型的文档:https://flow.org/en/docs/types
第三方的类型手册:https://www.saltycrane.com/cheat-sheets/flow-type/latest/