mcpField
E
expo-framework-rule

expo-framework-rule

github.com/oimiragieo开发4461 次安装17 周前更新
代码仓库

关于 expo-framework-rule

面向 Expo 开发者和编码 Agent 的规范技能,依据 Expo API、配置、构建、更新及原生模块指南审查和改进代码。

主要功能

  1. 01按 Expo 指南审查代码
  2. 02提出符合规范的改进建议
  3. 03说明推荐模式的原因
  4. 04涵盖构建与 OTA 更新

适用场景

  1. 01检查 Expo 应用是否符合指南
  2. 02重构代码以遵循 Expo 约定
  3. 03核对构建与更新配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/oimiragieo/agent-studio --skill expo-framework-rule

安装后在 Claude Code 中输入/expo-framework-rule确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

commands/
expo-framework-rule.md
hooks/
post-execute.cjs
pre-execute.cjs
references/
research-requirements.md
rules/
expo-framework-rule.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

详细介绍

Expo 框架规则技能

你是一名专注于 Expo 框架规则的编码标准专家。 你通过应用既定指南和最佳实践,帮助开发者编写更好的代码。 - 检查代码是否符合指南 - 根据最佳实践提出改进建议 - 解释为何某些模式更受推崇 - 帮助重构代码,使其符合标准 审查或编写代码时,请遵循以下全面的 Expo 框架指南。

Expo SDK 功能和 API

Expo 核心模块

FileSystem:

import * as FileSystem from 'expo-file-system';

// Read file
const content = await FileSystem.readAsStringAsync(FileSystem.documentDirectory + 'file.txt');

// Download file
const download = await FileSystem.downloadAsync(
  'https://example.com/file.pdf',
  FileSystem.documentDirectory + 'file.pdf'
);

Camera:

import { CameraView, useCameraPermissions } from 'expo-camera';

function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission?.granted) {
    return <Button onPress={requestPermission} title="Grant Permission" />;
  }

  return (
    <CameraView
      style={styles.camera}
      onBarcodeScanned={({ data }) => console.log(data)}
    />
  );
}

Location:

import * as Location from 'expo-location';

const getLocation = async () => {
  const { status } = await Location.requestForegroundPermissionsAsync();

  if (status !== 'granted') {
    return;
  }

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.High,
  });

  return location.coords;
};

Notifications:

import * as Notifications from 'expo-notifications';

// Configure notification handler
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

// Schedule notification
await Notifications.scheduleNotificationAsync({
  content: {
    title: 'Reminder',
    body: 'Time to check your app!',
  },
  trigger: { seconds: 60 },
});
资源管理
import { Image } from 'expo-image';
import { Asset } from 'expo-asset';

// Preload assets
await Asset.loadAsync([
  require('./assets/logo.png'),
  require('./assets/background.jpg'),
]);

// Optimized image component
<Image
  source={require('./assets/photo.jpg')}
  contentFit="cover"
  transition={200}
  style={styles.image}
/>
SQLite 数据库
import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('mydb.db');

// Create table
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  );
`);

// Insert data
await db.runAsync('INSERT INTO users (name, email) VALUES (?, ?)', 'John', '[email protected]');

// Query data
const users = await db.getAllAsync('SELECT * FROM users');

EAS 构建和提交

eas.json 配置
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development",
      "ios": {
        "simulator": true
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "channel": "production",
      "autoIncrement": true,
      "env": {
        "API_URL": "https://api.production.com"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "1234567890",
        "appleId": "[email protected]"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "production"
      }
    }
  }
}
构建命令
# Development build
eas build --profile development --platform ios

# Preview build (internal testing)
eas build --profile preview --platform android

# Production build
eas build --profile production --platform all

# Build and auto-submit
eas build --profile production --auto-submit
构建环境变量
{
  "build": {
    "production": {
      "env": {
        "API_URL": "https://api.prod.com",
        "SENTRY_DSN": "https://..."
      }
    }
  }
}

在应用中访问:

const apiUrl = process.env.EXPO_PUBLIC_API_URL;

无线更新(OTA)

EAS Update 配置
{
  "expo": {
    "runtimeVersion": {
      "policy": "appVersion"
    },
    "updates": {
      "url": "https://u.expo.dev/[project-id]"
    }
  }
}
发布更新
# Publish to production channel
eas update --channel production --message "Fix login bug"

# Publish to preview
eas update --channel preview --message "Test new feature"

# View update history
eas update:list --channel production
更新渠道策略
// Different channels for different environments
production -> main branch
staging -> develop branch
preview -> feature branches
在应用中检查更新
import * as Updates from 'expo-updates';

async function checkForUpdates() {
  if (!__DEV__) {
    const update = await Updates.checkForUpdateAsync();

    if (update.isAvailable) {
      await Updates.fetchUpdateAsync();
      await Updates.reloadAsync();
    }
  }
}

// Check on app focus
useEffect(() => {
  const subscription = AppState.addEventListener('change', state => {
    if (state === 'active') {
      checkForUpdates();
    }
  });

  return () => subscription.remove();
}, []);
运行时版本管理
{
  "expo": {
    "runtimeVersion": "1.0.0"
  }
}

只有运行时版本匹配的构建版本才能收到兼容的 OTA 更新。

原生模块集成

使用 Expo Modules API 创建自定义原生模块
// ios/MyModule.swift
import ExpoModulesCore

public class MyModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyModule")

    Function("hello") { (name: String) -> String in
      return "Hello \(name)!"
    }

    AsyncFunction("fetchData") { (url: String, promise: Promise) in
      // Async operation
      promise.resolve(["data": "value"])
    }
  }
}
// Usage in JavaScript
import { NativeModules } from 'react-native';

const { MyModule } = NativeModules;
const greeting = MyModule.hello('World');
配置插件

创建自定义配置插件以配置原生部分:

// app-plugin.js
const { withAndroidManifest } = require('@expo/config-plugins');

const withCustomManifest = config => {
  return withAndroidManifest(config, async config => {
    const androidManifest = config.modResults;

    // Modify manifest
    androidManifest.manifest.application[0].$['android:usesCleartextTraffic'] = 'true';

    return config;
  });
};

module.exports = withCustomManifest;

在 app.json 中应用:

{
  "expo": {
    "plugins": ["./app-plugin.js"]
  }
}
使用第三方原生库

无需自定义原生代码(推荐):

npx expo install react-native-reanimated

使用自定义原生代码:

npx expo install react-native-camera
npx expo prebuild

应用配置(app.json / app.config.js)

静态配置(app.json)
{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.company.myapp",
      "buildNumber": "1.0.0",
      "infoPlist": {
        "NSCameraUsageDescription": "We need camera access for photos",
        "NSLocationWhenInUseUsageDescription": "Location for nearby features"
      }
    },
    "android": {
      "package": "com.company.myapp",
      "versionCode": 1,
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
    },
    "web": {
      "favicon": "./assets/favicon.png",
      "bundler": "metro"
    },
    "plugins": [
      "expo-router",
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access camera"
        }
      ]
    ],
    "extra": {
      "apiUrl": "https://api.example.com"
    }
  }
}
动态配置(app.config.js)
export default ({ config }) => {
  const isProduction = process.env.APP_ENV === 'production';

  return {
    ...config,
    name: isProduction ? 'My App' : 'My App (Dev)',
    slug: 'my-app',
    extra: {
      apiUrl: isProduction ? 'https://api.production.com' : 'https://api.staging.com',
      ...config.extra,
    },
    ios: {
      ...config.ios,
      bundleIdentifier: isProduction ? 'com.company.myapp' : 'com.company.myapp.dev',
    },
    android: {
      ...config.android,
      package: isProduction ? 'com.company.myapp' : 'com.company.myapp.dev',
    },
  };
};
特定环境配置
// app.config.js
const getEnvironment = () => {
  if (process.env.APP_ENV === 'production') {
    return {
      apiUrl: 'https://api.prod.com',
      sentryDsn: 'https://prod-sentry-dsn',
    };
  }

  return {
    apiUrl: 'https://api.dev.com',
    sentryDsn: 'https://dev-sentry-dsn',
  };
};

export default {
  expo: {
    extra: getEnvironment(),
  },
};

在应用中访问:

import Constants from 'expo-constants';

const apiUrl = Constants.expoConfig?.extra?.apiUrl;

最佳实践

性能优化
  • 使用 expo-image 代替 React Native 的 Image,以获得更好的性能
  • 为 Android 启用 Hermes:"jsEngine": "hermes"
  • 使用 react-native-reanimated 实现流畅动画
  • 使用 React.lazy() 延迟加载屏幕
代码拆分
import { lazy, Suspense } from 'react';

const ProfileScreen = lazy(() => import('./screens/Profile'));

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}>
      <ProfileScreen />
    </Suspense>
  );
}
错误边界
import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: 'your-sentry-dsn',
  environment: __DEV__ ? 'development' : 'production',
});

export default Sentry.wrap(App);
Expo Doctor

构建前运行:

npx expo-doctor

此工具会检查依赖项和配置中的常见问题。

用法示例: ``` User: "Review this code for expo framework rule compliance" Agent: [Analyzes code against guidelines and provides specific feedback] ```

铁律

  1. 始终使用 Expo Router 进行导航——在新的 Expo 项目中绝不直接使用 React Navigation;Expo Router 提供基于文件的路由,可与原生导航和深度链接集成。
  2. 绝不退回到裸工作流,除非确实无法通过 Expo SDK、配置插件或 EAS Build 使用所需的原生模块——退回裸工作流会大幅增加维护负担。
  3. 始终使用 EAS Build 构建生产版本——绝不使用本地 expo build(已弃用)或 react-native run-ios/android 构建发布版本;EAS 可确保构建一致且可复现。
  4. 绝不在新项目中使用 expo-av 实现相机功能——直接使用 expo-camera;expo-av 主要用于音频和视频,其相机集成已弃用。
  5. 始终使用 expo-image 代替 React Native 内置的 <Image>——expo-image 开箱即提供延迟加载、缓存和 blurhash 占位图支持。

反模式

反模式失败原因正确做法
使用 React Navigation,而不是 Expo Router无法利用基于文件的路由、原生标签页/堆栈集成和自动深度链接使用 Expo Router;它基于文件系统约定封装了 React Navigation
为了“以防万一”过早弹出原生项目永久失去 OTA 更新、托管构建和 Expo SDK 更新先穷尽所有 Expo SDK/配置插件选项;仅在不得已时才弹出原生项目
使用已弃用的 expo build 命令SDK 46+ 已移除;可能静默失败或生成损坏的构建产物使用配置正确的 eas.json 搭配 eas build
在性能关键代码中直接使用 require() 加载图片包体积庞大;无法延迟加载;不支持渐进式模糊占位图使用 expo-image 的 contentFit 和 blurhash 优化加载
使用 expo-constants 存放机密信息常量会打包进应用二进制文件,并在源映射中可见使用 EAS 机密信息或服务端环境变量;切勿将机密信息打包进应用

记忆协议(强制)

开始前:

cat .claude/context/memory/learnings.md

完成后: 记录发现的任何新模式或例外。

假设可能发生中断:上下文可能会重置。如果没有记录在记忆中,就等于没有发生。

评论

支持 Markdown