Skip to content

使用 JSONP 请求心知天气数据

约 1388 字大约 5 分钟

JSONP

2024-09-26

写在前面

没想到一个简单的 API 请求,调通花了我一个小时,只能说官网文档描述的不够清晰,而且不同文档之间还有出入,挺让人迷糊的。

下面完整记录一下流程,其实真的很简单。

准备工作

获取公钥和私钥

使用 jsonp 请求天气数据,必须要通过公钥签名的方式进行请求,不能通过私钥请求。

当你添加了 API 产品后,即可在控制台->产品管理中点击某个产品,查看该 API 产品的密钥,这里用的就是免费版。

配置域名

心知天气回对发起请求的原站做域名校验,只有绑定的域名才被准许请求,对于本地调试来说,就需要绑定一个 127.0.0.1,见上图。

同时本地开发环境也需要修改,vue 默认的预览地址是 localhost,也需要改成 127.0.0.1。我这里使用的是 vite,在 defineConfig 中添加 server 成员即可。

export default defineConfig({
  // 其他配置
  server: {
      host: '127.0.0.1',
      port: 8080,
    },
})

jsonp 封装

jsonp 请求比较具有工具性质,可以单独封装一下,后面也可以重复使用,这里直接给出我写的接口,里面有详细注释,后面使用均基于此。

export function jsonpRequest(url, callbackName) {
    return new Promise((resolve, reject) => {
        // 检查 URL 是否已经包含 callback 参数
        let urlAlreadyHasCallback = /[?&]callback=/.test(url);
        let scriptSrc = url;
        let actualCallbackName = callbackName;

        if (!urlAlreadyHasCallback) {            
            // 使用用户指定的 callbackName 或生成一个唯一的回调函数名
            actualCallbackName = `${callbackName}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
        }

        // 定义全局的回调函数
        window[actualCallbackName] = function (data) {
            // 调用对应的 resolve 函数
            resolve(data);
            // 移除回调函数
            delete window[actualCallbackName];
            // 移除 script 标签
            let script = document.querySelector(`script[src*="${actualCallbackName}"]`);
            if (script) {
                document.body.removeChild(script);
            }
        };

        if (!urlAlreadyHasCallback) {
            scriptSrc = url + (url.indexOf('?') === -1 ? '?' : '&') + 'callback=' + actualCallbackName;
        }

        // 创建一个 script 标签
        let script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = scriptSrc;
        script.onerror = function () {
            reject(new Error('JSONP request failed'));
            // 移除回调函数
            delete window[actualCallbackName];
            document.body.removeChild(script);
        };

        // 添加 script 标签到文档中
        document.body.appendChild(script);
    });
}

请求 URL 构建

这里以查询地址为例(因为需要地址 ID 去查询天气,这个是基础)。

确定请求 API

心知天气的 API,查询地址的 API 如下:

https://api.seniverse.com/v3/location/search.json

其中请求所需参数为 q=查询文本

确认 jsonp API 参数

通过 jsonp 请求,除了基本的 API 参数外,还需要额外的公钥、时间戳、保活时间、回调函数,其中保活时间不是必须的。

let params = {
    q: searchText,
    public_key: import.meta.env.VITE_APP_XINZHI_WEATHER_PUBLIC_KEY,
    ts: Date.now(),
    ttl: 300,
    callback: callbackName,
};

计算签名

使用上一步的所有参数通过特定算法进行计算,具体步骤如下:

1、将所有参数按照 key=value 的格式字典序升序进行排列,中间用&进行连接。对于本请求,就是下面这样:

callback=callbackName&public_key=your_public_key&q=beijing&ts=1727534325845&ttl=300

2、使用私钥,对上一步产生的字符串进行 HMAC-SHA1 的方式做哈希运算得到二进制结果,并用 Base64 的方式编码,得到一串哈希字符串。

3、再将上一步的哈希字符串做 URLEncode 编码,即可得到最终的签名。

看起来很复杂,实际很简单,这里直接贴出计算代码,需要注意的是,首先要安装 crypto-js 库:

npm install crypto-js

然后就可以计算签名了:

import CryptoJS from "crypto-js"

function generateSig(params, privateKey) {
    // 先按照键的字典序升序排列所有的键值对
    let entries = Object.entries(params).sort((a, b) => a[0].localeCompare(b[0]));
    // 将键值对转换为 "key=value" 格式
    let keyValuePairs = entries.map(([key, value]) => `${key}=${value}`);
    // 将键值对连接成字符串
    let paramsString = keyValuePairs.join('&');
    // 生成签名
    let sig = encodeURIComponent(
        CryptoJS.HmacSHA1(paramsString, privateKey)
            .toString(CryptoJS.enc.Base64));

    return sig;
}

拼接最终 URL

计算完签名后,需要把签名在拼接到上一步排列好的字符串后面,即在后面加上 &sig=sigString

其实可以在上一步一并完成,这里合并为一个新的接口:

function (params) {
    // 先按照键的字典序升序排列所有的键值对
    let entries = Object.entries(params).sort((a, b) => a[0].localeCompare(b[0]));
    // 将键值对转换为 "key=value" 格式
    let keyValuePairs = entries.map(([key, value]) => `${key}=${value}`);
    // 将键值对连接成字符串
    let paramsString = keyValuePairs.join('&');
    // 生成签名
    let sig = encodeURIComponent(
        CryptoJS.HmacSHA1(paramsString, import.meta.env.VITE_APP_XINZHI_WEATHER_PRIVATE_KEY)
            .toString(CryptoJS.enc.Base64));
    paramsString += '&sig=' + sig;

    return paramsString;
}

测试请求

结合封装好的 jsonp,就可以进行请求测试了:

function searchLocation(searchText) {
    return new Promise((resolve, reject) => {
        let searchLocationUrl = 'https://api.seniverse.com/v3/location/search.json';
        let callbackName = 'jsonpCallback_searchLocation';
        let params = {
            q: searchText,
            public_key: import.meta.env.VITE_APP_XINZHI_WEATHER_PUBLIC_KEY,
            ts: Date.now(),
            ttl: 300,
            callback: callbackName,
        };
        let paramsString = generateRequestParamsString(params);
        searchLocationUrl += '?' + paramsString;
        jsonpRequest(searchLocationUrl, callbackName)
            .then(data => {
                if (data.results.length > 0) {
                    resolve(data.results);
                } else {
                    reject(data);
                }
            })
            .catch(error => {
                reject(error);
            });
    })
}

上述代码除了公钥和私钥需要自行替换外,可以直接使用,一共三个接口 jsonpRequestgenerateRequestParamsStringsearchLocation。搜索地址的接口也可以根据实际情况修改为其他业务,比如查询当前的天气,获取未来的天气等等。