引言:跨平台开发的兴起与挑战
在移动应用开发领域,跨平台开发已经成为一种主流趋势。随着React Native、Flutter、Xamarin等框架的成熟,开发者能够使用单一代码库构建运行在iOS、Android甚至Web平台的应用程序。这种开发模式不仅大幅降低了开发成本,还缩短了产品上线周期。然而,跨平台开发并非一帆风顺,它在带来便利的同时也引入了新的挑战。本文将通过一个完整的实战案例,详细剖析从零到一的跨平台应用开发全过程,并深度解析开发过程中可能遇到的常见问题及其解决方案。
案例背景:一个社交阅读应用的诞生
我们选择的案例是一个名为”ReadShare”的社交阅读应用。该应用的核心功能包括:用户注册登录、书籍搜索与展示、阅读进度同步、社交分享和评论系统。项目初期,团队面临的主要挑战是需要同时支持iOS和Android平台,且开发资源有限(仅3名前端开发者)。经过技术选型,团队最终选择了React Native作为主要开发框架,辅以TypeScript增强代码健壮性,后端采用Node.js + GraphQL构建API服务。
技术栈选择详解
前端技术栈:
- React Native 0.72(支持新版架构)
- TypeScript 5.0(强类型约束)
- React Navigation 6(路由管理)
- Redux Toolkit(状态管理)
- Axios(网络请求)
后端技术栈:
- Node.js 18 LTS
- Apollo Server(GraphQL服务)
- Prisma(ORM,支持PostgreSQL)
- Redis(缓存与会话管理)
基础设施:
- AWS ECR/ECS(容器化部署)
- GitHub Actions(CI/CD流水线)
- Sentry(错误监控)
实战开发流程详解
阶段一:项目初始化与架构设计
1. 环境搭建与脚手架创建
# 使用React Native官方模板初始化项目
npx react-native init ReadShare --template react-native-template-typescript
# 安装核心依赖
npm install @react-navigation/native @react-navigation/stack
npm install @reduxjs/toolkit react-redux
npm install axios graphql-request
npm install -D @types/react-native @types/node
2. 项目目录结构设计
/src
/api # API客户端封装
/components # 可复用UI组件
/hooks # 自定义Hooks
/navigation # 路由配置
/screens # 页面组件
/store # Redux状态管理
/types # TypeScript类型定义
/utils # 工具函数
/assets # 静态资源
3. TypeScript配置优化
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["es2017"],
"jsx": "react-native",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
}
4. Redux状态管理架构
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import bookReducer from './slices/bookSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
books: bookReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/store/slices/authSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { loginAPI } from '@/api/auth';
interface AuthState {
token: string | null;
user: User | null;
loading: boolean;
}
const initialState: AuthState = {
token: null,
user: null,
loading: false,
};
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials: { email: string; password: string }) => {
const response = await loginAPI(credentials);
return response.data;
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.token = null;
state.user = null;
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.token = action.payload.token;
state.user = action.payload.user;
});
},
});
export const { logout } = authSlice.actions;
export default authSlice.reducer;
阶段二:核心功能开发
1. 认证模块实现
// src/api/auth.ts
import axios from 'axios';
import { API_BASE_URL } from '@/config';
export const loginAPI = async (credentials: { email: string; password: string }) => {
try {
const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials);
return response.data;
} catch (error) {
throw new Error('登录失败,请检查网络连接');
}
};
// src/screens/LoginScreen.tsx
import React, { useState } from 'react';
import { View, TextInput, Button, Text, StyleSheet } from 'react-native';
import { useDispatch } from 'react-redux';
import { loginUser } from '@/store/slices/authSlice';
const LoginScreen = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch();
const handleLogin = async () => {
try {
await dispatch(loginUser({ email, password })).unwrap();
// 登录成功后的导航逻辑
} catch (error) {
alert('登录失败: ' + error.message);
}
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="邮箱"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="密码"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="登录" onPress={handleLogin} />
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', padding: 20 },
input: { borderWidth: 1, borderColor: '#ccc', padding: 10, marginBottom: 10, borderRadius: 5 },
});
export default LoginScreen;
2. 书籍列表与详情页面
// src/screens/BookListScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, FlatList, Text, TouchableOpacity, StyleSheet, Image } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { fetchBooks } from '@/api/books';
import { Book } from '@/types';
const BookListScreen = () => {
const [books, setBooks] = useState<Book[]>([]);
const [loading, setLoading] = useState(true);
const navigation = useNavigation();
useEffect(() => {
loadBooks();
}, []);
const loadBooks = async () => {
try {
const data = await fetchBooks();
setBooks(data);
} catch (error) {
console.error('Failed to load books:', error);
} finally {
setLoading(false);
}
};
const renderBookItem = ({ item }: { item: Book }) => (
<TouchableOpacity
style={styles.itemContainer}
onPress={() => navigation.navigate('BookDetail', { bookId: item.id })}
>
<Image source={{ uri: item.coverUrl }} style={styles.cover} />
<View style={styles.info}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.author}>{item.author}</Text>
<Text style={styles.rating}>★ {item.rating}</Text>
</View>
</TouchableOpacity>
);
if (loading) {
return (
<View style={styles.center}>
<Text>加载中...</Text>
</View>
);
}
return (
<FlatList
data={books}
renderItem={renderBookItem}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list}
/>
);
};
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
list: { padding: 10 },
itemContainer: {
flexDirection: 'row',
padding: 10,
marginBottom: 10,
backgroundColor: '#fff',
borderRadius: 8,
elevation: 2,
},
cover: { width: 60, height: 80, borderRadius: 4 },
info: { marginLeft: 10, flex: 1 },
title: { fontSize: 16, fontWeight: 'bold' },
author: { color: '#666', marginTop: 2 },
rating: { color: '#f39c12', marginTop: 2 },
});
export default BookListScreen;
3. 阅读进度同步功能
// src/hooks/useReadingProgress.ts
import { useEffect, useState } from 'react';
import { syncReadingProgress } from '@/api/reading';
import { useAppSelector } from '@/store/hooks';
export const useReadingProgress = (bookId: string, initialProgress: number) => {
const token = useAppSelector((state) => state.auth.token);
const [progress, setProgress] = useState(initialProgress);
const [isSyncing, setIsSyncing] = useState(false);
useEffect(() => {
const syncTimer = setTimeout(() => {
if (progress !== initialProgress && token) {
setIsSyncing(true);
syncReadingProgress(bookId, progress, token)
.then(() => {
setIsSyncing.syncing(false);
})
.catch((error) => {
console.error('同步失败:', error);
// 本地存储失败记录,稍后重试
storeFailedSync(bookId, progress);
});
}
}, 2000); // 2秒防抖
return () => clearTimeout(syncTimer);
}, [progress, bookId, token]);
return { progress, setProgress, isSyncing };
};
// src/screens/ReadingScreen.tsx
import React, { useRef, useState } from 'react';
import { View, Text, ScrollView, StyleSheet, Animated } from 'react-native';
import { useReadingProgress } from '@/hooks/useReadingProgress';
import { useRoute } from '@react-navigation/native';
const ReadingScreen = () => {
const route = useRoute();
const { bookId, content, initialProgress } = route.params;
const { progress, setProgress, isSyncing } = useReadingProgress(bookId, initialProgress);
const scrollY = useRef(new Animated.Value(0)).current;
const handleScroll = (event) => {
const offsetY = event.nativeEvent.contentOffset.y;
const contentHeight = event.nativeEvent.contentSize.height;
const layoutHeight = event.nativeEvent.layoutMeasurement.height;
const newProgress = Math.min(100, Math.max(0, (offsetY / (contentHeight - layoutHeight)) * 100));
setProgress(newProgress);
};
return (
<View style={styles.container}>
<View style={styles.progressHeader}>
<Text>阅读进度: {progress.toFixed(1)}%</Text>
{isSyncing && <Text style={styles.syncing}>同步中...</Text>}
</View>
<ScrollView
style={styles.content}
onScroll={handleScroll}
scrollEventThrottle={16}
>
<Text style={styles.textContent}>{content}</Text>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5' },
progressHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 10,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderColor: '#ddd',
},
syncing: { color: '#2196F3' },
content: { padding: 20 },
textContent: { fontSize: 16, lineHeight: 24 },
});
export default ReadingScreen;
阶段三:平台特定优化
1. iOS平台特定优化
// src/utils/platform/ios.ts
import { Platform, StatusBar } from 'react-native';
export const getIOSStyles = () => {
if (Platform.OS === 'ios') {
return {
headerStyle: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
safeArea: { paddingTop: StatusBar.currentHeight || 0 },
keyboardAvoiding: { behavior: 'padding' as const },
};
}
return {};
};
// 在iOS特定组件中使用
import { getIOSStyles } from '@/utils/platform/ios';
const IOSSpecificComponent = () => {
const { keyboardAvoiding } = getIOSStyles();
return (
<KeyboardAvoidingView style={styles.container} {...keyboardAvoiding}>
{/* iOS优化的表单 */}
</KeyboardAvoidingView>
);
};
2. Android平台特定优化
// src/utils/platform/android.ts
import { Platform, PermissionsAndroid } from 'react-native';
export const requestAndroidPermissions = async () => {
if (Platform.OS !== 'android') return true;
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
]);
return (
granted['android.permission.READ_EXTERNAL_STORAGE'] === PermissionsAndroid.RESULTS.GRANTED &&
granted['android.permission.WRITE_EXTERNAL_STORAGE'] === PermissionsAndroid.RESULTRUES.GRANTED
);
} catch (err) {
console.warn(err);
return false;
}
};
// Android返回键处理
import { BackHandler } from 'react-native';
import { useEffect } from 'react';
export const useAndroidBackHandler = (callback: () => boolean) => {
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', callback);
return () => backHandler.remove();
}, [callback]);
};
阶段四:测试与质量保证
1. 单元测试示例
// __tests__/store/authSlice.test.ts
import authReducer, { loginUser, logout } from '@/store/slices/authSlice';
import { configureStore } from '@reduxjs/tool18';
import thunk from 'redux-thunk';
describe('authSlice', () => {
let store;
beforeEach(() => {
store = configureStore({
reducer: { auth: authReducer },
middleware: [thunk],
});
});
it('should handle login success', async () => {
// Mock API
jest.mock('@/api/auth', () => ({
loginAPI: jest.fn().mockResolvedValue({
data: { token: 'test-token', user: { id: 1, name: 'Test User' } },
}),
});
await store.dispatch(loginUser({ email: 'test@example.com', password: 'password' }));
const state = store.getState().auth;
expect(state.token).toBe('test-token');
expect(state.user.name).toBe('Test User');
});
it('should handle logout', () => {
store.dispatch(logout());
const state = store.getState().auth;
expect(state.token).toBeNull();
expect(state.user).toBeNull();
});
});
2. 端到端测试(使用Detox)
// e2e/firstTest.e2e.js
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should login successfully', async () => {
await element(by.id('emailInput')).tap();
await element(by.id('emailInput')).typeText('test@example.com');
await element(by.id('passwordInput')).tap();
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
await expect(element(by.text('Welcome'))).toBeVisible();
});
it('should show error on invalid credentials', async () => {
await element(by.id('emailInput')).clearText();
await element(by.id('emailInput')).typeText('invalid@example.com');
await element(by.id('passwordInput')).clearText();
await element(by-id('passwordInput')).typeText('wrong');
await element(by.id('loginButton')).tap();
await expect(element(by.text('登录失败'))).toBeVisible();
});
});
常见问题深度解析
问题1:性能瓶颈与优化策略
问题描述: 在开发过程中,我们发现列表滚动时出现明显卡顿,特别是在Android设备上。使用React DevTools Profiler分析发现,每次滚动都会触发大量组件重新渲染。
解决方案:
// 优化前:每次渲染都创建新函数
const BookListScreen = () => {
const renderBookItem = ({ item }) => (
<TouchableOpacity onPress={() => navigation.navigate('BookDetail', { bookId: item.id })}>
{/* ... */}
</TouchableOpacity>
);
return <FlatList data={books} renderItem={renderBookItem} />;
};
// 优化后:使用useCallback和React.memo
import React, { useCallback } from 'react';
import { memo } from 'react';
const BookItem = memo(({ item, onPress }) => {
return (
<TouchableOpacity onPress={onPress} style={styles.itemContainer}>
{/* ... */}
</TouchableOpacity>
);
});
const BookListScreen = () => {
const navigation = useNavigation();
const handlePressItem = useCallback((bookId: string) => {
navigation.navigate('BookDetail', { bookId });
}, [navigation]);
const renderBookItem = useCallback(({ item }) => {
return <BookItem item={item} onPress={() => handlePressItem(item.id)} />;
}, [handlePressItem]);
return (
<FlatList
data={books}
renderItem={renderBookItem}
keyExtractor={(item) => item.id.toString()}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
/>
);
};
进一步优化:虚拟化列表
import { FlashList } from "@shopify/flash-list";
const OptimizedBookList = () => {
return (
<FlashList
data={books}
renderItem={renderBookItem}
estimatedItemSize={100}
keyExtractor={(item) => item.id.toString()}
/>
);
};
问题2:平台特定UI/UX差异
问题描述: iOS和Android的导航模式、手势操作和UI规范存在差异,直接统一体验可能导致平台用户不满。
解决方案:平台自适应UI
// src/components/PlatformAwareButton.tsx
import { Platform, TouchableOpacity, TouchableNativeFeedback, View } from 'react-native';
const PlatformAwareButton = ({ onPress, children, style }) => {
if (Platform.OS === 'android') {
return (
<TouchableNativeFeedback
onPress={onPress}
background={TouchableNativeFeedback.Ripple('#ddd', false)}
>
<View style={style}>{children}</View>
</TouchableNativeFeedback>
);
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
<View style={style}>{children}</View>
</TouchableOpacity>
);
};
// 使用示例
<PlatformAwareButton onPress={handleLogin} style={styles.button}>
<Text style={styles.buttonText}>登录</Text>
</PlatformAwareButton>
导航栏差异化处理
// src/navigation/PlatformHeader.tsx
import { Platform } from 'react-native';
import { HeaderStyleInterpolators } from '@react-navigation/stack';
export const getPlatformHeaderOptions = () => {
return {
headerStyle: {
backgroundColor: Platform.OS === 'ios' ? '#f8f8f8' : '#fff',
elevation: Platform.OS === 'android' ? 4 : 0,
shadowOpacity: Platform.OS === 'ios' ? 0.1 : 0,
height: Platform.OS === 'ios' ? 88 : 56,
},
headerTintColor: Platform.OS === 'ios' ? '#007AFF' : '#2196F3',
headerTitleStyle: {
fontWeight: Platform.OS === 'ios' ? '600' : '500',
},
headerBackTitle: Platform.OS === '10s' ? 'Back' : null,
headerTruncatedBackTitle: Platform.OS === 'ios' ? 'Back' : null,
headerPressColor: Platform.OS === 'android' ? '#ddd' : null,
headerStyleInterpolator:
Platform.OS === 'ios'
? HeaderStyleInterpolators.forUIKit
: HeaderStyleInterpolators.forFade,
};
};
问题3:状态管理复杂性
问题描述: 随着应用规模扩大,Redux boilerplate代码过多,调试困难。
解决方案:Redux Toolkit + RTK Query
// src/store/api/booksApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { API_BASE_URL } from '@/config';
export const booksApi = createApi({
reducerPath: 'booksApi',
baseQuery: fetchBaseQuery({
baseUrl: `${API_BASE_URL}/books`,
prepareHeaders: (headers, { getState }) => {
const token = (getState() as any).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['Books', 'ReadingProgress'],
endpoints: (builder) => ({
getBooks: builder.query<Book[], void>({
query: () => '/',
providesTags: ['Books'],
}),
getBookById: builder.query<Book, string>({
query: (id) => `/${id}`,
providesTags: (result, error, id) => [{ type: 'Books', id }],
}),
updateReadingProgress: builder.mutation({
query: ({ bookId, progress }) => ({
url: `/${bookId}/progress`,
method: 'PATCH',
body: { progress },
}),
invalidatesTags: (result, error, { bookId }) => [{ type: 'ReadingProgress', id: bookId }],
}),
}),
});
export const { useGetBooksQuery, useGetBookByIdQuery, useUpdateReadingProgressMutation } = booksApi;
在组件中使用
// src/screens/BookDetailScreen.tsx
import { useGetBookByIdQuery } from '@/store/api/booksApi';
const BookDetailScreen = ({ route }) => {
const { bookId } = route.params;
const { data: book, isLoading, error } = useGetBookByIdQuery(bookId);
if (isLoading) return <LoadingView />;
if (error) return <ErrorView error={error} />;
return (
<ScrollView>
<Image source={{ uri: book.coverUrl }} style={styles.cover} />
<Text style={styles.title}>{book.title}</Text>
<Text style={styles.author}>{book.author}</Text>
<Text style={styles.description}>{book.description}</Text>
</ScrollView>
);
};
问题4:原生模块集成
问题描述: 需要集成原生功能(如摄像头扫描二维码、文件系统访问)时,跨平台框架的局限性显现。
解决方案:编写原生模块
// android/app/src/main/java/com/readshare/QRScannerModule.java
package com.readshare;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import android.content.Intent;
import android.app.Activity;
import androidx.annotation.NonNull;
public class QRScannerModule extends ReactContextBaseJavaModule {
private static final int QR_SCANNER_REQUEST = 1;
public QRScannerModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@NonNull
@Override
public String getName() {
return "QRScanner";
}
@ReactMethod
public void scanQRCode(Promise promise) {
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject("ACTIVITY_NULL", "Activity is null");
return;
}
Intent intent = new Intent(activity, QRScannerActivity.class);
activity.startActivityForResult(intent, QR_SCANNER_REQUEST);
// 需要在MainActivity中处理onActivityResult并发送事件到JS
}
}
// iOS原生模块 (Objective-C)
// ReadShare/QRScannerModule.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface RCT_EXTERN_MODULE(QRScannerModule, RCTEventEmitter)
RCT_EXTERN_METHOD(scanQRCode:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@end
// JavaScript端调用
// src/utils/qrScanner.ts
import { NativeModules, Platform } from 'react-native';
const { QRScanner } = NativeModules;
export const scanQRCode = async (): Promise<string> => {
try {
const result = await QRScanner.scanQRCode();
return result;
} catch (error) {
throw new Error('扫描失败: ' + error.message);
}
};
// 在组件中使用
import { scanQRCode } from '@/utils/qrScanner';
const AddBookScreen = () => {
const handleScan = async () => {
try {
const qrData = await scanQRCode();
// 处理扫描结果
console.log('Scanned:', qrData);
} catch (error) {
alert(error.message);
}
};
return <Button title="扫描书籍" onPress={handleScan} />;
};
问题5:调试与错误监控
问题描述: 跨平台应用的错误难以复现和定位,特别是生产环境中的错误。
解决方案:Sentry集成
// src/config/sentry.ts
import * as Sentry from '@sentry/react-native';
import { RoutingInstrumentation } from '@sentry/react-native/tracing';
export const routingInstrumentation = new RoutingInstrumentation();
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: __DEV__ ? 'development' : 'production',
tracesSampleRate: 1.0,
integrations: [
new Sentry.ReactNativeTracing({
routingInstrumentation,
tracingOrigins: ['localhost', 'your-api.com', /^\//],
}),
],
beforeSend: (event) => {
// 过滤敏感信息
if (event.request?.headers?.authorization) {
delete event.request.headers.authorization;
}
return event;
},
});
// 自定义错误边界
import { ErrorBoundary } from '@sentry/react-native';
class AppErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
Sentry.captureException(error, { extra: errorInfo });
}
render() {
return this.props.children;
}
}
// 在App.tsx中使用
const App = () => {
return (
<ErrorBoundary>
<Provider store={store}>
<NavigationContainer ref={navigationRef} onReady={() => {
routingInstrumentation.registerNavigationContainer(navigationRef);
}}>
<RootNavigator />
</NavigationContainer>
</Provider>
</ErrorBoundary>
);
};
问题6:CI/CD与自动化部署
问题描述: 手动打包和部署iOS/Android应用效率低下,容易出错。
解决方案:GitHub Actions自动化流程
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run lint
- run: npm run test:unit
- run: npm run test:e2e:ios
build-android:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- name: Build Android APK
run: |
cd android
./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: android-apk
path: android/app/build/outputs/apk/release/app-release.apk
build-ios:
runs-on: macos-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.7'
- run: npm ci
- run: bundle install
- name: Build iOS IPA
run: |
cd ios
fastlane build
- name: Upload IPA
uses: actions/upload-artifact@v3
with:
name: ios-ipa
path: ios/build/ReadShare.ipa
总结与最佳实践
通过ReadShare项目的完整开发流程,我们总结出以下关键经验:
技术选型要务实:React Native适合中小型项目,但复杂动画和性能要求极高的场景可能需要考虑Flutter或原生开发。
架构设计先行:良好的目录结构和状态管理方案能大幅降低后期维护成本。
平台特性要尊重:不要试图完全统一UI,而是应该在保持核心体验一致的前提下,尊重平台设计规范。
性能优化要持续:从项目初期就建立性能监控机制,避免技术债务累积。
自动化是效率保障:CI/CD和自动化测试是跨平台项目成功的基石。
错误监控不可少:生产环境的问题需要完善的监控体系才能及时发现和修复。
跨平台开发是一条充满挑战但回报丰厚的道路。通过合理的架构设计、持续的性能优化和完善的工程化实践,完全可以用一套代码构建出高质量的跨平台应用。希望本文的实战经验分享能为您的跨平台开发之旅提供有价值的参考。
