Appearance
React与React Native代码复用 - 跨平台代码共享策略
1. 代码复用基础
1.1 可复用的代码类型
typescript
const reusableCodeTypes = {
businessLogic: {
description: '业务逻辑层',
examples: ['数据处理', '状态管理', '业务规则', 'API调用'],
reusability: '100%',
approach: '完全共享'
},
hooks: {
description: '自定义Hooks',
examples: ['useAuth', 'useData', 'useForm', 'useValidation'],
reusability: '90%',
approach: '平台特定部分抽离'
},
utilities: {
description: '工具函数',
examples: ['格式化', '验证', '计算', '转换'],
reusability: '100%',
approach: '完全共享'
},
components: {
description: 'UI组件',
examples: ['Button', 'Card', 'List', 'Form'],
reusability: '30-50%',
approach: '平台适配层'
},
styles: {
description: '样式定义',
examples: ['主题', '颜色', '间距', '字体'],
reusability: '80%',
approach: '样式变量共享'
}
};1.2 项目结构设计
monorepo/
├── packages/
│ ├── shared/ # 共享代码包
│ │ ├── src/
│ │ │ ├── hooks/ # 共享Hooks
│ │ │ ├── utils/ # 工具函数
│ │ │ ├── services/ # API服务
│ │ │ ├── store/ # 状态管理
│ │ │ ├── types/ # TypeScript类型
│ │ │ └── constants/ # 常量定义
│ │ └── package.json
│ │
│ ├── web/ # Web应用
│ │ ├── src/
│ │ │ ├── components/ # Web特定组件
│ │ │ ├── pages/ # 页面
│ │ │ └── App.tsx
│ │ └── package.json
│ │
│ └── mobile/ # React Native应用
│ ├── src/
│ │ ├── components/ # RN特定组件
│ │ ├── screens/ # 屏幕
│ │ └── App.tsx
│ └── package.json
│
├── pnpm-workspace.yaml
└── package.json2. 业务逻辑复用
2.1 API服务层
typescript
// packages/shared/src/services/api.ts
import axios, { AxiosInstance } from 'axios';
export class ApiService {
private api: AxiosInstance;
constructor(baseURL: string) {
this.api = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截
this.api.interceptors.request.use(
config => {
const token = this.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截
this.api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
this.handleUnauthorized();
}
return Promise.reject(error);
}
);
}
// 平台特定方法需要注入
private getToken: () => string | null = () => null;
private handleUnauthorized: () => void = () => {};
public setTokenGetter(getter: () => string | null) {
this.getToken = getter;
}
public setUnauthorizedHandler(handler: () => void) {
this.handleUnauthorized = handler;
}
// API方法
async get<T>(url: string, params?: any): Promise<T> {
return this.api.get(url, { params });
}
async post<T>(url: string, data?: any): Promise<T> {
return this.api.post(url, data);
}
async put<T>(url: string, data?: any): Promise<T> {
return this.api.put(url, data);
}
async delete<T>(url: string): Promise<T> {
return this.api.delete(url);
}
}
// packages/shared/src/services/user.service.ts
export class UserService {
constructor(private api: ApiService) {}
async login(email: string, password: string) {
return this.api.post('/auth/login', { email, password });
}
async getProfile() {
return this.api.get('/user/profile');
}
async updateProfile(data: any) {
return this.api.put('/user/profile', data);
}
}2.2 状态管理复用
typescript
// packages/shared/src/store/userStore.ts
import { create } from 'zustand';
import { UserService } from '../services/user.service';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface UserStore {
user: User | null;
loading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
fetchProfile: () => Promise<void>;
updateProfile: (data: Partial<User>) => Promise<void>;
}
export const createUserStore = (userService: UserService) => {
return create<UserStore>((set) => ({
user: null,
loading: false,
error: null,
login: async (email, password) => {
set({ loading: true, error: null });
try {
const response = await userService.login(email, password);
set({ user: response.user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => {
set({ user: null });
},
fetchProfile: async () => {
set({ loading: true });
try {
const user = await userService.getProfile();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
updateProfile: async (data) => {
set({ loading: true });
try {
const user = await userService.updateProfile(data);
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
}
}));
};3. Hooks复用
3.1 完全共享的Hooks
typescript
// packages/shared/src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// packages/shared/src/hooks/usePrevious.ts
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// packages/shared/src/hooks/useToggle.ts
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
const setTrue = useCallback(() => {
setValue(true);
}, []);
const setFalse = useCallback(() => {
setValue(false);
}, []);
return { value, toggle, setTrue, setFalse };
}3.2 平台适配的Hooks
typescript
// packages/shared/src/hooks/useStorage.ts
export interface StorageAdapter {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}
export function useStorage<T>(key: string, adapter: StorageAdapter) {
const [value, setValue] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadValue = async () => {
try {
const item = await adapter.getItem(key);
if (item) {
setValue(JSON.parse(item));
}
} finally {
setLoading(false);
}
};
loadValue();
}, [key]);
const saveValue = useCallback(async (newValue: T) => {
setValue(newValue);
await adapter.setItem(key, JSON.stringify(newValue));
}, [key, adapter]);
const removeValue = useCallback(async () => {
setValue(null);
await adapter.removeItem(key);
}, [key, adapter]);
return { value, saveValue, removeValue, loading };
}
// Web实现
// packages/web/src/adapters/storageAdapter.ts
export const webStorageAdapter: StorageAdapter = {
async getItem(key: string) {
return localStorage.getItem(key);
},
async setItem(key: string, value: string) {
localStorage.setItem(key, value);
},
async removeItem(key: string) {
localStorage.removeItem(key);
}
};
// React Native实现
// packages/mobile/src/adapters/storageAdapter.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
export const mobileStorageAdapter: StorageAdapter = {
async getItem(key: string) {
return AsyncStorage.getItem(key);
},
async setItem(key: string, value: string) {
await AsyncStorage.setItem(key, value);
},
async removeItem(key: string) {
await AsyncStorage.removeItem(key);
}
};3.3 条件导出Hook
typescript
// packages/shared/src/hooks/useOrientation.ts
import { useState, useEffect } from 'react';
export type Orientation = 'portrait' | 'landscape';
// 定义平台接口
export interface OrientationAdapter {
getCurrentOrientation(): Orientation;
subscribe(callback: (orientation: Orientation) => void): () => void;
}
export function useOrientation(adapter: OrientationAdapter) {
const [orientation, setOrientation] = useState<Orientation>(
adapter.getCurrentOrientation()
);
useEffect(() => {
const unsubscribe = adapter.subscribe(setOrientation);
return unsubscribe;
}, [adapter]);
return orientation;
}
// Web实现
export const webOrientationAdapter: OrientationAdapter = {
getCurrentOrientation() {
return window.innerWidth > window.innerHeight ? 'landscape' : 'portrait';
},
subscribe(callback) {
const handler = () => {
const orientation = window.innerWidth > window.innerHeight
? 'landscape'
: 'portrait';
callback(orientation);
};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}
};
// React Native实现
import { Dimensions } from 'react-native';
export const mobileOrientationAdapter: OrientationAdapter = {
getCurrentOrientation() {
const { width, height } = Dimensions.get('window');
return width > height ? 'landscape' : 'portrait';
},
subscribe(callback) {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
const orientation = window.width > window.height
? 'landscape'
: 'portrait';
callback(orientation);
});
return () => subscription.remove();
}
};4. 工具函数复用
4.1 数据处理工具
typescript
// packages/shared/src/utils/format.ts
export const formatUtils = {
// 格式化货币
currency(value: number, currency = 'CNY'): string {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency
}).format(value);
},
// 格式化日期
date(date: Date | string, format = 'YYYY-MM-DD'): string {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day);
},
// 格式化数字
number(value: number, decimals = 2): string {
return value.toFixed(decimals);
},
// 格式化文件大小
fileSize(bytes: number): string {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
}
};4.2 验证工具
typescript
// packages/shared/src/utils/validation.ts
export const validationUtils = {
email(value: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
},
phone(value: string): boolean {
const regex = /^1[3-9]\d{9}$/;
return regex.test(value);
},
url(value: string): boolean {
try {
new URL(value);
return true;
} catch {
return false;
}
},
password(value: string): { valid: boolean; message?: string } {
if (value.length < 8) {
return { valid: false, message: '密码至少8位' };
}
if (!/[A-Z]/.test(value)) {
return { valid: false, message: '密码需包含大写字母' };
}
if (!/[a-z]/.test(value)) {
return { valid: false, message: '密码需包含小写字母' };
}
if (!/[0-9]/.test(value)) {
return { valid: false, message: '密码需包含数字' };
}
return { valid: true };
}
};5. 组件复用策略
5.1 组件适配器模式
typescript
// packages/shared/src/components/Button/types.ts
export interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
icon?: string;
}
// Web实现
// packages/web/src/components/Button.tsx
import { ButtonProps } from '@shared/components/Button/types';
export function Button({
title,
onPress,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon
}: ButtonProps) {
return (
<button
onClick={onPress}
disabled={disabled || loading}
className={`btn btn-${variant} btn-${size}`}
>
{loading && <span className="spinner" />}
{icon && <i className={icon} />}
{title}
</button>
);
}
// React Native实现
// packages/mobile/src/components/Button.tsx
import { TouchableOpacity, Text, ActivityIndicator, View } from 'react-native';
import { ButtonProps } from '@shared/components/Button/types';
export function Button({
title,
onPress,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon
}: ButtonProps) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled || loading}
style={[styles.button, styles[variant], styles[size]]}
>
{loading && <ActivityIndicator color="#fff" />}
{icon && <Icon name={icon} />}
<Text style={styles.text}>{title}</Text>
</TouchableOpacity>
);
}5.2 平台特定导出
typescript
// packages/shared/src/components/Image/Image.web.tsx
export function Image({ source, style, ...props }) {
return (
<img
src={typeof source === 'string' ? source : source.uri}
style={style}
{...props}
/>
);
}
// packages/shared/src/components/Image/Image.native.tsx
import { Image as RNImage } from 'react-native';
export function Image({ source, style, ...props }) {
return (
<RNImage
source={typeof source === 'string' ? { uri: source } : source}
style={style}
{...props}
/>
);
}
// packages/shared/src/components/Image/index.ts
export { Image } from './Image';5.3 复合组件拆分
typescript
// packages/shared/src/components/Card/CardBase.tsx
export interface CardBaseProps {
children: React.ReactNode;
onPress?: () => void;
}
export function CardBase({ children, onPress }: CardBaseProps) {
// 只包含逻辑,不包含UI
const [isHovered, setIsHovered] = useState(false);
return { children, onPress, isHovered, setIsHovered };
}
// packages/web/src/components/Card.tsx
export function Card({ children, onPress }: CardBaseProps) {
const { isHovered, setIsHovered } = CardBase({ children, onPress });
return (
<div
onClick={onPress}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={isHovered ? 'card card-hover' : 'card'}
>
{children}
</div>
);
}
// packages/mobile/src/components/Card.tsx
export function Card({ children, onPress }: CardBaseProps) {
return (
<TouchableOpacity onPress={onPress} style={styles.card}>
{children}
</TouchableOpacity>
);
}6. 样式复用
6.1 设计Token
typescript
// packages/shared/src/styles/tokens.ts
export const tokens = {
colors: {
primary: '#2196F3',
secondary: '#FF5722',
success: '#4CAF50',
warning: '#FFC107',
error: '#F44336',
text: {
primary: '#212121',
secondary: '#757575',
disabled: '#BDBDBD'
},
background: {
default: '#FFFFFF',
paper: '#F5F5F5',
dark: '#121212'
}
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48
},
typography: {
fontFamily: {
primary: 'System',
mono: 'Monospace'
},
fontSize: {
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
xxl: 24
},
fontWeight: {
regular: '400',
medium: '500',
bold: '700'
},
lineHeight: {
tight: 1.2,
normal: 1.5,
relaxed: 1.8
}
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
full: 9999
},
shadows: {
sm: '0 1px 2px rgba(0,0,0,0.1)',
md: '0 4px 6px rgba(0,0,0,0.1)',
lg: '0 10px 15px rgba(0,0,0,0.1)'
}
};6.2 平台样式适配
typescript
// Web CSS-in-JS
// packages/web/src/styles/createStyles.ts
import { tokens } from '@shared/styles/tokens';
export function createStyles<T>(styles: T): T {
return styles;
}
export const commonStyles = createStyles({
container: {
padding: tokens.spacing.md,
backgroundColor: tokens.colors.background.default
},
text: {
color: tokens.colors.text.primary,
fontSize: tokens.typography.fontSize.md,
lineHeight: tokens.typography.lineHeight.normal
}
});
// React Native StyleSheet
// packages/mobile/src/styles/createStyles.ts
import { StyleSheet } from 'react-native';
import { tokens } from '@shared/styles/tokens';
export function createStyles<T>(styles: T): T {
return StyleSheet.create(styles as any) as T;
}
export const commonStyles = createStyles({
container: {
padding: tokens.spacing.md,
backgroundColor: tokens.colors.background.default
},
text: {
color: tokens.colors.text.primary,
fontSize: tokens.typography.fontSize.md,
lineHeight: tokens.typography.lineHeight.normal * tokens.typography.fontSize.md
}
});7. TypeScript类型复用
7.1 共享类型定义
typescript
// packages/shared/src/types/models.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user';
createdAt: Date;
}
export interface Product {
id: string;
title: string;
description: string;
price: number;
image: string;
category: string;
stock: number;
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
createdAt: Date;
}
export interface OrderItem {
productId: string;
quantity: number;
price: number;
}7.2 API类型定义
typescript
// packages/shared/src/types/api.ts
export interface ApiResponse<T> {
data: T;
message?: string;
success: boolean;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface ApiError {
code: string;
message: string;
details?: any;
}8. 配置文件复用
8.1 环境配置
typescript
// packages/shared/src/config/env.ts
export interface AppConfig {
apiUrl: string;
environment: 'development' | 'staging' | 'production';
enableAnalytics: boolean;
enableLogging: boolean;
}
export function createConfig(overrides: Partial<AppConfig> = {}): AppConfig {
return {
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000',
environment: (process.env.NODE_ENV as any) || 'development',
enableAnalytics: process.env.REACT_APP_ENABLE_ANALYTICS === 'true',
enableLogging: process.env.REACT_APP_ENABLE_LOGGING === 'true',
...overrides
};
}
// Web使用
const config = createConfig({
apiUrl: import.meta.env.VITE_API_URL
});
// React Native使用
import Config from 'react-native-config';
const config = createConfig({
apiUrl: Config.API_URL
});9. 测试复用
9.1 测试工具函数
typescript
// packages/shared/src/test-utils/setup.ts
export function createMockUser(overrides: Partial<User> = {}): User {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
role: 'user',
createdAt: new Date(),
...overrides
};
}
export function createMockProduct(overrides: Partial<Product> = {}): Product {
return {
id: '1',
title: 'Test Product',
description: 'Test Description',
price: 99.99,
image: 'https://example.com/image.jpg',
category: 'test',
stock: 10,
...overrides
};
}9.2 Mock服务
typescript
// packages/shared/src/test-utils/mockApi.ts
export class MockApiService extends ApiService {
private mockData: Map<string, any> = new Map();
setMockData(key: string, data: any) {
this.mockData.set(key, data);
}
async get<T>(url: string): Promise<T> {
return this.mockData.get(url) || null;
}
async post<T>(url: string, data: any): Promise<T> {
return data;
}
}10. Monorepo配置
10.1 pnpm-workspace.yaml
yaml
packages:
- 'packages/*'10.2 根package.json
json
{
"name": "my-app",
"private": true,
"scripts": {
"dev:web": "pnpm --filter web dev",
"dev:mobile": "pnpm --filter mobile start",
"build:web": "pnpm --filter web build",
"build:mobile": "pnpm --filter mobile build:android",
"lint": "pnpm -r lint",
"test": "pnpm -r test",
"type-check": "pnpm -r type-check"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}10.3 共享包配置
json
// packages/shared/package.json
{
"name": "@myapp/shared",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./hooks": "./src/hooks/index.ts",
"./utils": "./src/utils/index.ts",
"./types": "./src/types/index.ts"
},
"dependencies": {
"axios": "^1.6.0",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}11. 最佳实践
typescript
const codeReuseBestPractices = {
architecture: [
'清晰的层次划分',
'业务逻辑与UI分离',
'使用适配器模式处理平台差异',
'定义清晰的接口',
'TypeScript类型共享'
],
组织结构: [
'Monorepo管理多包',
'共享包独立维护',
'平台特定代码隔离',
'合理的目录结构',
'统一的命名规范'
],
components: [
'提取可复用的组件逻辑',
'使用平台特定的UI实现',
'共享设计Token',
'组件接口一致',
'支持主题定制'
],
state: [
'状态管理逻辑共享',
'使用平台无关的状态库',
'统一的数据模型',
'共享API层',
'类型安全的状态'
],
testing: [
'共享测试工具',
'复用Mock数据',
'统一的测试策略',
'跨平台测试覆盖',
'E2E测试复用'
]
};12. 总结
React与React Native代码复用的核心要点:
- 业务逻辑: 100%复用
- 状态管理: 完全共享
- Hooks: 大部分可复用
- 组件: 适配器模式
- 样式: Token系统
- 类型: TypeScript共享
- 工具函数: 完全复用
- 配置: 环境适配
通过合理的架构设计,可以实现70-80%的代码复用率。