React項(xiàng)目從Javascript到Typescript的遷移經(jīng)驗(yàn)總結(jié)
拋轉(zhuǎn)引用
現(xiàn)在越來越多的項(xiàng)目放棄了javascript,而選擇擁抱了typescript,就比如我們熟知的ant-design就是其中之一。面對(duì)越來越火的typescript,我們公司今年也逐漸開始擁抱typescript。至于為什么要使用typescript?本文不做深入探討。
這篇文章比較全面地介紹了TypeScript,并且和Javascript做了一個(gè)對(duì)比。看完上面這篇文章,你會(huì)對(duì)TypeScript有一個(gè)比較深入的認(rèn)識(shí),另外在TypeScript和Javascript的取舍上,可以拿捏得更好。
開始遷移
在開始遷移之前,我要說點(diǎn)題外話,本篇文章僅是記錄我在遷移過程中遇到的問題以及我是如何解決的,并不會(huì)涉及typescript的教學(xué)。所以大家在閱讀本篇文章之前,一定要對(duì)typescript有一個(gè)基礎(chǔ)的認(rèn)識(shí),不然你讀起來會(huì)非常費(fèi)力。
環(huán)境調(diào)整
由于Typescript是Javascript的超集,它的很多語法瀏覽器是不能識(shí)別的,因此它不能直接運(yùn)行在瀏覽器上,需要將其編譯成JavaScript才能運(yùn)行在瀏覽器上,這點(diǎn)跟ES6需要經(jīng)過babel編譯才能支持更多低版本的瀏覽器是一個(gè)道理。
tsconfig.json
首先我們得裝一個(gè)typescript,這就跟我們?cè)谟胋abel前需要先裝一個(gè)babel-core是一個(gè)道理。
- yarn global add typescript
這條命令是將typescript安裝在全局,其實(shí)我個(gè)人建議是裝在項(xiàng)目目錄下的,因?yàn)槊總€(gè)項(xiàng)目的typescript版本是不完全一樣的,裝在全局容易因?yàn)榘姹静煌霈F(xiàn)問題。但是后面我要執(zhí)行tsc命令,所以我裝在了全局。***的情況就是全局和項(xiàng)目都裝一個(gè),但是如果你把tsc命令放在package.json中的script中去用的話,那么在項(xiàng)目里裝就夠了。接下來我們執(zhí)行如下命令生成tsconfig.json,這玩意就跟.babelrc是一個(gè)性質(zhì)的。
- tsc --init
執(zhí)行完之后,你的項(xiàng)目根目錄下便會(huì)有一個(gè)tsconfig.json這么一個(gè)東西,但是里面會(huì)有很多注釋,我們先不用管他的。
webpack
安裝ts-loader用于處理ts和tsx文件,類似于babel-loader。
- yarn add ts-loader -D
相應(yīng)的webpack需要加上ts的loader規(guī)則:
- module.exports = {
- //省略部分代碼...
- module: {
- rules: [
- {
- test:/\.tsx?$/,
- loader:'ts-loader'
- }
- //省略部分代碼...
- ]
- }
- //...省略部分代碼
- }
之前用javascript的時(shí)候,可能有人不使用.jsx文件,整個(gè)項(xiàng)目都是用的.js文件,webapck里面甚至都不配.jsx的規(guī)則。但是在typescript項(xiàng)目中想要全部使用.ts文件這就行不通了,會(huì)報(bào)錯(cuò),所以當(dāng)用到了jsx的用法的時(shí)候,還是得乖乖用.tsx文件,因此這里我加入了.tsx的規(guī)則。
刪除babel
關(guān)于babel這塊,網(wǎng)上有不少人是選擇留著的,理由很簡(jiǎn)單,說是為了防止以后會(huì)使用到JavaScript,但是我個(gè)人覺得是沒有必要留著babel。因?yàn)槲覀冋麄€(gè)項(xiàng)目里面基本上只有使用第三方包的時(shí)候才會(huì)用到j(luò)avascript,而這些第三方包基本上都是已經(jīng)編譯成了es5的代碼了,不需要babel再去處理一下。而業(yè)務(wù)邏輯里面用javascript更是不太可能了,因?yàn)檫@便失去了使用typescript的意義。綜上所述,我個(gè)人覺得是要?jiǎng)h除babel相關(guān)的東西,降低項(xiàng)目復(fù)雜度。但是有一個(gè)例外情況:。
當(dāng)你用了某些babel插件,而這些插件的功能恰巧是typescript無法提供的,那你可以保留babel,并且與typescript結(jié)合。
文件名調(diào)整
整個(gè)src目下所有的.js結(jié)尾的文件都要修改文件名,使用到j(luò)sx語法的就改成.tsx文件,未使用的就改成.ts文件,這塊工作量比較大,會(huì)比較頭疼。另外改完之后文件肯定會(huì)有很多標(biāo)紅的地方,不要急著去改它,后面我們分類統(tǒng)一去改。
解決報(bào)錯(cuò)
webpack入口文件找不到
由于我們?cè)谧鑫募{(diào)整的時(shí)候,把main.js改成main.tsx,因此webpack的入口文件要改成main.tsx。
- module.exports = {
- //省略部分代碼...
- entry: {
- app: './src/main.tsx'
- },
- //省略部分代碼...
- }
提示不能使用jsx的語法
這個(gè)解決很簡(jiǎn)單,去tsconfig配置一下即可。
- {
- "compilerOptions":{
- "jsx": "react"
- }
- }
jsx這個(gè)配置項(xiàng)有三個(gè)值可選擇,分別是"preserve","react-native"和"react"。在preserve和react-native模式下生成代碼中會(huì)保留JSX以供后續(xù)的轉(zhuǎn)換操作使用(比如:Babel)。另外,preserve輸出文件會(huì)帶有.jsx擴(kuò)展名,而react-native是.js拓展名。react模式會(huì)生成React.createElement,在使用前不需要再進(jìn)行轉(zhuǎn)換操作了,輸出文件的擴(kuò)展名為.js。
模式 | 輸入 | 輸出 | 輸出文件擴(kuò)展名 |
---|---|---|---|
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/*"]
- }
- }
- }
具體如何配置,請(qǐng)看typescript的文檔,我就不展開介紹了,但是要注意的是baseUrl和paths一定要配合使用。
https://www.tslang.cn/docs/ha...
無法自動(dòng)添加拓展名而導(dǎo)致找不到對(duì)應(yīng)的模塊
原先我們?cè)趙ebpack里是這么配置的:
- module.exports = {
- //省略部分代碼...
- resolve: {
- //省略部分代碼...
- extensions: ['.js', '.jsx', '.json']
- },
- //省略部分代碼...
- }
但是我們項(xiàng)目里所有.js和.jsx的文件都改成了.ts和.tsx文件,因此配置需要調(diào)整。
- {
- //省略部分代碼...
- resolve: {
- //省略部分代碼...
- extensions: ['.ts','.tsx','.js', '.jsx', '.json']
- },
- //省略部分代碼...
- }
Could not find a declaration file for module '**'
這個(gè)比較簡(jiǎn)單,它提示找不到哪個(gè)模塊的聲明文件,你就裝個(gè)哪個(gè)模塊的就好了,安裝格式如下:
- yarn add @types/**
舉個(gè)🌰,如果提示Could not find a declaration file for module 'react',那你應(yīng)該執(zhí)行如下命令:
- yarn add @types/react
這個(gè)僅限于第三方包,如果是項(xiàng)目自己的模塊提示缺少聲明文件,那就需要你自己寫對(duì)應(yīng)的聲明文件了。比如你在window這個(gè)全局對(duì)象上掛載了一個(gè)對(duì)象,如果需要使用它的話,就需要做一下聲明,否則就會(huì)報(bào)錯(cuò)。至于具體怎么寫,這得看typescript的文檔,這里就不展開說明了。
https://www.tslang.cn/docs/ha...
Cannot find type definition file for '**'
這些并沒有在我們的業(yè)務(wù)代碼里直接用到,而是第三方包用到的,遇到這種情況,需要檢查一下tsconfig.json中的typeRoots這個(gè)配置項(xiàng)有沒有配置錯(cuò)誤。一般來說是不用配置typeRoots,但是如果需要加入額外的聲明文件路徑,就需要對(duì)其進(jìn)行修改。typeRoots是有一個(gè)默認(rèn)值,有人會(huì)誤以為這個(gè)默認(rèn)值是“["node_modules"]”,因此會(huì)有人這樣配置:
- {
- "compilerOptions":{
- "typeRoots":["node_modules",...,"./src/types"]
- }
- }
實(shí)際上typeRoots的默認(rèn)值“["@types"]”,所有可見的"@types"包都會(huì)在編輯過程中被加載進(jìn)來,比如“./node_modules/@types/”,“../node_modules/@types/”和“../../node_modules/@types/”等等都會(huì)被加載進(jìn)來。所以遇到這種問題,你的配置應(yīng)該改成:
- {
- "compilerOptions":{
- "typeRoots":["@types",...,"./src/types"]
- }
- }
在實(shí)際項(xiàng)目中,@types基本上存在于根目錄下的node_modules下,因此這里你可以改成這樣:
- {
- "compilerOptions":{
- "typeRoots":["node_modules/@types",...,"./src/types"]
- }
- }
不支持decorators(裝飾器)
typescript默認(rèn)是關(guān)閉實(shí)驗(yàn)性的ES裝飾器,所以需要在tsconfig.json中開啟。
- {
- "compilerOptions":{
- "experimentalDecorators":true
- }
- }
Module '**' has no default export
提示模塊代碼里沒有“export
default”,而你卻用“import from ”這種默認(rèn)導(dǎo)入的形式。對(duì)于這個(gè)問題,我們需要把tsconfig.json配置項(xiàng)“allowSyntheticDefaultImports”設(shè)置為true。允許從沒有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入。不過不必?fù)?dān)心會(huì)對(duì)代碼產(chǎn)生什么影響,這個(gè)僅僅為了類型檢查。
- {
- "compilerOptions":{
- "allowSyntheticDefaultImports":true
- }
- }
當(dāng)然你也可以使用“esModuleInterop”這個(gè)配置項(xiàng),將其設(shè)置為true,根據(jù)“allowSyntheticDefaultImports”的默認(rèn)值,如下:
- module === "system" or --esModuleInterop
對(duì)于“esModuleInterop”這個(gè)配置項(xiàng)的作用主要有兩點(diǎn):
- 提供__importStar和__importDefault兩個(gè)helper來兼容babel生態(tài)
- 開啟allowSyntheticDefaultImports
對(duì)于“esModuleInterop”和“allowSyntheticDefaultImports”選用上,如果需要typescript結(jié)合babel,毫無疑問選“esModuleInterop”,否則的話,個(gè)人習(xí)慣選用“allowSyntheticDefaultImports”,比較喜歡需要啥用啥。當(dāng)然“esModuleInterop”是最保險(xiǎn)的選項(xiàng),如果對(duì)此拿捏不準(zhǔn)的話,那就乖乖地用“esModuleInterop”。
無法識(shí)別document和window這種全局對(duì)象
遇到這種情況,需要我們?cè)趖sconfig.json中l(wèi)ib這個(gè)配置項(xiàng)加入一個(gè)dom庫(kù),如下:
- {
- "compilerOptions":{
- "lib":[
- "dom",
- ...,
- "esNext"
- ]
- }
- }
文件中的標(biāo)紅問題
關(guān)于這個(gè)問題,我們需要分兩種情況來考慮,***種是.ts的文件,第二種是.tsx文件。下面來看一下具體是哪些注意的點(diǎn)(Ps:以下提到的注意的點(diǎn)并不能完全解決文件中標(biāo)紅的問題,但是可以解決大部分標(biāo)紅的問題):
***種:.ts文件
這種文件在你的項(xiàng)目比較少,比較容易處理,根據(jù)實(shí)際情況去加一下類型限制,沒有特別需要講的。
第二種:.tsx文件
這種情況都是react組件了,而react組件又分為無狀態(tài)組件和有狀態(tài)組件組件,所以我們分開來看。
無狀態(tài)組件
對(duì)于無狀態(tài)組件,首先得限制他是一個(gè)FunctionComponent(函數(shù)組件),其次限制其props類型。舉個(gè)🌰:
- 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這個(gè)名字比較長(zhǎng),你可以選擇用類型別名“SFC”或者“FC”。
有狀態(tài)組件
對(duì)于有狀態(tài)組件,主要注意三點(diǎn):
- props和state都要做類型限制
- state用readonly限制“this.state=**”的操作
- 對(duì)event對(duì)象做類型限制
- 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}>點(diǎn)擊我</div>
- }
- }
實(shí)際項(xiàng)目里,組件的state可能會(huì)有很多值,如果按照我們上面這種方式去寫會(huì)比較麻煩,所以可以考慮一下下面這個(gè)簡(jiǎn)便寫法:
- 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}>點(diǎn)擊我</div>
- }
- }
這種寫法會(huì)簡(jiǎn)便很多代碼,但是類型限制效果上明顯不如***種,所以這種方法僅僅作為參考,可根據(jù)實(shí)際情況去選擇。
Ant Design丟失樣式文件
當(dāng)我們把項(xiàng)目啟動(dòng)起來之后,某些同學(xué)的頁面可能會(huì)出現(xiàn)樣式丟失的情況,如下:
打開控制臺(tái),我們發(fā)現(xiàn)Ant Design的類名都找不到對(duì)應(yīng)的樣式:
出現(xiàn)這種情況是因?yàn)槲覀儼裝abel刪除之后,用來按需加載組件樣式文件的babel插件babel-plugin-import也隨著丟失了。不過typescript社區(qū)有一個(gè)babel-plugin-import的Typescript版本,叫做“ts-import-plugin”,我們先來安裝一下:
- yarn add ts-import-plugin -D
這個(gè)插件需要結(jié)合ts-loader使用,所以webpack配置中需要做如下調(diào)整:
- 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這個(gè)配置,這是個(gè)可選配置,我建議是只有大項(xiàng)目中才加這個(gè)配置,小項(xiàng)目就沒有必要了。由于typescript的語義檢查器會(huì)在每次編譯的時(shí)候檢查所有文件,因此當(dāng)項(xiàng)目很大的時(shí)候,編譯時(shí)間會(huì)很長(zhǎng)。解決這個(gè)問題的最簡(jiǎn)單的方法就是用transpileOnly: true這個(gè)配置去關(guān)閉typescript的語義檢查,但是這樣做的代價(jià)就是失去了類型檢查以及聲明文件的導(dǎo)出,所以除非在大項(xiàng)目中為了提升編譯效率,否則不建議加這個(gè)配置。
配置完成之后,你的瀏覽器控制臺(tái)可能會(huì)報(bào)出類似下面這個(gè)錯(cuò)誤:
出現(xiàn)這個(gè)原因是因?yàn)槟愕膖ypescript配置文件tsconfig.json中的module參數(shù)設(shè)置不對(duì),兩種情況會(huì)導(dǎo)致這個(gè)問題:
- module設(shè)置成了“commonjs”
- target設(shè)置"ES5"但是并未設(shè)置module(當(dāng)target不為“ES6”時(shí),module默認(rèn)為“commonjs”)
解決這個(gè)辦法就是把module設(shè)置為“esNext”便可解決這個(gè)問題。
- {
- "compilerOptions":{
- "module":"esNext"
- }
- }
可能會(huì)有小伙們說設(shè)置成“ES6”或者“ES2015”也是可以的,至于我為什么選擇“esNext”而不是“ES6”或者“ES2015”,主要原因是設(shè)置成“ES6”或者“ES2015”之后,就不能動(dòng)態(tài)導(dǎo)入了,因?yàn)轫?xiàng)目使用了react-loadable這個(gè)包,要是設(shè)置成“ES6”或者“ES2015”的話,會(huì)報(bào)如下這個(gè)錯(cuò)誤:
typescript提示我們需要設(shè)置成“commonjs”或者“ESNext”才可動(dòng)態(tài)導(dǎo)入,所以保險(xiǎn)起見,我是建議大家設(shè)置成ESNext。完成之后我們的頁面就可以正常顯示了。
說到module參數(shù),這里要再多提一嘴說一下moduleResolution這個(gè)參數(shù),它決定著typescript如何處理模塊。當(dāng)我們把module設(shè)置成“esNext”時(shí),是可以不用管moduleResolution這個(gè)參數(shù),但是大家項(xiàng)目里要是設(shè)置成“ES6”的話,那就要設(shè)置一下了。先看一下moduleResolution默認(rèn)規(guī)則:
- module === "AMD" or "System" or "ES6" ? "Classic" : "Node"
當(dāng)我們module設(shè)置為“ES6”時(shí),此時(shí)moduleResolution默認(rèn)是“Classic”,而我們需要的是“Node”。為什么要選擇“node”,主要是因?yàn)閚ode的模塊解析規(guī)則更符合我們要求,解析速度會(huì)更快,至于詳情的介紹,可以參考Typescript的文檔。
https://www.tslang.cn/docs/ha...
同樣為了保險(xiǎn)起見,我是建議大家強(qiáng)行將moduleResolution設(shè)置為“node”。
總結(jié)
以上就是我自己在遷移過程中遇到的問題,可能無法覆蓋大家在遷移過程中所遇到的問題,如果出現(xiàn)我上面沒有涉及的報(bào)錯(cuò),歡迎大家在評(píng)論區(qū)告訴我,我會(huì)盡可能地完善這篇文章。***再?gòu)?qiáng)調(diào)一下,本篇文章僅僅只是介紹了我個(gè)人在遷移至typescript的經(jīng)驗(yàn)總結(jié),并未完全覆蓋tsconfig.json的所有配置項(xiàng),文章未涉及到的配置項(xiàng),還需大家多花點(diǎn)時(shí)間看看typescript的文檔。***附上我已遷移到typescript的項(xiàng)目的地址:
項(xiàng)目地址: https://github.com/ruichengpi...