【HarmonyOS】RN of HarmonyOS实战项目:URL链接输入与验证

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

- [【HarmonyOS】RN of HarmonyOS实战项目:URL链接输入与验证](#【HarmonyOS】RN of HarmonyOS实战项目:URL链接输入与验证)
项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现智能URL链接输入组件,涵盖URL格式验证、自动补全、协议检测、链接预览等完整功能,提供从基础实现到高级优化的生产级解决方案。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
一、项目背景
在现代移动应用中,URL链接输入是常见的需求场景,如网页分享、链接收藏、短链接生成等。在HarmonyOS平台上实现完善的URL输入功能需要考虑:
- 格式验证:支持多种URL协议(http、https、ftp等)
- 智能补全:根据历史记录和热门链接提供自动补全
- 实时检测:输入时实时验证URL有效性
- 链接预览:获取链接标题、描述等信息
- 用户体验:友好的错误提示和交互反馈
二、技术架构
2.1 URL验证流程
无效
有效
缺少协议
有协议
无效域名
有效域名
连接成功
连接失败
用户输入URL
格式检查
显示格式错误
协议检查
自动添加https://
域名验证
显示域名错误
尝试连接
获取链接信息
显示警告提示
显示预览卡片
2.2 URL数据结构
typescript
/**
* URL解析结果
*/
interface ParsedURL {
original: string; // 原始输入
normalized: string; // 标准化后的URL
protocol: string; // 协议(http/https/ftp等)
hostname: string; // 主机名
path?: string; // 路径
query?: Record<string, string>; // 查询参数
hash?: string; // 锚点
isValid: boolean; // 是否有效
error?: string; // 错误信息
}
/**
* 链接预览信息
*/
interface LinkPreview {
url: string;
title?: string;
description?: string;
image?: string;
favicon?: string;
siteName?: string;
}
三、核心实现代码
3.1 URL验证工具类
typescript
/**
* URL验证工具类
* 提供完整的URL格式验证和处理功能
*
* @platform HarmonyOS 2.0+
* @react-native 0.72+
*/
export class URLValidator {
/**
* URL协议白名单
*/
private static readonly ALLOWED_PROTOCOLS = [
'http:',
'https:',
'ftp:',
'ftps:',
'mailto:',
'tel:',
];
/**
* 常见域名后缀
*/
private static readonly COMMON_TLDS = [
'com', 'org', 'net', 'edu', 'gov', 'mil',
'io', 'co', 'ai', 'app', 'dev', 'tech',
'cn', 'com.cn', 'net.cn', 'org.cn',
'jp', 'kr', 'uk', 'de', 'fr', 'au',
];
/**
* 验证URL格式
*/
static validate(input: string): { isValid: boolean; error?: string; normalized?: string } {
// 空字符串检查
if (!input || !input.trim()) {
return { isValid: false, error: 'URL不能为空' };
}
let url = input.trim();
// 自动添加协议
if (!this.hasProtocol(url)) {
url = 'https://' + url;
}
try {
const parsed = new URL(url);
// 验证协议
if (!this.ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
return {
isValid: false,
error: `不支持的协议: ${parsed.protocol}`,
};
}
// 验证主机名
if (!parsed.hostname) {
return { isValid: false, error: '缺少主机名' };
}
// 验证域名格式
if (!this.isValidHostname(parsed.hostname)) {
return { isValid: false, error: '无效的域名格式' };
}
return {
isValid: true,
normalized: parsed.href,
};
} catch (error) {
return {
isValid: false,
error: 'URL格式无效',
};
}
}
/**
* 检查是否包含协议
*/
private static hasProtocol(url: string): boolean {
return /^[a-z][a-z0-9+.-]*:/i.test(url);
}
/**
* 验证主机名格式
*/
private static isValidHostname(hostname: string): boolean {
// IP地址检查
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
return true;
}
// 域名检查
const domainRegex = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
return domainRegex.test(hostname);
}
/**
* 解析URL
*/
static parse(input: string): ParsedURL {
const validation = this.validate(input);
if (!validation.isValid) {
return {
original: input,
normalized: input,
protocol: '',
hostname: '',
isValid: false,
error: validation.error,
};
}
try {
const url = new URL(validation.normalized!);
return {
original: input,
normalized: url.href,
protocol: url.protocol,
hostname: url.hostname,
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries()),
hash: url.hash,
isValid: true,
};
} catch {
return {
original: input,
normalized: input,
protocol: '',
hostname: '',
isValid: false,
error: '解析失败',
};
}
}
/**
* 提取域名
*/
static extractDomain(url: string): string | null {
try {
const parsed = this.parse(url);
return parsed.isValid ? parsed.hostname : null;
} catch {
return null;
}
}
/**
* 检查是否为内部链接
*/
static isInternalLink(url: string, baseDomain: string): boolean {
const domain = this.extractDomain(url);
return domain === baseDomain || domain?.endsWith(`.${baseDomain}`) || false;
}
/**
* 缩短显示URL
*/
static shortenForDisplay(url: string, maxLength: number = 40): string {
if (url.length <= maxLength) {
return url;
}
try {
const parsed = new URL(url);
const domain = parsed.hostname;
const path = parsed.pathname + parsed.search;
if (domain.length > maxLength - 3) {
return domain.substring(0, maxLength - 3) + '...';
}
const remainingLength = maxLength - domain.length - 4; // 留出 "..." 和 "://" 的空间
if (path.length > remainingLength) {
return `${domain}...${path.substring(path.length - remainingLength)}`;
}
return url;
} catch {
return url.substring(0, maxLength - 3) + '...';
}
}
/**
* 检测并处理常见问题
*/
static sanitize(input: string): string {
let sanitized = input.trim();
// 去除首尾空格
sanitized = sanitized.trim();
// 处理中文句号
sanitized = sanitized.replace(/。/g, '.');
// 处理多个斜杠
sanitized = sanitized.replace(/([^:])\/\//g, '$1/');
// 确保协议小写
sanitized = sanitized.replace(/^([A-Z][A-Z0-9+.-]*):/i, (match) => match.toLowerCase());
return sanitized;
}
/**
* 生成二维码数据
*/
static generateQRCode(url: string): string {
const parsed = this.parse(url);
return parsed.isValid ? parsed.normalized : url;
}
}
3.2 URL输入组件
typescript
/**
* URL输入组件
* 支持实时验证、自动补全、历史记录等功能
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
View,
TextInput,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
ScrollView,
Alert,
} from 'react-native';
import { URLValidator, ParsedURL } from './URLValidator';
interface URLInputProps {
placeholder?: string;
onSubmit?: (url: ParsedURL) => void;
showHistory?: boolean;
maxHistoryItems?: number;
}
export const URLInput: React.FC<URLInputProps> = ({
placeholder = '输入网址,如 example.com',
onSubmit,
showHistory = true,
maxHistoryItems = 10,
}) => {
const [input, setInput] = useState('');
const [validation, setValidation] = useState<{ isValid: boolean; error?: string }>({
isValid: false,
});
const [isValidating, setIsValidating] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(false);
const [history, setHistory] = useState<string[]>([]);
const inputRef = useRef<TextInput>(null);
const debounceTimer = useRef<NodeJS.Timeout>();
/**
* 从本地存储加载历史记录
*/
useEffect(() => {
loadHistory();
}, []);
/**
* 加载历史记录
*/
const loadHistory = async () => {
try {
// 实际项目中使用AsyncStorage
// const stored = await AsyncStorage.getItem('url_history');
// if (stored) setHistory(JSON.parse(stored));
setHistory(['https://www.example.com', 'https://github.com']);
} catch (error) {
console.error('Failed to load history:', error);
}
};
/**
* 保存到历史记录
*/
const saveToHistory = async (url: string) => {
try {
const newHistory = [url, ...history.filter(h => h !== url)].slice(0, maxHistoryItems);
setHistory(newHistory);
// await AsyncStorage.setItem('url_history', JSON.stringify(newHistory));
} catch (error) {
console.error('Failed to save history:', error);
}
};
/**
* 验证URL(防抖)
*/
const validateURL = useCallback((url: string) => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
setIsValidating(true);
debounceTimer.current = setTimeout(() => {
const sanitized = URLValidator.sanitize(url);
const result = URLValidator.validate(sanitized);
setValidation(result);
setIsValidating(false);
// 显示建议
if (result.isValid && sanitized !== url) {
setShowSuggestions(true);
}
}, 500);
}, []);
/**
* 处理输入变化
*/
const handleChange = useCallback((text: string) => {
setInput(text);
if (text.length > 0) {
validateURL(text);
} else {
setValidation({ isValid: false });
setShowSuggestions(false);
}
}, [validateURL]);
/**
* 处理提交
*/
const handleSubmit = useCallback(() => {
if (!input.trim()) {
Alert.alert('提示', '请输入URL');
return;
}
if (!validation.isValid) {
Alert.alert('验证失败', validation.error || 'URL格式无效');
return;
}
const parsed = URLValidator.parse(input);
onSubmit?.(parsed);
saveToHistory(parsed.normalized);
setInput('');
setValidation({ isValid: false });
}, [input, validation, onSubmit]);
/**
* 应用建议
*/
const applySuggestion = useCallback(() => {
const result = URLValidator.validate(input);
if (result.isValid && result.normalized) {
setInput(result.normalized);
setShowSuggestions(false);
}
}, [input]);
/**
* 选择历史记录
*/
const selectHistory = useCallback((url: string) => {
setInput(url);
const result = URLValidator.validate(url);
setValidation(result);
setShowSuggestions(false);
inputRef.current?.focus();
}, []);
return (
<View style={styles.container}>
{/* 输入框区域 */}
<View style={styles.inputContainer}>
<View style={styles.inputWrapper}>
<Text style={styles.protocolIndicator}>https://</Text>
<TextInput
ref={inputRef}
style={styles.input}
value={input.replace(/^https?:\/\//, '')}
onChangeText={handleChange}
onSubmitEditing={handleSubmit}
placeholder={placeholder}
placeholderTextColor="#999"
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
returnKeyType="go"
textContentType="URL"
/>
{isValidating && (
<ActivityIndicator size="small" color="#007AFF" style={styles.spinner} />
)}
{!isValidating && input.length > 0 && (
<TouchableOpacity
onPress={() => {
setInput('');
setValidation({ isValid: false });
}}
style={styles.clearButton}
>
<Text style={styles.clearButtonIcon}>×</Text>
</TouchableOpacity>
)}
</View>
{/* 提交按钮 */}
<TouchableOpacity
style={[
styles.submitButton,
!validation.isValid && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={!validation.isValid}
>
<Text style={styles.submitButtonText}>前往</Text>
</TouchableOpacity>
</View>
{/* 验证状态提示 */}
{input.length > 0 && !isValidating && (
<View style={styles.statusContainer}>
{validation.isValid ? (
<View style={styles.successBadge}>
<Text style={styles.successIcon}>✓</Text>
<Text style={styles.successText}>URL格式有效</Text>
</View>
) : (
<View style={styles.errorBadge}>
<Text style={styles.errorIcon}>!</Text>
<Text style={styles.errorText}>{validation.error}</Text>
</View>
)}
</View>
)}
{/* 自动修正建议 */}
{showSuggestions && validation.isValid && (
<View style={styles.suggestionContainer}>
<Text style={styles.suggestionText}>
是否访问: <Text style={styles.suggestionURL}>{validation.normalized}</Text>
</Text>
<TouchableOpacity
style={styles.suggestionButton}
onPress={applySuggestion}
>
<Text style={styles.suggestionButtonText}>确认</Text>
</TouchableOpacity>
</View>
)}
{/* 历史记录 */}
{showHistory && history.length > 0 && input.length === 0 && (
<View style={styles.historyContainer}>
<Text style={styles.historyTitle}>最近访问</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.historyList}
>
{history.map((url, index) => (
<TouchableOpacity
key={index}
style={styles.historyItem}
onPress={() => selectHistory(url)}
>
<Text style={styles.historyText} numberOfLines={1}>
{URLValidator.shortenForDisplay(url, 30)}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
inputContainer: {
flexDirection: 'row',
gap: 10,
},
inputWrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F5F5',
borderRadius: 10,
paddingHorizontal: 12,
borderWidth: 1,
borderColor: '#E5E5E5',
},
protocolIndicator: {
fontSize: 14,
color: '#999',
marginRight: 4,
},
input: {
flex: 1,
height: 44,
fontSize: 16,
color: '#333',
paddingVertical: 0,
},
spinner: {
marginRight: 8,
},
clearButton: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#E0E0E0',
justifyContent: 'center',
alignItems: 'center',
},
clearButtonIcon: {
fontSize: 18,
color: '#666',
fontWeight: 'bold',
},
submitButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
height: 44,
},
submitButtonDisabled: {
backgroundColor: '#B4D4FF',
},
submitButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
statusContainer: {
marginTop: 12,
},
successBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#E8F5E9',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
},
successIcon: {
fontSize: 16,
color: '#4CAF50',
marginRight: 6,
},
successText: {
fontSize: 14,
color: '#2E7D32',
},
errorBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFEBEE',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
},
errorIcon: {
fontSize: 16,
color: '#F44336',
marginRight: 6,
},
errorText: {
fontSize: 14,
color: '#C62828',
},
suggestionContainer: {
marginTop: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#E3F2FD',
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 8,
},
suggestionText: {
flex: 1,
fontSize: 14,
color: '#1976D2',
},
suggestionURL: {
fontWeight: '600',
},
suggestionButton: {
backgroundColor: '#2196F3',
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 6,
},
suggestionButtonText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
},
historyContainer: {
marginTop: 16,
},
historyTitle: {
fontSize: 13,
color: '#999',
marginBottom: 10,
},
historyList: {
gap: 8,
},
historyItem: {
backgroundColor: '#F5F5F5',
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 8,
minWidth: 120,
},
historyText: {
fontSize: 13,
color: '#666',
},
});
export default URLInput;
3.3 链接预览组件
typescript
/**
* 链接预览组件
* 显示链接的标题、描述、缩略图等信息
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
Image,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import { URLValidator } from './URLValidator';
interface LinkPreviewProps {
url: string;
onPress?: () => void;
}
export const LinkPreview: React.FC<LinkPreviewProps> = ({ url, onPress }) => {
const [preview, setPreview] = useState<LinkPreview | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchPreview();
}, [url]);
const fetchPreview = async () => {
setLoading(true);
setError(null);
try {
// 实际项目中需要调用后端API获取预览信息
// 这里使用模拟数据
await new Promise(resolve => setTimeout(resolve, 1000));
const domain = URLValidator.extractDomain(url);
setPreview({
url,
title: `${domain} - 网站标题`,
description: '这是网站的描述信息,展示链接内容的简要概括...',
image: 'https://via.placeholder.com/400x200',
favicon: `https://www.google.com/s2/favicons?domain=${domain}`,
siteName: domain,
});
} catch (err) {
setError('获取预览失败');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<View style={previewStyles.loadingContainer}>
<ActivityIndicator size="small" color="#007AFF" />
<Text style={previewStyles.loadingText}>正在获取链接信息...</Text>
</View>
);
}
if (error) {
return (
<View style={previewStyles.errorContainer}>
<Text style={previewStyles.errorText}>{error}</Text>
</View>
);
}
if (!preview) return null;
return (
<TouchableOpacity
style={previewStyles.container}
onPress={onPress}
activeOpacity={0.8}
>
{preview.image && (
<Image
source={{ uri: preview.image }}
style={previewStyles.image}
resizeMode="cover"
/>
)}
<View style={previewStyles.content}>
{preview.siteName && (
<View style={previewStyles.siteInfo}>
{preview.favicon && (
<Image
source={{ uri: preview.favicon }}
style={previewStyles.favicon}
/>
)}
<Text style={previewStyles.siteName}>{preview.siteName}</Text>
</View>
)}
{preview.title && (
<Text style={previewStyles.title} numberOfLines={2}>
{preview.title}
</Text>
)}
{preview.description && (
<Text style={previewStyles.description} numberOfLines={3}>
{preview.description}
</Text>
)}
<Text style={previewStyles.url} numberOfLines={1}>
{URLValidator.shortenForDisplay(preview.url, 50)}
</Text>
</View>
</TouchableOpacity>
);
};
const previewStyles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#E5E5E5',
},
image: {
width: '100%',
height: 180,
},
content: {
padding: 12,
},
siteInfo: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
favicon: {
width: 16,
height: 16,
borderRadius: 2,
marginRight: 6,
},
siteName: {
fontSize: 12,
color: '#666',
},
title: {
fontSize: 15,
fontWeight: '600',
color: '#333',
marginBottom: 4,
},
description: {
fontSize: 13,
color: '#666',
lineHeight: 18,
marginBottom: 8,
},
url: {
fontSize: 12,
color: '#007AFF',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
backgroundColor: '#F5F5F5',
borderRadius: 12,
},
loadingText: {
marginLeft: 10,
fontSize: 14,
color: '#999',
},
errorContainer: {
padding: 16,
backgroundColor: '#FFEBEE',
borderRadius: 12,
},
errorText: {
fontSize: 14,
color: '#C62828',
textAlign: 'center',
},
});
3.4 使用示例
typescript
/**
* URL输入组件使用示例
*/
import React, { useState } from 'react';
import {
View,
StyleSheet,
ScrollView,
Alert,
} from 'react-native';
import { URLInput } from './URLInput';
import { LinkPreview } from './LinkPreview';
import { ParsedURL } from './URLValidator';
const URLInputDemo: React.FC = () => {
const [currentURL, setCurrentURL] = useState<ParsedURL | null>(null);
const handleSubmit = (parsed: ParsedURL) => {
setCurrentURL(parsed);
Alert.alert(
'URL已验证',
`协议: ${parsed.protocol}\n域名: ${parsed.hostname}`,
[
{ text: '取消', style: 'cancel' },
{ text: '访问', onPress: () => console.log('Navigate to:', parsed.normalized) },
]
);
};
return (
<ScrollView style={demoStyles.container} contentContainerStyle={demoStyles.content}>
<Text style={demoStyles.title}>URL链接输入</Text>
<URLInput
placeholder="输入网址,如 example.com"
onSubmit={handleSubmit}
showHistory
/>
{currentURL && (
<View style={demoStyles.previewSection}>
<Text style={demoStyles.sectionTitle}>链接预览</Text>
<LinkPreview
url={currentURL.normalized}
onPress={() => console.log('Open link:', currentURL.normalized)}
/>
</View>
)}
</ScrollView>
);
};
const demoStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 20,
textAlign: 'center',
},
previewSection: {
marginTop: 24,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#666',
marginBottom: 12,
},
});
export default URLInputDemo;
四、HarmonyOS平台优化
4.1 深度链接处理
typescript
/**
* HarmonyOS深度链接处理
*/
import { Platform } from 'react-native';
export class DeepLinkHandler {
/**
* 打开链接
*/
static async openURL(url: string): Promise<boolean> {
try {
if (Platform.OS === 'harmony') {
// 使用HarmonyOS的Intent打开链接
// 需要配置相关权限
console.log('Opening URL on HarmonyOS:', url);
return true;
}
return false;
} catch (error) {
console.error('Failed to open URL:', error);
return false;
}
}
/**
* 检查是否为应用内链接
*/
static isAppLink(url: string): boolean {
return url.startsWith('myapp://');
}
}
五、最佳实践
| 功能 | 实现方式 | 效果 |
|---|---|---|
| 实时验证 | 防抖500ms后验证 | 减少不必要的计算 |
| 自动补全 | 检测缺失协议 | 用户体验更流畅 |
| 历史记录 | 本地存储最近访问 | 快速输入常用链接 |
| 链接预览 | 异步获取元数据 | 展示链接内容概览 |
| 错误处理 | 友好的错误提示 | 帮助用户理解问题 |
六、总结
本文详细介绍了在HarmonyOS平台上实现URL链接输入的完整方案,涵盖:
- URL验证:格式检查、协议验证、域名验证
- 智能处理:自动添加协议、URL标准化
- 用户体验:实时验证、历史记录、错误提示
- 高级功能:链接预览、深度链接处理
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
