React初學者教程5:創建復雜的組件

簡介:通過學習如何識別和創建依賴於其它組件的組件,從而輕松創建復雜 UI。即根據組件可組合性,來創建復雜的組件。

在前一教程中,我們學習瞭組件以及組件可以做的很棒的一些事情。我們知道組件是主要的方式,React 允許我們的界面元素像可重用的磚塊一樣,組件包含自己要運行所需要的所有 HTML、JavaScript 和樣式。除瞭可重用性外,組件還帶來另一個主要的優點:允許可組合性。我們可以組合組件來創建更復雜的組件。

在本教程中,我們會具體看看這些到底是個什麼意思。更具體地說,我們會看看兩件事情:

你需要知道的無聊的技術上的事情 當看到一大塊界面元素時如何識別組件這種你需要知道的無聊的事情

OK,我們將要學習的內容實際上並非那麼無聊。我是先把你的期望值降低 😛

前進!

一、從界面到組件

迄今為止我們所看到的幾個例子是很基礎的。對於突出技術概念來說,這幾個例子還不錯,但是對於要為真實世界做準備來說,它們就一般般瞭:

在真實世界中,你要用 React 實現的肯定不會是像一個姓名列表,一個彩色元音字母塊這麼簡單瞭。我們面對的往往是一些復雜用戶界面的視覺。這個視覺會采用很多形式 – 比如 塗鴉、簡圖、屏幕截圖、視頻、紅線等等。由你來決定給所有這些靜態像素帶來活力,我們會用一些動手操作來實現。

我們要做的是創建一個簡單的調色板卡:

調色板卡就是小的矩形卡片,幫助你用特定類型塗料匹配一種顏色。你會在傢裝市場頻繁看到它。你的設計師朋友可能在傢裡有一個巨大的儲藏室專門存放它們。不管怎麼樣,我們的任務是用 React 重建這些卡片之一。

這裡有幾種方式處理,但是我打算展示一種最系統化的方法,幫助你簡化並搞清楚甚至是最復雜的用戶界面。這種方法包含兩個步驟:

識別主要的視覺元素 瞭解組件將是什麼

這兩個步驟聽起來很復雜,但是隨著我們學習的推進,你會看到根本不需要擔心什麼。

1. 識別主要的視覺元素

第一個步驟是識別我們要處理的所有視覺元素。即使是最小的視覺元素也不能省略。識別相關判斷的最簡單的方式是從最明顯的視覺元素開始,然後深入到不那麼明顯的元素。

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

在卡片內,你會看到兩個明顯的區域。最上面的區域是顯示特定顏色的正方形區。底下的區域是顯示十六進制值的白色區。

我們把這兩個視覺元素安排到一個樹狀結構中:

把視覺元素安排到這種樹狀結構(即視覺層級)中,會讓你對視覺元素如何組織有更好的感覺。這個練習的目標是識別重要的視覺元素,並把它們分拆成父/子排列,直到不能再分為止。

註意: 試著忽略實現細節
雖然很難,但是依然不要考慮實現細節。不要把精力放在根據需要什麼 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>
  <p id="container"></p>
  <script type="text/babel">

    ReactDOM.render(
      <p>

      </p>,
      document.querySelector("#container")
    );
  </script>
</body>

</html>

這些代碼隻是讓 React 把一個空 p 渲染到 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(
  <p>

  </p>,
  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 (
        <p style={cardStyle}>

        </p>
      );
    }
});

雖然這看起來改變瞭很多,但是這大段代碼行就是上篇教程所學的通過 cardStyle 對象格式化我們的 Card 組件的輸出。在該對象內,註意到我們指定瞭一個廠商前綴版本的 CSS filter 屬性:WebkitFilter。這個不是啥有趣的細節。有趣的細節是大寫。這第一個字母不是按照駝峰命名規則命名為 webkitFilter,第一個 W 是大寫的。而其它 CSS 屬性不是這樣表示的,所以如果需要指定一個廠商前綴屬性,你得記住這點。

代碼修改的其他部分不咋惹人註目。我們返回瞭一個 p 元素,該元素的 style 屬性被設置為我們的 cardStyle 對象。現在,要看到我們的 Card 組件起作用,我們需要把它作為 ReactDOM.render 函數的一部分,這樣在 DOM 中才可以顯示它。要讓這一切發生,繼續,作出如下修改:

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

這裡我們做的就是告訴 ReactDOM.render 函數,通過調用 Card 組件,來渲染這個組件的輸出。如果一切運行正確,測試應用程序時就會看到如下結果:

是的,它隻是我們調色板卡的一個輪廓,但是絕對比幾分鐘前我們剛開始的時候要多!

2. Square 組件

現在該往組件層級的下一層前進瞭,我們來看看 Square 組件。這個組件很簡單,所以作出如下修改:

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

      </p>
    );
  }
});

跟 Card 組件一樣,我們返回瞭一個 p 元素,該元素的 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 (
        <p style={cardStyle}>
          <Square/>
        </p>
      );
    }
});

此時,如果預覽應用程序,你會看到一個彩色正方形出現瞭:

這裡最酷的是,我們從 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 (
        <p style={cardStyle}>
          <Square/>
          <Label/>
        </p>
      );
    }
});

註意,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(
      <p style={squareStyle}>

      </p>
    );
  }
});

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 (
        <p style={cardStyle}>
          <Square color={this.props.color}/>
          <Label color={this.props.color}/>
        </p>
      );
    }
});

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

一旦已經完成瞭這種代碼修改,我們就可以指定任意一個想要的二進制顏色作為調用 Card 組件的一部分:

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

最終的調色板卡就會變成我們指定的顏色瞭:

現在,我們回到修改瞭的代碼。盡管 color 屬性隻被 Square 和 Label 組件所用,但是父組件 Card 負責傳遞該屬性給它們。對於更深層次的嵌套,你會需要更多的中間組件負責傳遞屬性。這就變得更糟糕。當你有多個屬性想沿著多級組件傳遞時,你所做的打字(或者復制/粘貼)數量也會增加很多。有方法可以緩解,我們會在後面的教程中更詳細地瞭解這些緩解措施。


三、為什麼組件可組合性很棒?

當我們在 React 中埋頭苦幹時,經常傾向於忘記我們最終創建的隻是普通而煩人的 HTML、CSS 和 JavaScript。我們的調色板卡生成的 HTML 看起來是下面這樣子:

<p id="container">
  <p data-reactid=".0">
    <p 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">
      <p style="height:150px;
                  background-color:#FF6663;" 
           data-reactid=".0.0.0"></p>
      <p style="font-family:sans-serif;
                font-weight:bold;
                padding:13px;
                margin:0;" 
         data-reactid=".0.0.1">#FF6663</p>
    </p>
  </p>
</p>

這標記是不知道它如何到達這裡的。它也不知道每個組件負責什麼。它不關心組件可組合性或者我們不得不傳遞從父組件傳遞 color 屬性到子組件這種令人沮喪的方式。這提出瞭一個重要問題。

如果我們不得不概括組件要做什麼的最終結果,它們所做的一切就是返回一團 HTML 給要調用它的任何東西。每個組件的 render 函數都會返回一些 HTML 給另一個組件的 render 函數。所有這些 HTML 都一直積攢著,直到一大塊 HTML 被推送(很高效地)到我們的 DOM。這種簡單是為什麼組件重用和可組合性工作得如此好的原因。每個 HTML 塊獨立於其它 HTML 塊 – 特別是如果你按 React 推薦的方式指定行內樣式。這允許你很容易從其它視覺元素創建視覺元素,而不必擔心任何事情。任何事情!難道這不是超級棒嗎?

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *