expo-framework-rule について
Expo API、設定、ビルド、アップデート、ネイティブモジュールの指針に基づき、Expo プロジェクトのコードをレビュー・改善するスキルです。Expo 開発者やコーディングエージェント向けです。
主な機能
- 01Expo の指針に沿ったコードレビュー
- 02規約に基づく改善提案
- 03推奨パターンの理由を説明
- 04ビルドと OTA アップデートに対応
活用シーン
- 01Expo アプリの規約準拠を確認
- 02Expo の慣例に沿うコードへのリファクタリング
- 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.mdhooks/post-execute.cjspre-execute.cjsreferences/research-requirements.mdrules/expo-framework-rule.mdschemas/input.schema.jsonoutput.schema.jsonscripts/main.cjstemplates/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 Build と Submit
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;
Over-the-Air(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');
Config Plugins
ネイティブ設定用のカスタム Config Plugin を作成します。
// 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;
ベストプラクティス
パフォーマンスの最適化
- パフォーマンスを向上させるため、React Native の
Imageの代わりにexpo-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] ```絶対に守るべきルール
- 必ずナビゲーションには Expo Router を使用してください。新しい Expo プロジェクトで React Navigation を直接使用してはいけません。Expo Router は、ネイティブナビゲーションやディープリンクと統合されたファイルベースのルーティングを提供します。
- 絶対に、必要なネイティブモジュールが Expo SDK、Config Plugins、または EAS Build では本当に利用できない場合を除き、Bare ワークフローに移行してはいけません。移行すると、メンテナンスの負担が桁違いに増加します。
- 必ず本番ビルドには EAS Build を使用してください。非推奨のローカル
expo buildや、リリースビルドでのreact-native run-ios/androidは使用してはいけません。EAS を使うことで、一貫性のある再現可能なビルドが実現します。 - 絶対に新しいプロジェクトでカメラに
expo-avを使用してはいけません。カメラにはexpo-cameraを直接使用してください。expo-avは音声と動画を中心としたもので、カメラとの統合は非推奨です。 - 必ずReact Native 組み込みの
<Image>の代わりにexpo-imageを使用してください。expo-imageは遅延読み込み、キャッシュ、Blurhash プレースホルダーへの対応を標準で提供します。
アンチパターン
| アンチパターン | 失敗する理由 | 正しい方法 |
|---|---|---|
| Expo Router の代わりに React Navigation を使用する | ファイルベースのルーティング、ネイティブのタブ/スタック統合、自動ディープリンクを利用できません | Expo Router を使用します。ファイルシステムの規則で React Navigation をラップしています |
| 「念のため」に早い段階でイジェクトする | OTA アップデート、マネージドビルド、Expo SDK のアップデートを永久に利用できなくなります | まず Expo SDK/設定プラグインの選択肢をすべて試し、イジェクトは最後の手段にします |
非推奨の expo build コマンドを使用する | SDK 46 以降では削除されており、エラーが表示されないまま失敗したり、壊れた成果物が生成されたりします | 適切に設定した eas.json とともに eas build を使用します |
パフォーマンスが重要なコードで画像に直接 require() を使用する | バンドルが大きくなり、遅延読み込みができず、段階的なぼかしプレースホルダーも表示できません | 最適化された読み込みには contentFit と blurhash を設定した expo-image を使用します |
シークレットに expo-constants を使用する | 定数はアプリのバイナリにバンドルされ、ソースマップから確認できます | EAS のシークレットまたはサーバー側の環境変数を使用し、シークレットは決してバンドルしないでください |
メモリプロトコル(必須)
開始前:
cat .claude/context/memory/learnings.md
完了後: 新たに発見したパターンや例外を記録してください。
中断を想定してください。コンテキストがリセットされる可能性があります。メモリに記録されていなければ、実行されていないものとみなします。
コメント
Markdown 対応