【OpenHarmony】React Native of OpenHarmony实战项目:深度链接技术

在这里插入图片描述

摘要

深度链接(Universal Link)是实现应用间无缝跳转的关键技术。本文讲解在 OpenHarmony 平台上实现深度链接的完整方案,包括 Want 机制的桥接、Linking API 的使用、以及平台适配的最佳实践。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里插入图片描述

一、技术原理

1.1 深度链接概念

深度链接通过 HTTP/HTTPS URL 直接唤醒应用并跳转到指定页面,相比传统方案具有以下优势:

对比维度 Web 跳转 深度链接
用户体验 页面跳转明显 无缝原生体验
转化率 15-30% 流失 <5% 流失
安装后跳转 无法直达 精准定位内容

1.2 OpenHarmony Want 机制

┌─────────────────────────────────────────────────────┐
│                   深度链接调用链                      │
├─────────────────────────────────────────────────────┤
│  浏览器/消息 → SystemRouter → AppAbility → RN Bridge │
└─────────────────────────────────────────────────────┘

Want 是 OpenHarmony 的跨应用通信机制,携带以下关键信息:

  • uri:完整的 URL 字符串
  • entities:目标组件标识
  • parameters:附加参数字典

二、平台适配要点

2.1 权限配置

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.START_ABILITIES",
        "reason": "处理深度链接唤醒",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ],
    "abilities": [
      {
        "skills": [
          {
            "actions": ["ohos.want.action.view"],
            "uris": [
              {
                "scheme": "https",
                "host": "yourdomain.com",
                "pathPrefix": "/product"
              }
            ]
          }
        ]
      }
    ]
  }
}

2.2 生命周期处理

应用状态 处理方式 API
冷启动 getInitialURL() 获取初始链接 Linking.getInitialURL()
前台运行 事件监听接收链接 Linking.addEventListener('url')
后台挂起 Want 参数传递 onCreate(want)

三、完整实现

import React, { useState, useEffect, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  Pressable,
  ScrollView,
  Alert,
  Linking,
  Platform,
} from 'react-native';

/**
 * 链接事件类型
 */
interface LinkEvent {
  url: string;
  timestamp: number;
  type: 'incoming' | 'outgoing';
}

/**
 * 链接配置
 */
interface LinkConfig {
  scheme: string;
  host: string;
  path: string;
}

/**
 * 深度链接管理器
 */
class UniversalLinkManager {
  private static instance: UniversalLinkManager;
  private configs: LinkConfig[] = [];

  static getInstance(): UniversalLinkManager {
    if (!this.instance) {
      this.instance = new UniversalLinkManager();
    }
    return this.instance;
  }

  /**
   * 注册链接配置
   */
  registerLink(config: LinkConfig): void {
    this.configs.push(config);
  }

  /**
   * 检查 URL 是否匹配
   */
  matches(url: string): boolean {
    try {
      const parsed = new URL(url);
      return this.configs.some(
        config => config.scheme === parsed.protocol.replace(':', '') &&
                  config.host === parsed.hostname &&
                  parsed.pathname.startsWith(config.path)
      );
    } catch {
      return false;
    }
  }

  /**
   * 解析 URL 参数
   */
  parseParams(url: string): Record<string, string> {
    try {
      const parsed = new URL(url);
      const params: Record<string, string> = {};
      parsed.searchParams.forEach((value, key) => {
        params[key] = value;
      });
      return params;
    } catch {
      return {};
    }
  }
}

/**
 * 深度链接演示组件
 */
