天天看點

antd + react model自定義footer_React系列六 - 父子元件通信

一. 認識元件的嵌套

元件之間存在嵌套關系:

  • 在之前的案例中,我們隻是建立了一個元件App;
  • 如果我們一個應用程式将所有的邏輯都放在一個元件中,那麼這個元件就會變得非常的臃腫和難以維護;
  • 是以元件化的核心思想應該是對元件進行拆分,拆分成一個個小的元件;
  • 再将這些元件組合嵌套在一起,最終形成我們的應用程式;

我們來分析一下下面代碼的嵌套邏輯:

import React, { Component } from 'react';

function Header() {
  return <h2>Header</h2>
}

function Main() {
  return (
    <div>
      <Banner/>
      <ProductList/>
    </div>
  )
}

function Banner() {
  return <div>Banner</div>
}

function ProductList() {
  return (
    <ul>
      <li>商品1</li>
      <li>商品2</li>
      <li>商品3</li>
      <li>商品4</li>
      <li>商品5</li>
    </ul>
  )
}

function Footer() {
  return <h2>Footer</h2>
}

export default class App extends Component {
  render() {
    return (
      <div>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }
}
           

上面的嵌套邏輯如下,它們存在如下關系:

  • App元件是Header、Main、Footer元件的父元件;
  • Main元件是Banner、ProductList元件的父元件;
antd + react model自定義footer_React系列六 - 父子元件通信

自定義元件的嵌套邏輯

在開發過程中,我們會經常遇到需要元件之間互相進行通信:

  • 比如App可能使用了多個Header,每個地方的Header展示的内容不同,那麼我們就需要使用者傳遞給Header一些資料,讓其進行展示;
  • 又比如我們在Main中一次性請求了Banner資料和ProductList資料,那麼就需要傳遞給他們來進行展示;
  • 也可能是子元件中發生了事件,需要由父元件來完成某些操作,那就需要子元件向父元件傳遞事件;

總之,在一個React項目中,元件之間的通信是非常重要的環節;

父元件在展示子元件,可能會傳遞一些資料給子元件:

  • 父元件通過 屬性=值 的形式來傳遞給子元件資料;
  • 子元件通過 props 參數擷取父元件傳遞過來的資料;

二. 父元件傳遞子元件

2.1. 子元件是class元件

我們這裡先示範的元件是class元件:

import React, { Component } from 'react';

// 1.類子元件
class ChildCpn1 extends Component {
  constructor(props) {
    super();
    this.props = props;
  }

  render() {
    const { name, age, height } = this.props;

    return (
      <div>
        <h2>我是class的元件</h2>
        <p>展示父元件傳遞過來的資料: {name + " " + age + " " + height}</p>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn1 name="why" age="18" height="1.88" />
      </div>
    )
  }
}
           

按照上面的結構,我們每一個子元件都需要寫構造器來完成:this.props = props;

其實呢,大可不必,因為我們可以調用super(props),我們來看一下Component的源碼:

  • 這裡我們先不關心context、updater;
  • 我們發現傳入的props會被Component設定到this中(父類的對象),那麼人類就可以繼承過來;
  • 補充一個思考題:為什麼子類可以繼承過來呢?
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
           

是以我們的構造方法可以換成下面的寫法:

constructor(props) {
    super(props);
  }
           

甚至我們可以省略,為什麼可以省略呢?

如果不指定構造方法,則使用預設構造函數。對于基類,預設構造函數是

constructor() {}
           

對于派生類,預設構造函數是:

constructor(...args) {
  super(...args);
}
           

2.2. 子元件是function元件

我們再來演練一下,如果子元件是一個function元件:

function ChildCpn2(props) {
  const {name, age, height} = props;

  return (
    <div>
      <h2>我是function的元件</h2>
      <p>展示父元件傳遞過來的資料: {name + " " + age + " " + height}</p>
    </div>
  )
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn1 name="why" age="18" height="1.88"/>
        <ChildCpn2 name="kobe" age="30" height="1.98"/>
      </div>
    )
  }
}
           

