乐趣区

React项目从Javascript到Typescript的迁移经验总结

抛转引用

现在越来越多的项目放弃了 javascript,而选择拥抱了 typescript,就比如我们熟知的 ant-design 就是其中之一。面对越来越火的 typescript,我们公司今年也逐渐开始拥抱 typescript。至于为什么要使用 typescript?本文不做深入探讨,对这方面有兴趣的小伙伴们可以去看一下这篇文章:

TypeScript 体系调研报告

这篇文章比较全面地介绍了 TypeScript,并且和 Javascript 做了一个对比。看完上面这篇文章,你会对 TypeScript 有一个比较深入的认识,另外在 TypeScript 和 Javascript 的取舍上,可以拿捏得更好。

开始迁移

在开始迁移之前,我要说点题外话,本篇文章仅是记录我在迁移过程中遇到的问题以及我是如何解决的,并不会涉及 typescript 的教学。所以大家在阅读本篇文章之前,一定要对 typescript 有一个基础的认识,不然你读起来会非常费力。

环境调整

由于 Typescript 是 Javascript 的超集,它的很多语法浏览器是不能识别的,因此它不能直接运行在浏览器上,需要将其编译成 JavaScript 才能运行在浏览器上,这点跟 ES6 需要经过 babel 编译才能支持更多低版本的浏览器是一个道理。

tsconfig.json

首先我们得装一个 typescript,这就跟我们在用 babel 前需要先装一个 babel-core 是一个道理。

yarn global add typescript 

这条命令是将 typescript 安装在全局,其实我个人建议是装在项目目录下的,因为每个项目的 typescript 版本是不完全一样的,装在全局容易因为版本不同而出现问题。但是后面我要执行 tsc 命令,所以我装在了全局。最好的情况就是全局和项目都装一个,但是如果你把 tsc 命令放在 package.json 中的 script 中去用的话,那么在项目里装就够了。接下来我们执行如下命令生成 tsconfig.json, 这玩意就跟.babelrc 是一个性质的。

tsc --init

执行完之后,你的项目根目录下便会有一个 tsconfig.json 这么一个东西,但是里面会有很多注释,我们先不用管他的。

webpack

安装 ts-loader 用于处理 ts 和 tsx 文件,类似于 babel-loader。

yarn add ts-loader -D

相应的 webpack 需要加上 ts 的 loader 规则:

module.exports = {
    // 省略部分代码...
    module: {
        rules: [
            {
                test:/\.tsx?$/,
                loader:'ts-loader'
            }
            // 省略部分代码...
        ]
    }
    //... 省略部分代码
}

之前用 javascript 的时候,可能有人不使用.jsx 文件,整个项目都是用的.js 文件,webapck 里面甚至都不配.jsx 的规则。但是在 typescript 项目中想要全部使用.ts 文件这就行不通了,会报错,所以当用到了 jsx 的用法的时候,还是得乖乖用.tsx 文件,因此这里我加入了.tsx 的规则。

删除 babel

关于 babel 这块,网上有不少人是选择留着的,理由很简单,说是为了防止以后会使用到 JavaScript,但是我个人觉得是没有必要留着 babel。因为我们整个项目里面基本上只有使用第三方包的时候才会用到 javascript,而这些第三方包基本上都是已经编译成了 es5 的代码了,不需要 babel 再去处理一下。而业务逻辑里面用 javascript 更是不太可能了,因为这便失去了使用 typescript 的意义。综上所述,我个人觉得是要删除 babel 相关的东西,降低项目复杂度。但是有一个例外情况:。

那就是你用了某些 babel 插件, 而这些插件的功能 typescript 无法提供,那你可以保留 babel,并且与 typescript 结合。

文件名调整

整个 src 目下所有的.js 结尾的文件都要修改文件名,使用到 tsx 语法的就改成.tsx 文件,未使用的就改成.ts 文件,这块工作量比较大,会比较头疼。另外改完之后文件肯定会有很多标红的地方,不要急着去改它,后面我们分类统一去改。

解决报错

webpack 入口文件找不到


由于我们在做文件名调整的时候,把 main.js 改成 main.tsx, 因此 webpack 的入口文件要改成 main.tsx。

module.exports = {
    // 省略部分代码...
    entry: {app: './src/main.tsx'},
    // 省略部分代码...
}

提示不能使用 jsx 的语法


这个解决很简单,去 tsconfig 配置一下即可。

{
   "compilerOptions":{"jsx": "react"}
}

jsx 这个配置项有三个值可选择,分别是 ”preserve“,”react-native“ 和 ”react“。在 preservereact-native模式下生成代码中会保留 JSX 以供后续的转换操作使用(比如:Babel)。另外,preserve输出文件会带有.jsx 扩展名,而 react-native 是.js 拓展名。react模式会生成 React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。

模式 输入 输出 输出文件扩展名
preserve <div /> <div /> .jsx
react <div /> React.createElement(“div”) .js
react-native <div /> <div /> .js

webpack 里面配置的 alias 无法解析

module.exports = {
    // 省略部分代码...
    resolve: {
        alias:{'@':path.join(__dirname,'../src')
        }
        // 省略部分代码...    
    },
    // 省略部分代码...   
}


这里需要我们额外在 tsconfig.json 配置一下。

{
    "compilerOptions":{
        "baseUrl": ".",
        "paths": {"@/*":["./src/*"]
        } 
    }
}

具体如何配置,请看 typescript 的文档, 我就不展开介绍了,但是要注意的是 baseUrl 和 paths 一定要配合使用。

https://www.tslang.cn/docs/ha…

无法自动添加拓展名而导致找不到对应的模块


原先我们在 webpack 里是这么配置的:

module.exports = {
    // 省略部分代码... 
    resolve: {
        // 省略部分代码... 
        extensions: ['.js', '.jsx', '.json']
    },
    // 省略部分代码... 
}

但是我们项目里所有.js 和.jsx 的文件都改成了.ts 和.tsx 文件,因此配置需要调整。

{
    // 省略部分代码... 
    resolve: {
        // 省略部分代码... 
        extensions: ['.ts','.tsx','.js', '.jsx', '.json']
    },
    // 省略部分代码... 
}

Could not find a declaration file for module ‘**’

这个比较简单,它提示找不到哪个模块的声明文件,你就装个哪个模块的就好了,安装格式如下:

