37 KiB
初始化项目
创建项目命令:
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
-
安装依赖
npx expo add nativewind react-native-reanimated react-native-safe-area-context npx expo add tailwindcss@^3.4.17 -- --save-dev -
运行
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: [], } -
创建
global.css文件:@tailwind base; @tailwind components; @tailwind utilities;注意在全局引入
-
修改
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", ], }; }; -
修改
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' }) -
新建
src/types/nativewind-env.d.ts文件:/// <reference types="nativewind/types" /> -
检验是否生效
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
-
安装依赖:
npx expo add @gluestack-ui/core @gluestack-ui/utils react-native-svg @gluestack/ui-next-adapter -
从 github 复制必须的组件到
@/components/ui/gluestack-ui-provider。 -
修改
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)', }, }, }, } -
使用
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
-
安装相关依赖
npx expo add expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar -
修改
package.json{ "main": "expo-router/entry" } -
将根目录的
App.tsx和index.ts文件删除,新增_layout.tsx文件
布局
-
新建
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, }, }) -
新建
集成 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
}
}
打包
云打包
-
安装 EAS CLI
npm install -g eas-cli -
注册并登录 Expo 账户
-
访问 https://expo.dev/ 注册账户,然后命令行登录:
expo login
-
-
配置项目构建
-
输入以下命令,选择 Android 平台即可。
eas build:configure
-
-
构建 Android 包
-
默认构建 AAB 包(上传 Google Play):
eas build --platform android -
构建 APK(可直接安装):
eas build -p android --profile preview
-
本地打包(EAS方式)
本地打包(原生方式)
1. 安装 Android Studio
-
下载安装
-
按照官网提示依次安装 SDK 工具和必要组件。
-
安装完成后,记住 SDK 的安装路径(例如:
D:\data\sdk,默认位置是:C:\Users\<你的用户名>\AppData\Local\Android\Sdk),后续需要用到。
-
-
配置环境变量
-
打开 系统属性 → 高级 → 环境变量。
-
在 系统变量 中新增:
变量名:ANDROID_HOME 变量值:D:\data\sdk -
在 Path 中新增:
%ANDROID_HOME%\platform-tools %ANDROID_HOME%\tools %ANDROID_HOME%\tools\bin -
保存设置。
-
-
验证安装
-
打开命令行,执行:
echo %ANDROID_HOME% -
如果输出 SDK 路径,则说明配置成功。
-
2. 安装 jdk17
-
下载安装
- 下载对应版本解压即可。
- 安装成功后,记住 jdk 的安装路径,后续需要用到。
-
配置环境变量
-
打开 系统属性 → 高级 → 环境变量。
-
在 系统变量 中新增:
变量名:JAVA_HOME 变量值:D:\devtool\jdk\jdk-17.0.16+8 -
在 Path 中新增:
%JAVA_HOME%\bin -
保存设置。
-
-
验证安装
-
打开命令行,执行:
java --version -
如果输出 jdk 的信息,则说明配置成功。
-
3. Expo 项目启用 Prebuild
-
生成原生项目
npx expo prebuild然后执行完毕后,会生成
android目录。 -
清除并重新生成原生项目
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 文件
-
修改
signingConfigssigningConfigs { 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' } } -
在
buildTypes中设置 release 使用 release 签名buildTypes { release { signingConfig signingConfigs.release } debug { signingConfig signingConfigs.debug } }
4.3 脚本配置(推荐)
-
在 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; } -
在 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" // 证书密码 } } } -
在
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