使用 Webpack 為現代 React 項目設置 TypeScript
已發表: 2022-03-10在這個軟件開發時代,JavaScript 幾乎可以用來開發任何類型的應用程序。 然而,JavaScript 是動態類型的這一事實可能會引起大多數大型企業公司的關注,因為它具有鬆散的類型檢查功能。
幸運的是,我們不必等到 Ecma Technical Committee 39 將靜態類型系統引入 JavaScript。 我們可以改用 TypeScript。
JavaScript 是動態類型的,在運行時實例化該變量之前,它不知道該變量的數據類型。 編寫大型軟件程序的開發人員可能傾向於將之前聲明的變量重新分配給不同類型的值,而不會發出任何警告或問題,從而導致經常被忽視的錯誤。
在本教程中,我們將了解 TypeScript 是什麼以及如何在 React 項目中使用它。 最後,我們將使用 TypeScript 和當前類似 React 的鉤子( useState
、 useEffect
、 useReducer
、 useContext
)構建一個項目,該項目由電視節目Money Heist的情節選擇器應用程序組成。 有了這些知識,你就可以在自己的項目中繼續試驗 TypeScript。
本文不是對 TypeScript 的介紹。 因此,我們不會介紹 TypeScript 和 JavaScript 的基本語法。 但是,您不必成為任何這些語言的專家就可以跟隨,因為我們將嘗試遵循 KISS 原則(保持簡單,愚蠢)。
什麼是打字稿?
2019 年,TypeScript 在 GitHub 上被評為第七大最常用語言和第五增長最快的語言。 但是 TypeScript 到底是什麼?
根據官方文檔,TypeScript 是 JavaScript 的類型化超集,可編譯為純 JavaScript。 它由 Microsoft 和開源社區開發和維護。
在這種情況下,“超集”意味著該語言包含 JavaScript 的所有特性和功能,然後是一些。 TypeScript 是一種類型化的腳本語言。
它通過其類型註釋、類和接口為開發人員提供了對其代碼庫的更多控制,使開發人員不必手動修復控制台中令人討厭的錯誤。
創建 TypeScript 並不是為了改變 JavaScript。 相反,它使用有價值的新功能擴展了 JavaScript。 任何用純 JavaScript 編寫的程序也可以在 TypeScript 中按預期運行,包括跨平台的移動應用程序和 Node.js 中的後端。
這意味著您也可以使用 TypeScript 編寫 React 應用程序,就像我們將在本教程中所做的那樣。
為什麼選擇 TypeScript?
也許,您不相信接受 TypeScript 的優點。 讓我們考慮一下它的一些優點。
更少的錯誤
我們無法消除代碼中的所有錯誤,但我們可以減少它們。 TypeScript 在編譯時檢查類型並在變量類型更改時拋出錯誤。
能夠儘早發現這些明顯但頻繁的錯誤,可以更輕鬆地使用類型管理代碼。
重構更容易
您可能經常想要重構很多東西,但是因為它們涉及很多其他代碼和許多其他文件,所以您對修改它們持謹慎態度。
在 TypeScript 中,通常只需在集成開發環境 (IDE) 中單擊“重命名符號”命令即可重構此類內容。
在 JavaScript 等動態類型語言中,同時重構多個文件的唯一方法是使用正則表達式 (RegExp) 的傳統“搜索和替換”功能。
在諸如 TypeScript 之類的靜態類型語言中,不再需要“搜索和替換”。 使用諸如“查找所有匹配項”和“重命名符號”之類的 IDE 命令,您可以在應用程序中查看對象接口的給定函數、類或屬性的所有匹配項。
TypeScript 將幫助您找到重構位的所有實例,對其進行重命名,並在您的代碼在重構後出現任何類型不匹配時提醒您編譯錯誤。
TypeScript 比我們在這裡介紹的內容更具優勢。
TypeScript 的缺點
即使考慮到上面強調的有希望的特性,TypeScript 也肯定不是沒有缺點的。
一種虛假的安全感
TypeScript 的類型檢查功能通常會在開發人員中造成錯誤的安全感。 當我們的代碼出現問題時,類型檢查確實會警告我們。 但是,靜態類型不會降低總體錯誤密度。
因此,您的程序的強度將取決於您對 TypeScript 的使用,因為類型是由開發人員編寫的,而不是在運行時檢查。
如果您希望使用 TypeScript 來減少錯誤,請考慮改為測試驅動開發。
複雜的打字系統
打字系統雖然在很多方面都是一個很好的工具,但有時可能有點複雜。 這個缺點源於它與 JavaScript 完全可互操作,這為複雜化留下了更大的空間。
但是,TypeScript 仍然是 JavaScript,因此了解 JavaScript 很重要。
何時使用 TypeScript?
我建議您在以下情況下使用 TypeScript:
- 如果您希望構建一個可以長期維護的應用程序,那麼我強烈建議您從 TypeScript 開始,因為它可以促進自文檔化代碼,從而幫助其他開發人員在加入您的代碼庫時輕鬆理解您的代碼.
- 如果您需要創建一個庫,請考慮使用 TypeScript 編寫它。 它將幫助代碼編輯器向使用您的庫的開發人員建議適當的類型。
在最後幾節中,我們平衡了 TypeScript 的優缺點。 讓我們繼續今天的工作:在現代 React 項目中設置 TypeScript 。
入門
有幾種方法可以在 React 項目中設置 TypeScript。 在本教程中,我們將只介紹兩個。
方法一:創建 React App + TypeScript
大約兩年前,React 團隊發布了 Create React App 2.1,支持 TypeScript。 所以,你可能永遠不需要做任何繁重的工作來讓 TypeScript 進入你的項目。
要開始一個新的 Create React App 項目,你可以運行這個…
npx create-react-app my-app --folder-name
… 或這個:
yarn create react-app my-app --folder-name
要將 TypeScript 添加到 Create React App 項目,首先安裝它及其各自的@types
:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
… 要么:
yarn add typescript @types/node @types/react @types/react-dom @types/jest
接下來,重命名文件(例如, index.js
到index.tsx
),然後重新啟動您的開發服務器!
那很快,不是嗎?
方法 2:使用 Webpack 設置 TypeScript
Webpack 是 JavaScript 應用程序的靜態模塊打包器。 它從您的應用程序中獲取所有代碼,並使其在 Web 瀏覽器中可用。 模塊是由應用程序的 JavaScript、 node_modules
、圖像和 CSS 樣式構建的可重用代碼塊,它們被打包以便在您的網站上輕鬆使用。
創建一個新項目
讓我們首先為我們的項目創建一個新目錄:
mkdir react-webpack cd react-webpack
我們將使用 npm 來初始化我們的項目:
npm init -y
上面的命令將生成一個帶有一些默認值的package.json
文件。 我們還為 webpack、TypeScript 和一些特定於 React 的模塊添加一些依賴項。
安裝包
最後,我們需要安裝必要的軟件包。 打開命令行界面 (CLI) 並運行:
#Installing devDependencies npm install --save-dev @types/react @types/react-dom awesome-typescript-loader css-loader html-webpack-plugin mini-css-extract-plugin source-map-loader typescript webpack webpack-cli webpack-dev-server #installing Dependencies npm install react react-dom
讓我們在我們的react-webpack
文件夾下手動添加一些不同的文件和文件夾:
- 添加
webpack.config.js
以添加 webpack 相關的配置。 - 為我們所有的 TypeScript 配置添加
tsconfig.json
。 - 添加一個新目錄
src
。 - 在
src
文件夾中創建一個新目錄components
。 - 最後,在
components
文件夾中添加index.html
、App.tsx
和index.tsx
。
項目結構
因此,我們的文件夾結構將如下所示:
├── package.json ├── package-lock.json ├── tsconfig.json ├── webpack.config.js ├── .gitignore └── src └──components ├── App.tsx ├── index.tsx ├── index.html
開始添加一些代碼
我們將從index.html
開始:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React-Webpack Setup</title> </head> <body> <div></div> </body> </html>
這將創建一個帶有 ID 為output
的空div
的 HTML。
讓我們將代碼添加到我們的 React 組件App.tsx
:
import * as React from "react"; export interface HelloWorldProps { userName: string; lang: string; } export const App = (props: HelloWorldProps) => ( <h1> Hi {props.userName} from React! Welcome to {props.lang}! </h1> );
我們創建了一個接口對象並將其命名為HelloWorldProps
,其中userName
和lang
具有string
類型。
我們將props
傳遞給我們的App
組件並將其導出。
現在,讓我們更新index.tsx
中的代碼:
import * as React from "react"; import * as ReactDOM from "react-dom"; import { App } from "./App"; ReactDOM.render( <App userName="Beveloper" lang="TypeScript" />, document.getElementById("output") );
我們剛剛將App
組件導入index.tsx
。 當 webpack 看到任何擴展名為.ts
或.tsx
的文件時,它會使用 awesome-typescript-loader 庫轉譯該文件。
打字稿配置
然後我們將向tsconfig.json
添加一些配置:
{ "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "src/components/index.tsx" ] }
讓我們也看看我們添加到tsconfig.json
的不同選項:
-
compilerOptions
表示不同的編譯器選項。 -
jsx:react
在.tsx
文件中添加對 JSX 的支持。 -
lib
將庫文件列表添加到編譯中(例如,使用es2015
允許我們使用 ECMAScript 6 語法)。 -
module
生成模塊代碼。 -
noImplicitAny
為隱含any
類型的聲明引發錯誤。 -
outDir
表示輸出目錄。 -
sourceMap
生成一個.map
文件,這對於調試應用程序非常有用。 -
target
表示要將我們的代碼轉換成的目標 ECMAScript 版本(我們可以根據我們特定的瀏覽器要求添加一個版本)。 -
include
用於指定要包含的文件列表。
Webpack 配置
讓我們在 webpack.config.js 中添加一些webpack.config.js
配置。
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/components/index.tsx", target: "web", mode: "development", output: { path: path.resolve(\__dirname, "build"), filename: "bundle.js", }, resolve: { extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], }, module: { rules: [ { test: /\.(ts|tsx)$/, loader: "awesome-typescript-loader", }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader", }, { test: /\.css$/, loader: "css-loader", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(\__dirname, "src", "components", "index.html"), }), new MiniCssExtractPlugin({ filename: "./src/yourfile.css", }), ], };
讓我們看看我們添加到webpack.config.js
的不同選項:
-
entry
這指定了我們的應用程序的入口點。 它可能是我們想要包含在構建中的單個文件或文件數組。 -
output
這包含輸出配置。 該應用程序在嘗試將捆綁代碼從我們的項目輸出到磁盤時會查看此內容。 路徑代表代碼要輸出到的輸出目錄,文件名代表相同的文件名。 它通常被命名為bundle.js
。 -
resolve
Webpack 查看這個屬性來決定是打包還是跳過文件。 因此,在我們的項目中,webpack 將考慮使用擴展名為.js
、.jsx
、.json
、.ts
和.tsx
的文件進行捆綁。 -
module
我們可以讓 webpack 在應用程序請求時加載特定文件,使用加載器。 它需要一個規則對象,該對象指定:- 任何以擴展名
.tsx
或.ts
結尾的文件都應該使用awesome-typescript-loader
來加載; - 以
.js
擴展名結尾的文件應該使用source-map-loader
; - 以
.css
擴展名結尾的文件應使用css-loader
。
- 任何以擴展名
-
plugins
Webpack 有其自身的局限性,它提供了插件來克服它們並擴展其功能。 例如,html-webpack-plugin
創建一個模板文件,該文件從./src/component/index.html
目錄中的index.html
文件呈現給瀏覽器。
MiniCssExtractPlugin
呈現應用程序的父CSS
文件。
將腳本添加到 package.json
我們可以在package.json
文件中添加不同的腳本來構建 React 應用程序:
"scripts": { "start": "webpack-dev-server --open", "build": "webpack" },
現在,在 CLI 中運行npm start
。 如果一切順利,您應該會看到:
如果您有 webpack 的訣竅,請克隆此設置的存儲庫,並在您的項目中使用它。
創建文件
創建一個src
文件夾和一個index.tsx
文件。 這將是渲染 React 的基礎文件。
現在,如果我們運行npm start
,它將運行我們的服務器並打開一個新選項卡。 運行npm run build
將為生產構建 webpack 並將為我們創建一個構建文件夾。
我們已經了解瞭如何使用 Create React App 和 webpack 配置方法從頭開始設置 TypeScript。
全面掌握 TypeScript 的最快方法之一是將現有的 vanilla React 項目之一轉換為 TypeScript。 不幸的是,在現有的 vanilla React 項目中逐步採用 TypeScript 會帶來壓力,因為它需要彈出或重命名所有文件,如果項目屬於大型團隊,這將導致衝突和巨大的拉取請求。
接下來,我們將了解如何輕鬆地將 React 項目遷移到 TypeScript。
將現有的 Create React 應用程序遷移到 TypeScript
為了使這個過程更易於管理,我們將把它分解成幾個步驟,這將使我們能夠在單個塊中進行遷移。 以下是我們將採取的遷移項目的步驟:
- 添加 TypeScript 和類型。
- 添加
tsconfig.json
。 - 從小處著手。
- 將文件擴展名重命名為
.tsx
。
1. 將 TypeScript 添加到項目中
首先,我們需要將 TypeScript 添加到我們的項目中。 假設您的 React 項目是使用 Create React App 引導的,我們可以運行以下命令:
# Using npm npm install --save typescript @types/node @types/react @types/react-dom @types/jest # Using Yarn yarn add typescript @types/node @types/react @types/react-dom @types/jest
請注意,我們尚未對 TypeScript 進行任何更改。 如果我們運行命令在本地啟動項目( npm start
或yarn start
),沒有任何變化。 如果是這樣的話,那就太好了! 我們已準備好進行下一步。
2.添加tsconfig.json
文件
在使用 TypeScript 之前,我們需要通過tsconfig.json
文件對其進行配置。 最簡單的入門方法是使用以下命令搭建一個腳手架:
npx tsc --init
這為我們提供了一些基礎知識,並帶有大量註釋代碼。 現在,將tsconfig.json
中的所有代碼替換為:
{ "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "./src/**/**/\*" ] }
打字稿配置
讓我們也看看我們添加到tsconfig.json
的不同選項:
-
compilerOptions
表示不同的編譯器選項。-
target
將較新的 JavaScript 結構轉換為較舊的版本,例如 ECMAScript 5。 -
lib
將庫文件列表添加到編譯中(例如,使用 es2015 允許我們使用 ECMAScript 6 語法)。 -
jsx:react
在.tsx
文件中添加對 JSX 的支持。 -
lib
將庫文件列表添加到編譯中(例如,使用 es2015 允許我們使用 ECMAScript 6 語法)。 -
module
生成模塊代碼。 -
noImplicitAny
用於為隱含any
類型的聲明引發錯誤。 -
outDir
表示輸出目錄。 -
sourceMap
生成一個.map
文件,這對於調試我們的應用程序非常有用。 -
include
用於指定要包含的文件列表。
-
根據項目的需求,配置選項會有所不同。 您可能需要檢查 TypeScript 選項電子表格以確定適合您項目的內容。
我們只採取了必要的行動來做好準備。 我們的下一步是將文件遷移到 TypeScript。
3. 從一個簡單的組件開始
利用 TypeScript 逐漸被採用的能力。 按照自己的步調一次處理一個文件。 做對你和你的團隊有意義的事情。 不要試圖一次解決所有問題。
要正確轉換它,我們需要做兩件事:
- 將文件擴展名更改為
.tsx
。 - 添加類型註釋(這需要一些 TypeScript 知識)。
4.將文件擴展名重命名為.tsx
在大型代碼庫中,單獨重命名文件可能看起來很累。
在 macOS 上重命名多個文件
重命名多個文件可能會浪費時間。 這是在 Mac 上執行此操作的方法。 在包含要重命名的文件的文件夾上單擊鼠標右鍵(或Ctrl
+ 單擊,或者如果您使用的是 MacBook,則在觸控板上同時用兩根手指單擊)。 然後,單擊“在 Finder 中顯示”。 在 Finder 中,選擇要重命名的所有文件。 右鍵單擊選定的文件,然後選擇“重命名 X 項...”然後,您將看到如下內容:
插入要查找的字符串,以及要替換找到的字符串的字符串,然後點擊“重命名”。 完畢。
在 Windows 上重命名多個文件
在 Windows 上重命名多個文件超出了本教程的範圍,但提供了完整的指南。 重命名文件後通常會出錯; 您只需要添加類型註釋。 您可以在文檔中對此進行複習。
我們已經介紹瞭如何在 React 應用程序中設置 TypeScript。 現在,讓我們使用 TypeScript 為Money Heist構建一個情節選擇器應用程序。
我們不會介紹 TypeScript 的基本類型。 在繼續本教程之前,需要通讀文檔。
建造時間
為了讓這個過程不那麼令人生畏,我們將把它分解成幾個步驟,這將使我們能夠以單獨的塊構建應用程序。 以下是我們將採取的構建Money Heist劇集選擇器的所有步驟:
- 搭建一個 Create React 應用程序。
- 獲取劇集。
- 在 interface.ts 中為我們的情節創建適當的類型和
interface.ts
。 - 在
store.tsx
中設置商店以獲取劇集。 - 在
action.ts
中創建用於獲取劇集的操作。 - 創建一個
EpisodeList.tsx
組件來保存獲取的劇集。 - 使用
React Lazy and Suspense
將EpisodesList
組件導入我們的主頁。
- 在 interface.ts 中為我們的情節創建適當的類型和
- 添加劇集。
- 設置商店以在
store.tsx
中添加劇集。 - 在
action.ts
中創建用於添加劇集的操作。
- 設置商店以在
- 刪除劇集。
- 在
store.tsx
中設置商店以刪除劇集。 - 在
action.ts
中創建刪除劇集的動作。
- 在
- 最喜歡的一集。
- 在喜歡的劇集中導入
EpisodesList
組件。 - 在最喜歡的劇集中渲染
EpisodesList
。
- 在喜歡的劇集中導入
- 使用到達路由器進行導航。
設置反應
設置 React 的最簡單方法是使用 Create React App。 Create React App 是一種官方支持的創建單頁 React 應用程序的方式。 它提供了一個沒有配置的現代構建設置。
我們將使用它來引導我們將要構建的應用程序。 在 CLI 中,運行以下命令:
npx create-react-app react-ts-app && cd react-ts-app
安裝成功後,通過運行npm start
React 服務器。
理解 Typescript 中的接口和類型
當我們需要為對象屬性賦予類型時,會使用 TypeScript 中的接口。 因此,我們將使用接口來定義我們的類型。
interface Employee { name: string, role: string salary: number } const bestEmployee: Employee= { name: 'John Doe', role: 'IOS Developer', salary: '$8500' //notice we are using a string }
在編譯上面的代碼時,我們會看到這個錯誤:“財產salary
的類型不兼容。 類型string
不可分配給類型number
。”
當為屬性或變量分配了定義類型以外的類型時,TypeScript 中會發生此類錯誤。 具體來說,上面的代碼片段意味著salary
屬性被分配了一個string
類型而不是一個number
類型。
讓我們在src
文件夾中創建一個interface.ts
文件。 將此代碼複製並粘貼到其中:
/** |-------------------------------------------------- | All the interfaces! |-------------------------------------------------- */ export interface IEpisode { airdate: string airstamp: string airtime: string id: number image: { medium: string; original: string } name: string number: number runtime: number season: number summary: string url: string } export interface IState { episodes: Array<IEpisode> favourites: Array<IEpisode> } export interface IAction { type: string payload: Array<IEpisode> | any } export type Dispatch = React.Dispatch<IAction> export type FavAction = ( state: IState, dispatch: Dispatch, episode: IEpisode ) => IAction export interface IEpisodeProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> } export interface IProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> }
在接口名稱中添加“I”是一種很好的做法。 它使代碼可讀。 但是,您可以決定排除它。
I劇集界面
我們的 API 返回一組屬性,例如airdate
、 airstamp
、 airtime
、 id
、 image
、 name
、 number
、 runtime
、 season
、 summary
和url
。 因此,我們定義了一個IEpisode
接口並將適當的數據類型設置為對象屬性。
狀態接口
我們的IState
接口分別具有episodes
和favorites
屬性,以及一個Array<IEpisode>
接口。
動作
IAction
接口屬性是payload
和type
。 type
屬性為字符串類型,而有效負載的類型為Array | any
Array | any
.
請注意, Array | any
Array | any
表示情節接口或任何類型的數組。
Dispatch
類型設置為React.Dispatch
和一個<IAction>
接口。 請注意,根據@types/react
代碼庫, React.Dispatch
是dispatch
函數的標準類型,而<IAction>
是接口操作的數組。
此外,Visual Studio Code 有一個 TypeScript 檢查器。 因此,僅通過突出顯示或將鼠標懸停在代碼上,就足以建議適當的類型。
換句話說,為了讓我們在應用程序中使用我們的界面,我們需要將其導出。 到目前為止,我們已經有了存儲和保存對像類型的接口。 現在讓我們創建我們的商店。 請注意,其他接口遵循與所解釋的相同的約定。
獲取劇集
創建商店
為了獲取我們的劇集,我們需要一個存儲數據的初始狀態並定義我們的 reducer 函數的存儲。
我們將使用useReducer
鉤子來設置它。 在您的src
文件夾中創建一個store.tsx
文件。 將以下代碼複製並粘貼到其中。
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
以下是我們創建商店所採取的步驟:
- 在定義我們的 store 時,我們需要
useReducer
鉤子和來自 React 的createContext
API,這就是我們導入它的原因。 - 我們從
./types/interfaces
導入IState
和IAction
。 - 我們聲明了一個類型為
IState
的initialState
對象,以及劇集和收藏夾的屬性,它們都分別設置為一個空數組。 - 接下來,我們創建了一個包含
createContext
方法並傳遞了initialState
的Store
變量。
createContext
方法類型是<IState | any>
<IState | any>
,這意味著它可以是<IState>
或any
的類型。 我們將看到本文中經常使用的any
類型。
- 接下來,我們聲明了一個
reducer
函數,並將state
和action
作為參數傳入。reducer
函數有一個 switch 語句來檢查action.type
的值。 如果值為FETCH_DATA
,則它返回一個對象,該對象具有我們的狀態(...state)
和包含我們的動作有效負載的情節狀態的副本。 - 在 switch 語句中,我們返回
default
狀態。
請注意,reducer 函數中的state
和action
參數分別具有IState
和IAction
類型。 此外, reducer
函數具有IState
類型。
- 最後,我們聲明了一個
StoreProvider
函數。 這將使我們應用程序中的所有組件都可以訪問商店。 - 該函數將
children
作為 prop,在StorePrivder
函數內部,我們聲明了useReducer
鉤子。 - 我們解構了
state
和dispatch
。 - 為了讓所有組件都可以訪問我們的 store,我們傳入了一個包含
state
和dispatch
的對象值。
包含我們的情節和收藏狀態的state
將被其他組件訪問,而dispatch
是一個改變狀態的函數。
- 我們將導出
Store
和StoreProvider
,以便它可以在我們的應用程序中使用。
創建 Action.ts
我們需要向 API 發出請求以獲取將向用戶顯示的劇集。 這將在一個動作文件中完成。 創建一個Action.ts
文件,然後粘貼以下代碼:
import { Dispatch } from './interface/interfaces' export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) }
首先,我們需要導入我們的接口,以便它們可以在這個文件中使用。 採取了以下步驟來創建操作:
-
fetchDataAction
函數將dispatch
props 作為參數。 - 因為我們的函數是異步的,所以我們將使用
async
和await
。 - 我們創建一個變量(
URL
)來保存我們的 API 端點。 - 我們還有另一個名為
data
的變量,它保存來自 API 的響應。 - 然後,我們通過調用
data.json()
獲得 JSON 格式的響應後,將 JSON 響應存儲在dataJSON
中。 - 最後,我們返回一個具有
type
屬性和FETCH_DATA
字符串的調度函數。 它還有一個payload()
。_embedded.episodes
是來自我們endpoint
的劇集對像數組。
請注意, fetchDataAction
函數會獲取我們的端點,將其轉換為JSON
對象,並返回調度函數,該函數會更新之前在 Store 中聲明的狀態。
導出的調度類型設置為React.Dispatch
。 請注意,根據@types/react
代碼庫, React.Dispatch
是調度函數的標準類型,而<IAction>
是接口操作的數組。
EpisodesList 組件
為了保持我們應用程序的可重用性,我們會將所有獲取的劇集保存在一個單獨的文件中,然後將該文件導入我們的homePage
組件中。
在components
文件夾中,創建一個EpisodesList.tsx
文件,並將以下代碼複製並粘貼到其中:
import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => { const { episodes } = props return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Season: {episode.season} Number: {episode.number} </div> <button type='button' > Fav </button> </section> </section> ) }) } export default EpisodesList
- 我們從
interfaces.tsx
導入IEpisode
和IProps
。 - 接下來,我們創建一個帶有 props 的
EpisodesList
函數。 props 的類型為IProps
,而函數的類型為Array<JSX.Element>
。
Visual Studio Code 建議將我們的函數類型寫為JSX.Element[]
。
雖然Array<JSX.Element>
等於JSX.Element[]
,但Array<JSX.Element>
被稱為通用標識。 因此,本文將經常使用通用模式。
- 在函數內部,我們從
props
中解構episodes
,其中IEpisode
作為類型。
閱讀有關通用身份的信息,在我們繼續進行時將需要這些知識。
- 我們返回了
episodes
道具並通過它進行映射以返回一些 HTML 標籤。 - 第一部分包含
key
,即episode.id
和episode-box
的className
,稍後將創建。 我們知道我們的劇集有圖像; 因此,圖像標籤。 - 該圖像有一個三元運算符,用於檢查是否有
episode.image
或episode.image.medium
。 否則,如果沒有找到圖像,我們將顯示一個空字符串。 此外,我們將episode.name
包含在一個 div 中。
在section
中,我們顯示了劇集所屬的季節及其編號。 我們有一個帶有文本Fav
的按鈕。 我們導出了EpisodesList
組件,以便我們可以在我們的應用程序中使用它。
主頁組件
我們希望主頁觸發 API 調用並使用我們創建的EpisodesList
組件顯示劇集。 在components
文件夾中,創建HomePage
組件,然後將以下代碼複製並粘貼到其中:
import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { fetchDataAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch } } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
- 我們從 React 中導入
useContext
、useEffect
、lazy
和Suspense
。 導入的應用程序組件是所有其他組件必須接收商店價值的基石。 - 我們還從各自的文件中導入
Store
、IEpisodeProps
和FetchDataAction
。 - 我們使用 React 16.6 中可用的
React.lazy
功能導入EpisodesList
組件。
React 延遲加載支持代碼拆分約定。 因此,我們的EpisodesList
組件是動態加載的,而不是一次加載,從而提高了我們的應用程序的性能。
- 我們將
state
解構並作為來自Store
的道具進行dispatch
。 -
useEffect
掛鉤中的 && 符號檢查我們的情節狀態是否為empty
(或等於 0)。 否則,我們返回fetchDataAction
函數。 - 最後,我們返回
App
組件。 在其中,我們使用Suspense
包裝器,並將fallback
設置為帶有loading
文本的 div。 當我們等待 API 的響應時,這將顯示給用戶。 -
EpisodesList
組件將在數據可用時掛載,包含episodes
的數據就是我們傳播到其中的內容。
設置索引.txs
Homepage
組件必須是StoreProvider
的子組件。 我們必須在index
文件中這樣做。 將index.js
重命名為index.tsx
並粘貼以下代碼:
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store' import HomePage from './components/HomePage' ReactDOM.render( <StoreProvider> <HomePage /> </StoreProvider>, document.getElementById('root') )
我們從各自的文件中導入StoreProvider
、 HomePage
和index.css
。 我們將HomePage
組件包裝在StoreProvider
中。 正如我們在上一節中看到的,這使得Homepage
組件可以訪問商店。
我們已經走了很長一段路。 Let's check what the app looks like, without any CSS.
Create Index.css
Delete the code in the index.css
file and replace it with this:
html { font-size: 14px; } body { margin: 0; padding: 0; font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .episode-layout { display: flex; flex-wrap: wrap; min-width: 100vh; } .episode-box { padding: .5rem; } .header { display: flex; justify-content: space-between; background: white; border-bottom: 1px solid black; padding: .5rem; position: sticky; top: 0; }
Our app now has a look and feel. Here's how it looks with CSS.
Now we see that our episodes can finally be fetched and displayed, because we've adopted TypeScript all the way. Great, isn't it?
Add Favorite Episodes Feature
Let's add functionality that adds favorite episodes and that links it to a separate page. Let's go back to our Store component and add a few lines of code:
Note that the highlighted code is newly added:
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload }
case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider> }
To implement the “Add favorite” feature to our app, the ADD_FAV
case is added. It returns an object that holds a copy of our previous state, as well as an array with a copy of the favorite state
, with the payload
.
We need an action that will be called each time a user clicks on the FAV
button. Let's add the highlighted code to index.tx
:
import {
IAction, IEpisode, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON._embedded.episodes }) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
We create a toggleFavAction
function that takes dispatch
and episodes
as parameters, and any
and IEpisode|any
as their respective types, with IAction
as our function type. We have an object whose type
is ADD_FAV
and that has episode
as its payload. Lastly, we just return and dispatch the object.
我們將向EpisodeList.tsx
添加更多片段。 複製並粘貼突出顯示的代碼:
import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => {
const { episodes, toggleFavAction, favourites, store } = props const { state, dispatch } = store
return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist - ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Seasion: {episode.season} Number: {episode.number} </div> <button type='button'
onClick={() => toggleFavAction(state, dispatch, episode)} > {favourites.find((fav: IEpisode) => fav.id === episode.id) ? 'Unfav' : 'Fav'}
</button> </section> </section> ) }) } export default EpisodesList
我們包含togglefavaction
、 favorites
和store
作為 props,我們解構了state
,一個來自 store 的dispatch
。 為了選擇我們最喜歡的劇集,我們在onClick
事件中包含了toggleFavAction
方法,並將state
、 dispatch
和episode
props 作為參數傳遞給函數。
最後,我們遍歷favorite
的狀態以檢查fav.id
(最喜歡的 ID)是否與episode.id
匹配。 如果是,我們在Unfav
和Fav
文本之間切換。 這有助於用戶知道他們是否喜歡該劇集。
我們正在接近尾聲。 但是我們仍然需要一個頁面,當用戶在主頁上選擇劇集時,可以鏈接到最喜歡的劇集。
如果你已經走到了這一步,請給自己拍拍背。
收藏夾組件
在components
文件夾中,創建一個FavPage.tsx
文件。 將以下代碼複製並粘貼到其中:
import React, { lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { toggleFavAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) export default function FavPage(): JSX.Element { const { state, dispatch } = React.useContext(Store) const props: IEpisodeProps = { episodes: state.favourites, store: { state, dispatch }, toggleFavAction, favourites: state.favourites } return ( <App> <Suspense fallback={<div>loading...</div>}> <div className='episode-layout'> <EpisodesList {...props} /> </div> </Suspense> </App> ) }
為了創建選擇最喜歡劇集的邏輯,我們編寫了一些代碼。 我們從 React 導入lazy
和Suspense
。 我們還從各自的文件中導入Store
、 IEpisodeProps
和toggleFavAction
。
我們使用React.lazy
功能導入我們的EpisodesList
組件。 最後,我們返回App
組件。 在其中,我們使用Suspense
包裝器,並為帶有加載文本的 div 設置後備。
這類似於Homepage
組件。 該組件將訪問商店以獲取用戶收藏的劇集。 然後,劇集列表被傳遞給EpisodesList
組件。
讓我們在HomePage.tsx
文件中添加更多片段。
包括來自toggleFavAction
的../Actions
。 還包括toggleFavAction
方法作為道具。
import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces'
import { fetchDataAction, toggleFavAction } from '../Actions'
const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch },
toggleFavAction, favourites: state.favourites
} return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
我們的FavPage
需要鏈接,因此我們需要在App.tsx
的標題中添加一個鏈接。 為了實現這一點,我們使用 Reach Router,一個類似於 React Router 的庫。 William Le 解釋了 Reach Router 和 React Router 之間的區別。
在您的 CLI 中,運行npm install @reach/router @types/reach__router
。 我們正在安裝到達路由器庫和reach-router
類型。
安裝成功後,從@reach/router
導入Link
。
import React, { useContext, Fragment } from 'react' import { Store } from './tsx'
import { Link } from '@reach/router'
const App = ({ children }: { children: JSX.Element }): JSX.Element => {
const { state } = useContext(Store)
return ( <Fragment> <header className='header'> <div> <h1>Money Heist</h1> <p>Pick your favourite episode</p> </div>
<div> <Link to='/'>Home</Link> <Link to='/faves'>Favourite(s): {state.favourites.length}</Link> </div>
</header> {children} </Fragment> ) } export default App
我們從useContext
。 最後,我們的主頁將有一個Link
和一個到/
的路徑,而我們最喜歡的有一個到/faves
的路徑。
{state.favourites.length}
檢查收藏狀態中的劇集數量並顯示它。
最後,在我們的index.tsx
文件中,我們分別導入FavPage
和HomePage
組件,並將它們包裝在Router
中。
將突出顯示的代碼複製到現有代碼中:
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store'
import { Router, RouteComponentProps } from '@reach/router' import HomePage from './components/HomePage' import FavPage from './components/FavPage' const RouterPage = ( props: { pageComponent: JSX.Element } & RouteComponentProps ) => props.pageComponent
ReactDOM.render( <StoreProvider>
<Router> <RouterPage pageComponent={<HomePage />} path='/' /> <RouterPage pageComponent={<FavPage />} path='/faves' /> </Router>
</StoreProvider>, document.getElementById('root') )
現在,讓我們看看實現的ADD_FAV
是如何工作的。
刪除收藏功能
最後,我們將添加“刪除劇集功能”,這樣當點擊按鈕時,我們可以在添加或刪除最喜歡的劇集之間切換。 我們將在標題中顯示添加或刪除的劇集數。
店鋪
為了創建“刪除最喜歡的劇集”功能,我們將在我們的商店中添加另一個案例。 因此,轉到Store.tsx
並添加突出顯示的代碼:
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
case 'REMOVE_FAV': return { ...state, favourites: action.payload }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
我們添加了另一個名為REMOVE_FAV
的案例,並返回一個包含我們的initialState
副本的對象。 此外, favorites
狀態包含操作負載。
行動
複製以下突出顯示的代碼並將其粘貼到action.ts
中:
{ IAction, IEpisode, IState, Dispatch } from './types/interfaces'
import
{ IAction, IEpisode, IState, Dispatch }export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) } //Add IState withits type
export const toggleFavAction = (state: IState, dispatch: any, episode: IEpisode | any): IAction => { const episodeInFav = state.favourites.includes(episode)
let dispatchObj = { type: 'ADD_FAV', payload: episode }
if (episodeInFav) { const favWithoutEpisode = state.favourites.filter( (fav: IEpisode) => fav.id !== episode.id ) dispatchObj = { type: 'REMOVE_FAV', payload: favWithoutEpisode }
} return dispatch(dispatchObj) }
我們從./types/interfaces
導入IState
接口,因為我們需要將它作為類型傳遞給toggleFavAction
函數中的state
props。
創建一個episodeInFav
變量來檢查是否存在處於favorites
狀態的劇集。
我們過濾收藏夾狀態以檢查收藏夾 ID 是否不等於劇集 ID。 因此, dispatchObj
被重新分配了REMOVE_FAV
類型和favWithoutEpisode
的有效負載。
讓我們預覽一下我們的應用程序的結果。
結論
在本文中,我們了解瞭如何在 React 項目中設置 TypeScript,以及如何將項目從 vanilla React 遷移到 TypeScript。
我們還使用 TypeScript 和 React 構建了一個應用程序,以了解 TypeScript 如何在 React 項目中使用。 我相信你能夠學到一些東西。
請在下面的評論部分分享您對 TypeScript 的反饋和經驗。 我很想看看你想出了什麼!
本文的支持存儲庫可在 GitHub 上找到。
參考
- “如何將 React 應用程序遷移到 TypeScript,”Joe Previte
- “為什麼以及如何在 React 應用程序中使用 TypeScript?”,Mahesh Haldar