RN + TS 的 MVVM 架构设计
MVVM (Model-View-ViewModel) 架构通过引入 ViewModel 层,实现了视图与业务逻辑的分离,非常适合 React Native + TypeScript 的项目。下面是一个完整的 RN + TS MVVM 架构设计方案:
架构核心概念
- View:React 组件,负责 UI 展示,通过属性绑定接收数据,通过事件绑定触发 ViewModel 中的操作
- ViewModel:核心业务逻辑层,处理数据转换、状态管理和业务逻辑,与 Model 和 View 进行双向数据绑定
- Model:数据模型层,负责数据的存储、获取和处理,包括本地存储和远程 API 调用
数据流与通信机制
- View 到 ViewModel:通过事件绑定(如 onPress、onChange 等)触发 ViewModel 中的方法
- ViewModel 到 View:使用状态管理库(MobX)实现数据的响应式更新
- ViewModel 到 Model:ViewModel 调用 Model 层的方法获取或保存数据
- Model 到 ViewModel:数据变化通过 Promise 或回调通知 ViewModel
项目结构
my-rn-app/
├── src/
│ ├── app/ # 应用全局配置
│ │ ├── navigation/ # 导航配置
│ │ ├── theme/ # 主题和样式
│ │ └── App.tsx # 应用入口
│ │
│ ├── features/ # 功能模块
│ │ ├── auth/ # 认证模块
│ │ │ ├── models/ # 用户模型
│ │ │ ├── viewmodels/ # 认证 ViewModel
│ │ │ ├── views/ # 认证视图
│ │ │ └── services/ # 认证服务
│ │ │
│ │ └── posts/ # 文章模块
│ │ ├── models/ # 文章模型
│ │ ├── viewmodels/ # 文章 ViewModel
│ │ ├── views/ # 文章视图
│ │ └── services/ # 文章服务
│ │
│ ├── shared/ # 共享资源
│ │ ├── components/ # 通用组件
│ │ ├── viewmodels/ # 通用 ViewModel
│ │ ├── models/ # 通用模型
│ │ ├── utils/ # 工具函数
│ │ └── types/ # 全局类型
│ │
│ ├── services/ # 外部服务
│ │ ├── api/ # API 服务
│ │ ├── storage/ # 存储服务
│ │ └── eventBus/ # 事件总线
│ │
│ └── state/ # 状态管理
│ ├── stores/ # 全局存储
│ └── index.ts # 状态入口
│
├── __tests__/ # 测试
├── babel.config.js # Babel 配置
├── tsconfig.json # TypeScript 配置
└── package.json # 依赖配置
代码
model层 User.ts
BaseModel为自定义的基类model
import { BaseModel } from '../../../shared/models/BaseModel';
export interface UserData {
id?: string;
name: string;
email: string;
avatar?: string;
token?: string;
createdAt?: Date;
updatedAt?: Date;
}
export class User extends BaseModel {
name: string;
email: string;
avatar?: string;
token?: string;
constructor(data: UserData) {
super(data);
this.name = data.name;
this.email = data.email;
this.avatar = data.avatar;
this.token = data.token;
}
// 从API数据创建用户实例
static fromApi(data: any): User {
return new User({
id: data.id,
name: data.name,
email: data.email,
avatar: data.avatar,
token: data.token,
createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined
});
}
// 转换为API数据格式
toApi() {
return {
id: this.id,
name: this.name,
email: this.email,
avatar: this.avatar,
token: this.token
};
}
}
网络请求 service层
import { User } from '../models/User';
// 模拟API服务
export class AuthService {
// 登录方法
async login(email: string, password: string): Promise<User> {
// 实际项目中这里会调用真实API
console.log('模拟登录请求:', email, password);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟登录成功返回
return new User({
id: '1',
name: 'John Doe',
email,
avatar: 'https://picsum.photos/200/200',
token: 'fake_token_123456'
});
}
// 注册方法
async register(name: string, email: string, password: string): Promise<User> {
// 模拟注册请求
console.log('模拟注册请求:', name, email, password);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟注册成功返回
return new User({
id: Math.random().toString(36).substr(2, 9),
name,
email,
avatar: 'https://picsum.photos/200/200',
token: 'fake_token_' + Math.random().toString(36).substr(2, 10)
});
}
// 登出方法
async logout(): Promise<void> {
// 模拟登出请求
console.log('模拟登出请求');
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
}
viewModel层
import { makeObservable, observable, action, computed } from 'mobx';
import { AuthService } from '../services/AuthService';
import { User } from '../models/User';
import { AppStore } from '../../../state/stores/AppStore';
export class LoginViewModel {
@observable email: string = '';
@observable password: string = '';
@observable loading: boolean = false;
@observable error: string | null = null;
private authService: AuthService;
private appStore: AppStore;
constructor(authService: AuthService, appStore: AppStore) {
this.authService = authService;
this.appStore = appStore;
makeObservable(this);
}
@computed
get isValid(): boolean {
return this.email.includes('@') && this.password.length >= 6;
}
@action
setEmail = (email: string) => {
this.email = email;
this.error = null;
};
@action
setPassword = (password: string) => {
this.password = password;
this.error = null;
};
@action
login = async () => {
if (!this.isValid) {
this.error = '请输入有效的邮箱和密码';
return;
}
this.loading = true;
this.error = null;
try {
const user = await this.authService.login(this.email, this.password);
this.appStore.setCurrentUser(user);
// 登录成功后的操作
console.log('登录成功:', user);
} catch (error: any) {
this.error = error.message || '登录失败';
console.error('登录错误:', error);
} finally {
this.loading = false;
}
};
}
view容器层
import React, { Component } from 'react';
import { View, TextInput, Button, StyleSheet, ActivityIndicator, Text } from 'react-native';
import { observer } from 'mobx-react';
import { LoginViewModel } from '../viewmodels/LoginViewModel';
interface LoginViewProps {
viewModel: LoginViewModel;
onLoginSuccess?: () => void;
}
@observer
export default class LoginView extends Component<LoginViewProps> {
render() {
const { viewModel } = this.props;
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="邮箱"
value={viewModel.email}
onChangeText={viewModel.setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="密码"
value={viewModel.password}
onChangeText={viewModel.setPassword}
secureTextEntry
/>
{viewModel.error && <Text style={styles.error}>{viewModel.error}</Text>}
<Button
title={viewModel.loading ? '登录中...' : '登录'}
onPress={viewModel.login}
disabled={viewModel.loading || !viewModel.isValid}
/>
{viewModel.loading && (
<ActivityIndicator style={styles.loading} size="small" />
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
padding: 20,
flex: 1,
justifyContent: 'center'
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 15,
paddingLeft: 10
},
error: {
color: 'red',
marginBottom: 10,
textAlign: 'center'
},
loading: {
marginTop: 10
}
});
其他辅助类:
AppStore : 用于全局的数据存储
import { makeObservable, observable, action } from 'mobx';
import { User } from '../../features/auth/models/User';
export class AppStore {
@observable currentUser: User | null = null;
@observable isAuthenticated: boolean = false;
constructor() {
makeObservable(this);
}
@action
setCurrentUser = (user: User | null) => {
this.currentUser = user;
this.isAuthenticated = user !== null;
};
@action
logout = () => {
this.currentUser = null;
this.isAuthenticated = false;
};
}
ServiceLocator:提供全局的工具类方法,单利实现
import { AuthService } from '../features/auth/services/AuthService';
import { AppStore } from '../state/stores/AppStore';
export class ServiceLocator {
private static instance: ServiceLocator;
private authService: AuthService;
private appStore: AppStore;
private constructor() {
this.authService = new AuthService();
this.appStore = new AppStore();
}
static getInstance() {
if (!this.instance) {
this.instance = new ServiceLocator();
}
return this.instance;
}
getAuthService() {
return this.authService;
}
getAppStore() {
return this.appStore;
}
}
实现要点说明
-
双向数据绑定:
- 使用 MobX 的
@observable装饰器标记可观察属性 - 使用
@action装饰器标记修改状态的方法 - 通过
observer高阶组件使 React 组件响应状态变化 - 视图中的变化通过事件处理函数更新 ViewModel,ViewModel 的变化自动更新视图
- 使用 MobX 的
-
ViewModel 设计:
- 每个功能模块有自己的 ViewModel
- ViewModel 负责处理业务逻辑,与服务层交互
- ViewModel 不直接引用视图组件,保持松耦合
-
服务层:
- 负责与外部系统交互(API、存储等)
- 提供数据获取和操作的接口
- 可以被多个 ViewModel 共享使用
-
状态管理:
- 使用 AppStore 管理全局状态
- 通过 ServiceLocator 实现依赖注入
- 状态变化自动触发视图更新
-
视图层:
- 纯展示组件,不包含业务逻辑
- 通过 props 接收 ViewModel
- 通过事件处理函数将用户操作传递给 ViewModel
运行流程示例
-
用户在登录页面输入邮箱和密码
-
输入内容通过 onChangeText 事件更新到 LoginViewModel
-
用户点击登录按钮,触发 LoginViewModel 的 login 方法
-
LoginViewModel 调用 AuthService 的 login 方法
-
AuthService 模拟 API 请求并返回用户数据
-
LoginViewModel 更新 AppStore 中的当前用户状态
-
状态变化触发所有观察该状态的组件重新渲染
-
导航到主页
子组件共用ViewModel处理方式
复杂业务下 ViewModel 的共享方案
在复杂业务场景中,LoginView 可能包含多个子组件,如验证码组件、密码强度指示器等。这些子组件需要共享同一个 ViewModel 实例以保持数据一致性。以下是几种实现方案:
方案一:通过 props 逐层传递
最直接的方法是通过 props 从父组件逐层传递 ViewModel:
// LoginView.tsx
@observer
export default class LoginView extends Component<LoginViewProps> {
render() {
return (
<View style={styles.container}>
<EmailInput viewModel={this.props.viewModel} />
<PasswordInput viewModel={this.props.viewModel} />
<CaptchaComponent viewModel={this.props.viewModel} />
<LoginButton viewModel={this.props.viewModel} />
</View>
);
}
}
// 子组件示例
@observer
class PasswordInput extends Component<{ viewModel: LoginViewModel }> {
render() {
return (
<TextInput
value={this.props.viewModel.password}
onChangeText={this.props.viewModel.setPassword}
secureTextEntry
/>
);
}
}
方案二:使用 React Context
对于多层次嵌套的子组件,可以使用 React Context 避免 props 地狱:
// AuthContext.ts
import React from 'react';
import { LoginViewModel } from './viewmodels/LoginViewModel';
export const AuthContext = React.createContext<LoginViewModel | null>(null);
// LoginView.tsx
@observer
export default class LoginView extends Component<LoginViewProps> {
render() {
return (
<AuthContext.Provider value={this.props.viewModel}>
<EmailInput />
<PasswordInput />
<PasswordStrengthIndicator />
<CaptchaComponent />
<LoginButton />
</AuthContext.Provider>
);
}
}
// 深层子组件示例
@observer
class PasswordStrengthIndicator extends Component {
static contextType = AuthContext;
context!: LoginViewModel;
render() {
const strength = calculatePasswordStrength(this.context.password);
return <Text>密码强度: {strength}</Text>;
}
}
方案三:自定义 Hook (函数组件)
对于函数组件,可以创建自定义 Hook 获取 ViewModel:
// useLoginViewModel.ts
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
export const useLoginViewModel = () => {
const viewModel = useContext(AuthContext);
if (!viewModel) {
throw new Error('useLoginViewModel must be used within AuthContext');
}
return viewModel;
};
// 函数子组件
const PasswordStrengthIndicator = observer(() => {
const viewModel = useLoginViewModel();
const strength = calculatePasswordStrength(viewModel.password);
return <Text>密码强度: {strength}</Text>;
});
方案四:使用 ServiceLocator 直接获取
通过单例服务定位器直接获取 ViewModel 实例:
// ServiceLocator.ts
export class ServiceLocator {
// ...其他服务
getLoginViewModel() {
return new LoginViewModel(
this.getAuthService(),
this.getAppStore()
);
}
}
// 任意子组件中
@observer
class LoginButton extends Component {
private viewModel = ServiceLocator.getInstance().getLoginViewModel();
render() {
return (
<Button
title="登录"
onPress={this.viewModel.login}
disabled={this.viewModel.loading}
/>
);
}
}
最佳实践建议
- 浅层嵌套:使用 props 传递(方案一),简单直观
- 深层嵌套:使用 Context(方案二)或自定义 Hook(方案三)
- 全局访问:使用 ServiceLocator(方案四),适合工具类组件