const UniversalLinkScreen: React.FC<{ onBack: () => void }> = ({ onBack }) => {
  const [currentURL, setCurrentURL] = useState('');
  const [events, setEvents] = useState<LinkEvent[]>([]);
  const linkManager = UniversalLinkManager.getInstance();

  // 初始化链接配置
  useEffect(() => {
    linkManager.registerLink({
      scheme: 'https',
      host: 'app.example.com',
      path: '/product',
    });
  }, []);

  // 监听深度链接
  useEffect(() => {
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleIncomingLink(url);
    });

    // 处理冷启动链接
    Linking.getInitialURL().then(url => {
      if (url) {
        handleIncomingLink(url);
      }
    });

    return () => subscription.remove();
  }, []);

  /**
   * 处理接收到的链接
   */
  const handleIncomingLink = useCallback((url: string) => {
    if (!linkManager.matches(url)) {
      console.warn('URL 不匹配已注册的深度链接配置');
      return;
    }

    const params = linkManager.parseParams(url);

    setEvents(prev => [{
      url,
      timestamp: Date.now(),
      type: 'incoming',
    }, ...prev.slice(0, 9)]);

    setCurrentURL(url);

    // 根据路径跳转到对应页面
    if (url.includes('/product/')) {
      Alert.alert('深度链接', `跳转到产品页\nURL: ${url}\n参数: ${JSON.stringify(params)}`);
    }
  }, []);

  /**
   * 打开外部链接
   */
  const openExternalURL = useCallback(async (url: string) => {
    const supported = await Linking.canOpenURL(url);
    if (supported) {
      await Linking.openURL(url);
      setEvents(prev => [{
        url,
        timestamp: Date.now(),
        type: 'outgoing',
      }, ...prev.slice(0, 9)]);
    } else {
      Alert.alert('不支持', `无法打开此链接: ${url}`);
    }
  }, []);

  /**
   * 测试链接处理
   */
  const testLink = useCallback((url: string) => {
    handleIncomingLink(url);
  }, [handleIncomingLink]);

  return (
    <View style={styles.container}>
      {/* 导航栏 */}
      <View style={styles.navbar}>
        <Pressable onPress={onBack} style={styles.navButton}>
          <Text style={styles.navButtonText}></Text>
        </Pressable>
        <Text style={styles.navTitle}>深度链接</Text>
        <View style={styles.navSpacer} />
      </View>

      <ScrollView style={styles.content}>
        {/* 当前链接 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>当前接收</Text>
          <View style={styles.urlBox}>
            <Text style={styles.urlText}>
              {currentURL || '等待深度链接唤醒...'}
            </Text>
          </View>
        </View>

        {/* 快速测试 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>快速测试</Text>
          <View style={styles.testButtons}>
            <Pressable
              style={styles.testButton}
              onPress={() => testLink('https://app.example.com/product/123')}
            >
              <Text style={styles.testButtonText}>产品页</Text>
            </Pressable>
            <Pressable
              style={styles.testButton}
              onPress={() => testLink('https://app.example.com/user/profile')}
            >
              <Text style={styles.testButtonText}>用户页</Text>
            </Pressable>
          </View>
        </View>

        {/* 事件日志 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>事件日志</Text>
          {events.length === 0 ? (
            <Text style={styles.emptyText}>暂无事件记录</Text>
          ) : (
            events.map((event, index) => (
              <View key={index} style={styles.eventItem}>
                <Text style={styles.eventIcon}>
                  {event.type === 'incoming' ? '📥' : '📤'}
                </Text>
                <View style={styles.eventContent}>
                  <Text style={styles.eventTime}>
                    {new Date(event.timestamp).toLocaleTimeString()}
                  </Text>
                  <Text style={styles.eventUrl} numberOfLines={1}>
                    {event.url}
                  </Text>
                </View>
              </View>
            ))
          )}
        </View>

        {/* 配置说明 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>配置要点</Text>
          <View style={styles.configList}>
            <View style={styles.configItem}>
              <Text style={styles.configIcon}>🔗</Text>
              <Text style={styles.configText}>
                module.json5 中配置 skills 匹配 URI 规则
              </Text>
            </View>
            <View style={styles.configItem}>
              <Text style={styles.configIcon}>📱</Text>
              <Text style={styles.configText}>
                使用 Linking.addEventListener 监听前台链接
              </Text>
            </View>
            <View style={styles.configItem}>
              <Text style={styles.configIcon}>🚀</Text>
              <Text style={styles.configText}>
                冷启动使用 getInitialURL 获取初始链接
              </Text>
            </View>
          </View>
        </View>
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  navbar: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    backgroundColor: '#1890ff',
  },
  navButton: {
    width: 40,
    height: 40,
    justifyContent: 'center',
    alignItems: 'center',
  },
  navButtonText: {
    fontSize: 18,
    color: '#fff',
    fontWeight: '600',
  },
  navTitle: {
    flex: 1,
    fontSize: 18,
    fontWeight: 'bold',
    color: '#fff',
    textAlign: 'center',
  },
  navSpacer: {
    width: 40,
  },
  content: {
    flex: 1,
    padding: 16,
  },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 12,
  },
  urlBox: {
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    padding: 12,
  },
  urlText: {
    fontSize: 13,
    color: '#666',
    fontFamily: 'monospace',
  },
  testButtons: {
    flexDirection: 'row',
    gap: 12,
  },
  testButton: {
    flex: 1,
    backgroundColor: '#1890ff',
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  testButtonText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#fff',
  },
  emptyText: {
    fontSize: 14,
    color: '#999',
    textAlign: 'center',
    padding: 20,
  },
  eventItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  eventIcon: {
    fontSize: 18,
    marginRight: 12,
  },
  eventContent: {
    flex: 1,
  },
  eventTime: {
    fontSize: 12,
    color: '#999',
    marginBottom: 2,
  },
  eventUrl: {
    fontSize: 13,
    color: '#333',
    fontFamily: 'monospace',
  },
  configList: {
    gap: 12,
  },
  configItem: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  configIcon: {
    fontSize: 20,
    marginRight: 12,
  },
  configText: {
    fontSize: 14,
    color: '#333',
    flex: 1,
  },
});

export default UniversalLinkScreen;

在这里插入图片描述

四、平台特定配置

4.1 性能优化

优化项 配置 效果
Want 缓存 wantCacheSize: 5 减少重复解析
路由预加载 preloadRoutes: 3 加速深层跳转
JS 预热 enableJsPreload: true 冷启动提速 40%

4.2 常见问题

问题 原因 解决方案
链接无法唤醒 URI 未声明 检查 module.json5 的 uris 配置
参数丢失 Want 解析异常 使用 want.getUri() 替代直接访问
后台唤醒失败 保活限制 配置 continuousTask 后台模式

项目源码

鸿蒙社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