编译工具、代码检查
编译工具
ts-loader
我们搭建的 webpack 环境中,使用 ts-loader 进行编译。ts-loader 内部使用了官方的编译器 tsc。
ts-loader 和 tsc 共享 tsconfig.json 文件。此外 ts-loader 还有自己的一些配置。可以通过 options 属性来传入。
// webpack.base.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ...
module: {
rules: [
{
test: /\.tsx?$/i,
use: [{
loader: 'ts-loader',
options: {
// 当这个配置项开启后,只做语言转换而不做类型检查
// 实际项目中,你会发现随着项目越来越大,构建时间越来越长,开启下面这个配置就会启动一种快速构建模式
transpileOnly: false,
}
}],
exclude: /node_modules/
}
]
},
// ...
}
js
可以看到未开启 transpileOnly
编译需要 3.15s,开启后编译只需要 1.79s。不过这个模式也有缺点,就是编译时不能做类型检查。
// src/index.ts
;(() => {
let hello: string = 'hello world'
document.querySelectorAll('.app')[0].innerHTML = hello
hello = 1
})();
typescript
上述代码即使在 vscode 中会有错误提示,但是不会影响 ts 打包编译(开启 transpileOnly 时)。
那么如何在开始 transpileOnly
的时候做类型检查那,我们可以借助一个插件来实现,它会把类型检查放到一个独立的进程中进行。
pnpm i fork-ts-checker-webpack-plugin -D
shell
// webpack.base.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
// ...
module: {
rules: [
{
test: /\.tsx?$/i,
use: [{
loader: 'ts-loader',
options: {
// 当这个配置项开启后,只做语言转换而不做类型检查
// 实际项目中,你会发现随着项目越来越大,构建时间越来越长,开启下面这个配置就会启动一种快速构建模式
transpileOnly: true,
}
}],
exclude: /node_modules/
}
]
},
plugins: [
// ...
new ForkTsCheckerWebpackPlugin()
]
}
js
重新进行打包构建可以发现会提示错误。
awesome-typescript-loader
与 ts-loader 主要区别:
- 更适合与 Babel 集成,使用 Babel 的转义和缓存
- 不需要按照额外的插件,就可以把类型检查放在独立进程中执行
pnpm i awesome-typescript-loader -D
shell
然后我们来修改一下 webpack 配置。
const HtmlWebpackPlugin = require('html-webpack-plugin')
// const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const { CheckerPlugin } = require('awesome-typescript-loader')
module.exports = {
entry: './src/index.ts',
output: {
filename: 'app.js'
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
},
module: {
rules: [
{
test: /\.tsx?$/i,
// use: [{
// loader: 'ts-loader',
// options: {
// // 当这个配置项开启后,只做语言转换而不做类型检查
// // 实际项目中,你会发现随着项目越来越大,构建时间越来越长,开启下面这个配置就会启动一种快速构建模式
// transpileOnly: true,
// }
// }],
use: [{
loader: 'awesome-typescript-loader',
options: {
transpileOnly: true,
}
}],
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tpl/index.html'
}),
// new ForkTsCheckerWebpackPlugin()
new CheckerPlugin()
]
}
json
最后我们来对比一下两个 loader。
loader | 默认配置 | transpileOnly | transpileOnly + 类型检查 |
---|---|---|---|
ts-loader | 1600+ | 500+ | 3000+(时间较长) |
awesome-typescript-loader | 2200+ | 1600+ | 1600+(类型检查存在遗漏) |
就目前来看,其实推荐使用 ts-loader
默认配置就可以了。
ts 与 babel
我们使用了 ts,为什么还要继续使用 Babel?
编译能力 | 类型检查 | 插件 | |
---|---|---|---|
tsc | ts(x)、js(x) => es3/5/6… | 有 | 无 |
babel | ts(x)、js(x) => es3/5/6… | 无 | 非常丰富 |
babel 7 之前是不支持 ts 的,对于已经使用了 babel 的项目,如果想使用 ts,并不是一件非常容易的事情。需要将用 ts 相关 loader 将 ts 转换为 js,然后再交由 babel 处理。
babel 7 之后,babel 目前已经支持 ts,babel 在编译时可以不使用 ts loader,只让 ts 负责类型检查。
下面我们使用 babel 重新创建一个工程。
.
├── dist
├── node_modules
│ └── ...
├── package.json
├── src
│ ├── index.ts
├── .babelrc
└── package.json
// src/index.ts
class A {
a: number = 1
}
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4, c: 5 }
let n = { x, y, ...z }
ts
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}
json
// package.json
{
"name": "ts-babel",
"version": "1.0.0",
"description": "",
"main": "./src/index.js",
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts,.tsx\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-typescript": "^7.3.3"
}
}
json
以上就是一个最简的 babel + ts 的工程化配置。你可以看到,我们并没有安装 ts,而是直接使用 babel 作为编译工具编译 ts 文件。
不过 babel 是不能进行类型检查的,所以我们还需要引入 ts 进行类型检查。
pnpm install typescript -D
shell
tsc --init
shell
我们需要在 tsconfig.json 中开启 noEmit
选项。这个选项代表 ts 不会输出任何文件,只会做类型检查。
然后我们在添加一个类型检查脚本。
// package.json
{
"name": "ts-babel",
"version": "1.0.0",
"description": "",
"main": "./src/index.js",
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts,.tsx\"",
"type-check": "tsc --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-typescript": "^7.3.3",
"typescript": "^3.5.2"
}
}
json
pnpm run type-check
shell
这时 ts 就会实时监控编码中的类型错误。这样我们就把 babel 和 ts 结合在一起。babel 只做语言转换,ts 只做类型检查。
在 babel 中是无法编译默认导出语法,会报错。
export = A
typescript
总结
如何选择 ts 编译工具?
- 如果没有使用 babel,首选 ts 自身的编译器,可以配合 ts-loader 使用
- 如果项目中已经使用 babel,安装
@babel/preset-typescript
,可以配合 tsc 做类型检查 - 两种编译工具建议不要混用,这样只会增加工程复杂度
代码检查工具
目前使用 ts 主要有两种代码检查工具,分别是 TSLint 和 ESLint。
由于某些原因,官方由 TypeScript 转向 ESLint:
- TSLint 执行规则的模式存在一些架构问题,从而影响性能,修复这些问题会破坏现有规则;
- ESLint 的性能更好,并且社区用户熟悉 ESLint 的规则配置(比如针对 React 和 Vue 的规则),但是对 TSLint 并不熟悉。
使用 TypeScript ,为什么还需要 ESLint?
TypeScript 主要做两件事,分别是类型检查和语言转换,在这个过程中也会对语法错误进行检查。
ESLint 除了可以检查语法错误,还可以保证代码风格的统一。两者的功能有部分重合,但是也各自有各自的职责。
但是如果要使用 ESLint 去检查 TS 语法,就会面临一些问题,它们在工作之前,都需要把代码转换成抽象语法树,即 AST。而这两种语法树是不兼容的,相反,TSLint 是完全基于 TSLint 的语法树进行工作的,不会存在兼容性问题,缺点就是无法做到重用(这也是官方放弃 TSLint 的主要原因)。那么如何解决这个问题?我们可以使用 typescript-eslint
项目。它为 ESLint 提供了解析 TS 代码的编译器,可以把 TS 代码的语法树转换为 ESLint 所期望的语法树,即 ESTree。
下面我们就来看一下如何在 TS 中使用 ESLint。我们基于之前的 ts-config
项目进行改造,项目为为 ts-eslint
。
我们需要安装 eslint
,@typescript-eslint/eslint-plugin
和 @typescript-eslint/parser
。
然后我们看下 eslint 配置。
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"project": "./ts-eslint/tsconfig.json"
},
"extends": [
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-inferrable-types": "off"
}
}
json
然后我们添加 lint
脚本。
// package.json
"scripts": {
"start": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
"build": "webpack --mode=production --config ./build/webpack.config.js",
"lint": "eslint src --ext .js,.ts"
}
json
我们可以使用 pnpm run lint
执行脚本。
除了使用脚本做代码检查,我们还可以安装 eslint 插件来辅助我们开发。例如 vscode 的 eslint 插件。
// settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"html",
"vue"
]
}
json
这样 vscode 就会在文件保存时使用 eslint 格式化代码。
如果你使用 babel,可以使用 babel-eslint。
- babel-eslint:支持 ts 没有的额外的语法检查,抛弃 ts,不支持类型检查
- typescript-eslint:基于 ts 的 AST,支持创建基于类型信息的规则(tsconfig.json)
建议:两者底层机制不同,不要混用。Babel 体系建议使用 babel-eslint。
Jest 单元测试
单元测试可以使用 ts-jest 和或者babel-jest。
首先我们要安装 jest
和 ts-jest
。
pnpm i jest ts-jest @types/jest -D
shell
下面我们配置脚本文件。
// package.json
"scripts": {
"start": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
"build": "webpack --mode=production --config ./build/webpack.config.js",
"lint": "eslint src --ext .js,.ts",
"test": "jest"
}
json
然后通过以下命令创建 jest 配置文件。
npx ts-jest config:init
shell
// jest.config.js
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
js
我们在 src 目录下新建 math.ts
。
// src/math.ts
function add (a: number, b: number) {
return a + b
}
function sub (a: number, b: number) {
return a - b
}
export default {
add,
sub
}
typescript
我们在根目录新建 test
文件夹,然后建立 math.test.ts
文件。
// test/math.test.ts
import math from '../src/math'
test('add: 1 + 1 = 2', () => {
expect(math.add(1, 1)).toBe(2)
})
test('sub: 1 - 2 = -1', () => {
expect(math.sub(1, 2)).toBe(-1)
})
typescript
然后我们执行 pnpm run test
可以看到两个测试用例都已经通过。使用 ts-jest 的好处就是它能够在测试用例中进行类型检查。
// test/math.test.ts
import math from '../src/math'
test('add: 1 + 1 = 2', () => {
expect(math.add(1, 1)).toBe(2)
})
test('sub: 1 - 2 = -1', () => {
expect(math.sub(1, 2)).toBe(-1)
})
let x: number = '1'
typescript
再次运行错误,可以看到错误提示。
接下来我们在使用 babel-jest
进行测试,首先我们打开之前创建的 ts-babel
工程。同样我们需要安装 jest ,同时也要安装 babel-jest
。
pnpm i jest babel-jest @types/jest -D
shell
jest 安装完毕后,我们把刚才创建的文件拷贝过来。然后编写测试脚本。
// package.json
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts,.tsx\"",
"type-check": "tsc --watch",
"test": "jest"
},
json
执行脚本 pnpm run test
也可以正常通过测试。但是它不会进行类型检查。如果想让 babel-jest
支持类型检查,我们还需要提供类型检查脚本。我们执行 pnpm run type-check
可以看到错误提示。