普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月7日首页

React Native vs React Web:深度对比与架构解析

作者 北辰alk
2025年12月7日 12:34

一、引言:同一个理念,不同的实现

React 技术栈以其"Learn Once, Write Anywhere"的理念改变了前端开发格局。然而,许多开发者常混淆 React Native 和 React Web(通常简称 React)之间的区别。虽然它们共享相同的设计哲学,但在实现、架构和应用场景上存在本质差异。本文将深入探讨两者的核心区别,并通过代码示例、架构图展示实际差异。

二、核心理念对比

1. 设计哲学的异同

mindmap
  root(React 技术栈)
    核心理念
      组件化
      声明式UI
      单向数据流
    技术实现
      React Web
        :DOM操作
        :CSS样式
        :浏览器API
      React Native
        :原生组件
        :平台API
        :原生渲染

相同点:

  • 组件化开发模式
  • 虚拟DOM概念
  • JSX语法
  • 单向数据流
  • 生命周期管理(在函数组件中为Hooks)

不同点:

  • 渲染目标:React Web 渲染到浏览器DOM,React Native 渲染到原生UI组件
  • 样式系统:React Web 使用CSS,React Native 使用JavaScript对象
  • 生态体系:完全不同的第三方库生态系统
  • 平台能力:访问的平台API完全不同

三、架构深度解析

1. React Web 架构

// React Web 渲染流程
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <div className="container">
      <h1>Hello React Web</h1>
      <p>This renders to DOM</p>
    </div>
  );
};

// 渲染到浏览器DOM
ReactDOM.render(<App />, document.getElementById('root'));

React Web 架构流程图:

flowchart TD
    A[JSX/组件] --> B[React.createElement<br>创建虚拟DOM]
    B --> C[Reconciliation<br>对比虚拟DOM差异]
    C --> D[DOM操作<br>更新实际DOM]
    D --> E[浏览器渲染<br>布局与绘制]
    E --> F[用户界面<br>HTML/CSS渲染]
    
    G[用户交互] --> H[事件处理]
    H --> A

2. React Native 架构

// React Native 渲染流程
import React from 'react';
import { 
  View, 
  Text, 
  StyleSheet,
  AppRegistry 
} from 'react-native';

const App = () => {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Hello React Native</Text>
      <Text>This renders to native components</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 20,
    fontWeight: 'bold',
  },
});

// 注册并启动应用
AppRegistry.registerComponent('MyApp', () => App);

React Native 架构流程图:

flowchart TD
    A[JSX/组件] --> B[JavaScript Core<br>执行React代码]
    B --> C[React Native Bridge<br>跨平台通信]
    C --> D{iOS/Android<br>原生模块}
    D --> E[iOS UIKit<br>Objective-C/Swift]
    D --> F[Android Views<br>Java/Kotlin]
    E --> G[原生UI渲染<br>iOS屏幕]
    F --> H[原生UI渲染<br>Android屏幕]
    
    I[用户交互] --> J[原生事件]
    J --> C
    C --> B

四、组件系统对比

1. 基础组件差异对比表

组件类型 React Web (HTML) React Native (原生) 功能说明
容器 <div> <View> 布局容器
文本 <span>, <p> <Text> 文本显示
图片 <img> <Image> 图片显示
按钮 <button> <Button>, <TouchableOpacity> 交互按钮
输入 <input> <TextInput> 文本输入
列表 <ul>, <table> <FlatList>, <ScrollView> 列表展示
滚动 <div style="overflow:auto"> <ScrollView> 滚动容器

2. 实际代码对比

// ============ REACT WEB ============
import React, { useState } from 'react';
import './styles.css'; // 引入CSS文件

const WebComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="container">
      <header className="header">
        <h1 className="title">React Web App</h1>
      </header>
      <main className="content">
        <p className="count-text">Count: {count}</p>
        <button 
          className="button" 
          onClick={() => setCount(count + 1)}
        >
          Increment
        </button>
        <input 
          type="text" 
          className="input" 
          placeholder="Enter text..."
        />
        <img 
          src="/logo.png" 
          alt="Logo" 
          className="logo"
        />
      </main>
    </div>
  );
};

// ============ REACT NATIVE ============
import React, { useState } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  TextInput,
  Image,
  StyleSheet,
  SafeAreaView,
} from 'react-native';

const NativeComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>React Native App</Text>
      </View>
      <View style={styles.content}>
        <Text style={styles.countText}>Count: {count}</Text>
        <TouchableOpacity
          style={styles.button}
          onPress={() => setCount(count + 1)}
        >
          <Text style={styles.buttonText}>Increment</Text>
        </TouchableOpacity>
        <TextInput
          style={styles.input}
          placeholder="Enter text..."
          placeholderTextColor="#999"
        />
        <Image
          source={require('./logo.png')}
          style={styles.logo}
          resizeMode="contain"
        />
      </View>
    </SafeAreaView>
  );
};

// React Native 样式定义
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  header: {
    padding: 20,
    backgroundColor: '#007AFF',
    alignItems: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: 'white',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  countText: {
    fontSize: 32,
    marginBottom: 20,
  },
  button: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 30,
    paddingVertical: 15,
    borderRadius: 8,
    marginBottom: 20,
  },
  buttonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: '600',
  },
  input: {
    width: '80%',
    height: 50,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 15,
    marginBottom: 20,
    fontSize: 16,
  },
  logo: {
    width: 100,
    height: 100,
  },
});

五、样式系统深度对比

1. React Web 样式系统

/* styles.css - CSS Modules 示例 */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.button {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}

/* CSS-in-JS 示例 (styled-components) */
import styled from 'styled-components';

const StyledButton = styled.button`
  background: ${props => props.primary ? '#007AFF' : '#ccc'};
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  
  &:hover {
    opacity: 0.9;
  }
  
  &:active {
    transform: scale(0.98);
  }
`;

2. React Native 样式系统

// StyleSheet 示例
import { StyleSheet, Dimensions } from 'react-native';

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    width: width, // 响应式宽度
    backgroundColor: '#ffffff',
  },
  card: {
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5, // Android阴影
    borderRadius: 10,
    backgroundColor: 'white',
    margin: 10,
    padding: 15,
  },
  gradientButton: {
    // 注意:React Native 需要第三方库实现渐变
    backgroundColor: '#007AFF',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
  },
});

// 响应式布局示例
const responsiveStyles = StyleSheet.create({
  container: {
    flexDirection: width > 768 ? 'row' : 'column',
  },
  column: {
    flex: width > 768 ? 1 : undefined,
  },
});

// 平台特定样式
const platformStyles = StyleSheet.create({
  header: {
    paddingTop: Platform.OS === 'ios' ? 50 : 25, // iOS有安全区域
    ...Platform.select({
      ios: {
        backgroundColor: '#f8f8f8',
      },
      android: {
        backgroundColor: '#ffffff',
      },
    }),
  },
});

六、导航系统对比

1. React Web 导航

// React Router 示例
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const WebNavigation = () => (
  <BrowserRouter>
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
    </nav>
    
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
      <Route path="/user/:id" element={<UserProfile />} />
    </Routes>
  </BrowserRouter>
);

// 历史记录API访问
const navigateToAbout = () => {
  window.history.pushState({}, '', '/about');
  // 或使用react-router的useNavigate
};

2. React Native 导航

// React Navigation 示例
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();

// 栈式导航
const AppNavigator = () => (
  <NavigationContainer>
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions={{
        headerStyle: {
          backgroundColor: '#007AFF',
        },
        headerTintColor: '#fff',
      }}
    >
      <Stack.Screen 
        name="Home" 
        component={HomeScreen}
        options={{ title: '首页' }}
      />
      <Stack.Screen 
        name="Details" 
        component={DetailsScreen}
        options={({ route }) => ({ 
          title: route.params?.title || '详情'
        })}
      />
    </Stack.Navigator>
  </NavigationContainer>
);

// 标签页导航
const TabNavigator = () => (
  <Tab.Navigator
    screenOptions={({ route }) => ({
      tabBarIcon: ({ focused, color, size }) => {
        let iconName;
        if (route.name === 'Home') {
          iconName = focused ? 'home' : 'home-outline';
        } else if (route.name === 'Settings') {
          iconName = focused ? 'settings' : 'settings-outline';
        }
        return <Icon name={iconName} size={size} color={color} />;
      },
    })}
  >
    <Tab.Screen name="Home" component={HomeScreen} />
    <Tab.Screen name="Settings" component={SettingsScreen} />
  </Tab.Navigator>
);

七、平台API访问对比

1. React Web API 访问

// 浏览器API访问示例
class WebAPIService {
  // 本地存储
  static saveData(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  static getData(key) {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }
  
  // 地理位置
  static async getLocation() {
    return new Promise((resolve, reject) => {
      if (!navigator.geolocation) {
        reject(new Error('Geolocation not supported'));
        return;
      }
      
      navigator.geolocation.getCurrentPosition(
        position => resolve(position.coords),
        error => reject(error),
        { enableHighAccuracy: true }
      );
    });
  }
  
  // 摄像头访问
  static async accessCamera() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });
    return stream;
  }
  
  // 网络状态
  static getNetworkStatus() {
    return {
      online: navigator.onLine,
      connection: navigator.connection || {},
    };
  }
}

// 使用示例
WebAPIService.saveData('user', { name: 'John' });
const location = await WebAPIService.getLocation();

2. React Native API 访问

// React Native 原生模块访问
import {
  AsyncStorage,
  Geolocation,
  PermissionsAndroid,
  Platform,
} from 'react-native';
import CameraRoll from '@react-native-community/cameraroll';
import NetInfo from '@react-native-community/netinfo';

class NativeAPIService {
  // 本地存储(使用AsyncStorage)
  static async saveData(key, value) {
    try {
      await AsyncStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('保存数据失败:', error);
    }
  }
  
  static async getData(key) {
    try {
      const value = await AsyncStorage.getItem(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('读取数据失败:', error);
      return null;
    }
  }
  
  // 地理位置(需要权限)
  static async getLocation() {
    if (Platform.OS === 'android') {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
      );
      if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
        throw new Error('Location permission denied');
      }
    }
    
    return new Promise((resolve, reject) => {
      Geolocation.getCurrentPosition(
        position => resolve(position.coords),
        error => reject(error),
        { enableHighAccuracy: true, timeout: 15000 }
      );
    });
  }
  
  // 访问相册
  static async getPhotos(params) {
    try {
      const photos = await CameraRoll.getPhotos(params);
      return photos;
    } catch (error) {
      console.error('获取照片失败:', error);
      throw error;
    }
  }
  
  // 网络状态监听
  static setupNetworkListener(callback) {
    return NetInfo.addEventListener(state => {
      callback(state);
    });
  }
  
  // 设备信息
  static getDeviceInfo() {
    return {
      platform: Platform.OS,
      version: Platform.Version,
      isPad: Platform.isPad,
      isTV: Platform.isTV,
    };
  }
}

// 使用示例
const location = await NativeAPIService.getLocation();
const unsubscribe = NativeAPIService.setupNetworkListener(state => {
  console.log('网络状态:', state.isConnected);
});

八、性能优化策略对比

性能优化对比表

优化维度 React Web React Native 说明
渲染优化 Virtual DOM Diff 原生组件更新 React Web 操作DOM,RN直接更新原生组件
图片优化 Lazy Loading FastImage RN需要特殊处理图片缓存
列表优化 Virtual Scrolling FlatList优化 两者都需要虚拟化长列表
代码分割 Webpack动态导入 Metro Bundle分块 RN需要原生配置支持
内存管理 自动垃圾回收 需注意原生模块内存 RN需要手动管理部分内存

1. React Web 性能优化

// 代码分割和懒加载
const LazyComponent = React.lazy(() => import('./HeavyComponent'));

// 使用memo和useCallback
const MemoizedComponent = React.memo(({ data }) => (
  <div>{data}</div>
));

// 虚拟化长列表
import { FixedSizeList } from 'react-window';

const VirtualizedList = ({ items }) => (
  <FixedSizeList
    height={400}
    width={300}
    itemCount={items.length}
    itemSize={50}
  >
    {({ index, style }) => (
      <div style={style}>
        Item {items[index]}
      </div>
    )}
  </FixedSizeList>
);

// Web Workers 处理耗时任务
const worker = new Worker('./heavy-task.worker.js');
worker.postMessage(data);
worker.onmessage = (event) => {
  console.log('结果:', event.data);
};

2. React Native 性能优化

// 使用PureComponent或memo
class OptimizedComponent extends React.PureComponent {
  render() {
    return <Text>{this.props.data}</Text>;
  }
}

// 优化FlatList
const OptimizedList = ({ data }) => (
  <FlatList
    data={data}
    keyExtractor={item => item.id}
    renderItem={renderItem}
    initialNumToRender={10}
    maxToRenderPerBatch={5}
    windowSize={21}
    removeClippedSubviews={true}
    getItemLayout={(data, index) => ({
      length: 50,
      offset: 50 * index,
      index,
    })}
  />
);

// 使用InteractionManager处理动画
InteractionManager.runAfterInteractions(() => {
  // 耗时操作,避免阻塞动画
});

// 图片优化
import FastImage from 'react-native-fast-image';

<FastImage
  style={styles.image}
  source={{
    uri: 'https://example.com/image.jpg',
    priority: FastImage.priority.normal,
    cache: FastImage.cacheControl.immutable,
  }}
/>;

九、开发体验对比

开发环境配置差异

# React Web 开发环境
开发工具: VSCode/WebStorm
包管理器: npm/yarn
构建工具: Webpack/Vite
开发服务器: webpack-dev-server
热重载: 内置支持
调试工具: Chrome DevTools

# React Native 开发环境
开发工具: VSCode/WebStorm/Xcode/Android Studio
包管理器: npm/yarn
构建工具: Metro Bundler
模拟器: iOS Simulator/Android Emulator
真机调试: 需要USB连接
调试工具: React Native Debugger/Flipper

热重载机制对比

// React Web 热重载流程
1. 文件保存 → 2. Webpack检测变化 → 3. 重新编译模块
4. 通过WebSocket推送更新 → 5. 客户端接收更新
6. 替换模块 → 7. 保留应用状态

// React Native 热重载流程
1. 文件保存 → 2. Metro检测变化 → 3. 增量构建
4. 推送更新到设备 → 5. 原生容器重新渲染
6. 保持JavaScript状态

十、跨平台复用策略

1. 共享业务逻辑

// shared/ 目录结构
shared/
├── api/
│   └── apiClient.js      # 网络请求封装
├── utils/
│   ├── dateFormatter.js  # 日期格式化
│   ├── validator.js      # 表单验证
│   └── constants.js      # 常量定义
├── services/
│   └── authService.js    # 认证服务
└── hooks/
    └── useFetch.js       # 自定义Hook

// 示例:共享的API客户端
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  // 可在Web和Native中复用的方法
  async getUser(id) {
    return this.request(`/users/${id}`);
  }

  async createPost(data) {
    return this.request('/posts', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
}

2. 条件平台渲染

// PlatformSpecific.js
import React from 'react';
import { Platform } from 'react-native';

// 方法1: 平台特定文件扩展名
// MyComponent.ios.js 和 MyComponent.android.js

// 方法2: 平台检测
const PlatformSpecificComponent = () => {
  if (Platform.OS === 'web') {
    return (
      <div className="web-container">
        <p>This is web version</p>
      </div>
    );
  }

  return (
    <View style={styles.nativeContainer}>
      <Text>This is native version</Text>
    </View>
  );
};

// 方法3: 平台特定Hook
const usePlatform = () => {
  return {
    isWeb: Platform.OS === 'web',
    isIOS: Platform.OS === 'ios',
    isAndroid: Platform.OS === 'android',
    platform: Platform.OS,
  };
};

// 方法4: 共享组件适配器
const Button = ({ title, onPress }) => {
  const { isWeb } = usePlatform();
  
  if (isWeb) {
    return (
      <button 
        className="button"
        onClick={onPress}
      >
        {title}
      </button>
    );
  }
  
  return (
    <TouchableOpacity
      style={styles.button}
      onPress={onPress}
    >
      <Text style={styles.buttonText}>{title}</Text>
    </TouchableOpacity>
  );
};

十一、实际项目架构示例

跨平台项目结构

my-cross-platform-app/
├── packages/
│   ├── web/                    # React Web 应用
│   │   ├── public/
│   │   ├── src/
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── mobile/                 # React Native 应用
│   │   ├── ios/
│   │   ├── android/
│   │   ├── src/
│   │   └── package.json
│   └── shared/                 # 共享代码
│       ├── components/         # 跨平台组件
│       ├── utils/             # 工具函数
│       ├── services/          # API服务
│       └── hooks/             # 自定义Hooks
├── package.json
└── yarn.lock

跨平台组件实现

// shared/components/Button/index.js
import React from 'react';
import { Platform } from 'react-native';

// 平台特定的实现
import { ButtonWeb } from './Button.web';
import { ButtonNative } from './Button.native';

const Button = (props) => {
  if (Platform.OS === 'web') {
    return <ButtonWeb {...props} />;
  }
  
  return <ButtonNative {...props} />;
};

export default Button;

// shared/components/Button/Button.web.js
import React from 'react';
import PropTypes from 'prop-types';

export const ButtonWeb = ({ 
  title, 
  onPress, 
  variant = 'primary',
  disabled 
}) => {
  return (
    <button
      className={`button button-${variant}`}
      onClick={onPress}
      disabled={disabled}
      style={{
        padding: '12px 24px',
        borderRadius: '6px',
        border: 'none',
        cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.6 : 1,
      }}
    >
      {title}
    </button>
  );
};

// shared/components/Button/Button.native.js
import React from 'react';
import { 
  TouchableOpacity, 
  Text, 
  StyleSheet,
  ActivityIndicator 
} from 'react-native';

export const ButtonNative = ({ 
  title, 
  onPress, 
  variant = 'primary',
  disabled,
  loading 
}) => {
  const variantStyles = {
    primary: styles.primaryButton,
    secondary: styles.secondaryButton,
    outline: styles.outlineButton,
  };

  return (
    <TouchableOpacity
      style={[
        styles.button,
        variantStyles[variant],
        disabled && styles.disabled,
      ]}
      onPress={onPress}
      disabled={disabled || loading}
      activeOpacity={0.7}
    >
      {loading ? (
        <ActivityIndicator 
          color={variant === 'outline' ? '#007AFF' : 'white'} 
        />
      ) : (
        <Text style={[
          styles.buttonText,
          variant === 'outline' && styles.outlineText,
        ]}>
          {title}
        </Text>
      )}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  button: {
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 6,
    alignItems: 'center',
    justifyContent: 'center',
    minHeight: 48,
  },
  primaryButton: {
    backgroundColor: '#007AFF',
  },
  secondaryButton: {
    backgroundColor: '#6c757d',
  },
  outlineButton: {
    backgroundColor: 'transparent',
    borderWidth: 1,
    borderColor: '#007AFF',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  outlineText: {
    color: '#007AFF',
  },
  disabled: {
    opacity: 0.6,
  },
});

十二、总结与选择建议

关键差异总结表

方面 React Web React Native 建议
目标平台 浏览器 iOS/Android移动端 根据目标用户选择
渲染方式 DOM操作 原生组件调用 Web适合内容展示,RN适合应用体验
开发体验 浏览器DevTools 模拟器/真机调试 Web开发更直观
性能特点 受浏览器限制 接近原生性能 性能敏感选RN
热更新 即时生效 需要重新打包 快速迭代选Web
发布流程 直接部署 应用商店审核 频繁更新选Web

选择建议流程图

flowchart TD
    A[项目需求分析] --> B{目标平台?}
    B --> C[仅Web/桌面端]
    B --> D[仅移动端<br>iOS/Android]
    B --> E[全平台覆盖]
    
    C --> F[选择 React Web<br>最佳开发体验]
    D --> G[选择 React Native<br>原生体验]
    E --> H{项目类型?}
    
    H --> I[内容型应用<br>新闻/博客/电商]
    H --> J[交互型应用<br>社交/工具/游戏]
    
    I --> K[优先 React Web<br>考虑PWA]
    J --> L[优先 React Native<br>考虑跨平台组件]
    
    K --> M[评估用户需求<br>适时添加React Native]
    L --> N[复用业务逻辑<br>平台特定UI]
    
    M --> O[监控用户反馈<br>数据驱动决策]
    N --> P[持续优化<br>保持代码复用]

最佳实践建议

  1. 新项目启动

    • 明确目标平台和用户群体
    • 评估团队技术栈熟悉度
    • 考虑长期维护成本
  2. 现有项目扩展

    • React Web项目可逐步集成PWA
    • React Native项目可考虑Web支持
    • 优先复用业务逻辑,平台差异通过适配层处理
  3. 团队建设

    • React Web和React Native需要不同的专业知识
    • 建立共享代码规范和组件库
    • 培养全栈React开发工程师
  4. 技术选型

    • 内容为主的应用优先考虑Web + PWA
    • 需要设备功能的应用优先考虑React Native
    • 大型企业应用考虑微前端架构

结语

React Web和React Native虽然共享相同的设计理念,但在实现、架构和应用场景上有本质区别。理解这些差异不仅有助于选择正确的技术栈,还能在跨平台开发中做出更明智的架构决策。

随着React生态的不断发展,两个平台之间的界限逐渐模糊。React Native for Web等项目正在尝试弥合这个鸿沟,未来的开发可能会更加无缝。无论选择哪个平台,深入理解React的核心概念都是成功的关键。

从零构建Vue项目的完全指南:手把手打造现代化前端工程

作者 北辰alk
2025年12月7日 09:54

从零构建Vue项目的完全指南:手把手打造现代化前端工程

一、项目构建整体流程图

让我们先看看完整的项目构建流程:

deepseek_mermaid_20251207_d1ddc8.png

二、详细构建步骤

步骤1:环境准备与项目初始化

首先确保你的开发环境已准备好:

# 检查Node.js版本(建议18+)
node -v

# 检查npm版本
npm -v

# 安装Vue CLI(如果还没有)
npm install -g @vue/cli

# 创建新项目
vue create my-vue-project

# 选择配置(推荐手动选择)
? Please pick a preset: 
  Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
❯ Manually select features

# 选择需要的功能
? Check the features needed for your project:
 ◉ Babel
 ◉ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
❯◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

步骤2:项目目录结构设计

一个良好的目录结构是项目成功的基础。这是我推荐的目录结构:

my-vue-project/
├── public/                    # 静态资源
│   ├── index.html
│   ├── favicon.ico
│   └── robots.txt
├── src/
│   ├── api/                  # API接口管理
│   │   ├── modules/         # 按模块划分的API
│   │   ├── index.ts         # API统一导出
│   │   └── request.ts       # 请求封装
│   ├── assets/              # 静态资源
│   │   ├── images/
│   │   ├── styles/
│   │   └── fonts/
│   ├── components/          # 公共组件
│   │   ├── common/         # 全局通用组件
│   │   ├── business/       # 业务组件
│   │   └── index.ts        # 组件自动注册
│   ├── composables/        # 组合式函数
│   │   ├── useFetch.ts
│   │   ├── useForm.ts
│   │   └── index.ts
│   ├── directives/         # 自定义指令
│   │   ├── permission.ts
│   │   └── index.ts
│   ├── layouts/            # 布局组件
│   │   ├── DefaultLayout.vue
│   │   └── AuthLayout.vue
│   ├── router/             # 路由配置
│   │   ├── modules/       # 路由模块
│   │   ├── index.ts
│   │   └── guard.ts      # 路由守卫
│   ├── store/              # Vuex/Pinia状态管理
│   │   ├── modules/       # 模块化store
│   │   └── index.ts
│   ├── utils/              # 工具函数
│   │   ├── auth.ts        # 权限相关
│   │   ├── validate.ts    # 验证函数
│   │   └── index.ts
│   ├── views/              # 页面组件
│   │   ├── Home/
│   │   ├── User/
│   │   └── About/
│   ├── types/              # TypeScript类型定义
│   │   ├── api.d.ts
│   │   ├── global.d.ts
│   │   └── index.d.ts
│   ├── App.vue
│   └── main.ts
├── tests/                   # 测试文件
├── .env.*                   # 环境变量
├── vite.config.ts          # Vite配置
├── tsconfig.json           # TypeScript配置
└── package.json

步骤3:核心配置详解

1. 配置Vite(vite.config.ts)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@views': path.resolve(__dirname, 'src/views'),
    },
  },
  server: {
    host'0.0.0.0',
    port3000,
    proxy: {
      '/api': {
        target'http://localhost:8080',
        changeOrigintrue,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/assets/styles/variables.scss";`,
      },
    },
  },
})
2. 路由配置(router/index.ts)
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routesRouteRecordRaw[] = [
  {
    path'/',
    name'Home',
    component() => import('@views/Home/Home.vue'),
    meta: {
      title'首页',
      requiresAuthtrue,
    },
  },
  {
    path'/login',
    name'Login',
    component() => import('@views/Login/Login.vue'),
    meta: {
      title'登录',
    },
  },
  {
    path'/user/:id',
    name'User',
    component() => import('@views/User/User.vue'),
    propstrue,
  },
]

const router = createRouter({
  historycreateWebHistory(),
  routes,
})

// 路由守卫
router.beforeEach((to, from, next) => {
  document.title = to.meta.title as string || 'Vue项目'
  
  // 检查是否需要登录
  if (to.meta.requiresAuth && !localStorage.getItem('token')) {
    next('/login')
  } else {
    next()
  }
})

export default router
3. 状态管理(使用Pinia)
// store/user.ts
import { defineStore } from 'pinia'

interface UserState {
  userInfo: {
    namestring
    avatarstring
    rolesstring[]
  } | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    userInfonull,
  }),
  actions: {
    async login(credentials: { username: string; password: string }) {
      // 登录逻辑
      const response = await api.login(credentials)
      this.userInfo = response.data
      localStorage.setItem('token', response.token)
    },
    logout() {
      this.userInfo = null
      localStorage.removeItem('token')
    },
  },
  getters: {
    isLoggedIn(state) => !!state.userInfo,
    hasRole(state) => (role: string) => 
      state.userInfo?.roles.includes(role) || false,
  },
})

步骤4:核心工具库和插件选择

这是我在项目中推荐使用的库:

{
  "dependencies": {
    "vue""^3.3.0",
    "vue-router""^4.2.0",
    "pinia""^2.1.0",
    "axios""^1.4.0",
    "element-plus""^2.3.0",
    "lodash-es""^4.17.21",
    "dayjs""^1.11.0",
    "vxe-table""^4.0.0",
    "vue-i18n""^9.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue""^4.2.0",
    "@types/node""^20.0.0",
    "sass""^1.62.0",
    "eslint""^8.0.0",
    "prettier""^3.0.0",
    "husky""^8.0.0",
    "commitlint""^17.0.0",
    "vitest""^0.30.0",
    "unplugin-auto-import""^0.16.0",
    "unplugin-vue-components""^0.25.0"
  }
}

步骤5:实用的组件示例

1. 全局请求封装
// src/api/request.ts
import axios from 'axios'
import type { AxiosRequestConfigAxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'

const service = axios.create({
  baseURLimport.meta.env.VITE_API_BASE_URL,
  timeout10000,
})

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, data, message } = response.data
    
    if (code === 200) {
      return data
    } else {
      ElMessage.error(message || '请求失败')
      return Promise.reject(new Error(message))
    }
  },
  (error) => {
    if (error.response?.status === 401) {
      // 未授权,跳转到登录页
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    ElMessage.error(error.message || '网络错误')
    return Promise.reject(error)
  }
)

export default service
2. 自动导入组件配置
// vite.config.ts 补充配置
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    // 自动导入API
    AutoImport({
      imports: ['vue''vue-router''pinia'],
      dts'src/types/auto-imports.d.ts',
      resolvers: [ElementPlusResolver()],
    }),
    // 自动导入组件
    Components({
      dts'src/types/components.d.ts',
      resolvers: [ElementPlusResolver()],
      dirs: ['src/components'],
    }),
  ],
})
3. 实用的Vue 3组合式函数
// src/composables/useForm.ts
import { ref, reactive, computed } from 'vue'
import type { Ref } from 'vue'

export function useForm<T extends object>(initialData: T) {
  const formData = reactive({ ...initialData }) as T
  const errors = reactive<Record<stringstring>>({})
  const isSubmitting = ref(false)

  const validate = async (): Promise<boolean> => {
    // 这里可以集成具体的验证逻辑
    return true
  }

  const submit = async (submitFn: (data: T) => Promise<any>) => {
    if (!(await validate())) return
    
    isSubmitting.value = true
    try {
      const result = await submitFn(formData)
      return result
    } catch (error) {
      throw error
    } finally {
      isSubmitting.value = false
    }
  }

  const reset = () => {
    Object.assign(formData, initialData)
    Object.keys(errors).forEach(key => {
      errors[key] = ''
    })
  }

  return {
    formData,
    errors,
    isSubmittingcomputed(() => isSubmitting.value),
    validate,
    submit,
    reset,
  }
}

步骤6:开发规范与最佳实践

1. 代码提交规范
# 安装Git提交钩子
npx husky install
npm install -D @commitlint/config-conventional @commitlint/cli

# 创建commitlint配置
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > .commitlintrc.js

# 创建提交信息规范
# feat: 新功能
# fix: 修复bug
# docs: 文档更新
# style: 代码格式
# refactor: 重构
# test: 测试
# chore: 构建过程或辅助工具的变动
2. 环境变量配置
# .env.development
VITE_APP_TITLE=开发环境
VITE_API_BASE_URL=/api
VITE_USE_MOCK=true

# .env.production
VITE_APP_TITLE=生产环境
VITE_API_BASE_URL=https://api.example.com
VITE_USE_MOCK=false

步骤7:性能优化建议

// 路由懒加载优化
const routes = [
  {
    path'/dashboard',
    component() => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'),
  },
  {
    path'/settings',
    component() => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue'),
  },
]

// 图片懒加载指令
// src/directives/lazyLoad.ts
import type { Directive } from 'vue'

const lazyLoadDirective = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    })
    observer.observe(el)
  },
}

三、项目启动和常用命令

{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "format": "prettier --write src/",
    "prepare": "husky install",
    "test": "vitest",
    "test:coverage": "vitest --coverage"
  }
}

四、总结与建议

通过以上步骤,你已经拥有了一个现代化、可维护的Vue项目基础。记住几个关键点:

  1. 1. 保持一致性 - 无论是命名规范还是代码风格
  2. 2. 模块化设计 - 功能解耦,便于维护和测试
  3. 3. 类型安全 - 充分利用TypeScript的优势
  4. 4. 自动化 - 尽可能自动化重复工作
  5. 5. 渐进式 - 不要一开始就追求完美,根据项目需求逐步完善

项目代码就像一座大厦,良好的基础决定了它的稳固性和可扩展性。希望这篇指南能帮助你在Vue项目开发中少走弯路!

昨天以前首页

Vue Router 组件内路由钩子全解析

作者 北辰alk
2025年12月6日 11:38

一、什么是组件内路由钩子?

在 Vue Router 中,组件内路由钩子(也称为导航守卫)是在路由变化时自动调用的特殊函数,它们允许我们在特定时机执行自定义逻辑,比如:

  • • 权限验证(是否登录)
  • • 数据预加载
  • • 页面离开确认
  • • 滚动行为控制
  • • 动画过渡处理
// 一个简单的示例
export default {
  name'UserProfile',
  beforeRouteEnter(to, from, next) {
    console.log('组件还未创建,但即将进入...')
    next()
  }
}

二、三大核心钩子函数详解

Vue Router 提供了三个主要的组件内路由钩子,它们组成了一个完整的导航生命周期:

1. beforeRouteEnter - 进入前的守卫

调用时机:在组件实例被创建之前调用,此时组件还未初始化。

特点

  • • 不能访问 this(因为组件实例还未创建)
  • • 可以通过回调函数访问组件实例
export default {
  beforeRouteEnter(to, from, next) {
    // ❌ 这里不能使用 this
    console.log('from'from.path// 可以访问来源路由
    
    // ✅ 通过 next 的回调访问组件实例
    next(vm => {
      console.log('组件实例:', vm)
      vm.loadData(to.params.id)
    })
  },
  
  methods: {
    loadData(id) {
      // 加载数据逻辑
    }
  }
}

适用场景

  • • 基于路由参数的权限验证
  • • 预加载必要数据
  • • 重定向到其他页面

2. beforeRouteUpdate - 路由更新守卫

调用时机:在当前路由改变,但组件被复用时调用。

常见情况

  • • 从 /user/1 导航到 /user/2
  • • 查询参数改变:/search?q=vue → /search?q=react
export default {
  data() {
    return {
      usernull
    }
  },
  
  beforeRouteUpdate(to, from, next) {
    // ✅ 可以访问 this
    console.log('路由参数变化:'from.params.id'→', to.params.id)
    
    // 重新加载数据
    this.fetchUserData(to.params.id)
    
    // 必须调用 next()
    next()
  },
  
  methods: {
    async fetchUserData(id) {
      const response = await fetch(`/api/users/${id}`)
      this.user = await response.json()
    }
  }
}

实用技巧:使用这个钩子可以避免重复渲染,提升性能。

3. beforeRouteLeave - 离开前的守卫

调用时机:在离开当前路由时调用。

重要特性

  • • 可以阻止导航
  • • 常用于保存草稿或确认离开
export default {
  data() {
    return {
      hasUnsavedChangesfalse,
      formData: {
        title'',
        content''
      }
    }
  },
  
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      const answer = window.confirm(
        '您有未保存的更改,确定要离开吗?'
      )
      
      if (answer) {
        next() // 允许离开
      } else {
        next(false// 取消导航
      }
    } else {
      next() // 直接离开
    }
  },
  
  methods: {
    onInput() {
      this.hasUnsavedChanges = true
    },
    
    save() {
      // 保存逻辑
      this.hasUnsavedChanges = false
    }
  }
}

三、完整导航流程图

让我们通过一个完整的流程图来理解这些钩子的执行顺序:

是

否

是

next

next false

beforeRouteEnter 特殊处理
无法访问 this通过 next 回调访问实例开始导航组件是否复用?调用 beforeRouteUpdate调用 beforeRouteEnter组件内部处理确认导航 next创建组件实例执行 beforeRouteEnter 的回调渲染组件用户停留页面用户触发新导航?调用 beforeRouteLeave允许离开?执行新导航停留在当前页面

四、实际项目中的应用案例

案例1:用户权限验证系统

