yaoningvital / blog

my blog
31 stars 4 forks source link

如何在一个已经存在的 React/Babel/Webpack 项目中添加TypeScript ? #178

Open yaoningvital opened 4 years ago

yaoningvital commented 4 years ago

示例:https://github.com/Microsoft/TypeScript-React-Conversion-Guide#typescript-react-conversion-guide

示例中是一个九宫格的小游戏,完全由JS编写,按照下面的步骤操作完成后,将变成一个完全由TypeScript 编写的小游戏。

在一个项目中添加TypeScript 可以分解为以下两个步骤: 1、在构建流程中添加 TypeScript 编译器(TypeScript Compiler)。 2、将 JS 文件转为 TS 文件。

在动手之前,首先要充分地理解当前这个项目,了解它的项目结构、代码逻辑、用的什么JS编译器、什么打包工具等等。

1、在构建流程中添加 TypeScript 编译器

step1: 安装依赖

先安装 package.json 中定义的依赖

命令行切换到TicTacToe_JS 目录,先安装 package.json 中定义的依赖:

npm install

安装 typescript、ts-loader、source-map-loader

npm install --save-dev typescript ts-loader source-map-loader

1、ts-loader 是一个webpack插件,作用是将 TypeScript 文件编译为 JavaScript 文件。就像Babel的 babel-loader 一样。还有其他可选的 loader 也可以完成这个工作(比如 awesome-typescript-loader)。 2、source-map-loader 为调试时增加 source-map 支持。

为项目中用到的库,从@types中获取类型声明文件(.d.ts文件)

这个项目用到了 react 、react-dom。

npm install --save-dev @types/react @types/react-dom

step2:配置TypeScript

在项目根目录下添加 tsconfig.json 文件,来配置TypeScript。添加以下内容:

// tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist/",        // path to output directory
        "sourceMap": true,          // allow sourcemap support
        "strictNullChecks": true,   // enable strict null checks as a best practice
        "module": "es6",            // specify module code generation
        "jsx": "react",             // use typescript to transpile jsx to js
        "target": "es5",            // specify ECMAScript target version
        "allowJs": true             // allow a partial TypeScript and JavaScript codebase

    },
    "include": [
        "./src/"
    ]
}

step3:设置构建流程

要在构建流程中添加对TypeScript的编译,需要修改webpack配置文件 webpack.config.js 。

这里假设用的构建工具是 webpack ,只说明怎么修改 webpack.config.js 。如果你用的是其他的构建工具,比如 Gulp,方法是一致的,就是将构建流程中的 Babel构建步骤 用 TypeScript 取代。因为TypeScript也提供了 将JS编译为低版本JS 和 编译JSX 的功能,而且在大多数情况下,编译时间更短。当然也可以保留Babel,可以在构建流程中添加 TypeScript 的编译,然后将输出传入 Babel编译器。

总的来说,我们需要对 webpack.config.js 文件进行如下方面的修改: 1、对文件扩展名的识别要添加对 .ts.tsx的识别。 2、将 babel-loader 取代为 ts-loader。 3、增加 source-map 支持。

修改后的 webpack.config.js 文件内容如下:

module.exports = {
  // change to .tsx if necessary
  entry: './src/app.jsx',
  output: {
    filename: './dist/bundle.js'
  },
  resolve: {
    // changed from extensions: [".js", ".jsx"]
    extensions: [".ts", ".tsx", ".js", ".jsx"]
  },
  module: {
    rules: [
      // changed from { test: /\.jsx?$/, use: { loader: 'babel-loader' }, exclude: /node_modules/ },
      { test: /\.(t|j)sx?$/, use: { loader: 'ts-loader' }, exclude: /node_modules/ },

      // addition - add source-map support
      { enforce: "pre", test: /\.js$/, exclude: /node_modules/, loader: "source-map-loader" }
    ]
  },
  externals: {
    "react": "React",
    "react-dom": "ReactDOM",
  },
  // addition - add source-map support
  devtool: "source-map"
}

如果不再需要 babel 了,可以删除 .babelrc 文件 和 package.json 中所有对 babel 的依赖。

现在,你已经完成了在构建流程中添加TypeScript编译器的操作。可以用下面的命令打包,并且在浏览器中打开 index.html 文件查看页面。

npx webpack

2、将 JS 文件转为 TS 文件

在这个部分,我们将按照下面的步骤逐步地完成 :

step1:最少的转换步骤

gameStateBar.jsx 文件为例。

第一步是将 gameStateBar.jsx 重命名为 gameStateBar.tsx。改变名字之后,在编辑器中能看到类型报错。

需要将第一行的:

import React from 'react'

改为:

`import * as React from 'react'

这是因为原来用Babel编译时,Babel将 CommonJS 模块中的 module.exports当做默认导出的对象,但是 TypeScript不是这样处理的。

将第三行的 :

export class GameStateBar extends React.Component {

改为:

export class GameStateBar extends React.Component<any, any> {

这是因为,React.Component 的类型定义使用了 泛型,需要提供这个组件实例的 props 属性 和 state 属性对象的类型。 我们在这里将 这两个对象(props和state)的类型设置为 any,表示可以传递任何类型的值。 any 对真正的类型检查 并不会起到什么作用,但是满足了我们第一步的目标:使用最少的步骤来通过TypeScript编译器的类型检查。

到这里,这个文件已经能通过TypeScript编译器的类型检查了。使用npx webpack命令打包,然后用浏览器打开 index.html 文件查看。

step2:添加类型

提供越多的类型给TypeScript,就能得到越强大的类型检查的功能。

作为一个最佳实践,我们推荐对所有的声明提供类型。

接下来,还是以 gameStateBar 组件举例。

对于任何的 React.Component ,应当给出 props 和 state 对象的 合适的类型定义。 gameStateBar 组件没有properties,所以我们可以使用 {}当类型。

组件的 state 对象 只包含一个属性:gameState,它表示的是游戏的状态,可能的值有:

"" | "X Wins!" | "O Wins!" | "Draw"

因为 gameState 只有确定的已知的字符串字面量值,所以可以使用“字符串字面量类型”来定义这个接口,如下:

interface GameStateBarState {
    gameState: "" | "X Wins!" | "O Wins!" | "Draw";
}

这个接口定义需要写在类声明的前面。

使用这个定义的接口, GameStateBar 的声明修改为:

export class GameStateBar extends React.Component<{}, GameStateBarState> {...}

接下来还可以做这些事情:

// 给 函数的参数 添加类型声明
// add types for params
constructor(props: {}) {...}
handleGameStateChange(e: CustomEvent) {...}
handleRestart(e: Event) {...}
// 给 箭头函数的参数 添加类型声明
// add types in arrow functions
componentDidMount() {
    window.addEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e));
    window.addEventListener("restart", (e: CustomEvent) => this.handleRestart(e));
}

componentWillUnmount() {
    window.removeEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e));
    window.removeEventListener("restart", (e: CustomEvent) => this.handleRestart(e));
}

为了使用更加严格的类型声明,可以在 tsconfig.json 中配置一些有用的编译选项。

比如使用 noImplicitAny 选项,如果将它设置为true,那么如果代码中有隐式指明类型为 any 的地方,TypeScript 编译器就会报错。比如:

function logMe(x) {
  console.log(x);
}
// error TS7006: Parameter 'x' implicitly has an 'any' type.

这里的x的类型可以是任何值,但是没有用 any 做出明确的类型声明,所以就会报错。 必须明确声明 x 的类型为 any ,修改为:

function logMe(x: any) {
  console.log(x);
}
 // OK

你还可以为类的成员添加private/protected 修饰器,来实现类成员的访问控制。可以给 handleGameStateChange 和 handleRestart 两个方法添加 private 修饰器,因为它们是组件的内部方法:

private handleGameStateChange(e: CustomEvent) {...}
private handleRestart(e: Event) {...}

现在,可以使用npx webpack打包,用浏览器打开 index.html 文件查看。

step3:在整个代码中采用TypeScript

在全部代码中采用TypeScript,就是或多或少地在所有的 .js .jsx 文件中 重复前面的两个步骤。 全部转换完之后,重新打包,用浏览器打开 index.html 文件查看。