【HarmonyOS】RN_of_HarmonyOS实战项目:URL链接输入

发布时间 :2026年2月22日
技术栈 :HarmonyOS NEXT + React Native for OpenHarmony (RNOH)
适用版本:OpenHarmony 6.0.0 (API 20) + React Native 0.72.5


📋 目录

  1. 项目背景
  2. 技术原理
  3. 核心实现代码
  4. 常见问题与解决方案
  5. 完整示例
  6. 总结

一、项目背景

在现代移动应用中,URL链接输入是一个高频且关键的功能场景。无论是分享链接、配置服务器地址、输入网页跳转,还是API接口调试,都需要用户能够方便、准确地输入URL。

然而,在 HarmonyOS + React Native 的跨平台开发环境中,实现完善的URL输入功能面临着独特的挑战:

挑战类型 具体问题
格式验证 URL格式复杂,需支持http/https/ftp等多种协议
键盘优化 需要显示./-等URL常用符号的专用键盘
自动补全 用户常省略https://前缀,需要智能补全
安全过滤 需防范恶意URL、XSS攻击等安全风险
平台差异 HarmonyOS与Android/iOS的键盘行为和验证机制存在差异
Unicode支持 国际化域名(IDN)包含非ASCII字符,需特殊处理

本文将提供一个生产级的完整解决方案,帮助开发者在HarmonyOS平台上实现流畅、安全的URL输入体验。


二、技术原理

2.1 URL格式标准与验证规则

URL(Uniform Resource Locator)遵循 RFC 3986 标准,基本结构如下:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        URL 结构解析                              │
├─────────────────────────────────────────────────────────────────┤
│  https://user:pass@www.example.com:8080/path?query=value#hash   │
│    │      │    │        │         │    │           │            │
│    │      │    │        │         │    │           └─ 片段标识   │
│    │      │    │        │         │    └───────────── 查询参数   │
│    │      │    │        │         └────────────────── 路径       │
│    │      │    │        └─────────────────────────── 端口        │
│    │      │    └──────────────────────────────────── 主机名      │
│    │      └───────────────────────────────────────── 用户信息    │
│    └─────────────────────────────────────────────────── 协议     │
└─────────────────────────────────────────────────────────────────┘

2.2 正则表达式验证方案

javascript 复制代码
// URL验证正则表达式(兼容HarmonyOS JavaScriptCore)
const URL_REGEX = /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;

2.3 解决方案概述

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    URL输入处理流程                           │
├─────────────────────────────────────────────────────────────┤
│  用户输入 → 协议补全 → 格式验证 → 安全过滤 → 标准化输出      │
│      ↓          ↓          ↓          ↓          ↓          │
│   TextInput   https://  正则匹配  恶意检测   统一格式       │
└─────────────────────────────────────────────────────────────┘

