[译][摘要] 改进 React Native 样式管理的 5 种方法

[译][摘要] 改进 React Native 样式管理的 5 种方法

原文:https://shopify.engineering/5-ways-to-improve-your-react-native-styling-workflow

Shopify 开发者对如何在 React Native 中组织样式代码的一些总结,并把最佳实践开源到 Restyle

问题

  • 随着在不同国家和时区工作的团队不断壮大,我们如何确保应用程序在所有不同屏幕上保持一致的风格?
  • 我们可以做些什么来轻松设计应用程序的样式,使其在多种不同的设备尺寸和格式上看起来都很棒?
  • 我们如何让应用程序根据用户的喜好动态调整其主题,以支持例如暗模式?
  • 我们可以让在 React Native 中使用样式变得更愉快吗?

1. 创建设计系统

能够编写干净且一致的样式代码的先决条件是应用程序的设计基于干净且一致的设计系统。设计系统通常被定义为一组规则、约束和原则,它们为应用程序的外观和感觉奠定了基础。构建一个完整的设计系统是一个太大的话题,无法在这里深入研究,但我想指出系统应该为其定义规则的三个重要领域

间距

大小和间距是定义应用程序布局时使用的两个参数。虽然屏幕上呈现的不同组件之间的尺寸通常差异很大,但它们之间的间距通常应尽可能保持一致以创建连贯的外观。这意味着最好坚持使用一小组预定义的间距常量,这些常量用于应用程序中的所有边距和填充。

在决定如何命名间距常量时,有许多约定可供选择,但我发现 T 恤尺寸比例(XS、S、M、L、XL 等)效果最佳。大小的顺序很容易理解,并且系统可以通过添加更多 X 的前缀在两个方向上进行扩展。

// 主要用在 margin 和 padding
const spacing = {
  XS: 4,
  S: 8,
  M: 12,
  L: 16,
  XL: 24,
};

颜色

在设计系统中定义颜色时,重要的是不仅要选择要坚持使用的颜色,还要选择使用方式和使用时间。我喜欢将这些定义分为两层:

  • 调色板 - 这是使用的颜色集。这些可以按字面意思命名,例如“蓝色”、“浅橙色”、“深红色”、“白色”、“黑色”。
  • 语义颜色 - 一组名称,它们映射并描述应如何应用调色板,即它们的功能是什么。一些例子是“主要”、“背景”、“危险”、“失败”。请注意,多个语义颜色可以映射到相同的调色板颜色,例如,“危险”和“失败”颜色都可以映射到“深红色”。

在应用程序中引用颜色时,应该通过语义颜色映射。这使得以后可以轻松更改,例如,将“主要”颜色更改为绿色而不是蓝色。它还允许您轻松地即时更换配色方案,例如,轻松适应应用程序的明暗模式版本。只要元素使用“背景”语义颜色,您就可以根据选择的配色方案在浅色和深色之间进行交换。

// 调色板,定义基本的颜色集
const palette = {
  purple: '#5A31F4',
  green: '#0ECD9D',
  red: '#CD0E61',
  black: '#0B0B0B',
  white: '#F0F2F3',
};
// 语义化的颜色
const Colors = {
  primary: palette.purple,
  error: '#CD0E51',
};

排版

与间距类似,最好坚持一组有限的字体系列、粗细和大小,以在整个应用程序中实现连贯的外观。一组这些印刷元素被一起定义为一个命名的文本变体。您的“标题”文本的大小可能为 36,粗体,并使用字体系列“Raleway”。您的“正文”文本可能使用具有常规字体粗细和大小 16 的“Merriweather”系列。

// 把 fontFamily、fontSize、foneWeight 组合在一起
const R14 = {
  fontSize: 14,
  fontWeight: FontWeight.regular,
  fontFamily: 'Roboto-Regular',
};
const R14Bold = {
  fontSize: 14,
  fontWeight: FontWeight.bold,
  fontFamily: 'Roboto-Regular',
};

2. 定义主题对象