functional元件相對來說比較簡單,因為不需要有構造方法,也不需要有this的問題。

2.3. 參數驗證propTypes

對于傳遞給子元件的資料,有時候我們可能希望進行驗證,特别是對于大型項目來說:

  • 當然,如果你項目中預設繼承了Flow或者TypeScript,那麼直接就可以進行類型驗證;
  • 但是,即使我們沒有使用Flow或者TypeScript,也可以通過 prop-types 庫來進行參數驗證;

從 React v15.5 開始,React.PropTypes 已移入另一個包中:prop-types 庫

我們對之前的class元件進行驗證:

ChildCpn1.propTypes = {
  name: PropTypes.string,
  age: PropTypes.number,
  height: PropTypes.number
}
           

這個時候,控制台就會報警告:

antd + react model自定義footer_React系列六 - 父子元件通信

類型驗證警告

<ChildCpn1 name="why" age={18} height={1.88}/>
           

更多的驗證方式,可以參考官網:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

  • 比如驗證數組,并且數組中包含哪些元素;
  • 比如驗證對象,并且對象中包含哪些key以及value是什麼類型;
  • 比如某個原生是必須的,使用 requiredFunc: PropTypes.func.isRequired
如果沒有傳遞,我們希望有預設值呢?
  • 我們使用defaultProps就可以了
ChildCpn1.defaultProps = {
  name: "王小波",
  age: 40,
  height: 1.92
}
           

三. 子元件傳遞父元件

某些情況,我們也需要子元件向父元件傳遞消息:

  • 在vue中是通過自定義事件來完成的;
  • 在React中同樣是通過props傳遞消息,隻是讓父元件給子元件傳遞一個回調函數,在子元件中調用這個函數即可;

我們這裡來完成一個案例:

  • 将計數器案例進行拆解;
  • 将按鈕封裝到子元件中:CounterButton;
  • CounterButton發生點選事件,将内容傳遞到父元件中,修改counter的值;
antd + react model自定義footer_React系列六 - 父子元件通信

Counter案例

案例代碼如下:

import React, { Component } from 'react';

function CounterButton(props) {
  const { operator, btnClick } = props;
  return <button onClick={btnClick}>{operator}</button>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  changeCounter(count) {
    this.setState({
      counter: this.state.counter + count
    })
  }

  render() {
    return (
      <div>
        <h2>目前計數: {this.state.counter}</h2>
        <CounterButton operator="+1" btnClick={e => this.changeCounter(1)} />
        <CounterButton operator="-1" btnClick={e => this.changeCounter(-1)} />
      </div>
    )
  }
}
           

四. 元件通信案例練習

我們來做一個相對綜合的練習:

antd + react model自定義footer_React系列六 - 父子元件通信

TabControl案例練習

index.js代碼:

import React from "react";
import ReactDOM from 'react-dom';
import "./style.css";

import App from './App';

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

App.js

import React, { Component } from 'react';

import TabControl from './TabControl';

export default class App extends Component {
  constructor(props) {
    super(props);

    this.titles = ["流行", "新款", "精選"];

    this.state = {
      currentTitle: "流行"
    }
  }

  itemClick(index) {
    this.setState({
      currentTitle: this.titles[index]
    })
  }

  render() {
    return (
      <div>
        <TabControl titles={this.titles} itemClick={index => this.itemClick(index)} />
        <h2>{this.state.currentTitle}</h2>
      </div>
    )
  }
}
           

TabControl.js

import React, { Component } from 'react'

export default class TabControl extends Component {
  constructor(props) {
    super(props);

    this.state = {
      currentIndex: 0
    }
  }

  render() {
    const {titles} = this.props;
    const {currentIndex} = this.state;

    return (
      <div className="tab-control">
        {
          titles.map((item, index) => {
            return (
              <div className="tab-item" onClick={e => this.itemClick(index)}>
                <span className={"title " + (index === currentIndex ? "active": "")}>{item}</span>
              </div>
            )
          })
        }
      </div>
    )
  }

