Cordova IP地址查询应用实现技术博客

Cordova IP地址查询应用实现技术博客

📋 项目概述

IP地址查询是网络应用中常见的功能需求,无论是用于网络安全监控、用户行为分析,还是内容分发网络(CDN)优化,都需要准确、高效的IP归属地查询服务。本文将详细介绍如何在Cordova应用中集成高精度IP地址查询功能,支持IPv4和IPv6地址查询,并提供完整的实现方案和代码示例。

image-20251121213129872

功能特性

  • 高精度IP归属地查询 - 支持IPv4和IPv6地址格式
  • 自动IP检测 - 不输入IP时自动查询当前客户端IP
  • 地理位置信息 - 显示大洲、国家、省份、城市、区县等详细信息
  • 坐标定位 - 提供经纬度坐标和地图链接
  • ISP信息 - 显示IP运营商和城市编码
  • Token管理 - 本地存储API Token,提升用户体验
  • 响应式设计 - 完美适配移动端和桌面端

🛠️ 技术选型

核心技术栈

  • 前端框架: HTML5 + CSS3 + JavaScript (ES6+)
  • 跨平台框架: Apache Cordova
  • API服务: AlAPI IP查询接口 (https://v3.alapi.cn/api/ip)
  • 数据存储: LocalStorage (API Token持久化)
  • UI设计: 响应式布局,渐变背景,卡片式设计

技术优势

  1. 跨平台兼容: 基于Cordova框架,一套代码支持Android、iOS、HarmonyOS等多个平台
  2. API集成简单: RESTful API设计,使用标准fetch API即可调用
  3. 用户体验优化: 本地存储Token,减少重复输入;响应式设计适配各种屏幕尺寸
  4. 错误处理完善: 网络错误、参数验证、API错误等全方位错误处理

📦 项目结构

CordovaApp/
├── www/
│   ├── ip.html              # IP查询页面
│   └── js/
│       └── ip.js            # IP查询功能逻辑
├── harmonyos/
│   └── entry/src/main/resources/rawfile/www/
│       ├── ip.html          # HarmonyOS版本IP查询页面
│       └── js/
│           └── ip.js        # HarmonyOS版本IP查询逻辑
└── IP地址查询.md            # API文档

🚀 实现步骤

步骤1: 创建HTML页面结构

创建 ip.html 文件,包含以下核心部分:

  1. 导航栏 - 统一的导航菜单
  2. 表单区域 - API Token输入和IP地址输入(可选)
  3. 结果展示区域 - 动态显示查询结果
  4. Token获取弹窗 - 引导用户获取API Token
关键HTML结构
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: https://ssl.gstatic.com https://v3.alapi.cn https://www.alapi.cn 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://v3.alapi.cn https://www.alapi.cn; frame-src https://www.alapi.cn;">
    <meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
    <title>IP地址查询</title>
    <link rel="stylesheet" href="css/index.css">
</head>
<body>
    <!-- 导航栏 -->
    <div class="nav">
        <a href="index.html">首页</a>
        <a href="about.html">关于我们</a>
        <a href="poem.html">藏头诗</a>
        <a href="translate.html">翻译</a>
        <a href="express.html">快递查询</a>
        <a href="ip.html" class="active">IP查询</a>
    </div>

    <!-- 主容器 -->
    <div class="ip-container">
        <!-- 页面标题 -->
        <div class="ip-header">
            <h1>IP地址查询</h1>
            <p>高精度IP归属地查询,支持IPv4和IPv6</p>
        </div>

        <!-- 表单区域 -->
        <div class="form-container">
            <form id="ip-form">
                <div class="form-group">
                    <label for="token" class="label-with-help">
                        <span>API Token *</span>
                        <button type="button" class="help-btn" onclick="openTokenModal()">获取 Token</button>
                    </label>
                    <input type="text" id="token" name="token" placeholder="请输入API Token" required>
                </div>

                <div class="form-group">
                    <label for="ip">IP地址(可选)</label>
                    <input type="text" id="ip" name="ip" 
                           placeholder="留空则查询当前IP地址" 
                           pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$">
                    <div class="ip-hint">支持IPv4和IPv6格式,留空则自动查询当前IP地址</div>
                </div>

                <button type="submit" class="submit-btn" id="submit-btn">查询</button>
            </form>

            <!-- 加载状态 -->
            <div class="loading" id="loading">正在查询</div>
            
            <!-- 错误提示 -->
            <div class="error-message" id="error-message"></div>
        </div>

        <!-- 结果展示区域 -->
        <div class="result-container" id="result-container">
            <div class="result-title">查询结果</div>
            <div class="ip-info" id="ip-info"></div>
        </div>
    </div>

    <!-- Token获取弹窗 -->
    <div class="modal-overlay" id="token-modal" onclick="closeTokenModal(event)">
        <div class="modal-content" onclick="event.stopPropagation()">
            <div class="modal-header">
                <h2>获取 API Token</h2>
                <button class="modal-close" onclick="closeTokenModal()">&times;</button>
            </div>
            <div class="modal-body">
                <p>请在下方页面注册并获取您的 API Token,然后将 Token 复制到输入框中。</p>
                <iframe src="https://www.alapi.cn/aff/nutpi" id="token-iframe"></iframe>
            </div>
        </div>
    </div>

    <script src="cordova.js"></script>
    <script src="js/ip.js"></script>
</body>
</html>

步骤2: 实现核心JavaScript逻辑

创建 ip.js 文件,实现IP查询的核心功能:

2.1 表单提交处理
// 表单提交处理
document.getElementById('ip-form').addEventListener('submit', function(e) {
    e.preventDefault();
    queryIP();
});
2.2 IP查询函数
function queryIP() {
    const submitBtn = document.getElementById('submit-btn');
    const loading = document.getElementById('loading');
    const errorMessage = document.getElementById('error-message');
    const resultContainer = document.getElementById('result-container');
    
    // 获取表单数据
    const formData = {
        token: document.getElementById('token').value.trim(),
        ip: document.getElementById('ip').value.trim()
    };
    
    // 验证token
    if (!formData.token) {
        showError('请输入API Token');
        return;
    }
    
    // 验证IP格式(如果输入了IP)
    if (formData.ip && !isValidIP(formData.ip)) {
        showError('IP地址格式不正确,请输入有效的IPv4或IPv6地址');
        return;
    }
    
    // 显示加载状态
    submitBtn.disabled = true;
    submitBtn.textContent = '查询中...';
    loading.classList.add('show');
    errorMessage.classList.remove('show');
    resultContainer.classList.remove('show');
    
    // 构建请求参数
    const params = new URLSearchParams();
    params.append('token', formData.token);
    if (formData.ip) {
        params.append('ip', formData.ip);
    }
    
    // 发送POST请求
    fetch('https://v3.alapi.cn/api/ip', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params.toString()
    })
    .then(response => {
        if (!response.ok) {
            throw new Error('网络请求失败: ' + response.status);
        }
        return response.json();
    })
    .then(data => {
        console.log('API响应:', data);
        
        if (data.success && data.code === 200) {
            displayIPInfo(data.data);
        } else {
            showError(data.message || '查询失败,请检查参数');
        }
    })
    .catch(error => {
        console.error('请求错误:', error);
        showError('请求失败: ' + error.message + '。请检查网络连接或API Token是否正确');
    })
    .finally(() => {
        // 恢复按钮状态
        submitBtn.disabled = false;
        submitBtn.textContent = '查询';
        loading.classList.remove('show');
    });
}
2.3 IP格式验证
// 验证IP地址格式
function isValidIP(ip) {
    // IPv4格式验证
    const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
    // IPv6格式验证(简化版)
    const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/;
    
    return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
2.4 结果显示函数
// 显示IP信息
function displayIPInfo(data) {
    const resultContainer = document.getElementById('result-container');
    const ipInfo = document.getElementById('ip-info');
    
    let html = '';
    
    // IP基本信息
    html += '<div class="info-section">';
    html += '<div class="info-section-title">IP地址信息</div>';
    html += '<div class="info-grid">';
    html += `<div class="info-item"><div class="info-label">IP地址</div><div class="info-value">${data.ip || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">IP范围</div><div class="info-value">${data.beginip || '未知'} - ${data.endip || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">ISP运营商</div><div class="info-value">${data.isp || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">城市编码</div><div class="info-value">${data.adcode || '未知'}</div></div>`;
    html += '</div>';
    html += '</div>';
    
    // 地理位置信息
    html += '<div class="info-section">';
    html += '<div class="info-section-title">地理位置</div>';
    html += '<div class="info-grid">';
    html += `<div class="info-item"><div class="info-label">大洲</div><div class="info-value">${data.continent || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">国家</div><div class="info-value">${data.country || '未知'} ${data.country_en ? '(' + data.country_en + ')' : ''}</div></div>`;
    html += `<div class="info-item"><div class="info-label">国家代码</div><div class="info-value">${data.country_cc || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">省份</div><div class="info-value">${data.province || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">城市</div><div class="info-value">${data.city || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">区县</div><div class="info-value">${data.district || '未知'}</div></div>`;
    html += `<div class="info-item"><div class="info-label">详细地址</div><div class="info-value">${data.pos || '未知'}</div></div>`;
    html += '</div>';
    html += '</div>';
    
    // 坐标信息
    if (data.lng && data.lat) {
        html += '<div class="info-section">';
        html += '<div class="info-section-title">坐标信息</div>';
        html += '<div class="info-grid">';
        html += `<div class="info-item"><div class="info-label">经度</div><div class="info-value">${data.lng}</div></div>`;
        html += `<div class="info-item"><div class="info-label">纬度</div><div class="info-value">${data.lat}</div></div>`;
        html += '</div>';
        html += `<a href="https://www.openstreetmap.org/?mlat=${data.lat}&mlon=${data.lng}&zoom=15" target="_blank" class="map-link">在地图上查看</a>`;
        html += '</div>';
    }
    
    ipInfo.innerHTML = html;
    
    // 显示结果容器
    resultContainer.classList.add('show');
    
    // 滚动到结果区域
    resultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
2.5 Token本地存储
// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', function() {
    // 尝试从本地存储加载token
    const savedToken = localStorage.getItem('ip_api_token');
    if (savedToken) {
        document.getElementById('token').value = savedToken;
    }
    
    // 保存token到本地存储
    document.getElementById('token').addEventListener('blur', function() {
        const token = this.value.trim();
        if (token) {
            localStorage.setItem('ip_api_token', token);
        }
    });
    
    console.log('IP查询页面加载完成');
});

步骤3: 样式设计

3.1 响应式布局
.ip-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background-color: #fff;
    min-height: calc(100vh - 100px);
}

@media (max-width: 768px) {
    .ip-header h1 {
        font-size: 2em;
    }
    .nav a {
        display: block;
        margin: 5px 0;
    }
    .info-grid {
        grid-template-columns: 1fr;
    }
}
3.2 卡片式信息展示
.info-section {
    margin-bottom: 25px;
    padding-bottom: 25px;
    border-bottom: 1px solid #eee;
}

.info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
}

.info-item {
    padding: 12px;
    background-color: #f8f9fa;
    border-radius: 6px;
}
3.3 渐变按钮设计
.submit-btn {
    width: 100%;
    padding: 15px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 18px;
    font-weight: bold;
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
}

.submit-btn:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

🔍 核心代码分析

API集成详解

1. API端点
  • URL: https://v3.alapi.cn/api/ip
  • 方法: POST
  • Content-Type: application/x-www-form-urlencoded
2. 请求参数
参数名类型必填说明
tokenstring接口调用token
ipstring要查询的IP,默认获取请求客户端的IP
3. 响应数据结构
{
    "request_id": "722950494561095681",
    "success": true,
    "message": "success",
    "code": 200,
    "data": {
        "ip": "114.114.114.114",
        "beginip": "114.114.114.114",
        "endip": "114.114.114.114",
        "continent": "亚洲",
        "country": "中国",
        "province": "江苏",
        "city": "南京",
        "district": "",
        "isp": "114DNS",
        "adcode": "320100",
        "country_en": "China",
        "country_cc": "CN",
        "lng": "118.7674",
        "lat": "32.0415",
        "pos": "中国江苏南京"
    },
    "time": 1733324829,
    "usage": 0
}
4. 错误处理
.catch(error => {
    console.error('请求错误:', error);
    showError('请求失败: ' + error.message + '。请检查网络连接或API Token是否正确');
})

