使用 Webpack 為現代 React 項目設置 TypeScript

已發表: 2022-03-10
快速總結 ↬本文介紹了 Typescript,這是 JavaScript 的上標,它提供了靜態類型功能,用於發現開發人員代碼中的常見錯誤,從而提高性能,從而產生健壯的企業應用程序。 在我們構建 Money Heist Episode Picker 應用程序時,您還將學習如何在 React 項目中有效地設置 TypeScript,探索 TypeScript、React 鉤子,例如 useReducer、useContext 和 Reach Router。

在這個軟件開發時代,JavaScript 幾乎可以用來開發任何類型的應用程序。 然而,JavaScript 是動態類型的這一事實可能會引起大多數大型企業公司的關注,因為它具有鬆散的類型檢查功能。

幸運的是,我們不必等到 Ecma Technical Committee 39 將靜態類型系統引入 JavaScript。 我們可以改用 TypeScript。

JavaScript 是動態類型的,在運行時實例化該變量之前,它不知道該變量的數據類型。 編寫大型軟件程序的開發人員可能傾向於將之前聲明的變量重新分配給不同類型的值,而不會發出任何警告或問題,從而導致經常被忽視的錯誤。

在本教程中,我們將了解 TypeScript 是什麼以及如何在 React 項目中使用它。 最後,我們將使用 TypeScript 和當前類似 React 的鉤子( useStateuseEffectuseReduceruseContext )構建一個項目,該項目由電視節目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) 中單擊“重命名符號”命令即可重構此類內容。

將應用重命名為 expApp(大預覽)

在 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 中關於 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.jsindex.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文件夾下手動添加一些不同的文件和文件夾:

  1. 添加webpack.config.js以添加 webpack 相關的配置。
  2. 為我們所有的 TypeScript 配置添加tsconfig.json
  3. 添加一個新目錄src
  4. src文件夾中創建一個新目錄components
  5. 最後,在components文件夾中添加index.htmlApp.tsxindex.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 ,其中userNamelang具有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 。 如果一切順利,您應該會看到:

React-Webpack 設置輸出(大預覽)

如果您有 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

為了使這個過程更易於管理,我們將把它分解成幾個步驟,這將使我們能夠在單個塊中進行遷移。 以下是我們將採取的遷移項目的步驟:

  1. 添加 TypeScript 和類型。
  2. 添加tsconfig.json
  3. 從小處著手。
  4. 將文件擴展名重命名為.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 startyarn 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 逐漸被採用的能力。 按照自己的步調一次處理一個文件。 做對你和你的團隊有意義的事情。 不要試圖一次解決所有問題。

要正確轉換它,我們需要做兩件事:

  1. 將文件擴展名更改為.tsx
  2. 添加類型註釋(這需要一些 TypeScript 知識)。

4.將文件擴展名重命名為.tsx

在大型代碼庫中,單獨重命名文件可能看起來很累。

在 macOS 上重命名多個文件

重命名多個文件可能會浪費時間。 這是在 Mac 上執行此操作的方法。 在包含要重命名的文件的文件夾上單擊鼠標右鍵(或Ctrl + 單擊,或者如果您使用的是 MacBook,則在觸控板上同時用兩根手指單擊)。 然後,單擊“在 Finder 中顯示”。 在 Finder 中,選擇要重命名的所有文件。 右鍵單擊選定的文件,然後選擇“重命名 X 項...”然後,您將看到如下內容:

在 Mac 上重命名文件(大預覽)

插入要查找的字符串,以及要替換找到的字符串的字符串,然後點擊“重命名”。 完畢。

在 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 SuspenseEpisodesList組件導入我們的主頁。
  • 添加劇集。
    • 設置商店以在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 服務器。

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 返回一組屬性,例如airdateairstampairtimeidimagenamenumberruntimeseasonsummaryurl 。 因此,我們定義了一個IEpisode接口並將適當的數據類型設置為對象屬性。

狀態接口

我們的IState接口分別具有episodesfavorites屬性,以及一個Array<IEpisode>接口。

動作

IAction接口屬性是payloadtypetype屬性為字符串類型,而有效負載的類型為Array | any Array | any .

請注意, Array | any Array | any表示情節接口或任何類型的數組。

