Skip to content

Latest commit

 

History

History
546 lines (366 loc) · 22.2 KB

File metadata and controls

546 lines (366 loc) · 22.2 KB

三、我们的应用的登录页面

我们已经花了最后几章的时间为 React 的开发完全设置了我们的应用。现在,让我们全力以赴构建我们的应用。

在本章中,我们将在 React 中创建应用的登录页面。最后,您应该对基本的 React 语法感到满意。

我们将介绍以下关键概念:

  • 将 UI 分离为组件
  • 编写 JSX
  • 功能组件与类组件
  • 组分状态
  • 创建可重用组件

什么是反应成分?

React 组件在最基本的层面上,是用户界面的一部分,更具体地说,是用于单一用途的 UI 部分。

使用 React,您的 UI 被分为多个部分,这些部分中的部分,然后是这些部分中的部分,依此类推;你明白了。每个部分都是它自己的组件,并且位于单独的文件中。

这个系统的优点现在可能并不明显,但一旦我们深入研究,您将看到它如何使我们的应用更易于理解,也就是说,在开发过程中,我们更容易理解和导航。我们将只构建一个包含少量组件的小型应用。随着应用扩展到数百个组件,效果会增加。

让我们看一个将 UI 拆分为组件的快速示例。这是本书出版商 Packt 的在线商店:

如果我们要在 React 中重新构建它,我们将首先将 UI 划分为有意义的部分。哪些部分涉及不同的目的?

请注意,这个问题没有单一的正确答案;不同的开发人员会有不同的做法,但这里有一个划分对我来说是有意义的:将其分为一个FilterControl、一个搜索栏和一个ResultsGrid

这是我的想法,FilterControl(在顶部)与排序和分页有关,SearchSideBar都是关于搜索特定结果,ResultsGrid都是关于显示匹配结果。每一个都有一个非常具体和独特的目的。

然后,在这三个部分中,我们可以进行更小的划分。结果网格中的每本书都可以是BookCard组件,其中包含BookInfoBookImage组件,以此类推。

我们希望如何细化这些划分取决于我们自己。一般来说,更小的组件数量越多越好,但是一个人决定编写的组件越多,就必须编写更多的样板文件。

React 组件化的另一个优点是可重用性。比方说,在我们的结果网格中,我们为每个结果制作一个记账卡组件。然后,在 Packt 主页上,我们可以重用相同的组件!不再在两个地方两次重写同一代码:

代码重用性也是更小的组件更好的原因。如果构建组件以最大限度地提高可重用性(以适应最大数量的上下文),则可以从现有零件构建新功能。这提高了开发速度和易用性。我们将构建一个可重用组件作为登录表单的一部分,并在应用扩展时将其插入其他地方。

让我们跳转到App.js文件,看看我们构建的第一个组件:

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

const App = () => {
  return <h1>Hello from React!!</h1>
};

export default App;

我们的App组件是一个返回一点 JSX 的函数。就这样。这是一种非常方便的方法,可以将 React 组件看作是返回部分视图的函数。通过按特定顺序调用特定函数,我们构建了 UI。

当然,它会变得更复杂一些。但是,如果您对 React 语法和概念感到不知所措,请回到这个核心原则:React 组件只是返回部分 UI 的函数。

争议和关注点分离

当 React 第一次出现在现场时,它是非常有争议的(对许多人来说,现在仍然如此)。许多开发人员关心的核心问题是 JSX,在 JavaScript 代码的中间有 HTML。

多年来,开发人员一直在单独的文件中编写 HTML、CSS 和 JavaScript。他违反了这一传统。一些开发人员指责该库违反了关注点分离SoC)的编程原则,即代码应该被分割成文件,每个文件都要做一件事。从这个意义上讲,他们认为应该有一个 HTML 文件、一个 CSS 文件和一个 JavaScript 文件,而不是 HTML 和 JavaScript 的混合。

React 开发人员指出,基于类型(HTML 与 JavaScript)分离文件是技术的分离,而不是关注点的分离。HTML 和 JavaScript 都关注于呈现它们共同拥有的功能性 UI。

React 建议,如果你有一个按钮,那么按钮的 HTML 结构和它的功能(点击时发生了什么)都应该存在于同一个文件中,因为这是同样的问题。

因此,React 需要记住的重要一点是分离关注点的概念,您可以根据组件的用途在组件之间画出界限。

当然,所有这些中缺少的部分是 CSS。它不也应该在同一个文件中吗?许多人这样认为,但这样做的成熟解决方案尚未出现。您可以在上阅读更多关于 JS 中 CSS 的内容 https://medium.freecodecamp.org/css-in-javascript-the-future-of-component-based-styling-70b161a79a32

类组件与功能组件

我们刚刚将 React 组件定义为返回 UI 一部分的函数。这是一种很有用的思考方式,对于我们的App组件来说也是如此。但是,还有另一种编写 React 组件的方法。

现在,我们的App组件是一个功能组件。这意味着它实际上是作为函数编写的,但您也可以将组件作为 JavaScript 类编写。这些组件称为基于类的组件有状态组件(我们将在稍后讨论有状态部分)。

JavaScript 类是 ES6 的一个新特性。它们的工作方式与其他语言中的类类似(但不完全相同)。在这里,我们不会深入探讨它们,但出于我们的目的,您可以执行以下操作:

  • 让一个类扩展另一个类(并继承其属性)
  • 使用 new 关键字创建类的实例(即实例化它)

让我们看一个将App组件转换为基于类的组件的示例。

每个类组件必须做两件事:它必须从 React 库扩展Component类,并且必须有render方法。

让我们首先从 React 导入Component类:

import React, { Component } from 'react';

对于那些不熟悉这种语法的人来说,这是 ES6 中对象分解的一个示例。考虑以下事项:

const property = object.property;

对象分解将前面的代码转换成这样,这样可以节省一些键入,但做的是相同的事情:

const { property } = object;

无论如何,既然我们已经导入了Component类,那么让我们创建一个扩展它的类;删除我们的App函数,并编写以下内容:

class App extends Component {

}

JavaScript 类的功能非常类似于对象。它们可以具有值或函数属性(称为方法)。如前所述,我们需要一种render方法。下面是它的样子:

class App extends Component {
  render() {

  }
}

render方法的作用是什么?本质上,当我们将App作为功能组件编写时,它只包含render方法。整个事情只是一个大事件。因此,render 方法实现了我们对 React 组件的期望:它返回一点视图:

class App extends Component {
  render() {
    return <h1>Hello from React!!</h1>;
  }
}

如果你启动应用(或者它已经在运行),你会注意到没有任何变化。

那么,类组件和功能组件之间的区别是什么?

最佳做法是在应用中尽可能多地制作小型功能组件。就性能而言,React 的速度要快一点,React 团队表示有兴趣优化功能组件库。它们也更容易理解。

然而,类组件为我们提供了很多方便的功能。它们可以具有属性,然后我们在render方法中使用这些属性:

class App extends Component {
  greeting = 'Hello from React!!';

  render() {
    return <h1>{this.greeting}</h1>;
  }
}

我们可以从render方法调用方法:

class App extends Component {
  logGreeting = () => {
    console.log('Hello!');
  }

  render() {
    this.logGreeting()
    return <h1>Hello from React!!</h1>;
  }
}

