每天一个高级前端知识 - Day 21
今日主题:跨端开发 - React Native 与 Flutter 的深度对比与实践
核心概念:跨端不是"Write Once, Run Anywhere",而是"Learn Once, Write Anywhere"
跨端技术的本质是在开发效率、性能体验和平台一致性之间寻找最佳平衡点。
🔬 技术架构对比
┌─────────────────────────────────────────────────────────────┐
│ React Native │
├─────────────────────────────────────────────────────────────┤
│ JavaScript/TS 代码 │
│ ↓ (JS引擎: Hermes/JSC) │
│ Bridge / JSI (新架构) │
│ ↓ (序列化/直接调用) │
│ 原生组件 (UIKit/Android View) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Flutter │
├─────────────────────────────────────────────────────────────┤
│ Dart 代码 │
│ ↓ (AOT编译/JIT) │
│ Flutter Framework (Widget/Render) │
│ ↓ │
│ Skia 引擎 (自绘) │
│ ↓ │
│ Canvas (无原生组件) │
└─────────────────────────────────────────────────────────────┘
⚛️ React Native 深度实践
// ============ React Native 项目结构 ============
// App.tsx
import React, { useState, useEffect } from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
View,
FlatList,
TouchableOpacity,
Animated,
Platform,
StatusBar
} from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// 平台特定代码
const isIOS = Platform.OS === 'ios';
const statusBarHeight = Platform.OS === 'ios' ? 44 : StatusBar.currentHeight;
// 自定义Hooks
const useAnimations = () => {
const fadeAnim = new Animated.Value(0);
const scaleAnim = new Animated.Value(0.9);
const startAnimation = () => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}),
Animated.spring(scaleAnim, {
toValue: 1,
tension: 50,
friction: 7,
useNativeDriver: true
})
]).start();
};
return { fadeAnim, scaleAnim, startAnimation };
};
// 高性能列表组件
const ProductList = ({ data, onEndReached }) => {
const renderItem = ({ item, index }) => (
<Animated.View style={[styles.itemContainer, {
transform: [{ scale: 0.95 }]
}]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => console.log('Pressed', item.id)}
>
<View style={styles.card}>
<Text style={styles.title}>{item.name}</Text>
<Text style={styles.price}>¥{item.price}</Text>
</View>
</TouchableOpacity>
</Animated.View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={5}
removeClippedSubviews={true}
/>
);
};
// 自定义性能优化组件
const OptimizedImage = React.memo(({ uri, style }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<View style={style}>
{!isLoaded && <View style={styles.imagePlaceholder} />}
<Image
source={{ uri }}
style={[style, { opacity: isLoaded ? 1 : 0 }]}
onLoad={() => setIsLoaded(true)}
progressiveRenderingEnabled
fadeDuration={300}
/>
</View>
);
});
// 原生模块桥接(iOS/Android原生代码调用)
import { NativeModules, NativeEventEmitter } from 'react-native';
const { CameraModule, BiometricModule } = NativeModules;
const useBiometricAuth = () => {
const authenticate = async () => {
try {
const result = await BiometricModule.authenticate();
return result.success;
} catch (error) {
console.error('生物识别失败:', error);
return false;
}
};
return { authenticate };
};
// 主应用组件
const App = () => {
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
const { fadeAnim, scaleAnim, startAnimation } = useAnimations();
const { authenticate } = useBiometricAuth();
useEffect(() => {
startAnimation();
loadProducts();
}, []);
const loadProducts = async () => {
const response = await fetch(`/api/products?page=${page}`);
const newProducts = await response.json();
setProducts(prev => [...prev, ...newProducts]);
setPage(prev => prev + 1);
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<Animated.View style={[styles.content, {
opacity: fadeAnim,
transform: [{ scale: scaleAnim }]
}]}>
<Text style={styles.header}>商品列表</Text>
<ProductList data={products} onEndReached={loadProducts} />
</Animated.View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: statusBarHeight
},
header: {
fontSize: 24,
fontWeight: 'bold',
padding: 16,
color: '#333'
},
card: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 16,
marginHorizontal: 16,
marginVertical: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3
},
title: {
fontSize: 18,
fontWeight: '500'
},
price: {
fontSize: 16,
color: '#ff6b6b',
marginTop: 8
}
});
🎨 Flutter 深度实践
// ============ Flutter 项目 ============
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const ProductListPage(),
);
}
}
// 商品模型
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
name: json['name'],
price: json['price'],
imageUrl: json['imageUrl'],
);
}
}
// 状态管理(Riverpod)
final productProvider = FutureProvider<List<Product>>((ref) async {
final response = await http.get(Uri.parse('/api/products'));
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Product.fromJson(json)).toList();
});
final cartProvider = StateNotifierProvider<CartNotifier, List<Product>>((ref) {
return CartNotifier();
});
class CartNotifier extends StateNotifier<List<Product>> {
CartNotifier() : super([]);
void addToCart(Product product) {
state = [...state, product];
}
void removeFromCart(Product product) {
state = state.where((p) => p.id != product.id).toList();
}
double get totalPrice => state.fold(0, (sum, item) => sum + item.price);
}
// 自定义动画组件
class ShimmerEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const ShimmerEffect({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 1500),
});
@override
State<ShimmerEffect> createState() => _ShimmerEffectState();
}
class _ShimmerEffectState extends State<ShimmerEffect>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment> _alignmentAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_alignmentAnimation = AlignmentTween(
begin: Alignment(-1.2, 0),
end: Alignment(1.2, 0),
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_controller.repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ShaderMask(
shaderCallback: (bounds) {
return LinearGradient(
begin: _alignmentAnimation.value,
end: _alignmentAnimation.value + const Alignment(0.5, 0),
colors: const [
Colors.transparent,
Colors.white70,
Colors.transparent,
],
stops: const [0.0, 0.5, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.srcATop,
child: child,
);
},
child: widget.child,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
// 高性能商品列表
class ProductListPage extends ConsumerStatefulWidget {
const ProductListPage({super.key});
@override
ConsumerState<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends ConsumerState<ProductListPage>
with AutomaticKeepAliveClientMixin {
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoadingMore) return;
setState(() => _isLoadingMore = true);
// 加载更多数据
await Future.delayed(const Duration(seconds: 1));
setState(() => _isLoadingMore = false);
}
@override
Widget build(BuildContext context) {
super.build(context);
final productState = ref.watch(productProvider);
final cart = ref.watch(cartProvider);
return Scaffold(
appBar: AppBar(
title: const Text('商品列表'),
actions: [
Stack(
children: [
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
// 跳转到购物车
},
),
if (cart.isNotEmpty)
Positioned(
right: 8,
top: 8,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
cart.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
textAlign: TextAlign.center,
),
),
),
],
),
],
),
body: productState.when(
data: (products) {
return CustomScrollView(
controller: _scrollController,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final product = products[index];
return ProductCard(product: product);
},
childCount: products.length,
),
),
if (_isLoadingMore)
const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.payment),
label: Text('总计 ¥${ref.watch(cartProvider.notifier).totalPrice}'),
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
// 商品卡片组件(高性能)
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
child: InkWell(
onTap: () {
// 跳转到详情页
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
);
},
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'¥${product.price.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 18,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
),
Consumer(
builder: (context, ref, child) {
return IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () {
ref.read(cartProvider.notifier).addToCart(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已添加 ${product.name}')),
);
},
);
},
),
],
),
),
),
);
}
}
// 自定义绘画组件(Canvas)
class CustomPainterWidget extends StatelessWidget {
const CustomPainterWidget({super.key});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: BackgroundPainter(),
child: Container(),
);
}
}
class BackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue.withOpacity(0.1)
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(0, size.height * 0.8);
path.quadraticBezierTo(
size.width / 2,
size.height,
size.width,
size.height * 0.7,
);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
📊 性能对比与最佳实践
// React Native 性能优化清单
const RNOptimizations = {
// 1. 使用 Hermes 引擎
// 在 metro.config.js 中配置
// 2. FlatList 优化
optimizedList: {
initialNumToRender: 10,
maxToRenderPerBatch: 5,
windowSize: 5,
removeClippedSubviews: true,
getItemLayout: (data, index) => ({
length: 100,
offset: 100 * index,
index
})
},
// 3. 使用 InteractionManager
scheduleTask: () => {
InteractionManager.runAfterInteractions(() => {
// 执行非关键任务
});
},
// 4. 图片优化
imageOptimization: {
useFastImage: true,
cacheControl: 'cache',
priority: 'high'
},
// 5. 避免内联函数
avoidInlineFunctions: () => {
// ❌ 不好
// <TouchableOpacity onPress={() => handlePress(item)} />
// ✅ 好
// const handlePress = useCallback(() => handlePress(item), [item]);
// <TouchableOpacity onPress={handlePress} />
}
};
// Flutter 性能优化清单
const FlutterOptimizations = {
// 1. 使用 const 构造函数
// 2. 使用 ListView.builder (类似 FlatList)
// 3. 使用 RepaintBoundary 隔离重绘区域
// 4. 使用 Keys 保持状态
// 5. 异步操作使用 compute (类似 Web Worker)
};
🎯 今日挑战
实现一个跨端图片编辑器,要求:
- 同时支持 React Native 和 Flutter 双端
- 实现图片裁剪、滤镜、文字添加功能
- 高性能处理大图(使用原生模块)
- 实现撤销/重做功能
- 支持导出到相册
- 代码复用策略(React Native 使用 shared 目录,Flutter 使用统一逻辑层)
明日预告:前端未来趋势 - 2026年的前端技术展望(AI、边缘计算、新标准)
💡 跨端箴言:"真正的跨端不是代码复用,而是知识复用"——理解不同平台的差异,才能写出最佳实践!