Dispatch類型設置為React.Dispatch和一個<IAction>接口。 請注意,根據@types/react代碼庫, React.Dispatchdispatch函數的標準類型,而<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導入IStateIAction
  • 我們聲明了一個類型為IStateinitialState對象,以及劇集和收藏夾的屬性,它們都分別設置為一個空數組。
  • 接下來,我們創建了一個包含createContext方法並傳遞了initialStateStore變量。

createContext方法類型是<IState | any> <IState | any> ,這意味著它可以是<IState>any的類型。 我們將看到本文中經常使用的any類型。

  • 接下來,我們聲明了一個reducer函數,並將stateaction作為參數傳入。 reducer函數有一個 switch 語句來檢查action.type的值。 如果值為FETCH_DATA ,則它返回一個對象,該對象具有我們的狀態(...state)和包含我們的動作有效負載的情節狀態的副本。
  • 在 switch 語句中,我們返回default狀態。

請注意,reducer 函數中的stateaction參數分別具有IStateIAction類型。 此外, reducer函數具有IState類型。

  • 最後,我們聲明了一個StoreProvider函數。 這將使我們應用程序中的所有組件都可以訪問商店。
  • 該函數將children作為 prop,在StorePrivder函數內部,我們聲明了useReducer鉤子。
  • 我們解構了statedispatch
  • 為了讓所有組件都可以訪問我們的 store,我們傳入了一個包含statedispatch的對象值。

包含我們的情節和收藏狀態的state將被其他組件訪問,而dispatch是一個改變狀態的函數。

  • 我們將導出StoreStoreProvider ,以便它可以在我們的應用程序中使用。

創建 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 作為參數。
  • 因為我們的函數是異步的,所以我們將使用asyncawait
  • 我們創建一個變量( 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導入IEpisodeIProps
  • 接下來,我們創建一個帶有 props 的EpisodesList函數。 props 的類型為IProps ,而函數的類型為Array<JSX.Element>

Visual Studio Code 建議將我們的函數類型寫為JSX.Element[]

Visual Studio Code 建議一種類型(大預覽)

雖然Array<JSX.Element>等於JSX.Element[] ,但Array<JSX.Element>被稱為通用標識。 因此,本文將經常使用通用模式。

  • 在函數內部,我們從props中解構episodes ,其中IEpisode作為類型。

閱讀有關通用身份的信息,在我們繼續進行時將需要這些知識。

  • 我們返回了episodes道具並通過它進行映射以返回一些 HTML 標籤。
  • 第一部分包含key ,即episode.idepisode-boxclassName ,稍後將創建。 我們知道我們的劇集有圖像; 因此,圖像標籤。
  • 該圖像有一個三元運算符,用於檢查是否有episode.imageepisode.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 中導入useContextuseEffectlazySuspense 。 導入的應用程序組件是所有其他組件必須接收商店價值的基石。
  • 我們還從各自的文件中導入StoreIEpisodePropsFetchDataAction
  • 我們使用 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') )

我們從各自的文件中導入StoreProviderHomePageindex.css 。 我們將HomePage組件包裝在StoreProvider中。 正如我們在上一節中看到的,這使得Homepage組件可以訪問商店。

我們已經走了很長一段路。 Let's check what the app looks like, without any CSS.

App without CSS (Large preview)

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

我們包含togglefavactionfavoritesstore作為 props,我們解構了state ,一個來自 store 的dispatch 。 為了選擇我們最喜歡的劇集,我們在onClick事件中包含了toggleFavAction方法,並將statedispatchepisode props 作為參數傳遞給函數。

最後,我們遍歷favorite的狀態以檢查fav.id (最喜歡的 ID)是否與episode.id匹配。 如果是,我們在UnfavFav文本之間切換。 這有助於用戶知道他們是否喜歡該劇集。

我們正在接近尾聲。 但是我們仍然需要一個頁面,當用戶在主頁上選擇劇集時,可以鏈接到最喜歡的劇集。

如果你已經走到了這一步,請給自己拍拍背。

收藏夾組件

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 導入lazySuspense 。 我們還從各自的文件中導入StoreIEpisodePropstoggleFavAction

我們使用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文件中,我們分別導入FavPageHomePage組件,並將它們包裝在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 上找到。

參考

  1. “如何將 React 應用程序遷移到 TypeScript,”Joe Previte
  2. “為什麼以及如何在 React 應用程序中使用 TypeScript?”,Mahesh Haldar