编译工具、代码检查

编译工具

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

compiler.png

可以看到未开启 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。

首先我们要安装 jestts-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 可以看到错误提示。