w3ctech

React 初学者教程 3: React 中的组件

组件是让 React 变得美好的事情之一,它是定义人们在使用应用程序时所看到的视觉和交互的主要方式之一。假如我们要完成的 app 是下面这个样子:

这是完成了的界面。在开发期间,从 React 项目的角度看,就不那么动人了。这个应用的视觉的几乎每一个部分都被封装在在一个称为组件的自包含模块内。这里为了突出 “几乎每一个” 的含义,我们来看看如下的图:

每个虚线代表一个独立的组件,该组件负责视觉界面以及交互。不要让这吓到你。虽然它看起来很复杂,但是很快你会看到,一旦我们有机会用组件的方式开发,一切就会开始变得很有意义。

函数的快速回顾

在 JavaScript 中,我们都知道函数,它让代码更整洁,可重用性更高。那么这里我们为什么要花时间来回顾函数呢?原因是:函数从概念上与 React 组件有很多共同的理念。理解组件的最简单方式是先回顾一下函数。

在没有函数的可怕年代,你可能有如下的代码:

var speed = 10;
var time = 5;
alert(speed * time);

var speed1 = 85;
var time1 = 1.5;
alert(speed1 * time1);

var speed2 = 12;
var time2 = 9;
alert(speed2 * time2);

var speed3 = 42;
var time3 = 21;
alert(speed3 * time3);

而有了函数,你可以把所有重复的代码浓缩到如下的代码中:

function getDistance(speed, time) {
    var result = speed * time;
    alert(result);
}

现在 getDistance 函数删除掉所有重复的代码,用 speedtime 为参数,定制计算,并返回。

要调用这个函数,我们要做的只是:

getDistance(10, 5);
getDistance(85, 1.5);
getDistance(12, 9);
getDistance(42, 21);

这是不是看起来更好一点呢?现在,这里有另一个函数提供的伟大的价值。函数可以调用其它函数,作为运行的一部分。这里我们用一个 formatDistance 函数修改 getDistance 的返回值:

function formatDistance(distance) {
  return distance + " km";
}

function getDistance(speed, time) {
    var result = speed * time;
    alert(formatDistance(result));
}

函数调用其它函数这种能力,让我们可以干净地分离函数的功能。你不需要用一个巨大的函数完成所有事情,可以把功能分解为很多函数,每个函数完成一件特定类型的任务。

更好的是,在修改函数的功能后,不会影响函数的调用。如果函数签名没有改名,那么任何对该函数的调用都不需要修改。例如,已有的 getDistance 调用会看到 formatDistance 函数的结果,即使在第一次定义该调用时,formatDistance 函数不存在。这是相当棒的。

总而言之,函数是很棒的,你知,我知。这就是为什么我们写的所有代码到处都是函数的原因。

改变处理 UI 的方式

任何人都不会否认函数带来的好处。函数确实可以让我们组织应用程序代码的方式更健全。编写代码和编写 UI 的关注点是不太一样的。由于技术上和非技术上的原因,在编写 UI 时,我们总是会默许某种程度的无条理。

这个声明可能会带来一些争议,所以我们用一些示例来突出我要表达的意思。回去看看上一章中所用的 render 方法:

var destination = document.querySelector("#container");

ReactDOM.render(
  <h1>Batman</h1>,
  destination
);

在屏幕上我们会看到单词 Batman 被用大大的字符打印出来,这是因为浏览器采用 h1 默认的样式输出这个单词。现在我们对 render 方法做点修改,让应用打印出其他几个超级英雄的姓名:

var destination = document.querySelector("#container");

ReactDOM.render(
  <div>
    <h1>Batman</h1>
    <h1>Iron Man</h1>
    <h1>Nicolas Cage</h1>
    <h1>Mega Man</h1>
  </div>,
  destination
);

这里注意,我们用一个 div 元素包含四个 h1 元素。

JSX 问题:输出多个元素**

这里要呼出一个重要的 JSX 细节。包含 h1 元素的 div 应该去掉,是因为这看起来像个好主意。但是它在这里,是因为它必须在这里。在 React 中,你不能像下面这样输出多个相邻的元素:

