使用 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组件可以访问商店。

我们已经走了很长一段路。 让我们检查一下应用程序的外观,没有任何 CSS。

没有 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

我们包含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