React Native踩坑日記 —— tailwind-rn

糊糊糊糊糊了發表於2021-09-16

專案背景

在專案的初始階段,我們需要建立自己的design system,我們spike了一些方案,tailwind-rn就是其中一種,如果有用到或者即將用到tailwind-rn的,可以進來看一看,避免踩坑。

後來覺得專案比較簡單,tailwind對新上專案小夥伴確實不太友好,所以我們最終沒有采用。

簡介

GitHub - vadimdemedes/tailwind-rn: ? Use Tailwind CSS in React Native projects

Tailwind 提倡了原子型的CSS類,旨在靈活、複用,減少CSS重複,同時對於強迫症患者也有一定的治療效果(畢竟有時候想類名是一件頭疼的事)。當然,對於初學者有一定的熟悉成本,你需要要知道它的一些規則,熟悉它的命名系統等等。不太瞭解的可以自行google一下,這裡不再贅述tailwind的使用。

tailwind-rn 就是基於tailwind的實現,使用tailwind生成的css類,然後再進行一些處理(CSS宣告的轉換、去除一些在RN上不適用的CSS宣告等等),最終轉化成適用於RN的Styles格式。

All styles are generated from Tailwind CSS source and not hard-coded, which makes it easy to keep this module up-to-date with latest changes in Tailwind CSS itself.

使用

我們大致來看看,tailwind-rn應該怎麼在RN中使用。

import React from 'react';
import {SafeAreaView, View, Text} from 'react-native';
import tailwind from 'tailwind-rn';

const App = () => (
	<SafeAreaView style={tailwind('h-full')}>
		<View style={tailwind('pt-12 items-center')}>
			<View style={tailwind('bg-blue-200 px-3 py-1 rounded-full')}>
				<Text style={tailwind('text-blue-800 font-semibold')}>
					Hello Tailwind
				</Text>
			</View>
		</View>
	</SafeAreaView>
);

export default App;

tailwind這個方法是從tailwind.ts中重新暴露出來的,伴隨暴露的還有一個getColor方法。

import { create } from 'tailwind-rn';
import styles from './styles.json';

const { tailwind, getColor } = create(styles);

tailwind('text-blue-500 text-opacity-50');
//=> {color: 'rgba(66, 153, 225, 0.5)'}

styles.json是通過cli建立出來的,這個檔案就是tailwind CSS類 → RN Styles 的對映。所以如果開啟了purge功能,同時又新增了一些自定義的Style,需要每次都手動執行cli的命令列重新生成新的styles.json。

簡便的方法是,可以監聽tailwind.config.js是否更改,然後自動生成styles.json,因為一般自定義的Style都會更改這個檔案。

一些優點

purge功能

開啟purge配置,能讓你儘可能地生成最小化的原子型RN styles,最大化的減少體積。建議在開發的時候不要開啟,在打包的時候執行一次就好了。

// Default tailwind config
// Refer to https://unpkg.com/browse/tailwindcss@2.2.9/stubs/defaultConfig.stub.js

module.exports = {
    purge: {
        enabled: true,
        content: ['../../src/**/*.ts', '../../src/**/*.tsx']
    },
    variants: {
        extend: {}
    },
    plugins: []
};

自定義theme

藉助於tailwind的design system,tailwind-rn也同樣地能夠適用,你可以什麼都不需要配置,直接使用它內建的設計系統,也可以新增、覆蓋一些數值,來定義符合自己的設計系統。

theme: {
  fontSize: {
    xs: 12,
    sm: 14,
    base: 16,
    lg: 18,
  },
  extend: {
		// 自定義的extend,會在生成tailwind預設的同時,額外生成自定義的類
    colors: {
      brand: {
        primary: {
          100: '#2c28f7'
        },
        secondary: {
          100: '#146d23'
        },
      }
    },
    padding: {
      7.5: 30
    },
    width: {
      12.5: 50,
      '3/7': '42.857143%',
      '4/7': '57.142857%'
    },
    borderWidth: {
      1.5: 6
    }
  }
}

一些坑

不支援各個邊框(上、下、左、右)顏色屬性

tailwind只提供整個邊框顏色的支援,但是對於上、下、左、右四個邊的邊框是不支援的。

border-black {
	--tw-border-opacity: 1;
	border-color: rgba(0, 0, 0, var(--tw-border-opacity));
}

Tailwind ships with 73 colors out of the box and 5 breakpoints, which means there are already 365 border color utilities. If we added per-side border colors, that would jump to 1825 classes.

簡單搜尋一下,官方也有人提了這個issue,官方給出的答案是因為如果新增了四個邊框的顏色支援,tailwind可能需要額外增加1825個css宣告,所以暫時沒有在考慮的範圍之內。【issue連結

給出的解決方案是使用@apply建立一個新的component class

.code-block {
    @apply .border .border-grey-light;
    border-left-width: config('borderWidths.4');
    border-left-color: config('borderColors.grey-dark');
}

tailwind-rn本身沒有@apply 的方式去重新定義一個自定義的類,官方給出的解釋是使用@apply的本身其實就是使用tailwind('xx')【儘管我覺得有點扯,我其實是希望tailwind-rn能直接幫我把自定義的類打進styles.json,而不是我自己再手動定義一個tailwind('xx'),然後再手動引入】

I think tailwind() function itself is an equivalent of @apply already. @apply py-2 px-2 bg-blue-500 is the same as tailwind('py-2 px-2 bg-blue-500'). Support @apply

所以在RN上面的實現也是類似的

arrow: {
  ...tailwind(['w-0', 'h-0', 'border-solid', 'border-1.5']),
  borderTopColor: getColor('bg-black-medium'),
  borderLeftColor: getColor('bg-white-dark'),
  borderBottomColor: getColor('bg-black-medium'),
  borderRightColor: getColor('bg-black-medium')
}

