对我们的下一本书感兴趣吗?了解更多关于 使用 React 构建大型 JavaScript Web 应用程序

React.js 概述

React logo

多年来,对使用 JavaScript 组合用户界面的简单方法的需求不断增长。 React,也称为 React.js,是一个由 Facebook 设计的开源 JavaScript 库,用于构建用户界面或 UI 组件。

当然,React 并不是唯一可用的 UI 库。 PreactVueAngularSvelteLit 以及许多其他库也非常适合将可重用元素组合成界面。鉴于 React 的受欢迎程度,值得了解它的工作原理,因为我们将在本指南中使用它来介绍一些设计、渲染和性能模式。

当前端开发人员谈论代码时,通常是在设计网页界面的背景下。我们对界面组合的思考方式是元素,比如按钮、列表、导航等等。React 提供了一种优化和简化的方式来用这些元素表达界面。它还通过将您的界面组织成三个关键概念来帮助构建复杂和棘手的界面——组件、props状态

由于 React 以组合为中心,它可以完美地映射到您的设计系统的元素。因此,从本质上讲,为 React 设计实际上会奖励您以模块化的方式思考。它允许您在将页面或视图拼凑在一起之前设计单个组件,以便您完全了解每个组件的范围和目的——这个过程被称为组件化

我们将使用的术语

  • React / React.js / ReactJS - React 库,由 Facebook 于 2013 年创建
  • ReactDOM - 用于 DOM 和服务器渲染的包
  • JSX - JavaScript 的语法扩展
  • Redux - 集中式状态容器
  • Hooks - 一种新的方式来使用状态和其他 React 功能,而无需编写类
  • ReactNative - 使用 Javascript 开发跨平台原生应用程序的库
  • Webpack - JavaScript 模块打包器,在 React 社区中很受欢迎。
  • CRA (Create React App) - 用于创建 React 应用程序脚手架以启动项目的 CLI 工具。
  • Next.js - 一个 React 框架,具有许多最佳功能,包括 SSR、代码拆分、优化性能等。

使用 JSX 渲染

我们将在许多示例中使用 JSX。JSX 是 JavaScript 的扩展,它使用类似 XML 的语法将模板 HTML 嵌入 JS 中。它旨在转换为有效的 JavaScript,尽管这种转换的语义是特定于实现的。JSX 随着 React 库的流行而流行起来,但此后也看到了其他实现。

how JSX works

组件、Props 和状态

组件、props 和状态是 React 的三个关键概念。您在 React 中看到或做的几乎所有事情都可以归类为至少其中一个关键概念,以下是对这些关键概念的简要介绍

1. 组件

React components and composition

组件是任何 React 应用程序的构建块。它们就像 JavaScript 函数,接受任意输入(Props)并返回描述应该在屏幕上显示什么的 React 元素。

首先要理解的是,React 应用程序中屏幕上的所有内容都是组件的一部分。从本质上讲,一个 React 应用程序只是组件嵌套在组件中。因此,开发人员不会在 React 中构建页面;他们构建组件。

组件允许您将 UI 拆分为独立的可重用部分。如果您习惯于设计页面,以这种模块化的方式思考可能看起来像一个很大的改变。但是,如果您使用设计系统或样式指南?那么这可能不像看起来那样大的范式转变。

定义组件的最直接方法是编写 JavaScript 函数。

function Badge(props) {
  return <h1>Hello, my name is {props.name}</h1>;
}

此函数是一个有效的 React 组件,因为它接受一个带有数据的单个 prop(代表属性)对象参数,并返回一个 React 元素。此类组件被称为“函数组件”,因为它们实际上是 JavaScript 函数。

React welcome badge

除了函数组件之外,另一种类型的组件是“类组件”。类组件与函数组件的不同之处在于它是通过 ES6 类定义的,如下所示

class Badge extends React.Component {
  render() {
    return <h1>Hello, my name is {this.props.name}</h1>;
  }
}

提取组件

为了说明组件可以拆分为更小的组件,请考虑以下Tweet组件

Tweet component

它可以按如下方式实现

function Tweet(props) {
  return (
    <div className="Tweet">
      <div className="User">
        <Image
          className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="User-name">{props.author.name}</div>
      </div>
      <div className="Tweet-text">{props.text}</div>
      <Image
        className="Tweet-image"
        src={props.image.imageUrl}
        alt={props.image.description}
      />
      <div className="Tweet-date">{formatDate(props.date)}</div>
    </div>
  );
}

