这是一个对心知天气 API 前端 Jsonp 请求进行封装的组件。

近期在写一个天气类站点 Demo 时,为了实现轻量化,必须选用支持 Jsonp 格式的 API 服务商。经过一番查找,最终选择心知天气作为数据源。实际开发中得知,若调用心知天气的 API,需要按照指定流程对请求进行签名。Demo 制作完成后,特意将其中请求签名部分做了重写,并制成单独的组件,方便今后开发中进行调用。本文是对这一 Jsonp 请求封装组件的说明文档。

项目信息

项目地址:https://github.com/seanhuai/seniverse-jsonp

关于 Jsonp

Jsonp (Json with padding) 是一种非正式的 Json 数据处理格式,主要用于经前端请求获取并处理跨域内容。

  // 声明回调函数,用于接收数据
  function success(json){
    // 处理 json 数据的功能代码
  }

  // 发送的请求需包含 callback 参数,形如:
  'https://api.example.com/v1/?...&callback=success'

  // jsonp 请求返回值,直接调起回调函数处理数据
  success(json); // 参数 json 指返回的 json 数据

Jsonp 将原本直接返回的 Json 数据作为参数,添加在指定的回调函数中返回。这一做法可以回避同源策略和 JavaScript 的安全性检查。

  <script>
    function success(json){
      // 处理 json 数据的功能代码
    }
  </script>
  <!-- 
    以下 script 标签的加载相当于执行了一次 src 目标地址的 GET 请求
    返回值为 success(json),返回后作为函数自动执行
  -->
  <script src="https://api.example.com/v1/?...&callback=success"></script>

一般在使用 Jsonp 时,通过 script 标签完成。标签的 src 属性值为请求的目标地址,当标签加载时,相当于执行了一次对目标地址的 GET 请求。

由于返回值是函数,在返回之后自动执行。

组织流程

请求签名

按照心知天气提供的签名算法,依次完成。

构造验证字符串

将请求参数按照参数名字典升序排列后,把所有参数param=value 用&连接起来,类似 URI 中 Query string 的构造方式。目前支持的参数有:UNIX 时间戳 ts 签名失效时间ttl(单位为秒,缺省为 1800,可选)和用户 ID uid。例:ts=1443079775&ttl=30&uid=U123456789

ts 参数即 UNIX 时间戳,指从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。需要注意:通过 JavaScript Date 对象处理得到的时间戳单位为“毫秒”,在发起请求前,需要处理为单位为“秒”的数据。

  // 获取到当前毫秒时间戳,向下取整得到秒时间戳
  let ctime = Math.floor((new Date().getTime())/1000);

由于构造验证字符串中仅支持 ts/ttl/uid 三个参数,所以最多情况下参数按 ts-ttl-uid 顺序组织即可。其中 ttl 参数为可选,即这些参数也可以按 ts-uid 顺序组织。

  let string1 = 'ts=1443079775&ttl=30&uid=U123456789';
  let string2 = 'ts=1443079775&uid=U123456789';

使用HMAC-SHA1方式,以API密钥(key)对验证字符串进行加密

常见程序语言通常会内置加密函数,或通过扩展库提供支持。例如在 NodeJS 中,您可以使用 crypto 模块中的中的 createHmac 函数,例:crypto.createHmac("sha1", key)。

在 JavaScript 中,没有用于加密的原生算法模块支持,需要使用第三方模块进行加密操作。本项目使用的加密模块是 Crypto-JS 3.1.9 版本。其原作者为 brix

Crypto-JS 是 NodeJs 中 Crypto 模块的原生实现。在本项目中,我们使用这一模块执行 HMAC-SHA1 加密和 Base64 编码。

  // 调用 HmacSHA1() 方法,以用户 API 密钥(key)为盐,
  // 对验证字符串 string 进行加盐加密
  let sha1 = CryptoJS.HmacSHA1(string,key);

加盐加密,通常是在需要散列的字段的特定位置增加特定的字符,打乱原始的字符串,使其生成的散列结果产生变化。简单的说,加盐就是按一定规则修改加密的字串,使其加密后的结果更不易于被破解。

将加密结果用 Base64 编码,并做 urlencode,得到签名 sig

例:假设 key 为"secret",步骤(1)中的参数例子加密后得到的结果应为为 dTYeoN8WdOfW4PiwgEdLa0gWFzo=,做完 urlencode 最终得到的签名 sig 为 dTYeoN8WdOfW4PiwgEdLa0gWFzo%3d

将上一步计算结果使用 Base64 编码,并转为 URI 字符串即可。

  // toString() 方法可以传参指定编码格式,如本例为将 sha1 按 Base64 编码转换
  // encodeURI()/encodeURIComponent() 方法可以将字符串转为 URI 格式。
  let sign = encodeURIComponent(sha1.toString(CryptoJS.enc.Base64)); 

toString(radix) 方法除了可以实现基本的转换为字符串的功能外,还可以传参(radix)指定编码格式,如传入 2-36 的数字,则按照对应进制进行转换,默认情况下,参数 radix 值为 10,即按照十进制转换。本例是使用其将 sha1 按 Base64 编码转换。

JavaScript 原生的 encodeURI()/encodeURIComponent() 方法可以将字符串转为 URI 格式。

需要注意的是,encodeURI() 方法不会将 ";/?:@&=+$,#" 这些特殊内容进行转换。本例采用 encodeURIComponent() 方法。

将得到的签名sig附在验证字符串后,作为一个请求参数

上述例子里,请求参数即为ts=1443079775&ttl=30&uid=U123456789&sig=dTYeoN8WdOfW4PiwgEdLa0gWFzo%3d

最终,将上一步得出的 sign 值作为签名(sig)参数,附加在验证字符串后,使用新的请求字串发送请求即可。

组合请求

根据不同 API 所需参数内容不同,组合构成不同的请求,其中必用的参数有 location/ts/uid/sig/callback。关于心知天气 API,请参考官方文档:心知天气-天气数据 API 文档

  const signed = sign(); // 返回已添加签名信息的验证字符串
  let url = `location=${location}&unit=${unit}&language=${language}&${signed}&callback=${callback}`

location 支持多种格式,如城市 ID、城市中文名、城市英文名、城市名拼音、省市名等,另外支持 IP 地址和经纬度坐标。

callback 值为自定义的回调函数名。

发送请求

在 JavaScript 中新建一个 script 标签,并设置 src 属性为先前的请求地址,追加到页面结构中即可。

  let tag = document.createElement('script');
  tag.src = url; // url 即已经合成的完整请求字符串
  document.body.appendChild(tag);

页面加载后,继续加载已生成的 script 标签,等同于发送一次 GET请求,即可获取内容。

扩展使用

除使用给定信息的查询外,还可以使用 JavaScript Geolocation API 获取用户地理位置信息,使用实时定位地点发起请求。如使用地理定位获取坐标,调用分钟级降水预报 API,即可实现实时降水预报。

  // 经由 navigator 实现 Geolocation 对象的获取位置方法
  navigator.geolocation.getCurrentPosition(success);
  // success 是获取地理位置成功后的回调函数
  function success(position){ // position 即返回的 Position 对象 
    position.coords // Position 对象内置的坐标属性
    position.coords.latitude // 纬度属性
    position.coords.longitude // 经度属性
  }

需要注意,Geolocation API 需要依赖谷歌服务,如谷歌服务访问受到限制,则会造成用户体验的下降。