GitDiagram移动应用:React Native跨平台开发实战

【免费下载链接】gitdiagram Replace 'hub' with 'diagram' in any GitHub url to instantly visualize the codebase as an interactive diagram 【免费下载链接】gitdiagram 项目地址: https://gitcode.com/GitHub_Trending/gi/gitdiagram

痛点:代码可视化在移动端的缺失

你是否曾在移动设备上想要快速理解一个GitHub项目的架构,却因为屏幕尺寸限制而无法有效浏览复杂的代码结构?传统的方式需要来回切换文件、手动构建心智模型,这个过程在手机上尤其痛苦。

GitDiagram的Web版本已经解决了桌面端的代码可视化问题,但移动端体验仍然是一个空白。这正是React Native跨平台开发的绝佳应用场景!

读完本文你能得到什么

  • ✅ React Native与GitDiagram API集成的完整方案
  • ✅ 移动端Mermaid图表渲染与交互的最佳实践
  • ✅ 跨平台性能优化和用户体验提升技巧
  • ✅ 完整的项目架构和代码示例
  • ✅ 部署和发布到应用商店的指南

技术架构设计

整体架构图

mermaid

核心技术栈对比

技术组件 Web版本 移动版本 优势
前端框架 Next.js React Native 代码复用,跨平台
图表渲染 Mermaid.js React Native Mermaid 原生性能
状态管理 React Hooks React Hooks + Redux 一致性
API调用 Fetch API Axios + 缓存 移动优化

React Native集成实战

项目初始化

# 创建新的React Native项目
npx react-native init GitDiagramMobile --template react-native-template-typescript

# 安装核心依赖
cd GitDiagramMobile
npm install axios mermaid-react-native react-native-svg
npm install @react-navigation/native @react-navigation/stack
npm install react-native-gesture-handler react-native-reanimated

API服务层封装

// src/services/gitdiagramApi.ts
import axios from 'axios';

const BASE_URL = 'https://gitdiagram.com/api';

export interface GenerateRequest {
  username: string;
  repo: string;
  instructions?: string;
  apiKey?: string;
  githubPat?: string;
}

export interface DiagramResponse {
  status: string;
  diagram?: string;
  explanation?: string;
  error?: string;
}

class GitDiagramService {
  private client = axios.create({
    baseURL: BASE_URL,
    timeout: 30000,
  });

  async generateDiagram(request: GenerateRequest): Promise<DiagramResponse> {
    try {
      const response = await this.client.post('/generate/stream', request);
      return response.data;
    } catch (error) {
      throw new Error('生成图表失败');
    }
  }

  async estimateCost(request: GenerateRequest): Promise<{ cost: string }> {
    const response = await this.client.post('/generate/cost', request);
    return response.data;
  }
}

export const gitDiagramService = new GitDiagramService();

移动端Mermaid渲染组件

// src/components/MermaidDiagram.tsx
import React, { useRef, useEffect } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { WebView } from 'react-native-webview';

interface MermaidDiagramProps {
  chart: string;
  onLinkPress?: (url: string) => void;
}

const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ chart, onLinkPress }) => {
  const webViewRef = useRef<WebView>(null);

  const htmlContent = `
    <!DOCTYPE html>
    <html>
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
        <style>
          body { margin: 0; padding: 16px; background: white; }
          .mermaid { width: 100%; height: 100%; }
          .clickable { cursor: pointer; }
        </style>
      </head>
      <body>
        <div class="mermaid">${chart}</div>
        <script>
          mermaid.initialize({
            startOnLoad: true,
            theme: 'neutral',
            flowchart: { useMaxWidth: false }
          });
          
          // 处理点击事件
          document.addEventListener('click', function(e) {
            if (e.target.closest('.clickable')) {
              const link = e.target.closest('.clickable').getAttribute('data-link');
              if (link) {
                window.ReactNativeWebView.postMessage(JSON.stringify({
                  type: 'linkClick',
                  url: link
                }));
              }
            }
          });
        </script>
      </body>
    </html>
  `;

  const handleWebViewMessage = (event: any) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);
      if (data.type === 'linkClick' && onLinkPress) {
        onLinkPress(data.url);
      }
    } catch (error) {
      console.error('解析消息失败', error);
    }
  };

  return (
    <View style={styles.container}>
      <WebView
        ref={webViewRef}
        source={{ html: htmlContent }}
        style={styles.webview}
        onMessage={handleWebViewMessage}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        startInLoadingState={true}
        scalesPageToFit={true}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    height: Dimensions.get('window').height * 0.7,
  },
  webview: {
    flex: 1,
  },
});

export default MermaidDiagram;

核心功能实现

仓库输入界面

// src/screens/RepositoryInputScreen.tsx
import React, { useState } from 'react';
import {
  View,
  TextInput,
  TouchableOpacity,
  Text,
  StyleSheet,
  Alert,
  ScrollView,
} from 'react-native';
import { gitDiagramService } from '../services/gitdiagramApi';