  itemClick(index) {
    this.setState({
      currentIndex: index
    });
    this.props.itemClick(index);
  }
}
           

style.css

.tab-control {
  height: 40px;
  line-height: 40px;
  display: flex;
}

.tab-control .tab-item {
  flex: 1;
  text-align: center;
}

.tab-control .title {
  padding: 3px 5px;
}

.tab-control .title.active {
  color: red;
  border-bottom: 3px solid red;
}
           

五. React插槽實作

5.1. 為什麼使用插槽?

在開發中,我們抽取了一個元件,但是為了讓這個元件具備更強的通用性,我們不能将元件中的内容限制為固定的div、span等等這些元素。

我們應該讓使用者可以決定某一塊區域到底存放什麼内容。

舉個例子:假如我們定制一個通用的導航元件 - NavBar

  • 這個元件分成三塊區域:左邊-中間-右邊,每塊區域的内容是不固定;
  • 左邊區域可能顯示一個菜單圖示,也可能顯示一個傳回按鈕,可能什麼都不顯示;
  • 中間區域可能顯示一個搜尋框,也可能是一個清單,也可能是一個标題,等等;
  • 右邊可能是一個文字,也可能是一個圖示,也可能什麼都不顯示;
antd + react model自定義footer_React系列六 - 父子元件通信

京東導航

這種需求在Vue當中有一個固定的做法是通過slot來完成的,React呢?

  • React對于這種需要插槽的情況非常靈活;
  • 有兩種方案可以實作:children和props;

我這裡先提前給出NavBar的樣式:

.nav-bar {
  display: flex;
  height: 44px;
  line-height: 44px;
  text-align: center;
}

.nav-bar .left, .nav-bar .right {
  width: 80px;
  background: red;
}

.nav-bar .center {
  flex: 1;
  background: blue;
}
           

5.2. children實作

每個元件都可以擷取到 props.children:它包含元件的開始标簽和結束标簽之間的内容。

比如:

<Welcome>Hello world!</Welcome>
           

在 Welcome 元件中擷取 props.children,就可以得到字元串 Hello world!:

function Welcome(props) {
  return <p>{props.children}</p>;
}
           

當然,我們之前看過props.children的源碼:

  • 如果隻有一個元素,那麼children指向該元素;
  • 如果有多個元素,那麼children指向的是數組,數組中包含多個元素;

那麼,我們的NavBar可以進行如下的實作:

import React, { Component } from 'react';

class NavBar extends Component {
  render() {
    return (
      <div className="nav-bar">
        <div className="item left">{this.props.children[0]}</div>
        <div className="item center">{this.props.children[1]}</div>
        <div className="item right">{this.props.children[2]}</div>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <div>傳回</div>
          <div>購物街</div>
          <div>更多</div>
        </NavBar>
      </div>
    )
  }
}
           

5.3. props實作

通過children實作的方案雖然可行,但是有一個弊端:通過索引值擷取傳入的元素很容易出錯,不能精準的擷取傳入的原生;

另外一個種方案就是使用 props 實作:

  • 通過具體的屬性名,可以讓我們在傳入和擷取時更加的精準;
import React, { Component } from 'react';

class NavBar extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props;

    return (
      <div className="nav-bar">
        <div className="item left">{leftSlot}</div>
        <div className="item center">{centerSlot}</div>
        <div className="item right">{rightSlot}</div>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    const navLeft = <div>傳回</div>;
    const navCenter = <div>購物街</div>;
    const navRight = <div>更多</div>;

    return (
      <div>
        <NavBar leftSlot={navLeft} centerSlot={navCenter} rightSlot={navRight} />
      </div>
    )
  }
}
           

我是 @半糖學前端 ,專注前端技術領域分享,關注我和志同道合的人在一起,更容易進步!

源自: coderwhy