首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >鸿蒙 Cordova IP 地址速查应用

鸿蒙 Cordova IP 地址速查应用

作者头像
徐建国
发布2025-12-24 18:22:27
发布2025-12-24 18:22:27
1510
举报
文章被收录于专栏:个人路线个人路线

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

📋 项目概述

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

image-20251121213129872
image-20251121213129872

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 错误等全方位错误处理

📦 项目结构

代码语言:javascript
复制
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 结构
代码语言:javascript
复制
<!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 表单提交处理
代码语言:javascript
复制
// 表单提交处理
document.getElementById('ip-form').addEventListener('submit', function(e) {
    e.preventDefault();
    queryIP();
});
2.2 IP 查询函数
代码语言:javascript
复制
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) {
            thrownewError('网络请求失败: ' + 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 格式验证
代码语言:javascript
复制
// 验证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 结果显示函数
代码语言:javascript
复制
// 显示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 本地存储
代码语言:javascript
复制
// 页面加载完成后的初始化
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 响应式布局
代码语言:javascript
复制
.ip-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background-color: #fff;
    min-height: calc(100vh - 100px);
}

@media (max-width:768px) {
    .ip-headerh1 {
        font-size: 2em;
    }
    .nava {
        display: block;
        margin: 5px0;
    }
    .info-grid {
        grid-template-columns: 1fr;
    }
}
3.2 卡片式信息展示
代码语言:javascript
复制
.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 渐变按钮设计
代码语言:javascript
复制
.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: 04px12pxrgba(102, 126, 234, 0.4);
}

🔍 核心代码分析

API 集成详解

1. API 端点
  • URL: https://v3.alapi.cn/api/ip
  • 方法: POST
  • Content-Type: application/x-www-form-urlencoded
2. 请求参数

参数名

类型

必填

说明

token

string

接口调用 token

ip

string

要查询的 IP,默认获取请求客户端的 IP

3. 响应数据结构
代码语言:javascript
复制
{
    "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. 错误处理
代码语言:javascript
复制
.catch(error => {
    console.error('请求错误:', error);
    showError('请求失败: ' + error.message + '。请检查网络连接或API Token是否正确');
})

IP 格式验证

IPv4 验证正则表达式
代码语言:javascript
复制
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 验证正则表达式
代码语言:javascript
复制
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,提升用户体验:

代码语言:javascript
复制
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. 加载状态提示

代码语言:javascript
复制
// 显示加载状态
submitBtn.disabled = true;
submitBtn.textContent = '查询中...';
loading.classList.add('show');

3. 结果自动滚动

代码语言:javascript
复制
// 滚动到结果区域
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });

4. 地图链接集成

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

代码语言:javascript
复制
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 验证规则,支持压缩格式

代码语言:javascript
复制
// 更完善的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 有效性检测和友好的错误提示

代码语言:javascript
复制
.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 服务响应慢

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

代码语言:javascript
复制
function queryIPWithTimeout(url, options, timeout = 10000) {
    returnPromise.race([
        fetch(url, options),
        newPromise((_, reject) =>
            setTimeout(() => reject(newError('请求超时')), 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 域名

代码语言:javascript
复制
<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. 防抖处理

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

代码语言:javascript
复制
// 防抖函数
function debounce(func, delay) {
    let timeoutId;
    returnfunction(...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 调用:

代码语言:javascript
复制
// IP查询缓存
const ipCache = newMap();
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;
    }
    returnnull;
}

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. 懒加载地图

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

代码语言:javascript
复制
// 延迟加载地图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
代码语言:javascript
复制
// Token加密存储(示例)
function encryptToken(token) {
    // 简单的Base64编码(实际应用中应使用更安全的加密方法)
    return btoa(token);
}

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

2. 输入验证

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

代码语言:javascript
复制
// 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 进行:

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

📱 HarmonyOS 平台适配

1. 文件同步

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

代码语言:javascript
复制
# 同步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 中确保网络权限已配置:

代码语言:javascript
复制
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_permission",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

3. CSP 配置

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

代码语言:javascript
复制
<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(完整版)

代码语言:javascript
复制
<!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(完整版)

代码语言:javascript
复制
/*
 * 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 = newMap();
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) {
            thrownewError('网络请求失败: ' + 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);
        } elseif (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;'
    };
    returnString(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查询页面加载完成');
});

📖 参考资料

  • AlAPI IP 查询接口文档[1]
  • Apache Cordova 官方文档[2]
  • MDN Web API - Fetch[3]
  • IPv4/IPv6 地址格式规范[4]
  • HarmonyOS 应用开发指南[5]

🎉 总结

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

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

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


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

参考资料

[1]

AlAPI IP查询接口文档: https://www.alapi.cn/

[2]

Apache Cordova官方文档: https://cordova.apache.org/docs/en/latest/

[3]

MDN Web API - Fetch: https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API

[4]

IPv4/IPv6地址格式规范: https://tools.ietf.org/html/rfc791

[5]

HarmonyOS应用开发指南: https://developer.harmonyos.com/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 大前端之旅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Cordova IP 地址查询应用实现技术博客
    • 📋 项目概述
      • 功能特性
    • 🛠️ 技术选型
      • 核心技术栈
      • 技术优势
    • 📦 项目结构
    • 🚀 实现步骤
      • 步骤 1: 创建 HTML 页面结构
      • 步骤 2: 实现核心 JavaScript 逻辑
      • 步骤 3: 样式设计
    • 🔍 核心代码分析
      • API 集成详解
      • IP 格式验证
    • 🎨 UX 优化
      • 1. Token 获取引导
      • 2. 加载状态提示
      • 3. 结果自动滚动
      • 4. 地图链接集成
    • 🐛 常见问题与解决方案
      • 问题 1: IP 格式验证失败
      • 问题 2: API Token 过期
      • 问题 3: 网络请求超时
      • 问题 4: 跨域问题
    • ⚡ 性能优化
      • 1. 防抖处理
      • 2. 请求缓存
      • 3. 懒加载地图
    • 🔒 安全最佳实践
      • 1. Token 安全存储
      • 2. 输入验证
      • 3. HTTPS 强制
    • 📱 HarmonyOS 平台适配
      • 1. 文件同步
      • 2. 权限配置
      • 3. CSP 配置
    • 📊 测试用例
      • 功能测试
      • 性能测试
      • 兼容性测试
    • 🎯 最佳实践总结
      • 1. API 集成
      • 2. 用户体验
      • 3. 代码质量
      • 4. 安全性
    • 📚 完整代码示例
      • ip.html(完整版)
      • ip.js(完整版)
    • 📖 参考资料
    • 🎉 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档