由于此组件的聚合程度,它可能有点难以操作,并且重新使用它的各个部分也会很困难。但是,我们仍然可以从中提取一些组件。

我们首先要提取的是Avatar

function Avatar(props) {
  return (
    <Image
      className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Avatar 不需要知道它是在 Comment 中渲染的。这就是我们将其 prop 命名为更通用的名称的原因:user 而不是author

现在我们将简化评论

function Tweet(props) {
  return (
    <div className="Tweet">
      <div className="User">
        <Avatar user={props.author} />
        <div className="User-name">{props.author.name}</div>
      </div>
      <div className="Tweet-text">{props.text}</div>
      <Image
        className="Tweet-image"
        src={props.image.imageUrl}
        alt={props.image.description}
      />
      <div className="Tweet-date">{formatDate(props.date)}</div>
    </div>
  );
}

接下来,我们将创建一个 User 组件,它将渲染_Avatar_ 旁边是用户的姓名

function User(props) {
  return (
    <div className="User">
      <Avatar user={props.user} />
      <div className="User-name">{props.user.name}</div>
    </div>
  );
}

现在我们将进一步简化Tweet

function Tweet(props) {
  return (
    <div className="Tweet">
      <User user={props.author} />
      <div className="Tweet-text">{props.text}</div>
      <Image
        className="Tweet-image"
        src={props.image.imageUrl}
        alt={props.image.description}
      />
      <div className="Tweet-date">{formatDate(props.date)}</div>
    </div>
  );
}

提取组件似乎是一项繁琐的任务,但拥有可重用组件使为大型应用程序编码变得更容易。考虑简化组件时,一个好的标准是:如果您的 UI 的一部分被多次使用(Button、Panel、Avatar),或者它本身足够复杂(App、FeedStory、Comment),那么它就是一个很好的候选者,可以提取到一个单独的组件中。

2. Props

Props 是属性的简称,它们只是指 React 中组件的内部数据。它们写在组件调用中,并传递给组件。它们还使用与 HTML 属性相同的语法,例如 _prop=“value”。关于 props 值得记住的两件事是:首先,我们在组件构建之前确定 prop 的值,并将其用作蓝图的一部分。其次,prop 的值永远不会改变,也就是说,一旦 prop 传递给组件,它们就是只读的。

访问 prop 的方式是通过引用每个组件都可以访问的“this.props”属性。

3. 状态

状态是一个对象,它保存一些信息,这些信息可能会在组件的生命周期内发生变化。这意味着它只是组件 Props 中存储的数据的当前快照。数据可以随着时间的推移而改变,因此管理数据更改方式的技术变得必要,以确保组件在恰当的时间以工程师想要的方式显示——这称为状态管理

几乎不可能阅读关于 React 的一段话而不遇到状态管理的概念。开发人员喜欢详细阐述这个主题,但从核心上讲,状态管理并不像听起来那么复杂。

在 React 中,状态也可以在全局范围内跟踪,并且数据可以根据需要在组件之间共享。从本质上讲,这意味着在 React 应用程序中,在新的地方加载数据不像在其他技术中那样昂贵。React 应用程序更聪明地知道保存和加载哪些数据,以及何时保存和加载数据。这为以新的方式使用数据的界面打开了机会。

将 React 组件视为具有自己的数据、逻辑和表示的微应用程序。每个组件应该有一个单一目的。作为一名工程师,您可以决定这个目的,并完全控制每个组件的行为以及使用哪些数据。您不再受页面上其他数据限制。在您的设计中,您可以以各种方式利用这一点。存在呈现附加数据的机会,这些数据可以改善用户体验或使设计中的区域更具上下文。

如何在 React 中添加状态

在设计时,包括状态是一项您应该留到最后完成的任务。尽可能以无状态的方式设计所有内容,使用 props 和事件。这使组件更容易维护、测试和理解。添加状态应该通过状态容器(如 ReduxMobX)或容器/包装器组件来完成。Redux 是其他响应式框架的流行状态管理系统。它实现了由操作驱动的集中式状态机。

在下面的示例中,状态的位置可以是LoginContainer本身。让我们为此使用 React Hooks(这将在下一节中讨论)

const LoginContainer = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const login = async (event) => {
    event.preventDefault();
    const response = await fetch("/api", {
      method: "POST",
      body: JSON.stringify({
        username,
        password,
      }),
    });
    // Here we could check response.status to login or show error
  };

  return (
    <LoginForm onSubmit={login}>
      <FormInput
        name="username"
        title="Username"
        onChange={(event) => setUsername(event.currentTarget.value)}
        value={username}
      />
      <FormPasswordInput
        name="password"
        title="Password"
        onChange={(event) => setPassword(event.currentTarget.value)}
        value={password}
      />
      <SubmitButton>Login</SubmitButton>
    </LoginForm>
  );
};

有关更多类似上述的示例,请参阅 Thinking in React 2020


Props 与状态

Props 和状态有时会因为它们之间的相似性而混淆。以下是它们之间的一些关键区别

Props状态
数据从一个组件到另一个组件保持不变。

数据是存储在组件 Props 中的数据的当前快照。它在组件的生命周期内发生变化。

数据是只读的数据可以是异步的
props 中的数据无法修改

状态中的数据可以使用this.setState修改

Props 是传递给组件的内容状态是在组件内管理的

React 中的其他概念

组件、props 和状态是您在 react 中执行所有操作的三个关键概念。但还有其他概念需要学习

1. 生命周期

每个 react 组件都经历三个阶段;装载、渲染和卸载。在这些三个阶段发生的事件序列可以称为组件的生命周期。虽然这些事件与组件的状态(其内部数据)部分相关,但生命周期有所不同。React 具有内部代码,根据需要加载和卸载组件,并且组件可以在该内部代码中存在于几个使用阶段。

有很多生命周期方法,但最常见的是

render() - 此方法是 React 中类组件中唯一必需的方法,也是使用最广泛的方法。顾名思义,它处理组件到 UI 的渲染,并且在组件的装载和渲染期间发生。

组件创建或删除时

  • componentDidMount() 在组件输出渲染到 DOM 后运行。
  • componentWillUnmount() 在组件卸载和销毁之前立即调用。

当 props 或 state 更新时。

  • shouldComponentUpdate() 在接收新的 props 或 state 时的渲染之前调用。
  • componentDidUpdate() 在更新发生后立即调用。此方法不会在初始渲染时调用。

2. 高阶组件 (HOC)

高阶组件 (HOC) 是 React 中用于重用组件逻辑的高级技术。这意味着高阶组件是一个函数,它接受一个组件并返回一个新的组件。它们是源于 React 组成性本质的模式。组件将 props 转换为 UI,而高阶组件将组件转换为另一个组件,并且它们在第三方库中很受欢迎。

3. 上下文

在典型的 React 应用程序中,数据通过 props 传递,但这对于应用程序中许多组件所需的某些类型的 props 来说可能很麻烦。上下文提供了一种在组件之间共享这些类型的数据的方法,而无需显式地将 prop 传递到每个层次结构级别。这意味着使用上下文,我们可以避免将 props 传递到中间元素。

React Hooks

Hooks 是函数,可以让你从函数式组件“挂钩”到 React 状态和生命周期功能。它们允许你使用状态和其他 React 功能,而无需编写类。你可以在我们的 Hooks 指南中了解更多有关 Hooks 的信息。

two ways of creating components

React 思维

React 的一项真正惊人的功能是它如何让你在构建应用程序时思考应用程序。在本节中,我们将引导你完成使用 React Hooks 构建可搜索的产品数据表的思考过程。

步骤 1:从模拟开始 想象一下我们已经拥有一个 JSON API 和一个界面模拟

Mock tweet Search Results

我们的 JSON API 返回一些看起来像这样的数据

[
  {
    category: "Entertainment",
    retweets: "54",
    isLocal: false,
    text: "Omg. A tweet.",
  },
  {
    category: "Entertainment",
    retweets: "100",
    isLocal: false,
    text: "Omg. Another.",
  },
  {
    category: "Technology",
    retweets: "32",
    isLocal: false,
    text: "New ECMAScript features!",
  },
  {
    category: "Technology",
    retweets: "88",
    isLocal: true,
    text: "Wow, learning React!",
  },
];

提示:你可能会发现像 Excalidraw 这样的免费工具对绘制 UI 和组件的高级模拟很有用。

步骤 2:将 UI 分解为层次结构组件

有了模拟后,下一步是围绕模拟中的每个组件(以及子组件)绘制方框,并为它们命名,如下所示。

使用单一责任原则:一个组件理想情况下应该具有单一功能。如果它最终变得越来越大,应该将其分解成更小的子组件。使用相同的技术来决定是否应该创建一个新的函数或对象。

Mock tweet Search Results colors

你将在上面的图像中看到,我们的应用程序中有五个组件。我们列出了每个组件表示的数据。

  • TweetSearchResults(橙色):整个组件的容器
  • SearchBar(蓝色):用于搜索的用户输入
  • TweetList(绿色):根据用户输入显示和过滤推文
  • TweetCategory(青绿色):显示每个类别的标题
  • TweetRow(红色):显示每条推文的行

现在已经识别了模拟中的组件,下一步就是将它们排序为层次结构。在模拟中找到的另一个组件中的组件应该在层次结构中显示为子组件。就像这样

  • TweetSearchResults
    • SearchBar
    • TweetList
      • TweetCategory
      • TweetRow

步骤 3:在 React 中实现组件 完成组件层次结构后,下一步是实现你的应用程序。在去年之前,最快的方法是构建一个版本,该版本采用你的数据模型并呈现 UI,但没有任何交互性,但自从 React Hooks 的推出以来,实现应用程序的一个更简单的方法是使用 Hooks,如下所示

i. 可过滤的推文列表

const TweetSearchResults = ({ tweets }) => {
  const [filterText, setFilterText] = useState("");
  const [inThisLocation, setInThisLocation] = useState(false);
  return (
    <div>
      <SearchBar
        filterText={filterText}
        inThisLocation={inThisLocation}
        setFilterText={setFilterText}
        setInThisLocation={setInThisLocation}
      />
      <TweetList
        tweets={tweets}
        filterText={filterText}
        inThisLocation={inThisLocation}
      />
    </div>
  );
};

ii. SearchBar

const SearchBar = ({
  filterText,
  inThisLocation,
  setFilterText,
  setInThisLocation,
}) => (
  <form>
    <input
      type="text"
      placeholder="Search..."
      value={filterText}
      onChange={(e) => setFilterText(e.target.value)}
    />
    <p>
      <label>
        <input
          type="checkbox"
          checked={inThisLocation}
          onChange={(e) => setInThisLocation(e.target.checked)}
        />{" "}
        Only show tweets in your current location
      </label>
    </p>
  </form>
);

iii. Tweet 列表(推文列表)

const TweetList = ({ tweets, filterText, inThisLocation }) => {
  const rows = [];
  let lastCategory = null;

  tweets.forEach((tweet) => {
    if (tweet.text.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {
      return;
    }
    if (inThisLocation && !tweet.isLocal) {
      return;
    }
    if (tweet.category !== lastCategory) {
      rows.push(
        <TweetCategory category={tweet.category} key={tweet.category} />
      );
    }
    rows.push(<TweetRow tweet={tweet} key={tweet.text} />);
    lastCategory = tweet.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Tweet Text</th>
          <th>Retweets</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
};

iv. Tweet 类别行

const TweetCategory = ({ category }) => (
  <tr>
    <th colSpan="2">{category}</th>
  </tr>
);

v. Tweet 行

const TweetRow = ({ tweet }) => {
  const color = tweet.isLocal ? "inherit" : "red";

  return (
    <tr>
      <td>
        <span style="">{tweet.text}</span>
      </td>
      <td>{tweet.retweets}</td>
    </tr>
  );
};

最终实现将是之前提到的层次结构中编写在一起的所有代码

  • TweetSearchResults
    • SearchBar
    • TweetList
      • TweetCategory
      • TweetRow

入门

有各种方法可以开始使用 React。

直接在网页上加载: 这是设置 React 的最简单方法。将 React JavaScript 添加到你的页面,无论是作为 npm 依赖项还是通过 CDN。

使用 create-react-app: create-react-app 是一个项目,旨在让你尽快使用 React,任何需要超出单个页面的 React 应用程序都会发现 create-react-app 很容易满足这种需求。更严肃的生产应用程序应该考虑使用 Next.js,因为它具有更强大的默认值(如代码拆分)内置。

Code Sandbox: 在不安装 create-react-app 的情况下获得 create-react-app 结构的一种简单方法是转到 https://codesandbox.io/s 并选择“React”。

Codepen: 如果你正在原型设计一个 React 组件并且喜欢使用 Codepen,那么也有一些 React 模板 可供使用,例如 这个


结论

React.js 库旨在使构建模块化、可重用用户界面组件的过程变得简单直观。当你通读我们的一些其他指南时,我们希望你发现这个简短的介绍是一个有用的高级概述。

如果你有兴趣进一步阅读有关 React 基础知识的内容,请查看

本指南如果没有 官方 React 组件和 propsReact 思维React Hooks 思维scriptverse 文档中分享的教学风格,是不可能完成的。