Skip to content

React Native UI组件 #10

@fayching

Description

@fayching

React的组件概念

所谓组件,就是状态机器。React 将用户界面看做简单的状态机器。当组件处于某个状态时,那么就输出这个状态对应的界面。通过这种方式,就很容易去保证界面的一致性。在 React 中,你简单的去更新某个组件的状态,然后输出基于新状态的整个界面。React 负责以最高效的方式去比较两个界面并更新 DOM 树。

React框架的设计是希望一个App就是一个大的Component,各个功能模块又是由Component组成,而每个功能模块Component又能划分各个小的Component的。开发过程就是不断优化和拆分界面组件、构造整个组件树的过程。可以认为组件类似于其他框架中Widget(或Control)的概念,但又有所不同。React中的界面一切皆为组件,而Widget一般只是嵌入到界面中为完成某个功能的独立模块。

在React Native中,我们划分组件可以如下图,最小的组件粒度即React Native提供的Text,Image等基础组件。

image

组件化的开发思路

如果说 MVC 的思想让你做到视图-数据-控制器的分离,那么组件化的思考方式则是带来了 UI 功能模块之间的分离。

对于 MVC 开发模式来说,开发者将三者定义成不同的类,实现了表现,数据,控制的分离,开发者更多的是从技术的角度来对 UI 进行拆分,实现松耦合。对于 React 而言,则完全是一个新的思路,开发者从功能的角度出发,将 UI 分成不同的组件,每个组件都独立封装。组件的封装方式和单向数据流动能够极大地简化前端架构的理解难度。

下面我们通过组件化的思路来构建我们动漫的收藏页(header和footer为原生)。

image

在 React Native中,你按照界面模块自然划分的方式来组织和编写你的代码,对于详情界面而言,整个 UI 是一个通过小组件构成的大组件,每个组件只关心自己部分的逻辑,彼此独立。所以界面组织架构如下:

  • Collect
    • HistoryList
    • MyDownload
    • MyCollect
      • SubHeader
      • ComicList

这样最外层的 Collect 界面和 MyCollect 的 Render 只需要如下代码:

 class Detail extends Component {
    render() {
       return (
           <View>
               <HistoryList />
               <MyDownload />
               <MyCollect />
           </View>
       )
    }
}
class MyCollect extends Component {
   render() {
      return (
          <View>
              <SubHeader />
              <ComicList />
          </View>
      )
   }
}

具体到每个组件,HistoryList 需要用到ScrollView 组件,MyDownload 比较简单,只是呈现文字和图片,MyCollect 下的 SubHeader 和 ComicList 我们会发现很多地方会有类似的样式,我们可以像web端一样,做成通用组件。

自定义组件

已有组件

React Native 有一些已经封装好的UI组件:

image

通用组件库

那么我们做一套通用组件的结构应该是什么样呢,以下是我们的Frozen React Native 版本的项目结构:

image

参考了web版的组织结构,component下放frozen的组件库和app的demo文件,frozen中每个组件单独一个目录,公用的样式统一放在base下。style下的base提取出公用的文本样式typo.css.js,布局样式layout.css.js等,一个普通的按钮组件会有一个对应的样式component:Button.css.js,结构component: Button.js。
组件部分代码:

Button.css.js

