來聊聊怎麼寫react-native上的樣式吧

jonnyF發表於2018-02-23

我遇到了什麼問題?

不久之前我重構了一個古老的專案,總結了一些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中樣式的話題了,我們下次見~

相關文章