React Native 跨平台開發實戰:一份代碼,雙平台上架
React Native 讓你可以使用 JavaScript 和 React 開發原生行動應用,一份代碼同時支援 iOS 和 Android。本文將帶你從環境設置到成功上架的完整旅程。
為什麼選擇 React Native?
優勢分析
跨平台效率:
- 程式碼共用率 85-95%
- 一個團隊維護兩個平台
- 開發速度提升 50%
原生效能:
- 使用原生組件渲染
- 60 FPS 流暢動畫
- 原生模組無縫整合
生態豐富:
- 30萬+ npm 套件可用
- 活躍的社群支援
- Meta 官方維護
熱更新:
- CodePush 即時更新
- 無需重新上架審核
- 快速修復問題
環境設置
macOS 完整設置(支援 iOS + Android)
# 1. 安裝 Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. 安裝 Node.js
brew install node
brew install watchman
# 3. 安裝 React Native CLI
npm install -g react-native-cli
# 4. iOS 開發環境
# 安裝 Xcode(從 App Store)
sudo gem install cocoapods
# 5. Android 開發環境
brew install --cask android-studio
# 設定 Android SDK 環境變數
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
# 6. 建立新專案
npx react-native init MyAwesomeApp
cd MyAwesomeApp
# 7. 啟動專案
# iOS
npx react-native run-ios
# Android
npx react-native run-android
核心概念與組件
基礎組件庫
// App.js - 展示核心組件
import React, { useState } from 'react';
import {
SafeAreaView,
View,
Text,
Image,
TextInput,
Button,
TouchableOpacity,
FlatList,
ScrollView,
StyleSheet,
Alert,
} from 'react-native';
const App = () => {
const [text, setText] = useState('');
const [tasks, setTasks] = useState([]);
const addTask = () => {
if (text.trim()) {
setTasks([...tasks, { id: Date.now().toString(), title: text }]);
setText('');
}
};
const deleteTask = (id) => {
setTasks(tasks.filter(task => task.id !== id));
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>📝 待辦事項</Text>
</View>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="輸入新任務..."
value={text}
onChangeText={setText}
onSubmitEditing={addTask}
/>
<TouchableOpacity style={styles.addButton} onPress={addTask}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.taskItem}>
<Text style={styles.taskText}>{item.title}</Text>
<TouchableOpacity
onPress={() => deleteTask(item.id)}
style={styles.deleteButton}
>
<Text style={styles.deleteButtonText}>✕</Text>
</TouchableOpacity>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>還沒有任務,新增一個吧!</Text>
}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 20,
backgroundColor: '#6200ee',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: 'white',
},
inputContainer: {
flexDirection: 'row',
padding: 16,
backgroundColor: 'white',
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 12,
fontSize: 16,
},
addButton: {
marginLeft: 8,
backgroundColor: '#6200ee',
width: 50,
height: 50,
borderRadius: 25,
alignItems: 'center',
justifyContent: 'center',
},
addButtonText: {
color: 'white',
fontSize: 28,
},
taskItem: {
flexDirection: 'row',
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginTop: 8,
borderRadius: 8,
alignItems: 'center',
},
taskText: {
flex: 1,
fontSize: 16,
},
deleteButton: {
padding: 8,
},
deleteButtonText: {
color: 'red',
fontSize: 20,
},
emptyText: {
textAlign: 'center',
marginTop: 50,
fontSize: 16,
color: '#999',
},
});
export default App;
導航系統實作
React Navigation 設置
# 安裝 React Navigation
npm install @react-navigation/native
npm install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
# iOS 額外步驟
cd ios && pod install && cd ..
// Navigation.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
// 畫面組件
import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';
import SettingsScreen from './screens/SettingsScreen';
import DetailScreen from './screens/DetailScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
// 主頁 Stack
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="HomeMain"
component={HomeScreen}
options={{ title: '首頁' }}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{ title: '詳細資訊' }}
/>
</Stack.Navigator>
);
}
// 主導航
export default function Navigation() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
} else if (route.name === 'Settings') {
iconName = focused ? 'settings' : 'settings-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#6200ee',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeStack} options={{ headerShown: false }} />
<Tab.Screen name="Profile" component={ProfileScreen} options={{ title: '個人資料' }} />
<Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '設定' }} />
</Tab.Navigator>
</NavigationContainer>
);
}
狀態管理
Redux Toolkit 實作
npm install @reduxjs/toolkit react-redux
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import todosReducer from './todosSlice';
export const store = configureStore({
reducer: {
user: userReducer,
todos: todosReducer,
},
});
// store/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 非同步操作:從 API 獲取待辦事項
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async (userId) => {
const response = await fetch(`https://api.example.com/todos?userId=${userId}`);
return response.json();
}
);
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle',
error: null,
},
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now().toString(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.failed, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;
// App.js
import { Provider } from 'react-redux';
import { store } from './store';
export default function App() {
return (
<Provider store={store}>
<Navigation />
</Provider>
);
}
API 整合
// services/api.js
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
const API_BASE_URL = 'https://api.example.com';
// 建立 axios 實例
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
});
// 請求攔截器(添加 token)
api.interceptors.request.use(
async (config) => {
const token = await AsyncStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 回應攔截器(處理錯誤)
api.interceptors.response.use(
(response) => response.data,
async (error) => {
if (error.response?.status === 401) {
// Token 過期,重新登入
await AsyncStorage.removeItem('authToken');
// 導航到登入頁...
}
return Promise.reject(error);
}
);
// API 方法
export const authAPI = {
login: (email, password) =>
api.post('/auth/login', { email, password }),
register: (userData) =>
api.post('/auth/register', userData),
logout: () =>
api.post('/auth/logout'),
};
export const todosAPI = {
getAll: () =>
api.get('/todos'),
create: (todoData) =>
api.post('/todos', todoData),
update: (id, todoData) =>
api.put(`/todos/${id}`, todoData),
delete: (id) =>
api.delete(`/todos/${id}`),
};
export default api;
原生模組整合
相機與圖片選擇
npm install react-native-image-picker
cd ios && pod install && cd ..
// components/ImagePickerComponent.js
import React, { useState } from 'react';
import { View, Button, Image, StyleSheet } from 'react-native';
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
export default function ImagePickerComponent() {
const [imageUri, setImageUri] = useState(null);
const options = {
mediaType: 'photo',
quality: 0.8,
maxWidth: 1024,
maxHeight: 1024,
};
const handleTakePhoto = () => {
launchCamera(options, (response) => {
if (response.didCancel) {
console.log('用戶取消拍照');
} else if (response.errorCode) {
console.log('錯誤:', response.errorMessage);
} else {
setImageUri(response.assets[0].uri);
}
});
};
const handleSelectImage = () => {
launchImageLibrary(options, (response) => {
if (!response.didCancel && !response.errorCode) {
setImageUri(response.assets[0].uri);
}
});
};
return (
<View style={styles.container}>
{imageUri && (
<Image source={{ uri: imageUri }} style={styles.image} />
)}
<View style={styles.buttonContainer}>
<Button title="📷 拍照" onPress={handleTakePhoto} />
<View style={styles.spacing} />
<Button title="🖼️ 選擇圖片" onPress={handleSelectImage} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
image: {
width: '100%',
height: 300,
borderRadius: 12,
marginBottom: 20,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
},
spacing: {
width: 10,
},
});
效能優化
// 使用 React.memo 避免不必要的重新渲染
const TodoItem = React.memo(({ item, onToggle, onDelete }) => {
return (
<View style={styles.item}>
<TouchableOpacity onPress={() => onToggle(item.id)}>
<Text style={item.completed ? styles.completed : styles.active}>
{item.text}
</Text>
</TouchableOpacity>
<Button title="刪除" onPress={() => onDelete(item.id)} />
</View>
);
}, (prevProps, nextProps) => {
// 自定義比較函數
return prevProps.item.id === nextProps.item.id &&
prevProps.item.completed === nextProps.item.completed &&
prevProps.item.text === nextProps.item.text;
});
// 使用 useCallback 記憶回調函數
const App = () => {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<FlatList
data={todos}
renderItem={({ item }) => (
<TodoItem
item={item}
onToggle={handleToggle}
onDelete={handleDelete}
/>
)}
keyExtractor={item => item.id}
// 效能優化配置
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
/>
);
};
上架 App Store
iOS 上架流程
# 1. 更新版本號
# ios/MyApp/Info.plist
# CFBundleShortVersionString: 1.0.0
# CFBundleVersion: 1
# 2. 建立 Release Build
cd ios
xcodebuild archive -workspace MyApp.xcworkspace \
-scheme MyApp -archivePath build/MyApp.xcarchive
# 3. 匯出 IPA
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build \
-exportOptionsPlist ExportOptions.plist
# 4. 上傳到 App Store Connect
# 使用 Transporter 或 Xcode 上傳
Android 上架流程
# 1. 生成簽名金鑰
keytool -genkeypair -v -storetype PKCS12 \
-keystore my-release-key.keystore \
-alias my-key-alias \
-keyalg RSA -keysize 2048 -validity 10000
# 2. 配置 gradle
# android/app/build.gradle
android {
signingConfigs {
release {
storeFile file('my-release-key.keystore')
storePassword 'YOUR_PASSWORD'
keyAlias 'my-key-alias'
keyPassword 'YOUR_PASSWORD'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
# 3. 建立 Release APK/AAB
cd android
./gradlew bundleRelease
# 4. 上傳到 Google Play Console
# 檔案位置: android/app/build/outputs/bundle/release/app-release.aab
總結
React Native 是快速開發跨平台行動應用的最佳選擇之一。關鍵成功因素:
- 熟悉 React 生態系 - 狀態管理、Hooks、組件化
- 掌握原生整合 - 了解 iOS/Android 基本概念
- 效能優化意識 - 避免不必要的重新渲染
- 持續學習更新 - React Native 快速演進
在 BASHCAT,我們擁有豐富的 React Native 開發經驗,可協助您快速打造高品質的跨平台應用。歡迎與我們聯繫討論您的 App 開發需求!