来聊聊怎么写react-native上的样式吧

4,510 阅读11分钟

我遇到了什么问题?

不久之前我重构了一个古老的项目,总结了一些js方面的想法,不过对于一个前端项目而言不仅仅只由js组成的嘛,上学的时候老师和我说HTML+CSS+JS对应的是页面的骨架、皮肤和肌肉。既然骨架我们有了,肌肉也聊完了,今天我们就来聊聊“皮肤”吧。

由于我重构的是一个react-native项目,所以我们先来说说在react-native上是怎么写样式的吧,和传统的web不一样的是,在react-native上面是没有css代码,不过得益于Yoga,我们可以在客户端上像写css一样的去书写我们的样式。我们来看看react-native文档上是怎么说的吧:

在React Native中,你并不需要学习什么特殊的语法来定义样式。我们仍然是使用JavaScript来写样式。所有的核心组件都接受名为style的属性。这些样式名基本上是遵循了web上的CSS的命名,只是按照JS的语法要求使用了驼峰命名法,例如将background-color改为backgroundColor。

style属性可以是一个普通的JavaScript对象。这是最简单的用法,因而在示例代码中很常见。你还可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承。

没错,你几乎不需要什么成本就可以按照写css一样的写法去写我们的rn样式,我们来看一下文档中的例子:

import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View } from 'react-native';

export default class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles.red}>just red</Text>
        <Text style={{
            color: 'blue',
            fontWeight: 'bold',
            fontSize: 30,
        }}>just bigblue</Text>
        <Text style={[styles.bigblue, styles.red]}>bigblue, then red</Text>
        <Text style={[styles.red, styles.bigblue]}>red, then bigblue</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  bigblue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
});

AppRegistry.registerComponent('LotsOfStyles', () => LotsOfStyles);

在上面的demo中,我们有两种方式去写我们的样式,它和我们在写css时候遇到的外联式样式、内联式样式很相似,而项目中我们总是习惯将样式和页面分离,然后把他们都放在另外一个style.js文件中。这是个非常不错的习惯,但是也造成了一些困扰。

面对一个页面,我该怎么去模块化它的样式呢?在之前的项目中虽然做到了样式和页面的分离,让页面“看起来”干净了很多,但是在 style.js 文件中仍然是杂乱的代码,大量重复的变量、重复的内容、重复的声明。。。这个时候又有同学说了,我们可以把这些公共的变量、代码分离出来放到一个主题文件中呀,于是项目中除了各个页面的style.js之外又在全局出现了一个theme.js文件,在这里大家愉快的把诸如颜色、大小、布局等公共的代码放了进来。

这看似解决了style.js里重复冗余的代码,但是也会让我的import变得混乱:

import { StyleSheet } from 'react-native';
import {
  px,
  COLOR_BG_RED,
  COLOR_BG_GREEN,
  STYLE_FR_VC_HSB,
  STYLE_FR_VC_HC,
  STYLE_FR_VC_HFS,
} from 'MyStyle';

export default StyleSheet.create({
    // TODO
});

看到这里,我想你除了知道我import进来了两个颜色之外,对于其它变量会一头雾水吧,除非你去MyStyle模块里面亲眼看一下才会知道真正引入进来的是些什么了,如果这里的样式特别的多的话,除了再新建一个sytle.js之外,你就只能每次回到头部去看看自己引入了些什么。这是我不能忍受的。。。

为你的样式分类

除了由于一次性引入太多的公共样式导致我要来回滑动之外,当我再去写一个新的styel.js文件时,复制这么多引入也是一件头疼的事情,那么我能不能每次只需要写一行import呢?如果我的样式都是按固定规则分类放好的是不是每次就可以只import这几个类了呢?

经常写css的同学一定注意过样式的书写顺序,某一类的属性写在一起,虽然在web中,这样写是为了优化css引擎,但是这也体现出了样式是有一定类型的,控制颜色的、控制边距的、控制布局的,那么我们的公共变量是不是也可以按照这样的规则来声明呢?

import { color, size, layout } from 'MyStyle';

这样我们文件的头部是不是就清晰多了呢?在写代码的时候,也不需要再关心我之前引入了些什么了,只要只要关注我们要写什么就行了:

export default StyleSheet.create({
    lines: {
      height: px(88),
      backgroundColor: color.background,
      borderLeftWidth: size.border,
      borderRightWidth: size.border,
      borderBottomWidth: size.border,
      borderColor: color.border,
      // 子元素横向排列,垂直居中,水平分布,中间用空格填满,最两边元素各自靠边
      ...layout.flex.vchbs,
    },
});

在我的项目中默认边框的大小就是一个像素(1px),那么只要在最外层声明了 size.border的大小,后面写代码的时候就可以畅行无阻的书写下去了,其实我们已经模块化了,只是我们还不够彻底,不彻底就代表着我们的代码不完美,而且可复用性差,就如上面的demo,如果我们这里需要一个三面的边框,那么其它组件需不需要呢?如果需要的话是不是也可以像我这样写呢?

当然是不可以!为什么?因为我们是在复用这个边框,所以我们就不该再写一份一模一样的代码了,而是应该写类似这样的:

export default StyleSheet.create({
    lines: {
      height: px(88),
      backgroundColor: color.background,
      // 一个边框粗细为1px的红色边框
      ...layout.border
      // 子元素横向排列,垂直居中,水平分布,中间用空格填满,最两边元素各自靠边
      ...layout.flex.vchbs,
    },
});

这样我们的代码不仅少了很多,结构也清楚了,而且到时候替换或者修改的时候也容易一些了,不过写成这样就结束了嘛?当然不是了,我们现在有一个红色的边框,所以我们在layout模块下新增了一个border属性,那么如果我们有一个蓝色的边框呢?一个绿色的粗边框呢?我们会一直往layout模块上新增属性嘛?那最后你知道layout上面究竟有多少属性嘛?那不就又回到一开始了嘛。。。

所以,我的建议是,处于根节点的模块最好控制在3个左右:

  • color:用于存放整个项目的全部颜色,这也代表着,在组件的style内部,我们不应该再显示的书写诸如backgroundColor: '#fff'这样的代码了。
  • size:用于存放整个项目的通用大小,比如说行高、间距、字体大小等公共的数值参数。
  • layout:用于存放整个项目的公共布局,例如控制布局的flex属性、通用的padding、margin、position定位。

那么第二级中的属性我也建议控制在5个左右:

  • 颜色:边框颜色、背景颜色、字体颜色。。。
  • 大小:边框大小、间距大小、字体大小。。。
  • 布局:flex布局、position定位。。。

这样虽然增加了深度,但是分类清晰,结构明确,复用性也比较高。虽然可能会增加项目新建时的成本(创建各种分类),但是会给后续的开发、迁移、重构、复用等带来极大的便捷。但这就结束了嘛?有的同学和我说,我有很多的边框啊,我有很多样式要复用啊,到最后我的layout也会大到看不懂啊。。。还有的同学说我没有那么多可复用的样式啊,那是不是你总结的思路就用不上了啊。当然不是咯,我们只完成了样式模块化的第一步(抽离样式),接下来开始第二步。

该怎么更便捷的写样式?

现在很多web开发者在书写css的时候已经不再去写原生的css了吧,而是采用例如scss、less这样的预编译语言去写样式了,那么这些预编译语言给我们带来了哪些方便呢?我想大多数同学第一时间都会想到Mixin

利用混合器,可以很容易地在样式表的不同地方共享样式。如果你发现自己在不停地重复一段样式,那就应该把这段样式构造成优良的混合器,尤其是这段样式本身就是一个逻辑单元,比如说是一组放在一起有意义的属性。

在react-native上面,我们的样式代码是js代码,所以很天然的就自带预编译,不需要其它额外的语言去处理它,要做的只是判断你的属性是否需要一个Mixin。

判断一组属性是否应该组合成一个混合器,一条经验法则就是你能否为这个混合器想出一个好的名字。如果你能找到一个很好的短名字来描述这些属性修饰的样式,比如rounded-cornersfancy-font或者no-bullets,那么往往能够构造一个合适的混合器。如果你找不到,这时候构造一个混合器可能并不合适。

那么在js上面,我该如何实现一个Mixin呢?太简单了!我们只需要一个函数就可以了,没错,只需要一个返回对象的函数就可以做到这样的效果了,加上ES7的拓展运算符,我们就可以做到一个混合器的效果:

export default StyleSheet.create({
    lines: {
      height: px(88),
      backgroundColor: color.background,
      ...layout.border(1px, '#fff')
    },
});

常写react-native的同学一定都头疼过这样一个问题吧,就是我们并不能像写css样式一样在一行中把所有的属性都写完,在css中我们如果想要声明一个四面边框的大小,可以这样写:

.border {
    border: 10px 5px 10px 5px;
}

那么在我们写样式的时候是不是也可以这样写:

export default StyleSheet.create({
    lines: {
      height: px(88),
      backgroundColor: color.background,
      ...layout.border(10px, 5px, 10px, 5px),
    },
});

我们可以通过函数的不同数量的参数来模拟传统css开发的简写属性,很多时候我们更习惯在View上面去做样式的运算,利用react-native样式的覆盖数组去不断的覆盖之前的样式来达到运算的结果,这就导致View中除了需要计算你的组件要不要展示、如何展示之外,还要去计算样式该如何写,既然我们要做样式和页面的分离,那就应该做彻底一些,将样式的计算也放在style.js中。

总结

最后总结一下我们所做的:

  • 分离样式和页面
  • 提取项目级的公共属性
  • 归类提取的公共样式
  • 通过混合器去创造模板样式

我建议无论你的项目多大,代码多少,前三步都应该是一个必备的环节,可能你的项目不复杂,暂时用不到第四点,但前三条无论如何都应该尽早的去完善,这不仅仅能帮助你实现后续的迭代,也能在你的脑中保留出一个对于项目完整结构的印象,要知道样式是寄生于页面的,清楚了样式,那么页面如何你也多少会烂熟于心了。而相比于通过梳理js的逻辑去了解整个项目,我想通过页面也许会更快吧,这对刚刚接手项目的新同学来说,是非常友善的。

最后的最后

一般到这里,就该放上自己开源的项目地址或者安利一波作者写的库了,不过和上一篇一样,这里我们只讨论思路,表述想法,而具体的实践和代码还是要靠我们自己在项目中不断的总结和积累~

我相信很多同学对于我提到的前三点都会很快的理解,而对于第四点可能就有些懵了,该怎么去理解这个混合器呢?我该怎么用js去实现一个呢?下面我就用一段代码来举个例子,该如何实现一个Mixin:

const layout = {
  // 这里的形参顺序遵循css中的 “上、右、下、左”
  margin(...arg) {
    let margin = {};
    switch (arg.length) {
      case 1:
        margin = {
          marginTop: arg[0],
          marginRight: arg[0],
          marginBottom: arg[0],
          marginLeft: arg[0],
        };
        break;
      case 2:
        margin = {
          marginVertical: arg[0],
          marginHorizontal: arg[1],
        };
        break;
      case 3:
        margin = {
          marginTop: arg[0],
          marginHorizontal: arg[1],
          marginBottom: arg[2],
        };
        break;
      case 4:
        margin = {
          marginTop: arg[0],
          marginRight: arg[1],
          marginBottom: arg[2],
          marginLeft: arg[3],
        };
        break;
      default:
        break;
    }
    return margin;
  },
};

这是一个最简易的Mixin,你可以根据你的需求去写更多这样的Mixin,其实我个人觉得在项目一开始的时候是不一定需要这个的,这个存在的意义是对于复杂样式书写的,更多的情况下,你的项目只要做到了前三点,在样式这一块就已经非常的整洁、完善了,多数情况下你不需要Mixin就能组织好你的代码。

好了,以上就是这次我想和大家聊的关于react-native中样式的话题了,我们下次见~