yarn add @types/**

举个????,如果提示 Could not find a declaration file for module ‘react’,那你应该执行如下命令:

yarn add @types/react

这个仅限于第三方包,如果是项目自己的模块提示缺少声明文件,那就需要你自己写对应的声明文件了,比如你在 window 这个全局对象挂载了一个对象,就需要做一下声明,否则就会报错。至于具体怎么写,这得看 typescript 的文档,这里就不展开说明了。

https://www.tslang.cn/docs/ha…

Cannot find type definition file for ‘**’


这些并没有在我们的业务代码里直接用到,而是第三方包用到的,遇到这种情况,需要检查一下 tsconfig.json 中的 typeRoots 这个配置项有没有配置错误。一般来说是不用配置 typeRoots,但是如果需要加入额外的声明文件路径,就需要对其进行修改。typeRoots 是有一个默认值,有人会误以为这个默认值是“[“node_modules”]”,因此会有人这样配置:

{
    "compilerOptions":{"typeRoots":["node_modules",...,"./src/types"]
    }
}

实际上 typeRoots 的默认值“[“@types”]”,所有可见的 ”@types” 包都会在编辑过程中被加载进来,比如“./node_modules/@types/”,“../node_modules/@types/”和“../../node_modules/@types/”等等都会被加载进来。所以遇到这种问题,你的配置应该改成:

{
    "compilerOptions":{"typeRoots":["@types",...,"./src/types"]
    }
}

在实际项目中,@types 基本上存在于根目录下的 node_modules 下,因此这里你可以改成这样:

{
    "compilerOptions":{"typeRoots":["node_modules/@types",...,"./src/types"]
    }
}

不支持 decorators(装饰器)


typescript 默认是关闭实验性的 ES 装饰器,所以需要在 tsconfig.json 中开启。

{
    "compilerOptions":{"experimentalDecorators":true}
}

Module ‘**’ has no default export


提示模块代码里没有“export
default”,而你却用“import from ”这种默认导入的形式。对于这个问题,我们需要把 tsconfig.json 配置项“allowSyntheticDefaultImports”设置为 true。允许从没有设置默认导出的模块中默认导入。不过不必担心会对代码产生什么影响,这个仅仅为了类型检查。

{
    "compilerOptions":{"allowSyntheticDefaultImports":true}
}

当然你也可以使用“esModuleInterop”这个配置项,将其设置为 true,根据“allowSyntheticDefaultImports”的默认值,如下:

module === "system" or --esModuleInterop

对于“esModuleInterop”这个配置项的作用主要有两点:

  • 提供__importStar 和__importDefault 两个 helper 来兼容 babel 生态
  • 开启 allowSyntheticDefaultImports

对于“esModuleInterop”和“allowSyntheticDefaultImports”选用上,如果需要 typescript 结合 babel,毫无疑问选“esModuleInterop”,否则的话,个人习惯选用“allowSyntheticDefaultImports”,比较喜欢需要啥用啥。当然“esModuleInterop”是最保险的选项,如果对此拿捏不准的话,那就乖乖地用“esModuleInterop”。

无法识别 document 和 window 这种全局对象


遇到这种情况,需要我们在 tsconfig.json 中 lib 这个配置项加入一个 dom 库,如下:

{
    "compilerOptions":{
        "lib":[
            "DOM",
            ...,
            "ESNext"
        ]
    }
}

文件中的标红问题

关于这个问题,我们需要分两种情况来考虑,第一种是.ts 的文件,第二种是.tsx 文件。下面来看一下具体是哪些注意的点(Ps:以下提到的注意的点并不能完全解决文件中标红的问题,但是可以解决大部分标红的问题):

第一种:.ts 文件

这种文件在你的项目比较少,比较容易处理,根据实际情况去加一下类型限制,没有特别需要讲的。

第二种:.tsx 文件

这种情况都是 react 组件了,而 react 组件又分为无状态组件和有状态组件组件,所以我们分开来看。

无状态组件

对于无状态组件,首先得限制他是一个 FunctionComponent(函数组件),其次限制其 props 类型。举个????:

import React, {FunctionComponent, ReactElement} from 'react';
import {LoadingComponentProps} from 'react-loadable';
import './style.scss';

interface LoadingProps extends LoadingComponentProps{
  loading:boolean,
  children?:ReactElement
}

const Loading:FunctionComponent<LoadingProps> = ({loading=true,children})=>{
  return (
    loading?<div className="comp-loading">
      <div className="item-1"></div>
      <div className="item-2"></div>
      <div className="item-3"></div>
      <div className="item-4"></div>
      <div className="item-5"></div>
    </div>:children
  )  
}
export default Loading;

其中你要是觉得 FunctionComponent 这个名字比较长,你可以选择用类型别名“SFC”或者“FC”。

有状态组件

对于有状态组件,主要注意三点:

  1. props 和 state 都要做类型限制
  2. state 用 readonly 限制“this.state=**”的操作
  3. 对 event 对象做类型限制
import React,{MouseEvent} from "react";
interface TeachersProps{user:User}
interface TeachersState{
  pageNo:number,
  pageSize:number,
  total:number,
  teacherList:{
    id: number,
    name: string,
    age: number,
    sex: number,
    tel: string,
    email: string
  }[]}
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = {
        pageNo:1,
        pageSize:20,
        total:0,
        userList:[]}
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{console.log(e.target);
    }
    //... 省略部分代码
    render(){return <div onClick={this.handleClick}> 点击我 </div>
    }
}

实际项目里,组件的 state 可能会有很多值,如果按照我们上面这种方式去写会比较麻烦,所以可以考虑一下下面这个简便写法:

import React,{MouseEvent} from "react";
interface TeachersProps{user:User}
const initialState = {
  pageNo:1,
  pageSize:20,
  total:0,
  teacherList:[]}
type TeachersState = Readonly<typeof initialState>
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = initialState
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{console.log(e.target);
    }
    //... 省略部分代码
    render(){return <div onClick={this.handleClick}> 点击我 </div>
    }
}

这种写法会简便很多代码,但是类型限制效果上明显不如第一种,所以这种方法仅仅作为参考,可根据实际情况去选择。

Ant Design 丢失样式文件

当我们把项目启动起来之后,某些同学的页面可能会出现样式丢失的情况,如下:

打开控制台,我们发现 Ant Design 的类名都找不到对应的样式:


出现这种情况是因为我们把 babel 删除之后,用来按需记载组建样式文件的 babel 插件 babel-plugin-import 也随着丢失了。不过 typescript 社区有一个 babel-plugin-import 的 Typescript 版本,叫做“ts-import-plugin”,我们先来安装一下:

yarn add ts-import-plugin -D

这个插件需要结合 ts-loader 使用,所以 webpack 配置中需要做如下调整:

const tsImportPluginFactory = require('ts-import-plugin')
module.exports = {
    // 省略部分代码...
    module:{
        rules:[{
            test: /\.tsx?$/,
            loader: "ts-loader",
            options: {
                transpileOnly: true,//(可选)getCustomTransformers: () => ({
                  before: [
                    tsImportPluginFactory({
                        libraryDirectory: 'es',
                        libraryName: 'antd',
                        style: true
                    })
                  ]
                })
            }
        }]
    }
    // 省略部分代码...
}

这里要注意一下 transpileOnly: true 这个配置,这是个可选配置,我建议是只有大项目中才加这个配置,小项目就没有必要了。由于 typescript 的语义检查器会在每次编译的时候检查所有文件,因此当项目很大的时候,编译时间会很长。解决这个问题的最简单的方法就是用 transpileOnly: true 这个配置去关闭 typescript 的语义检查,但是这样做的代价就是失去了类型检查以及声明文件的导出,所以除非在大项目中为了提升编译效率,否则不建议加这个配置。

配置完成之后,你的浏览器控制台可能会报出类似下面这个错误:

出现这个原因是因为你的 typescript 配置文件 tsconfig.json 中的 module 参数设置不对,两种情况会导致这个问题:

  • module 设置成了“commonjs”
  • target 设置 ”ES5″ 但是并未设置 module(当 target 不为“ES6”时,module 默认为“commonjs”)

解决这个办法就是把 module 设置为“ESNEXT”便可解决这个问题。

{
    "compilerOptions":{"module":"ESNext"}
}

可能会有小伙们说设置成“ES6”或者“ES2015”也是可以的,至于我为什么选择“ESNEXT”而不是“ES6”或者“ES2015”,主要原因是设置成“ES6”或者“ES2015”之后,就不能动态导入了,因为项目使用了 react-loadable 这个包,要是设置成“ES6”或者“ES2015”的话,会报如下这个错误:

typescript 提示我们需要设置成“commonjs”或者“ESNext”才可动态导入,所以保险起见,我是建议大家设置成 ESNext。完成之后我们的页面就可以正常显示了。

说到 module 参数,这里要再多提一嘴说一下 moduleResolution 这个参数,它决定着 typescript 如何处理模块。当我们把 module 设置成“ESNext”时,是可以不用管 moduleResolution 这个参数,但是大家项目里要是设置成“ES6”的话,那就要设置一下了。先看一下 moduleResolution 默认规则:

module === "AMD" or "System" or "ES6" ? "Classic" : "Node"

当我们 module 设置为“ES6”时,此时 moduleResolution 默认是“Classic”,而我们需要的是“Node”。为什么要选择“node”, 主要是因为 node 的模块解析规则更符合我们要求,解析速度会更快,至于详情的介绍,可以参考 Typescript 的文档。

https://www.tslang.cn/docs/ha…

同样为了保险起见,我是建议大家强行将 moduleResolution 设置为“node”。

总结

以上就是我自己在迁移过程中遇到的问题,可能无法覆盖大家在迁移过程中所遇到的问题,如果出现我上面没有涉及的报错,欢迎大家在评论区告诉我,我会尽可能地完善这篇文章。最后再强调一下,本篇文章仅仅只是介绍了我个人在迁移至 typescript 的经验总结,并未完全覆盖 tsconfig.json 的所有配置项,文章未涉及到的配置项,还需大家多花点时间看看 typescript 的文档。最后附上我已迁移到 typescript 的项目的地址:

项目地址: https://github.com/ruichengpi…

退出移动版