47

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

 4 years ago
source link: https://segmentfault.com/a/1190000019075274?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

抛转引用

现在越来越多的项目放弃了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入口文件找不到

jQVZNvf.png!web

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

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

提示不能使用jsx的语法

NZNzmma.png!web

这个解决很简单,去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')
        }
        //省略部分代码...    
    },
    //省略部分代码...   
}

IfQ73eM.png!web

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

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

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

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

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

7NrUBvi.png!web

原先我们在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/**

举个:chestnut:,如果提示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 '**'

RVB7rqa.png!web

这些并没有在我们的业务代码里直接用到,而是第三方包用到的,遇到这种情况,需要检查一下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(装饰器)

eae6f2M.png!web

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

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

Module '**' has no default export

n6rI3aU.png!web

提示模块代码里没有“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这种全局对象

qYniqqZ.png!web

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

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

文件中的标红问题

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

第一种:.ts文件

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

第二种:.tsx文件

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

无状态组件

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

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丢失样式文件

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

ABZFrir.png!web

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

Jj26baA.png!web

出现这种情况是因为我们把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的语义检查,但是这样做的代价就是失去了类型检查以及声明文件的导出,所以除非在大项目中为了提升编译效率,否则不建议加这个配置。

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

yIvAJ3y.png!web

出现这个原因是因为你的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”的话,会报如下这个错误:

YjQ7Bfu.png!web

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

3IFFzqb.png!web

说到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...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK