如今愈來愈多的項目放棄了javascript,而選擇擁抱了typescript,就好比咱們熟知的ant-design就是其中之一。面對愈來愈火的typescript,咱們公司今年也逐漸開始擁抱typescript。至於爲何要使用typescript?本文不作深刻探討,對這方面有興趣的小夥伴們能夠去看一下這篇文章:javascript
TypeScript體系調研報告
這篇文章比較全面地介紹了TypeScript,而且和Javascript作了一個對比。看完上面這篇文章,你會對TypeScript有一個比較深刻的認識,另外在TypeScript和Javascript的取捨上,能夠拿捏得更好。css
在開始遷移以前,我要說點題外話,本篇文章僅是記錄我在遷移過程當中遇到的問題以及我是如何解決的,並不會涉及typescript的教學。因此你們在閱讀本篇文章以前,必定要對typescript有一個基礎的認識,否則你讀起來會很是費力。html
因爲Typescript是Javascript的超集,它的不少語法瀏覽器是不能識別的,所以它不能直接運行在瀏覽器上,須要將其編譯成JavaScript才能運行在瀏覽器上,這點跟ES6須要通過babel編譯才能支持更多低版本的瀏覽器是一個道理。java
首先咱們得裝一個typescript,這就跟咱們在用babel前須要先裝一個babel-core是一個道理。node
yarn global add typescript
yarn add typescript
有些人會選擇將typescript安裝在全局環境上,可是我我的建議是裝在項目目錄下的,由於每一個項目的typescript版本是不徹底同樣的,裝在全局容易由於版本不一樣而出現問題。若是須要用tsc命令的話,能夠藉助npx去實現。接下來咱們執行以下命令生成tsconfig.json,這玩意就跟.babelrc是一個性質的。react
npx tsc --init
執行完以後,你的項目根目錄下便會有一個tsconfig.json這麼一個東西,可是裏面會有不少註釋,咱們先不用管他的。webpack
安裝ts-loader用於處理ts和tsx文件,相似於babel-loader。git
yarn add ts-loader -D
相應的webpack須要加上ts的loader規則:github
module.exports = { //省略部分代碼... module: { rules: [ { test:/\.tsx?$/, loader:'ts-loader' } //省略部分代碼... ] } //...省略部分代碼 }
以前用javascript的時候,可能有人不使用.jsx文件,整個項目都是用的.js文件,webapck裏面甚至都不配.jsx的規則。可是在typescript項目中想要所有使用.ts文件這就行不通了,會報錯,因此當用到了jsx的用法的時候,仍是得乖乖用.tsx文件,所以這裏我加入了.tsx的規則。web
關於babel這塊,網上有很多人是選擇留着的,理由很簡單,說是爲了防止之後會使用到JavaScript,可是我我的以爲是沒有必要留着babel。由於咱們整個項目裏面基本上只有使用第三方包的時候纔會用到javascript,而這些第三方包基本上都是已經編譯成了es5的代碼了,不須要babel再去處理一下。而業務邏輯裏面用javascript更是不太可能了,由於這便失去了使用typescript的意義。綜上所述,我我的以爲是要刪除babel相關的東西,下降項目複雜度。可是有一個例外狀況:。
當你用了某些babel插件,而這些插件的功能恰巧是typescript沒法提供的,那你能夠保留babel,而且與typescript結合。
整個src目下全部的.js結尾的文件都要修改文件名,使用到jsx語法的就改爲.tsx文件,未使用的就改爲.ts文件,這塊工做量比較大,會比較頭疼。另外改完以後文件確定會有不少標紅的地方,不要急着去改它,後面咱們分類統一去改。
因爲咱們在作文件名調整的時候,把main.js改爲main.tsx,所以webpack的入口文件要改爲main.tsx。
module.exports = { //省略部分代碼... entry: { app: './src/main.tsx' }, //省略部分代碼... }
這個解決很簡單,去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 |
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'] }, //省略部分代碼... }
這個比較簡單,它提示找不到哪一個模塊的聲明文件,你就裝個哪一個模塊的就行了,安裝格式以下:
yarn add @types/**
舉個🌰,若是提示Could not find a declaration file for module 'react',那你應該執行以下命令:
yarn add @types/react
這個僅限於第三方包,若是是項目本身的模塊提示缺乏聲明文件,那就須要你本身寫對應的聲明文件了。好比你在window這個全局對象上掛載了一個對象,若是須要使用它的話,就須要作一下聲明,不然就會報錯。至於具體怎麼寫,這得看typescript的文檔,這裏就不展開說明了。
https://www.tslang.cn/docs/ha...
這些並無在咱們的業務代碼裏直接用到,而是第三方包用到的,遇到這種狀況,須要檢查一下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"] } }
typescript默認是關閉實驗性的ES裝飾器,因此須要在tsconfig.json中開啓。
{ "compilerOptions":{ "experimentalDecorators":true } }
提示模塊代碼裏沒有「export
default」,而你卻用「import from 」這種默認導入的形式。對於這個問題,咱們須要把tsconfig.json配置項「allowSyntheticDefaultImports」設置爲true。容許從沒有設置默認導出的模塊中默認導入。不過沒必要擔憂會對代碼產生什麼影響,這個僅僅爲了類型檢查。
{ "compilerOptions":{ "allowSyntheticDefaultImports":true } }
固然你也可使用「esModuleInterop」這個配置項,將其設置爲true,根據「allowSyntheticDefaultImports」的默認值,以下:
module === "system" or --esModuleInterop
對於「esModuleInterop」這個配置項的做用主要有兩點:
對於「esModuleInterop」和「allowSyntheticDefaultImports」選用上,若是須要typescript結合babel,毫無疑問選「esModuleInterop」,不然的話,我的習慣選用「allowSyntheticDefaultImports」,比較喜歡須要啥用啥。固然「esModuleInterop」是最保險的選項,若是對此拿捏不許的話,那就乖乖地用「esModuleInterop」。
遇到這種狀況,須要咱們在tsconfig.json中lib這個配置項加入一個dom庫,以下:
{ "compilerOptions":{ "lib":[ "dom", ..., "esNext" ] } }
關於這個問題,咱們須要分兩種狀況來考慮,第一種是.ts的文件,第二種是.tsx文件。下面來看一下具體是哪些注意的點(Ps:如下提到的注意的點並不能徹底解決文件中標紅的問題,可是能夠解決大部分標紅的問題):
這種文件在你的項目比較少,比較容易處理,根據實際狀況去加一下類型限制,沒有特別須要講的。
這種狀況都是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」。
對於有狀態組件,主要注意三點:
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的類名都找不到對應的樣式:
出現這種狀況是由於咱們把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設置爲「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...