Claude Code 与 React Native 实战:Expo、原生模块与发布检查
用 Claude Code 开发 React Native:Expo 取舍、权限、Metro 错误、模拟器测试、无障碍与发布检查。
先确定移动端边界,再让 Claude Code 动手
Claude Code 用在 React Native 上,价值不只是更快写出一个页面。真正有用的是,它可以阅读项目、改 TypeScript、运行检查命令、说明是否需要重新构建原生包,并在最后留下验证记录。移动端比普通 Web 组件多了很多边界:iOS 权限文案、Android package、Metro 依赖解析、development build、模拟器差异、无障碍和发布前检查。
最弱的请求是“帮我做一个 React Native app”。更稳的请求会先说明:这是 Expo 项目、需要 Expo development build,还是已经有 ios/ 和 android/ 的 bare React Native 项目。还要说明哪些文件可以改,哪些命令需要确认,必须在哪个设备上验证。
官方依据建议固定为 Claude Code overview、Claude Code permissions、Expo 文档、Expo development builds、React Native 环境设置、React Native Native Modules、Metro troubleshooting 和 React Native accessibility。如果团队还没熟悉 Claude Code,可以先看 Claude Code 入门指南,再用权限设置指南收紧边界。
先写 project map
project map 是给 agent 的短工作说明。这里的 harness 可以理解为“智能体的工作脚手架”:告诉 Claude Code 能在哪里移动、怎么判断完成、哪些动作不能自动做。没有它,Claude Code 也能生成代码,但很容易漏掉原生重构、平台差异和发布限制。
flowchart LR
A["Project map"] --> B["Claude Code task"]
B --> C["JS/TS implementation"]
B --> D["Native config"]
C --> E["Metro and unit tests"]
D --> F["Dev build / emulator"]
E --> G["Accessibility check"]
F --> G
G --> H["Release checklist"]
实际项目里可以放这样一个小说明:
# React Native task map
App type: Expo app using TypeScript and Expo Router.
Native runtime: Expo Go for pure JS changes, development build for native libraries.
Targets: Android emulator first, iOS simulator on macOS before release.
Allowed files: app/, components/, hooks/, app.config.ts, metro.config.js, __tests__/.
Do not change: package manager, app slug, bundle identifiers, signing files, .env files.
Verification: npm run lint, npm test, npx expo-doctor, Android emulator smoke test.
Handoff: list changed files, commands run, platform not tested, and any native rebuild needed.
新的 Expo 项目模板可能已经包含面向 AI coding agents 的上下文文件,但既有项目往往有旧 README、旧 Expo SDK 假设、未整理的原生设置。让 Claude Code 先读真实仓库,比相信通用教程更安全。
有意识地选择 Expo 或 bare React Native
React Native 官方文档会区分使用框架开发和直接配置 Android Studio/Xcode 的路径。实务上,新应用通常先用 Expo,已有原生 SDK、复杂 Gradle、Pod 设置或供应商要求的项目,则更适合 bare React Native。
| 判断项 | Expo 更适合 | bare React Native 更适合 |
|---|---|---|
| 启动速度 | 新 MVP、内部工具、学习项目、Expo SDK 能覆盖的功能 | 现有 iOS/Android 代码必须保留 |
| 原生能力 | Camera、SecureStore、通知、config plugin 支持的设置 | 自定义 SDK、支付终端、特殊 Bluetooth、细粒度构建设置 |
| Claude Code 范围 | 主要是 app/、components/、app.config.ts、测试 | ios/、android/、Codegen、Pods、Gradle、生成文件 |
| 验证方式 | Expo Go 或 development build,npx expo-doctor | npm run android、pod install、Xcode/Android Studio 构建 |
不要把 Expo Go 视为发布验证。Expo 官方说明中,Expo Go 是带固定原生库的快速试用环境。只要添加了包含原生代码的库,就应使用 development build,并要求 Claude Code 明确报告需要新二进制包。
干净的 Expo 测试项目可以这样创建:
npx create-expo-app@latest rn-claude-lab
cd rn-claude-lab
npx expo install expo-camera expo-secure-store @react-native-community/netinfo
npm install --save-dev @testing-library/react-native jest-expo @types/jest
npx expo start
Android Emulator 用 a 打开,macOS 上的 iOS Simulator 用 i 打开。如果局域网受限,可以用 npx expo start --tunnel,但它通常比 LAN 和模拟器慢。
为移动端收紧 Claude Code 权限
React Native 项目里,一个命令可能带来很大的副作用。expo prebuild、pod install、gradlew clean、eas build 都有合理场景,但不应该在一个小 UI 修改里静默发生。
Claude Code permissions 可以把 allow、ask、deny 分开。共享的 .claude/settings.json 可以允许安全检查命令,原生再生成和发布构建前要求确认,并阻止读取 secrets 或发布命令。
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm test)",
"Bash(npm run lint)",
"Bash(npx expo-doctor)",
"Bash(adb devices)"
],
"ask": [
"Edit",
"Bash(npx expo prebuild*)",
"Bash(eas build*)",
"Bash(cd ios && bundle exec pod install)"
],
"deny": [
"Read(.env)",
"Read(.env.local)",
"Bash(git push*)",
"Bash(rm -rf*)"
]
}
}
这不是不信任工具,而是让移动端副作用可审查。任务确实需要原生重构时,Claude Code 应先说明原因,而不是直接扩大差异。
实现带相机权限的页面
第一个具体场景是活动签到、库存管理和内部工具常见的 QR 扫描。Expo Camera 当前提供 CameraView 和 useCameraPermissions。给 Claude Code 的要求要写清楚:权限加载中、未授权、相机可用三个状态要分开;按钮要有 accessibility label;扫描成功后停止重复触发。
// components/CameraQrCheck.tsx
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
type ScanResult = {
data: string;
type: string;
} | null;
export function CameraQrCheck() {
const [permission, requestPermission] = useCameraPermissions();
const [scan, setScan] = useState<ScanResult>(null);
if (!permission) {
return (
<View style={styles.center}>
<Text>Checking camera permission...</Text>
</View>
);
}
if (!permission.granted) {
return (
<View style={styles.center}>
<Text style={styles.title}>Camera access is required</Text>
<Text style={styles.body}>
Allow camera access to scan QR codes on this device.
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="Allow camera access"
disabled={!permission.canAskAgain}
onPress={requestPermission}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
!permission.canAskAgain && styles.buttonDisabled,
]}
>
<Text style={styles.buttonText}>Allow camera</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.container}>
<CameraView
style={styles.camera}
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={
scan
? undefined
: ({ data, type }) => {
setScan({ data, type });
}
}
/>
<View style={styles.result}>
<Text accessibilityLiveRegion="polite" style={styles.title}>
{scan ? `Scanned ${scan.type}` : 'Point the camera at a QR code'}
</Text>
{scan ? <Text selectable>{scan.data}</Text> : null}
{scan ? (
<Pressable
accessibilityRole="button"
accessibilityLabel="Scan another QR code"
onPress={() => setScan(null)}
style={styles.button}
>
<Text style={styles.buttonText}>Scan again</Text>
</Pressable>
) : null}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#111827' },
center: { flex: 1, justifyContent: 'center', gap: 12, padding: 24 },
camera: { flex: 1 },
result: { gap: 12, padding: 16, backgroundColor: '#f9fafb' },
title: { fontSize: 18, fontWeight: '700', color: '#111827' },
body: { fontSize: 15, lineHeight: 22, color: '#374151' },
button: {
alignItems: 'center',
borderRadius: 8,
backgroundColor: '#2563eb',
paddingHorizontal: 16,
paddingVertical: 12,
},
buttonPressed: { opacity: 0.75 },
buttonDisabled: { backgroundColor: '#9ca3af' },
buttonText: { color: '#ffffff', fontWeight: '700' },
});
安装 expo-camera 后,这个组件可以直接放进 Expo 项目。权限文案和原生设置应该留在 app config 里,因为部分变化需要重新构建原生应用。
// app.config.ts
import type { ConfigContext, ExpoConfig } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: config.name ?? 'rn-claude-lab',
slug: config.slug ?? 'rn-claude-lab',
ios: {
...config.ios,
bundleIdentifier: 'com.example.rnclaudelab',
infoPlist: {
...config.ios?.infoPlist,
NSCameraUsageDescription: 'Scan QR codes for check-in.',
},
},
android: {
...config.android,
package: 'com.example.rnclaudelab',
permissions: ['CAMERA'],
},
plugins: ['expo-camera'],
});
Expo config plugins 会在 prebuild 和原生构建流程中应用这些配置。给 Claude Code 的指令可以写成:“如果修改 app.config.ts,必须说明是否需要 development build。”这一句能避免很多“Expo Go 能跑,内部分发包失败”的问题。
把 Metro 错误当证据处理
第二个具体场景是 Metro 的 Unable to resolve module,尤其是 monorepo。Metro 官方 troubleshooting 提到清缓存,但清缓存只是整理现场,不是完整诊断。
应该把完整错误、执行命令、包管理器、workspace 结构、最近移动过的文件一起交给 Claude Code。工作区内的 Expo app 有时需要这样的 Metro 配置:
// metro.config.js
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '..');
const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
module.exports = config;
不要盲目添加这段。普通单包 Expo app 通常不需要。让 Claude Code 说明为什么需要 watchFolders;如果原因其实是大小写错误、依赖缺失、Babel 配置过期或 import 路径错误,就不要改 Metro。
原生模块先做计划,不要直接改 native code
第三个场景是接入厂商 SDK 或系统 API。React Native 现在的 Native Modules 文档围绕 Turbo Native Modules 和 Codegen 展开,所以好的任务应先固定 TypeScript 侧接口,再讨论 Android 和 iOS 实现。
如果 Expo SDK 的 Camera、SecureStore、NetInfo 足够,就不要自写原生模块。只有支付终端、特殊 Bluetooth、企业认证 SDK 这类场景,才值得进入 bare React Native 或 Expo Modules。
Implement a native bridge plan, not the full code yet.
Goal: expose a device serial reader to TypeScript.
First output:
1. TypeScript interface and error model.
2. Android/iOS files that would need edits.
3. Build commands for each platform.
4. Risks: permissions, threading, simulator limitations, release signing.
Do not edit ios/ or android/ until the plan is reviewed.
这会让前十分钟慢一点,但能让后面的审查快很多。Kotlin、Swift、Pods、Gradle 的差异成本高,边界必须先清楚。
把测试和无障碍放进同一个任务
React Native accessibility API 会把信息交给 VoiceOver、TalkBack 等辅助技术。视图需要被读屏时,label 和 role 是实现的一部分,不是最后润色。
先写一个未授权状态的单元测试:
// __tests__/CameraQrCheck.test.tsx
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import { CameraQrCheck } from '../components/CameraQrCheck';
const mockRequestPermission = jest.fn();
jest.mock('expo-camera', () => ({
CameraView: 'CameraView',
useCameraPermissions: () => [
{ granted: false, canAskAgain: true },
mockRequestPermission,
],
}));
describe('CameraQrCheck', () => {
beforeEach(() => {
mockRequestPermission.mockClear();
});
it('requests camera permission from the empty state', () => {
render(<CameraQrCheck />);
fireEvent.press(screen.getByText('Allow camera'));
expect(mockRequestPermission).toHaveBeenCalledTimes(1);
});
});
然后在设备或模拟器上检查权限弹窗、拒绝后的路径、旋转、弱光扫描和读屏标签。交接记录要写清哪个平台测过。“Android emulator passed, iOS not tested”有价值;“looks good”没有价值。
发布前检查要命令化
React Native 的 Dev Menu 和 LogBox 属于开发工具,release build 行为不同。Claude Code 完成后应返回命令证据,而不是只说“已经实现”。
npm run lint
npm test -- --runInBand
npx expo-doctor
npx expo start -c
adb devices
adb shell input keyevent 82
npx expo run:android
# macOS only:
npx expo run:ios
使用 EAS Build 时,保持 development、preview、production profile 分离。除非任务明确要求,不要让 Claude Code 改 bundle identifier、Android package、签名文件或 production profile。
实际用例与失败模式
第一个用例是新 Expo MVP。登录、QR 扫描、安全本地存储、简单 API 和小型后台流程,可以拆成 screen、hook、test、config。若应用有收入目标,analytics 和 CTA 行为也应进入完成条件。
第二个用例是修 existing bare React Native app。Metro 错误、Android-only crash、iOS 权限文案、原生 SDK 升级,都适合 Claude Code,但要要求它分类错误、保持最小差异,并说明是否需要原生重构。
第三个用例是原生 SDK 接入前调研。支付、健康数据、Bluetooth、企业认证,都应该先整理支持 OS、权限、应用商店审核风险、模拟器限制和测试设备。可以结合review workflow checklist 和 TDD guide。
常见失败也很固定:Expo Go 成功不等于发布成功;清缓存不等于根因分析;平台检查不能最后才做;无障碍 label 不该事后补;原生构建命令不该没有理由地运行。这些是流程问题,不是 AI 本身的问题。
CTA:把移动端流程模板化
React Native 中最值得模板化的是 project map、权限文件、验证命令和 review checklist。个人开发可以先拿免费 cheatsheet。需要可复用 prompt 和设置材料时,看 ClaudeCodeLab 产品。团队要在真实仓库里整理 Expo/bare React Native 规则、CI、发布 gate 和培训,可以使用 Claude Code 培训与咨询。
相关内容还可以继续读 Claude Code React 开发指南 和 Flutter/Dart 指南。
把本文流程实际试过后,Masa 发现最能减少返工的三件事是:实现前先决定 Expo 还是 bare;只要改 app.config.ts 就要求写 development-build 说明;先在 Android Emulator 上验证权限和 Metro。对 Camera、SecureStore 这类原生功能来说,验证边界比代码生成速度更重要。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。