官网:https://expo.dev/ # 初始化项目 创建项目命令: ```bash pnpm create expo-app@latest --template blank-typescript ``` [官网模板](https://docs.expo.dev/more/create-expo/)如下: | 模板名称 | 描述 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | [`default`](https://github.com/expo/expo/tree/main/templates/expo-template-default) | 默认模板,用于构建多页面应用。包含推荐工具,如 **Expo CLI**、**Expo Router** 路由库,并启用 **TypeScript** 配置。适合大多数应用。 | | [`blank`](https://github.com/expo/expo/tree/main/templates/expo-template-blank) | 最简模板,仅安装最基本依赖,不包含导航配置。 | | [`blank-typescript`](https://github.com/expo/expo/tree/main/templates/expo-template-blank-typescript) | 空白模板,但启用了 **TypeScript** 支持。 | | [`tabs`](https://github.com/expo/expo/tree/main/templates/expo-template-tabs) | 启用基于文件的路由系统(Expo Router),并默认使用 **TypeScript**,适合多标签页结构的应用。 | | [`bare-minimum`](https://github.com/expo/expo/tree/main/templates/expo-template-bare-minimum) | 带有原生目录(`android` 与 `ios`)的空白模板,创建时会自动运行 [`npx expo prebuild`](https://docs.expo.dev/workflow/prebuild)。适合需要直接修改原生代码的项目。 | **启动项目:** ```bash npx expo start ``` # 基础配置 ## 集成 [antfu-eslint](https://github.com/antfu/eslint-config) 可以使用快捷命令(推荐): ```bash pnpm dlx @antfu/eslint-config@latest ``` 安装: ```bash pnpm i -D eslint @antfu/eslint-config ``` 新建 `eslint.config.mjs` 文件: ```js 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 插件 ```json { // 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/v5/getting-started/installation) 直接参考官网:https://www.nativewind.dev/docs/getting-started/installation 1. 安装依赖 ```bash 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` 文件,添加配置: ```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` 文件: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` 注意在全局引入 4. 修改 `babel.config.js` 文件: > 如果没有该文件,执行 `npx expo customize babel.config.js` 命令创建。 ```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` 命令创建。 ```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` 文件: ```ts /// ``` 7. 检验是否生效 ```tsx import "./global.css" import { Text, View } from "react-native"; export default function App() { return ( Welcome to Nativewind! ); } ``` 如果不生效,记得修改 `tailwind.config.js` 的目标路径: ```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](https://gluestack.io/ui/docs/home/getting-started/installation) ### 方式一:CLI ```bash npx gluestack-ui init ``` ### 方式二:Manual **前置条件:**需要安装 `nativewind` 1. 安装依赖: ```bash npx expo add @gluestack-ui/core @gluestack-ui/utils react-native-svg @gluestack/ui-next-adapter ``` 2. 从 [github](https://github.com/gluestack/gluestack-ui/tree/main/src/components/ui/gluestack-ui-provider) 复制必须的组件到 `@/components/ui/gluestack-ui-provider`。 3. 修改 `tailwind.config.js` 文件: ```bash /** @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)/)', 50: 'rgb(var(--color-primary-50)/)', 100: 'rgb(var(--color-primary-100)/)', 200: 'rgb(var(--color-primary-200)/)', 300: 'rgb(var(--color-primary-300)/)', 400: 'rgb(var(--color-primary-400)/)', 500: 'rgb(var(--color-primary-500)/)', 600: 'rgb(var(--color-primary-600)/)', 700: 'rgb(var(--color-primary-700)/)', 800: 'rgb(var(--color-primary-800)/)', 900: 'rgb(var(--color-primary-900)/)', 950: 'rgb(var(--color-primary-950)/)', }, secondary: { 0: 'rgb(var(--color-secondary-0)/)', 50: 'rgb(var(--color-secondary-50)/)', 100: 'rgb(var(--color-secondary-100)/)', 200: 'rgb(var(--color-secondary-200)/)', 300: 'rgb(var(--color-secondary-300)/)', 400: 'rgb(var(--color-secondary-400)/)', 500: 'rgb(var(--color-secondary-500)/)', 600: 'rgb(var(--color-secondary-600)/)', 700: 'rgb(var(--color-secondary-700)/)', 800: 'rgb(var(--color-secondary-800)/)', 900: 'rgb(var(--color-secondary-900)/)', 950: 'rgb(var(--color-secondary-950)/)', }, tertiary: { 50: 'rgb(var(--color-tertiary-50)/)', 100: 'rgb(var(--color-tertiary-100)/)', 200: 'rgb(var(--color-tertiary-200)/)', 300: 'rgb(var(--color-tertiary-300)/)', 400: 'rgb(var(--color-tertiary-400)/)', 500: 'rgb(var(--color-tertiary-500)/)', 600: 'rgb(var(--color-tertiary-600)/)', 700: 'rgb(var(--color-tertiary-700)/)', 800: 'rgb(var(--color-tertiary-800)/)', 900: 'rgb(var(--color-tertiary-900)/)', 950: 'rgb(var(--color-tertiary-950)/)', }, error: { 0: 'rgb(var(--color-error-0)/)', 50: 'rgb(var(--color-error-50)/)', 100: 'rgb(var(--color-error-100)/)', 200: 'rgb(var(--color-error-200)/)', 300: 'rgb(var(--color-error-300)/)', 400: 'rgb(var(--color-error-400)/)', 500: 'rgb(var(--color-error-500)/)', 600: 'rgb(var(--color-error-600)/)', 700: 'rgb(var(--color-error-700)/)', 800: 'rgb(var(--color-error-800)/)', 900: 'rgb(var(--color-error-900)/)', 950: 'rgb(var(--color-error-950)/)', }, success: { 0: 'rgb(var(--color-success-0)/)', 50: 'rgb(var(--color-success-50)/)', 100: 'rgb(var(--color-success-100)/)', 200: 'rgb(var(--color-success-200)/)', 300: 'rgb(var(--color-success-300)/)', 400: 'rgb(var(--color-success-400)/)', 500: 'rgb(var(--color-success-500)/)', 600: 'rgb(var(--color-success-600)/)', 700: 'rgb(var(--color-success-700)/)', 800: 'rgb(var(--color-success-800)/)', 900: 'rgb(var(--color-success-900)/)', 950: 'rgb(var(--color-success-950)/)', }, warning: { 0: 'rgb(var(--color-warning-0)/)', 50: 'rgb(var(--color-warning-50)/)', 100: 'rgb(var(--color-warning-100)/)', 200: 'rgb(var(--color-warning-200)/)', 300: 'rgb(var(--color-warning-300)/)', 400: 'rgb(var(--color-warning-400)/)', 500: 'rgb(var(--color-warning-500)/)', 600: 'rgb(var(--color-warning-600)/)', 700: 'rgb(var(--color-warning-700)/)', 800: 'rgb(var(--color-warning-800)/)', 900: 'rgb(var(--color-warning-900)/)', 950: 'rgb(var(--color-warning-950)/)', }, info: { 0: 'rgb(var(--color-info-0)/)', 50: 'rgb(var(--color-info-50)/)', 100: 'rgb(var(--color-info-100)/)', 200: 'rgb(var(--color-info-200)/)', 300: 'rgb(var(--color-info-300)/)', 400: 'rgb(var(--color-info-400)/)', 500: 'rgb(var(--color-info-500)/)', 600: 'rgb(var(--color-info-600)/)', 700: 'rgb(var(--color-info-700)/)', 800: 'rgb(var(--color-info-800)/)', 900: 'rgb(var(--color-info-900)/)', 950: 'rgb(var(--color-info-950)/)', }, typography: { 0: 'rgb(var(--color-typography-0)/)', 50: 'rgb(var(--color-typography-50)/)', 100: 'rgb(var(--color-typography-100)/)', 200: 'rgb(var(--color-typography-200)/)', 300: 'rgb(var(--color-typography-300)/)', 400: 'rgb(var(--color-typography-400)/)', 500: 'rgb(var(--color-typography-500)/)', 600: 'rgb(var(--color-typography-600)/)', 700: 'rgb(var(--color-typography-700)/)', 800: 'rgb(var(--color-typography-800)/)', 900: 'rgb(var(--color-typography-900)/)', 950: 'rgb(var(--color-typography-950)/)', white: '#FFFFFF', gray: '#D4D4D4', black: '#181718', }, outline: { 0: 'rgb(var(--color-outline-0)/)', 50: 'rgb(var(--color-outline-50)/)', 100: 'rgb(var(--color-outline-100)/)', 200: 'rgb(var(--color-outline-200)/)', 300: 'rgb(var(--color-outline-300)/)', 400: 'rgb(var(--color-outline-400)/)', 500: 'rgb(var(--color-outline-500)/)', 600: 'rgb(var(--color-outline-600)/)', 700: 'rgb(var(--color-outline-700)/)', 800: 'rgb(var(--color-outline-800)/)', 900: 'rgb(var(--color-outline-900)/)', 950: 'rgb(var(--color-outline-950)/)', }, background: { 0: 'rgb(var(--color-background-0)/)', 50: 'rgb(var(--color-background-50)/)', 100: 'rgb(var(--color-background-100)/)', 200: 'rgb(var(--color-background-200)/)', 300: 'rgb(var(--color-background-300)/)', 400: 'rgb(var(--color-background-400)/)', 500: 'rgb(var(--color-background-500)/)', 600: 'rgb(var(--color-background-600)/)', 700: 'rgb(var(--color-background-700)/)', 800: 'rgb(var(--color-background-800)/)', 900: 'rgb(var(--color-background-900)/)', 950: 'rgb(var(--color-background-950)/)', error: 'rgb(var(--color-background-error)/)', warning: 'rgb(var(--color-background-warning)/)', muted: 'rgb(var(--color-background-muted)/)', success: 'rgb(var(--color-background-success)/)', info: 'rgb(var(--color-background-info)/)', light: '#FBFBFB', dark: '#181719', }, indicator: { primary: 'rgb(var(--color-indicator-primary)/)', info: 'rgb(var(--color-indicator-info)/)', error: 'rgb(var(--color-indicator-error)/)', }, }, 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` 包括着整个组件: ```tsx // App.tsx import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; import '@/global.css'; // Make sure this path is correct export default function App() { return ( {/* Add your app code here */} ); } ``` ### 安装组件测试 下载对应组件: ```bash npx gluestack-ui add button ``` 引入使用: ```tsx 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 ( Welcome to Nativewind! ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16, } }) ``` ## 集成[@expo/vector-icons](https://docs.expo.dev/guides/icons/#expovector-icons) 图标网站地址:https://icons.expo.fyi/Index 该插件默认跟着expo安装了。可以直接使用,如果没有的话,则执行命令安装: ``` npx expo install @expo/vector-icons ``` 测试: ```tsx import { View, StyleSheet } from 'react-native'; import Ionicons from '@expo/vector-icons/Ionicons'; export default function App() { return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); ``` 如果需要没有想要的图标,可以用下面的方式: 安装 `svg` 依赖 ```bash npx expo install react-native-svg ``` 然后在components下面创建一个 `Icons.tsx` 文件,用来管理所有的图标 ```tsx import Svg, { Circle, Rect, Path } from 'react-native-svg'; interface IconProps { size?: string | number; color?: string; } // 测试图标 export const DemoIcon: React.FC = ({ size = 30, color = '#fff', ...rest }) => { return ( ); } // 招聘 export const ZhaoPinIcon: React.FC = ({ size = 30, color = '#fff', ...rest }) => { return ( ); } ``` ## 集成[lucide](https://lucide.dev/icons/) ``` npx expo add lucide-react-native ``` ## 集成[expo-router](expo-router) 1. 安装相关依赖 ```bash 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` 文件: ```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 ( Header Footer ) } 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](https://github.com/lottie-react-native/lottie-react-native) 安装依赖: ```bash npx expo install lottie-react-native ``` 使用案例: ```tsx import React from "react"; import LottieView from "lottie-react-native"; export default function Animation() { return ( ); } ``` lottie例子网站:https://lottiefiles.com/free-animations ## 集成zustand和@react-native-async-storage 安装依赖: ```bash npx expo add zustand npx expo add @react-native-async-storage/async-storage ``` 使用demo: ```ts // 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()( 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), }, ), ) ``` ```tsx // 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 ( User Info: {userInfo ? userInfo.username : '未登录'} Token: {token || '无'}