React关键概念——理解React组件与JSX

介绍

在上一章中,你了解了React的基本概念,它是什么,以及为什么你应该考虑使用它来构建用户界面。你还学会了如何通过Vite创建React项目,方法是运行 npm create vite@latest <your-project-name>

在这一章中,你将学习React中最重要的概念之一------组件(Components) 。组件是可复用的构建块,用于构建用户界面。此外,我们将更详细地讨论JSX代码,让你能够使用组件和JSX的概念来构建你自己的第一个基础React应用。


什么是组件?

React的一个核心概念是使用所谓的组件

组件是可复用的构建块,通过组合这些组件,可以构建出最终的用户界面。

例如,一个基本的网站可能由以下几个部分组成:

  • 一个包含导航项的侧边栏(Sidebar)
  • 一个包含用于添加和查看任务的元素的主区域(Main Section)

如果你查看这个示例页面,你可能会识别出不同的构建块(即组件)。其中一些组件甚至是重复使用的:

  • 侧边栏及其导航项
  • 主页面区域
  • 主区域中的标题和到期日期的头部
  • 用于添加任务的表单
  • 任务列表

请注意,有些组件是嵌套在其他组件中的------即组件本身也由其他组件构成。这是React和类似库的一个关键特性。

为什么是组件?

无论你查看哪个网页,都会发现它们都由类似的构建块组成。这并不是React特有的概念或思想。事实上,如果你仔细观察,HTML本身也是"以组件为单位进行思考"的。你有像 <img><header><nav> 等元素,然后通过组合这些元素来描述和构建网站内容。

然而,React采用了将网页拆分为可复用构建块的思想,因为这种方法允许开发者在小而易管理的代码块上工作。这比处理一个庞大的HTML文件(或React代码文件)更加简便且易于维护。

这就是为什么其他库------无论是前端库,如React和Angular,还是后端库和模板引擎,如EJS(嵌入式JavaScript模板)------也都采用组件的原因(虽然它们的名称可能不同,你还会看到"partials"或"includes"等常见名称)。


📢 小提示:
EJS 是一个流行的JavaScript模板引擎,特别在使用Node.js进行后端开发时非常常见。


在React中工作时,尤其需要保持代码的可管理性,并通过小而可复用的组件来进行开发。

因为React组件不仅仅是HTML代码的集合,

它们还封装了JavaScript逻辑 ,并且通常还包括CSS样式

对于复杂的用户界面,**标记(JSX)、逻辑(JavaScript)和样式(CSS)**的结合可能会迅速导致大量代码,从而使得代码的维护变得困难。

想象一下一个包含JavaScript和CSS代码的大型HTML文件,在这样的文件中工作可想而知是多么麻烦。


简单来说,在React项目中,你将与大量的组件 打交道。

你会将代码拆分成小而可管理的构建块,然后通过组合这些组件来形成整体的用户界面。

这正是React的一个关键特性。


📢 小提示:

在使用React时,你应该采用组件化的思想

但从技术上讲,它们是可选的 。理论上,你也可以仅用一个组件构建非常复杂的网页。

当然,这样做不太好玩,也不切实际,但从技术角度讲,是完全可行的

组件的结构

组件非常重要。那么,React组件到底是什么样的呢?你又该如何编写React组件呢?

这里有一个示例组件:

javascript 复制代码
import { useState } from 'react';

function SubmitButton() {
  const [isSubmitted, setIsSubmitted] = useState(false);

  function handleSubmit() {
    setIsSubmitted(true);
  };

  return (
    <button onClick={handleSubmit}>
      { isSubmitted ? 'Loading...' : 'Submit' }
    </button>
  );
};

export default SubmitButton;

通常,你会将像这样的代码片段保存在一个单独的文件中(例如,命名为 SubmitButton.jsx,并将其存储在 /components 文件夹中,该文件夹位于React项目的 /src 文件夹内),然后在需要此组件的其他组件文件中进行导入。

由于文件包含JSX代码,因此使用.jsx作为文件扩展名。如果你在Vite项目中写JSX代码,Vite要求使用.jsx扩展名,而不允许将这种代码存储在.js文件中(尽管在其他React项目设置中可能可以工作)。

接下来是一个组件,它导入了上面定义的组件,并在返回语句中使用它来输出 SubmitButton 组件:

javascript 复制代码
import SubmitButton from './submit-button.jsx';

function AuthForm() {
  return (
    <form>
      <input type="text" />
      <SubmitButton />
    </form>
  );
};

export default AuthForm;

你在这些示例中看到的导入语句是标准的JavaScript导入语句。

理论上,在基于Vite的项目中,你可以在导入语句中省略文件扩展名(在这种情况下是.jsx)。

