当前位置: 首页 > 科技观察

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

时间:2023-03-12 19:27:12 科技观察

投掷与引用现在越来越多的项目放弃了javascript而选择拥抱typescript,比如大家熟知的ant-design就是其中之一。面对越来越流行的typescript,我们公司今年也逐渐开始拥抱typescript。至于为什么要用打字稿?本文不深入。本文比较全面地介绍了TypeScript,并与Javascript做了对比。看完上面的文章,你会对TypeScript有更深入的了解。此外,您可以在TypeScript和Javascript之间做出更好的选择。在开始迁移之前,我想说一些题外话。本文只记录我在迁移过程中遇到的问题以及我是如何解决的,不涉及typescript的教学。所以在阅读这篇文章之前,你必须对typescript有一个基本的了解,否则你阅读起来会很吃力。环境调整由于Typescript是Javascript的超集,其语法很多浏览器无法识别,所以不能直接在浏览器上运行,需要编译成JavaScript才能在浏览器上运行,需要通过ES6传递babel编译可以支持更多低版本浏览器是个事实。tsconfig.json首先我们要安装一个typescript,这和我们在使用babel之前需要安装一个babel-core是一样的。yarnglobaladdtypescript命令是全局安装typescript。其实我个人比较推荐安装在项目目录下,因为每个项目的typescript版本不完全一样,全局安装容易因为版本不同而出现问题。但是后面想执行tsc命令,所以就全局安装了。最坏的情况是全局和项目都安装一个,但是如果你把tsc命令放在package.json中的脚本中使用,那么在项目中安装就可以了。接下来,我们执行下面的命令生成tsconfig.json,它和.babelrc是同一个性质。执行完tsc--init后,在你的项目根目录下会多出一个tsconfig.json,不过里面会有很多注释,暂且不管。webpack安装了ts-loader来处理ts和tsx文件,类似于babel-loader。yarnaddts-loader-D对应的webpack需要添加tsloader规则:module.exports={//省略部分代码...module:{rules:[{test:/\.tsx?$/,loader:'ts-loader'}//省略部分代码...]}//...省略部分代码}之前使用javascript的时候,有些人可能不会使用.jsx文件,整个项目都使用.js文件,甚至在webapck中都不符合.jsx规则。但是如果你想在typescript项目中使用所有的.ts文件,那是不行的,会报错,所以你在使用jsx用法的时候,还是要乖乖的使用.tsx文件,所以这里我加了.tsx文件。TSX规则。删除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={//Omitpartofthecode...entry:{app:'./src/main.tsx'},//Omitpartofthecode...}提示jsx的语法不能用过这个方案很简单,去tsconfig配置即可。{"compilerOptions":{"jsx":"react"}}jsx配置项有三个值可以选择,分别是"preserve"、"react-native"和"react"。JSX会保存在preserve和react-native模式下生成的代码中,用于后续的转换操作(eg:Babel)。此外,保留输出文件将具有.jsx扩展名,而react-native具有.js扩展名。react模式会生成React.createElement,使用前无需转换,输出文件后缀为.js。modeinputoutputoutputfileextensionpreserve

.jsxreact
React.createElement("div").jsreact-native
.js配置在webpack别名中不能resolvemodule.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:{//Omitsomecode...extensions:['.js','.jsx','.json']},//Omitsomecode...}Butall.jsandThe.jsx文件已更改为.ts和.tsx文件,因此需要调整配置。{//省略一些代码...resolve:{//省略一些代码...extensions:['.ts','.tsx','.js','.jsx','.json']},//省略部分代码...}Couldnotfindadeclarationfileformodule'**'这个比较简单,提示找不到哪个模块的声明文件,你就安装哪个模块,安装格式如下:yarnadd@types/**例如🌰,如果提示Couldnotfindadeclarationfileformodule'react',那么你应该执行如下命令:yarnadd@types/react这只限于第三方包,如果是项目自己的模块提示缺少声明文件,需要自己写相应的声明文件。比如你在全局对象窗口挂载了一个对象,如果需要使用它,就需要声明一下,否则会报错。至于怎么写,就看typescript文档了,这里就不多说了。https://www.tslang.cn/docs/ha...Cannotfindtypedefinitionfilefor'**'这些不是我们业务代码中直接使用的,而是第三方包使用的。此时需要检查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'**'hasnodefaultexport提示模块代码中没有"exportdefault",但是你使用了"importfrom"的默认导入形式。针对这个问题,我们需要将tsconfig.json的配置项“allowSyntheticDefaultImports”设置为true。允许从没有默认导出集的模块中进行默认导入。不过不用担心对代码的影响,这只是为了类型检查。{"compilerOptions":{"allowSyntheticDefaultImports":true}}当然你也可以使用配置项"esModuleInterop",设置为true。根据“allowSyntheticDefaultImports”的默认值,如下:module==="system"or--esModuleInterop对"esModuleInterop"配置项有两个主要作用:提供__importStar和__importDefault两个helper来兼容babelecology和enableallowSyntheticDefaultImports对于“esModuleInterop”和“allowSyntheticDefaultImports”的选择,如果typescript需要结合babel,没有问题就选择“esModuleInterop”,否则我习惯选择“allowSyntheticDefaultImports”,我更喜欢使用我需要的东西。当然,“esModuleInterop”是最安全的选择。如果你不确定这一点,那就乖乖用“esModuleInterop”吧。无法识别文档和窗口等全局对象。这种情况下,我们需要在tsconfig.json中的lib配置项中添加一个dom库,如下:{"compilerOptions":{"lib":["dom",...,"esNext"]}}的文件中的红色标记关于这个问题,我们需要考虑两种情况,第一种是.ts文件,第二种是.tsx文件。下面我们来看看具体需要注意的点(ps:下面说的几点不能完全解决文件中标红的问题,但是可以解决大部分标红的问题):***Type:.ts文件你项目中的文件比较少,比较好处理。根据实际情况添加类型限制,没有什么特别要说的。第二种:.tsx文件在本例中都是react组件,react组件分为stateless组件和stateful组件,下面分别来看。无状态组件对于无状态组件,首先要限制它是一个FunctionComponent(函数组件),其次要限制它的props类型。例如🌰:importReact,{FunctionComponent,ReactElement}from'react';import{LoadingComponentProps}from'react-loadable';import'./style.scss';interfaceLoadingPropsextendsLoadingComponentProps{loading:boolean,children?:ReactElement}constLoading:FunctionComponent=({loading=true,children})=>{return(loading?
:children)}exportdefaultLoading;其中,如果觉得FunctionComponent的名称比较长,可以选择使用类型别名“SFC”或“FC”。有状态组件对于有状态组件,主要需要注意三点:props和state都必须是类型限制state使用readonly来限制“this.state=**”的操作,event对象是类型限制importReact,{MouseEvent}来自“反应”;interfaceTeachersProps{user:User}interfaceTeachersState{pageNo:number,pageSize:number,total:number,teacherList:{id:number,name:string,age:number,sex:number,tel:string,email:string}[]}exportdefaultclassTeachersextendsReact.PureComponent{readonlystate={pageNo:1,pageSize:20,total:0,userList:[]}handleClick=(e:MouseEvent)=>{console.log(e.target);}//...省略部分代码render(){return点我
}}在实际项目中,状态组件可能有很多值,如果按照我们上面这种写法会比较麻烦,所以可以考虑如下简单的写法:importReact,{MouseEvent}from"react";interfaceTeachersProps{user:User}constinitialState={pageNo:1,pageSize:20,total:0,teacherList:[]}typeTeachersState=只读exportdefaultclassTeachersextendsReact.PureComponent{readonlystate=initialStatehandleClick=(e:MouseEvent)=>{console.log(e.target);}//...省略部分代码render(){return点我
}}这种写法会把代码简化很多,但是类型限制效果明显不如第一种,所以这个方法仅作为参考,各位可以根据实际情况选择AntDesign缺失的样式文件。我们启动项目后,部分同学的页面可能会丢失样式,如下:打开控制台,发现AntDesign的类名找不到对应的样式:出现这种情况是因为我们把babel删除后,babel用于按需加载组件样式文件的插件babel-plugin-import也丢失了。然而,typescript社区有一个名为“ts-import-plugin”的babel-plugin-import的Typescript版本。我们先安装:yarnaddts-import-plugin-D这个插件需要和ts-loader配合使用,所以webpack的配置需要做如下调整:consttsImportPluginFactory=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参数没有设置正确。有两种情况会导致这个问题:moduleissetto“commonjs”目标设置为“ES5”但没有设置模块(当目标不是“ES6”时,模块默认为“commonjs”)。这个问题的解决方法是将模块设置为“esNext”来解决这个问题。{"compilerOptions":{"module":"esNext"}}有小伙伴可能会说设置成“ES6”或者“ES2015”也是可以的。至于为什么我选择“esNext”而不是“ES6”或“ES2015”,主要是因为设置为“ES6”或“ES2015”后无法动态导入,因为项目使用了react-loadable包裹。如果设置为“ES6”或者“ES2015”,会报如下错误:typescript提醒我们需要设置为“commonjs”或者“ESNext”才可以动态导入,所以要在安全起见,我建议你将它设置为ESNext。完成后,我们的页面就可以正常显示了。说到模块参数,这里稍微多说一下moduleResolution参数,它决定了typescript如何处理模块。当我们设置module为“esNext”时,可以忽略moduleResolution参数,但是如果你在项目中设置为“ES6”,则需要设置。首先看一下moduleResolution的默认规则:module==="AMD"or"System"or"ES6"?"Classic":"Node"当我们的模块设置为"ES6"时,默认的moduleResolution是"Classic",而我们需要的是“节点”。之所以选择“node”,主要是因为node的模块解析规则更符合我们的要求,解析速度会更快。具体可以参考Typescript文档。https://www.tslang.cn/docs/ha...同样为了保险起见,建议大家强行设置moduleResolution为“node”。综上所述,以上是我在迁移过程中遇到的问题,可能无法涵盖您在迁移过程中遇到的问题。如果有上面我没有涉及到的报错,请在评论区告诉我,我会尽力完善这篇文章。***再次声明,本文只是介绍我个人迁移到typescript的经验,并没有完全涵盖tsconfig.json的所有配置项。文中没有涉及到的配置项需要多花点时间看看TypeScript的文档。***附上我迁移到typescript的项目地址:项目地址:https://github.com/ruichengpi...