const RepositoryInputScreen: React.FC = ({ navigation }) => {
  const [repoUrl, setRepoUrl] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const parseGitHubUrl = (url: string) => {
    const pattern = /github\.com\/([^\/]+)\/([^\/]+)/;
    const match = url.match(pattern);
    return match ? { username: match[1], repo: match[2] } : null;
  };

  const handleGenerate = async () => {
    const repoInfo = parseGitHubUrl(repoUrl);
    if (!repoInfo) {
      Alert.alert('错误', '请输入有效的GitHub仓库URL');
      return;
    }

    setIsLoading(true);
    try {
      // 先估算成本
      const cost = await gitDiagramService.estimateCost(repoInfo);
      
      Alert.alert(
        '成本估算',
        `生成此图表预计花费 ${cost.cost}`,
        [
          { text: '取消', style: 'cancel' },
          { 
            text: '继续生成', 
            onPress: () => generateDiagram(repoInfo) 
          }
        ]
      );
    } catch (error) {
      Alert.alert('错误', '估算成本失败');
    } finally {
      setIsLoading(false);
    }
  };

  const generateDiagram = async (repoInfo: any) => {
    setIsLoading(true);
    try {
      const response = await gitDiagramService.generateDiagram(repoInfo);
      
      if (response.error) {
        Alert.alert('生成失败', response.error);
        return;
      }

      navigation.navigate('DiagramView', {
        diagram: response.diagram,
        explanation: response.explanation,
        repoInfo,
      });
    } catch (error) {
      Alert.alert('错误', '生成图表时发生错误');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          placeholder="https://github.com/username/repository"
          value={repoUrl}
          onChangeText={setRepoUrl}
          autoCapitalize="none"
          autoCorrect={false}
          keyboardType="url"
        />
        
        <TouchableOpacity
          style={[styles.button, isLoading && styles.buttonDisabled]}
          onPress={handleGenerate}
          disabled={isLoading}
        >
          <Text style={styles.buttonText}>
            {isLoading ? '生成中...' : '生成图表'}
          </Text>
        </TouchableOpacity>
      </View>

      <View style={styles.examples}>
        <Text style={styles.examplesTitle}>示例仓库:</Text>
        {['facebook/react', 'vercel/next.js', 'vuejs/vue'].map((repo) => (
          <TouchableOpacity
            key={repo}
            style={styles.exampleButton}
            onPress={() => setRepoUrl(`https://github.com/${repo}`)}
          >
            <Text style={styles.exampleText}>{repo}</Text>
          </TouchableOpacity>
        ))}
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  inputContainer: {
    marginBottom: 30,
  },
  input: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#ddd',
    marginBottom: 15,
    fontSize: 16,
  },
  button: {
    backgroundColor: '#6b46c1',
    padding: 15,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonDisabled: {
    backgroundColor: '#a0aec0',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  examples: {
    marginTop: 20,
  },
  examplesTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 10,
    color: '#2d3748',
  },
  exampleButton: {
    backgroundColor: 'white',
    padding: 12,
    borderRadius: 6,
    marginBottom: 8,
    borderWidth: 1,
    borderColor: '#e2e8f0',
  },
  exampleText: {
    color: '#4a5568',
    fontSize: 14,
  },
});

export default RepositoryInputScreen;

图表查看与交互

// src/screens/DiagramViewScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  Linking,
  Share,
} from 'react-native';
import MermaidDiagram from '../components/MermaidDiagram';

const DiagramViewScreen: React.FC = ({ route }) => {
  const { diagram, explanation, repoInfo } = route.params;
  const [activeTab, setActiveTab] = useState<'diagram' | 'explanation'>('diagram');

  const handleLinkPress = async (url: string) => {
    try {
      await Linking.openURL(url);
    } catch (error) {
      console.error('打开链接失败', error);
    }
  };

  const handleShare = async () => {
    try {
      await Share.share({
        message: `查看 ${repoInfo.username}/${repoInfo.repo} 的架构图`,
        url: `https://gitdiagram.com/${repoInfo.username}/${repoInfo.repo}`,
      });
    } catch (error) {
      console.error('分享失败', error);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>
          {repoInfo.username}/{repoInfo.repo}
        </Text>
        
        <TouchableOpacity onPress={handleShare} style={styles.shareButton}>
          <Text style={styles.shareText}>分享</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.tabs}>
        <TouchableOpacity
          style={[styles.tab, activeTab === 'diagram' && styles.activeTab]}
          onPress={() => setActiveTab('diagram')}
        >
          <Text style={styles.tabText}>图表</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[styles.tab, activeTab === 'explanation' && styles.activeTab]}
          onPress={() => setActiveTab('explanation')}
        >
          <Text style={styles.tabText}>说明</Text>
        </TouchableOpacity>
      </View>

      {activeTab === 'diagram' ? (
        <MermaidDiagram
          chart={diagram}
          onLinkPress={handleLinkPress}
        />
      ) : (
        <ScrollView style={styles.explanationContainer}>
          <Text style={styles.explanationText}>{explanation}</Text>
        </ScrollView>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    color: '#2d3748',
  },
  shareButton: {
    backgroundColor: '#6b46c1',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
  },
  shareText: {
    color: 'white',
    fontSize: 14,
  },
  tabs: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  tab: {
    flex: 1,
    padding: 16,
    alignItems: 'center',
  },
  activeTab: {
    borderBottomWidth: 2,
    borderBottomColor: '#6b46c1',
  },
  tabText: {
    fontSize: 16,
    color: '#4a5568',
  },
  explanationContainer: {
    flex: 1,
    padding: 16,
  },
  explanationText: {
    fontSize: 16,
    lineHeight: 24,
    color: '#2d3748',
  },
});

