组件的演变
- 狭义上的组件,又称为 UI 组件,比如 Tabs 组件、Dropdown 组件。组件主要围绕在交互动作上的抽象,针对这些交互动作,利用 JavaScript 操作 DOM 结构或 style 样式来控制。
- 广义上的组件,即带有业务含义和数据的 UI 组件组合。这类组件不仅有交互动作,更重 要的是有数据与界面之间的交互。然而,这类组件往往有较大的争议。在规模较大的场 景下,我们更倾向于采用分层的思想去处理。
-
以常用的 Tabs 组件为例,对于 UI 组件来说,一定会有 3 个部分组件:结构、样式和交互行 为,分别对应着 HTML、CSS 和 JavaScript。
- 我们会先构建组件的基本结构:
<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>- 定义组件的样式:
$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; } }- 最后是交互行为。我们引入 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;
- 初始化
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 组件的构建
- React 的本质就是关心元素的构成, React 组件即为组件元素。组件元素被描述成纯粹的 JSON 对象,意味着可以使用方法或是类来 构建。
- React 组件基本上由 3 个部分组成——属性(props)、状态(state)以及生命周期方法。

-
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 还是可以通过向方法设置静态属性来实现的。
-
用 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;