Skip to content

Latest commit

 

History

History
784 lines (550 loc) · 28.7 KB

File metadata and controls

784 lines (550 loc) · 28.7 KB

五、React 和路由

“我们已经扩展了功能列表。”

你忍住呻吟然后等待。

“我们想为用户提供一切。他们需要的一切,他们想要的一切,他们可能想要的一切。”

“好吧,”你说。“但这是一个原型……”

“分析页面、个人资料页面、朋友分析页面、笔记页面、天气页面。”

你静静地展示自己,屏住呼吸重复,“这是一个原型。”

计划

现在,我们的应用在技术上可以正常工作(允许用户登录),但缺少真正有用的内容。是时候改变了。

然而,要做到这一点,我们需要在应用中添加额外的页面。你们中的一些人可能听说过术语单页应用SPA),它用于指 React 应用,因此可能会被更多页面的说法所迷惑。我们将在进一步讨论时讨论这一区别,然后使用 React Router 进入实际的路由设置。

以下是我们将学到的:

  • 如何安装和使用 React 路由 v4
  • 如何将其他管线添加到其他构件
  • 如何在路线之间移动

一页又一页

幸运的是,理智的头脑占了上风,首席产品设计师(该公司目前雇用的五位设计师中排名最高的)表示,他们只需要三个视图即可创建原型:登录视图(完成!)、主聊天视图和用户配置文件视图。

不过,我们显然需要一种健壮且可扩展的方式在应用的不同屏幕之间移动。我们需要一个可靠的布线解决方案。

传统上,路由一直是提供哪些 HTML/CSS/JavaScript 文件的问题。您点击static-site.com上的 URL 获取主index.html,然后进入static-site.com/resources获取resources.html

在这个模型中,服务器获取特定 URL 的请求并返回相应的文件。

然而,路由越来越多地转移到客户端。在一个反应的世界里,我们只上过我们的index.htmlbundle.js。我们的 JavaScript 从浏览器接收 URL,然后决定呈现什么 JSX。

因此出现了单页应用这个术语——从技术上讲,我们的用户只坐在一个页面上(如果我们从传统模式来看)。但是,它们能够在其他视图之间导航,并且以更加精简的方式进行导航,而无需从服务器请求更多文件。

我们的顶级容器组件(App.js)将始终被渲染,但更改的是它内部的渲染内容。

差异

对于某些 React 路由解决方案,模型将如下所示。

我们将呈现初始屏幕,如图所示:

<App>
  <LoginContainer />
</App>

这将与chatastrophe.com/login的 URL 相匹配。当用户完成登录后,我们会将其发送到chatastrophe.com/chat。在这一点上,我们将调用ReactDOM.render并提供以下信息:

<App>
  <ChatContainer />
</App>

React 的对账引擎会将旧应用与新应用进行比较,并调出发生更改的组件;在这种情况下,它会将LoginContainer交换为ChatContainer,而不会重新提交App

下面是一个非常简单的示例,使用一个名为page.js的基本路由解决方案,它可能看起来像什么:

page(/, () => {
  ReactDOM.render(
    <App>
      <ChatContainer />
    </App>.
    document.getElementById('root')
  );
});

page(/login’, () => {
 ReactDOM.render(
   <App>
    <LoginContainer />
   </App>.
   document.getElementById('root')
  );
});

这个解决方案很好用。我们能够在多个视图之间导航,React 的协调确保不会对未更改的组件进行浪费性的重新招标。

然而,这种解决方案不是很理想。每次更改页面时,我们都会将整个应用传递给ReactDOM.render,这会导致router.js文件中出现大量重复代码。我们正在定义应用的多个版本,而不是精确地选择应该在哪个时间呈现哪些组件。

换句话说,该解决方案采用了一种整体的路由方法,而不是按组件划分的方法。

输入React Router v4,这是对库的完全重写,过去是一种更传统的路由解决方案。不同之处在于,路由现在是基于 URL 呈现的组件。

让我们通过重写前面的示例来讨论这到底意味着什么:

ReactDOM.render(
  <Router>
    <App>
      <Route path="/" component={ChatContainer} />
      <Route path="/login" component={LoginContainer} />
    </App>
  </Router>,
  document.getElementById('root')
);

现在,我们只打一次电话。我们呈现我们的应用,并在其中呈现包裹两个容器的两个Route组件。

每个Route都有一个path道具。如果浏览器中的 URL 与该path匹配,Route将呈现其子组件(容器);否则,它将不提供任何结果。

我们从不试图重新播放我们的App。它应该是静止的。此外,我们的路由解决方案不再与router.js文件中的组件分离。现在,它存在于我们的组件中。

我们还可以在组件中进一步嵌套路由。在LoginContainer内部,我们可以添加两个路由——一个用于/login,另一个用于/login/new——如果我们想拥有单独的登录和注册视图。

在这个模型中,每个组件都可以根据当前 URL 决定要呈现什么。

老实说,这种方法习惯起来有点奇怪,我开始使用时一点也不喜欢它。对于有经验的开发人员来说,这需要以不同的方式考虑路由,而不是自上而下、整页地决定要呈现什么,现在鼓励您在组件级别做出决策,这可能很困难。

然而,在使用它一段时间之后,我认为这个范例正是 React 路由方法所需要的,它将给开发人员带来更大的灵活性。

好了,说够了。让我们创建第二个视图——聊天屏幕——在这里,用户可以同时查看并向世界上的每个人发送消息(“全球互联”,你知道)。首先,我们将创建一个基本组件,然后开始使用路由解决方案。

我们的聊天室

创建组件现在应该是老生常谈了。我们的ChatContainer将是一个基于类的组件,因为我们需要深入研究一些生命周期方法(稍后将详细介绍)。

在我们的components文件夹中,创建一个名为ChatContainer.js的文件。然后,设置我们的骨架:

import React, { Component } from 'react';

export default class ChatContainer extends Component {
  render() {
    return (

   );
  }
}

让我们继续我们的模式,将组件包装成一个div和一个id的组件名称:

import React, { Component } from 'react';

export default class ChatContainer extends Component {
  render() {
    return (
      <div id="ChatContainer">
      </div>
    );
  }
}

正如在我们的LoginContainer顶部一样,我们希望为用户呈现我们美丽的徽标和标题。如果我们有某种可重用的组件,这样我们就不必重写代码了:

import React, { Component } from 'react';
import Header from './Header';

export default class ChatContainer extends Component {
  render() {
    return (
      <div id="ChatContainer">
        <Header />
      </div>
    );
  }
}

这真漂亮。好的,让我们在Header之后添加<h1>Hello from ChatContainer</h1>并继续进行路由,这样我们就可以在工作时看到我们在做什么。现在,我们的ChatContainer不可见。要改变这一点,我们需要设置 React 路由。

安装 React 路由

让我们从基础开始。从项目根目录在终端中运行以下命令。

yarn add react-router-dom@4.2.2

react-router-dom包含我们希望通过应用路由用户的所有 React 组件。您可以在查看完整的文档 https://reacttraining.com/react-router 。然而,我们唯一感兴趣的组件是RouteBrowserRouter

It's important to ensure that you install react-router-dom and not react-router. Since version 4 was released, the package has been split into various branches. React-router-dom is specifically geared towards providing routing components, which is what we’re interested in. Note that it installed react-router as a peer dependency, though.

Route组件比较简单;它需要一个名为path的道具,它是一个字符串,如//login。当浏览器中的 URL 与该字符串匹配时(http://chatastrophe.com/loginRoute组件渲染组件,通过component道具传入;否则,它将不呈现任何内容。

与 web 开发中的任何内容一样,如何使用Route组件还有很多额外的复杂性。稍后我们将进一步深入探讨。但是,目前我们只希望根据路径是/还是/login有条件地呈现ChatContainerLoginContainer

BrowserRouter更为复杂,但出于我们的目的,它将更易于使用。本质上,它确保我们的Route组件与 URL 保持同步(呈现或不呈现)。它使用 HTML5 历史 API 来实现这一点。

我们的浏览器路由

我们需要做的第一件事是将整个应用包装在BrowserRouter组件中,然后我们可以添加Route组件。

因为我们想让路由围绕整个应用,所以最容易添加路由的地方是我们的src/index.js。在顶部,我们需要以下组件:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './components/App';

然后,我们将我们的App呈现为BrowserRouter的孩子:

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

您还应该在我们的热重新加载配置中执行相同的操作:

if (module.hot) {
  module.hot.accept('./components/App', () => {
    const NextApp = require('./components/App').default;
    ReactDOM.render(
      <BrowserRouter>
 <App />
 </BrowserRouter>,
      document.getElementById('root')
    );
  });
}

完成!现在我们可以开始添加路线了。

我们的前两条路线

在我们的App组件中,我们目前呈现LoginContainer,无论:

render() {
  return (
    <div id="container">
      <LoginContainer />
    </div>
  );
}

我们想改变这个逻辑,这样我们要么只渲染LoginContainer要么渲染ChatContainer。要做到这一点,让我们在我们的ChatContainer中要求它。

我们还需要react-router-dom提供的Route组件:

import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import LoginContainer from './LoginContainer';
import ChatContainer from './ChatContainer';
import './app.css';

I put the Route import above the two Container imports. Best practices say you should put absolute imports (imports from node_modules) before relative imports (files imported from within src). This keeps things clean.

现在,我们可以用带有component道具的Route组件替换容器:

render() {
  return (
    <div id="container">
      <Route component={LoginContainer} />
      <Route component={ChatContainer} />
    </div>
  );
}

We pass in our component prop as LoginContainer, and not as <LoginContainer />.

我们的应用重新加载,我们看到…一团混乱:

我们当前正在同时渲染两个容器!哎呀。问题是我们没有给Route一个path道具,告诉他们何时渲染(何时不渲染)。我们现在就开始吧。

我们的第一个Route``LoginContainer将在/login路线上进行渲染,因此我们添加了如下路径:

<Route path="/login" component={LoginContainer} />

我们的另一个容器ChatContainer将在用户位于根目录时显示,/(当前位于localhost:8080/https://chatastrophe-77bac.firebaseapp.com/ 用于我们部署的应用),因此我们添加了一个路径,如图所示:

<Route path="/" component={ChatContainer} />

保存并检查应用,您将获得以下信息:

美好的我们的LoginContainer不再呈现。让我们前往/login并确保在那里只看到我们的LoginContainer

啊!

我们正在/login处渲染两个容器。怎么搞的?

长话短说,React 路由使用RegEx模式匹配路由并确定渲染内容。我们的当前路径(/login与传递给我们的登录名Route的道具匹配,但技术上也与/匹配。事实上,所有内容都与/匹配,如果您希望在每个页面上呈现组件,这非常好,但我们希望我们的ChatContainer仅在路径为/时呈现(没有其他内容)。

换句话说,当路径与/完全匹配时,我们希望呈现ChatContainer路由。

好消息是 React 路由已经为这个问题做好了准备;只需在我们的Route中添加一个道具exact

<Route exact path="/" component={ChatContainer} />

The preceding is the same as writing:

<Route exact={true} path="/" component={ChatContainer} />

当我们检查/login时,我们应该只看到我们的LoginContainer。伟大的我们有前两条路线。

接下来我们要做的是添加一点强制路由;当用户登录时,我们希望将他们重定向到主聊天屏幕。让我们做吧!

登录时重定向

在这里,事情会变得有点棘手。首先,我们要做一些准备。

在我们的LoginContainer内部,当涉及到我们的signuplogin方法时,我们目前只是console.logthen声明中给出结果。换句话说,一旦用户登录,我们实际上什么也不做:

signup() {
  firebase.auth().createUserWithEmailAndPassword(this.state.email, this.state.password)
    .then(res => {
      console.log(res);
    }).catch(error => {
      console.log(error);
      this.setState({ error: 'Error signing up.' });
    })
}

让我们更改此位(在signuplogin中)以调用另一个方法onLogin

login() {
  firebase.auth().signInWithEmailAndPassword(this.state.email, this.state.password)
    .then(res => {
      this.onLogin();
    }).catch((error) => {
      if (error.code === 'auth/user-not-found') {
        this.signup();
      } else {
        this.setState({ error: 'Error logging in.' });
      }
    });
}

然后,我们可以定义我们的onLogin方法:

onLogin() {
  // redirect to '/'
}

那么,我们如何重定向到根路径?

我们知道我们的Route组件将基于浏览器中的 URL 呈现。我们可以确信,如果我们正确修改 URL,我们的应用将重新加载以显示适当的组件。诀窍是从LoginContainer中修改该 URL。

正如我们前面提到的,React 路由使用 HTML5 历史 API 在 URL 之间移动。在这个模型中,有一个名为history的对象,它具有某些方法,允许您将新 URL 推送到应用的当前状态。

所以,如果我们在/login,想去/

history.pushState(null, null, /)

React Router 允许我们以一种更加简化的方式与 HTML5 历史对象交互(例如,避免空参数)。这样做的方式很简单:传递给Route(通过component道具)的每个组件都会收到另一个名为history的道具,该道具有一个名为push的方法。

如果这听起来让人困惑,别担心,一会儿就会明白的。我们所要做的就是:

onLogin() {
  this.props.history.push(/);
}

通过进入/login并登录来尝试。您将被重定向到ChatContainer。魔术

当调用push时,history道具正在更新浏览器的 URL,这会导致我们的Route组件呈现它们的组件(或不呈现任何内容):

History.push -> URL change -> Re-render

请注意,这是一种革命性的网站导航方式。以前,情况大不相同:

Click link/submit form -> URL change -> Download new page

欢迎来到路由单页应用的世界。感觉很好,不是吗?

注销

好的,我们已经处理了用户的登录,但是他们什么时候想注销呢?

让我们在ChatContainer顶部为他们构建一个按钮,让他们可以注销。它最适合Header组件,所以我们为什么不在那里构建它呢?

等等。我们目前在/login路径的LoginContainer中使用Header。如果我们添加一个Logout按钮,它也会出现在登录屏幕上,这让人很困惑。我们需要一种只渲染ChatContainer上的Logout按钮的方法。

我们可以利用Route history属性并使用它根据 URL 对注销按钮进行有条件的呈现(如果路径为“/”,则呈现按钮,否则不呈现!)。然而,随着我们添加更多的路由,这对未来的开发人员来说可能是混乱和难以理解的。当我们想要注销按钮出现时,让我们把它变得非常明确。

换句话说,我们希望在Header内呈现注销按钮,但仅当HeaderChatContainer内时。这有意义吗?

这样做的方法是和孩子们一起做。如果从 HTML 的角度来看,儿童实际上非常容易理解:

<div>
  <h1>I am the child of div</h1>
</div>

h1div的孩子。如果是 React 组件,Parent组件将收到一个名为children的道具,它等于h1标签:

<Parent>
  <h1>I am the child of Parent</h1>
</Parent>

为了在Parent中呈现,我们只需这样做:

<div id=”Parent”>
  {this.props.children}
</div>

让我们看看它的实际效果,希望它会更有意义(并让您了解它的威力)。

ChatContainer内,让我们将<Header />标签替换为一个开始和结束标签:

<Header>
</Header>

在其中,我们将定义我们的按钮:

<Header>
  <button className="red">Logout</button>
</Header>

查看我们的页面,我们发现没有任何变化。这是因为我们没有告诉Header实际渲染其children。让我们跳到Header.js并改变它。

在我们的h1下方,添加以下内容:

import React from 'react';

const Header = (props) => {
  return (
    <div id="Header">
      <img src="/img/icon.png" alt="logo" />
      <h1>Chatastrophe</h1>
      {props.children}
    </div>
  );
};

export default Header;

我们在这里干什么?首先,我们将props定义为功能组件的参数:

const Header = (props) => {

所有功能组件都将props对象作为其第一个参数接收。

然后,在该对象中,我们访问children属性,该属性等于我们的按钮。现在,我们的Logout按钮应该出现:

令人惊叹的如果您检查/login路径,您会注意到我们的按钮没有出现。那是因为在LoginContainerHeader没有children,所以没有任何渲染。

Children make React components super composable and extra reuseable.

好吧,让我们的按钮真正起作用。我们想调用一个名为firebase.auth().signOut的方法。让我们为调用此函数的按钮创建一个单击处理程序:

export default class ChatContainer extends Component {
  handleLogout = () => {
    firebase.auth().signOut();
  };

  render() {
    return (
      <div id="ChatContainer">
        <Header>
          <button className="red" onClick={this.handleLogout}>
            Logout
          </button>
        </Header>
        <h1>Hello from ChatContainer</h1>
      </div>
    );
  }
}

现在,当我们按下按钮时,什么也没有发生,但我们正在注销。我们缺少登录拼图的最后一块。

当用户注销时,我们希望将其重定向到登录屏幕。如果我们能知道 Firebase 授权的状态就好了:

这太完美了。单击注销按钮后,当我们的用户注销时,Firebase 将使用用户的空参数调用firebase.auth().onAuthStateChanged。 换句话说,我们已经拥有了我们所需要的一切;我们只需要在if语句中添加一个else,以涵盖找不到用户的情况。

流程将如下所示:

  1. 当用户单击注销按钮时,Firebase 会将其注销。
  2. 然后,它将使用空参数调用onAuthStateChanged方法。
  3. 如果使用空用户调用onAuthStateChanged,我们将使用history属性将用户重定向到登录页面。

让我们跳到App.js来实现这一点。

我们的App不是Route的孩子,因此它无法访问LoginContainer中使用的history道具,但我们可以使用一些变通方法。

App.js顶部,将以下内容添加到我们的react-router-dom导入中:

import { Route, withRouter } from 'react-router-dom';

然后,在底部,将我们的export default语句替换为:

export default withRouter(App);

这里发生了什么事?本质上,withRouter是一个函数,它将一个组件作为一个参数,并按原样返回该组件,只是现在它可以访问history属性。我们将继续讨论更多内容,但让我们先完成注销流程。

最后,我们可以填写componentDidMount

componentDidMount() {
  firebase.auth().onAuthStateChanged((user) => {
    if (user) {
      this.setState({ user });
    } else {
      this.props.history.push('/login')
    }
  });
}

尝试再次登录并点击注销按钮。您应该直接进入登录屏幕。魔术

绕道-高阶组件

在前面的代码中,我们使用了withRouter函数(从react-router-dom导入)为App组件提供对history道具的访问权限。让我们花一点时间讨论这是如何运作的,因为它是你能学到的最有力的反应模式之一。

withRouter高阶分量HOC的一个示例。这个略显浮夸的名字比我最喜欢的解释要好:函数构建函数(感谢Tom Coleman的这个解释)。让我们看一个例子。

假设您有一个Button组件,如下所示:

const Button = (props) => {
  return (
    <button style={props.style}>{props.text}</button>
  );
};

另外,假设有一种情况,我们希望它有白色文本和红色背景:

<Button style={{ backgroundColor: 'red', color: 'white' }} text="I am red!" />

随着应用的发展,你会发现这个按钮经常使用这种特殊的样式。你需要很多红色按钮,上面有不同的文字,每次都要输入backgroundColor让人厌烦。

不仅如此;您还有另一个组件,即警报框,具有相同的样式:

<AlertBox style={{ backgroundColor: 'red', color: 'white' }} warning="ALERT!" />

在这里,你有两个选择。您需要两个新组件(RedAlertBoxRedButton,可以在任何地方使用。您可以如图所示定义它们:

const RedButton = (props) => {
  return (
    <Button style={{ backgroundColor: 'red', color: 'white' }} text={props.text} />
  );
};

以及:

const RedAlertBox = (props) => {
  return (
    <AlertBox style={{ backgroundColor: 'red', color: 'white' }} warning={props.text} />
  );
};

然而,有一种更简单、更可组合的方法,那就是生成一个更高阶的组件。

我们想要实现的是一种方法,采取一个组成部分,并给它的红色对白色的造型。就这样。我们希望将这些道具注入到任何给定的组件中。

让我们看看最终结果,然后看看我们的 HOC 将是什么样子。如果我们成功创建了一个名为makeRed的 HOC,我们可以使用它来创建我们的RedButtonRedAlertBox

// RedButton.js
import Button from './Button'
import makeRed from './makeRed'

export default makeRed(Button)
// RedAlertBox.js
import AlertBox from './AlertBox'
import makeRed from './makeRed'

export default makeRed(AlertBox)

这更容易,也更易于重用。我们现在可以重用makeRed将任何组件转换成漂亮的红色背景和白色文本。这就是力量。

好的,那么我们如何创建一个makeRed函数呢?我们希望将组件作为参数,并返回该组件及其所有指定的道具和正确的样式道具:

import React from 'react';

const makeRed = (Component) => {
  const wrappedComponent = (props) => {
    return (
      <Component style={{ backgroundColor: 'red', color: 'white' }} {...props} />
    );
  };
  return wrappedComponent;
}

export default makeRed;

以下是相同的代码,带有注释:

import React from 'react';

// We receive a component constructor as an argument
const makeRed = (Component) => {
  // We make a new component constructor that takes props, just as any component
  const wrappedComponent = (props) => {
    // This new component returns the original component, but with the style applied
    return (
      // But we also use the ES6 spread operator to apply the regular props passed in.
      // The spread operator applies props like the text in <RedButton text="hello" /> 
       to our new component
      // It will "spread" any and all props across our component
      <Component style={{ backgroundColor: 'red', color: 'white' }} {...props} />
    );
  };
  // We return the new constructor, so it can be called as <RedButton /> or <RedAlertBox />
  return wrappedComponent;
}

export default makeRed;

最令人困惑的可能是{...props}的扩展运算符。spread 操作符是一个有用但容易混淆的 ES6 工具。它允许您获取一个对象(此处为props对象),并将其所有键和值应用于一个新对象(组件):

const obj1 = { 1: 'one', 2: 'two' };
const obj2 = { 3: 'three', ...obj1 };
console.log(obj2);
// { 1: 'one', 2: 'two', 3: 'three' }

高阶组件是下一个级别的工具,可使您的 React 组件更易于重用。我们只触及了他们的表面。欲了解更多信息,请查阅Tom Coleman撰写的理解高阶组件,网址为https://medium.freecodecamp.org/understanding-higher-order-components-6ce359d761b

我们的第三条路线

正如本章开头所讨论的,Chatastrophe 团队已着手创建用户概要视图。让我们为它做骨架和基本布线。

src/components中,创建一个名为UserContainer.js的新文件。在内部,执行基本组件骨架:

import React, { Component } from 'react';
import Header from './Header';

export default class UserContainer extends Component {
  render() {
    return (
      <div id="UserContainer">
        <Header />
        <h1>Hello from UserContainer</h1>
      </div>
    );
  }
}

回到App.js,让我们导入我们的新容器并添加Route组件:

import UserContainer from './UserContainer';

// Inside render, underneath ChatContainer Route
<Route path="/users" component={UserContainer} />

等等前面的代码在/users为我们的UserContainer创建了一条路由,但我们并没有一个用户视图。我们的应用的每个用户都有一个用户视图。我们需要在chatastrophe.com/users/1为用户 1 和chatastrophe.com/users/2为用户 2 设置一条路由,以此类推。

我们需要某种方法将变量值传递给我们的path道具,等于用户id。幸运的是,这样做很容易:

<Route path="/users/:id" component={UserContainer} />

最好的部分是什么?现在,在我们的UserContainer中,我们将接收一个props.params.match对象,等于{ id: 1 }id是什么,然后我们可以使用它来获取该用户的消息。

让我们通过更改UserContainer.js中的h1来测试这一点:

<h1>Hello from UserContainer for User {this.props.match.params.id}</h1>

然后,前往localhost:8080/users/1

如果在嵌套路由中查找bundle.js时遇到问题,请确保webpack.config.js中的输出如下所示:

output: {
  path: __dirname + "/public",
  filename: "bundle.js",
  publicPath: "/"
},

美丽的现在,还有最后一步。让我们为用户添加一种从UserContainer返回主聊天屏幕的方式。

通过再次利用Header儿童,我们可以以非常简单的方式做到这一点;只是,在这种情况下,我们可以添加另一个 React 路由组件,使我们的生活超级轻松。它被称为Link,它就像 HTML 中的一个标记,但针对 React 路由进行了优化。

UserContainer.js中:

import { Link } from 'react-router-dom';
<Header>
  <Link to="/">
    <button className="red">
      Back To Chat
    </button>
  </Link>
</Header>

当您点击该按钮时,您应该被带到/的根路径。

总结

就这样!为了让应用的路由解决方案启动并运行,我们在本章中介绍了很多内容。如果有什么让人困惑的地方,我邀请您查看上的 React Router 文档 https://reacttraining.com/react-router/ 。接下来,当我们完成基本应用,然后开始将其转换为一个渐进的 Web 应用时,我们将更加深入地讨论 React。