使用 Expo 和 NativeWind 构建高质量 UI

学习如何直接用代码设计 React Native 应用,借助 Expo、NativeWind 和 Reanimated。构建可复用组件,实现主题化,跳过 Figma。

中文
复制
How to build high-quality UIs with Expo and NativeWind

本文是 Thomino 的客座文章——他是 Native Templates 的作者,也很值得在 X 上关注

我从 IE6 时代就开始做 Web 开发了。我还记得用切好的图片做盒阴影、用透明 GIF 做圆角、用嵌套表格搭结构。那是前端开发的“黑暗时代”。

好在时代变了。把 ExpoNativeWindReanimated 组合起来,设计应用变得轻而易举。事实上我已经不用 Figma 了,直接写代码设计,用 Expo Go 实时预览。

下面我来展示自己是怎么用工作流里那些可复用组件搭建应用的。有了这些组件,我可以直接在编辑器里做设计。我还会讲主题怎么处理,以及怎么做一个带动画的主题切换开关。

可复用组件

每个应用都不一样,但大多数应用共享很多页面、流程和组件。所以我做了一套模板,新项目一开就能按需拿来当基础。要做到这一点,关键是组件可复用,又能针对具体应用轻松改样式。

以下是我在 NativeTemplates 里实现的几个例子。

用 NativeWind 构建灵活的 header 组件

这大概是最重要、最基础的一个。header 组件必须足够灵活,能按需轻松调整。

import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useThemeColors } from "app/contexts/ThemeColors";
import { Link, router } from "expo-router";
import Icon, { IconName } from "./Icon";
import { useSafeAreaInsets } from "react-native-safe-area-context";

type HeaderProps = {
  title: string,
  showBackButton?: boolean,
  rightComponents?: React.ReactNode[],
};

const Header: React.FC<HeaderProps> = ({
  title,
  showBackButton = false,
  rightComponents = [],
}) => {
  const colors = useThemeColors();
  const insets = useSafeAreaInsets();

  const handleBackPress = () => {
    router.back();
  };

  return (
    <View
      style={{ paddingTop: insets.top }}
      className="w-full pb-2 flex-row justify-between px-6 bg-background"
    >
      <View className="flex-row items-center flex-1">
        {showBackButton && (
          <TouchableOpacity onPress={handleBackPress} className="mr-4 py-4">
            <Icon name="ArrowLeft" size={24} color={colors.icon} />
          </TouchableOpacity>
        )}

        <View className="py-4">
          <Text className="text-lg font-bold" style={{ color: colors.text }}>
            {title}
          </Text>
        </View>
      </View>

      {rightComponents.length > 0 && (
        <View className="flex-row items-center justify-end flex-1">
          {rightComponents.map((component, index) => (
            <View key={index} className="ml-6">
              {component}
            </View>
          ))}
        </View>
      )}
    </View>
  );
};

export default Header;
// Basic header with title
<Header title="Home" />
 
// Header with back button
<Header 
  title="Details" 
  showBackButton 
/>
 
// Header with action icons
<Header 
  title="Messages" 
  rightComponents={[
    <HeaderIcon key="search" icon="Search" href="/search" />,
    <HeaderIcon key="settings" icon="Settings" href="/settings" />
  ]}
/>
基本头

在 React Native 中创建带锚点的 tab 导航

选一个图标、动画或头像。

<TabTrigger name="home" href="/" asChild>
  <TabButton labelAnimated={true} icon="Home">Home</TabButton>
</TabTrigger>
<TabTrigger name="profile" href="/profile" asChild>
  <TabButton labelAnimated={true} avatar={require('@/assets/img/thomino.jpg')}>Profile</TabButton>
</TabTrigger>

构建多步表单和 onboarding 流程

这个我用得非常多,特别适合 onboarding 和其他用户流程。Onboarding 对激活用户、降低早期流失至关重要。

<MultiStep
  onComplete={handleComplete}
  onClose={handleClose}
  headerTitle="Get Started"
>
  <Step title="Profile">
    <Profile />
  </Step>
  <Step title="Role">
    <Role />
  </Step>
  <Step title="Capabilities">
    <Capabilities />
  </Step>
  <Step title="Review">
    <Review />
  </Step>
</MultiStep>;
多步标签页

用于筛选和表单的可复用 chip 组件

这个很基础,但在筛选或表单里用得很多。

<Chip 
  label="Custom" 
  className="my-2 mx-1" 
  size="xl"
/>

所有组件都可以在我的文档里查看。

用 NativeWind 实现主题变量

NativeWind 让主题处理变得非常强大。借助 vars,我们可以在一个文件里切换整个应用的主题。这样就能保证浅色和深色模式不是事后补上的,而是设计系统的一部分。

主题定制
import { vars } from "nativewind";

export const themes = {
  light: vars({
    "--color-primary": "#000000", 
    "--color-invert": "#ffffff", 
    "--color-secondary": "#ffffff",
    "--color-background": "#F4F4F5",
    "--color-darker": "#F4F4F5",
    "--color-text": "#000000",     
    "--color-highlight": "#7E55D8",
    "--color-border": "rgba(0, 0, 0, 0.15)",
  }),
  dark: vars({
    "--color-primary": "#ffffff",
    "--color-invert": "#000000",
    "--color-secondary": "#1e1e1e",
    "--color-background": "#141414",
    "--color-darker": "#000000",
    "--color-text": "#ffffff",  
    "--color-highlight": "#7E55D8",
    "--color-border": "rgba(255, 255, 255, 0.15)",
  }),
};

用 Reanimated 做一个会动的深色模式开关

X 上的帖子

import { Pressable, View } from "react-native";
import { useTheme } from "@/app/contexts/ThemeContext";
import Feather from "@expo/vector-icons/Feather";
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from "react-native-reanimated";
import { useEffect } from "react";

const ThemeToggle = () => {
  const { theme, toggleTheme } = useTheme();
  const isDark = theme === "dark";
  const translateX = useSharedValue(isDark ? 36 : 3.5);

  useEffect(() => {
    translateX.value = withSpring(isDark ? 36 : 3.5, {
      damping: 15,
      stiffness: 150,
    });
  }, [isDark]);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: translateX.value }],
    };
  });

  return (
    <Pressable
      onPress={toggleTheme}
      className="w-20 h-10 p-1 bg-secondary relative flex-row rounded-full items-center justify-between"
    >
      <Icon icon="sun" />
      <Icon icon="moon" />
      <Animated.View
        style={[animatedStyle]}
        className="w-9 h-9 bg-background rounded-full items-center justify-center flex flex-row absolute"
      />
    </Pressable>
  );
};

const Icon = (props: any) => {
  const { theme } = useTheme();
  const isDark = theme === "dark";

  return (
    <View className="w-9 h-9 relative z-50 rounded-full items-center justify-center flex flex-row">
      <Feather
        name={props.icon}
        size={16}
        color={`${isDark ? "white" : "black"}`}
      />
    </View>
  );
};

export default ThemeToggle;

现成的登录、引导和设置页面模板

有了扎实的组件基础,搭模板快得离谱。几乎每个 App 都有那么几个页面是共通的,比如登录、注册、引导、个人资料设置、隐私等等。但在这里,所有组件都活了起来!

屏幕
export default function ProfileScreen() {
  return (
    <View className="flex-1 bg-light-primary dark:bg-dark-primary">
      <Header
        leftComponent={<ThemeToggle />}
        rightComponents={[
          <HeaderIcon icon="ChartBar" href="/screens/analytics" />,
        ]}
      />
      <View className="flex-1 bg-light-primary dark:bg-dark-primary">
        <ThemedScroller>
          <AnimatedView className="pt-4" animation="scaleIn">
            <SubscriptionCard />
            <View className="gap-1 bg-secondary rounded-3xl">
              <ListLink
                className="px-4 py-2 border-b border-border"
                showChevron
                title="Account settings"
                icon="Settings"
                href="/screens/settings"
              />
              <ListLink
                className="px-4 py-2 border-b border-border"
                showChevron
                title="Billing"
                icon="CreditCard"
                href="/screens/billing"
              />
            </View>
          </AnimatedView>
        </ThemedScroller>
      </View>
    </View>
  );
}

Expo 给了我们平台,NativeWind 给了我们样式语言,做出来的 App 不光能用,还好看。

如果你想快速启动下一个项目,可以看看这里所有的 React Native Expo 模板,或者我在 GitHub 上的 Expo Playground

来源: Expo Blog← 返回首页