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

抛转引用

如今愈来愈多的项目放弃了javascript,而选择拥抱了typescript,就好比咱们熟知的ant-design就是其中之一。面对愈来愈火的typescript,咱们公司今年也逐渐开始拥抱typescript。至于为何要使用typescript?本文不作深刻探讨,对这方面有兴趣的小伙伴们能够去看一下这篇文章:javascript

TypeScript体系调研报告

这篇文章比较全面地介绍了TypeScript,而且和Javascript作了一个对比。看完上面这篇文章,你会对TypeScript有一个比较深刻的认识,另外在TypeScript和Javascript的取舍上,能够拿捏得更好。css

开始迁移

在开始迁移以前,我要说点题外话,本篇文章仅是记录我在迁移过程当中遇到的问题以及我是如何解决的,并不会涉及typescript的教学。因此你们在阅读本篇文章以前,必定要对typescript有一个基础的认识,否则你读起来会很是费力。html

环境调整

因为Typescript是Javascript的超集,它的不少语法浏览器是不能识别的,所以它不能直接运行在浏览器上,须要将其编译成JavaScript才能运行在浏览器上,这点跟ES6须要通过babel编译才能支持更多低版本的浏览器是一个道理。java

tsconfig.json

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

yarn global add typescript
yarn add typescript

有些人会选择将typescript安装在全局环境上,可是我我的建议是装在项目目录下的,由于每一个项目的typescript版本是不彻底同样的,装在全局容易由于版本不一样而出现问题。若是须要用tsc命令的话,能够借助npx去实现。接下来咱们执行以下命令生成tsconfig.json,这玩意就跟.babelrc是一个性质的。react

npx tsc --init

执行完以后,你的项目根目录下便会有一个tsconfig.json这么一个东西,可是里面会有不少注释,咱们先不用管他的。webpack

webpack

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

yarn add ts-loader -D

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

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

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

删除babel

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

当你用了某些babel插件,而这些插件的功能恰巧是typescript没法提供的,那你能够保留babel,而且与typescript结合。

文件名调整

整个src目下全部的.js结尾的文件都要修改文件名,使用到jsx语法的就改为.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...