Files
workspace/notes/前端开发/react-native/expo.md
T
2025-11-10 17:31:45 +08:00

37 KiB
Raw Blame History

官网:https://expo.dev/

初始化项目

创建项目命令:

pnpm create expo-app@latest --template blank-typescript

官网模板如下:

模板名称 描述
default 默认模板,用于构建多页面应用。包含推荐工具,如 Expo CLI、Expo Router 路由库,并启用 TypeScript 配置。适合大多数应用。
blank 最简模板,仅安装最基本依赖,不包含导航配置。
blank-typescript 空白模板,但启用了 TypeScript 支持。
tabs 启用基于文件的路由系统(Expo Router),并默认使用 TypeScript,适合多标签页结构的应用。
bare-minimum 带有原生目录(android 与 ios)的空白模板,创建时会自动运行 npx expo prebuild。适合需要直接修改原生代码的项目。

启动项目:

npx expo start

基础配置

集成 antfu-eslint

可以使用快捷命令(推荐):

pnpm dlx @antfu/eslint-config@latest

安装:

pnpm i -D eslint @antfu/eslint-config

新建 eslint.config.mjs 文件:

import antfu from '@antfu/eslint-config'

export default antfu({
  stylistic: {
    indent: 2, // 缩进为 2 个空格(可选 4 或 'tab')
    quotes: 'single', // 使用单引号(可选 'double')
  },
  rules: {
    'unused-imports/no-unused-imports': 'warn',
    'unused-imports/no-unused-vars': 'warn',
    '@typescript-eslint/no-use-before-define': 'off', // 关闭 TS 的“定义前使用”警告
    'no-console': 'off', // 关闭 console 报错
  },
})

配置 vscode 模板,修改 .vscode/settings.json :

注意安装 eslint 插件

{
  // Disable the default formatter, use eslint instead
  "prettier.enable": false,
  "editor.formatOnSave": false,

  // Auto fix
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },

  // Silent the stylistic rules in you IDE, but still auto fix them
  "eslint.rules.customizations": [
    { "rule": "style/*", "severity": "off", "fixable": true },
    { "rule": "format/*", "severity": "off", "fixable": true },
    { "rule": "*-indent", "severity": "off", "fixable": true },
    { "rule": "*-spacing", "severity": "off", "fixable": true },
    { "rule": "*-spaces", "severity": "off", "fixable": true },
    { "rule": "*-order", "severity": "off", "fixable": true },
    { "rule": "*-dangle", "severity": "off", "fixable": true },
    { "rule": "*-newline", "severity": "off", "fixable": true },
    { "rule": "*quotes", "severity": "off", "fixable": true },
    { "rule": "*semi", "severity": "off", "fixable": true }
  ],

  // Enable eslint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml",
    "toml",
    "xml",
    "gql",
    "graphql",
    "astro",
    "svelte",
    "css",
    "less",
    "scss",
    "pcss",
    "postcss"
  ]
}

集成 nativewind

直接参考官网:https://www.nativewind.dev/docs/getting-started/installation

  1. 安装依赖

    npx expo add nativewind react-native-reanimated react-native-safe-area-context
    npx expo add tailwindcss@^3.4.17 -- --save-dev
    
  2. 运行 npx tailwindcss init 生成 tailwind.config.js 文件,添加配置:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      // NOTE: Update this to include the paths to all files that contain Nativewind classes.
      content: ["./App.tsx", "./components/**/*.{js,jsx,ts,tsx}"],
      presets: [require("nativewind/preset")],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    
  3. 创建 global.css 文件:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

    注意在全局引入

  4. 修改 babel.config.js 文件:

    如果没有该文件,执行 npx expo customize babel.config.js 命令创建。

    module.exports = function (api) {
      api.cache(true);
      return {
        presets: [
          ["babel-preset-expo", { jsxImportSource: "nativewind" }],
          "nativewind/babel",
        ],
      };
    };
    
  5. 修改 metro.config.js 文件:

    如果没有该文件,执行 npx expo customize metro.config.js 命令创建。

    const { getDefaultConfig } = require("expo/metro-config");
    const { withNativeWind } = require('nativewind/metro');
    
    const config = getDefaultConfig(__dirname)
    
    module.exports = withNativeWind(config, { input: './global.css' })
    
  6. 新建 src/types/nativewind-env.d.ts 文件:

    /// <reference types="nativewind/types" />
    
  7. 检验是否生效

    import "./global.css"
    import { Text, View } from "react-native";
    
    export default function App() {
      return (
        <View className="flex-1 items-center justify-center bg-white">
          <Text className="text-xl font-bold text-blue-500">
            Welcome to Nativewind!
          </Text>
        </View>
      );
    }
    

    如果不生效,记得修改 tailwind.config.js 的目标路径:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      // NOTE: Update this to include the paths to all files that contain Nativewind classes.
      content: ["./src/app/_layout.tsx", "./src/components/**/*.{js,jsx,ts,tsx}"],
      presets: [require("nativewind/preset")],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    

集成 gluestack-ui

方式一:CLI

npx gluestack-ui init

方式二:Manual

**前置条件:**需要安装 nativewind

  1. 安装依赖:

    npx expo add @gluestack-ui/core @gluestack-ui/utils react-native-svg @gluestack/ui-next-adapter
    
  2. 从 github 复制必须的组件到 @/components/ui/gluestack-ui-provider。

  3. 修改 tailwind.config.js 文件:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class',
      content: [
        './app/**/*.{html,js,jsx,ts,tsx,mdx}',
        './components/**/*.{html,js,jsx,ts,tsx,mdx}',
        './utils/**/*.{html,js,jsx,ts,tsx,mdx}',
        './*.{html,js,jsx,ts,tsx,mdx}',
        './src/**/*.{html,js,jsx,ts,tsx,mdx}',
      ],
      presets: [require('nativewind/preset')],
      important: 'html',
      safelist: [
        {
          pattern:
          // eslint-disable-next-line regexp/no-dupe-disjunctions
            /(bg|border|text|stroke|fill)-(primary|secondary|tertiary|error|success|warning|info|typography|outline|background|indicator)-(0|50|100|200|300|400|500|600|700|800|900|950|white|gray|black|error|warning|muted|success|info|light|dark|primary)/,
        },
      ],
      theme: {
        extend: {
          colors: {
            primary: {
              0: 'rgb(var(--color-primary-0)/<alpha-value>)',
              50: 'rgb(var(--color-primary-50)/<alpha-value>)',
              100: 'rgb(var(--color-primary-100)/<alpha-value>)',
              200: 'rgb(var(--color-primary-200)/<alpha-value>)',
              300: 'rgb(var(--color-primary-300)/<alpha-value>)',
              400: 'rgb(var(--color-primary-400)/<alpha-value>)',
              500: 'rgb(var(--color-primary-500)/<alpha-value>)',
              600: 'rgb(var(--color-primary-600)/<alpha-value>)',
              700: 'rgb(var(--color-primary-700)/<alpha-value>)',
              800: 'rgb(var(--color-primary-800)/<alpha-value>)',
              900: 'rgb(var(--color-primary-900)/<alpha-value>)',
              950: 'rgb(var(--color-primary-950)/<alpha-value>)',
            },
            secondary: {
              0: 'rgb(var(--color-secondary-0)/<alpha-value>)',
              50: 'rgb(var(--color-secondary-50)/<alpha-value>)',
              100: 'rgb(var(--color-secondary-100)/<alpha-value>)',
              200: 'rgb(var(--color-secondary-200)/<alpha-value>)',
              300: 'rgb(var(--color-secondary-300)/<alpha-value>)',
              400: 'rgb(var(--color-secondary-400)/<alpha-value>)',
              500: 'rgb(var(--color-secondary-500)/<alpha-value>)',
              600: 'rgb(var(--color-secondary-600)/<alpha-value>)',
              700: 'rgb(var(--color-secondary-700)/<alpha-value>)',
              800: 'rgb(var(--color-secondary-800)/<alpha-value>)',
              900: 'rgb(var(--color-secondary-900)/<alpha-value>)',
              950: 'rgb(var(--color-secondary-950)/<alpha-value>)',
            },
            tertiary: {
              50: 'rgb(var(--color-tertiary-50)/<alpha-value>)',
              100: 'rgb(var(--color-tertiary-100)/<alpha-value>)',
              200: 'rgb(var(--color-tertiary-200)/<alpha-value>)',
              300: 'rgb(var(--color-tertiary-300)/<alpha-value>)',
              400: 'rgb(var(--color-tertiary-400)/<alpha-value>)',
              500: 'rgb(var(--color-tertiary-500)/<alpha-value>)',
              600: 'rgb(var(--color-tertiary-600)/<alpha-value>)',
              700: 'rgb(var(--color-tertiary-700)/<alpha-value>)',
              800: 'rgb(var(--color-tertiary-800)/<alpha-value>)',
              900: 'rgb(var(--color-tertiary-900)/<alpha-value>)',
              950: 'rgb(var(--color-tertiary-950)/<alpha-value>)',
            },
            error: {
              0: 'rgb(var(--color-error-0)/<alpha-value>)',
              50: 'rgb(var(--color-error-50)/<alpha-value>)',
              100: 'rgb(var(--color-error-100)/<alpha-value>)',
              200: 'rgb(var(--color-error-200)/<alpha-value>)',
              300: 'rgb(var(--color-error-300)/<alpha-value>)',
              400: 'rgb(var(--color-error-400)/<alpha-value>)',
              500: 'rgb(var(--color-error-500)/<alpha-value>)',
              600: 'rgb(var(--color-error-600)/<alpha-value>)',
              700: 'rgb(var(--color-error-700)/<alpha-value>)',
              800: 'rgb(var(--color-error-800)/<alpha-value>)',
              900: 'rgb(var(--color-error-900)/<alpha-value>)',
              950: 'rgb(var(--color-error-950)/<alpha-value>)',
            },
            success: {
              0: 'rgb(var(--color-success-0)/<alpha-value>)',
              50: 'rgb(var(--color-success-50)/<alpha-value>)',
              100: 'rgb(var(--color-success-100)/<alpha-value>)',
              200: 'rgb(var(--color-success-200)/<alpha-value>)',
              300: 'rgb(var(--color-success-300)/<alpha-value>)',
              400: 'rgb(var(--color-success-400)/<alpha-value>)',
              500: 'rgb(var(--color-success-500)/<alpha-value>)',
              600: 'rgb(var(--color-success-600)/<alpha-value>)',
              700: 'rgb(var(--color-success-700)/<alpha-value>)',
              800: 'rgb(var(--color-success-800)/<alpha-value>)',
              900: 'rgb(var(--color-success-900)/<alpha-value>)',
              950: 'rgb(var(--color-success-950)/<alpha-value>)',
            },
            warning: {
              0: 'rgb(var(--color-warning-0)/<alpha-value>)',
              50: 'rgb(var(--color-warning-50)/<alpha-value>)',
              100: 'rgb(var(--color-warning-100)/<alpha-value>)',
              200: 'rgb(var(--color-warning-200)/<alpha-value>)',
              300: 'rgb(var(--color-warning-300)/<alpha-value>)',
              400: 'rgb(var(--color-warning-400)/<alpha-value>)',
              500: 'rgb(var(--color-warning-500)/<alpha-value>)',
              600: 'rgb(var(--color-warning-600)/<alpha-value>)',
              700: 'rgb(var(--color-warning-700)/<alpha-value>)',
              800: 'rgb(var(--color-warning-800)/<alpha-value>)',
              900: 'rgb(var(--color-warning-900)/<alpha-value>)',
              950: 'rgb(var(--color-warning-950)/<alpha-value>)',
            },
            info: {
              0: 'rgb(var(--color-info-0)/<alpha-value>)',
              50: 'rgb(var(--color-info-50)/<alpha-value>)',
              100: 'rgb(var(--color-info-100)/<alpha-value>)',
              200: 'rgb(var(--color-info-200)/<alpha-value>)',
              300: 'rgb(var(--color-info-300)/<alpha-value>)',
              400: 'rgb(var(--color-info-400)/<alpha-value>)',
              500: 'rgb(var(--color-info-500)/<alpha-value>)',
              600: 'rgb(var(--color-info-600)/<alpha-value>)',
              700: 'rgb(var(--color-info-700)/<alpha-value>)',
              800: 'rgb(var(--color-info-800)/<alpha-value>)',
              900: 'rgb(var(--color-info-900)/<alpha-value>)',
              950: 'rgb(var(--color-info-950)/<alpha-value>)',
            },
            typography: {
              0: 'rgb(var(--color-typography-0)/<alpha-value>)',
              50: 'rgb(var(--color-typography-50)/<alpha-value>)',
              100: 'rgb(var(--color-typography-100)/<alpha-value>)',
              200: 'rgb(var(--color-typography-200)/<alpha-value>)',
              300: 'rgb(var(--color-typography-300)/<alpha-value>)',
              400: 'rgb(var(--color-typography-400)/<alpha-value>)',
              500: 'rgb(var(--color-typography-500)/<alpha-value>)',
              600: 'rgb(var(--color-typography-600)/<alpha-value>)',
              700: 'rgb(var(--color-typography-700)/<alpha-value>)',
              800: 'rgb(var(--color-typography-800)/<alpha-value>)',
              900: 'rgb(var(--color-typography-900)/<alpha-value>)',
              950: 'rgb(var(--color-typography-950)/<alpha-value>)',
              white: '#FFFFFF',
              gray: '#D4D4D4',
              black: '#181718',
            },
            outline: {
              0: 'rgb(var(--color-outline-0)/<alpha-value>)',
              50: 'rgb(var(--color-outline-50)/<alpha-value>)',
              100: 'rgb(var(--color-outline-100)/<alpha-value>)',
              200: 'rgb(var(--color-outline-200)/<alpha-value>)',
              300: 'rgb(var(--color-outline-300)/<alpha-value>)',
              400: 'rgb(var(--color-outline-400)/<alpha-value>)',
              500: 'rgb(var(--color-outline-500)/<alpha-value>)',
              600: 'rgb(var(--color-outline-600)/<alpha-value>)',
              700: 'rgb(var(--color-outline-700)/<alpha-value>)',
              800: 'rgb(var(--color-outline-800)/<alpha-value>)',
              900: 'rgb(var(--color-outline-900)/<alpha-value>)',
              950: 'rgb(var(--color-outline-950)/<alpha-value>)',
            },
            background: {
              0: 'rgb(var(--color-background-0)/<alpha-value>)',
              50: 'rgb(var(--color-background-50)/<alpha-value>)',
              100: 'rgb(var(--color-background-100)/<alpha-value>)',
              200: 'rgb(var(--color-background-200)/<alpha-value>)',
              300: 'rgb(var(--color-background-300)/<alpha-value>)',
              400: 'rgb(var(--color-background-400)/<alpha-value>)',
              500: 'rgb(var(--color-background-500)/<alpha-value>)',
              600: 'rgb(var(--color-background-600)/<alpha-value>)',
              700: 'rgb(var(--color-background-700)/<alpha-value>)',
              800: 'rgb(var(--color-background-800)/<alpha-value>)',
              900: 'rgb(var(--color-background-900)/<alpha-value>)',
              950: 'rgb(var(--color-background-950)/<alpha-value>)',
              error: 'rgb(var(--color-background-error)/<alpha-value>)',
              warning: 'rgb(var(--color-background-warning)/<alpha-value>)',
              muted: 'rgb(var(--color-background-muted)/<alpha-value>)',
              success: 'rgb(var(--color-background-success)/<alpha-value>)',
              info: 'rgb(var(--color-background-info)/<alpha-value>)',
              light: '#FBFBFB',
              dark: '#181719',
            },
            indicator: {
              primary: 'rgb(var(--color-indicator-primary)/<alpha-value>)',
              info: 'rgb(var(--color-indicator-info)/<alpha-value>)',
              error: 'rgb(var(--color-indicator-error)/<alpha-value>)',
            },
          },
          fontFamily: {
            'heading': undefined,
            'body': undefined,
            'mono': undefined,
            'jakarta': ['var(--font-plus-jakarta-sans)'],
            'roboto': ['var(--font-roboto)'],
            'code': ['var(--font-source-code-pro)'],
            'inter': ['var(--font-inter)'],
            'space-mono': ['var(--font-space-mono)'],
          },
          fontWeight: {
            extrablack: '950',
          },
          fontSize: {
            '2xs': '10px',
          },
          boxShadow: {
            'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
            'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)',
            'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
            'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)',
            'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)',
            'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)',
            'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)',
            'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)',
            'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)',
          },
        },
      },
    }
    
    
  4. 使用 GluestackUIProvider 包括着整个组件:

    // App.tsx
    import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
    import '@/global.css'; // Make sure this path is correct
    
    export default function App() {
      return (
        <GluestackUIProvider>{/* Add your app code here */}</GluestackUIProvider>
      );
    }
    

