官网: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
```
# 基础配置
## 生成基础文件
```bash
npx expo customize babel.config.js
npx expo customize metro.config.js
```
## 集成 [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. 安装 ` react-native-css-interop` :
```bash
npx expo install react-native-css-interop
```
8. 检验是否生效
```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 || '无'}
)
}
```
## 集成moti
```bash
npx expo add moti
npx expo add react-native-reanimated react-native-gesture-handler
npx expo add react-native-gesture-handler
```
引入:
```ts
import 'react-native-reanimated'
import 'react-native-gesture-handler'
```
## 集成[react-native-iconfont-cli](https://github.com/iconfont-cli/react-native-iconfont-cli)
安装:
```bash
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`:
```tsx
/* 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, ViewProps {
size?: number;
color?: string | string[];
}
const #componentName#: FunctionComponent = ({
size = #size#,
color,
...rest
}) => {
return (
#iconContent#
);
};
export default React.memo(#componentName#);
```
生成图标命令:
```bash
npx iconfont-rn
```
## 配置深度链接
作用:通过链接就可以跳转到该app。
在 `app.json` 文件下进行配置:
```json
{
"expo": {
"scheme":"oneao",
}
}
```
然后就可以通过这个配置 从别的应用或者浏览器跳转到你的app:
```
oneao://courses/react-native
```
## [定义 src 路径别名 @](https://docs.expo.dev/guides/typescript/)
修改 `tsconfig.json` 文件:
```json
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"strict": true
}
}
```
安装 `module-resolver` :
```
pnpm add -D babel-plugin-module-resolver
```
修改 `babel.config.js` :
```js
module.exports = function (api) {
api.cache(true)
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@': './src',
},
},
],
],
}
}
```
# 打包
## 云打包
1. 安装 EAS CLI
```bash
npm install -g eas-cli
```
2. 注册并登录 Expo 账户
- 访问 https://expo.dev/ 注册账户,然后命令行登录:
```bash
expo login
```
3. 配置项目构建
- 输入以下命令,选择 Android 平台即可。
```bash
eas build:configure
```
4. 构建 Android 包
- 默认构建 AAB 包(上传 Google Play):
```bash
eas build --platform android
```
- 构建 APK(可直接安装):
```bash
eas build -p android --profile preview
```
## 本地打包(EAS方式)
## 本地打包(原生方式)
### 1. 安装 [Android Studio]( https://developer.android.com/?hl=zh-cn)
1. 下载安装
- 按照官网提示依次安装 SDK 工具和必要组件。
- 安装完成后,记住 SDK 的安装路径(例如:`D:\data\sdk`,默认位置是:`C:\Users\<你的用户名>\AppData\Local\Android\Sdk`),后续需要用到。
2. 配置环境变量
- 打开 **系统属性 → 高级 → 环境变量**。
- 在 **系统变量** 中新增:
```
变量名:ANDROID_HOME
变量值:D:\data\sdk
```
- 在 **Path** 中新增:
```bash
%ANDROID_HOME%\platform-tools
%ANDROID_HOME%\tools
%ANDROID_HOME%\tools\bin
```
- 保存设置。
3. 验证安装
- 打开命令行,执行:
```
echo %ANDROID_HOME%
```
- 如果输出 SDK 路径,则说明配置成功。
### 2. 安装 [jdk17](https://adoptium.net/zh-CN/temurin/releases?version=8&os=any&arch=any)
1. 下载安装
- 下载对应版本解压即可。
- 安装成功后,记住 jdk 的安装路径,后续需要用到。
2. 配置环境变量
- 打开 **系统属性 → 高级 → 环境变量**。
- 在 **系统变量** 中新增:
```bash
变量名:JAVA_HOME
变量值:D:\devtool\jdk\jdk-17.0.16+8
```
- 在 **Path** 中新增:
```bash
%JAVA_HOME%\bin
```
- 保存设置。
3. 验证安装
- 打开命令行,执行:
```bash
java --version
```
- 如果输出 jdk 的信息,则说明配置成功。
### 3. Expo 项目启用 [Prebuild](https://docs.expo.dev/workflow/prebuild/)
1. 生成原生项目
```bash
npx expo prebuild
```
然后执行完毕后,会生成 `android` 目录。
2. 清除并重新生成原生项目
```bash
npx expo prebuild --clean
```
会删除已有的 `android` 和 `ios` 目录,然后重新生成。
### 4. 配置 Keystore
#### 4.1 生成密钥
```bash
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`
```bash
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 签名
```bash
buildTypes {
release {
signingConfig signingConfigs.release
}
debug {
signingConfig signingConfigs.debug
}
}
```
#### 4.3 脚本配置(推荐)
1. 在 expo 项目根目录下新增 `plugins\withAndroidSignature.js` 文件,内容如下:
```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 的信息):
```json
{
"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` 中指定这个项目级插件:
```json
{
"expo": {
"plugins": [
"./plugins/withAndroidSignature"
]
}
}
```
### 5. 打包
- 编译成 Google Play 商店用的 .aab 文件
```bash
./gradlew bundleRelease
```
- 直接安装的 .apk 文件
```bash
./gradlew assembleRelease
```
**注意:**这个时候可能会下载 `gradle` 、`apk` 等资源,网络速度可能过慢。
**解决方法:**在 `android/gradle.properties` 添加网络代理:
```bash
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=7890
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7890
```
打包结果在 `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` ,根据需要删除即可
```properties
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` 架构的设备上运行,但体积最大。
# 报错
## 检查依赖是否有误
```bash
npx expo-doctor
```
## Cannot find module 'react-native-worklets/plugin'
```bash
npx expo add react-native-worklets
```
## Filename longer than 260 characters
解决方法:
1️⃣ 启用 Windows 长路径支持
通过 **注册表**开启 Windows 对长路径(超过 260 字符)的支持:
- 路径:
```
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem
```
- 键值:`LongPathsEnabled`
- 设置为 `1` 表示开启长路径支持。
✅ 作用:允许系统和应用使用超过 260 个字符的文件路径。
2️⃣ 替换 Ninja 版本
- 旧版本 Ninja(v1.10)在 Windows 下仍然可能受路径长度限制,导致构建 `.o` 文件失败。
- 下载新版 Ninja(v1.12+)并替换 Android SDK 自带的 Ninja:
旧路径(Android SDK 自带):
```
C:\Users\\AppData\Local\Android\Sdk\cmake\3.22.1\bin\ninja.exe
```
> 该路径可能不在这里,C盘这里是默认安装的,如果安装到其他位置的话,那么就换成具体的位置。
新 Ninja 下载地址:
👉 https://github.com/ninja-build/ninja/releases
替换后,Ninja 就可以处理更长的路径,不再报 “Filename longer than 260 characters” 错误。