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 overviewClaude Code permissionsExpo 文档Expo development buildsReact Native 环境设置React Native Native ModulesMetro troubleshootingReact 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 androidpod 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 prebuildpod installgradlew cleaneas 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 当前提供 CameraViewuseCameraPermissions。给 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 时,保持 developmentpreviewproduction 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 checklistTDD 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,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

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