IP格式验证

IPv4验证正则表达式
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

说明:

  • 25[0-5]: 匹配250-255
  • 2[0-4][0-9]: 匹配200-249
  • [01]?[0-9][0-9]?: 匹配0-199(允许前导零)
IPv6验证正则表达式
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/;

说明:

  • ([0-9a-fA-F]{1,4}:){7}: 匹配7组4位十六进制数加冒号
  • [0-9a-fA-F]{1,4}: 匹配最后一组4位十六进制数
  • ^::1$: 匹配本地回环地址
  • ^::$: 匹配未指定地址

🎨 UX优化

1. Token获取引导

通过弹窗方式引导用户获取API Token,提升用户体验:

function openTokenModal() {
    document.getElementById('token-modal').classList.add('show');
    document.body.style.overflow = 'hidden';
}

function closeTokenModal(event) {
    if (event && event.target !== event.currentTarget) {
        return;
    }
    document.getElementById('token-modal').classList.remove('show');
    document.body.style.overflow = '';
}

2. 加载状态提示

// 显示加载状态
submitBtn.disabled = true;
submitBtn.textContent = '查询中...';
loading.classList.add('show');

3. 结果自动滚动

// 滚动到结果区域
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });

4. 地图链接集成

当查询结果包含经纬度信息时,自动生成地图查看链接:

if (data.lng && data.lat) {
    html += `<a href="https://www.openstreetmap.org/?mlat=${data.lat}&mlon=${data.lng}&zoom=15" target="_blank" class="map-link">在地图上查看</a>`;
}

🐛 常见问题与解决方案

问题1: IP格式验证失败

症状: 输入有效的IP地址但提示格式不正确

原因: IPv6格式验证过于严格,未考虑压缩格式

解决方案: 扩展IPv6验证规则,支持压缩格式

// 更完善的IPv6验证
function isValidIPv6(ip) {
    // 支持压缩格式的IPv6验证
    const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
    return ipv6Regex.test(ip);
}

问题2: API Token过期

症状: 查询失败,提示token无效

原因: API Token可能过期或被撤销

解决方案: 添加token有效性检测和友好的错误提示

.then(data => {
    if (data.success && data.code === 200) {
        displayIPInfo(data.data);
    } else if (data.code === 401 || data.message.includes('token')) {
        showError('API Token无效或已过期,请重新获取Token');
        // 清除本地存储的token
        localStorage.removeItem('ip_api_token');
    } else {
        showError(data.message || '查询失败,请检查参数');
    }
})

问题3: 网络请求超时

症状: 长时间无响应,最终超时

原因: 网络不稳定或API服务响应慢

解决方案: 添加请求超时处理

function queryIPWithTimeout(url, options, timeout = 10000) {
    return Promise.race([
        fetch(url, options),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('请求超时')), timeout)
        )
    ]);
}

// 使用超时请求
queryIPWithTimeout('https://v3.alapi.cn/api/ip', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: params.toString()
}, 10000)

问题4: 跨域问题

症状: 浏览器控制台报CORS错误

原因: Content-Security-Policy配置不当

解决方案: 正确配置CSP,允许API域名

<meta http-equiv="Content-Security-Policy" content="
    default-src 'self' data: https://ssl.gstatic.com https://v3.alapi.cn https://www.alapi.cn 'unsafe-eval'; 
    style-src 'self' 'unsafe-inline'; 
    media-src *; 
    img-src 'self' data: content:; 
    script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
    connect-src 'self' https://v3.alapi.cn https://www.alapi.cn; 
    frame-src https://www.alapi.cn;
">

⚡ 性能优化

1. 防抖处理

避免用户快速点击查询按钮导致重复请求:

// 防抖函数
function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

// 应用防抖
const debouncedQueryIP = debounce(queryIP, 300);
document.getElementById('ip-form').addEventListener('submit', function(e) {
    e.preventDefault();
    debouncedQueryIP();
});

2. 请求缓存

对于相同IP的查询结果进行缓存,减少API调用:

// IP查询缓存
const ipCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存

function getCachedResult(ip) {
    const cacheKey = ip || 'current';
    const cached = ipCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
        return cached.data;
    }
    return null;
}

