React 初学者教程5 :创建复杂的组件

发布于 大漠

本文转载自:众成翻译 译者:网络埋伏纪事 链接:http://www.zcfy.cc/article/1533 原文:https://www.kirupa.com/react/creating_complex_components.htm

简介:通过学习如何识别和创建依赖于其它组件的组件,从而轻松创建复杂 UI。即根据组件可组合性,来创建复杂的组件。

在前一教程中,我们学习了组件以及组件可以做的很棒的一些事情。我们知道组件是主要的方式,React 允许我们的界面元素像可重用的砖块一样,组件包含自己要运行所需要的所有 HTML、JavaScript 和样式。除了可重用性外,组件还带来另一个主要的优点:允许可组合性。我们可以组合组件来创建更复杂的组件。

在本教程中,我们会具体看看这些到底是个什么意思。更具体地说,我们会看看两件事情:

  • 你需要知道的无聊的技术上的事情
  • 当看到一大块界面元素时如何识别组件这种你需要知道的无聊的事情

OK,我们将要学习的内容实际上并非那么无聊。我是先把你的期望值降低 :P

前进!

一、从界面到组件

迄今为止我们所看到的几个例子是很基础的。对于突出技术概念来说,这几个例子还不错,但是对于要为真实世界做准备来说,它们就一般般了:

在真实世界中,你要用 React 实现的肯定不会是像一个姓名列表,一个彩色元音字母块这么简单了。我们面对的往往是一些复杂用户界面的视觉。这个视觉会采用很多形式 - 比如 涂鸦、简图、屏幕截图、视频、红线等等。由你来决定给所有这些静态像素带来活力,我们会用一些动手操作来实现。

我们要做的是创建一个简单的调色板卡:

调色板卡就是小的矩形卡片,帮助你用特定类型涂料匹配一种颜色。你会在家装市场频繁看到它。你的设计师朋友可能在家里有一个巨大的储藏室专门存放它们。不管怎么样,我们的任务是用 React 重建这些卡片之一。

这里有几种方式处理,但是我打算展示一种最系统化的方法,帮助你简化并搞清楚甚至是最复杂的用户界面。这种方法包含两个步骤:

  • 识别主要的视觉元素
  • 了解组件将是什么

这两个步骤听起来很复杂,但是随着我们学习的推进,你会看到根本不需要担心什么。

1. 识别主要的视觉元素

第一个步骤是识别我们要处理的所有视觉元素。即使是最小的视觉元素也不能省略。识别相关判断的最简单的方式是从最明显的视觉元素开始,然后深入到不那么明显的元素。

在我们的示例中,你会看到的第一件事情是卡片本身:

在卡片内,你会看到两个明显的区域。最上面的区域是显示特定颜色的正方形区。底下的区域是显示十六进制值的白色区。

我们把这两个视觉元素安排到一个树状结构中:

-c-w456

把视觉元素安排到这种树状结构(即视觉层级)中,会让你对视觉元素如何组织有更好的感觉。这个练习的目标是识别重要的视觉元素,并把它们分拆成父/子排列,直到不能再分为止。

注意: 试着忽略实现细节

虽然很难,但是依然不要考虑实现细节。不要把精力放在根据需要什么 HTML 和 CSS 组合来划分视觉元素上。后面有大量时间干这事!

继续,我们可以看到彩色正方形是没法再划分下去的。但是,这并不意味着我们已经干完了。我们可以更进一步将 label 从围绕它的白色区域中划分出来。现在,我们的视觉层级看来是这样子的:

到了这里,我们就没有什么可以进一步划分的了。我们已经完成了对视觉元素的识别和划分,那么下一步就是用我们在这里找到的来帮助我们识别组件。

2. 识别组件

到这里事情才有点意思。我们需要搞清楚我们识别出来的视觉元素中哪些需要变成组件,哪些不需要。并非所有视觉元素都需要变成组件,并且我们肯定是不想只创建一个特别复杂的组件的。这需要有一个权衡:

推算出哪些视觉元素成为一个组件的一部分,那么不是,是个艺术活。通用的规则是我们的组件应该只做一件事情。如果你发现你可能的组件将会做太多事情,可能就要将组件拆分成多个组件。另一方面,如果潜在的组件做的事情太少,可能就要完全略过让这个视觉元素成为一个组件。

我们试试推算出我们示例中哪些元素会走向一个好的组件。从我们的视觉层级来看,马上,卡片和彩色正方形看起来好像都符合成为一个好组件的要求。卡片充当外层容器,彩色正方形只是显示一种颜色。

那只是在我们的 label 和包裹它的白色区域上放一个问号:

这里重要的部分是 label 本身。如果没有它,我们就看不到十六机制值。然后就只剩下白色区域了。它的用途可以忽略不计。它只是一个空白区域,显示一个空白区域责任完全可以用 label 本身来搞定。所以我们的白色矩形区域不会被变成组件。

到这里,我们已经识别出来三个组件,组件层级看起来如下:

这里有一个重要的事情要指出来:组件层次更多是帮助我们定义我们的代码,而不是定义最终产品的外观。你会注意到组件层级跟我们前面看到的视觉层级有点不同。对于视觉细节,你应该总是引用源材料(即你的视觉样稿、屏幕截图以及及其它相关条目)。要推算出要创建哪些组件,你必须用组件层级。

OK,现在我们已经识别出我们的组件以及它们之间的关系,是时候开始将调色板卡变得有活力了。

二、创建组件

这是容易的部分。。。有点!是时候开始写点代码了。第一件事情是创建一个作为起点的近乎为空的 HTML 页面了:

<!DOCTYPE html>
<html>
 
<head>
  <title>More Components!</title>
  <script src="https://fb.me/react-15.0.0-rc.2.js"></script>
  <script src="https://fb.me/react-dom-15.0.0-rc.2.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
 
  <style>
    #container {
      padding: 50px;
      background-color: #FFF;
    }
  </style>
</head>
 
<body>
  <div id="container"></div>
  <script type="text/babel">
 
    ReactDOM.render(
      <div>
         
      </div>,
      document.querySelector("#container")
    );
  </script>
</body>
 
</html>

这些代码只是让 React 把一个空 div 渲染到 container 元素所需的几乎是最低限度的代码了。

这事干完后,我们就要开始定义我们的三个组件了。这三个组件的名字将是 Card、Label 和 Square。继续,在 ReactDOM.render 函数前面添加如下代码:

var Square = React.createClass({
  render: function() {
    return(
 
    );
  }
});
 
var Label = React.createClass({
  render: function() {
    return (
 
    );
  }
});
 
var Card = React.createClass({
  render: function() {
      return (
 
      );
    }
});
 
ReactDOM.render(
  <div>
 
  </div>,
  document.querySelector("#container")
);

除了声明我们的三个组件以外,我们还在 render 函数中扔出:每个组件绝对需要是函数。除此以外,我们的组件是空的。在后面的小节中,我们填充它们。

1. Card 组件

我们打算从组件层次的顶部开始,先锁定 Card 组件。这个组件将充当 Square 和 Label 组件的容器。

要实现这个,继续,作出如下更改:

var Card = React.createClass({
  render: function() {
      var cardStyle = {
        height: 200,
        width: 150,
        padding: 0,
        backgroundColor: "#FFF",
        WebkitFilter: "drop-shadow(0px 0px 5px #666)",
        filter: "drop-shadow(0px 0px 5px #666)"
      };
 
      return (
        <div style={cardStyle}>
 
        </div>
      );
    }
});

虽然这看起来改变了很多,但是这大段代码行就是上篇教程所学的通过 cardStyle 对象格式化我们的 Card 组件的输出。在该对象内,注意到我们指定了一个厂商前缀版本的 CSS filter 属性:WebkitFilter。这个不是啥有趣的细节。有趣的细节是大写。这第一个字母不是按照驼峰命名规则命名为 webkitFilter,第一个 W 是大写的。而其它 CSS 属性不是这样表示的,所以如果需要指定一个厂商前缀属性,你得记住这点。

代码修改的其他部分不咋惹人注目。我们返回了一个 div 元素,该元素的 style 属性被设置为我们的 cardStyle 对象。现在,要看到我们的 Card 组件起作用,我们需要把它作为 ReactDOM.render 函数的一部分,这样在 DOM 中才可以显示它。要让这一切发生,继续,作出如下修改:

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

这里我们做的就是告诉 ReactDOM.render 函数,通过调用 Card 组件,来渲染这个组件的输出。如果一切运行正确,测试应用程序时就会看到如下结果:

是的,它只是我们调色板卡的一个轮廓,但是绝对比几分钟前我们刚开始的时候要多!