安装组件测试

下载对应组件:

npx gluestack-ui add button

引入使用:

import { StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Button, ButtonText } from '@/components/ui/button'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import '@/styles/global.css'

export default function Layout() {
  return (
    <GluestackUIProvider>
      <SafeAreaView style={styles.container}>
        <View className="flex-1 items-center justify-center bg-white">
          <Text className="text-xl font-bold text-blue-500">
            Welcome to Nativewind!
          </Text>
          <Button>
            <ButtonText>你好</ButtonText>
          </Button>
        </View>
      </SafeAreaView>
    </GluestackUIProvider>

  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingHorizontal: 16,
  }
})

集成@expo/vector-icons

图标网站地址:https://icons.expo.fyi/Index

该插件默认跟着expo安装了。可以直接使用,如果没有的话,则执行命令安装:

npx expo install @expo/vector-icons

测试:

import { View, StyleSheet } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';

export default function App() {
  return (
    <View style={styles.container}>
      <Ionicons name="checkmark-circle" size={32} color="green" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

如果需要没有想要的图标,可以用下面的方式:

安装 svg 依赖

npx expo install react-native-svg

然后在components下面创建一个 Icons.tsx 文件,用来管理所有的图标

import Svg, { Circle, Rect, Path } from 'react-native-svg';

interface IconProps {
  size?: string | number;
  color?: string;
}

// 测试图标
export const DemoIcon: React.FC<IconProps> = ({ size = 30, color = '#fff', ...rest }) => {
  return (
    <Svg height="50%" width="50%" viewBox="0 0 100 100" {...rest}>
      <Circle cx="50" cy="50" r="45" stroke="blue" strokeWidth="2.5" fill="green" />
      <Rect x="15" y="15" width={size} height={size} stroke="red" strokeWidth="2" fill="yellow" />
    </Svg>
  );
}

// 招聘
export const ZhaoPinIcon: React.FC<IconProps> = ({ size = 30, color = '#fff', ...rest }) => {
  return (
    <Svg viewBox="0 0 1024 1024" p-id="5316" width={size} height={size}  {...rest}>
      <Path d="M473.6 732.8c-9.6 0-25.6 3.2-48 6.4v112h-83.2v-102.4c-73.6 9.6-150.4 19.2-172.8 22.4L160 681.6c9.6-3.2 22.4-3.2 41.6-6.4V278.4h-19.2V188.8h272v89.6h-28.8v368c19.2-3.2 28.8-3.2 38.4-6.4l3.2 48 6.4 44.8zM288 278.4v70.4h51.2V278.4H288z m0 224h51.2v-67.2H288v67.2z m51.2 153.6v-67.2H288v73.6l51.2-6.4z m259.2-54.4l-3.2 25.6h220.8c0 9.6-9.6 112-12.8 131.2-6.4 76.8-32 89.6-121.6 99.2l-54.4 6.4-25.6-86.4 64-3.2c41.6-3.2 54.4-6.4 57.6-35.2 3.2-9.6 3.2-19.2 3.2-32h-233.6l16-102.4H448v-73.6h416v73.6l-265.6-3.2z m233.6-99.2H467.2V211.2H608V160l80 3.2v48H832v291.2zM547.2 288v32H608V288h-60.8z m0 140.8H608v-32h-60.8v32zM688 288v32h60.8V288h-60.8z m60.8 140.8v-32h-60.8v32h60.8z" p-id="5317" fill={color} />
    </Svg>
  );
}

集成lucide

npx expo add lucide-react-native

集成expo-router

  1. 安装相关依赖

    npx expo add expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
    
  2. 修改 package.json

    {
      "main": "expo-router/entry"
    }
    
  3. 将根目录的 App.tsx 和 index.ts 文件删除,新增 _layout.tsx 文件

布局

  1. 新建 app/_layout.tsx 文件:

    import { Slot } from 'expo-router'
    import { StyleSheet, Text } from 'react-native'
    import { SafeAreaView } from 'react-native-safe-area-context'
    
    export default function Layout() {
      return (
        <SafeAreaView style={styles.container}>
          <Text style={styles.header}>Header</Text>
    
          <Slot />
    
          <Text style={styles.footer}>Footer</Text>
        </SafeAreaView>
      )
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: '#f5f5f5',
        paddingHorizontal: 16,
      },
      header: {
        fontSize: 20,
        fontWeight: 'bold',
        paddingVertical: 12,
        textAlign: 'center',
        backgroundColor: '#4f46e5',
        color: '#fff',
        borderRadius: 8,
        marginVertical: 8,
      },
      footer: {
        fontSize: 16,
        paddingVertical: 10,
        textAlign: 'center',
        backgroundColor: '#e5e7eb',
        borderRadius: 8,
        marginVertical: 8,
      },
    })
    
  2. 新建

集成 lottie-react-native

安装依赖:

npx expo install lottie-react-native

使用案例:

import React from "react";
import LottieView from "lottie-react-native";

export default function Animation() {
  return (
    <LottieView
      source={require("../path/to/animation.json")}
      style={{width: "100%", height: "100%"}}
      autoPlay
      loop
    />
  );
}

lottie例子网站:https://lottiefiles.com/free-animations

集成zustand和@react-native-async-storage

安装依赖:

npx expo add zustand
npx expo add @react-native-async-storage/async-storage

使用demo:

// store/userStore.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'

interface UserState {
  userInfo: { userId: string, username: string } | null
  token: string | null
  setUserInfo: (userInfo: UserState['userInfo']) => void
  setToken: (token: string) => void
  logout: () => void
}

export const useUserStore = create<UserState>()(
  persist(
    (set, get) => ({
      userInfo: null,
      token: null,
      setUserInfo: userInfo => set({ userInfo }),
      setToken: token => set({ token }),
      logout: () => {
        console.log('当前 token:', get().token) // 使用 get 获取最新状态
        set({ userInfo: null, token: null })
      },
    }),
    {
      name: 'user-storage', // 存储在 AsyncStorage 的 key
      storage: createJSONStorage(() => AsyncStorage),
    },
  ),
)
// App.tsx
import React, { useEffect } from 'react'
import { Button, Text, View } from 'react-native'
import { useUserStore } from '@/store/userStore'

export default function App() {
  const { userInfo, token, setUserInfo, setToken, logout } = useUserStore()

  // 模拟 App 启动后读取状态
  useEffect(() => {
    console.log('用户信息:', userInfo)
    console.log('Token:', token)
  }, [userInfo, token])

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>
        User Info:
        {userInfo ? userInfo.username : '未登录'}
      </Text>
      <Text>
        Token:
        {token || '无'}
      </Text>

      <Button
        title="登录"
        onPress={() => {
          setUserInfo({ userId: '1', username: '张三' })
          setToken('abc123')
        }}
      />

      <Button
        title="退出登录"
        onPress={() => {
          logout()
        }}
      />
    </View>
  )
}

集成moti

npx expo add moti
npx expo add react-native-reanimated react-native-gesture-handler
npx expo add react-native-gesture-handler

引入:

import 'react-native-reanimated'
import 'react-native-gesture-handler'

集成react-native-iconfont-cli

安装:

pnpm add react-native-iconfont-cli -D

BUG位置:https://github.com/iconfont-cli/react-native-iconfont-cli/issues/42

解决fang'an修改模板 node_modules/react-native-iconfont-cli/templates/SingleIcon.tsx.template:

/* eslint-disable */
/* tslint:disable */

import React from 'react';
import type { FunctionComponent } from 'react';
import type { ViewProps } from 'react-native';
#svgComponents#
#helper#

interface Props extends Omit<GProps, 'color'>, ViewProps {
  size?: number;
  color?: string | string[];
}

const #componentName#: FunctionComponent<Props> = ({
  size = #size#,
  color,
  ...rest
}) => {
  return (
    #iconContent#
  );
};

export default React.memo(#componentName#);

生成图标命令:

npx iconfont-rn

配置深度链接

作用:通过链接就可以跳转到该app。

在 app.json 文件下进行配置:

{
  "expo": {
    "scheme":"oneao",
  }
}

然后就可以通过这个配置 从别的应用或者浏览器跳转到你的app:

oneao://courses/react-native

定义 src 路径别名 @

修改 tsconfig.json 文件:

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "strict": true
  }
}

打包

云打包

  1. 安装 EAS CLI

    npm install -g eas-cli
    
  2. 注册并登录 Expo 账户

  3. 配置项目构建

    • 输入以下命令,选择 Android 平台即可。

      eas build:configure
      
  4. 构建 Android 包

    • 默认构建 AAB 包(上传 Google Play):

      eas build --platform android
      
    • 构建 APK(可直接安装):

      eas build -p android --profile preview
      

本地打包(EAS方式)

本地打包(原生方式)

1. 安装 Android Studio

  1. 下载安装

    • 按照官网提示依次安装 SDK 工具和必要组件。

    • 安装完成后,记住 SDK 的安装路径(例如:D:\data\sdk,默认位置是:C:\Users\<你的用户名>\AppData\Local\Android\Sdk),后续需要用到。

  2. 配置环境变量

    • 打开 系统属性 → 高级 → 环境变量。

    • 在 系统变量 中新增:

      变量名:ANDROID_HOME
      变量值:D:\data\sdk  
      
    • 在 Path 中新增:

      %ANDROID_HOME%\platform-tools
      %ANDROID_HOME%\tools
      %ANDROID_HOME%\tools\bin
      
    • 保存设置。

  3. 验证安装

    • 打开命令行,执行:

      echo %ANDROID_HOME%
      
    • 如果输出 SDK 路径,则说明配置成功。

