React專案從Javascript到Typescript的遷移經驗總結

wuming發表於2019-05-06

拋轉引用

現在越來越多的專案放棄了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結尾的檔案都要修改檔名,使用到jsx語法的就改成.tsx檔案,未使用的就改成.ts檔案,這塊工作量比較大,會比較頭疼。另外改完之後檔案肯定會有很多標紅的地方,不要急著去改它,後面我們分類統一去改。

解決報錯

webpack入口檔案找不到

React專案從Javascript到Typescript的遷移經驗總結
由於我們在做檔名調整的時候,把main.js改成main.tsx,因此webpack的入口檔案要改成main.tsx。

module.exports = {
    //省略部分程式碼...
    entry: {
        app: './src/main.tsx'
    },
    //省略部分程式碼...
}
複製程式碼

提示不能使用jsx的語法

React專案從Javascript到Typescript的遷移經驗總結
這個解決很簡單,去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')
        }
        //省略部分程式碼...    
    },
    //省略部分程式碼...   
}
複製程式碼

React專案從Javascript到Typescript的遷移經驗總結
這裡需要我們額外在tsconfig.json配置一下。

{
    "compilerOptions":{
        "baseUrl": ".",
        "paths": {
          "@/*":["./src/*"]
        } 
    }
}
複製程式碼

具體如何配置,請看typescript的文件,我就不展開介紹了,但是要注意的是baseUrl和paths一定要配合使用。

www.tslang.cn/docs/handbo…

無法自動新增擴充名而導致找不到對應的模組

React專案從Javascript到Typescript的遷移經驗總結
原先我們在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的文件,這裡就不展開說明了。

www.tslang.cn/docs/handbo…

Cannot find type definition file for '**'

React專案從Javascript到Typescript的遷移經驗總結
這些並沒有在我們的業務程式碼裡直接用到,而是第三方包用到的,遇到這種情況,需要檢查一下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(裝飾器)

React專案從Javascript到Typescript的遷移經驗總結
typescript預設是關閉實驗性的ES裝飾器,所以需要在tsconfig.json中開啟。

{
    "compilerOptions":{
        "experimentalDecorators":true
    }
}
複製程式碼

Module '**' has no default export

React專案從Javascript到Typescript的遷移經驗總結
提示模組程式碼裡沒有“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這種全域性物件

React專案從Javascript到Typescript的遷移經驗總結
遇到這種情況,需要我們在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丟失樣式檔案

當我們把專案啟動起來之後,某些同學的頁面可能會出現樣式丟失的情況,如下:

React專案從Javascript到Typescript的遷移經驗總結
開啟控制檯,我們發現Ant Design的類名都找不到對應的樣式:

React專案從Javascript到Typescript的遷移經驗總結
出現這種情況是因為我們把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的語義檢查,但是這樣做的代價就是失去了型別檢查以及宣告檔案的匯出,所以除非在大專案中為了提升編譯效率,否則不建議加這個配置。

配置完成之後,你的瀏覽器控制檯可能會報出類似下面這個錯誤:

React專案從Javascript到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”的話,會報如下這個錯誤:

React專案從Javascript到Typescript的遷移經驗總結
typescript提示我們需要設定成“commonjs”或者“ESNext”才可動態匯入,所以保險起見,我是建議大家設定成ESNext。完成之後我們的頁面就可以正常顯示了。
React專案從Javascript到Typescript的遷移經驗總結

說到module引數,這裡要再多提一嘴說一下moduleResolution這個引數,它決定著typescript如何處理模組。當我們把module設定成“esNext”時,是可以不用管moduleResolution這個引數,但是大家專案裡要是設定成“ES6”的話,那就要設定一下了。先看一下moduleResolution預設規則:

module === "AMD" or "System" or "ES6" ? "Classic" : "Node"
複製程式碼

當我們module設定為“ES6”時,此時moduleResolution預設是“Classic”,而我們需要的是“Node”。為什麼要選擇“node”,主要是因為node的模組解析規則更符合我們要求,解析速度會更快,至於詳情的介紹,可以參考Typescript的文件。

www.tslang.cn/docs/handbo…

同樣為了保險起見,我是建議大家強行將moduleResolution設定為“node”。

總結

以上就是我自己在遷移過程中遇到的問題,可能無法覆蓋大家在遷移過程中所遇到的問題,如果出現我上面沒有涉及的報錯,歡迎大家在評論區告訴我,我會盡可能地完善這篇文章。最後再強調一下,本篇文章僅僅只是介紹了我個人在遷移至typescript的經驗總結,並未完全覆蓋tsconfig.json的所有配置項,文章未涉及到的配置項,還需大家多花點時間看看typescript的文件。最後附上我已遷移到typescript的專案的地址:

專案地址: github.com/ruichengpin…

相關文章