var destination = document.querySelector("#container");
ReactDOM.render(
  <h1>Batman</h1>
  <h1>Iron Man</h1>
  <h1>Nicolas Cage</h1>
  <h1>Mega Man</h1>,
  destination
);

即使这是一个有效的 HTML,但是在 JSX 和 JavaScript 之间的邪恶联盟眼里,它是无效的。这貌似是一个可怕的限制,但是应变方式很简单。虽然我们只能输入一个元素,但是一个元素可以有很多子节点。这就是为什么我们把 h1 元素放在 div 中的原因。我们这样做,是因为 JSX 如何转换为 JavaScript。细节我们在后面会看到,但是此时我们不应该在学习组件时分心。

OK,现在我们有了四个 h1 元素,每个元素包含了一个英雄的名字。如果我们要把 h1 元素变为 h3 会是发生什么?我们可以手动修改这些元素如下:

var destination = document.querySelector("#container");

ReactDOM.render(
  <div>
    <h3>Batman</h3>
    <h3>Iron Man</h3>
    <h3>Nicolas Cage</h3>
    <h3>Mega Man</h3>
  </div>,
  destination
);

如果预览一下,你会看到没有样式的普通文本:

我们不想在这里就为样式发疯。这里我们只是想让所有姓名变成斜体字,所以将代码修改如下:

var destination = document.querySelector("#container");

ReactDOM.render(
  <div>
    <h3><i>Batman</i></h3>
    <h3><i>Iron Man</i></h3>
    <h3><i>Nicolas Cage</i></h3>
    <h3><i>Mega Man</i></h3>
  </div>,
  destination
);

我们修改每个 h3 元素,用标记 i 将内容包起来。这里你开始看到了问题么?我们在写 UI 时干的事情与写如下代码时干的事情没有什么不同:

var speed = 10;
var time = 5;
alert(speed * time);

var speed1 = 85;
var time1 = 1.5;
alert(speed1 * time1);

var speed2 = 12;
var time2 = 9;
alert(speed2 * time2);

var speed3 = 42;
var time3 = 21;
alert(speed3 * time3);

每次修改,我们就要重复对每个 h1 或者 h3 元素做同样的事情。如果我们要的事情比只修改元素的外观更复杂该怎么办?如果我们想表示比我们这个简单示例更复杂的东西该怎么办?我们正在做的是不可扩展的,手动修改每个我们想修改的副本是耗时而且无聊的。

现在,有一个疯狂的想法:如果把函数里面很棒的功能以某种方法应用到应用程序的 UI 定义上会怎么样?会不会解决我们在本节突出的所有效率低下问题呢?事实证明,对这个会怎么样的回答组成了所有关于 React 的核心。是时候来迎接组件了。

迎接 React 组件

上面所有问题的解决方案都可以在 React 组件中找到。React 组件是通过 JSX 输出 HTML 元素的可重用的 JavaScript 代码块。这听起来很平淡无奇,但是随着我们开始创建组件,并逐渐加大复杂性,你会看到组件是真的很强大,并且就像我宣称的那样棒。

OK,我们开始创建几个组件。首先创建一个空的 React 文档:

<!DOCTYPE html>
<html>

<head>
  <title>React Components</title>
  <script src="https://unpkg.com/react@15.3.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.3.2/dist/react-dom.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
</head>

<body>
  <div id="container"></div>
  <script type="text/babel">

  </script>
</body>

</html>

创建一个 “Hello, World!” 组件

我们要开始做的很简单,就是用一个组件帮助我们在屏幕上打印出著名的 “Hello, World!“。我们已经知道,只用 ReactDOMrender 方法就可以做到:

ReactDOM.render(
  <div>
    <p>Hello, world!</p>
  </div>,
  document.querySelector("#container")
);

现在我们用组件重建一个。在 React 中创建组件的方式有几种,但是最开始我们创建组件的方式是用 React.createClass。继续,在 render 方法前添加如下代码:

var HelloWorld = React.createClass({

});

ReactDOM.render(
  <div>
    <p>Hello, world!</p>
  </div>,
  document.querySelector("#container")
);