function setCachedResult(ip, data) {
    const cacheKey = ip || 'current';
    ipCache.set(cacheKey, {
        data: data,
        timestamp: Date.now()
    });
}

// 在queryIP函数中使用缓存
function queryIP() {
    const ip = document.getElementById('ip').value.trim();
    const cached = getCachedResult(ip);
    if (cached) {
        displayIPInfo(cached);
        return;
    }
    // ... 继续API请求
}

3. 懒加载地图

地图链接仅在用户点击时加载,减少初始页面加载时间:

// 延迟加载地图iframe
function loadMap(lat, lng) {
    const mapContainer = document.getElementById('map-container');
    if (!mapContainer.querySelector('iframe')) {
        const iframe = document.createElement('iframe');
        iframe.src = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}&zoom=15`;
        iframe.width = '100%';
        iframe.height = '400px';
        iframe.frameBorder = '0';
        mapContainer.appendChild(iframe);
    }
}

🔒 安全最佳实践

1. Token安全存储

  • 使用LocalStorage存储Token,但要注意安全性
  • 考虑使用加密存储(对于敏感应用)
  • 定期清理过期的Token
// Token加密存储(示例)
function encryptToken(token) {
    // 简单的Base64编码(实际应用中应使用更安全的加密方法)
    return btoa(token);
}

function decryptToken(encryptedToken) {
    try {
        return atob(encryptedToken);
    } catch (e) {
        return null;
    }
}

2. 输入验证

对所有用户输入进行严格验证,防止XSS攻击:

// HTML转义函数
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    return text.replace(/[&<>"']/g, m => map[m]);
}

// 在显示数据时使用
html += `<div class="info-value">${escapeHtml(data.ip || '未知')}</div>`;

3. HTTPS强制

确保所有API请求都通过HTTPS进行:

// 检查协议
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
    console.warn('建议使用HTTPS协议以确保安全');
}

📱 HarmonyOS平台适配

1. 文件同步

确保HarmonyOS版本的文件与通用版本保持同步:

# 同步IP查询页面
cp www/ip.html harmonyos/entry/src/main/resources/rawfile/www/ip.html
cp www/js/ip.js harmonyos/entry/src/main/resources/rawfile/www/js/ip.js

2. 权限配置

module.json5 中确保网络权限已配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_permission",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

3. CSP配置

config.xml 中配置Content-Security-Policy:

<preference name="Content-Security-Policy" 
            value="default-src 'self' data: https://ssl.gstatic.com https://v3.alapi.cn https://www.alapi.cn 'unsafe-eval'; 
                   style-src 'self' 'unsafe-inline'; 
                   media-src *; 
                   img-src 'self' data: content:; 
                   script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
                   connect-src 'self' https://v3.alapi.cn https://www.alapi.cn; 
                   frame-src https://www.alapi.cn;" />

📊 测试用例

功能测试

  1. 基本查询测试

    • 输入有效的API Token
    • 不输入IP地址,查询当前IP
    • 验证返回结果包含IP地址和地理位置信息
  2. IP格式验证测试

    • 输入有效的IPv4地址(如:114.114.114.114)
    • 输入有效的IPv6地址
    • 输入无效的IP格式,验证错误提示
  3. 错误处理测试

    • 输入无效的API Token,验证错误提示
    • 断开网络连接,验证网络错误提示
    • 输入空Token,验证必填项提示
  4. Token存储测试

    • 输入Token后刷新页面,验证Token是否自动填充
    • 清除浏览器缓存,验证Token是否清除

性能测试

  1. 响应时间测试

    • 测试API响应时间(目标:< 2秒)
    • 测试页面加载时间(目标:< 1秒)
  2. 并发测试

    • 测试多个并发请求的处理能力
    • 验证防抖功能是否正常工作

兼容性测试

  1. 浏览器兼容性

    • Chrome、Firefox、Safari、Edge
    • 移动端浏览器(iOS Safari、Chrome Mobile)
  2. 平台兼容性

    • Android平台
    • iOS平台
    • HarmonyOS平台

🎯 最佳实践总结

1. API集成

  • ✅ 使用标准的fetch API进行HTTP请求
  • ✅ 实现完善的错误处理机制
  • ✅ 添加请求超时处理
  • ✅ 实现请求结果缓存

2. 用户体验

  • ✅ 提供清晰的加载状态提示
  • ✅ 友好的错误信息提示
  • ✅ 自动保存用户输入(Token)
  • ✅ 响应式设计适配各种设备

3. 代码质量

  • ✅ 模块化代码结构
  • ✅ 完善的注释和文档
  • ✅ 输入验证和XSS防护
  • ✅ 性能优化(防抖、缓存)

4. 安全性

  • ✅ HTTPS协议通信
  • ✅ 输入验证和转义
  • ✅ Token安全存储
  • ✅ CSP配置

📚 完整代码示例

ip.html(完整版)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: https://ssl.gstatic.com https://v3.alapi.cn https://www.alapi.cn 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://v3.alapi.cn https://www.alapi.cn; frame-src https://www.alapi.cn;">
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
    <meta name="color-scheme" content="light dark">
    <link rel="stylesheet" href="css/index.css">
    <title>IP地址查询</title>
    <!-- 样式代码见上文 -->
</head>
<body>
    <!-- HTML结构代码见上文 -->
    <script src="cordova.js"></script>
    <script src="js/ip.js"></script>
</body>
</html>

ip.js(完整版)

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

// 等待设备就绪
document.addEventListener('deviceready', function() {
    console.log('设备就绪,IP查询功能可用');
}, false);

// IP查询缓存
const ipCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存

// 表单提交处理
document.getElementById('ip-form').addEventListener('submit', function(e) {
    e.preventDefault();
    queryIP();
});

// 查询IP地址
function queryIP() {
    const submitBtn = document.getElementById('submit-btn');
    const loading = document.getElementById('loading');
    const errorMessage = document.getElementById('error-message');
    const resultContainer = document.getElementById('result-container');
    
    // 获取表单数据
    const formData = {
        token: document.getElementById('token').value.trim(),
        ip: document.getElementById('ip').value.trim()
    };
    
    // 验证token
    if (!formData.token) {
        showError('请输入API Token');
        return;
    }
    
    // 检查缓存
    const cacheKey = formData.ip || 'current';
    const cached = ipCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_DURATION && cached.token === formData.token) {
        displayIPInfo(cached.data);
        return;
    }
    
    // 验证IP格式(如果输入了IP)
    if (formData.ip && !isValidIP(formData.ip)) {
        showError('IP地址格式不正确,请输入有效的IPv4或IPv6地址');
        return;
    }
    
    // 显示加载状态
    submitBtn.disabled = true;
    submitBtn.textContent = '查询中...';
    loading.classList.add('show');
    errorMessage.classList.remove('show');
    resultContainer.classList.remove('show');
    
    // 构建请求参数
    const params = new URLSearchParams();
    params.append('token', formData.token);
    if (formData.ip) {
        params.append('ip', formData.ip);
    }
    
    // 发送POST请求
    fetch('https://v3.alapi.cn/api/ip', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params.toString()
    })
    .then(response => {
        if (!response.ok) {
            throw new Error('网络请求失败: ' + response.status);
        }
        return response.json();
    })
    .then(data => {
        console.log('API响应:', data);
        
        if (data.success && data.code === 200) {
            // 缓存结果
            ipCache.set(cacheKey, {
                data: data.data,
                timestamp: Date.now(),
                token: formData.token
            });
            displayIPInfo(data.data);
        } else if (data.code === 401 || data.message.includes('token')) {
            showError('API Token无效或已过期,请重新获取Token');
            localStorage.removeItem('ip_api_token');
        } else {
            showError(data.message || '查询失败,请检查参数');
        }
    })
    .catch(error => {
        console.error('请求错误:', error);
        showError('请求失败: ' + error.message + '。请检查网络连接或API Token是否正确');
    })
    .finally(() => {
        // 恢复按钮状态
        submitBtn.disabled = false;
        submitBtn.textContent = '查询';
        loading.classList.remove('show');
    });
}

// 验证IP地址格式
function isValidIP(ip) {
    // IPv4格式验证
    const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
    // IPv6格式验证(简化版)
    const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/;
    
    return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}

// HTML转义函数
function escapeHtml(text) {
    if (!text) return '';
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    return String(text).replace(/[&<>"']/g, m => map[m]);
}

// 显示错误信息
function showError(message) {
    const errorMessage = document.getElementById('error-message');
    errorMessage.textContent = message;
    errorMessage.classList.add('show');
}

// 显示IP信息
function displayIPInfo(data) {
    const resultContainer = document.getElementById('result-container');
    const ipInfo = document.getElementById('ip-info');
    
    let html = '';
    
    // IP基本信息
    html += '<div class="info-section">';
    html += '<div class="info-section-title">IP地址信息</div>';
    html += '<div class="info-grid">';
    html += `<div class="info-item"><div class="info-label">IP地址</div><div class="info-value">${escapeHtml(data.ip || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">IP范围</div><div class="info-value">${escapeHtml(data.beginip || '未知')} - ${escapeHtml(data.endip || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">ISP运营商</div><div class="info-value">${escapeHtml(data.isp || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">城市编码</div><div class="info-value">${escapeHtml(data.adcode || '未知')}</div></div>`;
    html += '</div>';
    html += '</div>';
    
    // 地理位置信息
    html += '<div class="info-section">';
    html += '<div class="info-section-title">地理位置</div>';
    html += '<div class="info-grid">';
    html += `<div class="info-item"><div class="info-label">大洲</div><div class="info-value">${escapeHtml(data.continent || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">国家</div><div class="info-value">${escapeHtml(data.country || '未知')} ${data.country_en ? '(' + escapeHtml(data.country_en) + ')' : ''}</div></div>`;
    html += `<div class="info-item"><div class="info-label">国家代码</div><div class="info-value">${escapeHtml(data.country_cc || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">省份</div><div class="info-value">${escapeHtml(data.province || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">城市</div><div class="info-value">${escapeHtml(data.city || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">区县</div><div class="info-value">${escapeHtml(data.district || '未知')}</div></div>`;
    html += `<div class="info-item"><div class="info-label">详细地址</div><div class="info-value">${escapeHtml(data.pos || '未知')}</div></div>`;
    html += '</div>';
    html += '</div>';
    
    // 坐标信息
    if (data.lng && data.lat) {
        html += '<div class="info-section">';
        html += '<div class="info-section-title">坐标信息</div>';
        html += '<div class="info-grid">';
        html += `<div class="info-item"><div class="info-label">经度</div><div class="info-value">${escapeHtml(data.lng)}</div></div>`;
        html += `<div class="info-item"><div class="info-label">纬度</div><div class="info-value">${escapeHtml(data.lat)}</div></div>`;
        html += '</div>';
        html += `<a href="https://www.openstreetmap.org/?mlat=${data.lat}&mlon=${data.lng}&zoom=15" target="_blank" class="map-link">在地图上查看</a>`;
        html += '</div>';
    }
    
    ipInfo.innerHTML = html;
    
    // 显示结果容器
    resultContainer.classList.add('show');
    
    // 滚动到结果区域
    resultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', function() {
    // 尝试从本地存储加载token
    const savedToken = localStorage.getItem('ip_api_token');
    if (savedToken) {
        document.getElementById('token').value = savedToken;
    }
    
    // 保存token到本地存储
    document.getElementById('token').addEventListener('blur', function() {
        const token = this.value.trim();
        if (token) {
            localStorage.setItem('ip_api_token', token);
        }
    });
    
    console.log('IP查询页面加载完成');
});

📖 参考资料


🎉 总结

本文详细介绍了如何在Cordova应用中实现IP地址查询功能,包括:

  1. 完整的实现方案 - 从HTML结构到JavaScript逻辑的完整代码
  2. API集成详解 - 详细的API调用和数据处理流程
  3. 用户体验优化 - Token管理、加载状态、错误处理等
  4. 性能优化 - 缓存机制、防抖处理等
  5. 安全最佳实践 - 输入验证、XSS防护、HTTPS等
  6. 跨平台适配 - HarmonyOS平台的特殊配置

通过本文的指导,开发者可以快速集成IP地址查询功能到自己的Cordova应用中,并提供良好的用户体验和稳定的性能表现。


作者: 坚果派(NutPi)技术团队
版本: 1.0

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛夏天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值