3

[学习笔记] Cordova+AmazeUI+React 做个通讯录 系列文章

目录

  1. 准备
  2. 联系人列表(1)
  3. 联系人列表(2)
  4. 联系人详情
  5. 单页应用 (With Router)
  6. 使用 SQLite

传送门:全部章节 示例代码


前一节已经讲述了表头和列表的组件应用,但组件列表项的简单应用并不能满足我们的需求。本节将继续深入探讨联系人列表的细节实现

自定义列表项

本来列表项并不需要自定义,只需要在 <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 />
});

很明显,上面的代码是有问题的:

  1. Person 组件中的 t 变量没有赋值,所以 t.name 一定会抛异常;
  2. return <Person /> 看起来没有问题,但是并没有传入 t 值,所以列表的每一项都会是一模一样的

于是这里遇到了问题:怎么从父级控件向子级控件传入参数?

从父级控件向子级控件传入参数

这里可以作个比喻:从父级控件调用子级控件,就像在某个函数中调用其它函数一样。那么传入参数也就像调用函数时传入的参数一样。

React 通过 props 向子级控件传入参数,在 JSX 语法中,写法和 XML 的属性定义类似。比如向 Person 控件传入 nametel 参数——应该叫属性更准确,就可以这样

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 组件的时候,也向其传入了 amStyleradius 两个属性……等等,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 的两个属性 nametel 都被传递给了 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 引擎解释。

效果

clipboard.png

现在的效果已经有点像样了,但仍然需要改进。不过如之前所述,这个可以通过样式表来解决,待功能完成得差不多了再来调整。

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

除此之外还有几点需要注意

  1. $.getJSON().then() 的回调函数中,直接使用了 this.isMounted()this.setState() 等。函数中的 this 怎么还会是组件对象呢?——请注意回调函数后的 .bind(this)。这个方法在 React 的各方实例中经常出现,不失为传递 this 的一个好办法。
  2. this.isMounted() 的作用是判断当前组件仍然处于 mounted 状态,只有在这个状态下 setState 才有意义。虽然在 componentDidMount 事件中写的这段代码,但是由于是异步加载,所以并不知道当前组件是否已经有所变化。
  3. 如果在 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 属性

  1. 在 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>);
        }
    });
  2. 在 Person 组件中分别添加 personperson-iconperson-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>
            );
        }
    });
  3. 修改样式表

    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>
}


边城
59.3k 声望29.2k 粉丝

一路从后端走来,终于走在了前端!