// UserProfile.vue
export default {
  beforeRouteEnter(to, from, next) {
    // 检查用户是否登录
    const isAuthenticated = checkAuth()
    
    if (!isAuthenticated) {
      // 未登录,重定向到登录页
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else if (!hasPermission(to.params.id)) {
      // 没有权限,重定向到403页面
      next('/403')
    } else {
      // 允许访问
      next()
    }
  },
  
  beforeRouteLeave(to, from, next) {
    // 如果是管理员,记录操作日志
    if (this.user.role === 'admin') {
      logAdminAccess(from.fullPath, to.fullPath)
    }
    next()
  }
}

案例2:电商商品详情页优化

// ProductDetail.vue
export default {
  data() {
    return {
      productnull,
      relatedProducts: []
    }
  },
  
  beforeRouteEnter(to, from, next) {
    // 预加载商品基础信息
    preloadProduct(to.params.id)
      .then(product => {
        next(vm => {
          vm.product = product
          // 同时开始加载相关商品
          vm.loadRelatedProducts(product.category)
        })
      })
      .catch(() => {
        next('/404'// 商品不存在
      })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 商品ID变化时,平滑过渡
    this.showLoading = true
    this.fetchProductData(to.params.id)
      .then(() => {
        this.showLoading = false
        next()
      })
      .catch(() => {
        next(false// 保持当前商品
      })
  },
  
  methods: {
    async fetchProductData(id) {
      const [product, related] = await Promise.all([
        api.getProduct(id),
        api.getRelatedProducts(id)
      ])
      this.product = product
      this.relatedProducts = related
    },
    
    loadRelatedProducts(category) {
      // 异步加载相关商品
    }
  }
}

五、高级技巧与最佳实践

1. 组合式API中的使用

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

export default {
  setup() {
    const unsavedChanges = ref(false)
    
    // 使用组合式API守卫
    onBeforeRouteLeave((to, from) => {
      if (unsavedChanges.value) {
        return confirm('确定要离开吗?')
      }
    })
    
    onBeforeRouteUpdate(async (to, from) => {
      // 处理路由参数更新
      await loadData(to.params.id)
    })
    
    return { unsavedChanges }
  }
}

2. 异步操作的优雅处理

export default {
  beforeRouteEnter(tofromnext) {
    // 使用async/await
    const enterGuard = async () => {
      try {
        const isValid = await validateToken(to.query.token)
        if (isValid) {
          next()
        } else {
          next('/invalid-token')
        }
      } catch (error) {
        next('/error')
      }
    }
    
    enterGuard()
  }
}

3. 避免常见的坑

坑1:忘记调用 next()

// ❌ 错误示例 - 会导致导航挂起
beforeRouteEnter(to, from, next) {
  if (checkAuth()) {
    // 忘记调用 next()
  }
}

// ✅ 正确示例
beforeRouteEnter(to, from, next) {
  if (checkAuth()) {
    next()
  } else {
    next('/login')
  }
}

坑2:beforeRouteEnter 中直接修改数据

// ❌ 错误示例
beforeRouteEnter(to, from, next) {
  next(vm => {
    // 避免直接修改响应式数据
    vm.someData = 'value' // 可能导致响应式问题
  })
}

// ✅ 正确示例
beforeRouteEnter(to, from, next) {
  next(vm => {
    vm.$nextTick(() => {
      vm.someData = 'value' // 在下一个tick中修改
    })
  })
}

六、与其他导航守卫的配合

组件内守卫还可以与全局守卫、路由独享守卫配合使用:

// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log('全局守卫 → 组件守卫')
  next()
})

// 路由配置中的独享守卫
const routes = [
  {
    path'/user/:id',
    componentUserProfile,
    beforeEnter(to, from, next) => {
      console.log('路由独享守卫 → 组件守卫')
      next()
    }
  }
]

执行顺序

    1. 导航被触发
    1. 调用全局 beforeEach
    1. 调用路由配置中的 beforeEnter
    1. 调用组件内的 beforeRouteEnter
    1. 导航被确认
    1. 调用全局的 afterEach

七、性能优化建议

1. 懒加载守卫逻辑

export default {
  beforeRouteEnter(to, from, next) {
    // 按需加载验证模块
    import('@/utils/auth').then(module => {
      if (module.checkPermission(to.meta.requiredRole)) {
        next()
      } else {
        next('/forbidden')
      }
    })
  }
}

2. 缓存验证结果

let authCache = null

export default {
  beforeRouteEnter(to, from, next) {
    if (authCache === null) {
      // 首次验证
      checkAuth().then(result => {
        authCache = result
        handleNavigation(result, next)
      })
    } else {
      // 使用缓存结果
      handleNavigation(authCache, next)
    }
  }
}

总结

Vue Router 的组件内路由钩子为我们提供了强大的导航控制能力。通过合理使用这三个钩子函数,我们可以:

  1. 1. beforeRouteEnter:在组件创建前进行权限验证和数据预加载
  2. 2. beforeRouteUpdate:优化动态参数页面的用户体验
  3. 3. beforeRouteLeave:防止用户意外丢失未保存的数据

记住这些钩子的调用时机和限制,结合实际的业务需求,你就能构建出更加健壮、用户友好的单页应用。

❌
❌