2. Square 组件

现在该往组件层级的下一层前进了,我们来看看 Square 组件。这个组件很简单,所以作出如下修改:

var Square = React.createClass({
  render: function() {
    var squareStyle = {
      height: 150,
      backgroundColor: "#FF6663"
    };
    return(
      <div style={squareStyle}>
 
      </div>
    );
  }
});

跟 Card 组件一样,我们返回了一个 div 元素,该元素的 style 属性被设置为一个样式对象,用来定义该组件的外观。要看到 Square 组件起作用,我们需要把它放到 DOM 中,就跟前面我们创建 Card 组件时所做的一样。不同的是,这次我们不会通过 ReactDOM.render 函数调用 Square 组件。而是在 Card 组件内部调用 Square 组件。回到 Card 组件的 render 函数,作出如下修改:

var Card = React.createClass({
  render: function() {
      var cardStyle = {
        height: 200,
        width: 150,
        padding: 0,
        backgroundColor: "#FFF",
        WebkitFilter: "drop-shadow(0px 0px 5px #666)",
        filter: "drop-shadow(0px 0px 5px #666)"
      };
 
      return (
        <div style={cardStyle}>
          <Square/>
        </div>
      );
    }
});

此时,如果预览应用程序,你会看到一个彩色正方形出现了:

这里最酷的是,我们从 Card 组件内部调用 Square 组件。这是组件可组合性的一个示例,这里一个组件依赖于另一个组件的输出。你看到的最终显示是这两个组件相互勾结在一起的结果。

3. Label 组件

剩下来最后一个组件是 Label。继续,作出如下修改:

var Label = React.createClass({
  render: function() {
    var labelStyle = {
      fontFamily: "sans-serif",
      fontWeight: "bold",
      padding: 13,
      margin: 0
    };
 
    return (
      <p style={labelStyle}>#FF6663</p>
    );
  }
});

到这里我们所做的事情也就成了例行公事了。我们有一个 style 对象赋值给返回的元素。返回的是一个 p 元素,其内容是字符串 \#FF6663。要让返回的元素最终跑到 DOM 里面,我们需要在 Card 组件中调用 Label 组件。继续,作出如下修改:

var Card = React.createClass({
  render: function() {
      var cardStyle = {
        height: 200,
        width: 150,
        padding: 0,
        backgroundColor: "#FFF",
        WebkitFilter: "drop-shadow(0px 0px 5px #666)",
        filter: "drop-shadow(0px 0px 5px #666)"
      };
 
      return (
        <div style={cardStyle}>
          <Square/>
          <Label/>
        </div>
      );
    }
});

注意,Label 组件是放在前面添加到 Card 组件的 return 函数中的 Square 组件之下。如果在浏览器中预览,应该会看到如下结果:

现在,调色板卡完成了,并且可见,多亏了 Card、Square 和 Label 组件的努力。但是这并不意味着就完事了。这里还有几个事情要涉及。

4. 再次传递属性!

在当前示例中,我们把 Squar 和 Label 组件用的颜色值硬编码了。这是件奇怪的事情,可能会也可能不会故意地这样做以得到戏剧性的效果,但是修复它是很简单的。只需要指定一个属性名,并且通过 this.props 方法该属性名就可以了。我们在前面已经这样做过了。不同的是这次我们将不得不多次这样做。

这里没有一种方法来正确地在一个父组件上指定一个属性,并让所有后代自动地获得对该属性的访问。有很多不正确的方式来处理这种情况,比如定义一个全局对象、直接在一个组件的属性上设置值,等等。现在我们不会关注这些不正确的解决方案。我们不是动物!

不管怎样,传递一个属性值给子组件的正确方式是,让每个中间父组件也传递到该属性上(the proper way to pass a property value to a child component is to have each intermediate parent component pass on the property as well)。要看这个起作用,我们来看看如下修改过的代码,这里我们移除了硬编码的颜色,用一个 color 属性定义 Card 的颜色:

var Square = React.createClass({
  render: function() {
    var squareStyle = {
      height: 150,
      backgroundColor: this.props.color
    };
    return(
      <div style={squareStyle}>
 
      </div>
    );
  }
});
 
var Label = React.createClass({
  render: function() {
    var labelStyle = {
      fontFamily: "sans-serif",
      fontWeight: "bold",
      padding: 13,
      margin: 0
    };
 
    return (
      <p style={labelStyle}>{this.props.color}</p>
    );
  }
});
 