核心策略

  1. 使用keyboardType="url"优化键盘布局
  2. 实现智能协议补全(自动添加https://
  3. 多层验证机制(格式 + 安全 + 业务规则)
  4. 适配HarmonyOS系统键盘行为
  5. 提供实时验证反馈

三、核心实现代码

3.1 URL输入组件实现

jsx 复制代码
// components/URLTextInput.jsx
import React, { useState, useRef, useEffect } from 'react';
import {
  View,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Text,
  Platform,
  ActivityIndicator,
} from 'react-native';

/**
 * 支持URL输入的TextInput组件
 * @param {Object} props - 组件属性
 * @param {Function} props.onUrlValid - URL验证通过回调
 * @param {Function} props.onUrlInvalid - URL验证失败回调
 * @param {boolean} props.autoComplete - 是否自动补全协议
 */
const URLTextInput = ({
  onUrlValid,
  onUrlInvalid,
  autoComplete = true,
  placeholder = '请输入网址...',
  ...restProps
}) => {
  const [url, setUrl] = useState('');
  const [isValid, setIsValid] = useState(null);
  const [errorMessage, setErrorMessage] = useState('');
  const [isChecking, setIsChecking] = useState(false);
  const inputRef = useRef(null);

  /**
   * URL验证正则表达式(兼容HarmonyOS)
   */
  const validateUrl = (inputUrl) => {
    if (!inputUrl || inputUrl.trim() === '') {
      return { valid: false, error: 'URL不能为空' };
    }

    // 移除首尾空格
    const trimmedUrl = inputUrl.trim();

    // 协议验证
    const protocolRegex = /^(https?|ftp):\/\//i;
    if (!protocolRegex.test(trimmedUrl)) {
      return { valid: false, error: 'URL必须包含协议 (http/https/ftp)' };
    }

    // 完整URL验证(兼容HarmonyOS JavaScriptCore)
    const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/i;
    
    if (!urlRegex.test(trimmedUrl)) {
      return { valid: false, error: 'URL格式不正确' };
    }

    // 安全检查
    const securityCheck = performSecurityCheck(trimmedUrl);
    if (!securityCheck.safe) {
      return { valid: false, error: securityCheck.reason };
    }

    return { valid: true, error: '' };
  };

  /**
   * 安全检查
   */
  const performSecurityCheck = (inputUrl) => {
    try {
      const urlObj = new URL(inputUrl);

      // 检查是否为内网地址
      const hostname = urlObj.hostname;
      const internalPatterns = [
        /^127\./,
        /^10\./,
        /^172\.(1[6-9]|2\d|3[0-1])\./,
        /^192\.168\./,
        /^localhost$/i,
        /^0\.0\.0\.0$/,
      ];

      for (const pattern of internalPatterns) {
        if (pattern.test(hostname)) {
          return { safe: false, reason: '不允许访问内网地址' };
        }
      }

      // 检查危险协议
      const dangerousProtocols = ['javascript:', 'data:', 'vbscript:'];
      if (dangerousProtocols.some(p => inputUrl.toLowerCase().startsWith(p))) {
        return { safe: false, reason: '不允许使用危险协议' };
      }

      return { safe: true, reason: '' };
    } catch (e) {
      return { safe: false, reason: 'URL解析失败' };
    }
  };

  /**
   * 自动补全协议
   */
  const autoCompleteProtocol = (inputText) => {
    if (!autoComplete) return inputText;

    const trimmed = inputText.trim();
    
    // 已有协议,直接返回
    if (/^https?:\/\//i.test(trimmed) || /^ftp:\/\//i.test(trimmed)) {
      return trimmed;
    }

    // 自动添加https://
    return `https://${trimmed}`;
  };

  /**
   * 处理文本变化
   */
  const handleTextChange = (inputText) => {
    setUrl(inputText);
    setIsValid(null);
    setErrorMessage('');

    // 防抖验证
    if (inputText.trim().length > 0) {
      setIsChecking(true);
      setTimeout(() => {
        const completedUrl = autoCompleteProtocol(inputText);
        const validation = validateUrl(completedUrl);
        
        setIsValid(validation.valid);
        setErrorMessage(validation.error);
        setIsChecking(false);

        if (validation.valid) {
          onUrlValid?.(completedUrl);
        } else {
          onUrlInvalid?.(validation.error);
        }
      }, 300);
    }
  };

  /**
   * 获取标准化URL
   */
  const getNormalizedUrl = () => {
    return autoCompleteProtocol(url);
  };

  /**
   * 一键清除
   */
  const handleClear = () => {
    setUrl('');
    setIsValid(null);
    setErrorMessage('');
    inputRef.current?.clear();
  };

  /**
   * 一键粘贴
   */
  const handlePaste = async () => {
    try {
      const clipboard = await Clipboard.getString();
      if (clipboard) {
        handleTextChange(clipboard);
      }
    } catch (e) {
      console.warn('读取剪贴板失败', e);
    }
  };

  return (
    <View style={styles.container}>
      <View style={[styles.inputWrapper, isValid === false && styles.inputError]}>
        <TextInput
          ref={inputRef}
          style={styles.textInput}
          value={url}
          onChangeText={handleTextChange}
          placeholder={placeholder}
          placeholderTextColor="#999"
          keyboardType="url"
          autoCapitalize="none"
          autoCorrect={false}
          spellCheck={false}
          {...restProps}
        />

        {/* 加载状态 */}
        {isChecking && (
          <ActivityIndicator size="small" color="#666" style={styles.loading} />
        )}

        {/* 清除按钮 */}
        {url.length > 0 && (
          <TouchableOpacity
            style={styles.clearButton}
            onPress={handleClear}
            activeOpacity={0.7}
          >
            <Text style={styles.clearButtonText}>✕</Text>
          </TouchableOpacity>
        )}
      </View>

      {/* 验证状态提示 */}
      {isValid !== null && (
        <View style={[
          styles.statusContainer,
          isValid ? styles.statusSuccess : styles.statusError
        ]}>
          <Text style={[
            styles.statusText,
            isValid ? styles.textSuccess : styles.textError
          ]}>
            {isValid ? '✓ URL格式正确' : `✗ ${errorMessage}`}
          </Text>
        </View>
      )}

      {/* 快捷协议按钮 */}
      <View style={styles.protocolButtons}>
        <TouchableOpacity
          style={styles.protocolButton}
          onPress={() => handleTextChange('https://' + url.replace(/^https?:\/\//i, ''))}
        >
          <Text style={styles.protocolButtonText}>https://</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.protocolButton}
          onPress={() => handleTextChange('http://' + url.replace(/^https?:\/\//i, ''))}
        >
          <Text style={styles.protocolButtonText}>http://</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
    padding: 10,
  },
  inputWrapper: {
    flexDirection: 'row',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    backgroundColor: '#fff',
    paddingHorizontal: 12,
    paddingVertical: 8,
  },
  inputError: {
    borderColor: '#ff4444',
    backgroundColor: '#fff5f5',
  },
  textInput: {
    flex: 1,
    fontSize: 16,
    color: '#333',
    minHeight: 44,
  },
  loading: {
    marginLeft: 8,
  },
  clearButton: {
    padding: 5,
    marginLeft: 8,
  },
  clearButtonText: {
    fontSize: 18,
    color: '#999',
  },
  statusContainer: {
    marginTop: 6,
    paddingHorizontal: 10,
    paddingVertical: 4,
    borderRadius: 4,
  },
  statusSuccess: {
    backgroundColor: '#e8f5e9',
  },
  statusError: {
    backgroundColor: '#ffebee',
  },
  statusText: {
    fontSize: 13,
  },
  textSuccess: {
    color: '#2e7d32',
  },
  textError: {
    color: '#c62828',
  },
  protocolButtons: {
    flexDirection: 'row',
    marginTop: 8,
    gap: 8,
  },
  protocolButton: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    backgroundColor: '#f5f5f5',
    borderRadius: 4,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  protocolButtonText: {
    fontSize: 13,
    color: '#666',
  },
});

export default URLTextInput;

3.2 HarmonyOS平台适配层

javascript 复制代码
// utils/harmonyosUrlAdapter.js
import { Platform } from 'react-native';

/**
 * HarmonyOS平台特定的URL处理
 */
export const HarmonyOSUrlAdapter = {
  /**
   * 检测是否为HarmonyOS平台
   */
  isHarmonyOS: () => {
    return Platform.OS === 'harmony' || Platform.OS === 'ohos';
  },

  /**
   * 获取平台特定的键盘类型
   */
  getKeyboardType: () => {
    if (HarmonyOSUrlAdapter.isHarmonyOS()) {
      // HarmonyOS URL键盘配置
      return 'url';
    }
    return 'url';
  },

  /**
   * HarmonyOS特定的URL验证
   */
  validateUrl: (url) => {
    if (!HarmonyOSUrlAdapter.isHarmonyOS()) {
      return { valid: true, error: '' };
    }

    // HarmonyOS安全策略检查
    const harmonyRestrictedDomains = [
      '127.0.0.1',
      'localhost',
      '0.0.0.0',
    ];

    try {
      const urlObj = new URL(url);
      if (harmonyRestrictedDomains.includes(urlObj.hostname)) {
        return { 
          valid: false, 
          error: 'HarmonyOS安全策略:不允许访问本地地址' 
        };
      }
    } catch (e) {
      return { valid: false, error: 'URL格式错误' };
    }

    return { valid: true, error: '' };
  },

  /**
   * 处理HarmonyOS网络权限
   */
  checkNetworkPermission: async () => {
    if (!HarmonyOSUrlAdapter.isHarmonyOS()) {
      return true;
    }

    // HarmonyOS网络权限检查
    // 需要在module.json5中配置ohos.permission.INTERNET
    return true;
  },

  /**
   * 标准化URL输出
   */
  normalizeUrl: (url) => {
    try {
      const urlObj = new URL(url);
      return urlObj.href;
    } catch (e) {
      return url;
    }
  },
};

export default HarmonyOSUrlAdapter;

3.3 URL验证工具类

javascript 复制代码
// utils/urlValidator.js

/**
 * URL验证工具类
 */
export class UrlValidator {
  /**
   * 支持的协议列表
   */
  static ALLOWED_PROTOCOLS = ['http', 'https', 'ftp'];

  /**
   * 最大URL长度
   */
  static MAX_URL_LENGTH = 2048;

  /**
   * 危险域名黑名单
   */
  static BLACKLISTED_DOMAINS = [
    'malware.com',
    'phishing.example',
    // 可根据业务需求扩展
  ];

  /**
   * 验证URL格式
   */
  static validateFormat(url) {
    if (!url || typeof url !== 'string') {
      return { valid: false, error: 'URL不能为空' };
    }

    if (url.length > this.MAX_URL_LENGTH) {
      return { valid: false, error: `URL长度不能超过${this.MAX_URL_LENGTH}字符` };
    }

    // 协议检查
    const protocolRegex = /^(https?|ftp):\/\//i;
    if (!protocolRegex.test(url)) {
      return { valid: false, error: 'URL必须包含有效协议 (http/https/ftp)' };
    }

    // 基本格式检查(兼容HarmonyOS)
    const basicRegex = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
    if (!basicRegex.test(url)) {
      return { valid: false, error: 'URL格式不正确' };
    }

    return { valid: true, error: '' };
  }

  /**
   * 验证URL安全性
   */
  static validateSecurity(url) {
    try {
      const urlObj = new URL(url);

      // 协议安全检查
      if (!this.ALLOWED_PROTOCOLS.includes(urlObj.protocol.replace(':', '').toLowerCase())) {
        return { safe: false, reason: '不支持的协议类型' };
      }

      // 域名黑名单检查
      const domain = urlObj.hostname.toLowerCase();
      if (this.BLACKLISTED_DOMAINS.some(d => domain.includes(d))) {
        return { safe: false, reason: '域名在黑名单中' };
      }

      // 内网地址检查
      const internalPatterns = [
        /^127\./,
        /^10\./,
        /^172\.(1[6-9]|2\d|3[0-1])\./,
        /^192\.168\./,
        /^localhost$/i,
        /^0\.0\.0\.0$/,
        /^::1$/,
        /^fe80:/i,
      ];

      for (const pattern of internalPatterns) {
        if (pattern.test(urlObj.hostname)) {
          return { safe: false, reason: '不允许访问内网地址' };
        }
      }

      // 危险协议检查
      const dangerousPatterns = ['javascript:', 'data:', 'vbscript:', 'file:'];
      for (const pattern of dangerousPatterns) {
        if (url.toLowerCase().includes(pattern)) {
          return { safe: false, reason: '包含危险协议' };
        }
      }

      return { safe: true, reason: '' };
    } catch (e) {
      return { safe: false, reason: 'URL解析失败' };
    }
  }

  /**
   * 完整验证
   */
  static validate(url, options = {}) {
    const {
      checkSecurity = true,
      allowedProtocols = this.ALLOWED_PROTOCOLS,
    } = options;

    const errors = [];

    // 格式验证
    const formatResult = this.validateFormat(url);
    if (!formatResult.valid) {
      errors.push(formatResult.error);
    }

    // 安全验证
    if (checkSecurity) {
      const securityResult = this.validateSecurity(url);
      if (!securityResult.safe) {
        errors.push(securityResult.reason);
      }
    }

    return {
      isValid: errors.length === 0,
      errors,
      normalizedUrl: this.normalize(url),
    };
  }

  /**
   * 标准化URL
   */
  static normalize(url) {
    try {
      const urlObj = new URL(url);
      return urlObj.href;
    } catch (e) {
      return url;
    }
  }

  /**
   * 提取域名
   */
  static extractDomain(url) {
    try {
      const urlObj = new URL(url);
      return urlObj.hostname;
    } catch (e) {
      return '';
    }
  }

  /**
   * 提取协议
   */
  static extractProtocol(url) {
    try {
      const urlObj = new URL(url);
      return urlObj.protocol.replace(':', '');
    } catch (e) {
      return '';
    }
  }
}

export default UrlValidator;

四、常见问题与解决方案

4.1 问题:URL键盘在HarmonyOS上显示异常

现象keyboardType="url"在HarmonyOS上没有显示预期的./符号。

解决方案

jsx 复制代码
// 平台特定的键盘配置
const getOptimizedKeyboardType = () => {
  if (Platform.OS === 'harmony' || Platform.OS === 'ohos') {
    // HarmonyOS使用default + 自定义输入过滤
    return 'default';
  }
  return 'url';
};

// 配合inputFilter使用
<TextInput
  keyboardType={getOptimizedKeyboardType()}
  inputFilter={{
    filter: (text) => {
      // 允许URL常用字符
      const urlChars = /^[a-zA-Z0-9\-_.~:/?#$$$$@!$&'()*+,;=%]*$/;
      return urlChars.test(text);
    },
  }}
/>

4.2 问题:自动补全导致重复协议

现象 :用户输入https://example.com,自动补全后变成https://https://example.com

解决方案

javascript 复制代码
const autoCompleteProtocol = (inputText) => {
  const trimmed = inputText.trim();
  
  // 检查是否已有协议
  const hasProtocol = /^(https?|ftp):\/\//i.test(trimmed);
  
  if (hasProtocol) {
    return trimmed;
  }
  
  // 检查是否以www开头
  if (/^www\./i.test(trimmed)) {
    return `https://${trimmed}`;
  }
  
  // 检查是否包含域名特征
  if (/\.[a-z]{2,}/i.test(trimmed) && !trimmed.includes('://')) {
    return `https://${trimmed}`;
  }
  
  return trimmed;
};

4.3 问题:国际化域名(IDN)验证失败

现象:包含中文或其他非ASCII字符的域名验证失败。

解决方案

javascript 复制代码
// 支持国际化域名的验证
const validateIdnUrl = (url) => {
  try {
    // 使用URL API自动处理IDN
    const urlObj = new URL(url);
    
    // Punycode转换检查
    const asciiDomain = urlObj.hostname;
    
    // 允许Unicode字符
    const idnRegex = /^([a-z0-9\u00a1-\uffff-]+\.)+[a-z\u00a1-\uffff]{2,}$/i;
    
    return idnRegex.test(asciiDomain) || urlObj.hostname.length > 0;
  } catch (e) {
    return false;
  }
};

4.4 问题:HarmonyOS网络权限配置

现象:URL验证通过但无法访问,提示网络权限不足。

解决方案

json 复制代码
// module.json5 配置
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:network_access_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

4.5 问题:长URL输入性能问题

现象:输入长URL时组件响应变慢。

解决方案

jsx 复制代码
// 使用防抖优化验证
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
};

// 使用示例
const debouncedUrl = useDebounce(url, 500);

useEffect(() => {
  if (debouncedUrl) {
    const validation = UrlValidator.validate(debouncedUrl);
    setIsValid(validation.isValid);
  }
}, [debouncedUrl]);

五、完整示例

5.1 链接分享页面完整实现

jsx 复制代码
// screens/ShareLinkScreen.jsx
import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  SafeAreaView,
  TouchableOpacity,
  ScrollView,
  Alert,
  Platform,
} from 'react-native';
import URLTextInput from '../components/URLTextInput';
import UrlValidator from '../utils/urlValidator';
import HarmonyOSUrlAdapter from '../utils/harmonyosUrlAdapter';

const ShareLinkScreen = ({ navigation }) => {
  const [shareUrl, setShareUrl] = useState('');
  const [isValid, setIsValid] = useState(false);
  const [urlHistory, setUrlHistory] = useState([]);

  const handleUrlValid = (url) => {
    setShareUrl(url);
    setIsValid(true);
  };

  const handleUrlInvalid = (error) => {
    setIsValid(false);
    console.warn('URL验证失败:', error);
  };

  const handleShare = async () => {
    if (!isValid) {
      Alert.alert('提示', '请输入有效的URL');
      return;
    }

    // HarmonyOS平台特定处理
    if (HarmonyOSUrlAdapter.isHarmonyOS()) {
      const permissionCheck = await HarmonyOSUrlAdapter.checkNetworkPermission();
      if (!permissionCheck) {
        Alert.alert('提示', '需要网络权限才能分享链接');
        return;
      }
    }

    // 添加到历史记录
    const newHistory = [
      { url: shareUrl, time: new Date().toISOString() },
      ...urlHistory.slice(0, 9),
    ];
    setUrlHistory(newHistory);

    // 执行分享操作
    Alert.alert('分享成功', `已复制链接:${shareUrl}`);
  };

  const handleQuickLink = (link) => {
    setShareUrl(link);
    const validation = UrlValidator.validate(link);
    setIsValid(validation.isValid);
  };

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        <View style={styles.content}>
          <Text style={styles.title}>分享链接</Text>
          <Text style={styles.subtitle}>请输入要分享的网址</Text>

          {/* URL输入组件 */}
          <URLTextInput
            onUrlValid={handleUrlValid}
            onUrlInvalid={handleUrlInvalid}
            autoComplete={true}
            placeholder="https://example.com"
          />

          {/* 快捷链接 */}
          <View style={styles.quickLinks}>
            <Text style={styles.sectionTitle}>常用链接</Text>
            <View style={styles.quickLinkButtons}>
              <TouchableOpacity
                style={styles.quickLinkButton}
                onPress={() => handleQuickLink('https://developer.huawei.com')}
              >
                <Text style={styles.quickLinkText}>华为开发者</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.quickLinkButton}
                onPress={() => handleQuickLink('https://gitee.com')}
              >
                <Text style={styles.quickLinkText}>Gitee</Text>
              </TouchableOpacity>
            </View>
          </View>

          {/* 分享按钮 */}
          <TouchableOpacity
            style={[
              styles.shareButton,
              !isValid && styles.shareButtonDisabled,
            ]}
            onPress={handleShare}
            disabled={!isValid}
          >
            <Text style={styles.shareButtonText}>
              {isValid ? '分享链接' : '请输入有效URL'}
            </Text>
          </TouchableOpacity>

          {/* 历史记录 */}
          {urlHistory.length > 0 && (
            <View style={styles.historySection}>
              <Text style={styles.sectionTitle}>最近分享</Text>
              {urlHistory.map((item, index) => (
                <View key={index} style={styles.historyItem}>
                  <Text style={styles.historyUrl} numberOfLines={1}>
                    {item.url}
                  </Text>
                  <Text style={styles.historyTime}>
                    {new Date(item.time).toLocaleString()}
                  </Text>
                </View>
              ))}
            </View>
          )}
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  scrollView: {
    flex: 1,
  },
  content: {
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
    marginBottom: 20,
  },
  quickLinks: {
    marginTop: 20,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 12,
  },
  quickLinkButtons: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 10,
  },
  quickLinkButton: {
    paddingHorizontal: 16,
    paddingVertical: 10,
    backgroundColor: '#fff',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  quickLinkText: {
    fontSize: 14,
    color: '#007AFF',
  },
  shareButton: {
    marginTop: 30,
    backgroundColor: '#007AFF',
    paddingVertical: 16,
    borderRadius: 12,
    alignItems: 'center',
  },
  shareButtonDisabled: {
    backgroundColor: '#ccc',
  },
  shareButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#fff',
  },
  historySection: {
    marginTop: 30,
    borderTopWidth: 1,
    borderTopColor: '#ddd',
    paddingTop: 20,
  },
  historyItem: {
    backgroundColor: '#fff',
    padding: 12,
    borderRadius: 8,
    marginBottom: 10,
  },
  historyUrl: {
    fontSize: 14,
    color: '#333',
  },
  historyTime: {
    fontSize: 12,
    color: '#999',
    marginTop: 4,
  },
});

export default ShareLinkScreen;

5.2 项目依赖配置

json 复制代码
// package.json
{
  "name": "harmonyos-url-input-demo",
  "version": "1.0.0",
  "dependencies": {
    "react-native": "0.72.5",
    "@react-native-oh/react-native-harmony": "0.72.108",
    "@react-native-clipboard/clipboard": "^1.11.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "@babel/runtime": "^7.20.0"
  }
}

5.3 HarmonyOS工程配置

json5 复制代码
// harmony/entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:network_access_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

六、总结

✅ 核心要点回顾

技术点 解决方案
URL格式验证 多层正则 + URL API验证
协议自动补全 智能检测 + 一键切换
安全检查 内网拦截 + 黑名单 + 危险协议过滤
平台适配 HarmonyOS特定键盘 + 权限配置
用户体验 实时验证反馈 + 快捷操作

🔒 安全建议

  1. 服务端二次验证:客户端验证仅为用户体验,服务端必须重新验证
  2. HTTPS优先:默认使用HTTPS协议,强制安全连接
  3. 域名白名单:敏感操作应限制在可信域名范围内
  4. 日志记录:记录所有URL访问行为,便于审计

📚 推荐资源

  1. HarmonyOS TextInput官方文档
  2. React Native for OpenHarmony
  3. RFC 3986 - URI标准
  4. OWASP URL验证安全指南

🎯 后续优化方向

  1. URL预览:输入完成后显示网页缩略图预览
  2. 二维码生成:将URL转换为二维码分享
  3. 短链接服务:集成短链接生成API
  4. 深度链接:支持App内深度链接跳转

💡 提示:本文代码已在OpenHarmony 6.0.0 + React Native 0.72.5环境测试通过。如遇兼容性问题,请根据实际版本调整。
📢 欢迎交流:如有问题或建议,欢迎在评论区留言讨论!


本文同步发表于 HarmonyOS开发者社区、CSDN、掘金等技术平台

© 2026 技术探索者 | 转载请注明出处

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

相关推荐
FrameNotWork1 小时前
HarmonyOS 实现仿抖音上下滑动照片浏览(弹簧阻尼动画详解)
华为·harmonyos
●VON6 小时前
HarmonyOS应用开发实战(基础篇)Day09-《构建布局详解下》
华为·harmonyos·训练营·von
●VON6 小时前
HarmonyOS应用开发实战(基础篇)Day08-《构建布局详解上》
华为·harmonyos·鸿蒙·von
键盘鼓手苏苏19 小时前
Flutter for OpenHarmony:csslib 强力 CSS 样式解析器,构建自定义渲染引擎的基石(Dart 官方解析库) 深度解析与鸿蒙适配指南
css·flutter·harmonyos
阿林来了1 天前
Flutter三方库适配OpenHarmony【flutter_speech】— 持续语音识别与长录音
flutter·语音识别·harmonyos
松叶似针1 天前
Flutter三方库适配OpenHarmony【secure_application】— 与 HarmonyOS 安全能力的深度集成
安全·flutter·harmonyos
星空22231 天前
【HarmonyOS】day39:React Native实战项目+智能文本省略Hook开发
react native·华为·harmonyos
星空22231 天前
【HarmonyOS】day40:React Native实战项目+自定义Hooks开发指南
react native·华为·harmonyos
Swift社区2 天前
鸿蒙 PC 架构的终点:工作流
华为·harmonyos