react重要基础概念---02组件

296 阅读4分钟

组件的演变

  1. 狭义上的组件,又称为 UI 组件,比如 Tabs 组件、Dropdown 组件。组件主要围绕在交互动作上的抽象,针对这些交互动作,利用 JavaScript 操作 DOM 结构或 style 样式来控制。
  2. 广义上的组件,即带有业务含义和数据的 UI 组件组合。这类组件不仅有交互动作,更重 要的是有数据与界面之间的交互。然而,这类组件往往有较大的争议。在规模较大的场 景下,我们更倾向于采用分层的思想去处理。
  • 以常用的 Tabs 组件为例,对于 UI 组件来说,一定会有 3 个部分组件:结构、样式和交互行 为,分别对应着 HTML、CSS 和 JavaScript。

    1. 我们会先构建组件的基本结构:
    <div id="tab-demo">
      <div class="tabs-bar" role="tablist">
          <ul class="tabs-nav">
              <li role="tab" class="tabs-tab">Tab 1</li>
              <li role="tab" class="tabs-tab">Tab 2</li>
              <li role="tab" class="tabs-tab">Tab 3</li>
          </ul>
      </div>
      <div class="tabs-content">
          <div role="tabpanel" class="tabs-panel">
              第一个 Tab 里的内容
          </div>
          <div role="tabpanel" class="tabs-panel">
              第二个 Tab 里的内容
          </div>
          <div role="tabpanel" class="tabs-panel">
              第三个 Tab 里的内容
          </div>
      </div>
    </div>
    
    1. 定义组件的样式:
       $class-prefix: "tabs";
      .#{$class-prefix} {
          &-bar {
              margin-bottom: 16px;
          }
          &-nav {
              font-size: 14px;
              &:after,
              &:before {
                  display: table;
                  content: " ";
              }
              &:after {
                  clear: both;
              }
          }
          &-nav > &-tab {
              float: left;
              list-style: none;
              margin-right: 24px;
              padding: 8px 20px;
              text-decoration: none;
              color: #666;
              cursor: pointer;
          }
          &-nav > &-active {
              border-bottom: 2px solid #00C49F;
              color: #00C49F;
              cursor: default;
          }
          &-content &-panel {
              display: none;
          }
          &-content &-active {
              display: block;
          }
      }
    
    1. 最后是交互行为。我们引入 jQuery 方便操作 DOM,使用 ES6 classes 语法糖来替换早期利用 原型构建面向对象的方法,以及使用 ES6 modules 替换 AMD 模块加载机制:
    import $ from 'jquery';
    import EventEmitter from 'events';
    const Selector = (classPrefix) = >({
        PREFIX: classPrefix,
        NAV: `$ {
            classPrefix
        } - nav`,
        CONTENT: `$ {
            classPrefix
        } - content`,
        TAB: `$ {
            classPrefix
        } - tab`,
        PANEL: `$ {
            classPrefix
        } - panel`,
        ACTIVE: `$ {
            classPrefix
        } - active`,
        DISABLE: `$ {
            classPrefix
        } - disable`,
    });
    class Tabs {
        static defaultOptions = {
            classPrefix: 'tabs',
            activeIndex: 0,
        };
        constructor(options) {
            this.options = $.extend({},
            Tabs.defaultOptions, options);
            this.element = $(this.options.element);
            this.fromIndex = this.options.activeIndex;
            this.events = new EventEmitter();
            this.selector = Selector(this.options.classPrefix);
            this._initElement();
            this._initTabs();
            this._initPanels();
            this._bindTabs();
            if (this.options.activeIndex !== undefined) {
                this.switchTo(this.options.activeIndex);
            }
        }
        _initElement() {
            this.element.addClass(this.selector.PREFIX);
            this.tabs = $(this.options.tabs);
            this.panels = $(this.options.panels);
            this.nav = $(this.options.nav);
            this.content = $(this.options.content);
            this.length = this.tabs.length;
        }
        _initTabs() {
            this.nav && this.nav.addClass(this.selector.NAV);
            this.tabs.addClass(this.selector.TAB).each((index, tab) = >{
                $(tab).data('value', index);
            });
        }
        _initPanels() {
            this.content.addClass(this.selector.CONTENT);
            this.panels.addClass(this.selector.PANEL);
        }
        _bindTabs() {
            this.tabs.click((e) = >{
                const $el = $(e.target);
                if (!$el.hasClass(this.selector.DISABLE)) {
                    this.switchTo($el.data('value'));
                }
            });
        }
        events(name) {
            return this.events;
        }
        switchTo(toIndex) {
            this._switchTo(toIndex);
        }
        _switchTo(toIndex) {
            const fromIndex = this.fromIndex;
            const panelInfo = this._getPanelInfo(toIndex);
            this._switchTabs(toIndex);
            this._switchPanel(panelInfo);
            this.events.emit('change', {
                toIndex,
                fromIndex
            });
            this.fromIndex = toIndex;
        }
        _switchTabs(toIndex) {
            const tabs = this.tabs;
            const fromIndex = this.fromIndex;
            if (tabs.length < 1) return;
            tabs.eq(fromIndex).removeClass(this.selector.ACTIVE).attr('aria-selected', false);
            tabs.eq(toIndex).addClass(this.selector.ACTIVE).attr('aria-selected', true);
        }
        _switchPanel(panelInfo) {
            panelInfo.fromPanels.attr('aria-hidden', true).hide();
            panelInfo.toPanels.attr('aria-hidden', false).show();
        }
        _getPanelInfo(toIndex) {
            const panels = this.panels;
            const fromIndex = this.fromIndex;
            let fromPanels,
            toPanels;
            if (fromIndex > -1) {
                fromPanels = this.panels.slice(fromIndex, (fromIndex + 1));
            }
            toPanels = this.panels.slice(toIndex, (toIndex + 1));
            return {
                toIndex: toIndex,
                fromIndex: fromIndex,
                toPanels: $(toPanels),
                fromPanels: $(fromPanels),
            };
        }
        destroy() {
            this.events.removeAllListeners();
        }
    }
    export defaults Tabs;
    
  1. 初始化
  const tab = new Tabs({
      element: '#tab-demo',
      tabs: '#tab-demo .tabs-nav li',
      panels: '#tab-demo .tabs-content div',
      activeIndex: 1,
  });
  tab.events.on('change', (o) = >{
      console.log(o);
  });