var Card = React.createClass({
  render: function() {
      var cardStyle = {
        height: 200,
        width: 150,
        padding: 0,
        backgroundColor: "#FFF",
        WebkitFilter: "drop-shadow(0px 0px 5px #666)",
        filter: "drop-shadow(0px 0px 5px #666)"
      };
 
      return (
        <div style={cardStyle}>
          <Square color={this.props.color}/>
          <Label color={this.props.color}/>
        </div>
      );
    }
});
 
ReactDOM.render(
  <div>
    <Card color="#FF6663"/>
  </div>,
  document.querySelector("#container")
);

一旦已经完成了这种代码修改,我们就可以指定任意一个想要的二进制颜色作为调用 Card 组件的一部分:

ReactDOM.render(
  <div>
    <Card color="#FFA737"/>
  </div>,
  document.querySelector("#container")
);

最终的调色板卡就会变成我们指定的颜色了:

现在,我们回到修改了的代码。尽管 color 属性只被 Square 和 Label 组件所用,但是父组件 Card 负责传递该属性给它们。对于更深层次的嵌套,你会需要更多的中间组件负责传递属性。这就变得更糟糕。当你有多个属性想沿着多级组件传递时,你所做的打字(或者复制/粘贴)数量也会增加很多。有方法可以缓解,我们会在后面的教程中更详细地了解这些缓解措施。

三、为什么组件可组合性很棒?

当我们在 React 中埋头苦干时,经常倾向于忘记我们最终创建的只是普通而烦人的 HTML、CSS 和 JavaScript。我们的调色板卡生成的 HTML 看起来是下面这样子:

<div id="container">
  <div data-reactid=".0">
    <div style="height:200px;
                width:150px;
                padding:0;
                background-color:#FFF;
                -webkit-filter:drop-shadow(0px 0px 5px #666);
                filter:drop-shadow(0px 0px 5px #666);" 
         data-reactid=".0.0">
      <div style="height:150px;
                  background-color:#FF6663;" 
           data-reactid=".0.0.0"></div>
      <p style="font-family:sans-serif;
                font-weight:bold;
                padding:13px;
                margin:0;" 
         data-reactid=".0.0.1">#FF6663</p>
    </div>
  </div>
</div>

这标记是不知道它如何到达这里的。它也不知道每个组件负责什么。它不关心组件可组合性或者我们不得不传递从父组件传递 color 属性到子组件这种令人沮丧的方式。这提出了一个重要问题。

如果我们不得不概括组件要做什么的最终结果,它们所做的一切就是返回一团 HTML 给要调用它的任何东西。每个组件的 render 函数都会返回一些 HTML 给另一个组件的 render 函数。所有这些 HTML 都一直积攒着,直到一大块 HTML 被推送(很高效地)到我们的 DOM。这种简单是为什么组件重用和可组合性工作得如此好的原因。每个 HTML 块独立于其它 HTML 块 - 特别是如果你按 React 推荐的方式指定行内样式。这允许你很容易从其它视觉元素创建视觉元素,而不必担心任何事情。任何事情!难道这不是超级棒吗?

总结

到这里你也许已经认识到,我们正慢慢地将焦点转向 React 蓬勃发展的更高级场景上。实际上,高级并非是正确的词汇。正确的词汇是现实的(realistic)。

在本教程中,我们最开始是学习如何查看 UI 片段,然后用一种稍后可以实现的方式,来识别组件。这是一种你会发现自己总是处在的环境。虽然我们采用的方法看起来正式,但是随着你对在 React 中创建组件越来越有经验,你可以减少一下手续。如果在不创建视觉和组件层级的情况就能快速识别组件以及它们的父子关系,那么这就是你变得真正擅长 React 的一个标志。

识别组件只是方程式的一个部分。另一个部分是让这些组件活起来。我们在这里所看到的大多数技术活只是对较早时候我们已经看到的一个轻微扩展。在前面的教程中,我们处理的是一级组件,而这里处理的是多级组件。在前面的教程中,我们看到的是如何在一个父组件和一个子组件之间传递属性,在这里我们看到的是如何在多个父组件和多个子组件之间传递属性。也许在将来的教程中,我们会做一些开创性的事情,比如在屏幕上画多个调色板卡。或者,也许可以指定两个属性,而不是单一一个。谁知道呢?

React初学者系列教程