React-和库教程-一-

131 阅读51分钟

React 和库教程(一)

原文:React and Libraries

协议:CC BY-NC-SA 4.0

一、学习 React 基本概念

这一章是我们起飞和开始飞行之前的地面学校。我们将创建一个极简的独立 React 应用,名为“Hello World”然后我们将回顾它是如何工作的,看看在引擎盖下发生了什么。我们还将学习 TypeScript 和初学者模板项目,我们可以用它们来加速开发。

这一章是理解 React 的基础,尽管我们并不是在这里构建一个华而不实的应用,但是其中涉及的概念是至关重要的。一旦你完成了这本书,甚至在阅读后面的章节时,如果你需要参考一些基本的概念,你可以随时回到这一章。

我们开始吧!

React 入门

在本节中,我们将创建一个极简的、独立的“Hello World”React 示例。我们将安装一个集成开发环境(IDE ),我们将涵盖一些重要的概念,如 JSX、DOM、React 虚拟 DOM、Babel 和 ES5/ES6。

不过,首先,我们将讨论 React 是什么以及为什么要使用它。尽管直接开始编写代码可能更令人满意,但关注关键概念将有助于您更好地理解 React。整本书依赖于你对这部分内容的理解。

什么是 React?

React(也称为 ReactJS)是一个 JavaScript 库,由脸书( https://github.com/facebook/react )开发,用于创建 Web 用户界面。React 是由当时在脸书广告公司工作的乔丹·沃克发明的。与 jQuery、Angular、Vue.js、Svelte 等其他前端开发框架和库竞争。

在 2017 年 9 月发布的上一版本 React 16.x 中,React 团队增加了更多的工具和开发支持,并消除了 bug。React 17 于 2020 年 10 月发布。

Note

React 17 是一个“垫脚石”版本,该版本主要专注于使 React 在未来版本中更容易升级,以及提高与浏览器的兼容性。React 团队的支持表明该库势头强劲,不会很快消失。

为什么要 React?

你知道吗?

安装 IDE

处理代码时,建议您使用 IDE。

为什么我甚至需要一个 IDE?

你不“需要”一个 IDE 您总是可以用文本编辑器编写代码。然而,IDE 可以帮助人们编写代码并提高生产率。这是通过提供编写代码所需的常见功能来实现的,例如源代码编辑器、构建自动化工具和调试器,以及许多其他功能。

说到 ide,可以选择的有很多。一些例子是微软的 Visual Studio、IntelliJ IDEA、WebStorm、NetBeans、Eclipse 和 Aptana Studio。

对于这本书,我选择了 Visual Studio Code (VS Code ),因为它是一个轻量级、免费、跨平台(Linux、macOS、Windows)的编辑器,可以用插件进行扩展。您可能正在为一家将为您提供特定 ide 的公司工作,或者您可能决定投资购买顶级 IDE 许可证。提到的大多数顶级 ide 都提供类似的功能,所以归结起来就是你习惯使用的东西,许可证等等。

Note

选择一个 IDE 取决于很多因素,我不会深入讨论这些因素。您可以安装或使用您习惯使用的任何 IDEVS 代码只是一个建议。

您还可以选择 Microsoft Visual Studio Express 版本,而不是 VS 代码,与 Microsoft Visual Studio 相比,VS 代码的功能有所减少。

如何安装 VS 代码?

要开始,请访问 VS 代码下载页面:

https://code.visualstudio.com/download

选择你的平台,一旦下载完成,打开 VS 代码。您应该会看到如图 1-1 所示的欢迎页面。

img/503823_1_En_1_Fig1_HTML.jpg

图 1-1

Visual Studio 代码欢迎页

创建一个极简的独立“Hello World”程序

是时候创建我们新的“Hello World”应用,并在我们的浏览器中运行该项目了。要在 VS 代码中创建新文件,请选择 new file。然后,粘贴以下代码,并将文件保存到您想要的任何位置:

<html>
  <head>
      <meta charSet="utf-8">
          <title>Hello world</title>
          <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
          <script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.production.min.js"></script>
          <script src="https://unpkg.com/@babel/standalone/babel.min.js">
          </script>
  </head>
  <body>
  <div id="app"></div>
  <script type="text/babel">
      ReactDOM.render(
      <h1>Hello World</h1>,
      document.getElementById('app')
      );
  </script>
  </body>
</html>

您可以从本书的 GitHub 位置下载该代码:

https://github.com/Apress/react-and-libraries/tree/master/01/index.html

Note

你可以从这本书的 GitHub 位置( https://github.com/Apress/react-and-libraries )下载你在这本书里看到的所有代码和练习。这些文件是按章组织的。

现在我们已经创建了文件,将其重命名为index.html,并用您最喜欢的浏览器打开它(见图 1-1 )。

img/503823_1_En_1_Fig2_HTML.jpg

图 1-2

在浏览器中 React“Hello World”index.html 应用

祝贺您,您刚刚创建了您的第一个 React 应用!

现在,让我们解码这个例子来理解这里发生了什么。

看看我们创建的ReactDOM.render函数。看起来是这样的:

ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('app')
);

虽然代码看起来像 HTML,但它不是。这是 JavaScript 扩展(JSX)代码。

Note

JSX 是一个 React 扩展,它使用 JavaScript 标签来模仿 HTML 代码,因此代码与 HTML 相似但不相同。

什么是 JSX,我们为什么需要它?

为了理解 React 为什么使用 JSX 而不仅仅是 HTML,我们首先需要谈谈文档对象模型(DOM)。简单来说,React 在将这些更改提交给用户浏览器之前,会在后台处理您的 JSX 代码,以加快用户页面的加载速度。让我们更深入地了解这一过程。

Note

HTML 中的 DOM 代表文档对象模型。它是 HTML 的内存表示,并且是树形结构。

数字正射影像图

文档对象模型是 HTML 甚至 XML 文档(如 SVG 图像)的 API。API 通过包含定义 HTML 元素的功能的接口以及它们所依赖的任何接口和类型来描述文档。HTML 文档包括对各种事物的支持和访问,如与用户交互,如事件处理程序、焦点、复制和粘贴等。

DOM 文档由节点( https://developer.mozilla.org/en-US/docs/Web/API/Node )的层次树组成,节点接口不仅允许访问文档,还允许访问每个元素,如图 1-3 所示。

img/503823_1_En_1_Fig3_HTML.jpg

图 1-3

DOM 文档由一个层次树组成

使用 Chrome 的 DevTools 控制台检查器(右键单击网页并选择检查➤控制台)并在控制台中键入以下内容:

window.document

图 1-4 显示了结果。您可以访问文档分层树结构以及构成该树的元素。

img/503823_1_En_1_Fig4_HTML.jpg

图 1-4

Chrome 的 DevTools 控制台检查器显示 DOM 文档

DOM 操作是现代交互式网页的核心,可以动态地改变网页的内容。从技术上来说,这可以通过使用 JavaScript 的方法来完成,如getElementByIdgetElementsByClassName以及removeChildgetElementById("myElement").remove();。这些 API 由 HTML DOM 公开,旨在提供在运行时更改代码的能力。

React 根据 React 元素表示确定对实际 DOM 进行什么更改,并在虚拟 DOM 中的后台进行更改。您可以将 React 虚拟 DOM 视为模拟。

然后,它只向实际用户的 DOM(浏览器)提交所需的更改。这个过程背后的原因是为了加快性能。

关键是实际的 DOM 树结构操作要尽可能快。例如,假设我们有一个产品页面,我们想更新产品列表的第一项。大多数 JavaScript 框架更新整个列表只是为了更新一个条目,而这个条目可能包含数百个条目。

大多数现代 web 页面拥有大型 DOM 结构,这种行为给用户带来了太多负担,导致 HTML 页面加载速度变慢。

引擎盖下的 React 虚拟 DOM

虚拟 DOM (VDOM)是一个编程概念,其中 UI 的理想或虚拟表示保存在内存中,并通过 ReactDOM 等库与“真实”DOM 同步。这个过程叫做和解。React VDOM 的目的是加快这一进程。

React 持有 HTML DOM 的一个副本(那就是虚拟 DOM)。一旦需要更改,React 首先对虚拟 DOM 进行更改,然后同步实际的 HTML DOM,避免了更新整个 HTML DOM 的需要,从而加快了过程。

例如,当我们渲染 JSX 元素时,每个虚拟 DOM 对象都会更新。更新整个虚拟 DOM 而不是实际的 HTML DOM 的过程更快。在对账过程中,React 需要弄清楚哪些对象发生了变化,这个过程叫做 diffing 。然后 React 只更新真实 HTML DOM 中需要更改的对象,而不是整个 DOM 树。

在我们的代码中,JSX 代码被包装在一个ReactDOM.render方法中。为了进一步挖掘幕后的过程,了解引擎盖下发生的事情,我们还需要了解巴别塔和 ES5/ES6。

巴别塔和 ES5/ES6

我们编写的 JSX 代码只是编写React.createElement()函数声明的一种更简洁的方式。每次组件使用 render 函数时,它都会输出一个 React 元素树或组件输出的 HTML DOM 元素的虚拟表示。

ECMAScript version 5 (ES5)是 2009 年完成的普通老式“常规 JavaScript”。所有主流浏览器都支持它。ES6 是下一个版本;它于 2015 年发布,增加了语法和功能。在撰写本文时,所有主流浏览器几乎都完全支持 ES6。事实上,React 团队在版本 17 中做了许多更改,以便与 ES6 更加一致和兼容。

我们希望利用 ES6 的功能;然而,与此同时,我们希望向后兼容旧的 ES5,这样我们就可以兼容所有版本的浏览器。为此,我们使用巴别塔。

Babel 是将 ES6 转换成 ES5 的库(不支持 ES6 的浏览器需要它)。ReactDOM.render()函数,顾名思义,渲染 DOM。render 函数应该返回一个虚拟 DOM(浏览器 DOM 元素的表示)。

Note

从巴别塔 8 开始,React 获得了一个特殊的功能。render方法改为jsx

注意,从 Babel 8 开始,React 得到了一个名为jsx的特殊函数来代替render函数。在 Babel 8 中,它还会在需要的时候自动导入react(或者其他支持新 API 的库),这样你直接写到 Babel 就不用再手动包含了。

例如,看看这个输入:

function Foo() {
  return <div />;
}

巴别塔会把代码变成这样:

import { jsx as _jsx } from "react/jsx-runtime";
function Foo() {
  return _jsx("div", ...);
}

同样从 Babel 8 开始,jsx将自动成为默认运行时。你可以在我关于媒介的文章中读到更多关于这些变化: http://shorturl.at/bxPZ7 .

现在您已经理解了 React 在幕后做什么以及 Babel 的角色,您可以回头看看我们的代码示例。注意,我们导入了三个库。

  • React version 17 :我们使用 React 来定义和创建我们的元素,例如使用生命周期钩子(在本书后面会有更多关于钩子的内容)。

  • ReactDOM 版本 17 :这是用于 web 应用的(移动设备有 React Native)。这就是为什么 React 和 ReactDOM 之间的代码会有一个分割。ReactDOM 是 React 和 DOM 之间的粘合剂。它包含了允许我们访问 DOM 的特性,比如用ReactDOM.findDOMNode()找到一个元素或者用ReactDOM.render()安装我们的组件。

  • 正如我们所解释的,Babel 是将 ES6 转换成 ES5 的库。

现在我们已经看了 React“Hello World”代码,我们理解了为什么我们要导入这些库以及在幕后发生了什么。这些关键概念对于理解 React 至关重要。如果你需要复习,请随时回到这一章。

在本节中,我们创建了一个名为“Hello World”的极简独立 React 应用,并回顾了如何工作。我们安装了 VS 代码集成开发环境,学习了一些重要的概念,比如 JSX、DOM、React 虚拟 DOM、Babel 和 ES5/ES6。

TypeScript 入门

在编写 React 代码时,有两种选择。您可以使用 JavaScript (JS)或 TypeScript (TS)编写代码。TypeScript 是 transpiler,这意味着 ES6 不理解 TS,但 TS 会被编译成标准的 JS,这可以用 Babel 来完成。在下一章中,我们将配置我们的项目来接受 TS 文件和 ES6 JS 文件。

为什么应该将 TypeScript 集成到 React 项目中?

以下是一些有趣的事实:

  • 您知道 TypeScript 是由微软开发和维护的开源编程语言吗?

  • 根据 2020 年 StackFlow 的一项调查,TypeScript 编程语言的受欢迎程度排名第二,超过了 Python!

  • ReactJS 框架以 35.9%的比例位居第二,它正在绕过“国王”jQuery。

  • 与此同时,32.9%的受访者说他们实际上害怕打字稿。

问题是,为什么 TypeScript 这么受欢迎?

TypeScript 与 JavaScript:有什么大不了的?

JavaScript 是一种脚本语言,而 TypeScript (TS)是一种成熟的编程语言。TS,顾名思义,就是要有更多的类型。TS 比 JS 更容易调试和测试,并且 TS 通过描述预期的内容来防止潜在的问题(当我们在本书后面测试我们的组件时,您将看到这一点)。拥有成熟的面向对象编程(OOP)语言和模块将 JS 带到了企业级,并提高了代码质量。

以下是 TypeScript 相对于 JavaScript 的优势:

  • TypeScript 是一种 OOP 语言;JavaScript 是一种脚本语言。

  • TypeScript 使用遵循 ECMAScript 规范的静态类型。

  • TypeScript 支持模块。

Note

类型系统将一个类型与每个值相关联。通过检查这些值的流程,它确保没有类型错误。

静态类型意味着在运行之前检查类型(允许您在运行之前跟踪 bug)。JS 只包括以下八种动态(运行时)类型:BigIntBooleanIntegerNullNumberStringSymbolObject(对象、函数、数组)和Undefined

Note

所有这些类型都被称为原始类型,除了Object,它是非原始类型。TS 通过设置编译器对源代码进行类型检查,将静态类型添加到 JavaScript 中,从而将其转换为动态代码。

React 和 TypeScript 配合得很好,通过使用 TypeScript 和描述类型,您的代码质量提高了使用 OOP 最佳实践的应用的代码质量,这是值得学习的。

在下一章中,我们将用 TypeScript 创建一个启动项目;但是,您可以现在就开始尝试 TypeScript 并学习基础知识。

这一步让您很好地了解了 TS 的类型,以及如何在代码中利用 TS 的力量。

TS 的版本是 4。要玩编码 TS,可以在 TS 游乐场运行实际的 TS 代码,可在 https://tc39.github.io/ecma262/#sec-ecmascript-language-types 处获得,如图 1-5 所示。

img/503823_1_En_1_Fig5_HTML.jpg

图 1-5

TS 游乐场

操场左边是 TS,右边是同样编译成 JS 的代码。接下来,打开 JavaScript 控制台,点击 Run 查看代码运行情况。

操场网站有大量的例子可以帮助你更好地理解 TS。我建议研究这些例子。

注意,这个例子使用了“strict”,在 Config 菜单项中,您可以从 Config 链接设置编译器选项。不同的编译器选项位于 https://www.typescriptlang.org/docs/handbook/compiler-options.html

这可能会对抗编写代码时左右弹出的错误,但它会避免以后编译器无法识别类型时出现的问题。我提到 TS 是 OOP,遵循 ECMAScript 规范;然而,规范是动态的,经常变化,所以您可以指定 ECMAScript (ES)目标。参见图 1-6 。

img/503823_1_En_1_Fig6_HTML.jpg

图 1-6

指定 TS 操场中的 ECMAScript 目标

打字稿备忘单

开始使用 TypeScript 的一个很好的地方是通过查看不同的可用类型来理解 TS 的功能。我的编程风格是遵循像莎士比亚那样的编码的函数命名惯例,让方法的名称自我解释。

要试用 TypeScript,请将此处显示的 TypeScript 备忘单代码粘贴到位于 https://www.typescriptlang.org/play/ 的 TS 游乐场。然后,您可以继续查看 JS 编译器的结果并运行代码。正如你所看到的,我把我的例子分成了基本类型和非基本类型。

// primitive types
// The most basic datatype - true/false switch
const flag1: Boolean = true;
// inferred-type
const flag2 = true;

// Be a programming god - create your own type
type Year = number;
const year: Year = 2020;

// Undefined and null not included in types like js, to
// get a null you will set it as either null or number
let willSetValueLater: null | number = null;
console.log('willSetValueLater: ' + willSetValueLater);
willSetValueLater = 2020;
console.log('willSetValueLater: ' + willSetValueLater);

// none-primitive

// Arrays
let arrayNumber: number[] = [];
let myCastTypeArrayNumber: Array<number> = [];
myCastTypeArrayNumber.push(123);
console.log('myCastTypeArrayNumber: ' + JSON.stringify(myCastTypeArrayNumber));

// Tuples (two values)
let longlatSimple: [number, number] = [51.5074, 0.1278];
let longlatInferredType = [51.5074, 0.1278];

// Enums design pattern
enum ChildrenEnum {JOHN = 'John', JANE = 'Jane', MARY = 'Mary'}
let john: ChildrenEnum = ChildrenEnum.JOHN;
console.log('What is JOHN enum? ' + john);

// Maps
// inferred type: Map<string, string>
const myLongLatMapWithInferredType = new Map([
  ['London', '51.5074'],
  ['London', '0.1278'],
]);

// interfaces

// Typing objects-as-records via interfaces
interface longlat {
  long: number;
  lat: number;
}
function longlatToString(longlat: longlat) {
  return `${longlat.long}, ${longlat.lat}`;
}
// Object literal (anonymous interfaces) inline:
function longlatToStringWithAnonymousInterfaces(longlat: {
  long: number;
  lat: number;
}) {
  return `${longlat.long}, ${longlat.lat}`;
}

// Place optional params (phone) and method in interface
interface Client {
    name: string;
    email: string;
    phone?: string

;
    longlatToString(longlat: longlat): string;
}

// Factory design pattern made easy using type cast
interface resultsWithUnkownTypeCast<SomeType> {
  result: SomeType;
}
type numberResult = resultsWithUnkownTypeCast<number>;
type arrayResult = resultsWithUnkownTypeCast<[]>;

// functions

// Simple function
const turnStringToNumber: (str: String) => Number =
    (str: String) => Number(str);
// %inferred-type: (num: number) => string
const turnStringToNumberMinimalist = (str: String) => Number(str);
console.log('turnStringToNumber: ' + turnStringToNumber('001'));
console.log('turnStringToNumberMinimalist: ' + turnStringToNumberMinimalist('002'));

function functionWithExplicitReturn(): void { return undefined }
function functionWithImplicitReturn(): void { }

// Simple functions with callbacks
function simpleFunctionWithCallback(callback: (str: string) => void ) {
  return callback('done something successfully');
}
simpleFunctionWithCallback(function (str: string): void {
    console.log(str);
});

// Never callback  - not placing never is inferred as never
function neverCallbackFunction(callback: (str: string) => never ) {
  return callback('fail');
}
// neverCallbackFunction(function(message: string): never {
//     throw new Error(message);
// });

// Complex Callback and specifiy result types
function complexCallbackWithResultType(callback: () => string): string {
  return callback();
}
console.log('doSomethingAndLetMeKnow: ' + (complexCallbackWithResultType(String), 'Done it!'));

// Function with optional params using '?'
function functionWithOptionalCallbackParams(callback?: (str: String) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback('sucess');
}

// Function with setting the type with default values
function createLonglatWithDefaultValues(long:number = 0, lat:number = 0): [number, number] {
  return [long, lat];
}
console.log('createLonglatWithDefaultValues: ' + createLonglatWithDefaultValues(51.5074, 0.1278))
console.log('createLonglatWithDefaultValues: ' + createLonglatWithDefaultValues())

// function with rest parameters
// A rest parameter declared by prefixing an identifier with three dots (...)
function functionWithRestParams(...names: string[]): string {
  return names.join(', ');
}
console.log(functionWithRestParams('John', 'Jane', 'Mary'));

// Function with potential two params types
function isNumber(numOrStr: number|string): Boolean {
  if (typeof numOrStr === 'string') {
      return false;
  } else if (typeof numOrStr === 'number') {
      return true;
  } else {
    throw new Error('Not sure the type');
  }
}
console.log('isNumber: ' + isNumber('123'));

您可以从这里下载完整的代码:

https://github.com/Apress/react-and-libraries/tree/master/01/TS-getting-started.ts

如果你需要更多的解释,请看官方的 TS 基本类型手册:

https://www.typescriptlang.org/docs/handbook/basic-types.html

将代码从 TS 转换为 JS 的 JS 编译器和控制台结果将帮助您理解这些示例。参见图 1-7 。

img/503823_1_En_1_Fig7_HTML.jpg

图 1-7

TypeScript 操场示例

React 模板启动项目选项

在编写 React 应用时,您有几个选择。您可以自己编写整个代码,就像我们在“Hello World”示例中所做的那样,然后添加库来帮助您完成打包代码并为生产做好准备的任务。另一个选择是使用一个 starter template 项目,它已经处理好了搭建和配置工作,并且包含了一些库,可以帮助您只需编写代码就可以快速完成工作。

正如您所看到的,VDOM 过程发生在幕后,当页面发生变化时,我们不需要刷新页面。

事实上,创建 React 应用最流行的模板是 Create-React-App (CRA)项目( https://github.com/facebook/create-react-app )。该项目由脸书团队创建,GitHub 上有近 84,000 颗星星。

CRA 是基于单页面应用(SPA)的,这很好,因为你不用刷新页面,感觉就像你在一个移动应用中。

这些页面应该在客户端呈现。这对中小型项目来说非常好。

另一个选项是服务器端呈现(SSR)。SSR 在服务器上呈现页面,因此客户端(浏览器)将显示应用,而无需做任何工作。这有利于某些用例(通常是大型应用),在这些用例中,如果渲染发生在客户端,用户体验会很慢。

CRA 一开始就不支持 SSR。有一些方法可以配置 CRA 并让它像 SSR 一样工作,但是对于一些开发人员来说,这可能太复杂了,并且需要您自己维护配置,所以可能不值得这样做。

如果您正在构建需要 SSR 的更大的东西,最好使用已经配置了 SSR 的不同 React 库,如 Next.js framework、Razzle 或 Gatsby(在构建时将 prerender 网站包含到 HTML 中)。

Note

如果你想用 React 和 Node.js 做服务器渲染,可以去看看 Next.js,Razzle,或者 Gatsby。Create React App 与后端无关,只生成静态的 HTML/JS/CSS 包。 https://github.com/facebook/create-react-app

也就是说,通过 CRA,我们可以做一个 prerender,这是最接近 SSR 的方法,在本书的最后一章,当我们优化 React 应用时,你会看到这一点。

在本书的例子中,我们将使用 CRA;但是,如果对这本书有了扎实的理解,您可以很容易地迁移到任何使用 React 的模板库。

摘要

在本章中,我们创建了一个名为“Hello World”的极简独立 React 应用,并探索了它的工作原理。我们安装了 VS 代码集成开发环境,学习了一些重要的概念,如 JSX、DOM、React 的虚拟 DOM 和 Babel,以及 ES5/ES、SPA、SSR 和 TypeScript。

在下一章,我们将学习更多关于 CRA 项目的知识,并用基本库建立我们的启动项目和开发环境。

二、React 起始项目和朋友

在这一章中,我们将涉及许多库,这可能会让人不知所措。然而,我想为你提供一个很好的基础,这样你就可以拥有最好的创业项目。启动项目将为您提供很好的服务,因为您可以复制并重用我们将在本书中使用的所有项目和示例的代码。这种实践将使你成为一流的 React 开发人员,能够处理任何规模的项目,并帮助你找到理想的工作。也会加速你的发展。

在本章结束时,你将会有一个入门项目,其中包括了我们将在本书中涉及的许多库。我们开始吧!

Create-React-App Starter 项目入门

在前一章中,我们从头开始创建了一个简单的“Hello World”应用,我们讨论了它在幕后是如何工作的。我们讨论了 JSX、DOM、React 虚拟 DOM、Babel、ES5/ES6 和 spa 等主题。

创建一个 React 应用很容易,只需要几行代码。然而,要创建一个基于一个页面(一个 SPA)的真实应用,这个页面有多个视图、许多用户手势、有成百上千个条目的列表、成员区域和其他在当今应用中常见的部分,我们需要学习的还有很多。

在本书中,我的目标是为您提供一个大型工具箱,里面装满了最好的库和工具,这样您就可以构建一个顶级的 React 应用,并充分利用 React。为此,我们需要看看 React 世界中几个流行的库和工具。

我们可以从零开始,但这不是必需的。正如我在前一章提到的,Create-React-App (CRA)是 React 基于 web 的应用最流行的启动项目。(见 https://github.com/facebook/create-react-app )。)它提供了一个样板项目,在构建顶级 React 应用时,您可以快速启动并运行许多必要的工具和标准库。

它包括香草口味的包装和其他更有价值的包装。此外,CRA 可以选择包含更复杂库的模板,或者您可以创建自己的模板,其中包含 CRA 没有包含的某些元素。

CRA 已经为我们做了一些决定。例如,构建工具是一个名为 Webpack over Rollup 或 Parcel 的工具。Task Runners 是用 NPM 脚本安装的,而不是像 Gulp 这样的工具。CSS、JS 和 Jest 被用作默认值。

在使用 React 完成工作的项目和库之后,很难保持中立而不推荐某些工具,这就是为什么我选择使用本章中的工具。此外,由于许多库不容易移植到,您将希望从正确的方面开始,而不是在以后切换工具。这就是为什么我们在这个库中建立我们的项目。这些库将帮助您完成工作,并且您可以将该模板用于其他项目,无论是小型项目还是大型企业级项目。

React 开发者路线图

成为一名真正的专业 React 开发人员不仅仅是了解 React 库。正如我们提到的,有一些工具和库可以帮助加速开发,因为记住,React 不是一个框架。React 是脸书开发的 JavaScript 库,用于处理用户界面,仅此而已。

为了帮助找出将 React 转变为一个成熟框架的部分,该框架能够高效地创建高质量的应用,并能与其他 JS 框架竞争,请查看图 2-1 中的路线图。

img/503823_1_En_2_Fig1_HTML.jpg

图 2-1

React 开发者路线图(来源: https://github.com/adam-golab/react-developer-roadmap )

该图表展示了成为顶尖专业 React 开发人员的推荐途径。

不要被这张图表吓倒。如果你阅读了这本书,当这本书结束时,你将会了解许多这样的库,并且能够处理任何规模的项目。

如何将这些工具集成到我的 ReactJS 项目中?

正如我提到的,CRA 没有包括许多可以帮助您处理现实生活中的 React 项目的工具。然而,我已经建立了一个 CRA 模板,将包括所有必须知道的图书馆。只需一个命令,您就可以获得包含所有需要的库的启动项目。我们将在本章中对此进行简要介绍。

请记住,事物瞬息万变;另外,您的项目可能需要不同的工具,或者您可能想要进行试验。这就是为什么在这一节我们将把它全部分解并展示如何安装每个库。

在本节中,我们将安装以下库:

  • 打字检查器:打字稿

  • 预处理器:萨斯/SCSS

  • 状态管理 : Redux 工具包/反冲

  • CSS 框架:素材-UI

  • 组件:样式组件

  • 路由:React 路由

  • 单元测试 : Jest 和 Enzym + Sinon

  • E2E 测试:笑话和木偶师

  • 棉绒:变得又亮又漂亮

  • 其他有用的库 : Lodash,Moment,Classnames,Serve,react-snap,React-Helmet,Analyzer Bundle

该列表是 React 库。文件夹结构是我将要介绍的内容的一部分,但它不是一个图书馆。我们可以加上这句话向读者解释。

先决条件

您可以使用 NPM ( https://www.npmjs.com/ )来安装这些库。你需要 Node.js 来获取 NPM。

NPM 和 Node.js 携手并进。NPM 是 JavaScript 包管理器,也是 JavaScript Node.js 环境的默认包管理器。

在 Mac/PC 上安装节点和 NPM

Node.js 版本至少应为 8.16.0 或 10.16.0。我们需要那个版本的原因是我们需要使用 NPX。NPX 是 2017 年推出的 NPM 任务跑步者,它用于设置 CRA。

如果缺少它,请在终端(Mac)或 Windows 终端(Windows)中键入以下内容进行安装:

$ node -v

如果没有安装,您可以从这里为 Mac 和 PC 安装(见图 2-2 ):

img/503823_1_En_2_Fig2_HTML.jpg

图 2-2

Nodejs.org

https://nodejs.org/en/

安装程序可以识别你的平台,所以如果你在 PC 上,步骤是一样的。

下载安装程序后,运行它。完成后,在终端中运行node命令,如下所示:

$ node -v

该命令将输出 Node.js 版本号。

下载库:纱线或 NPM

要从 NPM 资源库下载包,我们有两个选项:Yarn 或 NPM。NPM 是在我们安装 Node.js 的时候安装的,然而在本书中,我们将主要使用纱库。我们将尽可能使用 Yarn 来下载包,而不是 NPM,因为 Yarn 比 NPM 快。Yarn 缓存已安装的包并同时安装包。

在 Mac/PC 上安装 Yarn

要在 Mac 上安装 Yarn,请在终端中使用brew

$ brew install yarn

就像 Node.js 一样,运行带有-v标志的yarn来输发布本号。

$ yarn -v

在 PC 上,您可以从这里下载.msi下载文件:

https://classic.yarnpkg.com/latest.msi

您可以在此找到更多安装选项:

https://classic.yarnpkg.com/en/docs/install/#mac-stable

创建-React-应用 MHL 模板项目

配备了 Node.js 以及 NPM 和 Yarn,我们就可以开始了。我们可以使用我为您创建的 CRA 必备库(MHL)模板项目,它将产生本章的最终结果,并包括我们在本章中设置的所有库。

您可以从这里获得:

https://github.com/EliEladElrom/cra-template-must-have-libraries

将这个模板项目作为您的启动库是很好的,不仅因为它很容易并且包含了我们需要的所有库,还因为我将能够在书发布后很长时间内更新这个启动项目,以防出现任何问题或需要更新,就像 NPM 图书馆经常发生的那样。

您可以使用带有纱线的 CRA 模板和一个命令来创建本章的最终项目,如下所示:

$ yarn create react-app starter-project --template must-have-libraries

或者您可以使用 NPX 创建它,如下所示:

$ npx create-react-app your-project-name --template must-have-libraries

要运行这个项目,您可以将目录更改为starter-project并在终端中运行start命令,如下所示:

$ cd starter-project
$ yarn start

此命令将安装所有依赖项,并在本地服务器上启动项目。在下一节中,您将了解到更多关于引擎盖下发生的事情。

在这一章的其余部分,我将解释这个项目包括什么,并为你逆向工程这个项目,这样你就可以完全理解在引擎盖下发生了什么。因为你已经准备好了最终项目,所以不一定要完成本章中的所有步骤。

香草风味创造 React 应用

因为您能够安装 starter 模板项目,所以您可以立即开始开发。了解每个安装的库是很好的,这样你就可以完全理解这个项目包含了什么,并且可以根据你的具体需求定制你的项目。要创建 CRA 而不使用 CRA MHL 模板项目,您可以安装我们自己的每个库。只需使用 vanilla-flavor 模板,通过在终端中使用yarn运行以下命令来启动一个新的 CRA:

$ yarn create react-app hello-cra

请记住,yarn命令相当于下面使用 NPM NPX 任务运行器的命令,就像我们在 MHL 模板项目中看到的一样:

$ npx create-react-app hello-cra

第一个参数是我们正在下载的库。第三个参数的hello-cra是我们的项目名称。接下来,我们需要将目录更改为我们的项目。

$ cd hello-cra

最后,键入以下命令在终端/Windows 终端中启动项目:

$ yarn start

NPM 的情况也是如此,如下图所示:

$ npm start

我们在终端收到这条消息:

Happy hacking!
Compiled successfully!

您现在可以在浏览器中查看hello-cra

  Local:            http://localhost:3000
  On Your Network:  http://192.168.2.101:3000

请注意,开发构建并没有优化。要创建生产版本,请使用yarn build

我们的应用将使用默认浏览器在端口 3000 上自动打开。如果没有,可以用任何浏览器,用这个网址:http://localhost:3000/

Note

如果您想停止开发服务器,请使用 Ctrl+C。

后台发生的事情是 CRA 应用为我们创建了一个开发服务器。参见图 2-3 。

img/503823_1_En_2_Fig3_HTML.jpg

图 2-3

我们浏览器 3000 端口上的香草 CRA

祝贺您,您刚刚在开发服务器上创建并发布了 CRA!React 项目包括 CRA 开箱即用的所有库。

如果你检查为我们创建的代码和文件(见图 2-4 ,你可以看到有许多文件组成了我们的单页应用。例子有App.jsindex.js。我们也有样式表文件,如App.cssindex.css

img/503823_1_En_2_Fig4_HTML.jpg

图 2-4

CRA 文件和文件夹

React 最初被设计为 SPA,通常我们需要编写一个脚本来将我们的文件组合成一个文件或一大块文件,我们需要将这些文件包含在我们的应用中,以便代码可用。但是,由于我们使用 CRA,所有这些都是自动完成的。

CRA 使用 Webpack 和 Babel 将我们的文件打包到一个单独的index.html页面和名为bundle.js*.chunk.js的文件中。它可以根据需要生成其他文件。

Note

CRA 正在使用现成的 Webpack 模块捆绑器。Webpack 是一个开源的 JavaScript bundler,它使用加载器来捆绑文件和创建静态资产。

当我们键入命令yarn start时,脚本启动一个 NPM 脚本,为我们创建一个开发环境。该脚本将我们的文件缓存在内存中,并在我们导航到 URL 时提供给我们。

看一下图 2-5 ,它说明了这个过程。

img/503823_1_En_2_Fig5_HTML.jpg

图 2-5

从 10,000 英尺的角度来说明 CRA 使用 Webpack 和 Babel 将我们的文件打包到一个 index.html 页面中

要查看应用的源代码并检查为我们创建的文件,请访问 Chrome 上的 URL:http://localhost:3000/

或者我们也可以用 Chrome DevTools 检查 HTML DOM 元素。在 Chrome 上右键单击并选择 Inspect 来查看 DOM 树。

您可以在 body HTML 标记中看到以下文件:

<script src="/static/js/bundle.js"></script>
<script src="/static/js/0.chunk.js"></script>
<script src="/static/js/main.chunk.js"></script>

bundle.js文件,顾名思义,将我们的 JavaScript 源代码文件捆绑成一个文件,*.chunk.js文件将我们的样式捆绑成文件块。您可以访问这些文件的 URL 来查看内容。

繁重的工作是在我们的项目中一个名为node_modules的文件夹中完成的,这个文件夹包含了许多我们的项目正在使用的依赖库。看一下图 2-6 。

img/503823_1_En_2_Fig6_HTML.jpg

图 2-6

具有依赖关系的 node_modules 文件夹

您可以在 Node.js 使用的名为package.json的文件中的node_modules文件夹中找到依赖项列表。该文件包含有关我们项目的信息,如版本、正在使用的库,甚至 Node.js 将运行的脚本。

打开package.json文件。请注意,如果与node_modules文件夹中的长列表相比,我们的项目使用的库依赖列表很短(请记住,在图书发布后,库版本可能会经常更改)。

"dependencies": {
  "@testing-library/jest-dom": "⁴.2.4",
  "@testing-library/react": "⁹.3.2",
  "@testing-library/user-event": "⁷.1.2",
  "react": "¹⁶.13.1",
  "react-dom": "¹⁶.13.1",
  "react-scripts": "3.4.3"

每个依赖项都包含库名和版本号。

如果我们检查我们的node_modules文件夹,它充满了其他开发者的库。事实上,有 1000 多个,当我们下载我们的 CRA 时,需要一段时间(当然取决于你的网速)来下载项目。

这是因为所有这些依赖性。那么,所有这些依赖来自哪里呢?

每个库都包含其他依赖项或子依赖项,所以尽管我们没有在项目的package.json文件中列出这些库,但是它们会在其他库或子库中列出。

还要注意,在我们的package.json文件中有一个指定脚本的部分。

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

事实上,当我们运行yarn start时,创建本地服务器的过程正在进行。那个剧本是从哪里来的?要回答这个问题,请打开下面的库,查看下面构建开发服务器的代码:

hello-cra/node_modules/react-scripts/scripts/start.js

React 脚本

这些脚本正在使用一个名为react-scripts的库。如果您转到hello-cra/node_modules/react-scripts/package.json文件,您会看到一长串依赖项,代码使用这些依赖项将文件与 Babel 和 Webpack 打包并构建我们的服务器。这些子库中的每一个都有其他的依赖项,依此类推。

这就是为什么我们的node_modules文件夹中有超过 1000 个库。

吉蒂尔

通常的做法是在每个项目中创建一个.gitignore文件。该文件可以包含我们想要排除的文件。

例如,CRA 已经包含了一个.gitignore文件,而node_modules已经列出了要排除的文件。然后我们运行yarn命令并检查package.json文件,它为我们安装了所有这些依赖项,而不是将所有这些库包含在我们的项目中,这将使我们的项目非常大。

公共文件夹

我们的应用中还有一个名为public的文件夹,其中包含我们的应用图标。具体来说,它包含以下内容:

  • public/favicon.icologo192.pnglogo512.png:在manifest.json文件中使用的图标

  • public/index.html:我们的索引页

  • 关于我们的应用和风格的信息

  • public/robots.txt:搜索引擎说明

如果我们检查我们的index.html页面,我们会看到在manifest.json文件中设置的令牌,但是如果我们打开 HTML body 标签中的public/index.html文件,我们不会看到任何 JS 包块,比如我们在浏览器中检查代码时看到的*.chunk.jsbundle.js文件。看一看:

public/index.html;
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>

原因是 NPM 脚本根据这个文件以及它生成的文件来生成我们的索引文件。例如,如果我们想将我们的应用发布到生产环境中,我们将运行不同的命令(build而不是start),并且会生成不同的文件。在本书的后面,您将了解更多关于发布到产品以及如何优化 JS bundle 块的信息。

您可以从这里下载代码:

https://github.com/Apress/react-and-libraries/tree/master/01/hello-cra

要让 Yarn 使用package.json文件并下载所有依赖项,运行以下命令:

$ yarn start

使用类型脚本创建-React-应用

CRA 有两种风格:香草和打字稿。注意,我们的应用文件src/public/App.jssrc/public/index.js.js文件扩展名,这意味着它们是 JavaScript 文件。CRA 将我们项目的默认设置设置为 JavaScript。

然而,在编写 React 代码时,有两个主要选项。我们可以用 JavaScript (JS)或 TypeScript (TS)编写代码。您下载的香草味 CRA 被设置为 JavaScript。然而,正如我们在前一章提到的,我们将在本书中使用 TypeScript 编写代码。

带打字稿的 CRA 模板

要使用 Yarn 设置我们的 TS 项目,命令类似于我们设置 CRA 的方式。唯一的区别是我们为 React 社区创建的 TypeScript 添加了 TS 模板。

$ yarn create react-app starter-project --template TypeScript

我们在 TS 中使用了--template标志,并将我们的项目命名为starter-project

现在,将目录更改为您的项目,并启动项目以确保一切顺利,就像我们之前所做的那样。

$ cd starter-project

Tip

我建议在每次安装后测试您的项目,以确保它仍然在运行。库和依赖项经常变化,确保项目不中断应该是您的首要任务。

$ yarn start

应用应该在端口 3000 上打开,就像我们之前做的一样:http://localhost:3000/.参见图 2-7 。

img/503823_1_En_2_Fig7_HTML.jpg

图 2-7

在端口 3000 上运行的 CRA TS 应用

请注意,应用上的副本更改为src/App.tsx(来自图 2-3 ),我们应用中的文件也从.js更改为.tsx

接下来,我们需要为 TS 添加以下类型,以便 Webpack 知道如何处理.tsx文件类型并捆绑它们,这样它们就可以作为静态资产包含在我们的项目中。

$ yarn add -D typescript @types/node @types/react @types/react-dom @types/jest @typescript-eslint/scope-manager

当使用 Yarn 时,当我们想要更新项目的package.json以在devDependencies下包含一个库时,我们使用-D标志(代表开发者“依赖”)。package.json文件保存了项目的库。

Note

为 TypeScript 安装的类型用于为 TypeScript 提供有关用 JavaScript 编写的 API 的类型信息。

我们刚刚安装的类型包括 Jest 测试的 TypeScript 和 ESLint。CRA 捆绑了 Jest 和 Jest-dom 来测试我们的应用。我们将在本书的后面学习用 Jest 和 ESlint 测试我们的应用,但是我想让你知道我们已经在设置这些类型了。

除了将我们的代码文件从.js更改为.ts,模板项目还包括一个名为tsconfig.json的文件。这个文件为编译器保存了一个特定的设置,编译器将编译从.tsx.js的文件,其中包含我们的目标信息,比如 ES6 和其他设置。

CSS 预处理程序:Sass/SCSS

级联样式表(CSS)是 HTML 的核心功能,如果您还不熟悉 CSS,那么您需要熟悉它。这尤其适用于 HTML,尤其是 React。在大型项目中,CSS 预处理程序通常用于补充 CSS 和添加功能。我们将在本书的后面讨论这些。

就 CSS 预处理程序而言,有四种主要的 CSS 预处理程序经常与 React 项目一起使用:Sass/SCSS、PostCSS、Less 和 Stylus。

Note

CSS 用于表示不同设备上网页的可视布局。CSS 预处理器用于增强 CSS 功能。

萨斯/SCSS 公司在今天的大多数项目中占据上风,所以这就是我们将使用的。事实上,萨斯/SCSS 是最受欢迎的,根据调查( https://ashleynolan.co.uk/blog/frontend-tooling-survey-2019-results )作为一名开发人员,它可能会给你带来收入最高的工作。如果你想看不同的 CSS 预处理程序之间的比较,可以看看我关于 Medium 的文章: http://shorturl.at/dJQT3 .

我们将在本书的后面学习更多关于 CSS 和 SCSS 的知识,但是现在,用 Yarn 安装它。

$ yarn add -D node-sass

就像 CSS 一样,如果我们想像在 JavaScript 中一样在 TypeScript 中使用 SCSS 模块,我们需要安装 Webpack 的 Sass/SCSS 加载器。它叫做scss-loader.

因为我们正在使用 TS,我们需要一个能够与 TS 一起工作并为 Sass/SCSS 生成类型的scss-loader的替代品。用纱线安装加载器。

$ yarn add -D scss-loader typings-for-scss-modules-loader

Redux 工具包/反冲

Redux 工具包 是组织 React 应用数据和用户交互的标准方式,因此您的代码不会变得混乱。

Note

Redux 是一个用于管理应用状态的开源 JavaScript 库。它通常与 React 一起用于构建用户界面。 https://redux.js.org

我们将在本书的后面讨论 Redux 和 Redux 工具包。现在,让我们安装 Redux 工具包库和类型。

$ yarn add -D redux @reduxjs/toolkit react-redux @types/react-redux

反冲是另一个由脸书团队创建的状态管理库,我相信它将接管 Redux 工具包。我们将在本章的后面使用这两个库。要安装反冲,使用:

$ yarn add recoil@⁰.0.13

材质-UI CSS 框架

CSS 框架(或 CSS 库)是基于为你的 web 开发带来更标准化的实践的概念。与只使用 CSS(或其他样式表)相比,使用 CSS 框架可以加快您的 web 开发工作,因为它允许您使用预定义的 web 元素。

是的,我们可以创建所有的定制组件并设计它们的样式,但是大多数时候这是不值得的,除了编写组件,我们还需要在所有的浏览器和设备上测试它们。你能想象吗?

import 'bootstrap/dist/css/bootstrap.css';

在某些项目中,Bootstrap 和 Material-UI 都是很好的 CSS 框架,无需花费大量时间创建自己的组件就可以立即开始使用。

为了安装它,我将设置 Material-UI 核心以及图标包。

$ yarn add -D @material-ui/core @material-ui/icons

为了让 Material-UI 与 TS 无缝协作,我们还需要更新我们的tsconfig.json文件中的一些设置,以便 React 上的 TS 页面不会遇到任何错误。

用文本编辑器打开tsconfig.json并更新。

// tsconfig.json
{
  "compilerOptions": {

    "target": "es5",
    "lib": [
      "es6", "dom",
      ...
      ...
    ],
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,

样式组件

风格化的组件与 Material-UI 密切相关。Styled Components 是一个样式化的解决方案,也可以用在 Material-UI 之外。

要将样式化的组件和类型添加到我们的项目中,请使用:

$ yarn add -D styled-components @types/styled-components

此外,如果您需要安装字体,请将此链接放在手边:

https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Ffontsource%2Ffontsource

React 路由

React 基于单页应用;然而,在 React 的单页面应用范例中,大多数应用需要多个视图。

在单个组件上构建应用并不理想,因为代码和复杂性可能会增加,这可能会成为开发人员维护和测试的噩梦。我们将在下一章学习更多关于创建组件和子组件的知识。

为了处理路由,有许多工具可供选择:React Router、Router5、Redux-First Router 和 Reach Router 等等。

在撰写本文时,React 项目的标准是 React Router。要为 Webpack 添加 React 路由和类型,请执行以下命令:

$ yarn add -D react-router-dom @types/react-router-dom

Jest 和酶+否则

Jest 是 JavaScript 单元测试框架,也是 React 应用的标准。它是为任何 JavaScript 项目而构建的,并且是 CRA 自带的。然而,我们确实需要 Jest-dom 和 Enzyme 来增强 Jest 的能力。

对于 Enzyme,我们希望安装 React 16 适配器(这是撰写本文时的最新版本,但可能会更改为 17)。此外,我们需要安装react-test-renderer,这样我们就可以将 React 组件呈现为纯 JavaScript 对象,而不依赖于 DOM 或原生移动环境。在本书的后面,我们将使用 Jest 的快照测试特性来自动将 JSON 树的副本保存到一个文件中,并使用测试来检查它是否发生了变化。

要安装这些工具,请使用以下命令:

$ yarn add -D enzyme enzyme-adapter-react-16 react-test-renderer

我们还希望通过安装enzyme-to-json库来简化我们的生活,这样我们在本书后面使用这些库时,我们的代码会得到简化。

$ yarn add -D enzyme-to-json

不然呢

另一个我们应该知道并添加到我们工具箱中的必备库是 Sinon ( https://github.com/sinonjs/sinon )。以下是您可以用来添加它的命令:

$ yarn add sinon @types/sinon

Jest 和 Sinon 的目的是一样的,但是有时候你会发现一个框架对于特定的测试来说更自然、更容易使用。我们将在本书后面讨论 Sinon。

E2E 测试:笑话和木偶

测试是交付高质量软件的核心。测试是有层次的,通常只有在单元测试和集成测试完成后才会考虑 E2E。

端到端测试(E2E 测试)是一种测试方法,包括从头到尾测试我们的应用工作流程。我们在 E2E 所做的是复制真实的用户场景,因此我们的应用在集成和数据完整性方面得到了验证。

E2E 测试的解决方案是 Jest 和木偶师。木偶师是最受欢迎的 E2E 解决方案,它与 Jest 集成。要开始这项工作,请使用:

$ yarn add puppeteer jest-puppeteer ts-jest

不要忘记添加 TS 的类型。

$ yarn add yarn add @types/puppeteer @types/expect-puppeteer @types/jest-environment-puppeteer

这就是你设置 Jest 和 Puppeteer,配置所有东西,查看 E2E 测试App.tsx的例子所需要的。在这本书的后面,你会学到更多关于 E2E 的知识。

组件文件夹结构

当您在一个 React 项目中工作,并且代码不断增长时,您可能拥有的组件数量会变得令人不知所措,然后很难找到它们。

组织你的组件以便更容易找到的一个简洁的方法是将组件分成一个独立的组件类型,如图 2-8 所示。

img/503823_1_En_2_Fig8_HTML.jpg

图 2-8

Redux 和 TS 的建议 React 文件夹结构

这是我推荐的开始使用的文件夹结构;但是,请随意使用它作为一个建议。

要遵循此结构,请创建以下文件夹:

  • src/components

  • src/features

  • src/layout

  • src/pages

  • src/redux

  • src/recoil/atoms

如果您使用的是 Mac,您可以在终端中使用这个一行程序:

$ mkdir src/components src/features src/layout src/pages src/redux src/recoil/atoms

生成模板

作为开发人员,我们不喜欢一遍又一遍地写代码。是我们可以使用的有用工具。它是基于模板的,所以我们不需要一遍又一遍地写代码。

如果您来自 Angular,您可能会喜欢 Angular CLI,它可以为您的项目生成模板。

您可以在 React 中以类似的方式使用generate-react-cli项目生成项目模板。为了安装,我们将使用 NPX 任务脚本。

$ npx generate-react-cli component Header

因为这是第一次运行这个脚本,它会用您第一次使用这个工具时选择的选项来安装和创建generate-react-cli.json,但是您可以随意手动更改这些选项。

一个很酷的功能是我们可以创建自己的模板。下面是一个为 React 页面创建自定义模板的示例。

此时不要深究模板代码。我们只是设置这些模板,我们将在下一章构建我们的组件时检查代码及其含义。

generate-react-cli.json改为指向我们将要创建的模板文件。

{
  "usesTypeScript": true,
  "usesCssModule": false,
  "cssPreprocessor": "scss",
  "testLibrary": "Enzyme",
  "component": {
    "default": {
      "path": "src/components",
      "withStyle": true,
      "withTest": true,
      "withStory": false,
      "withLazy": false
    },
    "page": {

      "customTemplates": {
        "component": "templates/page/component.tsx",
        "style": "templates/page/style.scss",
        "test": "templates/page/test.tsx"
      },
      "path": "src/pages",
      "withLazy": false,
      "withStory": false,
      "withStyle": true,
      "withTest": true
    },
    "layout": {
      "customTemplates": {
        "component": "templates/component/component.tsx",
        "style": "templates/component/style.scss",
        "test": "templates/component/test.tsx"
      },
      "path": "src/layout",
      "withLazy": false,
      "withStory": false,
      "withStyle": true,
      "withTest": true
    }
  }
}

用一个 React 路由和一个到路径名:templates/component/component.tsx的钩子为TypeScript类页面组件创建一个模板文件。在下一章中,我们将创建一个自定义的 React 组件,这个模板组件将是有意义的。当然可以把作者名改成自己的名字和网址。

/*
Author: Eli Elad Elrom
Website: https://EliElrom.com
License: MIT License
Component: src/component/TemplateName/TemplateName.tsx
*/

import React from 'react';
import './TemplateName.scss';
import { RouteComponentProps } from 'react-router-dom'

export default class TemplateName extends React.PureComponent<ITemplateNameProps, ITemplateNameState> {

  constructor(props: ITemplateNameProps) {
    super(props);
    this.state = {

      name: this.props.history.location.pathname.substring(
        1,
        this.props.history.location.pathname.length
      ).replace('/', '')
    }
  }

  // If you need 'shouldComponentUpdate' -> Refactor to React.Component
  // Read more about component lifecycle in the official docs:
  // https://reactjs.org/docs/react-component.html

  /*
  public shouldComponentUpdate(nextProps: IMyPageProps, nextState: IMyPageState) {
    // invoked before rendering when new props or state are being received.
    return true // or prevent rendering: false
  } */

  static getDerivedStateFromProps:
    React.GetDerivedStateFromProps<ITemplateNameProps, ITemplateNameState> = (props:ITemplateNameProps, state: ITemplateNameState) => {
    // invoked right before calling the render method, both on the initial mount and on subsequent updates
    // return an object to update the state, or null to update nothing.
    return null
  }

  public getSnapshotBeforeUpdate(prevProps: ITemplateNameProps, prevState: ITemplateNameState) {
    // invoked right before the most recently rendered output is committed
    // A snapshot value (or null) should be returned.
    return null
  }

  componentDidUpdate(prevProps: ITemplateNameProps, prevState: ITemplateNameState, snapshot: ITemplateNameSnapshot) {
    // invoked immediately after updating occurs. This method is not called for the initial render.
    // will not be invoked if shouldComponentUpdate() returns false.
  }

  render() {
    return (

      <div className="TemplateName">
        {this.state.name} Component
      </div>)
  }
}

interface ITemplateNameProps extends RouteComponentProps<{ name: string }> {
  // TODO
}

interface ITemplateNameState {
  name: string
}

interface ITemplateNameSnapshot {
  // TODO
}

创建一个 SCSS 文件模板:templates/component/style.scss

.TemplateName {
  font-family: 'Open Sans', sans-serif;
  font-weight: 700;
}

用酶创建一个测试文件:templates/component/test.tsx

import React from 'react'
import { shallow } from 'enzyme'
import TemplateName from './TemplateName'

describe('<TemplateName />', () => {
    let component

    beforeEach(() => {
        component = shallow(<TemplateName />)
    });

    test('It should mount', () => {
        expect(component.length).toBe(1)
    })
})

此时,你应该有一个模板文件夹,里面的文件如图 2-9 所示。

img/503823_1_En_2_Fig9_HTML.jpg

图 2-9

模板文件夹结构和文件

您可以对类型页面的组件或任何您喜欢的组件重复相同的步骤。

林挺:埃斯林特和更漂亮

进行代码审查,并让别人格式化你的代码以确保它的一致性,这有多好?

任何代码库中的所有代码都应该看起来像是一个人输入的,不管有多少人贡献。

——瑞克·瓦德伦,约翰尼-五

的创造者

幸运的是,这是可以做到的。

Lint 是一个分析代码的工具。它是一个静态代码分析工具,用来识别在代码中发现的有问题的模式。漂亮是一个固执己见的代码格式化程序。

Note

林挺是运行程序来分析您的代码以发现潜在错误的过程。

Lint 工具可以分析您的代码,并警告您潜在的错误。为了让它工作,我们需要用特定的规则来配置它。

争论每一行或一个制表符中是否应该有两个空格,应该有单引号还是双引号等等是不明智的。这个想法是有某种风格指南,并遵循它的一致性。正如有人说得好:

关于风格的争论毫无意义。应该有一个风格指南,你应该遵循它。

—丽贝卡·墨菲

作为其风格指南的一部分,Airbnb 提供了一个 ESLint 配置,任何人都可以将其用作标准。

ESLint 已经安装在 Create-React-App 上,但它没有使用样式指南和 TypeScript 进行优化。

要使用 Airbnb 的样式指南(被认为是标准的)用 ESLint 和 Prettier for TypeScript 设置您的项目,请使用以下内容:

$ yarn add -D --save-exact eslint-config-airbnb eslint-config-airbnb-TypeScript eslint-config-prettier eslint-config-react-app eslint-import-resolver-TypeScript eslint-loader eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks babel-eslint eslint-plugin-jest @TypeScript-eslint/parser @TypeScript-eslint/eslint-plugin$ yarn add -D --save-exact prettier prettier-eslint prettier-eslint-cli eslint-plugin-prettier

阅读我的文章( https://medium.com/react-courses/react-create-react-app-v3-4-1-a55f3e7a8d6d )了解更多信息。

按照shorturl.at/otuU8的指示更新以下文件,或者从 CRA·MHL 模板项目中复制它们。

我们将配置三个文件。

  • .eslintrc : ESLint 运行命令配置文件。

  • .eslintignore斯洛文尼亚语忽略文件

  • .prettierrc:漂亮运行命令配置文件

最后,我们可以更新package.json文件的运行脚本,这样我们就可以运行 Lint 和 format 实用程序,甚至只用一个命令就可以运行应用构建(我们将在本书后面介绍的生产构建)。

"scripts": {
    ..
    ..
    ..
    "lint": "eslint --ext .js,.jsx,.ts,.tsx src --color",
    "format": "prettier --write 'src/**/*.{ts,tsx,scss,css,json}'",
    "isready": "npm run format && npm run lint && npm run build"
}

我们已经准备好让 Lint 完成它的工作并修改我们的代码(见图 2-10 )。

img/503823_1_En_2_Fig10_HTML.jpg

图 2-10

运行 Lint 后的输出

$ yarn run lint

为了运行格式化程序来清理我们的代码,我们也可以使用 Yarn。

$ yarn run format

现在,通过检查端口 3000 或运行yarn start确认我们仍然可以编译,如果你停止了进程(见图 2-11 )。

img/503823_1_En_2_Fig11_HTML.jpg

图 2-11

格式化并林挺后编译我们的代码

$ yarn start

其他有用的库

我们将安装几个有用的库,它们将在本书后面的开发练习中派上用场。

同学们

Classnames ( https://github.com/JedWatson/classnames )是一个简单的 JavaScript 实用程序,用于有条件地将classNames连接在一起。

$ yarn add -D classnames @types/classnames

下面是它的用法示例:

import classNames from 'classnames'
const footerClasses = classNames('foo', 'bar') // => 'foo bar'

属性类型

Prop-types 是一个很棒的小工具( https://github.com/facebook/prop-types ),用于 React 属性和类似对象的运行时类型检查。我们正在用 TypeScript 设置我们的 starter 项目,所以我们真的不需要*?? 这个工具,因为我们将传递 TypeScript 对象并进行类型检查。然而,仅仅因为我们使用 TS 并不意味着我们永远不需要 JS。有些情况下,比如从一个不同的项目中导入一个组件,我们可能需要这个小工具。*

$ yarn add -D prop-types

您可以从这里下载代码:

https://github.com/Apress/react-and-libraries/tree/master/01/starter-project

下面是它的用法示例:

import PropTypes from "prop-types";
whiteFont: PropTypes.bool

要让 Yarn 使用package.json并下载所有依赖项,运行以下命令:

$ yarn start

其他有用的实用程序

以下是一些附加的有用实用程序:

  • Lodash ( https://github.com/lodash/lodash ):这让 JS 变得更容易,因为它免去了处理数组、数字、对象、字符串等的麻烦。

  • 时刻 ( https://github.com/moment/moment ):对于与日期打交道的人来说,这是必备的。

  • 发球 ( https://github.com/vercel/serve ):用$ yarn add serve安装这个。它添加了一个本地服务器。CRA 脚本包括发布应用的运行脚本。它会生成一个名为build的文件夹。我们希望能够在投入生产之前测试我们的构建代码。您将在后面的章节中了解更多关于生产构建的内容。

  • Precache React-snap 离线工作:这是一个优化库,我们将使用它来配置我们的应用离线工作。参见第十一章。

  • react-helmet change header metadata:为 SEO 更新每个页面的一个表头;你会在第十一章学到更多。

  • 分析器包:你可以安装source-map-explorercra-bundle-analyzer工具来查看我们的 JS 包块内部(更多内容在第十一章)。

摘要

在这一章中,我们学习了 Create-React-App 项目,并用我们将在本书中学习的基本库设置了我们的启动项目和开发环境。我们安装了 CRA·MHL 模板项目,它已经包含了我们需要的一切,我们还学习了香草 CRA 和 TypeScript 模板。

我们还了解了一些库,如 NPM、Yarn、Webpack、NPM 脚本、TypeScript、萨斯/SCSS、Redux 工具包、Material-UI、样式组件、路由、Jest 和 Enzyme、生成模板、ESLint 和 appelliter 以及其他有用的库。

在下一章,我们将构建 React 定制组件和子组件。

三、React 组件

在这一章中,我会给你一个 React 组件的概述,以及你可以在 React 中用它们做什么。您需要理解 React 组件是什么,因为它们是 React 的核心。

在前面的章节中,我们创建了我们的第一个 React 项目,我们设置了我们的环境,我们创建了一个 starter 项目,它包括了我们将在本书中使用的许多库。

我们的简单项目已经包含了组件和子组件。在本章中,我们将更深入地研究组件,并创建更复杂的组件和子组件。我们还将查看相关的库,它们可以帮助我们加速开发以及维护我们的项目。

什么是 React 组件?

React 组件类似于函数。它们允许您通过将复杂的 UI 分解成独立的小块来构建前端实现。事实上,React 的核心只不过是协调工作的组件的集合。看看 React.org 对组件是怎么说的:

组件让你将用户界面分割成独立的、可重用的部分,并孤立地考虑每一部分。【—React.org 文档】

https://reactjs.org/docs/components-and-props.html

有三种类型的组件。

  • 功能组件

  • 类别组件

  • 工厂组件

    以正确的方式编写组件可以帮助您降低应用的复杂性,确保您为工作选择正确的组件类型,避免缺陷,并提高性能。

本节分为以下几个部分:

  • JavaScript (JS)函数和类组件

  • TypeScript (TS)函数和类组件

  • 外来组件,如工厂组件

  • 复杂的 TS 工厂组件

  • React.PureComponentReact.Component

JavaScript 函数和类组件

函数组件(也叫功能性无状态组件)无非就是 JavaScript 函数。它们与函数式编程(FP)携手并进。FP 意味着用纯功能构建我们的软件,避免共享状态、可变数据和副作用。

FP 是声明性的而不是命令性的,应用状态流过纯函数。因为 React 是一种声明式语言(它实际上并不直接操纵 DOM 本身),所以它与 React 集成得很好,因为我们希望使用声明式架构。

Note

声明式编程是一种表达计算逻辑而不描述其控制流的范式。命令式范式使用改变程序状态的语句,比如直接改变 DOM。

如果我们想通过在 HTML 文件的 JavaScript 标记内创建一个 React 组件来编写最基本的 React 组件,代码应该如下所示。在我们在第一章编写的应用中,我们通过在一个带有 JavaScript 标签的 HTML 文件中创建一个 React 组件来编写最基本的 React 组件。

<div id="app"></div>
<script type="text/babel">
    ReactDOM.render(
    <h1>Hello World</h1>,
    document.getElementById('app')
    );
</script>

以简单的“Hello World”为例,假设我们想要传递用户定义的属性。我们可以设置一个函数并分配一个用户定义的属性,然后将其传递给 React DOM render,作为 JSX 代码执行。请参见以下示例:

function WelcomeUser(props) {
    return <h1>Hi {props.userName}</h1>;
}
const element = <WelcomeUser userName="John" />;

ReactDOM.render(
    element,
    document.getElementById('app')
);

props代表我们传递的属性和 React Element返回的函数。这里我们传递一个userName,它将显示在我们的用户界面上。

在自己喜欢的浏览器中打开这个例子,如图 3-1 所示。

img/503823_1_En_3_Fig1_HTML.jpg

图 3-1

WelcomeUser.html 输出示例

您可以从 GitHub 网站下载该代码。

https://github.com/Apress/react-and-libraries/tree/master/03/ WelcomeUser.html

JavaScript 功能组件

当我们编写组件时,我们通常不会使用一个组件,而是将子组件包含在父组件中。

我们可以看到,例如,在创建-React-应用(CRA)。App子组件嵌套在我们的主组件索引中。

ReactDOM.render(<App />, document.getElementById('root'))

然后,我们可以创建一个子组件,并用纯 JavaScript 编写子组件。看一下这个基本的例子,它给我们同样的“Hi John”结果,但是这次是在我们的 CRA starter 项目中,而不是在独立的 HTML 页面中:

function Welcome(props) {
  return <h1>Hi {props.userName}</h1>;
}

Javascript 类组件

使用纯 JavaScript 创建一个带有props的类组件来产生与Hello userName相同的输出是类似的。使用 ES6 语法,产生相同输出的 JS 类组件如下所示:

class Welcome extends React.Component {
  render() {
    return <h1>Hi {this.props.userName}</h1>;
  }
}

React 钩

在函数和类组件中,我们可以使用钩子来访问状态和组件生命周期特性。如果我们通过从 React 导入特性来使用Hook函数,那么类组件会扩展React.Component

Note

钩子是允许我们“挂钩”React 状态和生命周期特性的函数。

你可以在 ReactJS.org 网站上找到关于钩子的好资源。

https://reactjs.org/docs/hooks-overview.html

对于下一步,您可以使用您在上一步中创建的starter-project,或者使用一个命令重新开始,如下所示:

$ yarn create react-app starter-project --template must-have-libraries

接下来,我们可以将目录更改为新项目并启动项目。

$ cd starter-project
$ yarn start

如果端口 3000 没有被另一个应用使用,该应用应该在该端口上运行,换句话说,http://localhost:3000。更多细节请参考上一章。

CRA 生成了一个名为src/index.tsx的组件,它将另一个组件应用封装在 React 渲染函数中。

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

如果我们查看src/App.tsx子组件,我们会发现 JSX 代码,它包括我们运行纱线启动脚本(yarn start)时看到的代码。

import React from 'react'

function App() {

  return (
    <div className="App">
    ..
    </div>
  )
}

export default App

如果我们想利用 React 的状态特性,我们需要从 React 库中导入useState特性。

import {useState} from 'react'

接下来,我们可以在函数组件中编写 JSX 代码,该代码将呈现一个按钮,该按钮在单击事件时将增加计数器的值。计数器变量就是我们的state

function App() {
  const [count, setCount] = useState(0);
  return (
  ..
  ..
  <p>You clicked {count} times</p>
  <button onClick={() => setCount(count + 1)}>
    Click me
  </button>
  ..
  ..
  )
}

export default App

useState是状态挂钩;它返回一个带有应用当前状态的状态,在本例中,是我们的计数器的状态(count)。注意,还有第二个参数,它是一个更新状态的函数(setCounter)。

一旦用户点击按钮,我们调用函数通过setCounter更新应用的状态,在那里我们可以增加Count + 1 的值。这将更新变量 count,使值增加 1。使用reflection {count},段落标签中的数据会出现在用户界面上。

src/App.tsx的完整更新代码将如下所示:

import React, { useState } from 'react'
import logo from './logo.svg'
import './App.scss'

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>You clicked {count} times</p>

        <button type="submit" onClick={() => setCount(count + 1)}>
          Click me
        </button>

        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://github.com/EliEladElrom/react-tutorials"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App

如果你还在运行yarn start命令,可以直接进入http://localhost:3000页面,你会看到变化(图 3-2);否则,使用yarn start命令。

img/503823_1_En_3_Fig2_HTML.jpg

图 3-2

用计数器状态更新 src/App.tsx

单击该按钮后,您将看到计数器变量按预期发生变化。见图 3-2 。

TypeScript 组件

目前为止一切顺利。我们能够创建 JavaScript 函数和类组件。注意,在前面的例子中,我们使用了一个.tsx文件扩展名;然而,我们并没有真正编写任何特定的 TypeScript 代码。代码仍然是好的。

在本书中,我们用 TypeScript 建立了我们的项目。使用函数和类组件类似于我们用ES6类设置 JavaScript 函数的方式;但是,除了类型检查之外,还有其他特定于语法的方法,这些方法是 TypeScript 所独有的。让我们来看看。

纯函数

这是 React 的核心。FP 是声明性的而不是命令性的,应用状态流过纯函数。因为 React 是一种声明性语言(它实际上并不直接操纵 DOM 本身),所以我们希望 React 组件是纯函数。

举个例子:

const Component = (props: IProps) =>
  render() {
    return
  }
}

它可以编译和工作,但不理想,因为它不纯。要把它变成一个纯组件,就像这样写你的纯函数:

const Component = (props: IProps) =>

有副作用的纯函数

Note

副作用是在被调用函数之外可以观察到的状态变化,而不是它的返回值。副作用的例子包括:改变外部变量或对象属性(全局变量,或父函数作用域链中的变量)的值,没有说明符的导入语句(即,import someLib),登录到控制台,获取数据,设置订阅,或手动改变 DOM。

我们的纯函数可以处理副作用(也称为效果)。为了做到这一点,我们将副作用包装在useEffect中,并确保它们不会在每次更改时都呈现出来。例如,查看下面的组件,滚动到使用浏览器 API 的顶部:

export default function ScrollToTop() {
  const { pathname } = useLocation()
   useEffect(
    () => () => {
      try {
        window.scrollTo(0, 0)
    },
    [pathname]
  )
  return null
}

TypeScript 函数组件

对于 TypeScript,我们可以指定我们的props接口的类型。我们可以使用useState功能,就像我们对纯 JavaScript 示例所做的那样。

// src/components/MyCounter/MyCounter.tsx

import React, { useState } from 'react'

export const MyCounter: React.FunctionComponent<IMyCounterProps> = (props: IMyCounterProps) => {
  const [count, setCount] = useState(0)
  return (

      <p>You clicked MyCounter {count} times</p>
      <button type="submit" onClick={() => setCount(count + 1)}>Click MyCounter</button>

  )
}
interface IMyCounterProps {
  // TODO
}

你可以从 GitHub 下载这个。

https://github.com/Apress/react-and-libraries/tree/master/03/starter-project/src/components/MyCounter/MyCounter.tsx

Tip

在 JSX,<div></div>相当于<></>

现在我们可以将这个子组件包含在我们的src/App.tsx组件中。

import {MyCounter} from './components/MyCounter/MyCounter'

function App() {

  return (
      <div classname="App">
        <header className="App-header">
        ...
        <MyCounter />
        ...
        </div>
      </div>
  )
}

这将产生图 3-3 中的结果。

img/503823_1_En_3_Fig3_HTML.jpg

图 3-3

用 MyCounter 子组件更新了 src/App.tsx

TypeScript 类组件

要创建与MyCounter中相同的子组件作为 TypeScript 类,我们不需要使用useState方法。

相反,我们可以创建一个状态接口(IMyClassCounterState),并将变量 count 设置为类型号。

然后在构造函数中,我们设置初始组件状态。我们可以将计数器设置为零。

最后,当我们需要改变组件的状态时,我们可以使用this.setState函数并更新我们的计数器。点击此处查看完整代码:

// src/components/MyCounter/MyClassCounter.tsx

import React from 'react'

export default class MyClassCounter extends React.Component<IMyClassCounterProps, IMyClassCounterState> {
  constructor(props: IMyClassCounterProps) {
    super(props)
    this.state = {
      count: 0,
    }
  }

  render() {

    return (
      <div>
        <p>You clicked MyClassCounter {this.state.count} times</p>
        <button type="submit" onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click MyClassCounter
        </button>
      </div>
    )
  }
}

interface IMyClassCounterProps {
  // TODO
}

interface IMyClassCounterState {
  count: number
}

这段代码可以从 GitHub 下载。

https://github.com/Apress/react-and-libraries/tree/master/03/starter-project/src/components/MyClassCounter/MyClassCounter.tsx

要在我们的用户界面中查看这些变化,请在src/App.tsx组件中包含我们的子组件,这样我们就可以直观地看到我们的子组件(图 3-4 )。

img/503823_1_En_3_Fig4_HTML.jpg

图 3-4

用 MyClassCounter 子组件更新了 src/App.tsx

import { MyCounter } from './components/MyCounter/MyCounter'
import MyClassCounter from './components/MyCounter/MyClassCounter'

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <header className="App-header">
        ...
        <p>You clicked {count} times</p>
        <MyCounter />
        <MyClassCounter />
        ...
      </header>
    </div>
  )
}

export default App

这个例子很好,但是这里有一个问题。运行我们在项目中设置的 linter 来检查潜在的编码错误。

$ yarn lint

当引用以前的状态时,您将得到一条错误消息error Use callback in setState

starter-project/src/components/MyCounter/MyClassCounter.tsx
 15:69 error Use callback in setState when referencing the previous state react/no-access-state-in-setstate
✖ 1 problem (1 error, 0 warnings)

在 React 中使用状态时,我们可以通过基于以前的状态更新当前状态来简化我们的代码,对于许多用例,代码将按预期编译和工作;然而,这实际上破坏了 React 架构,应该避免。

让我们修改我们的代码。

// src/components/MyCounter/MyClassCounter.tsx

  handleClick(e: React.MouseEvent) {
    this.setState(prevState => {
      const newState = prevState.count+1
      return ({
        ...prevState,
        count: newState
      });
    })
  }

  render() {
    return (

      <div>
        <p>You clicked MyClassCounter {this.state.count} times</p>
        <button type="submit" onClick={this.handleClick}>
          Click MyClassCounter
        </button>
      </div>
    )
  }
}

再次运行 Lint,我们现在没有错误了。

$ yarn lint
yarn run v1.22.10
$ eslint — ext .js,.jsx,.ts,.tsx ./
 Done in 7.31s.

阅读我的文章(shorturl.at/cmRW0)了解常见的林挺错误以及如何避免它们。

外来组件:工厂组件

我想谈的最后一类组件是工厂组件。React 允许您创建看起来像功能组件的组件;然而,功能组件使用 React 钩子来访问生命周期事件。

看一下这段代码:

function Hello(props) {
  return {
    componentDidMount() {
      alert('wow')
    }
    render() {
      return <div>Hi, {this.props.name}</div>
    }
  };
}

您可以在 React 16 中编写代码;但是,在 React 版本 17 中,如果您这样做,代码将生成一个错误消息。

Tip

在 React 17 中,对之前事件如componentDidMount的访问被否决了,你应该使用钩子特性来访问组件生命周期事件getDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidUpdate。在本章的后面你会学到更多关于这些钩子的知识。

看看 GitHub 上的问题,React 核心团队要求为使用这种类型组件的开发人员创建一个警告;参见 https://github.com/facebook/react/issues/13560 .

复杂 TS 功能组件

扩展功能组件的props接口赋予了我们的功能超能力。在这一节中,我将向您展示两个扩展 React Router 和 Material-UI 样式的接口的例子,以及创建您自己的接口扩展。

当扩展一个类时,有三种选择。

  • 遗产

  • 连接

  • 作文

继承是指一个类继承了另一个类的特性。一个接口只是这个类的签名(蓝图)。组合是指创建小功能,然后创建更大更完整的功能,并将两者结合使用。

当我们扩展接口时,最好避免继承,因为对象是接口。

一般来说,当涉及到组件继承时,React 团队提倡尽可能使用复合而不是继承;但是,这里我们做的是props继承。https://reactjs.org/docs/composition-vs-inheritance.html见。

使用这种类型的模式使我们能够重用代码以及实现纯 JavaScript 或 TypeScript 代码。对于什么是最佳实践有一些不同意见。无论您做什么,您都希望避免大量的样板代码和复杂性,以便您的代码更具可读性和可测试性。

这是一个更大的讨论,超出了本书的范围,但是如果您发现自己需要扩展接口,请注意有几种方法。

路由的延伸支柱

如果你还记得在第二章中,我们设置了项目以便能够使用generate-react-cli库生成项目模板,并且我们设置了一个名为starter-project/ templates/component/component.js的定制模板文件。

既然我们已经理解了 React 函数和类组件以及钩子,我们可以再次检查代码。

这里我们有一个类组件,它有两个接口,ITemplateNamePropsITemplateNameState。它们尚未实现。

在我们的类内部,我们调用类的构造函数,传递props,并设置应用的状态。

父组件将设置属性,我们将设置应用的状态,就像我们对useState所做的一样。因为我们扩展了React.Component,所以不需要从 React 导入那个特性;我们可以自动使用它。

此外,我们得到这些钩子特征来访问组件生命周期:getDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidUpdate

这些都与组件被安装、渲染和卸载的顺序有关,如果我们需要做一些操作,比如避免内存泄漏,这些都可以在这里完成。

看一下我们用来为generate-react-cli生成组件的模板文件。它包括接口ITemplateNameProps,它扩展RouteComponentProps以获得组件名称,以及componentDidUpdate钩子的ITemplateNameSnapshot和状态的接口(ITemplateNameState)。你可以在 React 文档中读到何时使用哪个钩子: https://reactjs.org/docs/react-component.html

import React from 'react';
import './TemplateName.scss';
import { RouteComponentProps } from 'react-router-dom'

export default class TemplateName extends React.Component<ITemplateNameProps, ITemplateNameState> {

  constructor(props: ITemplateNameProps) {
    super(props);
    this.state = {
      name: this.props.history.location.pathname.substring(
        1,
        this.props.history.location.pathname.length
      ).replace('/', '')
    }
  }

  // Read more about component lifecycle in the official docs:
  // https://reactjs.org/docs/react-component.html

  public shouldComponentUpdate(nextProps: ITemplateNameProps, nextState: ITemplateNameState) {
    // invoked before rendering when new props or state are being received.
    return true // or prevent rendering: false
  }

  static getDerivedStateFromProps:
    React.GetDerivedStateFromProps<ITemplateNameProps, ITemplateNameState> = (props:ITemplateNameProps, state: ITemplateNameState) => {
    // invoked right before calling the render method, both on the initial mount and on subsequent updates
    // return an object to update the state, or null to update nothing.
    return null
  }

  public getSnapshotBeforeUpdate(prevProps: ITemplateNameProps, prevState: ITemplateNameState) {
    // invoked right before the most recently rendered output is committed
    // A snapshot value (or null) should be returned.
    return null
  }

  componentDidUpdate(prevProps: ITemplateNameProps, prevState: ITemplateNameState, snapshot: ITemplateNameSnapshot) {
    // invoked immediately after updating occurs. This method is not called for the initial render.
    // will not be invoked if shouldComponentUpdate() returns false.
  }

  render() {

    return (
      <div className="TemplateName">
        {this.state.name} Component
      </div>)
  }
}

interface ITemplateNameProps extends RouteComponentProps<{ name: string }> {
  // TODO
}

interface ITemplateNameState {
  name: string
}

interface ITemplateNameSnapshot {
  // TODO
}

Note

componentWillMountcomponentDidUpdatecomponentWillUpdate在 React 16.9.0 ( https://reactjs.org/blog/2019/08/08/react-v16.9.0.html )中被弃用。

props接口扩展了RouteComponentProps并要求我们传递名称,当我们将这个组件包含在一个Route标签中时,名称将从父组件传递过来。你将在本章后面看到这一点。

interface ITemplateNameProps extends RouteComponentProps<{ name: string }>

然后我们可以通过钩子访问路由 API。

this.state = {
            name: this.props.history.location.pathname.substring(1, this.props.history.location.pathname.length)
        }

我的父组件需要用Router标签包装我的组件。React 路由可以将数据传递给我们的子组件。

function AppRouter() {

  return (
    <Router>
        <Switch>
          <Route exact path="/" component={App} />
        </Switch>
        <div className="footer">
        </div>
    </Router>
  )
}

扩展材质的属性-用户界面风格

如果我想创建一个使用 Material-UI 的组件,并且我想将样式保存在一个单独的类中,我可以通过用WithStyles扩展该类来赋予我的props特殊能力,并且props类和属性将对我可用。

例如,如果我想创建一个布局组件,它将为其他组件居中显示内容,我可以创建一个名为Centered.tsx的组件。

// src/layout/Centered/Centered.tsx

import * as React from 'react'
import { withStyles, WithStyles } from '@material-ui/core/styles'
import styles from './Centered.styles'

const CenteredViewInner: React.FunctionComponent<Props> = (props) => (
  <div className={props.classes.container}>{props.children}</div>
)

interface Props extends WithStyles<typeof styles> {}

export const Centered = withStyles(styles)(CenteredViewInner)

然后创建一个名为ClassName.styles.ts的样式类,供Centered.tsx访问。

import { createStyles, Theme } from '@material-ui/core/styles'

export default (theme: Theme) =>
  createStyles({
    '@global': {
      'body, html, #root': {
        paddingTop: 40,
        width: '100%',
      },
    },
    container: {
      maxWidth: '400px',
      margin: '0 auto',
    },
  })

我们将在本书的后面了解更多关于这种类型的组件。

自己扩展属性继承

为了使用继承来扩展props并创建我们自己的组件,我们可以创建一个扩展接口的基类。然后,我们的类将扩展我们的基类子类,并且可以从基类和子类中强制属性。

用一个例子更容易理解。例如,假设我想创建一个自定义按钮。我不用按钮,只是用标签给出按钮的名称。我的自定义按钮叫SpecialButton。按钮将使用标签和名称变量向用户显示按钮名称。我需要实现逻辑来处理按钮的名称,并设置一个点击事件,这样用户就可以与我的按钮进行交互。看一下下面的代码:

// src/components/SpecialButton.tsx

// eslint-disable-next-line max-classes-per-file
import React from 'react'

interface IBaseProps {
  name: string
}

// eslint-disable-next-line react/prefer-stateless-function
class Base<P> extends React.Component<P & IBaseProps, {}> {
  // TODO
}

interface IChildProps extends IBaseProps {
  label: string
  className: string
  handleClick: () => void
}

export class SpecialButton extends Base<IChildProps> {
  render(): JSX.Element {

    return (
      <div>
        <button type="submit" className={this.props.className} onClick={this.props.handleClick}>
          {this.props.label} - {this.props.name}
        </button>
      </div>
    )
  }
}

Notice

我在两个地方禁用 Lint,因为我们将 Lint 设置为每个类只有一个类。这只是为了举例说明,所以我想保持简单。对于生产代码,这应该分解成两个文件,每个文件包含一个类。

现在,为了实现我们的SpecialButton,我们的父组件需要传递类名、标签、名称和事件处理程序。看一看:

<SpecialButton
  className='specialButton'
  label='Special'
  name='Button'
  handleClick={() => setCount(count + 1)}
/>

我们可以将这段代码放在App.tsx中。由于我的事件处理程序被放在保存计数状态的App.tsx代码中,它将在我们特殊按钮的每次点击中增加状态,如图 3-5 所示。

img/503823_1_En_3_Fig5_HTML.jpg

图 3-5

src/App.tsx 更新了特殊按钮子组件

在这一节中,我们将使用 JavaScript 和 TypeScript 创建函数和类组件。我们还学习了如何扩展prop接口来实现特定的功能。

理解函数和类组件有助于我们知道何时使用什么。如果我们出于各种原因(比如清理和设置)需要完全访问 React 组件生命周期事件,那么使用类组件是明智的。当我们不需要设置完整的钩子时,功能组件应该被更多地使用。使用 TypeScript 有助于我们防止错误数据的渗入,并且在测试过程中会派上用场。

尽可能使用纯组件

最后但同样重要的是,在前面的例子中,我们在创建类组件时使用了React.Component。它让我们可以访问组件的shouldComponentUpdate。但是,请记住,有两种选择。

不需要shouldComponentUpdate的时候,用PureComponent代替比较好。

extends React.PureComponent

React.PureComponent在某些情况下提供了性能提升,但代价是失去了shouldComponentUpdate生命周期。你可以在 React 文档( https://reactjs.org/docs/react-api.html#reactpurecomponent )中了解更多。

这里有一个例子:

import React from 'react'
import './MyPage.scss'
import { RouteComponentProps } from 'react-router-dom'
import Button from '@material-ui/core/Button
export default class MyPage extends React.PureComponent<IMyPageProps, IMyPageState> {
  constructor(props: IMyPageProps) {
    super(props)
    this.state = {
      name: this.props.history.location.pathname
        .substring(1, this.props.history.location.pathname.length)
        .replace('/', ''),
      results: 0
    }
  }
  render() {
    return (
      <div className="TemplateName">
        {this.state.name} Component
      </div>)
    )
  }
}
interface IMyPageProps extends RouteComponentProps<{ name: string }> {
  // TODO
}
interface IMyPageState {
  name: string
  results: number
}

重新-重新-重新-重新渲染

也就是说,有时需要使用shouldComponentUpdate,因为我们可以使用该方法让 React 知道该组件不受父组件状态变化的影响,并且不需要重新呈现。

public shouldComponentUpdate(nextProps: IProps, nextState: IState) {
  return false // prevent rendering
}

摘要

在这一章中,我们讲述了 React 组件基础知识,并转移到更复杂的练习。我们看了 JS 和 ts 函数和类组件、外来组件、复杂的 TS 函数组件等等。

理解函数和类组件有助于我们知道何时使用什么。如果我们出于各种原因(比如清理和设置)需要完全访问 React 组件生命周期事件,那么使用类组件是明智的。当我们不需要完整的钩子设置时,功能组件应该被更多地使用。使用 TypeScript 有助于我们防止错误的数据进入,并且在测试过程中会派上用场。

在学习编写组件的方法时,您了解了如何使用 React 挂钩、避免副作用以及扩展prop接口。

在下一章,我们将学习 React 路由和 Material-UI 框架。