遵循上述间距、颜色和排版实践的精心组合的设计系统应在应用程序的代码库中定义为主题对象。以下是简单版本的外观:

const palette = {
  purple: '#5A31F4',
  green: '#0ECD9D',
  red: '#CD0E61',
  black: '#0B0B0B',
  white: '#F0F2F3',
};

export const theme = {
  colors: {
    background: palette.white,
    foreground: palette.black,
    primary: palette.purple,
    success: palette.green,
    danger: palette.red,
    failure: palette.red,
  },
  spacing: {
    s: 8,
    m: 16,
    l: 24,
    xl: 40,
  },
  textVariants: {
    header: {
      fontFamily: 'Raleway',
      fontSize: 36,
      fontWeight: 'bold',
    },
    body: {
      fontFamily: 'Merriweather',
      fontSize: 16,
    },
  },
};

export const darkTheme = {
  ...theme,
  colors: {
    ...theme.colors,
    background: palette.black,
    foreground: palette.white,
  },
};

与您的设计系统相关的所有值以及这些值在应用程序中的所有使用都应通过此主题对象。这使得只需在单一事实来源中编辑值就可以轻松调整系统。

请注意调色板如何对这个文件保密,并且主题中只包含语义颜色名称。这会在您的设计系统中强制使用颜色的最佳实践。

3. 通过 React 的 Context API 提供主题

通过 context 把样式主题传递到深层组件,取代 import style

React Context 中的主题将确保每当应用程序在明暗模式之间切换时,所有访问该主题的组件都会自动使用更新后的值重新渲染。将主题置于上下文中的另一个好处是能够在子树级别上交换主题。这允许您为应用程序中的不同屏幕使用不同的配色方案,例如,这可以允许用户在社交应用程序中自定义其个人资料页面的颜色。

import React, { useState, useContext } from 'react';
import { View, Switch } from 'react-native';

import { theme, darkTheme } from './theme';

const ThemeContext = React.createContext({});

