拋轉引用
現在越來越多的專案放棄了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入口檔案找不到
由於我們在做檔名調整的時候,把main.js改成main.tsx,因此webpack的入口檔案要改成main.tsx。
module.exports = {
//省略部分程式碼...
entry: {
app: './src/main.tsx'
},
//省略部分程式碼...
}
提示不能使用jsx的語法
這個解決很簡單,去tsconfig配置一下即可。
{
"compilerOptions":{
"jsx": "react"
}
}
jsx這個配置項有三個值可選擇,分別是"preserve","react-native"和"react"。在preserve和react-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”。
有狀態元件
對於有狀態元件,主要注意三點:
- props和state都要做型別限制
- state用readonly限制“this.state=**”的操作
- 對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...