然而,最好还是包括扩展名,因为这与标准的JavaScript保持一致。当从第三方包(例如 useStatereact 包)导入时,通常不添加文件扩展名------你只需使用包名即可。
importexport 是标准的JavaScript关键字,它们帮助将相关代码拆分到多个文件中。像变量、常量、类或函数等可以通过 exportexport default 导出,然后可以在其他文件中导入并使用。


📢 小提示:

如果你对将代码拆分到多个文件,并使用importexport的概念还不熟悉,

你可以先深入学习一些基础的JavaScript资源。

例如,MDN提供了一篇很好的文章,解释了模块的基本原理,

你可以在 这里 查看。


当然,这些示例中的组件是高度简化的,并且包含一些你尚未学习的功能(例如,useState())。

然而,将UI拆分成独立的构建块并组合它们的核心思想应该是清楚的。


在使用React时,定义组件有两种替代方式:

  • 基于类的组件(Class-based components) :通过class关键字定义的组件
  • 函数式组件(Functional components) :通过常规的JavaScript函数定义的组件

在本书的所有示例中,组件都是作为JavaScript函数构建的。作为React开发者,你需要使用这两种方法中的一种,因为React要求组件必须是函数或类。


📢 小提示:

直到2018年底,你需要使用基于类的组件来完成某些任务------
特别是使用内部状态(State)的组件

(状态会在第4章《事件与状态处理》中讲解)。

然而,2018年底引入了React Hooks 的概念,

使得你也可以在函数组件中执行所有操作和任务。

因此,虽然React仍然支持基于类的组件,但它们逐渐被淘汰,

本书将不涉及基于类的组件。


