传统的组件
传统组件是结构,样式和交互分离的,分别对应html,css和js,以一个常见的tab组件为例,我们会先构建组件的基本结构
<div>
<ul>
<li>Tab1</li>
<li>Tab2</li>
<li>Tab3</li>
</ul>
</div>
<div class="content">
<div>
tab1内容...
</div>
<div>
tab2内容...
</div>
<div>
tab3内容...
</div>
</div>
然后通过一个js的tab组件操作dom
class Tab {
static defaultOptions = {
classPrefix: 'tabs',
activeIndex: 0,
};
}
constructor(options) {
this.options = Object.assign(Tabs.defaultOptions, options);
// 从options中定义组件属性
this.element = this.options...
// 各种dom操作事件
this._initElement();
this._initTabs();
this._bindTabs();
}
_initElement() { ... }
_initTabs() { ... }
_bindTabs() { ... }
destroy() { ... }
初始化过程十分简单,传入几个参数就可以赋予交互
const tab = new Tabs({
element: ...,
tabs: ...
})
组件封装的基本思路就是面向对象思想,交互基本上以dom为主,逻辑上是结构(html)上需要操作哪里,我们就操作哪里
- 基本的封装性 通过原型继承来实现了组件的封装
- 简单的生命周期 最明显的两个方法constructor和destroy,代表了组件的挂载和卸载过程,但其他的过程,如组件更新的生命周期并没有体现
- 明确的数据流动 这里的数据指的是调用组件的参数,一旦确定参数的值,就会解析传进来的参数,根据参数的不同作出不同的响应,然后反映到视图上
传统组件的主要问题是逻辑一旦复杂,就存在大量的DOM操作,开发和维护成本很高。后面前端又出现了MVC的架构,View只关心怎么输出变量,于是诞生了各种模板语言,让模板本身能承载逻辑,减轻了在js中操作DOM的逻辑,不过这种组件化实现的还是字符串拼接级别的组件化。
React的组件化
Web Component通过自定义元素的方式实现组件化,React的组件元素被描述成纯粹的JSON对象,由三部分组成——属性(props),状态(state)以及生命周期方法。
React组件构建方法
React组件有三种构建方法
- React.createClass
这种方法兼容性最好
const Button = React.createClass({
getDefaultProps(){
return {
color: 'blue',
text: ''
}
},
render(){
const { color, text } = this.props;
return (
// 虚拟节点
<button className = {'btn-${color}'}>
<em>text</em>
</button>
)
}
})
当另一个组件需要调用Button,就和new一个对象差不多,只需要写< Button />就会被解析成React.createElement(Button)方法来创建Button实例,这意味者在应用中调用几次Button,就会创建几次Button实例
- ES6 classes
ES6 classes的写法是通过ES6标准的类语法的方式来构建方法:
import React, { Component } from 'react';
class Button extends Component {
constructor(props){
super(props);
}
static defaultProps = {
color: 'blue',
text: ''
}
render(){
return (
<button className={btn-${color}}>
<em>{text}</em>
</button>
)
}
}
如果我们学过面向对象的知识,就知道继承与组合的不同,他们可以用IS-A和HAS-A来区别,在实际应用React的过程中,我们极少让子类去继承功能组件。试想在UI层面小的修改就会影响到整体交互或样式,用继承来抽象太死板了。所以在React组件开发中,常用的方式是将组件拆分到合理的粒度,用组合的方式合成业务组件。
- stateless function
使用无状态函数构建的组件称为无状态组件,只传入了props和context两个参数,也就是说它不存在state,也没有生命周期方法,无状态组件不像上述两种方法在调用时会创建新实例。
function Button({ color = 'blue', text = 'Confirm'}){
return (
<button className={ btn-$color }>
<em>{text}</em>
</button>
)
}
用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>
}
}
state
在使用React之前,常见的MVC框架也非常容易实现交互界面的状态管理,比如Backbone。它们将View中与界面交互的状态解耦,一般将状态放在Model中管理。当组件内部使用库内置的setState方法时,最大的表现行为是该组件会尝试重新渲染。
值得注意的是,setState是一个异步方法,一个生命周期内所有的setState方法会合并操作。
我们再来看Tabs组件的state,我们需要维护两个可能的内部状态activeIndex和prevIndex,它们分别代表当前选中tab的索引和前一次tab选中的索引。针对这点我们有两个不同的视角
- activeIndex在内部更新 当我们切换标签的时候,可以看作组件内部的交互行为,被选择后通过回调函数返回具体选择 的索引。
- activeIndex在外部更新 当我们切换tab标签时候,可以看作是组件外部在传入具体的索引,而组件就像木偶一样被操控着。
这两种情形在React组件的设计中非常常见,第一种组件写法叫做智能组件(smart component)和木偶组件(dumb component)
我们来看下Tabs组件中初始化时的实现部分
constructor(props){
super(props);
const currProps = this.props;
let activeIndex = 0;
// 来源核心判断
if('activeIndex' in currProps){
activeIndex = currProps.activeIndex
} else if('defaultActiveIndex' in currProps){
activeIndex = currProps.defaultActiveIndex;
}
this.state = {
activeIndex,
prevIndex: activeIndex
}
}
对于activeIndex来说,既可能来源于使用内部更新的defaultActiveIndex prop,即我们不需要外组件控制组件状态,也可能来源于需要外部更新的activeIndex prop(比如一个input选择框)
props
props是React用来让组件互相联系的一种机制,通俗说就像方法的参数一样。React的单项数据流,主要的流动管道就是props。props本身是不可变的,当我们试图改变props的原始值时,React会报出类型错误的警告,组件的props一定来自于默认属性或通过父组件传递而来,如果要渲染对props加工后的值,最简单的方法就是使用局部变量(在组件中定义的)或者直接在JSX中计算结果。
再一次仔细观察Tabs组件在Web界面的特征,会看到两个区域:切换区域和内容区域,那么我们就定义两个子组件,其中TabNav组件对应切换区域,TabContent组件对应内容区域。在Tabs组件中只显示定义内容区域的子组件集合,头部区域对应内部区域每一个TabPane组件的props,让其在TabNav组件内拼装
<Tabs classfix={'tabs'} defaultActiveIndex={0}>
<TabPane key={0} tab={'Tab 1'}>第一个Tab里的内容</TabPane>
<TabPane key={1} tab={'Tab 2'}>第二个Tab里的内容</TabPane>
<TabPane key={2} tab={'Tab 3'}>第三个Tab里的内容</TabPane>
</Tabs>
基本的结构确定之后,只有两个props放在Tabs组件上,而其他参数直接放到TabPane组件中,由它的父组件TabContent隐式对TabPane组件拼装。渲染TabPane组件的方法如下:
getTabPanes(){
const { classPrefix, activeIndex, panels, isActive } = this.props;
return React.children.map(panels, (child)=>{
if(!child) { return; }
//将字符串转成10进制数字
const order = parseInt(child.props.order, 10);
const isActive = activeIndex === order;
return React.cloneElement(child, {
classPrefix,
isActive,
children: child.props.children,
key: 'tabpane-${order}',
})
})
}
上述代码讲述了子组件集合是怎么渲染的,通过React.Children.map方法遍历子组件,将order(渲染顺序),isActive(是否激活tab),children(Tabs组件中传入的children)和key利用React的cloneElement方法克隆到TabPane组件中,最后返回这个TabPane组件集合。
其中React.children是React官方提供的一系列children的方法,就像js中提供给数组的方法一样。
最后,TabContent组件的render方法只需要调用getTabPanes方法即可渲染。
在TabPane组件上,除了可以传递字符串,还可以直接传入DOM节点
<TabPane
order="0"
tab={<span><i className=""></i></span>}
第一个Tab里的内容
</TabPane>