不支援StyleSheet.hairLineWidth

React Native定義的邏輯畫素單位是沒有單位,為什麼

因為RN是個跨平臺的框架,在IOS上通常以邏輯畫素單位pt描述尺寸,在Android上通常以邏輯畫素單位dp描述尺寸,RN選哪個都不好,既然大家意思相同,乾脆不帶單位,在哪個平臺渲染就預設用哪個單位。
RN提供給開發者的就是已經通過DPR(裝置畫素比)轉換過的邏輯畫素尺寸,開發者無需再關心因為裝置DPR不同引起的尺寸數值計算問題

通過上面的解釋,所以width為1的,在ios上代表的是1pt,在android上代表的是1dp,表現為的裝置畫素在二倍屏上是即是2物理畫素,三倍屏則是3物理畫素,而一畫素邊框其實是代表的物理畫素,所以ios在三倍屏上要想顯示一畫素的邊框,對應的應該是0.33333pt.

由於我們需要使用RN的hairLineWidth來幫我們自動根據裝置來計算,所以就不能使用配置檔案來處理,所以解決的方案也比較硬核,就是直接往styles.json中塞值。

自定義一個custom.style.ts,專門用來處理一些tailwind-rn理不了的類宣告。

// custom.styles.ts

import { StyleSheet } from 'react-native';

export const customStyles = {
  'border-hair': {
	    borderWidth: StyleSheet.hairlineWidth
  },
  'border-t-hair': {
	    borderTopWidth: StyleSheet.hairlineWidth
  },
  'border-b-hair': {
      borderBottomWidth: StyleSheet.hairlineWidth
  },
  'border-l-hair': {
      borderLeftWidth: StyleSheet.hairlineWidth
  },
  'border-r-hair': {
      borderRightWidth: StyleSheet.hairlineWidth
  },
  'width-hair': {
      width: StyleSheet.hairlineWidth
  }
};

export type CustomStylesKey = keyof typeof customStyles;

然後在tailwind.ts中merge一下

// tailwind.ts
import { create } from 'tailwind-rn';
import { assign } from 'lodash';

const { tailwind } = create(assign(styles, customStyles));

不支援智慧提示

在現在主流的IDE上,都存在tailwind的智慧提示外掛,但是對於tailwind-rn的提示卻不友好,要解決也挺簡單

  • 自己實現一個外掛,相容各個IDE
  • 重新定義下型別,一個討巧的做法,這裡講一下這種方法

編輯器不支援智慧提示,我們可以利用Typescript的型別系統來稍微改造一下,讓其能夠自己推斷

// tailwind.ts

import { create } from 'tailwind-rn';
import styles from 'src/styles/styles.json';
import { assign } from 'lodash';
import { customStyles, CustomStylesKey } from 'src/styles/custom.style';

const { tailwind } = create(assign(styles, customStyles));

export type TailwindKey = keyof typeof styles | CustomStylesKey;

export default (keys: TailwindKey[]) => tailwind(keys.join(' '));

這裡強行將之前的string變成了一個陣列,目的就是為了讓IDE去識別自己定義的tailwind key型別

// 推薦使用
tailwind('h-11 bg-red-100')

// 改造之後
tailwind(['h-11', 'bg-red-100'])

getColor與purge衝突

當使用tailwind-rn提供的getColor方法,並開啟了purge配置時

// tailwind.config.js
module.exports = {
    purge: {
        enabled: true,
        content: ['../../src/**/*.ts', '../../src/**/*.tsx']
    },
    // ...
}

由於tailwind預設不支援邊框顏色,所以我不得不使用RN提供的方法。但是這樣使用,我就需要使用getColor方法。

// PageA.styles.ts
const styles = StyleSheet.create({
    container: {
        ...tailwind('w-11 h-11 bg-black text-white'),
        borderTopColor: getColor("blue-500")
    }
})

但是在我使用purge之後,tailwind掃描了預設已經在使用的CSS類,所以blue-500沒有被識別,也沒有被打包到styles.json中。

這就是問題所在。解決的方法也比較簡單,就是使用tailwind提供的css類

// PageA.styles.ts
const styles = StyleSheet.create({
    container: {
        ...tailwind('w-11 h-11 bg-black text-white'),
        borderTopColor: getColor("bg-blue-500 bg-opacity-50")
    }
})

原始碼中的getColor是會預設掃描background,所以預設拼接了bg-,那麼幹掉就成了

// Pass the name of a color (e.g. "bg-blue-500") and receive a color value (e.g. "#4399e1"),
// or a color and opacity (e.g. "bg-black bg-opacity-50") and get a color with opacity (e.g. "rgba(0,0,0,0.5)")
const getColor = name => {
		// const style = tailwind(name.split(' ').map(className => `bg-${className}`).join(' '));
    const style = tailwind(name);
    return style.backgroundColor;
};

針對這個問題,我給官方提了個PR,但是不知道何時才能merge了。

Purge function will conflict with getColor by Rynxiao · Pull Request #96 · vadimdemedes/tailwind-rn

顯然修改原始碼是不可靠的,一下次的更新可能就會幹掉你原先apply的程式碼,所以我們自己實現一遍就好。

// tailwind.ts

export const getColor = (keys: TailwindKey[]) => {
    const style = tailwind(key.join(' '));
    return style.backgroundColor;
};

總結

  • 使用初期確實挺煩的,一個類一個類去找,但是熟悉了它的命名規範之後,其實寫起來還挺順暢的。
  • 有一些坑,但都不是不能解決的問題,大不了使用原生的RN Style擼一擼。

參考連結

相關文章