这里我们创建了一个叫做 HelloWorld 的新组件。现在这个 HelloWorld 组件还没有做任何事。实际上,此时它只是一个空 JavaScript 对象。在该对象内,我们可以放入各种属性来进一步定义 HelloWorld 要做的事。你定义的有些属性是特殊的, 被 React 用来帮助组件发挥其魔力。其中一个强制性的属性是 render

继续,按照如下代码,修改 HelloWorld 组件,给它添加一个 render 属性:

var HelloWorld = React.createClass({
  render: function() {

  }
});

与我们之前在 ReactDOM.render 中看到的 render 方法一样,一个组件中的 render 方法也是负责处理 JSX。我们来修改 render 方法,让它返回 “你好,组件化的世界!”:

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>你好,组件化的世界!</p>
    );
  }
});

我们添加的代码是告诉 render 方法返回代表 “你好,组件化的世界!” 文本的 JSX。剩下的就是如何用这个组件。一个组件定义后,使用该组件的方法就是调用它。下面,我们打算在我们的老朋友 ReactDOM.render 方法中调用它:

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>你好,组件化的世界!</p>
    );
  }
});

ReactDOM.render(
  <p>你好,组件化的世界!</p>,
  document.querySelector("#container")
);

调用组件的方式有点独特。继续,将 ReactDOM.render 方法的第一个参数用如下代码替换:

ReactDOM.render(
  <HelloWorld/>,
  document.querySelector("#container")
);

这里不是打错字!我们用来调用 HelloWorld 组件的 JSX 是很像 HTML 的。如果在浏览器中预览你的页面,你会看到 “你好,组件化的世界!” 出现在屏幕上。如果你因为悬念而摒住呼吸,现在可以放松一下了。

如果在看到用来调用 HelloWorld 的语法后,你无法放松,那就盯着下面的圆一会儿:

好吧,回到现实。我们所做的看起来很疯狂,但是只需要把组件当作是一个酷和新的 HTML 标记就可以了,它的功能是你完全可以控制的。也就是说我们可以把它像 HTML 一样用。

例如,继续,将 ReactDOM.render 方法的代码修改为如下:

ReactDOM.render(
  <div>
    <HelloWorld/>
  </div>,
  document.querySelector("#container")
);

这里我们把对 HelloWorld 组件的调用包裹进一个 div 元素中,如果在浏览器中预览,依然可以运行。下面我们再进一步!修改代码,在 ReactDOM.render 方法中多次调用 HelloWorld 组件:

ReactDOM.render(
  <div>
    <HelloWorld/>
    <HelloWorld/>
    <HelloWorld/>
    <HelloWorld/>
    <HelloWorld/>
    <HelloWorld/>
  </div>,
  document.querySelector("#container")
);

预览一下,你会看到一堆“你好,组件化的世界!” 出现。现在,在转向更酷的事情之前,我们再来做点事情。回到 HelloWorld 组件声明,将返回的文本修改为更传统的 Hello, world! 值:

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>Hello, world!</p>
    );
  }
});

就做这一处修改,然后预览一下。此时,前面我们指定的所有 HelloWorld 调用现在都返回 “Hello, world!“ 到屏幕上。我们再也不需要手动修改每一个 HelloWorld 调用。这是件好事!

指定属性

现在,我们的组件只做一件事,打印 Hello, world! 到屏幕上。这与如下的 JavaScript 函数做的事情类似:

function getDistance() {
  alert("42km");
}

除了一个非常特殊的情况,就是这个 JavaScript 函数看起来并没有多大用处。要让这个函数更有用,方法是修改它,让它带有参数:

function getDistance(speed, time) {
    var result = speed * time;
    alert(result);
}

现在,你的函数更通用,可以用在各种条件下,而不仅仅是输出 42km

类似的事情也适用于组件。像函数一样,你可以传进参数来更改组件要做的事情。当然,在术语上,还是有点点不同。在函数的世界中我们称为参数,在组件世界里我们称为属性。现在我们来看看属性是如何起作用的。

我们打算修改一下 HelloWorld 组件,让你可以指定欢迎什么或者欢迎谁。比如,假设能指定 Bono 作为 HelloWorld 调用的一部分,并且看到 Hello, Bono! 出现在屏幕上。

要给组件添加属性,有两个部分需要注意:

第一部分:修改组件定义