2. 安装 jdk17

  1. 下载安装

    • 下载对应版本解压即可。
    • 安装成功后,记住 jdk 的安装路径,后续需要用到。
  2. 配置环境变量

    • 打开 系统属性 → 高级 → 环境变量。

    • 在 系统变量 中新增:

      变量名:JAVA_HOME
      变量值:D:\devtool\jdk\jdk-17.0.16+8
      
    • 在 Path 中新增:

      %JAVA_HOME%\bin
      
    • 保存设置。

  3. 验证安装

    • 打开命令行,执行:

      java --version
      
    • 如果输出 jdk 的信息,则说明配置成功。

3. Expo 项目启用 Prebuild

  1. 生成原生项目

    npx expo prebuild
    

    然后执行完毕后,会生成 android 目录。

  2. 清除并重新生成原生项目

    npx expo prebuild --clean
    

    会删除已有的 android 和 ios 目录,然后重新生成。

4. 配置 Keystore

4.1 生成密钥

keytool -genkey -v -keystore android-release.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

说明:

  • my-release-key.keystore:生成的密钥文件名,可自定义路径
  • my-key-alias:密钥别名
  • RSA:加密算法
  • 2048:密钥长度
  • 10000:有效期(天)

4.2 手动配置

修改 android/app/build.gradle 文件

  1. 修改 signingConfigs

    signingConfigs {
        release {
            storeFile file('android-release.keystore')  // keystore 文件路径
            storePassword '你的Keystore密码'             // keystore 密码
            keyAlias '你的KeyAlias'                     // 密钥别名
            keyPassword '你的Key密码'                   // key 密码
        }
    
        debug {
            storeFile file('debug.keystore')
            storePassword 'android'
            keyAlias 'androiddebugkey'
            keyPassword 'android'
        }
    }
    
  2. 在 buildTypes 中设置 release 使用 release 签名

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    
        debug {
            signingConfig signingConfigs.debug
        }
    }
    

4.3 脚本配置(推荐)

  1. 在 expo 项目根目录下新增 plugins\withAndroidSignature.js 文件,内容如下:

    // plugins/withAndroidSignature.js
    
    const { withAppBuildGradle } = require("@expo/config-plugins");
    const fs = require("fs");
    const path = require("path");
    
    module.exports = function withAndroidSignature(config) {
        return withAppBuildGradle(config, config => {
            if (config.modResults.language === "groovy") {
                config.modResults.contents = setAndroidSignature(config.modResults.contents);
            } else {
                throw new Error("如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs");
            }
            return config;
        });
    };
    
    function setAndroidSignature(appBuildGradle) {
        if (!fs.existsSync(path.resolve(__dirname, "../credentials.json"))) {
            console.warn("警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。");
            return appBuildGradle;
        }
        const info = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../credentials/credentials.json"), { encoding: "utf8" }));
    
        // 使用正则表达式插入签名信息
        let output = appBuildGradle.replace(
            /(signingConfigs\s*\{)/,
            `$1
            release {
                storeFile file(${JSON.stringify(path.resolve(__dirname, "../credentials/android-release.keystore"))})
                storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
                keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
                keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
            }`,
        );
    
        // 使用正则表达式替换 signingConfig
        output = output.replace(
            /(release\s*\{)[^}]*?signingConfig\s+signingConfigs\.debug/s,
            `$1
                signingConfig signingConfigs.release
    `,
        );
    
        return output;
    }
    
  2. 在 expo 项目根目录下新增 /credentials/credentials.json 文件(该文件主要是 Keystore 的信息):

    {
      "android": {
        "keystore": {
          "keystorePath": "credentials/android-release.keystore",  // Android keystore 文件路径
          "keystorePassword": "123456",                             // keystore 密码
          "keyAlias": "my-key-alias",                               // Key 别名
          "keyPassword": "123456"                                   // Key 密码
        }
      },
      "ios": {
        "provisioningProfilePath": "ios/certs/profile.mobileprovision", // iOS 描述文件路径
        "distributionCertificate": {
          "path": "ios/certs/dist-cert.p12",                              // iOS 分发证书路径
          "password": "password"                                          // 证书密码
        }
      }
    }
    
  3. 在 app.json 中指定这个项目级插件:

    {  
      "expo": {  
        "plugins": [  
          "./plugins/withAndroidSignature"
        ]
      }  
    }
    

5. 打包

  • 编译成 Google Play 商店用的 .aab 文件

    ./gradlew bundleRelease 
    
  • 直接安装的 .apk 文件

    ./gradlew assembleRelease
    

**注意:**这个时候可能会下载 gradle 、apk 等资源,网络速度可能过慢。

**解决方法:**在 android/gradle.properties 添加网络代理:

systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=7897
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7897

打包结果在 android/app/build/outputs/apt/release 包下。

打包体积优化

默认初始项目打包就有 80MB 左右,是因为 expo 默认将 armeabi-v7a、arm64-v8a、x86、x86_64 这些架构的包都打包进来了。

架构名称 位数 设备类型 应用场景 备注
armeabi-v7a 32 位 老旧的 ARM 芯片手机(如部分安卓 5/6 设备) 兼容旧设备 已逐渐被淘汰,但仍有少数旧机型使用
arm64-v8a 64 位 主流安卓手机(大多数真机) ✅ 主力目标平台 目前最主流、性能最佳的架构
x86 32 位 早期安卓模拟器(Intel 架构) 开发调试 真机几乎不会用
x86_64 64 位 现代安卓模拟器 开发调试 用于 Android Studio 模拟器或测试环境

方法1(简单)

找到 android/gradle.properties 文件,找到 reactNativeArchitectures ,根据需要删除即可

reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64

方法2(原生方法 选择更多)

在 android/app/build.gradle 文件中添加或修改以下配置:

android {
    // 其他配置...

    splits {
        abi {
            enable true          // 启用按 CPU 架构拆分
            reset()              // 重置默认架构列表
            include 'arm64-v8a', 'armeabi-v7a'
            universalApk false   // 是否生成包含所有架构的通用 APK(false 表示只生成分架构的独立包)
            // 还有一个 exclude 配置是排除指定架构的
        }
    }

    // 其他配置...
}

说明:

  • enable true:启用架构拆分功能。
  • reset():清空默认的架构配置,避免重复。
  • include:指定要生成的架构包,比如只打 arm64-v8a 和 armeabi-v7a。
  • universalApk false:不生成通用包(即包含所有架构的 APK)。

举例:

执行打包命令后,将生成多个包,例如:

app-arm64-v8a-release.apk
app-armeabi-v7a-release.apk

每个 APK 只包含对应架构的二进制文件,体积更小。

如果把 universalApk 设置为 true,则只会生成一个:

app-universal-release.apk

这个包能在 arm64-v8a 和 armeabi-v7a 架构的设备上运行,但体积最大。

报错

Cannot find module 'react-native-worklets/plugin'

npx expo add react-native-worklets

​