一、 为什么我们要考虑代码共享?
想象一下,你正在为一家公司开发一款产品,它既需要一个漂亮的官方网站(Web端),又需要一款方便用户随时随地使用的手机应用(移动端)。按照传统做法,你需要组建两个团队,或者至少让同一批人写两套几乎完全不同的代码。Web团队用React写网页,移动团队用React Native写App。很快你会发现,很多功能是重复的:比如用户登录的逻辑、商品数据的处理、甚至是一些按钮和提示框的样式。
这就像建房子,一个团队在砌砖,另一个团队也在旁边用同样的砖砌另一面墙,但却不能共享图纸和工人。代码共享就是为了解决这个“重复劳动”的问题。它的核心思想是:把那些通用的、不依赖具体平台的部分(比如数据处理、业务规则、纯逻辑组件)抽出来,写成一份代码,让Web和App都能使用。
这样做的好处显而易见:开发更快、维护更轻松、体验更一致。当你需要修改一个业务规则时,只需要改一个地方,Web和App就同时更新了。这大大减少了出错的可能,也节省了宝贵的时间和人力。
二、 我们可以共享什么?不能共享什么?
在动手之前,我们必须清楚边界。不是所有代码都能“一份代码,到处运行”。
可以放心共享的部分:
- 业务逻辑和状态管理:这是共享的“黄金地带”。比如用户认证、购物车管理、数据格式化工具、API请求层(使用
fetch或axios等通用库)。这些是应用的大脑,不关心自己是在浏览器里还是在手机里运行。 - 工具函数和常量:日期处理函数、货币格式化、定义好的颜色值、应用配置等。
- TypeScript类型定义:确保Web和Native两端数据模型一致性的最强保障。
- 部分UI组件逻辑:那些不直接渲染原生视图或DOM元素的“逻辑组件”或“容器组件”。例如,一个负责获取数据并向下传递的组件。
需要谨慎处理或不能共享的部分:
- 平台特定的UI组件:React Native的
<View>,<Text>,<Image>和 Web的<div>,<span>,<img>无法直接互换。它们的底层实现完全不同。 - 导航:Web使用URL和浏览器历史(如React Router),而React Native使用自己的导航栈(如React Navigation)。这是差异最大的部分之一。
- 设备功能API:调用摄像头、GPS、本地文件系统等,在Web和Native上有完全不同的调用方式。
- 样式:React Native使用类似CSS的
StyleSheet对象,但不支持所有CSS属性(如伪类),且布局引擎是Yoga(实现Flexbox),与浏览器CSS引擎有细微差别。
理解了这些,我们就可以开始设计我们的项目了。
三、 实战:构建一个共享代码的项目
让我们通过一个简单的“用户待办事项”应用来演示。我们将共享:1) 数据类型,2) 状态管理逻辑,3) API服务,4) 部分可复用的UI逻辑。
技术栈声明: 本项目统一使用 TypeScript + React (Web) + React Native + Zustand (状态管理) 技术栈。
首先,我们推荐使用 Monorepo 项目结构。这就像一个大仓库里管理着多个相关的包,非常适合我们的场景。
my-cross-platform-app/
├── packages/
│ ├── shared/ # 我们的共享核心代码库
│ │ ├── src/
│ │ │ ├── types/ # 共享的类型定义
│ │ │ ├── stores/ # 共享的状态管理
│ │ │ ├── services/ # 共享的API服务
│ │ │ └── utils/ # 共享的工具函数
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── web/ # React Web 项目
│ │ ├── src/
│ │ └── package.json
│ └── mobile/ # React Native 项目
│ ├── src/
│ └── package.json
├── package.json (workspace root)
└── yarn.lock / package-lock.json
步骤1:创建共享类型和模型
在 packages/shared/src/types/todo.ts 中:
// 共享的待办事项数据类型定义
export interface TodoItem {
id: string;
title: string;
description?: string; // 描述是可选的
isCompleted: boolean;
createdAt: Date;
}
// API响应数据的通用格式
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
步骤2:创建共享的状态管理
我们使用Zustand,因为它轻量且可以在React和React Native中完美运行。在 packages/shared/src/stores/useTodoStore.ts 中:
import { create } from 'zustand';
import { TodoItem } from '../types/todo';
import { todoService } from '../services/todoService'; // 假设的服务层
interface TodoStore {
// 状态
todos: TodoItem[];
isLoading: boolean;
error: string | null;
// 操作
fetchTodos: () => Promise<void>;
addTodo: (title: string, description?: string) => Promise<void>;
toggleTodo: (id: string) => Promise<void>;
deleteTodo: (id: string) => Promise<void>;
}
// 创建Store,这里的逻辑Web和Native完全通用
export const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
isLoading: false,
error: null,
fetchTodos: async () => {
set({ isLoading: true, error: null });
try {
const todos = await todoService.fetchAll(); // 调用共享服务
set({ todos, isLoading: false });
} catch (err: any) {
set({ error: err.message, isLoading: false });
}
},
addTodo: async (title, description) => {
const newTodo = await todoService.create({ title, description });
set((state) => ({ todos: [...state.todos, newTodo] }));
},
toggleTodo: async (id) => {
const todo = get().todos.find(t => t.id === id);
if (todo) {
const updated = await todoService.update(id, { isCompleted: !todo.isCompleted });
set((state) => ({
todos: state.todos.map(t => t.id === id ? updated : t)
}));
}
},
deleteTodo: async (id) => {
await todoService.delete(id);
set((state) => ({
todos: state.todos.filter(t => t.id !== id)
}));
},
}));
步骤3:创建共享的API服务
在 packages/shared/src/services/todoService.ts 中:
import { TodoItem } from '../types/todo';
// 这是一个模拟的API服务,实际项目中会替换为真实的fetch或axios调用
// 关键点:服务层的接口设计是通用的,不依赖平台。
export const todoService = {
async fetchAll(): Promise<TodoItem[]> {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 500));
const stored = localStorage.getItem('todos'); // 注意:这里在Native端会报错!
// 在实际项目中,这里应该使用一个平台无关的存储抽象层,比如异步存储库
return stored ? JSON.parse(stored) : [];
},
async create(todo: Omit<TodoItem, 'id' | 'isCompleted' | 'createdAt'>): Promise<TodoItem> {
const newTodo: TodoItem = {
...todo,
id: Date.now().toString(),
isCompleted: false,
createdAt: new Date(),
};
const allTodos = await this.fetchAll();
allTodos.push(newTodo);
localStorage.setItem('todos', JSON.stringify(allTodos)); // 同样,需要抽象
return newTodo;
},
async update(id: string, updates: Partial<TodoItem>): Promise<TodoItem> {
let allTodos = await this.fetchAll();
const index = allTodos.findIndex(t => t.id === id);
if (index > -1) {
allTodos[index] = { ...allTodos[index], ...updates };
localStorage.setItem('todos', JSON.stringify(allTodos));
return allTodos[index];
}
throw new Error('Todo not found');
},
async delete(id: string): Promise<void> {
let allTodos = await this.fetchAll();
allTodos = allTodos.filter(t => t.id !== id);
localStorage.setItem('todos', JSON.stringify(allTodos));
},
};
// 为了解决上面提到的平台特定API问题,我们可以引入一个抽象层:
// `packages/shared/src/storage/` 目录下,定义一个接口,然后分别为Web和Native实现。
// 例如:`interface IStorage { getItem(key): Promise<string>; setItem(key, val): Promise<void>; }`
// Web端实现用localStorage,Native端用`@react-native-async-storage/async-storage`。
// 这样,我们的`todoService`就完全与平台解耦了。
步骤4:在Web端使用共享代码
在 packages/web/src/components/TodoList.tsx 中:
import React, { useEffect } from 'react';
// 从共享包中导入Store!这是关键。
import { useTodoStore } from 'shared/src/stores/useTodoStore';
const TodoList: React.FC = () => {
// 使用共享的Store,逻辑与Native端完全一致
const { todos, isLoading, error, fetchTodos, toggleTodo } = useTodoStore();
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
if (isLoading) return <div>加载中...</div>; // Web的div
if (error) return <div style={{ color: 'red' }}>错误:{error}</div>;
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.isCompleted ? 'line-through' : 'none' }}>
<span onClick={() => toggleTodo(todo.id)}>{todo.title}</span>
</li>
))}
</ul>
);
};
export default TodoList;
步骤5:在React Native端使用共享代码
在 packages/mobile/src/components/TodoList.tsx 中:
import React, { useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
// 导入的是同一个共享Store!
import { useTodoStore } from 'shared/src/stores/useTodoStore';
const TodoList: React.FC = () => {
// 看!状态和逻辑与Web端一模一样
const { todos, isLoading, error, fetchTodos, toggleTodo } = useTodoStore();
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
if (isLoading) return <ActivityIndicator size="large" />; // Native的加载指示器
if (error) return <Text style={{ color: 'red' }}>错误:{error}</Text>;
const renderItem = ({ item }: { item: TodoItem }) => (
<TouchableOpacity onPress={() => toggleTodo(item.id)}>
<View style={{ padding: 15, borderBottomWidth: 1 }}>
<Text style={{ textDecorationLine: item.isCompleted ? 'line-through' : 'none' }}>
{item.title}
</Text>
{item.description && <Text>{item.description}</Text>}
</View>
</TouchableOpacity>
);
return <FlatList data={todos} renderItem={renderItem} keyExtractor={(item) => item.id} />;
};
export default TodoList;
看,TodoList组件在两端的核心逻辑(数据获取、状态更新)是完全相同的,唯一的区别在于渲染部分:Web端用了<div>和<li>,而Native端用了<View>和<Text>。这就是“一次逻辑,多处渲染”的精髓。
四、 深入场景、优缺点与避坑指南
典型应用场景:
- 内容型应用:新闻阅读、博客、电商产品展示。这些应用的核心是数据获取和展示,UI交互相对标准,共享度可以非常高。
- 工具型应用:计算器、单位转换器、健康追踪器。业务逻辑复杂但UI相对独立,非常适合逻辑共享。
- 企业级后台管理系统:通常需要在Web大屏和移动端报表查看,可以共享所有的数据模型、API层和状态管理。
技术优势:
- 开发效率倍增:写一次核心逻辑,两端同时受益。
- 维护成本降低:修复Bug或添加功能只需在一处修改。
- 保证一致性:业务规则和行为在Web和App上绝对同步,用户体验统一。
- 团队协作优化:前端开发者可以更专注于业务逻辑,平台专家负责特定平台的集成和优化。
挑战与缺点:
- 设计妥协:为了共享,UI可能需要设计成同时适应Web和移动端的交互模式,有时会牺牲某个平台的最佳体验。
- 平台特性整合复杂:深度集成摄像头、蓝牙等设备功能时,需要在共享层做大量抽象工作,可能增加复杂度。
- 调试难度增加:一个问题可能出现在共享代码、Web包装层或Native包装层,需要更系统的调试方法。
- 初期搭建成本:设计项目结构、搭建构建流水线(Monorepo工具如Turborepo、Nx)需要额外精力。
关键的注意事项(避坑指南):
- 抽象,抽象,再抽象:遇到平台特定API(如存储、导航、设备信息),立刻想到抽象。定义一个通用接口,然后分别实现。不要在任何旨在共享的代码中直接写
localStorage或AsyncStorage。 - 样式隔离:绝对不要在共享代码中写具体的样式代码。样式应该作为“属性”或“主题”向下传递。可以考虑共享一个“主题定义”对象(如颜色、字体大小常量),但具体的
StyleSheet或CSS应在平台层实现。 - 组件共享策略:对于简单的UI组件(如按钮、卡片),可以考虑使用像
react-native-web这样的库,它让你用React Native的组件写Web应用。但这会增加包体积,且样式表现可能仍需调整。更稳健的做法是共享组件的“逻辑”(通过自定义Hook),然后分别实现UI。 - 类型安全是生命线:充分利用TypeScript。共享的类型定义是两端通信的“合同”,能提前发现大量潜在错误。
- 构建与依赖管理:使用成熟的Monorepo管理工具(Yarn Workspaces, pnpm Workspaces, Turborepo, Nx)来处理包之间的链接、构建顺序和任务执行,手动管理会非常痛苦。
五、 总结
React Native与React Web的代码共享,不是追求100%的代码复用,而是追求核心业务逻辑和模型的高度复用。它更像是一种架构哲学,鼓励我们将应用程序中稳定、核心的部分与多变、平台相关的部分分离开来。
通过采用Monorepo结构,精心设计共享层(类型、状态、服务),并在平台层处理渲染和特定API,我们可以在保证用户体验的同时,获得巨大的开发效能提升。这尤其适合那些业务驱动、需要快速迭代并覆盖多端的项目。
记住,成功的代码共享始于良好的设计,而不是生硬地将代码堆在一起。从一个小模块开始尝试,比如先共享工具函数和类型定义,再逐步扩展到状态管理,你会更平滑地掌握这项强大的技能。在当今多端时代,这种能力正变得越来越重要。
评论