export default DiagramViewScreen;

性能优化策略

图表缓存机制

// src/utils/cache.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const CACHE_PREFIX = 'gitdiagram_';
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24小时

export interface CachedDiagram {
  diagram: string;
  explanation: string;
  timestamp: number;
  repoInfo: any;
}

export const cacheDiagram = async (key: string, data: CachedDiagram) => {
  try {
    const cacheData = {
      ...data,
      timestamp: Date.now(),
    };
    await AsyncStorage.setItem(
      `${CACHE_PREFIX}${key}`,
      JSON.stringify(cacheData)
    );
  } catch (error) {
    console.error('缓存图表失败', error);
  }
};

export const getCachedDiagram = async (key: string): Promise<CachedDiagram | null> => {
  try {
    const cached = await AsyncStorage.getItem(`${CACHE_PREFIX}${key}`);
    if (!cached) return null;

    const data: CachedDiagram = JSON.parse(cached);
    const isExpired = Date.now() - data.timestamp > CACHE_DURATION;

    return isExpired ? null : data;
  } catch (error) {
    console.error('获取缓存失败', error);
    return null;
  }
};

export const clearExpiredCache = async () => {
  try {
    const keys = await AsyncStorage.getAllKeys();
    const cacheKeys = keys.filter(key => key.startsWith(CACHE_PREFIX));
    
    for (const key of cacheKeys) {
      const cached = await AsyncStorage.getItem(key);
      if (cached) {
        const data: CachedDiagram = JSON.parse(cached);
        if (Date.now() - data.timestamp > CACHE_DURATION) {
          await AsyncStorage.removeItem(key);
        }
      }
    }
  } catch (error) {
    console.error('清理缓存失败', error);
  }
};

离线功能支持

// src/hooks/useOffline.ts
import { useState, useEffect } from 'react';
import NetInfo from '@react-native-community/netinfo';

export const useOffline = () => {
  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener(state => {
      setIsOffline(!state.isConnected);
    });

    return unsubscribe;
  }, []);

  return isOffline;
};

// 在组件中使用
const DiagramViewScreen: React.FC = ({ route }) => {
  const isOffline = useOffline();
  const { diagram, explanation, repoInfo } = route.params;

  if (isOffline) {
    return (
      <View style={styles.offlineContainer}>
        <Text style={styles.offlineText}>当前处于离线模式</Text>
        <Text style={styles.offlineSubtext}>可以查看已缓存的图表</Text>
      </View>
    );
  }

  // ... 正常渲染逻辑
};

部署与发布

Android发布配置

// android/app/build.gradle
android {
    defaultConfig {
        applicationId "com.gitdiagram.mobile"
        minSdkVersion 23
        targetSdkVersion 34
        versionCode 1
        versionName "1.0.0"
    }
    
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

iOS发布配置

<!-- iOS/Info.plist -->
<dict>
    <key>CFBundleDisplayName</key>
    <string>GitDiagram</string>
    <key>CFBundleIdentifier</key>
    <string>com.gitdiagram.mobile</string>
    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>github</string>
    </array>
</dict>

总结与展望

通过React Native开发GitDiagram移动应用,我们实现了:

  1. 跨平台兼容性:一套代码同时支持iOS和Android
  2. 原生性能:利用WebView渲染Mermaid图表,保持流畅体验
  3. 完整功能:支持图表生成、查看、分享和离线缓存
  4. 优秀用户体验:移动端优化的界面设计和交互流程

未来可以进一步扩展的功能:

  • 实时协作功能,多人同时查看和标注图表
  • 高级自定义选项,支持更多图表类型和样式
  • 集成GitHub通知,及时获取仓库架构变化
  • AI辅助的架构建议和优化方案

GitDiagram移动应用不仅填补了移动端代码可视化的空白,更为开发者提供了随时随地理解项目架构的强大工具。无论是代码审查、技术学习还是项目规划,都能在掌上轻松完成。

立即开始你的React Native跨平台开发之旅,将优秀的Web应用扩展到移动端,触达更广泛的用户群体!

【免费下载链接】gitdiagram Replace 'hub' with 'diagram' in any GitHub url to instantly visualize the codebase as an interactive diagram 【免费下载链接】gitdiagram 项目地址: https://gitcode.com/GitHub_Trending/gi/gitdiagram

更多推荐