我们看到,组件封装的基本思路就是面向对象思想。交互基本上以操作 DOM 为主,逻辑上 是结构上哪里需要变,我们就操作哪里。此外,对于 JavaScript 的结构,我们得到了几项规范标 准组件的信息。

  • 基本的封装性。尽管说 JavaScript 没有真正面向对象的方法,但我们还是可以通过实例化 的方法来制造对象。
  • 简单的生命周期呈现。最明显的两个方法 constructor 和 destroy ,代表了组件的挂载和 卸载过程。但除此之外,其他过程(如更新时的生命周期)并没有体现。
  • 明确的数据流动。这里的数据指的是调用组件的参数。一旦确定参数的值,就会解析传 进来的参数,根据参数的不同作出不同的响应,从而得到渲染结果。

下面来看看react组件的构建:

React 组件的构建

  1. React 的本质就是关心元素的构成, React 组件即为组件元素。组件元素被描述成纯粹的 JSON 对象,意味着可以使用方法或是类来 构建。
  2. React 组件基本上由 3 个部分组成——属性(props)、状态(state)以及生命周期方法。

  1. React 组件的构建方法

    官方在 React 组件构建上提供了 3 种不同的方法: React.createClass 、ES6 classes 和无状态 函数(stateless function)。

  • React.createClass
const Button = React.createClass({
    getDefaultProps() {
        return {
            color: 'blue',
            text: 'Confirm',
        };
    },
    render() {
        const {color,text} = this.props;
        return ( 
        < button className = {`btn btn - $ {color}`} > 
            <em> {text} </em>
        </button > );
    }
});
  • ES6 classes
import React,{Component} from 'react';
class Button extends Component {
    constructor(props) {
        super(props);
    }
    static defaultProps = {
        color: 'blue',
        text: 'Confirm',
    };
    render() {
        const {
            color,
            text
        } = this.props;
        return ( 
        < button className = {`btn btn - $ {color}`} >
            <em > {text} < /em>
        </button > );
    }
}
  • 无状态函数
function Button({
    color = 'blue',
    text = 'Confirm'
}) {
    return (
        < button className = {`btn btn - $ {color}`} >
            <em > {text} < /em>
        </button > );
}

无状态组件只传入 props 和 context 两个参数;也就是说,它不存在 state ,也没有生命周 期方法,组件本身即上面两种 React 组件构建方法中的 render 方法。不过,像 propTypes 和 defaultProps 还是可以通过向方法设置静态属性来实现的。

  1. 用 React 实现 Tabs 组件

    用 ES6 classes 的 写法来初始化 Tabs 组件的“骨架”:

import React, { Component, PropTypes } from 'react';
class Tabs extends Component {
    constructor(props) {
        super(props);
    }
    // ... 
    render() {
        return <div className="ui-tabs"></div>;
    }
}; 
export defaults Tabs;