const App = () => {
  const [darkMode, setDarkMode] = useState(false);
  return (
    <ThemeContext.Provider value={darkMode ? darkTheme : theme}>
      <ThemedComponent />
      <Switch value={darkMode} onValueChange={setDarkMode} />
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  // Get the theme with React's context API
  const themeFromContext = useContext(ThemeContext);
  return (
    <View
      style={{
        width: 200,
        height: 200,
        background: themeFromContext.colors.background,
      }}
    />
  );
};

4. 将样式系统分解成组件

单纯使用样式系统会导致代码重复和冗长,因此把常用样式封装成组件,类似 styled-components

虽然完全有可能继续深入上下文以从需要样式化的任何视图的主题中获取值,但这将很快变得重复且过于冗长。更好的方法是让组件直接将属性映射到主题中的值。在以这种方式处理主题时,我发现自己最需要两个组件:容器和文本。

Box 组件类似于 View,但它不接受样式对象属性来进行样式设置,而是直接接受marginpaddingbackgroundColor等属性。这些属性配置为仅接收主题中可用的值,如下所示:

<Box margin="m" padding="s" backgroundColor="primary" />

这里的“m”和“s”值映射到我们在主题中定义的间距,“primary”映射到相应的颜色。这个组件用在我们需要添加一些间距和背景颜色的大多数地方,只需将它包裹在其他组件周围即可。

虽然 Box 组件可方便地创建布局和添加背景颜色,但 Text 组件在显示文本时发挥作用。由于 React Native 已经要求你在应用程序中的任何文本周围使用他们的 Text 组件,这成为它的替代品:

<Text variant="header" color="primary">Hello</Text>

variant 属性应用了我们在主题中为 定义的所有属性textVariant.header,而 color 属性遵循与 Box 组件相同的原则backgroundColor,但用于文本颜色。

以下是这两个组件的实现方式:

import React, { useContext } from 'react';
import { View, Text as RNText } from 'react-native';

import ThemeContext from './ThemeContext';

const Box = ({ style, padding, margin, backgroundColor, ...rest }) => {
  const theme = useContext(ThemeContext);

  return (
    <View
      style={{
        margin: theme.spacing[margin],
        padding: theme.spacing[padding],
        backgroundColor: theme.colors[backgroundColor],
        ...style,
      }}
      {...rest}
    />
  );
};

const Text = ({ style, variant, color, ...rest }) => {
  const theme = useContext(ThemeContext);

  return (
    <RNText
      style={{
        color: theme.colors[color],
        ...theme.textVariants[variant],
        ...style,
      }}
      {...rest}
    />
  );
};

直接通过属性设置样式而不是保留单独的样式表起初可能看起来很奇怪。我保证,一旦您开始这样做,您将很快开始体会到在样式工作流程中无需在组件和样式表之间来回切换,您节省了多少时间和精力。

5. 使用响应式样式属性

响应式设计是 Web 开发中的常见做法,其中通常为不同的屏幕尺寸和设备类型指定替代样式。在 React Native 应用程序的开发中,这种做法似乎还没有变得司空见惯。响应式设计的需求在 Web 应用程序中很明显,其中设备大小可以从小型移动电话到宽屏桌面设备。仅针对移动设备的 React Native 应用程序可能无法处理相同的极端设备尺寸差异,但潜在屏幕尺寸的差异已经足够大,很难为您的样式找到一刀切的解决方案。

在最新的 iPhone Pro 上显示出色的应用程序引导屏幕很可能无法在第一代 iPhone SE 上可用的有限屏幕空间中正常工作。通常需要根据可用的屏幕尺寸对布局、间距和字体大小进行小幅调整,以便为所有设备打造最佳体验。在响应式设计中,这项工作是通过将设备分类为一组由其断点定义的预定义屏幕尺寸来完成的,例如:

export const theme = {
  // ...
  breakpoints: {
    smallPhone: 0,
    phone: 321,
    tablet: 768,
  },
};

通过这些断点,我们说宽度低于 321 像素的任何东西都应该属于小型手机,高于此但低于 768 像素的任何东西都是常规手机尺寸,而比它宽的任何东西都属于平板电脑。

有了这些设置,让我们扩展我们之前的 Box 组件,以这种方式也接受每个屏幕尺寸的特定道具:

<Box margin='m' padding={{ smallPhone: 'xs', phone: 's', tablet: 'm' }} backgroundColor='primary' />

以下是您将如何实现此功能的大致方法:

import React, { useContext } from 'react';
import { View, Dimensions } from 'react-native';

const getBreakpointForScreenSize = ({ theme, dimensions }) => {
  const sortedBreakpoints = Object.entries(theme.breakpoints).sort((valA, valB) => {
    return valA[1] - valB[1];
  });

  return sortedBreakpoints.reduce((acc, [breakpoint, minWidth]) => {
    if (dimensions.width >= minWidth) return breakpoint;
    return acc;
  }, null);
};

const getResponsiveValue = ({ value, dimensions, theme }) => {
  if (typeof value === 'object') {
    return value[getBreakpointForScreenSize({ theme, dimensions })];
  }
  return value;
};

const Box = ({ style, padding, margin, backgroundColor, ...rest }) => {
  const theme = useContext(ThemeContext);
  const dimensions = Dimensions.get('window');

  return (
    <View
      style={{
        margin: theme.spacing[getResponsiveValue({ value: margin, dimensions, theme })],
        padding: theme.spacing[getResponsiveValue({ value: padding, dimensions, theme })],
        backgroundColor:
          theme.colors[getResponsiveValue({ value: backgroundColor, dimensions, theme })],
        ...style,
      }}
      {...rest}
    />
  );
};

在上述的完整实现中,您最好使用基于钩子的方法来获取当前屏幕尺寸,该尺寸也会在更改时刷新(例如,在更改设备方向时),但为了简洁起见,我将其省略了。

最后,建议配合 Typescript 以获得更好的开发体验。



本博客(marsk6)所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

posted @ 2021-12-31 00:43  marsk68  阅读(395)  评论(0编辑  收藏  举报