正如我们前面所讨论的,类可以被实例化(语法如const app = new App()。这就是 React 在我们ReactDOM.render通话中所做的;它实例化了我们的App,然后调用render方法来获取 JSX。

因此,将 React 组件视为返回视图位的函数仍然很有用。类组件只是在render函数周围添加了一些额外的功能。

我们的第二部分

我们制作了一个反应组件;让我们再做一个!

如前所述,本章的目标是创建应用的登录页面。首先,让我们在src文件夹中创建一个名为components/的文件夹,然后在其中创建一个名为LoginContainer.js的文件。

If you still have the folder from our Chapter 2, Getting Started with Webpack, with Component1.js, Component2.js, and Component3.js, feel free to delete those files now.

我们的LoginContainer将是另一个类组件,原因我们将在后面介绍。与我们的应用一样,让我们设置一个基本类组件框架:

import React, { Component } from 'react';

class LoginContainer extends Component {
  render() {

  }
}

export default LoginContainer;

在进一步深入之前,让我们先测试一下组件的渲染效果。从我们的render方法返回一个简单的<h1>Hello from LoginContainer</h1>;那么,让我们回到我们的App.js

我对代码组织有点执着,所以在继续之前,让我们把我们的App.js移到components文件夹中。这也意味着我们必须将index.js中的进口声明更改为以下内容:

import App from './components/App';

另外,将我们的app.css移动到components文件夹中,然后在index.js中更改我们的热重新加载配置:

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

现在我们所有的组件都在同一个文件夹中,这就好多了。

App.js内部,我们首先导入LoginContainer

import LoginContainer from './LoginContainer';

然后我们render它而不是<h1>

import React, { Component } from 'react';
import LoginContainer from './LoginContainer';
import './app.css';

class App extends Component {
  render() {
    return <LoginContainer />
  }
}

export default App;

翻回到应用,您应该会看到我们新组件的 Hello from LoginContainer:

正如我们将看到的,随着我们构建更多的组件,我们的App将成为我们主要Container组件的包装。本质上,它将是我们的容器的容器。在App.js中,我们将LoginContainer包装在div#container中,用于 CSS 目的:

class App extends Component {
  render() {
    return (
      <div id="container" className="inner-container">
        <LoginContainer />
      </div>
    );
  }
}

好的,回到LoginContainer.js,让我们写一些 JSX!

删除我们的<h1>标签,并将其替换为以下内容:

class LoginContainer extends Component {
  render() {
    return (
      <div id="LoginContainer" className="inner-container">

      </div>
    );
  }
}

这是一种我非常喜欢的模式——将大多数 React 组件包装在一个带有类名称的iddiv中;不过,这只是一种偏好(因为我写了 CSS,所以这本书必须遵循这种偏好!)。

Note the brackets around the JSX! This format makes multiline JSX much more readable.

当然,我们登录表单的本质是表单。此表单将处理登录和注册。以下是基本的 JSX:

class LoginContainer extends Component {
   render() {
     return (
       <div id="LoginContainer" className="inner-container">
         <form>
           <p>Sign in or sign up by entering your email and password.</p>
           <input 
             type="text" 
             placeholder="Your email" />
           <input 
             type="password" 
             placeholder="Your password" />
           <button className="red light" type="submit">Login</button>
         </form>
       </div>
     )
  }
}

在前面的 JSX 中,您可能会注意到我为<button>编写了className而不是类。还记得我说过 JSX 有一些警告吗?这是一个:因为类在 JavaScript 中是一个受保护的关键字,所以我们不能使用它,所以我们使用className。你很快就会习惯的。

在这一点上,请注意前面 JSX 中的IDclassName,否则您的 CSS 看起来不会很漂亮。

在我们的表格上方,我们将用我们的徽标写一个基本标题:

<div id="LoginContainer" className="inner-container">
  <div id="Header">
    <img src="/img/icon.png" alt="logo" />
    <h1>Chatastrophe</h1>
  </div>
  <form>

您的应用现在应该是这样的(如果您还没有这样做,请从index.html中删除<h1><img>标记):

看起来很漂亮,但它有什么用呢?

反应状态

每个反应组分都有一种叫做**状态的东西。**您可以将其视为组件在某个时间点的配置。

举个例子,当你点击一个心脏图标时,它会变成红色,比如 Twitter。按钮有两种状态:解锁点击*。*点击按钮会改变其状态,从而改变其外观。

这就是反应中的流动;用户操作或事件会导致组件状态更改,从而导致组件的外观更改。

前面的陈述对“好吧,不总是……”有很大的帮助,但它是理解国家的一个有用的起点:

User event -> State change -> Appearance change

让我们在LoginContainer中添加一些state,然后从那里开始。

状态易于定义;它是一个对象,是类的属性。我们可以对其进行如下定义:

class LoginContainer extends Component {
  state = { text: ‘Hello from state! }

   render() {

我们总是在组件的顶部定义state

然后我们可以通过render方法访问我们的state权限:

class LoginContainer extends Component {
  state = { text: ‘Hello from state! };

  render() {
    return (
      <div id="LoginContainer" className="inner-container">
        <div id="Header">
          <img src="/img/icon.png" alt="logo" />
          <h1>{this.state.text}</h1>
        </div>

在前面的代码中,JSX 中的大括号表示我们正在插入一些 Javascript 代码。

这就是我们初始化state的方式,但是这种状态不是很有用,因为没有改变它的机制。

我们需要做的是提供一种方法来响应用户事件,并根据它们修改状态。

如果当用户单击“来自状态的问候”时文本发生了更改怎么办!?

让我们在h1标记中添加一个onClick属性,如下所示:

<h1 onClick={this.handleClick}>{this.state.text}</h1>

它引用了我们类中名为handleClick的一个方法,我们可以对其进行如下定义:

class LoginContainer extends Component {
  state = { text: 'Hello from state!' };

  handleClick = () => {
    this.setState({ text: 'State changed!' });
  };

  render() {

handleClick里面,我们想要改变我们的状态。我们可以在 React 中通过一个名为this.setState的函数来实现这一点,我们将新的状态对象传递到该函数中。

试试看!当你点击“你好来自州”时!,它应该立即换成新的文本。

那么,这是如何工作的呢?setState所做的是将传入的对象作为参数,并将其合并到当前状态(如果您在状态中有多个属性,但只将一个具有一个属性的对象传入setState,它将只更改该属性,而不会覆盖其余属性)。然后,它再次调用render()方法,我们的组件在 DOM 中更新以反映新的状态。

如果这看起来令人困惑,不要担心,我们还有几个例子要介绍,所以您将获得更多关于组件状态的实践。

我们的LoginContainer将有两个状态,每个<input>标签对应一个状态。我们将在状态中的电子邮件和密码字段中存储用户键入的内容,以便当他们提交表单时,我们可以访问他们。

“等等 Scott,”您可能会说,“为什么我们不直接进入 DOM,在用户提交表单时获取每个输入的值,即 jQuery 方式?”

我们当然可以这样做,但它会破坏反应流,如下所示:

User edits input -> Update state -> Re-render input to reflect new value.

这样,我们的输入值存储在状态中,视图与之保持同步,而不是将输入值存储为 DOM 元素的属性,并在需要时进行访问。

在这一点上,这种方法的优势似乎并不明显,但它使我们的代码更加明确和易懂。

因此,在前面的流程中,每当用户更改输入时,我们都需要更新状态。首先,让我们更改状态初始化的方式:

state = { email: '', password: '' };

然后,我们删除handleClick并将handleEmailChangehandlePasswordChange方法添加到我们的组件中:

 handleEmailChange = (event) => {
   this.setState({ email: event.target.value });
 };

 handlePasswordChange = (event) => {
   this.setState({ password: event.target.value });
 };

前面的方法接收事件(用户在字段中键入),从事件中获取值,然后将状态设置为该值。

再次注意,我们不必在每次调用setState时同时定义电子邮件和密码;它将合并对现有状态对象的更改,而不会覆盖其他值。

好的,现在是最后一步。让我们将onChange属性添加到我们的输入中,这些属性调用我们的更改处理程序。另一个关键步骤是,我们的输入的value必须来自状态。我们可以这样做:

<input
  type="text"
  onChange={this.handleEmailChange}
  value={this.state.email}
  placeholder="Your email"
/>
<input
  type="password"
  onChange={this.handlePasswordChange}
  value={this.state.password}
  placeholder="Your password"
/>

You can reset your h1 to <h1>Chatastrophe</h1>.

如果一切顺利,您应该注意输入功能没有变化(如果代码中有输入错误,您将无法输入其中一个)。让我们通过在表单提交时添加处理程序来确保它实际工作:

<form onSubmit={this.handleSubmit}>

我们的方法是:

handleSubmit = (event) => {
  event.preventDefault();
  console.log(this.state);
};

前面的方法将在用户提交表单时为我们注销状态(单击按钮),并阻止表单实际提交。

尝试在这两个字段中键入,然后单击提交。您应该看到一个控制台日志,其中有state对象:

Object { email: "email@email.com", password: "asdfas" }

我们做到了!我们的第一个组件与状态发生反应。

我希望您已经了解了 React 数据流。我们的应用具有状态(存储在不同的组件中),它会根据事件(通常是用户启动的)进行更新,这会导致我们的应用的某些部分根据新状态重新启动:

Events -> State changes -> Re-render.

这是一个简单的模式,只要您仔细考虑一下,就可以很容易地跟踪应用在任何时候的外观。

重用组件

在我们完成我们的LoginContainer骨架之前,我还想做一个改变。

我们前面讨论过如何使 React 组件可重用,以便您可以在应用的多个位置实现相同的代码。为了节省时间,我们应该尽可能地将 UI 分成许多小的、可重用的部分,我认为我们的LoginContainer中有一个很好的候选者。

LoginContainer将不是我们唯一的集装箱。在接下来的几章中,我们将创建具有不同内容的新页面,但我们希望它们具有相同的外观,并且我们希望将 Chatastrophe 徽标和标题放在顶部与现在相同的位置。

我的建议是我们制造一个新的Header组件,我们可以保存下来供将来使用。

现在,我们将LoginContainer作为类组件,因为我们需要使用状态和方法。另一方面,我们的标题没有任何状态或功能;它实际上只是一个用户界面。最好的选择是让它成为一个功能组件,因为我们可以。

The rule for a class versus a functional component is essentially make a component functional wherever you can, unless you need state or methods.

在我们的src/组件文件夹中,创建一个名为Header.js的新文件。然后,我们可以创建功能组件的骨架。复制粘贴LoginContainer中的相关div#Header并添加为return声明:

import React from 'react';

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

export default Header;

现在,回到我们的LoginContainer,我们想导入我们的标题,如图所示:

import Header from './Header';

然后我们可以用一个简单的<Header />标签替换div#Header

render() {
 return (
   <div id="LoginContainer" className="inner-container">
     <Header />
     <form onSubmit={this.handleSubmit}>

Another JSX gotcha--all JSX tags must be closed. You can’t just use <Header>.

就这样!这就是制作一个小的、可重用的组件所需要的全部。我们的LoginContainer现在看起来更干净了,我们节省了一些打字时间。

我们的登录表单看起来很棒,但有个问题。当你在 Chatastrophe 总部为团队演示时(尽管你是唯一的开发人员,但团队不知何故已经膨胀到了 20 人),一名实习生举手——“你知道,它实际上是如何工作的?”

总结

我们创建了第一个有状态的 React 组件,一个登录表单。我们学习了所有关于 React 组件的知识,以及创建它们的最佳实践。然后,我们构建了登录表单,并介绍了如何处理表单的更改,更新状态。

不幸的是,只注销电子邮件和密码的登录表单并没有那么有用(或安全!)。我们的下一步将是设置应用的后端,以便用户可以实际创建帐户并登录。