Use Cases (更新: 2026/6/2)

Claude Code 与 React Native 实战:Expo、原生模块与发布检查

用 Claude Code 开发 React Native:Expo 取舍、权限、Metro 错误、模拟器测试、无障碍与发布检查。

Claude Code 与 React Native 实战:Expo、原生模块与发布检查

先确定移动端边界,再让 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-doctornpm 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 这类原生功能来说,验证边界比代码生成速度更重要。

#Claude Code #React Native #移动开发 #Expo #TypeScript
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。