...
module.exports = require('react-native').StyleSheet.create({
    default: {
        height: 30,
        flexDirection:'row',
        alignItems: 'center',
        justifyContent: 'center',
        paddingLeft: 14,
        paddingRight: 14,
        borderWidth: 1/PixelRatio.get(),
        borderColor: '#818181',
        backgroundColor: '#fff',
        marginLeft: 10,
        marginRight: 10,
    },
    defaultText: {
        fontSize: 15,
        backgroundColor: 'transparent'
    },
    //  尺寸大小
    smallSize: {
        height: 30,
        borderRadius: 2,
    },
    smallSizeText: {
        fontSize: 15,
    },
    largeSize: {
        height: 44,
        borderRadius: 3,
    },
...

Button.js

...
export default class Button extends React.Component {

    constructor(props) {
        super(props);
    }
    static defaultProps = {
        disabled: false,
        type: 'default',
        size: 'small',
    };
    static propTypes = {
        value: PropTypes.string.isRequired,
        disabled: PropTypes.bool,
        type: PropTypes.oneOf(TypeName),
        size: PropTypes.oneOf(SizeName),
        style: Text.propTypes.style,
        textStyle: Text.propTypes.style,
        activeStyle: Text.propTypes.style,
        activeTextStyle: Text.propTypes.style,
        onPress: PropTypes.func,
        onPressIn: PropTypes.func,
        onPressOut: PropTypes.func,
        onLongPress: PropTypes.func,
        elementStyles: PropTypes.object,
    };
    state = { buttonState: 'Normal', customActive: false };

    render = () => {
        const {
            value,
            disabled,
            type,
            size,
            style,
            textStyle,
            activeStyle,
            activeTextStyle,
            onPress,
            onPressIn,
            onPressOut,
            onLongPress,
            children,
        } = this.props;
...

ButtonExample.js

...
export default class ButtonExample extends Component{
    longPress(){
        alert("你按了好久啊");
    }
    onPress(){
        alert("别碰我!");
    }
    onPressOut(){
        alert("你还是离开了!");
    }
    // 构造
    render() {
        return (
            <ScrollView>
            <View style={styles.container}>
                <View style={styles.row}>
                    <Button value="确定" />
                    <Button value="确定" disabled={true} />
                </View>
...

使用样式

样式重写

所有的核心组件接受样式属性。

<Text style={styles.base} />
<View style={styles.background} />

它们也接受一系列的样式。

<View style={[styles.base, styles.background]} />

在冲突值的情况下,从最右边元素的值将会优先,并且 falsy 值如 false,undefined 和 null 将被忽略。一个常见的模式是基于某些条件有条件地添加一个样式。

<View style={[styles.base, this.state.active && styles.active]} /> 

基于以上,我们可以在我们封装好的组件里写好基础样式,再通过this.propTypes.style将用户增加或覆盖的样式传进来。

<View style={[styles.base,this.props.style]} /> 

样式传递

与web版的css不同,RN的样式不能继承和传递,那么我们如何在外层组件控制子组件的样式呢,比如按钮里的文本的样式。这时我们考虑到父组件可以写各种自定义属性的,我们可以自定义属性来传递样式。

假使我们有如下的组件:我们在其中传递了影响组件表现的两种样式,style和elementStyle(自定义属性,style无法直接调用,需要强制修改属性)

<Button style={styles.button} elementStyle={styles.text} />

渲染组件:

export default class Button extends React.Component {
    static propTypes = {
        style: View.propTypes.style,
        value: PropTypes.string.isRequired,
        elementStyle: View.propTypes.style,
    },
    render = () => {
        return (
            <View  style={[styles.button,this.props.style]}>
                <Text style={this.props.elementStyle}>{value}</Text>
            </View>
        )
    }
}

可以看到其中我们使用了View.propTypes.style(还有Text.propTypes.style)来将父组件的属性style和elementStyle 强制为样式属性,这样子组件就可以直接调用this.props.elementStyle (依赖于父组件的自定义属性),而非styles.text(全局的样式属性)。

这样,我们在调用父组件Button的时候,只要赋予不同的style和elementStyle就能轻松控制整个组件的外观!

实际上子组件的结构可能不止是一个View,可能会有多个子组件,那么我们可以约定每个组件都可以有一个elementStyles属性,elementStyles是子组件的样式合集,即可解决多个子组件的样式传递问题。

 render = () => {
        return (
            <View style={[styles.button,this.props.style]}>
                <Text style={this.props.elementStyles.text}>{value}</Text>
            </View>
        )
    }

修改children组件的样式

当子组件的结构不固定时,我们不可避免的需要修改children组件的样式,而this.props的属性都是只读的,设置this.props.children的style是会报错的。

我们有两种方式可以修改children的样式:

  1. 通过遍历获取this.props.children 并包裹一层结构,在这层结构上增加样式:
render = () => {
let elements = Array.isArray(children)?children:[children];
return (
    <View style={this.props.style}>
        {elements.map((element) =>
            <View style={[styles.element]} >
                {element}
            </View>
        )}
    </View>
)
}
  1. 通过遍历this.props.children,同时通过React.cloneElement(element, {style: someNewStyle})来克隆出修饰过样式之后的“子组件复制品”。第一种方法适合多个子组件的情况,第二种方式更适合一个子组件的情况。

同理,修改children的结构和其他属性也可以通过这两种方式。

总结

React Native中一切皆为组件,想要做出更通用,能最大化被复用的一套通用UI组件库,需要清晰的组织结构和代码规范,还需要灵活使用各种组件属性和样式技巧。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions