js 类型系统
最近纠结一个问题,前端的 js 由于其动态的特性,写几行代码,在浏览器刷新一下就能看到结果了,非常适合快速开发和迭代。但随着代码的规模越来越大,到了后期就会变得难以维护,任何的改动都有可能引入新的 bug,js 工程师需要花费越来越多的时间来调试修复各种 bug。
造成这样结果的原因有多样,而其中之一的原因,是由于 js 缺乏类型系统,导致我们无法通过工具来在开发的过程中检测到那些可能会发生的错误,也无法通过具体的类型定义来约束别人如何调用自己写的代码库。
这样的想法其实在 js 社区里已经得到越来越多人的认同,最近几年也陆续推出了多种不同的 js 类型系统用于增强 js 的健壮性,其中像typescript就是其中的佼佼者。当然我今天要讲的并不是 typescript,而是由 facebook 推出的flow。
flow 和 typescript 不同,typescript 是 js 的超集,是另外一门语言(向下兼容 js),而 flow,则是一个静态类型检测工具,并没有修改 js 的语言特性。flow 通过自动推断 js 代码中各个变量的类型,来约束代码的行为,举个例子,在 js 中对两个变量进行相加,在不同情况下会得到不一样的结果:
代码语言:javascript复制let strA = "hello ";
let strB = "world";
let numC = 1;
let numD = 2;
let objE = { "key": "value" };
let arrF = [ 1, 2, 3 ];
// 情况 1
strA srtB // "hello world"
// 情况 2
numC numD // 3;
// 情况 3
strA numC // "hello 1"
// 情况 4
strA objE // "hello [object Object]"
// 情况 5
strA arrF // hello 1,2,3"
上面的 5 种情况,在 js 中都是被允许的,然而情况 4 和 5,在一般情况下并不是我们所期望的。而在 flow 中,则只允许情况 1~3 通过检测,而对于情况 4 和 5 则直接报错了。
代码语言:javascript复制strA objE;
^^^^ object literal. This type cannot be added to
strA objE;
^^^^ string
strA arrF;
^^^^ array literal. This type cannot be added to
strA arrF;
^^^^ string
flow 除了可以自动的进行类型推断外,还可以通过类型声明的来进一步限制代码的行为,例如我们声明一个函数,接受一个参数,并返回一个字符串,如果我们不进行额外的类型声明,flow 默认是会接受 string 和 number 两种类型的参数
代码语言:javascript复制function hello(val) {
return "hello " val;
}
hello("world");
hello(1);
但如果我们希望我们的函数只接受 string 作为参数,并且明确返回 string,则可以
代码语言:javascript复制function hello(val: string): string {
return "hello " val;
}
这是如果再传入 number 则会报错,对于完整的 flow 使用,大家可以参考 这里
代码语言:javascript复制hello(1);
^ number. This type is incompatible with the expected param type of
function hello(val: string): string {
^^^^^^ string
flow 环境的搭建
要在项目中使用 flow,需要完成三件事情
第一安装 flow 命令行工具
代码语言:javascript复制$mkdir flow-proj && cd flow-proj
$npm init
$npm install flow-bin --save-dev
第二,编写 js 代码(hello.js),并在代码的第一行,加入注释 // [@flow][2]
// @flow
exports.hello = function (val: string): string {
return "hello " val;
}
这时候,flow 已经知道该文件是需要做类型检测的,这时候运行 flow init
命令初始化 flow
$./node_module/.bin/flow init
flow 会自动在该目录下创建.flowconfig 文件,接着我们运行 flow
命令,就可以在后台启动 flow 进程进行类型检测了
Spawned flow server (pid=28687)
Logs will go to /private/tmp/flow/zSUserszSlizZzZzZledzSDevzSflow-proj.log
No errors!
如果需要停止 flow 进程,只需要运行 flow stop
命令即可
$./node_module/.bin/flow stop
到现在为止,虽然 flow 已经可以正常运行了,然而因为我们在 js 代码里添加了额外类型声明,导致 js 代码不能直接在浏览器里执行,这时候我们需要做第三步,通过代码构建的方式,把 js 转换成浏览器能执行的形式。
这里我采用的 webpack babel 作为我们构建环境,所以首先我们需要安装 webpack 和 babel
代码语言:javascript复制$npm install webpack babel-core babel-loader --save-dev
然后我们需要安装 babel 的 flow 套装,使得 babel 支持 flow
代码语言:javascript复制$npm install babel-preset-flow --save-dev
然后我们编写 webpack 的配置文件 webpack.config.js
代码语言:javascript复制module.exports = {
entry: "./hello.js",
output: {
path: __dirname,
filename: "hello.bundle.js"
},
module: {
loaders: [
{
test: /.js$/,
exclude: /node_module/,
loaders: ["babel-loader"]
}
]
}
};
还有我们的 babel 配置文件.babelrc
代码语言:javascript复制{
presets: [
"flow"
]
}
到这里为止,我们已经完成了构建环境的配置,接下来执行 webpack
命令,就会产生我们的目标文件(hello.bundle.js)
$./node_modules/.bin/webpack
打开目标文件,我们就会发现,flow 的类型声明已经被正确去掉了。
到现在为止,整个 flow 的环境已经算搭建完成了,然而在写了没几行代码之后,我们就会发现,每次要对代码进行检测,都需要打开 terminal,敲上一堆命令才能看到结果,实在是不爽。有没有办法可以节省这些多余的工作,把 flow 集成到编辑器中呢?答案当然是肯定的。
这里我使用的编辑器是 sublime text3,如果有的同学是使用其他编辑器,可以在 这里,找一下
对与像我一样使用 st3 的同学,首先我们要在 st3 里安装 SublimeLinter 插件,Ctrl Shift P
打开 Package Control,选中 Package Control:Install Package。输入 SublimeLinter
,安装即可。
SublimeLinter 是一个语法校验的框架,但其本身并不会去做实际的校验工作,我们需要另外安装 SublimeLinter 的 flow 插件,同样是打开 Package Control,输入 SublimeLinter-flow
,安装即可。
这时 SublimeLinter-flow 就会在当前文件夹向上查找.flowconfig 文件,并对带有 // [@flow]
的文件进行自动检测,如果出现错误,就会直接在编辑器上提示,十分的方便。
加入 eslint 语法校验
除了类型检测,有时候我们还需要对 js 进行语法校验,当然很多成熟都工具都可以帮我们完成这样的功能,这里我使用的 eslint,对于其他的例如 jshint,jslint,小伙伴都可以自行 google。
首先我们要安装 eslint 以及 eslint-loader,使得 eslint 可以集成到 webpack 里
代码语言:javascript复制$npm install eslint eslint-loader --save-dev
安装好 eslint 后,就可以执行 eslint --init
命令来初始化 eslint 了
$npm ./node_modules/.bin/eslint --init
这时 eslint 就会问你一些问题,一步步帮你完成初始化的工作,并生成对应.eslintrc 文件,这时候,我们需要更新一下 webpack.config.js 文件,加入对 eslint 的支持,记得 eslint-loader 一定要写在 babel-loader 之后,这样 eslint 才能正确执行
代码语言:javascript复制module.exports = {
entry: "./hello.js",
output: {
path: __dirname,
filename: "hello.bundle.js"
},
module: {
loaders: [
{
test: /.js$/,
exclude: /node_module/,
loaders: ["babel-loader", "eslint-loader"]
}
]
}
};
不过如果这时候,你试着执行 webpack
命令进行构建,你会发现,报错了,原因是 eslint 不认识 flow 的类型声明语法。为了让 eslint 能通过 flow 的类型声明,我们需要安装两个工具,一个是 flow 的 eslint 插件 eslint-plugin-flowtype,另一个是 eslint 的 babel 版 js 解析器 babel-eslint,这是由于 eslint 默认的 espree 解析器认不得 flow 的类型声明
$npm install eslint-plugin-flowtype babel-eslint --save-dev
接着我们修改一下 eslint 的配置文件.eslintrc,把 parser
字段设置为 babel-eslint
,然后分别在 extends
和 plugins
加入 flow 相应的设置
{
"env": {
"node": true,
"browser": true,
"commonjs": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:flowtype/recommended",
],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module"
},
"plugins": [
"flowtype"
],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"]
}
}
done,现在我们再次执行 webpack
命令就能正常进行构建了。
最后最后,我们再通过在 sublime 中安装在 SublimeLinter 的插件 SublimeLinter-contrib-eslint 就可以让我们的编辑器也支持 eslint 的语法校验了。
这就是我这次给大家分享的,如何大家一个"摩登"的前端开发环境