使用 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
组件可以访问商店。
我们已经走了很长一段路。 让我们检查一下应用程序的外观,没有任何 CSS。

创建索引.css
删除index.css
文件中的代码并将其替换为:
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