六、使用 React 建立一个博客

嘿! 读到书的最后一部分,你会学到 Facebook 的 React 图书馆。 在我们开始这一章之前,让我们先看看你在这本书中的经历:

  • 你首先使用 JavaScript 的 ES6 语法构建了一个简单的 ToDo 列表应用,然后创建一个构建脚本将其编译为 ES5,这样它就可以兼容旧的浏览器。
  • 然后,您在建立自己的自动化开发环境的同时构建了一个 Meme Creator,并在此过程中学习了许多新的概念和工具。
  • 接下来,使用开发环境并构建了一个 Event Registration 应用,在该应用中构建了用于 API 调用和表单验证的第一个可重用 JavaScript 模块。
  • 然后,利用 JavaScript webapi 的强大功能构建了一个使用 WebRTC 的点对点视频调用应用。
  • 最后,您构建了自己的 HTML5 自定义元素,该元素将显示天气小部件,并且可以轻松导入并与其他项目一起使用。

从初学者阶段开始,您已经构建了一些非常棒的应用,现在您已经熟悉了现代 JavaScript 的许多重要概念。 现在,是时候使用这些技能来学习 JavaScript 框架了,它将促进您的开发过程。 本章将专注于帮助你开始使用 React。

为什么要使用框架?

现代应用开发主要关注速度、可维护性和可伸缩性。 鉴于 web 是许多应用的主要平台,任何 web 应用都应该是如此。 JavaScript 可能是一种很棒的语言,但当您在团队环境中处理大型应用时,编写普通的 JavaScript 有时会是一个乏味的过程。

在这样的应用中,您将不得不操作许多 DOM 元素。 每当您对 DOM 元素的 CSS 进行更改时,就称为重绘。 它将影响元素在浏览器上的显示方式。 每当您在 DOM 中删除、更改或添加一个元素时,它就被称为 reflow。 父元素的回流会导致它的所有子元素也回流。 重绘和回流是昂贵的操作,因为它们是同步的。 这意味着当重绘或回流发生时,JavaScript 将无法运行。 这将导致 web 应用的延迟或缓慢执行(特别是在较小的设备上,如低端智能手机)。 到目前为止,我们一直在构建非常小的应用; 因此,我们没有注意到任何性能问题,但对于应用,比如 Facebook,这是至关重要的(实际上有 1000 个 DOM 元素)。

此外,编写大量 JavaScript 代码意味着增加代码的文件大小。 对于依赖 3G 或较低连接的移动用户来说,这意味着你的应用需要更长的加载时间。 这将导致糟糕的用户体验。

最后,前端 JavaScript 代码需要处理许多副作用(如单击、滚动、悬停和网络请求等事件)。 当在团队环境中工作时,每个开发人员都应该知道您的代码要处理什么样的副作用。 当 web 应用发展时,每个副作用都需要被正确跟踪。 在普通的 JavaScript 中,在这样的环境中编写可维护的代码也很困难。

幸运的是,JavaScript 社区很清楚所有这些场景,因此,创建并积极维护了许多开源 JavaScript 库和框架,以解决上述问题并提高开发人员的生产率。

选择一个框架

在 2017 年选择一个 JavaScript 框架比学习 JavaScript 本身还要困难(是的,这是真的!),因为几乎每周都有新的框架发布。 但是,除非您的要求非常具体,否则您不需要担心大多数问题。 目前,有一些框架在开发者中非常流行,比如 React、Vue.js、Angular、Ember 等等。

这些框架非常受欢迎,因为它们可以让您在几乎没有时间的情况下启动并运行您的应用,并得到使用这些框架的大型开发人员社区的出色支持。 这些框架还附带了它们自己的构建工具,这将为您省去设置自己的开发环境的麻烦。

反应

在本章中,我们将学习使用 React 构建 web 应用的基础知识。 React 被 Facebook 建立并广泛使用。 许多其他著名的应用,如 Instagram、Airbnb、Uber、Pinterest、Periscope 等,也在他们的 web 应用中使用 React,这有助于将 React 开发成一个成熟的、久经考验的 JavaScript 库。 在写这本书的时候,React 是 GitHub 中最受欢迎的前端 JavaScript 框架,拥有 70,000 多名活跃的开发者社区。

与其他大多数 JavaScript 框架不同,React 并不认为自己是一个框架,而是一个用于构建用户界面的库。 它通过将应用的每个部分组成更小的功能组件,完美地处理应用的视图层。

函数是执行任务的简单 JavaScript 代码。 从本书一开始我们就一直在使用函数。 React 使用函数的概念来构建 web 应用的每个组件。例如,看看下面的元素:

<h1 class="hello">Hello World!</h1>

假设您想用一个动态变量替换单词world,例如,某人的名字。 React 通过将元素转换为函数的结果来实现这一点:

const hello = (name) => React.createElement("h1", { className: "hello"}, "Hello ", name, "!")

现在,函数hello包含所需的元素作为其结果。 如果你尝试,hello('Rahul'),你会得到以下结果:

<h1 class="hello">Hello Rahul!</h1>

但是等等! 什么是React.createElement()方法? 忘了告诉你了。 这就是 React 创建 HTML 元素的方式。 但是将其应用到构建应用中对我们来说是不可能的! 想象一下,为了创建一个包含大量 DOM 元素的应用,您需要键入多少个元素。

为此,React 在 XML(JSX)中引入了JavaScript。 它是一个在 JavaScript 中编写 xml 风格标记的过程,它被 React 编译为React.createElement()方法,长话短说,你也可以编写hello函数,如下所示:

const hello = (name) => <h1 className="hello">Hello {name}!</h1>

这将更有意义,因为我们只是在 JavaScript 的 return 语句中编写 HTML。 有趣的是,元素的内容直接依赖于函数的参数。 在使用 JSX 时,你需要注意一些事情:

  • JSX 元素的属性不能包含 JavaScript 关键字。 注意,class 属性被替换为className,因为 class 在 JavaScript 中是一个保留关键字。 同样,对于 attribute,它变成了htmlFor
  • 要在 JSX 中包含变量或表达式,您应该将它们包装在花括号{}中。 它类似于模板字符串中使用的${}
  • JSX 需要 Babel React 预设来编译成 JavaScript。
  • JSX 中的所有 HTML 元素应该只使用小写字母。
    • 例如:<p></p><div></div><a></a>
  • 用大写字母表示 HTML 是无效的。
    • 例如:<Div></Div><Input></Input>均无效。
  • 我们创建的自定义组件应该以大写字母开头。
    • 例如:考虑我们之前创建的hello函数,它是一个无状态的 React 组件。 要将其包含在 JSX 中,您应该将其命名为Hello并将其包含为<Hello></Hello>

上面的函数是一个简单的无状态React 组件。 无状态的 React 组件直接根据作为函数参数提供的变量输出元素。 它的产出不依赖于任何其他因素。

Detailed information on JSX can be found at: https://facebook.github.io/react/docs/jsx-in-depth.html.

这种表示方法适用于较小的元素,但许多 DOM 元素都有各种副作用,比如 DOM 事件和 AJAX 调用,它们会导致函数范围之外的因素(或变量)修改 DOM 元素。 为了解决这个问题,React 提出了有状态组件的概念。

有状态组件有一个叫做state的特殊变量。 state变量包含一个 JavaScript 对象,它应该是不可变的。 我们马上就会看到不变性。 现在,看看下面的代码:

class Counter extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    }
  }

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

这是一个简单的有状态 React 组件。 正如你所看到的,我们正在从React.Component接口扩展一个类,就像我们在上一章从HTMLElement扩展它来创建我们的自定义元素一样,就像自定义元素一样,React 组件也有生命周期方法。

在组件插入到 DOM 的不同阶段或组件更新时调用 react lifecycle 方法。 当一个组件被插入到 DOM 中时,下面的生命周期方法会被调用(按照确切的顺序):

  1. 构造函数()
  2. componentWillMount ()
  3. 呈现()
  4. componentDidMount ()

当由于组件的状态或道具的改变而导致更新时,将调用以下生命周期方法。

  1. componentWillReceiveProps ()
  2. shouldComponentUpdate ()
  3. componentWillUpdate ()
  4. 呈现()
  5. componentDidUpdate ()

还有一个 lifecycle 方法,当组件从 DOM 中移除时,会调用它:

  • componentWillUnmount ()

For a detailed explanation of how each of the lifecycle method works in react, refer the following page in react documentation: https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle

上面的Counter类中的render方法是 React 组件的生命周期方法之一。 顾名思义,使用render()方法来呈现 DOM 中的元素。 每当组件被挂载和更新时,都会调用render方法。

当 React 组件的stateprops发生改变时,就会发生更新。 我们还没看道具。 为了检测状态变量的变化,React 要求状态为不可变对象。

不变的状态

不可变对象是一旦设置就不能更改的对象! 是的,这是正确的。 一旦创建了该对象,就没有回头路了。 这让你想知道“如果我需要修改该对象的属性怎么办?” Well, it's simple; 您只需从旧对象创建一个新对象,但这次使用的是新属性。

现在,这可能看起来有很多工作,但相信我,创建一个新对象实际上更好。 因为,大多数时候,React 只需要知道对象是否被更改以更新视图。 例如:

this.state = { a: 'Tree', b: 'Flower', c: 'Fruit' };
this.state.a = 'Plant';

这是更改 JavaScript 对象属性的标准方法。 这里,我们称之为可变方式。 太棒了! 你只是改变了状态。 但是 React 如何知道状态被修改了,并且应该调用它的生命周期方法来更新 DOM 元素呢? 这是个问题。

为了克服这个问题,React 组件有一个叫做setState()的特殊方法,它可以以不可变的方式更新状态,并调用所需的生命周期方法(包括render,它将更新 DOM 元素)。 让我们看看如何以不可变的方式更新状态:

this.state = { a: 'Tree', b: 'Flower', c: 'Fruit' };
this.setState({ a: 'Plant' });

这将通过创建一个新的状态对象而不是旧的状态对象来更新状态。 旧状态和新状态是两个不同的对象:

oldState = { a: 'Tree', b: 'Flower', c: 'Fruit' }
newState = { a: 'Plant', b: 'Flower', c: 'Fruit' }

React 现在可以很容易地通过两个对象oldState !== newState的简单比较来检查状态是否被改变了,如果状态被改变了,oldState !== newState将返回 true; 因此,给出了一个快速的视图更新。 通过这种方式比较对象要比遍历每个对象的属性并检查任何属性是否被更改快得多,效率也高得多。

The goal of using setState() is to call the render method, which will update the view. Hence, setState() should not be used inside the render method, or else it will result in an infinite loop.

JavaScript 数据类型不是不可变的; 然而,使用不可变数据类型是非常重要的,您很快就会了解更多。

道具

Props 是从父组件传递给反应组件的数据。 除了道具是只读的之外,道具与状态相似。 您不应该从组件本身内部更改组件的 props。 例如,考虑以下组件:

class ParentComponent extends Component {
  render() {
    return (
      <ChildrenComponent name={'World'} />
    )
  }
}

class ChildrenComponent extends Component {
  render() {
    return (
      <h1>Hello {this.props.name}!</h1>
    )
  }
}

这里,在ParentComponent的渲染方法中,传递给ChildrenComponent元素的 name 属性已经成为ChildrenComponent的支柱。 这个道具不应该被ChildrenComponent改变。 但是,如果值从ParentComponent改变,ChildrenComponent也会用新的道具重新渲染。

To learn more about components and props, visit the following page in react documentation: https://facebook.github.io/react/docs/components-and-props.html

建立柜台

看看我们之前创建的Counter类。 顾名思义,它应该呈现一个每秒钟递增 1 的计数器。 为此,我们需要使用setInterval来增加计数器的状态对象的 count 属性。 我们可以使用componentWillMountcomponentDidMount生命周期方法添加setInterval。 由于这个过程不需要任何对 DOM 元素的引用,我们可以使用componentWillMount

Counter类中,我们需要添加以下代码行:

increaseCount() {
  this.setState({ count: this.state.count+1 })  
}
componentWillMount() {
  setInterval(this.increaseCount.bind(this), 1000);  
}

这将自动执行增量每秒钟和render方法将更新所需的 DOM 元素。 查看计数器的行动,访问以下 JSFiddle 页面:https://jsfiddle.net/reb5ohgk/

现在,在 JSFiddle 页面,看看左上角的外部资源部分。 你应该会看到其中包含的三种资源,如下面的截图所示:

与此同时,在 JavaScript 代码块中,我选择了 Babel+JSX 语言。 如果你点击 JavaScript 部分右上角的设置图标,你将能够看到如下截图所示的一组选项:

下面是配置的全部内容:

  • 我包含的第一个 JavaScript 文件是react.js库。 React 库是核心,负责将 DOM 元素创建为组件。 然而,React 在一个虚拟 DOM中渲染组件,而不是在真正的 DOM 中。
  • 我包含的第二个库是ReactDOM。 它用于为 React 组件提供包装器,以便它们可以在 DOM 中呈现。 考虑下面这行:
ReactDOM.render( <Counter />,  document.querySelector("app"));
  • 这将使用ReactDOM.render()方法将Counter组件渲染到 DOM 中的<app></app>元素中。
  • 第三个库是 Bootstrap; 我只是为了发型加的。 那么,让我们看看构型的下一步。
  • 在 JavaScript 代码块中,我选择了 Babel + JSX 语言。 这是因为浏览器只知道 JavaScript。 它们对 JSX 一无所知,就像老版本的浏览器对 ES6 一无所知一样。
  • 所以,我只是指示 JSFiddle 使用浏览器内的 Babel 转换器将 ES6 和 JSX 代码编译回正常的 JavaScript,这样它就可以在所有的浏览器中工作。
  • 在实际的应用中,我们将使用 Webpack 和 Babel 加载器和 React 预设来编译 JSX,就像我们在 ES6 中做的那样。

到目前为止,你应该对 React 有了一个很好的了解。所以,让我们开始构建你的第一个 React 应用——一个待办事项列表——在下一节中。

React 速成班

在本节中,我们将花 10 分钟构建您的第一个 React 应用。 对于本节,您不需要任何文本编辑器,因为您将在 JSFiddle 构建应用!

开始访问 JSFiddle 页面:https://jsfiddle.net/uhxvgcqe/,在这里我设置了构建 React 应用所需的所有库和配置。 您应该在这个页面中为 React 速成课程部分编写代码。

这个页面有 React 和ReactDOM作为窗口对象(全局作用域)的属性可用,因为我在外部资源中包含了这些库。 我们还将从 React 对象创建一个组件对象。 在 ES6 中,有一个技巧可以将对象的属性或方法获取为独立变量。 看看下面的例子:

const vehicles = { fourWheeler: 'Car', twoWheeler: 'Bike' };
const { fourWheeler, twoWheeler } = vehicles;

这将创建两个新的常量,fourWheelertwoWheeler,从车辆对象的各自属性。 这被称为解构赋值,它可以处理对象和数组。 遵循同样的原则,在你的 JSFiddle 的第一行,添加以下代码:

const { Component } = React;

这将从 React 对象的 component 属性创建组件对象。 接下来,我在 HTML 部分包含了一个<app></app>元素,这是我们要渲染 React 组件的地方。 因此,使用以下代码创建一个对<app>元素的引用:

const $app = document.querySelector('app');

让我们创建一个有状态的应用组件来渲染我们的 ToDo 列表。 在 JSFiddle 中,输入以下代码:

class App extends Component {
  render() {
    return(
    <div className="container">      
      <h1>To Do List</h1>      
      <input type="text" name="newTask"/>      
      <div className="container">        
        <ul className="list-group">          
          <li>Do Gardening</li>          
          <li>Return books to library</li>          
          <li>Go to the Dentist</li>        
        </ul>      
      </div>    
    </div> 
    ); 
  }
}

在类之外,添加以下代码块,它将在 DOM 中呈现 React 组件:

ReactDOM.render( <App/>,  $app);

现在,点击运行在左上角的 JSFiddle 页面。 你的应用现在应该像这样:https://jsfiddle.net/uhxvgcqe/1/

For more information and usage details regarding destructuring assignments, visit the following MDN page: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.

添加和管理状态

有状态的 React 组件最重要的部分是它的状态,它提供了渲染 DOM 元素所需的数据。 对于我们的应用,我们需要两个状态变量:一个包含任务数组,另一个包含文本字段的输入值。 作为一个全功能表示,我们总是需要为每个视图更改维护一个状态,包括输入字段的值。

在你的App类中,添加以下代码行:

constructor() {  
  super();        
  this.state = {    
    tasks: [],      
    inputValue: "",    
  }  
}

这将为类添加一个构造函数,我们应该首先调用super(),因为我们的类是一个扩展类。 super()将调用Component接口的构造函数。 在下一行中,我们创建了状态变量的任务和inputValuetasks是一个数组,它将包含一个带有任务名称的字符串数组。

管理输入字段的状态

首先,我们将把inputValue状态与输入字段连接起来。 在你的render()方法中,为输入的 JSX 元素添加 value 属性,如下面的代码所示:

<input type="text" name="newTask" value={this.state.inputValue} />

我们已经显式地将输入字段的值与状态变量绑定在一起。 现在,尝试单击 Run 并编辑输入字段。 您应该不能编辑它。

这是因为无论您在这个字段中输入什么,render()方法将简单地呈现我们在return()语句中指定的内容,这是一个带有空inputValue的输入字段。 那么,我们如何更改输入字段的值呢? 通过向输入字段添加一个onChange属性。 让我告诉你怎么做。

App类内部,在位置添加以下代码行,正如我在以下代码块中指定的:

class App extends Component { 
  constructor() {
    ...
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {  
    this.setState({inputValue: event.target.value});  
  }

  ...
}    

这个handleChange方法将接收输入事件,并根据事件目标的值更新状态,该目标应该是输入字段。 注意,在构造函数中,我已经将this对象与handleChange方法绑定。 这省去了在 JSX 元素中使用this.handleChange.bind(this)的麻烦。

现在,我们需要将handleChange方法添加到输入元素的onChange属性中。 在你的 JSX 中,向输入元素添加onChange属性,如下所示:

<input type="text" name="newTask" value={this.state.inputValue} onChange={this.handleChange} />

单击 Run,您应该能够再次在输入字段中输入。 但这一次,每次编辑输入字段时,您的inputValue状态都会更新。 你的 JSFiddle 现在应该像这样:https://jsfiddle.net/uhxvgcqe/2/

这是 React 的单向数据流(或单向数据绑定),其中数据只在一个方向上流动,从状态到render方法。 呈现组件中的任何事件都必须触发状态更新来更新视图。 此外,状态只能使用this.setState()方法以不可变的方式更新。

管理任务的状态

我们需要在应用中维护的第二个状态是tasks数组。 目前,我们有一个无序的示例任务列表。 将这些任务作为字符串添加到tasks数组中。 构造函数中的state对象现在应该如下所示:

this.state = {          
  tasks: [      
    'Do Gardening',        
    'Return books to library',        
    'Go to the Dentist',      
  ],            
  inputValue: "",        
};

现在,让我们从状态填充任务。 在你的render方法中,在<ul>元素中,移除所有的<li>元素,并将其替换为以下内容:

<ul className="list-group">            
  {            
    this.state.tasks.map((task, index) => <li key={index}>{ task }</li>)            
  }          
</ul>

JSX 中的花括号{}只接受直接返回值的表达式,就像模板字面量中的${}一样。 因此,我们可以使用数组的 map 方法来返回 JSX 元素的数组。 当我们以数组形式返回 JSX 元素时,我们应该添加一个具有惟一值的key属性,React 使用该属性来标识数组中的元素。

因此,在前面的代码中,我们需要执行以下步骤:

  1. 我们遍历statetasks数组,并使用数组的map()方法将列表项作为 JSX 元素的数组返回。
  2. 对于key属性的唯一值,我们使用数组中每个元素的index

单击 Run,您的代码将生成与之前相同的输出,除了任务现在是从状态填充的。 您的代码现在应该像这样:https://jsfiddle.net/uhxvgcqe/3/

添加新任务

我们在应用中的最后一步是允许用户添加一个新任务。 让我们简单一点,通过在键盘上按输入返回添加一个新任务。 为了检测输入按钮,我们需要在输入字段上使用类似于onChange的属性,但它应该发生在onChange事件之前。 onKeyUp就是这样一个属性,当用户在键盘上按下键或释放键时调用它。 它也发生在onChange事件之前。 首先创建将处理 keyup 进程的方法:

class App extends Component {
  constructor() {
    ...
    this.handleKeyUp = this.handleKeyUp.bind(this);
  }

  handleKeyUp(event) {
    if(event.keyCode === 13) {    
      if(this.state.inputValue) {        
        const newTasks = [...this.state.tasks, this.state.inputValue];
        this.setState({tasks: newTasks, inputValue: ""});      
      } else {      
        alert('Please add a Task!');      
      }    
    }
  }

  ...
}

以下是handleKeyUp方法的工作原理:

  1. 首先,检查事件的keyCode是否为13keyCode,输入(Windows)键,返回(Mac)键。 然后检查是否有this.state.inputValue可用。 否则,它将抛出一个提示'Please add a Task'的警告。
  2. 第二个也是最重要的部分是在不改变状态的情况下更新数组。 在这里,我使用了 spread 语法来创建一个新的任务数组并更新状态。

在你的render方法中,再次将输入的 JSX 元素修改为以下内容:

<input type="text" name="newTask" value={this.state.inputValue} onChange={this.handleChange} onKeyUp={this.handleKeyUp}/>

现在,点击 Run,输入一个新任务,点击,输入。 您应该看到一个新任务被添加到 ToDo 列表中。 您的代码现在应该类似于https://jsfiddle.net/uhxvgcqe/4/,这是 ToDo 列表的完整代码。 在讨论 React 的优点之前,让我们先看看添加任务时使用的扩展语法。

使用扩展语法防止突变

在 JavaScript 中,数组和对象在赋值期间通过引用传递。 例如,打开一个新的 JSFiddle 窗口并尝试以下代码:

const a = [1,2,3,4];
const b = a;
b.push(5);
console.log('Value of a = ', a);
console.log('Value of b = ', b);

我们正在从数组a创建一个新的数组b。 然后我们将一个新值5推入数组b。 如果您查看控制台,您的输出将如下所示:

令人惊讶的是,这两个数组都被更新了。 这就是我所说的引用。 ab都持有对同一个数组的引用,这意味着更新其中一个就会同时更新两个数组。 这对数组和对象都成立。 这意味着,如果我们使用正常的赋值,我们显然将改变状态。

然而,ES6 为数组和对象提供了扩展语法。 我已经在handleKeyUp方法中使用了这个,其中我从this.state.tasks array创建了一个newTask数组。 在 JSFiddle 窗口中,你尝试了前面的代码,将代码改为如下:

const a = [1,2,3,4];
const b = [...a, 5];
console.log('Value of a = ', a);
console.log('Value of b = ', b);

看看我这次是如何创建一个新的数组b的。 三个点...(称为扩展算子)用于展开数组a中的所有元素。 与此同时,添加了一个新元素5,并创建了一个新数组,并将其赋值给b。 这个语法一开始可能会令人困惑,但这是我们应该如何在 React 中更新数组值,因为这将创建一个新的数组(以不可变的方式)。

同样地,对于对象,你应该做以下事情:

const obj1 = { a: 'Tree', b: 'Flower', c: 'Fruit' };
const obj2 = { ...obj1, a: 'plant' };
const obj3 = { ...obj1, d: 'seed' };

console.log('Value of obj1 = ', obj1);
console.log('Value of obj2 = ', obj2);
console.log('Value of obj3 = ', obj3);

我在https://jsfiddle.net/bLo4wpx1/中创建了一个传播运营商。 请随意使用它来理解扩展语法的工作原理,我们将在本章和下一章中经常使用它。

For more practical examples of using the spread syntax, visit the MDN page https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator.

使用 React 的优点

我们用 React 在 10 分钟内创建了一个待办事项列表应用。 在本章的开始,我们讨论了为什么我们需要一个 JavaScript 框架以及使用普通 JavaScript 的缺点。 在本节中,让我们着眼于 React 如何克服这些因素。

性能

DOM 更新代价高昂。 重绘和回流是同步事件,因此,它们需要尽可能地最小化。 React 通过维护一个虚拟 DOM 来处理这个场景,这使得 React 应用非常快。

当我们在render方法中修改 JSX 元素时,React 将更新虚拟 DOM 而不是真实 DOM。 更新虚拟 DOM 速度快、效率高,而且比更新实际 DOM 便宜得多,而且只有在虚拟 DOM 中更改的元素才会在实际 DOM 中修改。 React 使用了一种智能的差分算法来做到这一点,我们基本上不用担心这个问题。

要了解 React 的工作细节和性能,你可以阅读以下 React 文档中的文章:

可维护性

React 在这一节非常出色,因为它将应用整洁地组织成状态,并将相应的 JSX 元素分组为组件。 在 ToDo 列表应用中,我们只使用了一个有状态组件。 但是我们也可以把它的 JSX 分成更小的无状态子组件。 这意味着子组件中的任何修改都不会影响父组件。 因此,即使我们修改列表的样子,核心功能也不会受到影响。

查看 JSFiddle:https://jsfiddle.net/7s28bdLe/,在那里我把列表项目组织在 ToDo 列表中作为较小的子组件。

这在团队环境中非常有用,在团队环境中,每个人都可以创建自己的组件,并且可以很容易地被其他人重用,这将提高开发人员的生产率。

大小

反应很小。 整个 React 库缩小后大约 23 KB,而react-dom大约 130 KB。 这意味着即使在缓慢的 2G/3G 连接上,它也不会在页面加载时间上造成任何严重问题。

用 React 建立一个博客

本节的目的是通过构建一个简单的博客应用来学习 React 的基础知识,以及如何在 web 应用中使用它。 到目前为止,我们一直在学习 React,但现在是时候看看它是如何在实际的 web 应用中使用。 React 将在我们的开发环境中工作得很好,到目前为止,我们已经在这本书中使用了,除了我们需要添加一个额外的react预设到babel-loader

但是react-community提出了一个更好的解决方案,那就是create-react-app命令行工具。 基本上,这个工具用所有必要的开发工具、Babel 编译器和插件创建你的项目,所以你只需要专注于编写代码,而不必担心 Webpack 配置。

create-react-app recommends using yarn instead of npm while working on React, but since we are very familiar with npm, we will not use yarn in this chapter. If you want to learn about yarn, visit: https://yarnpkg.com/en/.

要了解create-react-app是如何工作的,首先让我们使用 npm 全局安装这个工具。 打开你的终端并输入以下命令(因为这是一个全局安装,它将在任何目录下工作):

npm i -g create-react-app

Linux 用户可能必须添加sudo前缀。 安装完成后,你可以运行一个简单的命令来创建 React 项目的样板文件:

create-react-app my-react-project

这个命令需要一段时间,因为它必须创建一个my-react-project目录,并为 React 开发环境安装所有的 npm 依赖项。 命令完成后,可以在终端中使用以下命令运行应用:

cd my-react-project
npm start

这将启动 React 开发服务器,并将打开浏览器,显示一个用 React 构建的欢迎页面,如下面的截图所示:

让我们看看文件在项目中是如何组织的。 项目根文件夹中的文件将按以下结构排列:

.
├── node_modules
├── package.json
├── public
├── README.md
├── src
└── yarn.lock

公共文件夹将包含index.html文件,其中包含div#root元素,我们的 React 组件将被渲染到该元素。 此外,它还包含faviconmanifest.json文件,当网页被添加到主屏幕时,该文件向 Android 设备提供信息(通常用于先进的 web 应用)。

src目录包含 React 应用的源文件。 src目录的文件结构如下:

.
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js

index.js文件是应用的入口点,它只是呈现公共目录中index.html文件中的App.js文件中的App组件。 我们在App.js文件中写入主App组件。 应用中的所有其他组件都是App组件的子组件。

到目前为止,我们一直在使用 JavaScript 构建多页面应用。 但是现在,我们将使用 React 构建一个单页面应用。 Single Page Application(SPA)是一个应用的所有资产被加载初始,然后它将像一个正常的应用在用户的浏览器上工作。 水疗中心是现在的趋势,因为它们为用户提供了跨各种设备的良好用户体验。

为了在 React 中构建 SPA,我们需要一个库来管理应用中页面(组件)之间的导航。react-router就是这样一个库,它将帮助我们管理应用中页面(路由)之间的导航。

就像其他章节一样,我们的博客也将在移动设备上响应。 让我们来看看我们将要构建的博客应用:

对于这个应用,我们将不得不编写大量代码。 因此,我已经准备了启动器文件供您使用。 不是从create-react-app工具开始,而是从图书代码Chapter06文件夹中的启动器文件开始。

除了 React 和react-dom,starter 文件还包含以下库:

为博客提供 API 的服务器可以在图书代码Chapter06\Server目录中找到。 在构建应用时,应该保持该服务器运行。 我强烈建议您在开始构建博客之前查看完整的应用。

create-react-app supports reading environment variables from the .env file straight out of the box; however, with the condition that all environment variables should be prefixed with the REACT_APP_ keyword. For more information, read: https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-custom-environment-variables.

要运行完成的应用,请执行以下步骤:

  1. 首先启动服务器,在服务器目录中运行npm install,然后运行npm start
  2. 它将打印控制台中应该添加到Chapter 6\completedCode文件的.env文件的 URL。
  3. Chapter 6\CompletedCode文件中,使用.env.example文件创建.env文件,并将控制台输出的第一行打印的 URL 粘贴为REACT_APP_SERVER_URL值。
  4. 在您的终端中导航到图书代码Chapter 6\CompletedCode文件目录,并运行相同的npm installnpm start命令。
  5. 它应该能打开你浏览器上的博客。 如果没有打开博客,那么在浏览器上手动打开http://localhost:3000/

我还使用 swagger 为服务器创建了一个 API 文档。 要访问 API 文档,当服务器运行时,它将在控制台输出的第二行中打印文档 URL。 只需在浏览器中打开 URL 即可。 在文档页面中,单击默认组,您应该会看到 API 端点列表,如下面的截图所示:

你可以看到关于 API 端点的所有信息,甚至通过单击 API 来尝试它们,然后单击 try it out:

花你的时间。 访问完整的博客的所有部分,尝试 swagger 文档中的所有 api,并了解它是如何工作的。 一旦你完成了它们,我们将进入下一节,我们将开始构建应用。

创建导航栏

希望你试过这个 app。目前我设置了服务器 3 秒响应; 因此,当您尝试在页面之间导航时,应该会看到一个加载指示器。

在这个应用中,所有页面都有一个共同点,那就是顶部导航栏:

在前几章中,我们使用 Bootstrap 很容易地创建了导航栏。 但是,我们不能在这里使用 Bootstrap,因为在 React 中,所有的 DOM 元素都是通过组件动态呈现的。 然而,Bootstrap 需要 jQuery,它只能在一个普通的 DOM 上工作,所以它可以显示动画,当点击汉堡包菜单时,导航栏是在手机上查看,如下图所示:

然而,有几个库可以让你在 React 中使用 Bootstrap,为每个 Bootstrap 样式的元素提供等效的 React 组件。 在这个项目中,我们将使用一个这样的库,叫做 reactstrap。 它需要安装 Bootstrap 4 (alpha 6); 因此,我也在项目的启动文件中安装了 Bootstrap 4。

现在,导航到图书代码Chapter06\Starter files目录,并在项目根目录中创建.env文件。 .env文件的值应该与REACT_APP_SERVER_URL的完整代码文件的值相同,REACT_APP_SERVER_URL是服务器在控制台中打印的 URL。

从您的终端的启动文件目录,运行npm install,然后是npm start。 它应该为启动器文件启动开发服务器。 它将打开浏览器,显示消息“应用到这里…”。 打开 VSCode 中的文件夹,查看src/App.js文件。 它应该在render方法中包含该消息。

The starter files will be compiled with a lot of warnings saying no-unused-vars. It is because I have already included the import statements in all the files but none of them are yet used. Therefore, it is telling you that there are a lot of unused variables. Just ignore the warnings.

在你的App.js文件的顶部,你应该看到我已经从 reactstrap 库导入了一些模块。 它们都是 React 组件:

import { Collapse, Navbar, NavbarToggler, Nav, NavItem } from 'reactstrap';

在这里解释每个组件并不重要,因为本章重点是学习 React,而不是设计 React 组件。 因此,要了解 reaction strap,请访问项目主页:https://reactstrap.github.io/

在你的App类中,在App.js文件中,用以下语句替换render方法的return语句:

    return (
      <div className="App">
        <Navbar color="faded" light toggleable>
          <NavbarToggler right onClick={() => {}} />
          <a className="navbar-brand" href="home">Blog</a>
          <Collapse isOpen={false} navbar>
            <Nav className="ml-auto" navbar>
              <NavItem>
                <a className="nav-link" href="home">Home</a>
              </NavItem>
              <NavItem>
                <a className="nav-link" href="authors">Authors</a>
              </NavItem>
              <NavItem>
                <a className="nav-link" href="new-post">New Post</a>
              </NavItem>
            </Nav>
          </Collapse>
        </Navbar>
      </div>
    );

前面的代码将使用 reactstrap 组件,并将为博客创建一个顶部导航条,就像在完成的项目中一样。 在 Chrome 浏览器的响应式设计模式下查看页面,看看它在移动设备上的外观。 在响应式设计模式中,汉堡包菜单不起作用。

这是因为我们还没有创建任何状态和方法来管理导航栏的展开和折叠。 在你的App类中,添加以下构造函数和方法:

constructor(props) {
    super(props);
    this.state = {
      isOpen: false,
    };
    this.toggle = this.toggle.bind(this);
}

toggle() {
    this.setState({
      isOpen: !this.state.isOpen
    });
}

这将添加状态变量,isOpen,用于识别汉堡菜单的打开/关闭状态,而切换方法展开或折叠汉堡菜单通过改变的价值isOpen``truefalse状态。

要在导航栏中绑定这些,使用render方法,执行以下步骤:

  1. <Collapse isOpen={false} navbar>组件所在行的isOpen属性的false值替换为this.state.isOpen。 这一行现在应该如下所示:
 <Collapse isOpen={this.state.isOpen} navbar>
  1. 将包含<NavbarToggler right onClick={()=>{}}``/>行的onClick属性的空函数()=>{}值替换为this.toggle。 这一行现在应该如下所示:
<NavbarToggler right onClick={this.toggle} />

一旦添加这些行并保存文件,导航栏中的汉堡包按钮将在浏览器中正常工作。 但是,单击导航栏中的链接只会重新加载页面。 我们不能在单个页面应用中使用锚标记进行常规导航,因为应用只显示一个页面。 在下一节中,我们将看到如何使用 React Router 库实现页面之间的导航。

使用 React Router 实现路由和导航

React Router 通过显示基于用户在 web 应用中访问的 URL 的组件来实现路由。 React Router 可以在 React.js 和 React Native 中使用。 然而,由于我们只关注 React.js,我们应该使用特定的 React 路由器库react-router-dom,它处理浏览器上的路由和导航。

实现 React Router 的第一步是将整个App组件包裹在react-router-dom<BrowserRouter>组件中。 要包装整个应用,打开 VSCode 中的项目目录中的src/index.js文件。

index.js文件的顶部,添加如下导入语句:

import {BrowserRouter as Router} from 'react-router-dom';

这将导入名称为 router 的BrowserRouter组件。 一旦你添加了 import 语句,用下面的代码替换ReactDOM.render()行:

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

这只是简单地将<App />组件包装在<Router>组件中,这将允许我们在App组件的其他子组件中使用 React Router。

路径文件

在启动器文件中,我在src/routes.js路径中包含了一个routes.js文件。 这个文件以 JSON 对象的形式包含了我们将在博客上使用的所有路由:

const routes = {
  home: '/home',
  authors: '/authors',
  author: '/author/:authorname',
  newPost: '/new-post',
  post: '/post/:id',
};

export default routes;

查看完成的博客应用的主页。URL 将指向'/home'路线。 同样,每个页面都有其各自的路由。 但是,有些路由具有动态值。 例如,如果你在一篇博客文章中点击“阅读更多”,它会把你带到带有 URL 的页面:

http://localhost:3000/post/487929f5-47bc-47af-864a-f570d2523f3e

在这里,URL 的第三部分是文章的 ID。 为了表示这样的 url,我在路由文件中使用了'/post/:id',其中的 ID 意味着 React Router 将理解 ID 将是一个动态值。

你不需要在一个路由文件中管理所有的路由。 我已经创建了一个路由文件,以便您在构建应用时更容易添加路由。

在 app 组件中添加路由

React Router 的功能非常简单; 它只是基于地址栏中的 URL 呈现组件。 它使用历史和位置 Web api 来实现这一目的,但给我们提供了简单、易于使用、基于组件的 api,因此我们可以快速设置路由逻辑。

要在App.js文件的组件之间添加导航,请在<Navbar></Navbar>组件之后的App.js文件的render方法中添加以下代码:

  render() {
    return (
      <div className="App">
        <Navbar color="faded" light toggleable>
          ....
        </Navbar>

        <Route exact path={routes.home} component={Home} />
        <Route exact path={routes.post} component={Post} />
        <Route exact path={routes.authors} component={AuthorList} />
        <Route exact path={routes.author} component={AuthorPosts} />
        <Route exact path={routes.newPost} component={NewPost} />
      </div>
    );
  }

此外,如果您在添加代码文件之后遇到任何问题,请参考已完成的代码文件。 我已经在App.js文件中添加了所有的导入语句。 路由组件从react-router-dom包中导入。 下面是前面的路由组件的作用:

  • 路由组件将检查当前页面的 URL,并呈现与给定路径匹配的组件。 看看下面的路线:
        <Route exact path={routes.home} component={Home} />
  • 当你的 URL 有路径'/home'(路由文件中的routes.home值)时,React Router 会渲染Home组件。
  • 下面是它的每个属性的含义:
    • exact:仅当路径完全匹配时。 这是可选的,如果它没有出现在'/home':它也将为真'/home/otherpaths'。 我们需要精确的匹配; 因此,我把它包括在内。
    • path:必须与 URL 匹配的路径。 在我们的例子中,它是路由文件中的routes.home变量中的'/home'
    • component:当路径与 URL 匹配时必须呈现的组件。

一旦你添加了路由组件,导航回到 Chrome 中的应用。 如果您的应用在http://localhost:3000/中运行,您将只会看到一个空白页。 但是,如果您单击导航栏中的菜单项,您应该看到在页面上呈现的各个组件!

By adding the navigation bar outside the route components, we can easily reuse the same navigation bar across the entire application.

然而,我们应该让我们的应用自动导航到首页'/home',而不是在第一次加载时显示一个空白页面。 要做到这一点,我们应该通过编程将 URL 替换为所需的'/home'路径,就像我们在第 4 章、中使用 WebRTC实时视频呼叫应用中所做的那样,使用历史对象。

但我们有个问题。 React Router 维护自己的历史对象用于导航。 这意味着我们需要修改 React Router 的历史对象。

使用 withRouter 管理历史

React Router 有一个叫做withRouterhigher-order组件,我们可以通过它来传递 React Router 的历史、位置,并将对象作为道具匹配到 React 组件。 要使用withRouter,您应该将App组件作为参数包装在withRouter()内部。 目前,这里是我们如何导出App.js文件的最后一行App组件:

export default App;

你应该把这一行改为:

export default withRouter(App);

这将提供三个道具,historylocation,和match对象到我们的App组件。 对于我们的初始目标,默认显示 home 组件,添加以下componentWillMount()方法到App类:

  componentWillMount() {
    if(this.props.location.pathname === '/') {
      this.props.history.replace(routes.home);
    }
  }

这是前面的代码所做的:

  1. 因为它写在componentWillMount里面,所以它会在App组件渲染之前被执行。
  2. 它将使用location.pathname属性检查 URL 的路径。
  3. 如果路径为'/',即默认的http://localhost:3000/,则自动将历史记录和 URL 替换为http://localhost:3000/home
  4. 这样,当用户导航到网页的根 URL 时,home组件将自动呈现。

现在,在你的浏览器上打开http://localhost:3000/,它将显示主页。 不过,我们还有另一个问题。 每次我们点击导航栏中的链接,都会导致页面重新加载。 因为我们的博客是一个单页面应用,所以应该避免重新加载,因为所有的资产和组件都已经下载了。 每次点击导航时重新加载应用只会导致不必要的多次下载整个应用。

Proptype 验证

当我们向 React 组件传递道具时,建议执行原型验证。 proptype 验证是发生在 React 开发构建中的一种简单的类型检查,用于检查 React 组件是否正确地提供了所有的道具。 如果不是,它将显示一条警告消息,这对调试非常有帮助。

所有可以传递给 React 组件的道具类型都在'prop-types'包中定义,该包与create-react-app一起安装。 你可以看到我在文件的顶部包含了下面的 import 语句:

import PropTypes from 'prop-types';

要对我们的App组件进行 proptype 验证,在App类中,在构造函数之前添加以下静态属性(在顶部声明 proptypes 可以让你很容易地知道 React 组件依赖的是什么道具):

  static propTypes = {
    history: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    match: PropTypes.object.isRequired,
  }

如果您不知道在哪里包含前面的代码片段,请参考已完成的代码文件。 这就是原型验证的工作方式。

考虑前面代码history: PropTypes.object.isRequired的第二行。 这意味着:

  • history应该是App组件的支柱
  • history的类型应该是对象
  • 需要history支柱(isRequired为可选,可为可选道具取下)

For detailed information on proptype validation, refer to the React documentation page at https://facebook.github.io/react/docs/typechecking-with-proptypes.html.

使用 NavLink 实现无缝导航

React Router 有一个完美的解决方案来解决导航过程中的重载问题。 React Router 提供了LinkNavLink组件,您应该使用它们来代替传统的锚标记。 NavLinklink组件有更多的特性,比如当链接是活动的时候指定一个活动的类名。 因此,我们将在应用中使用NavLink

例如,考虑一下我们在App.js文件中用于导航到作者页面的锚标记:

<a className="nav-link" href="authors">Authors</a>

我们可以用 React Router 的NavLink组件替换它,如下所示:

 <NavLink className={'nav-link'} activeClassName={'active'} to={routes.authors}>Authors</NavLink>

以下是 JSX 组件的属性:

  • className:当NavLink在 DOM 中作为锚标记呈现时给元素的类名。
  • activeClassName:当链接是当前活动页面时给元素的类名。
  • to:链接将导航到的路径。

参考已完成代码文件中的App.js文件,将App.js文件中的所有锚标记替换为NavLink组件。 一旦你完成了这个更改,当你点击导航栏中的菜单项时,你的应用将无缝导航,而不会重新加载任何页面。

此外,由于.active类被添加到活动链接,Bootstrap 样式将突出导航栏中的菜单项,当各自的导航栏菜单项是活动的时候,使用稍微深一点的黑色。

我们已经成功地为应用创建了导航栏,并实现了一些基本路由。 从我们的 routes 文件中,你可以看到我们的博客有 5 个页面。 我们将在下一节中构建主页。

博客主页

通过在完整的代码文件中探索应用,您应该已经对博客的主页有了一个想法。 我们的博客有一个简单的主页,列出了所有的文章。 您可以点击文章中的“阅读更多”按钮来详细阅读文章。 由于这个博客是一个学习目的的项目,这个简单的主页就足够了。

理想情况下,您应该从头开始创建每个 React 组件。 然而,为了加快开发过程,我已经为有状态父组件创建了所有的无状态组件和样板文件。 所有组件均可在src/Components目录下使用。 因为 React 组件名应该以大写字母开头,所以我用大写字母创建了所有组件目录名,以表明它们包含 React 组件。 这是Components目录的文件夹结构:

.
├── Author
   ├── AuthorList.js
   └── AuthorPosts.js
├── Common
   ├── ErrorMessage.js
   ├── LoadingIndicator.js
   ├── PostSummary.js
   └── SuccessMessage.js
├── Home
   └── Home.js
├── NewPost
   ├── Components
      └── PostInputField.js
   └── NewPost.js
└── Post
    └── Post.js

我们的博客主页是在src/Components/Home/Home.js文件中显示的Home组件。 目前,Home组件的render方法只呈现Home文本。 我们需要在主页中显示文章列表。 以下是我们将如何实现这一目标:

  1. 服务器有/posts端点,它以数组的形式返回GET请求中的所有 post。 因此,我们可以使用这个 API 来检索文章的数据。
  2. 因为Home是一个有状态组件,所以我们需要维护Home组件中的每个动作的状态。

  3. Home组件从服务器检索数据时,我们应该有一个状态——loading,它应该是一个布尔值,用来显示加载指示符。

  4. 如果网络请求成功,我们应该将帖子存储在一个状态——posts 中,然后可以使用该状态呈现所有博客帖子。
  5. 如果网络请求失败,我们应该简单地使用另一个状态——hasError,它应该是一个布尔值,用来显示错误消息。

让我们开始吧! 首先,在你的Home类中,添加以下构造函数来定义组件的状态变量:

  constructor() {
    super();

    this.state = {
      posts: [],
      loading: false,
      hasError: false,
    };
  }

一旦定义了状态,让我们发出网络请求。 由于网络请求是异步的,我们可以在componentWillMount中使用它,但如果你想要执行同步操作,这将延迟呈现。 最好是加入componentDidMount

为了进行网络请求,我在src/services/api/apiCall.js文件中添加了前面章节中使用过的apiCall服务,并在Home.js文件中添加了 import 语句。 下面是componentWillMount方法的代码:

  componentWillMount() {
    this.setState({loading: true});
    apiCall('posts', {}, 'GET')
    .then(posts => {
      this.setState({posts, loading: false});
    })
    .catch(error => {
      this.setState({hasError: true, loading: false});
      console.error(error);
    });
  }

下面是前一个函数的作用:

  1. 首先,将状态变量 loading 设置为true
  2. 调用apiCall函数进行网络请求。

  3. 由于网络请求是一个异步函数,render方法将被执行,组件将被呈现。

  4. 渲染完成后,网络请求将在 3 秒内完成(我已经在服务器中设置了这么多的延迟)。
  5. 如果apiCall成功,并检索到数据,它将用从服务器返回的帖子数组更新帖子的状态,并将加载状态设置为false
  6. 否则,将设置hasError状态为true,并将加载状态设置为false

为了测试前面的代码,让我们添加呈现 posts 所需的 JSX。 由于 JSX 部分需要大量的代码,我已经在src/Components/Common目录中创建了在这个页面上使用所需的无状态组件,并在Home.js文件的顶部包含了 import 语句。 用以下代码替换render方法的return语句:

    return (
      <div className={`posts-container container`}>
        {
          this.state.loading
          ?
            <LoadingIndicator />
          :
            null
        }
        {
          this.state.hasError
          ?
            <ErrorMessage title={'Error!'} message={'Unable to retrieve posts!'} />
          :
            null
        }
        {
          this.state.posts.map(post => <PostSummary key={post.id} post={post}>Post</PostSummary>)
        }
      </div>
    );

一旦您添加了上述代码片段,请保持服务器运行并访问博客的主页。 它应该列出所有的帖子,如下面的截图所示:

但是,如果你关闭服务器并重新加载页面,它会显示错误信息,如下图所示:

一旦您了解了状态和生命周期方法如何使用 React,实现过程就非常简单。 然而,在本节中,我们仍然需要讨论一个重要的主题,即我之前创建的供您使用的子组件。

使用子组件

让我们看一下ErrorMessage组件,我创建它是为了在我们无法从服务器检索帖子时显示一条错误消息。 ErrorMessage组件是这样包含在render方法中的:

<ErrorMessage title={'Error!'} message={'Unable to retrieve posts!'} />

如果ErrorMessage是通过扩展Component接口创建的有状态组件。 ErrorMessageJSX 元素标题和消息的属性将成为子ErrorMessage组件的道具。 然而,如果你看看ErrorMessage元素的实现,你会发现它是一个无状态功能组件:

const ErrorMessage = ({title, message}) => (
  <div className="alert alert-danger">
    <strong>{title}</strong> {message}
  </div>
);

所以,以下是功能组件的属性如何工作:

  • 由于函数组件不支持状态或道具,属性成为函数调用的参数。 考虑以下 JSX 元素:
<ErrorMessage title={'Error!'} message={'Unable to retrieve posts!'} />
  • 这相当于一个函数调用,它的形参是一个对象:
ErrorMessage({
  title: 'Error!',
  message: 'Unable to retrieve posts!',
})
  • 通过使用你之前学过的解构赋值,你可以像下面这样使用函数中的参数:
const ErrorMessage = ({title, message}) => {}; // title and message retrieved as normal variables
  • 我们也可以对功能组件使用propType验证,但在这里,propTypes用于验证函数的参数。

Whenever you are typing the JSX code in a functional component, make sure you have included the import React from 'react' statement in the file. Otherwise, the Babel compiler will not know how to compile the JSX back to JavaScript.

PostSummary组件带有一个 Read More 按钮,你可以在页面上看到整个帖子的细节。 目前,如果你点击这个链接,它将简单地显示'Post Details'文本。 那么,让我们通过创建帖子详情页面来完成我们的博客主页。

显示文章的细节

博客中的每一篇文章都有一个唯一的 ID。 我们需要使用这个 ID 从服务器检索文章的详细信息。 当你点击 Read More 按钮时,我已经创建了PostSummary组件,它将带你到'/post/:id'路线,其中:id包含帖子的 ID。 这是 post URL 的样子:

http://localhost:3000/post/487929f5-47bc-47af-864a-f570d2523f3e

这里,第三部分是 post ID。 在 VSCode 的src/Components/Post/Post.js路径下打开Post.js文件。 我们需要访问在Post组件的 URL 中存在的 ID。 要访问 URL 参数,我们需要使用 React Router 的 match 对象。 在这个过程中,我们必须将Post组件包装在withRouter()组件中,就像我们对App组件所做的那样。

在您的Post.js文件中,将导出语句更改为以下内容:

export default withRouter(Post);

此外,由于这将为Post组件提供historylocationmatch道具,我们还应该将原型验证添加到Post类中:

  static propTypes = {
    history: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    match: PropTypes.object.isRequired,
  }

我们必须为我们的Post组件创建状态。 状态与Home组分相同; 但是,这里不是 post 状态(数组),而是 post 状态(对象),因为这个页面只需要一个 post。 在Post类中,添加以下构造函数:

  constructor() {
    super();

    this.state = {
      post: {},
      loading: false,
      hasError: false,
    };
  }

在服务器的 swagger 文档中,您应该看到一个 API 端点GET /post/{id},我们将在本章中使用它从服务器检索Post。 我们将在这个组件中使用的componentWillMount方法与之前的Home组件非常相似,只是我们必须从 URL 中检索id参数。 这可以通过下面的代码行来实现:

const postId = this.props.match.params.id;

在这里,this.props.match是 React Router 的withRouter()组件提供给Post组件的道具。 所以,你的componentWillMount方法应该如下所示:

  componentWillMount() {
    this.setState({loading: true});
    const postId = this.props.match.params.id;
    apiCall(`post/${postId}`, {}, 'GET')
    .then(post => {
      this.setState({post, loading: false});
    })
    .catch(error => {
      this.setState({hasError: true, loading: false});
      console.error(error);
    });
  }

最后,在render方法中添加以下代码:

    return(
      <div className={`post-container container`}>
        {
          this.state.loading
          ?
            <LoadingIndicator />
          :
            null
        }
        {
          this.state.hasError
          ?
            <ErrorMessage title={'Error!'} message={`Unable to retrieve post!`} />
          :
            null
        }
        <h2>{this.state.post.title}</h2>
        <p>{this.state.post.author}</p>
        <p>{this.state.post.content}</p>
      </div>
    );

这将创建发布页面。 现在,你应该可以通过点击“阅读更多”按钮来看到整篇文章。 这个页面的工作方式与主页相同。 通过使用可重用组件,您可以看到我们已经将代码最小化了很多。

添加一个新的博客文章

我们已经成功地为我们的博客建立了主页。 下一个任务是构建作者列表页面。 但是,我将把作者列表的构造留给您。 您可以参考已完成的代码文件并构建作者列表页面。 这对你们来说是个很好的练习。

那么,剩下的就是最后一页,也就是新的发布页面。 我们将使用 post /post 的 API 来添加新的博客文章,您可以在 swagger 文档中看到。 post 请求的主体形式如下:

{
  "id": "string",
  "title": "string",
  "content": "string",
  "datetime": "string",
  "author": "string"
}

这里,id是博客文章的唯一 ID,datetime是字符串形式的时间戳。 通常,这两个属性是由服务器生成的,但由于我们只在项目中使用模拟服务器,所以需要在客户端生成它们。

src/Components/NewPost/NewPost.js路径打开NewPost.js文件。 该组件需要三个输入字段:

  • 作者姓名
  • 文章标题
  • 帖子文本

我们需要维护这三个字段的状态。 博客文章将需要textarea,它将动态增加其大小(行)作为博客文章的类型。 因此,我们需要维护行数的状态来管理行数。

除此之外,我们还需要在上一个组件的加载和hasError网络请求中使用的状态。 我们还需要一个状态成功,以指示用户文章已经成功提交。

在你的NewPost类中,用所有必需的状态变量创建constructor,如下所示:

  constructor() {
    super();

    this.state = {
      author: '',
      title: '',
      content: '',
      noOfLines: 0,
      loading: false,
      success: false,
      hasError: false,
    };
  }

与前面的组件不同,我们不仅要显示从服务器检索到的数据,还必须在这个组件中将数据从输入字段发送到服务器。 每当涉及到输入字段时,这意味着我们将需要许多方法来编辑输入字段的状态。

用完整代码文件的NewPost.js文件中的render方法替换NewPost.js文件中的render方法。 因为作者名和标题使用相同的输入字段,所以我为它们创建了一个简单的PostInputField组件。 下面是作者名输入的PostInputField组件:

        <PostInputField
          className={'author-name-input'}
          id={'author'}
          title={'Author Name:'}
          value={this.state.author}
          onChange={this.editAuthorName}
        />

下面是对应的PostInputField函数的样子:

const PostInputField = ({className, title, id, value, onChange}) => (
  <div className={`form-group ${className}`}>
    <label htmlFor={id}>{title}</label>
    <input type="text" className="form-control" id={id} value={value} onChange={onChange}/>
  </div>
); 

你可以看到,我基本上使classNamelabelidvalue,和onChange属性在返回的 JSX 元素动态。 这将让我在同一个表单中为多个输入元素重用整个输入字段。 由于最终呈现的 DOM 元素将具有不同的类和 id,但共享相同的代码,所以您所要做的就是在组件中导入并使用它。 它将节省大量长时间的开发工作,而且在许多情况下,它比您在前一章学到的自定义元素更有效。

让我们看看textarea是如何工作的。

render方法中,你应该看到下面一行,我们正在使用状态变量创建一个noOfLines常量:

 const noOfLines = this.state.noOfLines < 5 ? 5 : this.state.noOfLines;

this.state.noOfLines将包含在博客文章中出现的行数。 使用该方法,如果行数小于5,则将 row 属性的值设置为5。 否则,我们可以将 row 属性增加到 blog 文章中的行数。

这是用于文本输入的 JSX 的样子:

<div className="form-group content-text-area">
  <label htmlFor="content">Post:</label>
  <textarea className="form-control" rows={noOfLines} id="content" value={this.state.content} onChange={this.editContent}></textarea>
</div>

可以看到,rows 属性的值是在render方法中创建的noOfLines常量。 在 textarea 字段之后,我们有以下部分:

  • 加载部分,我们可以根据网络请求状态显示<LoadingIndicator />或提交按钮(this.state.loading)
  • hasError和成功部分,我们可以根据来自服务器的响应显示成功或错误消息

让我们创建输入字段用于更新其值的方法。 在你的NewPost类中,添加以下方法:

  editAuthorName(event) {
    this.setState({author: event.target.value});
  }

  editTitle(event) {
    this.setState({title: event.target.value});
  }

  editContent(event) {
    const linesArray = event.target.value.split('\n');
    this.setState({content: event.target.value, noOfLines: linesArray.length});
  }

这里,editContenttextinput字段使用的方法。 您可以看到,我使用了 split('\n')来根据换行符将行划分为一个数组。 然后,我们可以使用数组的长度来计算文章中的行数。 另外,记住要向构造函数中的所有方法添加this绑定。 否则,从 JSX 调用的方法将不能使用类的this变量:

constructor() {
  ...

  this.editAuthorName = this.editAuthorName.bind(this);
  this.editContent = this.editContent.bind(this);
  this.editTitle = this.editTitle.bind(this);
}

提交后

添加文章部分的最后一部分是提交文章。 这里,我们需要做两件事:为 post 生成 UUID,并以 epoch 时间戳格式获取当前日期和时间:

  • 为了生成使用帖子 ID 的 UUID,我包含了uuid库。 您只需调用uuidv4(),它将返回 UUID 供您使用。
  • 创建日期和时间的epoch时间戳格式,可以使用以下代码:
 const date = new Date();
 const epoch = (date.getTime()/1000).toFixed(0).toString();

JSX 中的 Submit 按钮已经设置为在单击时调用this.submit()方法。 所以,让我们用下面的代码创建AddPost类的submit方法:

  submit() {
    if(this.state.author && this.state.content && this.state.title) {
      this.setState({loading: true});

      const date = new Date();
      const epoch = (date.getTime()/1000).toFixed(0).toString();
      const body = {
        id: uuidv4(),
        author: this.state.author,
        title: this.state.title,
        content: this.state.content,
        datetime: epoch,
      };

      apiCall(`post`, body)
      .then(() => {
        this.setState({
          author: '',
          title: '',
          content: '',
          noOfLines: 0,
          loading: false,
          success: true,
        });
      })
      .catch(error => {
        this.setState({hasError: true, loading: false});
        console.error(error);
      });

    } else {
      alert('Please Fill in all the fields');
    }
  }

另外,将以下代码添加到构造函数中,以便将其与 Submit 按钮绑定:

this.submit = this.submit.bind(this)

这就是前面的 submit 方法所做的:

  1. 它构造了网络请求的主体,这是我们需要添加的 post,然后向 post /post 服务器端点发出请求。
  2. 如果请求成功,它将使用状态变量将输入字段重置为空字符串。
  3. 如果请求失败,它将简单地将hasError状态设置为 true,这将显示一条错误消息。

如果它像预期的那样工作,然后点击 Home,你应该看到你的新文章添加到博客。 恭喜你! 您刚刚使用 React!成功地构建了自己的博客应用。

尝试自己构建作者列表页面,如果在构建过程中遇到任何问题,可以参考已完成的文件来获得帮助。

生成生产构建

在每一章中,我们一直在做的一件事就是生成产品构建。 我们将.env文件中的NODE_ENV变量设置为production,然后在终端中运行npm run webpack。 然而,在本章中,由于我们使用了create-react-app,所以不必担心设置环境变量。 我们只需要在终端中从项目根文件夹运行以下命令:

npm run build

运行此命令后,您就可以在项目的 build 目录中使用已完成的所有优化的产品构建。 用create-react-app生成构建就是这么简单!

一旦生产构建生成,在项目的构建目录中运行http-server,并通过访问浏览器控制台中http-server打印的 URL 来查看应用是如何工作的。

React has a browser extension, which will let you debug the component hierarchy, including the component's state and props. Since we are only working with a basic application in this chapter, we didn't use that tool. However, you can try it out yourself if you are building applications with React at https://github.com/facebook/react-devtools.

总结

本书旨在帮助你理解 React 的基础知识。 因为在本章中我们只构建了一个简单的应用,所以我们没有去探索 React 的许多很酷的功能。 在本章中,你从一个简单的计数器开始,然后在 React 速成课程中构建一个待办事项列表,最后,使用create-react-app工具和一些库(如react-router和 React strap)构建一个简单的博客应用。

作为应用的一个简单的视图层,React 确实需要几个库一起使用,才能让它像一个成熟的框架一样工作。 React 并不是唯一的 JavaScript 框架,但 React 绝对是一个革命性的现代 UI 开发库。

React 和我们刚刚创建的博客应用的一切都很棒,除了博客中的每个页面都要花 3 秒加载。 我们可以通过使用浏览器的 localStorage API 离线存储发布细节并使用它们更新状态来解决这个问题。 但是,我们的应用再次向服务器发出了太多的网络请求,以检索在前面的请求中已经检索到的数据。

在你开始考虑一些复杂的方法来重用数据,同时离线存储它之前,有一件事我们需要在这本书中学习,这是一个新的图书馆,正在采取现代前端开发风暴-Redux