现在,HelloWorld 组件是硬编码的,总是发送 Hello, world! 为返回值。我们要做的第一件事情就是改变这种行为,让组件返回的是通过一个属性传递进来的值。我们要给这个属性一个名字,对于本例,我们打算把我们的属性叫做 greetTarget

要指定 greetTarget 作为组件的一部分,我们需要对代码作出如下修改:

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>Hello, {this.props.greetTarget}!</p>
    );
  }
});

访问一个属性的方式,是通过每个组件都可以访问的 this.props 属性来调用它。注意我们指定这个属性的方式:我们把它放在一个大括号中。在 JSX 中,如果你想把一些事情当作表达式来计算,就必须将它放在大括号中。如果不这样做,那么就会看到原始文本 “this.props.greetTarget” 被打印出来了。

第二部分:修改组件调用

在更新组件定义后,剩下的就是传递属性值为组件调用的一部分。实现方式是在组件调用中添加一个同名属性后跟要传递进来的值。在我们的示例中,就是用 greetTarget 属性和想给它的值来修改 HelloWorld 调用。

继续,修改 HelloWorld 调用为如下:

ReactDOM.render(
  <div>
    <HelloWorld greetTarget="Batman"/>
    <HelloWorld greetTarget="Iron Man"/>
    <HelloWorld greetTarget="Nicolas Cage"/>
    <HelloWorld greetTarget="Mega Man"/>
    <HelloWorld greetTarget="Bono"/>
    <HelloWorld greetTarget="Catwoman"/>
  </div>,
  document.querySelector("#container")
);

现在每个 HelloWorld 调用都有 greetTarget 属性,该属性的属性值为我们想欢迎的超级英雄的名字。如果在浏览器中预览这个示例,你会看到问候语快乐地打印在屏幕上。

在继续下一步之前,最后一件事情要提醒一下。一个组件上不限制只有一个属性。你想要几个属性就可以有几个,this.props 属性可以轻易容纳任意属性,而不会有任何麻烦。

处理子元素

在前面,我提到在 JSX 中我们的组件与正规 HTML 元素很相似。在我们将一个组件放在 div 元素内,或者指定一个属性和值作为给定属性的一部分的时候,我们自己都可以看得出来。还有一件事情是组件可以有的,就像很多 HTML 元素一样:组件可以有子元素。

意思是,我们可以像这样做:

<CleverComponent foo="bar">
  <p>Something!</p>
</CleverComponent>

我们有一个 CleverComponent 组件,它有一个 p 元素作为子元素。从 CleverComponent 内部,通过 this.props.children,你可以访问 子元素 p 以及该组件所有的任何子元素。

为了搞清楚我说的意思,我们来写另一个简单的示例。此时,我们有一个组件 Buttonify,该组件在一个按钮内包含了一个子元素。组件的代码如下:

var Buttonify = React.createClass({
  render: function() {
    return (
      <div>
        <button type={this.props.behavior}>{this.props.children}</button>
      </div>
    );
  }
});

使用该组件的方式是,通过在 ReactDOM.render 方法中按照如下方式调用它:

ReactDOM.render(
  <div>
    <Buttonify behavior="Submit">SEND DATA</Buttonify>
  </div>,
  document.querySelector("#container")
);

在这段代码运行时,根据 Buttonify 组件的 render 方法的 JSX,我们所看到的将是单词 SEND DATA 包在一个 button 元素内。如果加上合适的样式,结果会是下面这样:

无论如何,回到 JSX,可以看到我们指定了一个自定义属性叫做 behavior。该属性允许我们指定 button 元素的 type 属性,其后可以看到我们在该组件定义的 render 方法中,通过 this.props.behavior 来访问它。

访问组件的子元素有更多东西。例如,如果子元素是一个深层嵌套结构的根,那么 this.props.children 属性返回的是一个数组。如果子元素只是一个元素(像本例),那么 this.props.children 属性返回的是没有包在一个数组中的单个组件。还有更多东西要提出来,但是与其先列出各种情况来烦你,还不如自然在后面我们精心安排的示例中自然而然地涉及。

总结

如果你想用 React 创建应用,如果不用组件你不会走的太远。不用组件来创建 React 应用,就像不用函数来创建基于 JavaScript 的应用程序一样。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复