在上面的示例中,还有一些值得注意的地方:

  • 组件函数的名称使用大写字母(例如 SubmitButton
  • 在组件函数内部,可以定义其他"内部"函数(例如 handleSubmit,通常使用驼峰命名法)
  • 组件函数返回的是HTML-like代码(即JSX代码)
  • 可以在组件函数中使用诸如 useState() 之类的功能
  • 组件函数通过 export default 导出
  • 某些功能(例如 useState 或自定义组件 SubmitButton)通过 import 关键字导入

组件函数到底是什么?

在React中,组件是函数(或者类,但如上所述,类现在已经不再相关)。

函数是常规的JavaScript构造,不是React特有的概念。

这一点很重要,值得注意。React是一个JavaScript库,因此它使用JavaScript的特性(比如函数);React并不是一种全新的编程语言。

在使用React时,常规的JavaScript函数可以用来封装HTML(或者更精确地说是JSX)代码和与这些标记代码相关的JavaScript逻辑。然而,是否能够将一个函数视为React组件,取决于你在该函数中编写的代码。例如,在上面的代码片段中,handleSubmit函数也是一个常规的JavaScript函数,但它并不是一个React组件。下面的例子展示了另一个普通的JavaScript函数,它并不符合React组件的标准:

bash 复制代码
function calculate(a, b) {
  return {sum: a + b};
};

事实上,如果一个函数返回一个可渲染的值(通常是JSX代码),它就会被视为一个React组件,并且可以像HTML元素一样在JSX代码中使用。这一点非常重要。

只有当一个函数返回一个React能够渲染的内容时,它才能在JSX代码中作为React组件使用。

返回的值在技术上不一定非要是JSX代码,但在大多数情况下,它会是JSX代码。在第7章《门户和Refs》中,你将看到一个返回非JSX代码的例子。

在之前定义了SubmitButtonAuthForm这两个函数的代码片段中,它们被视为React组件,因为它们都返回了JSX代码(JSX代码是React能够渲染的代码,因此是可渲染的)。

一旦一个函数被认为是React组件,它就可以像HTML元素一样在JSX代码中使用,就像 <SubmitButton /> 被当作一个(自闭合的)HTML元素一样。


与原生JavaScript的区别

在使用原生JavaScript时,你通常是通过调用函数 来执行它们。

但是对于函数组件而言,情况有所不同。React会代表你调用这些函数,因此作为开发者,你会像使用HTML元素一样在JSX代码中使用它们。


📢 小提示:

当提到可渲染的值时,值得注意的是,最常见的返回值类型确实是JSX代码 ,即通过JSX定义的标记结构。

这应该不难理解,因为使用JSX,你可以定义内容和用户界面的HTML-like结构。


除了JSX标记之外,还有其他几个关键的值类型也可以被视为可渲染的,因此也可以由自定义组件返回(而不是JSX代码)。最明显的是,你也可以返回字符串、数字,以及包含JSX元素或字符串或数字的数组

React 如何处理所有这些组件?

如果你跟踪所有组件及其导入和导出语句,你会在React项目的主入口脚本中找到一个 root.render(...) 的指令。通常,这个主入口脚本可以在项目的 src/ 文件夹中的 main.jsx 文件中找到。

这个 render() 方法是由React库提供的(更准确地说,是由 react-dom 包提供的),它接收一段JSX代码并为你解释并执行它。

你在根入口文件(main.jsx)中找到的完整代码片段通常如下所示:

javascript 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App.jsx';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

你在新的React项目中找到的代码可能会略有不同。

例如,它可能会包含一个额外的 <StrictMode> 元素,包裹在 <App> 组件周围。
<StrictMode> 会启用额外的检查,有助于捕捉React代码中的细微错误。但它也可能导致一些令人困惑的行为和意外的错误消息,尤其是在实验或学习React时。由于本书主要关注React核心特性和关键概念,所以不会使用 <StrictMode>

严格模式会在第10章《React幕后与优化机会》中进行详细讲解。如果你现在就想了解它,你可以查看官方文档:React StrictMode。不过请注意,严格模式触发的一些效果,在你读完本书更多内容后,会更容易理解。

为了顺利跟进,建议将新创建的 main.jsx 文件整理成上面的代码片段。


createRoot() 方法

createRoot() 方法指示React创建一个新的入口点,用于将生成的用户界面注入到实际的HTML文档中,该文档将提供给网站访问者。

因此,传递给 createRoot() 的参数是一个指向DOM元素的指针,这个DOM元素可以在 index.html 中找到------这是会提供给网站访问者的单一页面。

在许多情况下,document.getElementById('root') 被用作参数。

这个内置的原生JavaScript方法会返回一个指向已经是 index.html 文档一部分的DOM元素的引用。因此,作为开发者,你必须确保在React应用脚本加载的HTML文件中存在该元素,并且它的 id 属性值与提供的值(在此示例中是 root)相符。在通过 npm create vite@latest 创建的默认React项目中,通常是这样的。你可以在项目根目录的 index.html 文件中找到 <div id="root"> 元素。


index.html 文件

这个 index.html 文件是一个相对空的文件,它仅作为React应用的外壳存在。React只需要一个入口点(通过 createRoot() 定义),这个入口点将用于将生成的用户界面附加到显示的网页上。因此,这个HTML文件和它的内容不会直接定义网页内容。相反,它只是React应用的起点,允许React接管并控制实际的用户界面。


render() 方法

一旦定义了根入口点,就可以在通过 createRoot() 创建的根对象上调用一个叫做 render() 的方法:

ini 复制代码
root.render(<App />);

这个 render() 方法告诉React应该将哪个内容(即哪个React组件)注入到该根入口点。在大多数React应用中,这个组件通常是 App。然后,React会生成适当的DOM操作指令,将通过JSX在 App 组件中定义的标记显示在实际的网页上。


根组件与嵌套组件

这个 App 组件是一个从其他文件导入的组件函数。在默认的React项目中,App 组件函数在 App.jsx 文件中定义并导出,这个文件也位于 src/ 文件夹中。

这个传递给 render() 的组件(通常是 <App />)也叫做React应用的根组件 。它是被渲染到DOM中的主组件。所有其他组件都嵌套在这个 App 组件的JSX代码中,或者更深层次的嵌套组件的JSX代码中。

你可以将所有这些组件看作是构建起一个组件树,React会评估这个树,并将其转换为实际的DOM操作指令。

📢 小提示:

正如上一章提到的,React可以在各种平台上使用。

使用 react-native 包,React可以用于构建iOS和Android的原生移动应用。
react-dom 包提供了 createRoot() 方法(因此也隐式提供了 render() 方法),专注于浏览器。

它提供了React功能与浏览器所需指令之间的"桥梁",使得UI(通过JSX和React组件描述)能够在浏览器中呈现。如果你为不同的平台进行开发,那么需要替代 ReactDOM.createRoot()render() 的方法(当然,确实存在这样的替代方案)。

无论如何,无论你是将组件函数像HTML元素一样在其他组件的JSX代码中使用,还是像HTML元素一样将其作为参数传递给 render() 方法,React都会代表你解释并执行组件函数。


当然,这并不是一个新概念。在JavaScript中,函数是一等公民 ,这意味着你可以将函数作为参数传递给其他函数。

这基本上就是这里发生的事情,只不过额外增加了使用JSX语法的 twist(这不是JavaScript的默认特性)。

React为你执行这些组件函数,并将返回的JSX代码转换为DOM指令。

更精确地说,React会遍历返回的JSX代码,并深入查看可能在JSX代码中使用的其他自定义组件,直到最终得到仅由原生内置HTML元素组成的JSX代码(技术上它并不是真的HTML,这将在本章后面讨论)。


示例

举这两个组件为例:

javascript 复制代码
function Greeting() {
  return <p>Welcome to this book!</p>;
};

function App() {
  return (
    <div>
      <h2>Hello World!</h2>
      <Greeting />
    </div>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

App 组件在其JSX代码中使用了 Greeting 组件。React将遍历整个JSX标记结构,并推导出这个最终的JSX代码:

javascript 复制代码
root.render((
  <div>
    <h2>Hello World!</h2>
    <p>Welcome to this book!</p>
  </div>
), document.getElementById('root'));

这段代码会指示React和ReactDOM执行以下DOM操作:

  1. 创建一个 <div> 元素。
  2. 在这个 <div> 内部,创建两个子元素: <h2><p>
  3. <h2> 元素的文本内容设置为 'Hello World!'。
  4. <p> 元素的文本内容设置为 'Welcome to this book!'。
  5. 将这个 <div> 元素及其子元素插入到已有的DOM元素中,该元素的ID为 'root'。

这有点简化了,但你可以把React处理组件和JSX代码的方式理解为上述描述。


📢 小提示:

React实际上并不在内部直接处理JSX代码。

它只是作为开发者使用时更加方便。

在本章后面,你将学习JSX代码是如何被转换的,以及React实际处理的代码是什么样的。

内置组件

如前面示例所示,您可以通过创建返回JSX代码的函数来创建自己的自定义组件。事实上,这正是作为React开发人员,您将经常做的事情:创建组件函数------很多组件函数。

但是,最终,如果您将所有JSX代码合并成一大段JSX代码,就像最后一个示例所示,您最终得到的将是一大段仅包含标准HTML元素的JSX代码,如<div><h2><p>等。

在使用React时,您并不会创建浏览器能够显示和处理的全新HTML元素。相反,您创建的是仅在React环境中工作的组件。在这些组件到达浏览器之前,它们已经被React评估并"翻译"成操作DOM的JavaScript指令(例如 document.append(...))。

但请记住,所有这些JSX代码并不是JavaScript语言本身的一部分。它基本上是React库提供的一种语法糖(即在代码语法上的简化),以及您用来编写React代码的项目设置。因此,像<div>这样的元素在JSX代码中也不是普通的HTML元素,因为您并没有写HTML代码。它看起来像HTML,但它是写在.jsx文件中的,这并不是HTML标记,而是这种特殊的JSX代码。记住这一点是很重要的。

因此,您在这些示例中看到的<div><h2>元素,最终也只是React组件。但它们不是您自己构建的组件,而是由React(或者更准确地说,由ReactDOM)提供的组件。

在与React一起工作时,您最终总是会使用这些原始的------这些内置的组件函数,它们随后会被转换为浏览器指令,生成和附加或删除常规DOM元素。构建自定义组件的理念是将这些元素组合在一起,以便最终得到可重用的构建模块,您可以使用这些模块来构建整个UI。但最终,这个UI是由常规HTML元素组成的。

注意: 根据您前端开发的知识水平,您可能听说过一个叫做Web组件的Web特性。这个特性背后的想法是,您确实可以使用纯JavaScript构建全新的HTML元素。

如前所述,React并没有使用这个特性;您不会用React来构建新的自定义HTML元素。

命名约定

本书中所有的组件函数名称,如SubmitButtonAuthFormGreeting,都遵循一定的命名规则。

您通常可以根据自己的意愿命名React函数------至少在定义它们的文件中。但是,常见的约定是使用PascalCase命名约定,即首字母大写,并且多个单词组合成一个单词(例如:SubmitButton而不是Submit Button),每个"子单词"都以大写字母开始。

在定义组件函数的地方,这只是命名约定,而不是硬性规则。然而,在使用组件函数的地方,也就是在JSX代码中嵌入您自己的自定义组件时,这是一项硬性规则。

您不能像这样使用您自己的自定义组件函数:

xml 复制代码
<greeting />

React要求您在JSX代码中使用大写字母开头的自定义组件名称。这条规则的存在是为了让React清楚且容易地区分自定义组件和内置组件(如<div>等)。React只需要查看首字母来判断它是内置元素还是自定义组件。

除了实际组件函数的名称外,理解文件命名约定也很重要。自定义组件通常存储在位于src/components/文件夹中的独立文件中。然而,这不是硬性规定。确切的存放位置以及文件夹名称由您决定,但它应该位于src/文件夹中的某个地方。使用components/命名文件夹是标准做法。

而对于组件函数,使用PascalCase命名是标准做法,但对于文件名并没有统一的默认命名约定。一些开发者也喜欢为文件名使用PascalCase;事实上,在本书所描述的新React项目中,App组件通常存储在名为App.jsx的文件中。不过,您也会遇到很多React项目,其中组件存储在遵循kebab-case命名约定的文件中(即全部小写,多个单词通过连字符组合成一个单词)。按照这个约定,组件函数可以存储在名为submit-button.jsx的文件中。

最终,文件命名约定由您(以及您的团队)决定。在本书中,文件名将使用PascalCase。

JSX 与 HTML 与 Vanilla JavaScript

如上所述,React项目通常包含大量的JSX代码。大多数自定义组件将返回JSX代码片段。您可以在迄今为止分享的所有示例中看到这一点,并且在您探索的几乎每个React项目中都会看到,无论您是否在浏览器中使用React,还是在其他平台(如react-native)中使用。

那么,JSX代码到底是什么?它与HTML有什么不同?它与vanilla JavaScript有什么关系?

JSX是一个不属于vanilla JavaScript的特性。然而,令人困惑的是,它也不是React库的一部分。相反,JSX是由构建工作流提供的语法糖,构建工作流是整个React项目的一部分。当您通过npm run dev启动开发Web服务器,或者通过npm run build为生产环境(即部署)构建React应用时,您启动了一个将JSX代码转回常规JavaScript指令的过程。作为开发者,您看不到这些最终的指令,但实际上,React库会接收并评估它们。

那么,JSX代码到底会被转换成什么?

在现代React项目中,它会被转换成相当复杂、不直观的代码,类似这样:

php 复制代码
function Ld() {
  return St.jsx('p', { children: 'Welcome to this book!' });
}

当然,这段代码对于开发者来说并不友好。这不是您会写的代码。相反,这是由Vite(即底层构建过程)为浏览器执行所生成的代码。

但理论上,您也可以写出这样的代码,而不是使用JSX------如果某种原因下,您不想写JSX代码。React提供了一个内置的方法,您可以使用它代替JSX:您可以使用React的createElement(...)方法。

下面是一个具体的例子,首先使用JSX:

javascript 复制代码
function Greeting() {
  return <p>Hello World!</p>;
};

而不是使用JSX,您也可以将这个组件代码写成这样:

csharp 复制代码
function Greeting() {
  return React.createElement('p', {}, 'Hello World!');
};

createElement()是React库内置的方法。它指示React创建一个<p>元素,并将'Hello World!'作为子内容(即内部嵌套内容)。然后,这个<p>元素会首先在内部创建(通过一个叫做虚拟DOM的概念,这将在本书第10章"Behind the Scenes of React and Optimization Opportunities"中讨论)。在创建所有JSX元素的元素后,虚拟DOM会被转换成实际的DOM操作指令,由浏览器执行。

注意: 之前提到过,React(在浏览器中)实际上是由两个包组成的:reactreact-dom

通过引入React.createElement(...),现在更容易解释这两个包是如何协同工作的:React在内部创建这个虚拟DOM,然后将其传递给react-dom包。这个包接着生成实际的DOM操作指令,必须执行这些指令才能更新网页,以便显示期望的用户界面。

如前所述,这将在第10章中详细讨论。

中间参数值(在示例中为{})是一个JavaScript对象,它可能包含额外的配置,用于创建元素。

这是一个中间参数变得重要的例子:

javascript 复制代码
function Advertisement() {
  return <a href="https://my-website.com">Visit my website</a>;
};

这将被转换为以下内容:

php 复制代码
function Advertisement() {
  return React.createElement(
    'a',
    { href: ' https://my-website.com ' },
    'Visit my website' 
  );
};

传递给React.createElement(...)的最后一个参数是元素的子内容------即应该放在元素的开闭标签之间的内容。对于嵌套的JSX元素,将会生成嵌套的React.createElement(...)调用:

javascript 复制代码
function Alert() {
  return (
    <div>
      <h2>This is an alert!</h2>
    </div>
  );
};

这将被转换成如下内容:

csharp 复制代码
function Alert() {
  return React.createElement(
    'div', {}, React.createElement('h2', {}, 'This is an alert!')
  );
};

无需JSX的React使用

由于所有JSX代码最终都会被转换为这些原生的JavaScript方法调用,实际上您可以在不使用JSX的情况下,使用React来构建React应用和用户界面。

如果您愿意,完全可以跳过JSX。您可以直接调用React.createElement(...),而不在组件中编写JSX代码,也不在所有期望JSX的地方使用JSX。

例如,以下两个代码片段将在浏览器中产生完全相同的用户界面:

使用JSX的代码:

javascript 复制代码
function App() {
  return (
    <p>Please visit my <a href="https://my-blog-site.com">Blog</a></p>
  );
};

上述代码最终会被转换为:

php 复制代码
function App() {
  return React.createElement(
    'p',
    {},
    [
      'Please visit my ',
      React.createElement(
        'a',
        { href: 'https://my-blog-site.com' },
        'Blog'
      )
    ]
  );
};

当然,是否选择这样做是另一个问题。正如您在这个例子中看到的,完全依赖React.createElement(...)确实更加繁琐。您最终将编写更多的代码,并且深度嵌套的元素结构可能导致代码几乎变得无法阅读。

这就是为什么通常React开发人员使用JSX的原因。它是一个很好的特性,使得使用React构建用户界面变得更加愉快。但是,理解它既不是HTML,也不是vanilla JavaScript的特性,而是一些语法糖,背后会被转换成函数调用,这是很重要的。

JSX元素像普通JavaScript值一样处理

因为JSX只是语法糖,最终会被转换成普通JavaScript值,所以有几个值得注意的概念和规则您应该了解:

  1. JSX元素最终只是普通的JavaScript值(更准确地说是函数)。
  2. 所有JavaScript值适用的规则同样适用于JSX元素。

因此,在仅期望一个值的地方(例如,在return关键字后面),您只能返回一个JSX元素。

例如,以下代码会引发错误:

javascript 复制代码
function App() {
  return (
    <p>Hello World!</p>
    <p>Let's learn React!</p>
  );
};

这段代码看起来一开始是有效的,但实际上是错误的。在这个例子中,您将返回两个值而不是一个。JavaScript中不允许这样做。

例如,以下非React代码也无效:

css 复制代码
function calculate(a, b) {
  return (
    a + b
    a - b
  );
};

您不能返回多个值。不管您怎么写,它都不行。

当然,您可以返回一个数组或对象。例如,以下代码是有效的:

css 复制代码
function calculate(a, b) {
  return [    a + b,    a - b  ];
};

这是有效的,因为您只返回了一个值:一个数组。这个数组包含多个值,就像数组通常那样。这是可以的,如果您使用JSX代码也是一样:

javascript 复制代码
function App() {
  return [
    <p>Hello World!</p>,
    <p>Let's learn React!</p>
  ];
};

这种代码是允许的,因为您返回的是一个包含两个元素的数组。在这个例子中,这两个元素是JSX元素,但正如前面提到的,JSX元素只是普通的JavaScript值。因此,您可以在任何期望值的地方使用它们。

不过,在使用JSX时,您通常不会看到这种数组方法------仅仅是因为记得通过方括号包装JSX元素可能会变得麻烦。它看起来也不像HTML,这有点违背了JSX的初衷和核心思想(它的发明是为了让开发者能够在JavaScript文件中编写HTML代码)。

相反,如果需要兄弟元素(就像这些示例中的情况),会使用一种特殊的包装组件:React片段。那是一个内置组件,允许您返回或定义兄弟JSX元素:

javascript 复制代码
function App() {
  return (
    <>
      <p>Hello World!</p>
      <p>Let's learn React!</p>
    </>
  );
};

这个特殊的<>...</>元素在大多数现代React项目中都有(例如,使用Vite创建的项目),您可以将其看作是在幕后通过数组包装您的JSX元素。或者,您也可以使用<React.Fragment>...</React.Fragment>。由于某些React项目可能不支持较短的<>...</>语法,因此此内置组件始终可用。

在所有这些示例中,JSX代码周围的括号(())是必需的,以便实现漂亮的多行格式化。技术上讲,您可以将所有JSX代码写成一行,但那样会非常难以阅读。为了像在.html文件中的常规HTML代码一样将JSX元素分布到多行,您需要这些括号;它们告诉JavaScript返回值的开始和结束位置。

由于JSX元素是普通的JavaScript值(至少在经过构建过程转换后是这样),您还可以在所有可以使用值的地方使用JSX元素。

到目前为止,这适用于所有这些return语句,但您也可以将JSX元素存储在变量中或将其作为参数传递给其他函数:

javascript 复制代码
function App() {
  const content = <p>Stored in a variable!</p>; // 这可以工作!
  return content;
};

当您深入学习一些稍微复杂的概念,比如条件渲染或重复内容时,这将变得非常重要------这些内容将在第五章"渲染列表和条件内容"中讨论。

JSX元素必须有闭合标签

与JSX元素相关的另一个重要规则是,它们必须始终具有闭合标签。因此,如果在开闭标签之间没有内容,JSX元素必须是自闭合的:

ini 复制代码
function App() {
  return <img src="some-image.png" />;
};

在常规HTML中,您不需要在标签末尾添加斜杠。相反,常规HTML支持空元素(例如,<img src="...">)。您也可以在这里添加斜杠,但这不是强制性的。

在使用JSX时,如果您的元素没有任何子内容,这些斜杠是强制性的。

超越静态内容

到目前为止,在所有这些示例中,返回的内容都是静态的。它是像 <p>Hello World!</p> 这样的内容------这当然是不会改变的内容。它始终会输出一段说"Hello World!"的段落。

但当然,大多数网站需要输出可能会变化的动态内容(例如,由于用户输入而变化)。同样,您很难找到没有任何图像的网站。

因此,作为React开发者,了解如何输出动态内容(以及"动态内容"究竟意味着什么)以及如何在React应用中显示图像是很重要的。

输出动态内容

在本书的这一部分,您还没有任何工具来使内容更加动态。确切地说,React需要使用状态概念(将在第4章"处理事件和状态"中讨论)来更改显示的内容(例如,在用户输入或其他事件发生时)。

尽管如此,由于本章是关于JSX的,值得探讨输出动态内容的语法,即使它还不完全是动态的:

javascript 复制代码
function App() {
  const userName = 'Max';
  return <p>Hi, my name is {userName}!</p>;
};

这个例子在技术上仍然产生静态输出,因为userName从未变化,但您已经可以看到输出动态内容的语法作为JSX代码的一部分。您在花括号{...}中使用一个JavaScript表达式(例如变量或常量的名称,在这里是userName)。

您可以在这些花括号中放入任何有效的JavaScript表达式。例如,您还可以调用一个函数(例如,{getMyName()})或做简单的内联计算(例如,{1 + 1})。

然而,您不能在花括号中添加复杂的语句,如循环或if语句。再次强调,标准的JavaScript规则适用。您输出一个(可能)动态的值,因此,任何能够生成单一值的内容都是允许的。然而,值得注意的是,某些值类型不能用于在JSX中输出。例如,尝试在JSX中输出一个JavaScript对象会导致错误。

还值得注意的是,您不仅仅局限于在元素标签之间输出动态内容。相反,您还可以为属性设置动态值:

ini 复制代码
function App() {
  const userName = 'Max';
  return <input type="text" value={userName} />;
};

渲染图像

大多数网站不仅仅显示纯文本。相反,您通常还需要渲染图像。

当然,在使用React时,您可以像在其他Web项目中一样使用默认的<img />元素。但是,在React项目中显示图像时,有两个重要的事情需要记住:

  • <img />必须是自闭合标签。
  • 当显示存储在src/文件夹中的本地图像时,您必须将它们导入到您的.jsx文件中。

如上所述,在JSX元素必须有闭合标签的部分中,您不能有空的JSX元素,即没有任何闭合标签的元素。

此外,在输出本地存储的图像(即存储在项目的src/文件夹中的图像,而不是某个远程服务器上的图像)时,您通常不会在代码中直接设置图像的字符串路径。

您可能习惯这样输出图像:

ini 复制代码
<img src="assets/images/wave.jpg">

但是,React项目(例如,通过Vite创建的项目)确实涉及某种构建过程。在大多数项目中,最终部署到服务器的项目结构与您在开发过程中使用的项目结构将有很大不同。

因此,如果您将图像存储在Vite基础的React项目中的src/assets文件夹中,并且使用它作为路径(<img src="src/assets/my-image.jpg" />),图像将无法在部署的网站上加载。它不会加载,因为部署的文件夹结构将不再包含src/assets文件夹。

事实上,您可以通过运行npm run build来查看生产环境准备好的文件夹结构。这将构建项目并为部署生成一个新的dist文件夹。将要部署到某个服务器的就是dist文件夹中的内容。如果您检查该文件夹,您将找不到src文件夹。

换句话说:您无法预先确定本地存储图像的确切路径。这就是为什么您应该将图像文件导入到.jsx文件中的原因。因此,您将获得一个包含实际路径的字符串值(该路径在生产环境中有效)。然后,您可以将该值作为动态值设置为<img />元素的src属性:

javascript 复制代码
import myImage from './assets/my-image.png';
function App() {
  return <img src={myImage} />;
};

一开始这看起来可能有点奇怪,但这段代码在几乎所有的React项目中都会工作。在幕后,这个导入语句会被底层的构建过程分析。然后,导入语句会被移除,图像路径将被硬编码到生产环境准备好的输出代码中(即存储在dist文件夹中的代码)。

然而,有一个重要的例外:如果您将图像文件(或者,实际上,任何资源文件)存储在项目的public/文件夹中,您可以直接引用它的路径。

例如,存储在public/images/demo.jpg中的demo.jpg图像文件可以这样渲染和显示:

ini 复制代码
function App() {
  return <img src="/images/demo.jpg" />;
};

之所以可以这样做,是因为public/文件夹的内容会直接复制到dist/文件夹中。与src/文件夹及其嵌套文件不同,public/文件夹中的文件跳过了转译步骤。

请注意,public文件夹的文件夹名称本身并不包括在引用的路径中------它是src="/images/demo.jpg",而不是src="/public/images/demo.jpg"

那么,您应该使用哪种方法?将图像存储在src/文件夹还是public/文件夹?

对于大多数图像,src/是一个合理的选择,因为预处理步骤会为每个导入的文件分配一个唯一的文件名。因此,一旦应用程序部署,文件可以更有效地进行缓存。

任何在根index.html文件中导入的文件,或文件名必须永不更改的文件(例如,因为它也被某个其他应用程序引用,运行在某个其他服务器上),通常应放入public/文件夹。

因此,在大多数情况下,当输出存储在本地项目中的图像时,您应该将它们存储在src/文件夹中,并将其导入到您的JSX文件中。当使用存储在远程服务器上的图像时,您应该使用完整的图像URL:

ini 复制代码
function App() {
  return <img src="https://some-server.com/my-image.jpg" />;
};

何时应该拆分组件?

随着您在React中工作的深入,了解React的更多内容,并开始涉足更具挑战性的React项目,您很可能会遇到一个非常常见的问题:我应该什么时候将一个React组件拆分成多个独立的组件?

如本章前面所提到的,React完全基于组件,因此,在一个React项目中拥有几十、几百甚至几千个React组件是非常常见的。

当涉及将一个React组件拆分成多个较小的组件时,并没有硬性规则要求您必须遵循。如前所述,您可以将所有的UI代码放入一个大的单一组件中。或者,您可以为UI中每一个HTML元素和内容单独创建一个自定义组件。这两种方法可能都不太好。相反,一个好的经验法则是,为每一个可以识别的数据实体创建一个独立的React组件。

例如,如果您正在输出一个"待办事项"列表,您可以识别出两个主要的实体:单个的待办事项项和整个列表。在这种情况下,将其拆分为两个单独的组件可能更合适,而不是写一个更大的组件。

将代码拆分成多个组件的优点是,单个组件保持可管理性,因为每个组件和组件文件的代码较少。

然而,当涉及将组件拆分成多个组件时,会出现一个新的问题:如何使您的组件可重用并可配置?

javascript 复制代码
import Todo from './todo.jsx';
function TodoList() {
  return (
    <ul>
      <Todo />
      <Todo />
    </ul>
  );
};

在这个例子中,所有的"待办事项"都是相同的,因为我们使用了相同的<Todo />组件,它无法配置。您可能希望通过添加自定义属性(例如<Todo text="Learn React!" />)或通过在开闭标签之间传递内容(例如<Todo>Learn React!</Todo>)来使它变得可配置。

当然,React支持这样做。在下一章中,您将学习一个关键概念,叫做props,它允许您像这样使组件变得可配置。

总结与关键要点

  • React强调组件:可重用的构建块,通过组合这些组件来定义最终的用户界面。
  • 组件必须返回可渲染的内容------通常是JSX代码,定义最终应生成的HTML代码。
  • React提供了许多内置组件:除了像<>...</>这样的特殊组件,您还可以使用所有标准HTML元素的组件。
  • 为了让React区分自定义组件和内置组件,自定义组件名称在JSX代码中使用时必须以大写字母开头(通常使用PascalCase命名法)。
  • JSX既不是HTML,也不是标准的JavaScript特性------它是构建工作流提供的语法糖,构建工作流是所有React项目的一部分。
  • 您可以用React.createElement(...)方法替代JSX代码,但由于这会导致代码可读性大幅下降,因此通常避免这样做。
  • 使用JSX元素时,在期望单一值的地方(例如,return关键字后面)不能有兄弟元素。
  • 如果开闭标签之间没有内容,JSX元素必须始终是自闭合的。
  • 动态内容可以通过花括号输出(例如,<p>{someText}</p>)。
  • 图像可以通过引用它们的路径来渲染(如果存储在远程或public/文件夹中),或者通过将图像文件导入到JSX文件中并使用动态内容语法输出它们。
  • 在大多数React项目中,您将UI代码拆分成几十个或几百个组件,然后导出和导入这些组件,以便再次组合它们。
相关推荐
三原2 小时前
2025 乾坤(qiankun)和 Vue3 最佳实践(提供模版)
vue.js·架构·前端框架
小矮马2 小时前
React-组件和props
前端·javascript·react.js
懒羊羊我小弟2 小时前
React Router v7 从入门到精通指南
前端·react.js·前端框架
gaog2zh2 小时前
0803分页_加载更多-网络ajax请求2-react-仿低代码平台项目
react.js·ajax·分页·加载更多
Mars狐狸3 小时前
AI项目改用服务端组件实现对话?包体积减小50%!
前端·react.js
吃面必吃蒜4 小时前
从 Vue 到 React:React 合成事件
javascript·vue.js·react.js
举个栗子dhy4 小时前
【血缘关系图下钻节点,节点展开收起功能,递归和迭代问题处理】
javascript·react.js
Aiolimp5 小时前
React中CSS使用方法
前端·react.js
Moment5 小时前
受控组件和非受控组件的区别?别再傻傻分不清了 😁😁😁
前端·javascript·react.js
boxser5 小时前
前端实现高效的国际化解决方案
前端·javascript·前端框架