联系人列表(续)
前一节已经讲述了表头和列表的组件应用,但组件列表项的简单应用并不能满足我们的需求。本节将继续深入探讨联系人列表的细节实现
自定义列表项
本来列表项并不需要自定义,只需要在 <A.ListItem>
中加入合适的组件就行,比如
var items = data.map(function(t) {
return (
<A.ListItem>
<A.Icon icon="user" />
{t.name}
<A.Icon icon="phone" />
</A.ListItem>
);
});
然而为了将列表项抽象出来,以及便于对列表项的细节调整和后期可能需要实现的操作,还是把它定义成一个组件的好,这个组件就叫 Pserson
,只需要把原来 map()
中的代码拷贝到 render()
的 return
中,再把 map()
中的 return
稍稍改一下
var Person = React.createClass({
render: function() {
return (
<A.ListItem>
<A.Icon icon="user" />
{t.name}
<A.Icon icon="phone" />
</A.ListItem>
);
}
});
var items = data.map(function(t) {
return <Person />
});
很明显,上面的代码是有问题的:
- Person 组件中的
t
变量没有赋值,所以t.name
一定会抛异常; return <Person />
看起来没有问题,但是并没有传入t
值,所以列表的每一项都会是一模一样的
于是这里遇到了问题:怎么从父级控件向子级控件传入参数?
从父级控件向子级控件传入参数
这里可以作个比喻:从父级控件调用子级控件,就像在某个函数中调用其它函数一样。那么传入参数也就像调用函数时传入的参数一样。
React 通过 props
向子级控件传入参数,在 JSX 语法中,写法和 XML 的属性定义类似。比如向 Person 控件传入 name
和 tel
参数——应该叫属性更准确,就可以这样
var items = data.map(function(t) {
return <Person name={t.name} tel={t.tel} />
});
注意到属性值是用的花括号
{}
包起来的,这表示传入的是表达式,需要先计算其结果。如果是已知的字符串,不需要计算的,可以像 XML 属性那样用使用引号。
然后,在 Person 组件中,通过 this.props
对象可以使用传入的属性值,比如 this.props.name
就引用了传入的 name
属性。同时,为了稍稍解决一点显示仍然不够美观的问题,这里可以用 AMUIReact.Badge
组件包装一下第 2 个 Icon。
正确的 Person 组件定义如下
var Person = React.createClass({
render: function() {
return (
<A.ListItem>
<A.Icon icon="user" />
{this.props.name}
<A.Badge amStyle="success" radius>
<A.Icon icon="phone" />
</A.Badge>
</A.ListItem>
);
}
});
对了,这里 {this.props.name}
是引用了输入的 name
属性,而传入的 tel
属性暂时还没使用。同时在使用 Badge 组件的时候,也向其传入了 amStyle
和 radius
两个属性……等等,radius
是属性?
正确,radius
是属性,作为布尔值属性,是可以省略值,这时其值会被当作 true
。这当然不符合 XML 的语法,不过这不是问题,因为这是 JSX 不是 XML。当然对于有强迫症的朋友,也可以显示的指定布尔值:radius={true}
或者 radius={false}
。
千万注意
{true}
和{false}
是用花括号而不是引号包起来的。当然如果用"true"
也不会有问题,但是用"false"
就有问题了——因为在 JavaScript 中"false"
是“真”值(不懂原因的找度娘)!
添加拨号功能
说起来,添加拨号功能真不难——只需要提供一个到 "tel:电话号码
的链接就可以了。上例的 Badge 中,使用一个 <a>
标签包裹 Icon 组件即可,只不过第一次尝试通常都不怎么顺利:
<A.Badge amStyle="success" radius={false}>
<a href="tel:{this.props.tel}">
<A.Icon icon="phone" />
</a>
</A.Badge>
上例中 {this.props.tel}
并没有被计算出来,直接作为 URI 字符串的一部分了。好吧,JSX 解释器不认识字符串中的 {}
表达式,所以只好换个写法
<A.Badge amStyle="success" radius={false}>
<a href={"tel:" + this.props.tel}>
<A.Icon icon="phone" />
</a>
</A.Badge>
由于个人洁癖,最终把 {"tel:" + this.props.tel}
先赋值给一个变量再在 JSX 中引用,所以最终的 Person 组件定义如下
var Person = React.createClass({
render: function() {
var link = "tel:" + this.props.tel;
return (
<A.ListItem>
<A.Icon icon="user" />
{this.props.name}
<A.Badge amStyle="success" radius>
<a href={link}>
<A.Icon icon="phone" />
</a>
</A.Badge>
</A.ListItem>
);
}
});
有没有注意到,link
变量定义和赋值都在 return
之前——因为,一定记住,return
后面的是个表达式,不能写语句!
Spread Attributes (传播属性)
“传播属性”这个词翻译得很别扭,所以我宁愿用“Spread 属性”。
在 Page 的 render()
中,可以发现 t
的两个属性 name
和 tel
都被传递给了 Person 组件对象。还好这里只需要传递 2 个属性,如果需要传递的属性是十个八个的,光写属性传递就得累死。
React 当然不会想不到这个问题,所以 JSX 提供了“Spread 属性”语法。只需要简单的使用 {...t}
就可以将 t
的所有属性拷贝到组件的 props 中。因此,map()
部分可以简化为
var items = this.state.persons.map(function(t) {
return <Person {...t} />
});
Spread 属性很容易让人联想到 ES2015(ES6)中的“可变参数”(或称“不定参数”)语法——那么在不支持 ESA015 的浏览器中是不是就不能使用 Spread 属性了呢?——当然不会,因为 Spread 属性是 JSX 提供的语法,则 React 解释,而不是由 JavaScript 引擎解释。
效果
现在的效果已经有点像样了,但仍然需要改进。不过如之前所述,这个可以通过样式表来解决,待功能完成得差不多了再来调整。
Ajax 加载数据
到目前为止,数据仍然是以硬代码的形式写在 index.jsx 中的,这是一个同步过程。虽然目前这么做没有问题,但是如果数据需要保存在数据库中,而从数据库获取数据像 AJAX 一样是一个异步过程(到目前状态,是同步还是异步并不清楚,这涉及到 Corodva 对数据库的操作,暂未研究)就麻烦了。因此,现在先把数据独立出来保存在 /js/data.json
中,通过 AJAX 方式先研究一下异步加载数据。
说实在的,为了研究这个问题费了不少脑筋,最后还是在 React 文档中得到了答案(参阅:Load Initial Data vi AJAX)。解决这个问题涉及到了 React 组件数据的另一种保存形式:state,以及 render
之外的两个组件生命周期方法 getInitalState()
和 `componentDidMount()。
到目前为止,一共只写了两个组件:Page 和 Person。很显示,加载整个列表数据的任务应该落在 Page 上。依葫芦画瓢,先把功能实现了再说
var Page = React.createClass({
// [1]
getInitialState: function() {
return {
persons: []
}
},
// [2]
componentDidMount: function() {
$.getJSON("/js/data.json").then(function(data) {
if (this.isMounted()) {
this.setState({
persons: data
});
}
}.bind(this));
},
render: function() {
// [3]
var items = this.state.persons.map(function(t) {
return <Person {...t} />
});
return (<div>
<A.Header title="通讯录" />
<A.List>
{items}
</A.List>
</div>);
}
});
注意 Page 组件中 3 个地方的变化,
- 1,添加了
getInitialState()
方法 - 2,添加了
componentDidMount()
方法 - [3],修改了
map()
的数据源。原来的data
已经不存在了,取而代之的是this.state.persons
。
除此之外还有几点需要注意
$.getJSON().then()
的回调函数中,直接使用了this.isMounted()
和this.setState()
等。函数中的this
怎么还会是组件对象呢?——请注意回调函数后的.bind(this)
。这个方法在 React 的各方实例中经常出现,不失为传递this
的一个好办法。
this.isMounted()
的作用是判断当前组件仍然处于 mounted 状态,只有在这个状态下 setState 才有意义。虽然在componentDidMount
事件中写的这段代码,但是由于是异步加载,所以并不知道当前组件是否已经有所变化。
- 如果在 render 中写上日志,可以发现,它在 componentDidMount 之前和之后都有执行。很显然,在之前执行的那一次,
this.state.persons
是不存在的。如果没有getIntialState()
,会发现第1次 render 的时候连this.state
都还不存在(不过是null
不是undefined
)。由此证明在使用了React 组件状态数据的情况下,从getInitalSate()
返回初始的状态对象是非常有必要的。
使用样式表美化
现在联系人列表的功能部分已经基本完成,是时候美化一下了。通过浏览器的 Inspect 功能可以看到列表部分的 HTML 渲染出来是这样的(只保留了一个 <li>
示例)
<ul class="am-list">
<li class="">
<i icon="user" class="am-icon-user"></i>
<span>张三</span>
<span class="am-badge am-badge-success am-radius">
<a href="tel:13801234567">
<i icon="phone" class="am-icon-phone"></i>
</a>
</span>
</li>
</ul>
现在需要美化的事项包括
- 去掉 ul 的 margin-top
- 给 li 加上 padding
- 在第 1 个 icon 后加上 margin-right
所以在 index.css
中删除原来的内容,改为如下内容
ul {
margin-top: 0;
}
li {
padding: 3px 6px;
}
li i:first-child {
margin-right: 8px;
}
li span:last-child {
margin-top: 4px;
}
最后一句是在调试的时候发现电话图标不在正中才加的。
用 className 来定位样式元素
前面的 CSS 最大的问题是选择器不够精准,样式表内容多了之后容易发生各种冲突。在 HTML 中比较好的解决办法是添加 class="xxx"
属性。但是在 React 中添加 class="xxx"
属性,会被认为是 props 数据。React 中是用 className
来表示样式类的。
所以,需要将原来的代码稍做变动,加上适当的 className
属性
- 在 Page 组件中为
<A.List>
添加person-list
类
var Page = React.createClass({
// ...... 省略代码若干
render: function() {
var items = this.state.persons.map(function(t) {
return <Person {...t} />
});
return (<div>
<A.Header title="通讯录" />
<A.List className="person-list">
{items}
</A.List>
</div>);
}
});
- 在 Person 组件中分别添加
person
、person-icon
、person-phone
类
var Person = React.createClass({
render: function() {
var link = "tel:" + this.props.tel;
return (
<A.ListItem className="person">
<A.Icon icon="user" className="person-icon" />
{this.props.name}
<A.Badge amStyle="success" radius className="person-phone">
<a href={link}>
<A.Icon icon="phone" />
</a>
</A.Badge>
</A.ListItem>
);
}
});
- 修改样式表
ul.person-list {
margin-top: 0;
}
li.person {
padding: 3px 6px;
}
li>.person-icon {
margin-right: 6px;
}
li>.person-phone {
margin-top: 4px;
}
内联样式
个人习惯,我不太喜欢使用内联样式。但如果确实需要使用内联样式,可以通过组件的 style
属性设置,其值可以是一个对象,示例:
render: function() {
var styles = {
color: "#666666",
"background-color": "#efefef"
};
return <A.List style={styles}></A.List>
}
更多建议: