返回部落格
精選文章

React Native 跨平台開發實戰:一份代碼,雙平台上架

從零開始學習 React Native 跨平台開發,涵蓋環境設置、核心概念、狀態管理、原生模組整合,到最終上架 App Store 和 Google Play 的完整流程。

BASHCAT 技術團隊
14 分鐘閱讀
#React Native#跨平台開發#Mobile App#iOS#Android#App開發

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 是快速開發跨平台行動應用的最佳選擇之一。關鍵成功因素:

  1. 熟悉 React 生態系 - 狀態管理、Hooks、組件化
  2. 掌握原生整合 - 了解 iOS/Android 基本概念
  3. 效能優化意識 - 避免不必要的重新渲染
  4. 持續學習更新 - React Native 快速演進

在 BASHCAT,我們擁有豐富的 React Native 開發經驗,可協助您快速打造高品質的跨平台應用。歡迎與我們聯繫討論您的 App 開發需求!

延伸閱讀

探索更多相關的技術洞察與開發經驗分享

更多 mobile 文章

即將推出更多相關技術分享

查看全部文章